OpenTCS学习笔记(一)——为模型中的元素添加新的属性(本文持续更新中)
openTCS是一个用于自动车辆的控制系统/车队管理软件,主要用于协调AGV任务,软件是java开发。在进行建模过程中,我们会根据自己的需要,为模型元素增加新的属性,并使其可视化。不过官方文档对于如何为模型元素添加新属性并没有讲解,所以我就阅读了源码,下面讲讲基本操作:
OpenTCS源码下载地址:http://www.opentcs.org/en/download.html
我下载的是4.18.0版本
数据结构
先来谈谈模型中元素的数据结构,下面以点(Point)为例,其他元素类似。我们需要用到的数据结构如下:(加上了我的一些个人理解,还在学习,不一定正确哈)
- Point ,点作为一种资源的存储结构,在Kernel中访问点的时候,此类结构会被调用;
- PointModel,一般与上述的Point结构绑定,用于创建点在PlantOverView中的模型实例;
- PointAdapter ,将Point与PointModel进行绑定,个人理解是将PlantOverView中建立的模型实例(PointModel)与Kernel中的点资源进行同步,保证用户在可视化界面看到的模型数据与内核读取到的数据是一样的;
- PointTO,点在转换过程中的中间产物结构,它的功能(我暂时了解到的)仅为:在向XML文件读取模型或者将模型写入文件时,作为转换的中间产物 ;
- PointCreationTO,如果要将点写入XML文件,我们需要先将它从PointModel转化为PointCreationPO,再转化为PointTO。
实现流程
下面以添加点的z坐标为例,强烈建议在修改源代码之前先备份
一、修改Kernel中的点结构
Kernel中的点结构也就是Point,我们需要在Point类中,添加我们想要的新成员,此处我们添加z轴坐标,并添加相对应的构造方法和以及对z轴坐标的操作方法(获取、修改)
不过巧合的是,OpenTCS本身的Point类是保留了z轴坐标的,所以在这里不需要对Point类进行改动,如果是其他新成员则要注意添加对应的操作方法和构造方法,此外还需要修改其clone()方法,比如将clone()方法里调用的构造方法换成包含有新成员的构造方法(这一点很重要),至于为什么重要?我们下面再具体讲。
二、修改PlantOverView中的点结构
也就是修改建模工厂总的点模型,这里面就涉及比较多的类了
1.修改PointModel
PointModel是点在建模工厂中的主要表现结构,我们在PlantOverView中看到的一个点的信息,都是来自于PointModel
修改步骤:在点的模型中我们不需要添加成员,因为Model类中一般直接调用了属性创建函数,如下:
public PointModel() {
createProperties();
}
所以我们只需要创建一个子方法(用来创建我们需要的属性),再在属性创建函数中调用这个子方法就行了,我这里直接参照了X,Y坐标的子方法,比如:
子方法:
public CoordinateProperty getPropertyModelPositionZ() {
return (CoordinateProperty) getProperty(MODEL_Z_POSITION);
}
属性创建方法中:
CoordinateProperty pPosZ = new CoordinateProperty(this,
DEFAULT_XY_POSITION,
LengthProperty.Unit.MM);
pPosZ.setDescription(bundle.getString("pointModel.property_modelPositionZ.description"));
pPosZ.setHelptext(bundle.getString("pointModel.property_modelPositionZ.helptext"));
setProperty(MODEL_Z_POSITION, pPosZ);
首先,这里面多了一个新的变量MODEL_Z_POSITION,它其实就是该属性的全局唯一名字,可以理解为该属性的id,但要注意该名字并不是属性显示在可视化界面的名字;我参照X,Y坐标在PositionableModelComponent.java文件中添加了
/**
* Key for the Z (model) cordinate.
*/
public static final String MODEL_Z_POSITION = "modelZPosition";
}
如果是其他的属性,可以根据属性的特性放在对应的文件中,例如:如果是Point特有,则直接在PointModel中添加该字符串即可;但一定要注意该字符串不能与其他已有的属性id重复。
此外,我们可以看到这个坐标属性还创建了该属性对应的描述和帮助文档。这个其实是PlantOverView中,我们鼠标停放在属性栏上时,出现的文字描述以及该属性的名称,如下图:
我们可以在文件Bundle.properties中添加、修改属性的描述和帮助文档,如下:
locationModel.property_modelPositionZ.description=z-Position (model)
locationModel.property_modelPositionZ.helptext=The z coordinate of the location in the kernel model
这一部分就修改完了
2.修改PointAdapter
修改适配器,因为PointModel类是与Point类保持同步的,我们在Point中增加了新的成员,在PointModel增加了新的属性创建子方法,接下来要做到就是把这个属性创建子方法对应到增加的新成员上,保证两个类是同步的
同步的实现主要是靠updateModelProperties方法和storeToPlantModel方法,前者是将Point里的信息同步到PointModel,后者是将PointModel信息同步到Point里,我们需要做的就是将Point里面的成员对应到PointModel的属性创建子方法,因此参照里面原有的代码,将Point里的新成员与PointModel的属性创建子方法对应起来,如下:
updateModelProperties方法新增:
model.getPropertyModelPositionZ().setValueAndUnit(point.getPosition().getZ(),
LengthProperty.Unit.MM);
storeToPlantModel方法不太一样,它调用了其他子方法来获取PointModel中的信息以同步到Point中,因此我们不仅要修改storeToPlantModel方法,还要增加或者修改其调用的子方法;
由于我只是要新增z坐标,原本的storeToPlantModel方法只是同步了X和Y坐标,因此我需要先修改子方法getKernelCoordinates,如下:
getKernelCoordinates方法:
private Triple getKernelCoordinates(PointModel model) {
return convertToTriple(model.getPropertyModelPositionX(),
model.getPropertyModelPositionY(),
model.getPropertyModelPositionZ());
}
然后在storeToPlantModel方法中,我这个例子基本不用修改,不过如果是其他的新属性,则要考虑修改;
此外,注意PointAdapter中还有一个updateModelLayoutProperties方法,顾名思义,这是用于同步模型的布局(layout)属性的,布局属性也就是一些可视化的属性,比如点在布局中的坐标(区别于点的模型坐标)
3.读取和写入模型文件(XML文件)
进行了上述操作后,我们已经在Kernel的主要点结构Point和PlantOverView的主要点结构PointModel添加了新的属性,并修改了他们的同步方法,之后我们需要考虑的问题就是:
- 如何保证在保存该模型到模型文件时,OpenTCS有将我们创建的新属性保存进去
- 如何保证我们从模型文件中读取模型时,OpenTCS有将我们创建的新属性读取进来
为了解决上述问题,我们需要先了解一下OpenTCS读取和写入模型文件的流程
读取XML文件:
- 从xml文件中读取内容并转化为V002PlantModelTO,具体实现:ModelParser.java, V002PlantModelTO.java
- 从中提取出各种*TO实例,例如本例子中的PointTO,具体实现:V002PlantModelTO.java
- 将提取出来的PointTO实例转化为PointCreationTO实例;整合到PlantModelCreationTO,具体实现:V002TOMapper.java
- 将PlantModelCreationTO中的PointCreationTO实例转化为PointModel实例,并整合到SystemModel,具体实现:ModelImportAdapter.java, PlantModelElementConverter.java
- SystemModel中各实例的信息就被同步到Kernel中,例如将PointModel信息同步到Point,具体实现:OpenTCSModelManager.java中的persistModel方法和ModelKernelPersistor.java中的persist方法
读取过程调用接口的大致顺序为:(*表示要修改的模型元素,例如Point)
OpenTCSModelManager.loadModel()—> UnifiedModelReader.deserialize()—> ModelParser.readModel()—> V002TOMapper.map()和to*CreationTO()—> ModelImportAdapter.convert()和import*s()—> PlantModelElementConverter.import*()
写入XML文件
其实就是上面的过程反着来
- Kernel中Point信息发生改变后,通过PointAdapter同步到其对应的PointModel中,具体实现:PointAdapter.java中的updateModelProperties方法
- 通过PointAdapter 将PointModel转换为PointCreationTO,此处虽然实现方法在ModelExportAdapter.java中,但它实际调用的方法是各种PointAdapter的storeToPlantModel方法(这个我们之前改过了)
- 将PointCreationTO转化为PointTO实例,整合到V002PlantModelTO,具体实现:ModelParser.java, V002TOMapper.java
- 调用V002PlantModelTO的toXml方法将模型写入模型文件,具体实现:V002PlantModelTO.java
写入过程调用接口的大致顺序为:(*表示要修改的模型元素,例如Point)
OpenTCSModelManager.persistModel() —> ModelExportAdapter.convert()和persist()—> *Adapter.storeToPlantModel()—>UnifiedModelPersistor.serialize()和writeFile()
—>ModelParser.writeModel()
—>V002TOMapper.map()和to*TO()
除了上面提到的那些文件,我们还需要修改PointTO和PointCreationTO(如果是其他的模型元素,则修改对应的*TO和*CreationTO)
PointTO:需要增加新成员和相应方法
此例中需要增加成员zPosition
public class PointTO
extends PlantModelElementTO {
private Long xPosition = 0L;
private Long yPosition = 0L;
private Long zPosition = 0L;
并增加getzPosition, setzPostion方法
@XmlAttribute
public Long getzPosition() {
return zPosition;
}
//modified by Henry
public PointTO setzPosition(@Nonnull Long zPosition) {
requireNonNull(zPosition, "zPosition");
this.zPosition = zPosition;
return this;
}
注意:方法前面要加上注释@XmlAttribute 这样才能让xml的marshaller、unmarshaller找到这个属性的调用方法
PointCreationTO中由于它获取的坐标信息已经是包括了z轴的,所以在这个例子中就不需要修改
然后,读取和写入模型文件中涉及到的其他java文件,已经在流程中给大家标出来了,建议大家都去看一下,我这里就不细讲具体的修改方法了,内容也不多,只要是里面需要调用新增属性的地方,都需要小改一下,下面附上我在这些文件中的修改:
V002TOMapper.java
- toPointCreationTO方法
result.add(
new PointCreationTO(point.getName())
.withPosition(new Triple(point.getxPosition(),
point.getyPosition(),
//modified by Henry
point.getzPosition()))
.withVehicleOrientationAngle(point.getVehicleOrientationAngle().doubleValue())
.withType(Point.Type.valueOf(point.getType()))
.withProperties(convertProperties(point.getProperties()))
);
- toPointTO方法
pointTO.setxPosition(point.getPosition().getX())
.setyPosition(point.getPosition().getY())
.setzPosition(point.getPosition().getZ())
PlantModelElementConverter.java
- importPoint方法
model.getPropertyModelPositionX().setValueAndUnit(pointTO.getPosition().getX(),
LengthProperty.Unit.MM);
model.getPropertyModelPositionY().setValueAndUnit(pointTO.getPosition().getY(),
LengthProperty.Unit.MM);
model.getPropertyModelPositionZ().setValueAndUnit(pointTO.getPosition().getZ(),
LengthProperty.Unit.MM);
补充:修改XML架构
在V002PlantModelTO.java中,我们可以看到OpenTCS读取和写入模型文件所使用的XML架构文件是:/org/opentcs/util/persistence/model-0.0.2.xsd
private static Schema createSchema()
throws SAXException {
URL schemaUrl = V002PlantModelTO.class.getResource("/org/opentcs/util/persistence/model-0.0.2.xsd");
SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
return schemaFactory.newSchema(schemaUrl);
}
因此,为了保证我们添加的新成员有被写入xml文件,我们需要改一下XML架构,打开model-0.0.2.xsd(建议先备份):
先找到元素Point的定义
<xsd:element name="point"
type="pointType"
minOccurs="0"
maxOccurs="unbounded"/>
可以看到point元素的数据类型为pointType,于是我们再找到pointType的定义
<xsd:complexType name="pointType">
...
...
...
<xsd:attribute name="id"
type="xsd:unsignedInt"/>
<xsd:attribute name="name"
type="xsd:string"
use="required"/>
<xsd:attribute name="xPosition"
type="xsd:long"
use="required"/>
<xsd:attribute name="yPosition"
type="xsd:long"
use="required"/>
<xsd:attribute name="zPosition"
type="xsd:long"/>
<xsd:attribute name="vehicleOrientationAngle"
type="xsd:float"/>
<xsd:attribute name="type"
type="pointTypeType"
use="required"/>
可以看到这里面已经有了zPosition这个属性,所以此例中不需要进行改动;但如果是添加其他的属性,则可以仿照已有属性将其添加到XML架构中。
补充:修改Location须知
在修改Location的时候一定要注意Location是与Link相联系的,同理,LocationModel与LinkModel相联系;不过并不存在LinkCreationTO,而是在LocationCreationTO里面加入了withLink方法,也就是通过输入LinkModel和LocationModel,将两者集成到LocaitonCreationTO,而这恰恰也是问题所在,很可能会发生这样的情况:
我们在LocationCreationTO里面加入了新成员,比如number,但是我们为了保险起见,就没有改原有的构造函数,而是新增一个包含number的构造函数
LocationCreationTO(.....,@Nonnull number){
...
this.number = number;
}
然后再添加了一个方法
withNumber(number){
return new LocationCreationTO(......,number);
}
上面这些都是添加新成员的常用手段,但是这种情况下要保证没有问题的话,必须保证withNumber方法在最后被调用,否则number的值就会被初始化为默认值,为什么呢?
因为在将LocationModel和LinkModel整合到LocationCreationTO中时,是肯定会用到withLink方法的,但是该方法中使用的构造函数并没有number形参,所以如果最后调用的不是withNumber而是其他的with**方法,那么number就会被初始化成默认值。
源代码中将会发生上述情况的地方在ModelExportAdapter的convert方法,相关代码如下:
timeBefore = System.currentTimeMillis();
for (LocationModel model : systemModel.getLocationModels()) {
plantModel = persist(model, systemModel, plantModel);
}
LOG.debug("Converting LocationModels took {} milliseconds.",
System.currentTimeMillis() - timeBefore);
timeBefore = System.currentTimeMillis();
for (LinkModel model : systemModel.getLinkModels()) {
plantModel = persist(model, systemModel, plantModel);
}
LOG.debug("Converting LinkModels took {} milliseconds.",
System.currentTimeMillis() - timeBefore);
可以看到这里先将LocationModel转为LocationCreationTO,再将LinkModel整合到里面去,所以就必然会在最后调用withLink;具体调用withLink的地方在LinkAdapter的mapLocation方法:
private LocationCreationTO mapLocation(LinkModel model, LocationCreationTO location) {
if (!Objects.equals(location.getName(), model.getLocation().getName())) {
return location;
}
return location.withLink(model.getPoint().getName(), getAllowedOperations(model));
这里提供一种解决思路:
- 直接从源头解决,新增一个包含新成员的构造函数之后,要把该类中所有的with*方法所调用的构造函数,都改成刚刚新增的构造方法;这样即使其他的with*方法在之后才被调用,也不用怕新成员的值被设为默认值了
三、Kernel与PlantOverView同步
接下来要做的就是保证Kernel中的Point与PlantOverView中的PointModel同步。而Kernel和PlantOverView是两个不同进程,如何保证两者之间的模型同步呢?OpenTCS通过两者之间的TCP连接发送数据以保证模型同步,而且OpenTCS还将这条同步线路封装为事件总线(Event bus),每次当模型里面的东西发生改变之后,就向事件总线提交这个新的模型,标注“这是一个模型更改事件”,这样就能将新的模型同步到PlantOverView中,保证两个进程之间的模型同步。
因此,我们可以看到Kernel中提交“模型更改事件”的代码:
//在更改了模型里的Point之后,得到newPoint,向事件总线提交这个新的Point
objectPool.emitObjectEvent(newPoint.clone(), null, TCSObjectEvent.Type.OBJECT_CREATED);
可以看到这里在提交时,调用了新的Point的clone()方法,因此我们上面在修改Kernel中的Point结构时,就需要修改其clone()方法,如果没有修改的话,那么在PlantOverView就没办法接收到Kernel发给它的Point里面的新增成员。
PlantOverView接收来自于Kernel的新的Point之后,就使用PointAdapter里面的updateModelProperties方法,将其转换为PlantOverView想要的点结构。这个方法我们上面已经改过了,所以不用再改;这样就实现了Kernel里的模型同步到PlantOverView中。
而PlantOverView如果想要将模型同步给Kernel,就需要通过storeToPlantModel方法:从PointModel里面提取数据并存储为PointCreationTO,这里就比较疑惑了,明明是存储为PointCreationTO,怎么就能将其同步到Kernel里的Point呢?
是这样的,openTCS是先将PointModel转存为PointCreationTO,再将其同步到Kernel中的Point;至于为什么这么做,我觉得是为了方便快速吧,因为我们从XML文件中提取模型时,是先存为PointTO,转为PointCreationTO,这时就可以同时进行两条路径,既能将PointCreationTO转为PointModel,并在PlantOverView中显示出来,又能同时转为Kernel中的Point;两条路同时进行,就不需要先转为PointModel,再转为Point。
所以,我们要找到将PointCreationTO转为Point的方法实现,并进行修改,保证我们新增的属性有同步到Kernel里面;具体实现方法就是Model.java中的createPoint方法以及getPoints方法
// createPoint方法
@SuppressWarnings("deprecation")
public Point createPoint(PointCreationTO to)
throws ObjectExistsException {
// Get a unique ID for the new point and create an instance.
Point newPoint = new Point(to.getName())
.withPosition(to.getPosition())
.withType(to.getType())
.withVehicleOrientationAngle(to.getVehicleOrientationAngle())
.withProperties(to.getProperties());
objectPool.addObject(newPoint);
objectPool.emitObjectEvent(newPoint.clone(), null, TCSObjectEvent.Type.OBJECT_CREATED);
// Return the newly created point.
return newPoint;
}
// getPoints方法
private List<PointCreationTO> getPoints() {
Set<Point> points = objectPool.getObjects(Point.class);
List<PointCreationTO> result = new ArrayList<>();
for (Point curPoint : points) {
result.add(
new PointCreationTO(curPoint.getName())
.withPosition(curPoint.getPosition())
.withVehicleOrientationAngle(curPoint.getVehicleOrientationAngle())
.withType(curPoint.getType())
.withProperties(curPoint.getProperties())
);
}
return result;
}
这些方法里在进行同步的时候已经考虑了z轴坐标,所以我这个例子中不用修改;但如果是其他的属性,则要考虑用with*方法来进行修改。