feat: 增加VTM要素渲染解析
This commit is contained in:
parent
8514dc3c60
commit
b1a6f2ae38
@ -16,6 +16,7 @@
|
||||
## VTM引擎
|
||||
|
||||
* [VTM引擎使用指南](VTM引擎使用指南.md)
|
||||
* [VTM要素渲染流程解析](VTM要素渲染流程解析.md)
|
||||
|
||||
## xviz
|
||||
|
||||
|
@ -82,7 +82,7 @@ Spatialite支持所有的sqlite语句的执行,事实上,Spatialite是在sql
|
||||
|
||||
#### 2.1.1 查看Geometry的bolb对象,使用AsText
|
||||
|
||||
* AsText函数也许是Spatialite中使用最频繁的函数之一,多数情况下,我们会将数据的位置信息以Bolb格式(数据对象)保存,并且为了日后可以按照空间关系去查询数据,还会以该字段建立空间索引。而查询后的数据又该如何使用呢?就是通过AsText函数将Bolb对象转换为WKT文本,然后在应用层再将该WKT文本重新转换为空间对象(Java中使用jts.jar中提供的函数实现转换)。
|
||||
* AsText函数也许是Spatialite中使用最频繁的函数之一,多数情况下,我们会将数据的位置信息以Bolb格式(数据对象)保存,并且为了日后可以按照空间关系去查询数据,还会以该字段建立空间索引。而查询后的数据又该如何使用呢?就是通过AsText函数将Blob对象转换为WKT文本,然后在应用层再将该WKT文本重新转换为空间对象(Java中使用jts.jar中提供的函数实现转换)。
|
||||
|
||||
> ```sql
|
||||
> Select Name, Peoples, AsText(Geometry) from Towns order by Peoples Desc
|
||||
|
234
gis/VTM要素渲染流程解析.md
Normal file
234
gis/VTM要素渲染流程解析.md
Normal file
@ -0,0 +1,234 @@
|
||||
# VTM要素渲染流程解析
|
||||
|
||||
> 先看整体流程图
|
||||

|
||||
|
||||
接下来我们以一个交限的渲染流程为例,讲解使用VTM引擎如何渲染显示想要的数据。
|
||||
|
||||
### 数据导入
|
||||
1. 获取到的OMDB数据源是一个zip包文件,zip包内包含了所有经过上游程序转换来的数据,每一个数据要素都是一个单独的文件,而每个文件内一条数据都是单独的一个json。
|
||||
zip文件
|
||||
|
||||
对应的交限文件的数据格式如下所示:
|
||||
|
||||
```json
|
||||
{"angle":154.77928694048447,"flag":0,"geometry":"POINT(116.32239664126244 40.033977905512685 0)","inDirect":2,"linkIn":"84208764957432729","linkOut":"84208515299875817","mesh":"20596467","outDirect":1,"restrictionPid":"109677","sequenceNo":""}
|
||||
```
|
||||
|
||||
2. 该数据在下载完成后,App端会开始导入数据流程。默认情况下,上述数据会按照k-v形式存储在Realm的RenderEntity类中的properties中,而如果因为具体的渲染或业务要求,需要对数据做一定的处理,可以在数据导入阶段进行预处理。
|
||||
1. 首先关注App端的图层控制配置文件,这个文件配置了当前App可以捕捉、显示的数据类型,以及数据导入的预处理流程,具体的配置如下图。
|
||||

|
||||
- 4006是该要素的分类Code
|
||||
- table是要素对应的表名,需要与图1的文件名保持一致
|
||||
- code码与前述4006保持一致,name为要素名称
|
||||
- transformer为数据导入前的转换函数,对应数据为数组
|
||||
- k为需要被处理的key
|
||||
- v为需要被处理的value,该配置支持或“|”语法,可以配置对应的多个value
|
||||
- klib为转换后的key
|
||||
- vlib为转换后的value
|
||||
**注意: 数据处理后k-v以及klib-vlib都会保留在数据中**
|
||||
如果数据转换需要较为复杂的逻辑运算或处理,可以通过对应的函数来处理,只需要配置对应的方法名,以"()"结尾,程序会通过反射找到ImportPreProcess类下对应的方法。如上图所示,普通交限调用了generateRestrictionRerference方法来处理。
|
||||
2. 接下来进入对应的数据处理方法中。
|
||||
|
||||
```kotlin {.line-numbers}
|
||||
/**
|
||||
* 自动生成普通交限的参考数据
|
||||
* */
|
||||
fun generateRestrictionRerference(renderEntity: RenderEntity) {
|
||||
// 获取当前renderEntity的geometry
|
||||
val geometry = renderEntity.wkt
|
||||
var radian = 0.0 // geometry的角度,如果是点,获取angle,如果是线,获取最后两个点的方向
|
||||
var point = Coordinate(geometry?.coordinate)
|
||||
if (Geometry.TYPENAME_POINT == geometry?.geometryType) {
|
||||
var angle = if(renderEntity?.properties?.get("angle") == null) 0.0 else renderEntity?.properties?.get("angle")?.toDouble()!!
|
||||
radian = Math.toRadians(angle)
|
||||
}
|
||||
|
||||
// 计算偏移距离
|
||||
val dx: Double = GeometryTools.convertDistanceToDegree(3.0, geometry?.coordinate?.y!!) * Math.sin(radian)
|
||||
val dy: Double = GeometryTools.convertDistanceToDegree(3.0, geometry?.coordinate?.y!!) * Math.cos(radian)
|
||||
|
||||
// 计算偏移后的点
|
||||
val pointTranS =
|
||||
GeoPoint(point.getY() - dx, point.getX() + dy) // 向右偏移的点
|
||||
|
||||
// 计算与原有方向平行的终点坐标
|
||||
val pointTranE = GeoPoint(pointTranS.latitude + dy, pointTranS.longitude + dx)
|
||||
|
||||
renderEntity.geometry = GeometryTools.createGeometry(pointTranS).toString()
|
||||
|
||||
// 将这个点记录在数据中
|
||||
val startEndReference = ReferenceEntity()
|
||||
startEndReference.renderEntityId = renderEntity.id
|
||||
startEndReference.name = "普通交限参考线"
|
||||
startEndReference.table = renderEntity.table
|
||||
// 起终点坐标组成的线
|
||||
startEndReference.geometry = GeometryTools.createLineString(listOf(GeoPoint(point.y, point.x), pointTranS)).toString()
|
||||
startEndReference.properties["qi_table"] = renderEntity.table
|
||||
startEndReference.properties["type"] = "s_2_e"
|
||||
Realm.getDefaultInstance().insert(startEndReference)
|
||||
|
||||
val angleReference = ReferenceEntity()
|
||||
angleReference.renderEntityId = renderEntity.id
|
||||
angleReference.name = "普通交限参考方向"
|
||||
angleReference.table = renderEntity.table
|
||||
// 与原有方向指向平行的线
|
||||
angleReference.geometry = GeometryTools.createLineString(listOf(pointTranS, pointTranE)).toString()
|
||||
angleReference.properties["qi_table"] = renderEntity.table
|
||||
angleReference.properties["type"] = "angle"
|
||||
Realm.getDefaultInstance().insert(angleReference)
|
||||
}
|
||||
```
|
||||
如上所述,程序在对数据进行处理,并将对应字段保存在RenderEntity中时,还会保存对应的参考数据ReferenceEntity,参考数据中会记录RenderEntity的Id,在渲染时会自动查询这两类数据。
|
||||
|
||||
|
||||
### 数据渲染
|
||||
3. 数据导入完成后,开始进入读取和渲染环节,在VTM中,数据都是通过图层来渲染的,图层主要包括VectorLayer(普通矢量图层,用于绘制指定的矢量数据,主要在数据高亮阶段使用)、VectorBitmapTileLayer(栅格瓦片图层)和VectorTileLayer(矢量瓦片图层),而对于包含大量数据且希望方便地根据地图等级、渲染样式对图层数据进行管理,一般采用VectorTileLayer来显示。
|
||||
每个VectorTileLayer(事实上是TileLayer,因此BitmapLayer的流程也是一致的)都对应一个VectorTileSource,用以管理每个图层对应的数据源,这个数据源可以是本地的.map文件,可以是本地的数据库,或者对应服务的指定接口。
|
||||
每个VectorTileSource对应指定的DataSource,DataSource中的query方法是对每个tile获取对应数据的查询方法,具体代码如下(OMDBTileDataSource.java):
|
||||
|
||||
```java {.line-numbers}
|
||||
public void query(MapTile tile, ITileDataSink mapDataSink) {
|
||||
// 获取tile对应的坐标范围
|
||||
if (tile.zoomLevel >= Constant.OMDB_MIN_ZOOM && tile.zoomLevel <= Constant.OVER_ZOOM) {
|
||||
int m = Constant.OVER_ZOOM - tile.zoomLevel;
|
||||
int xStart = (int) tile.tileX << m;
|
||||
int xEnd = (int) ((tile.tileX + 1) << m);
|
||||
int yStart = (int) tile.tileY << m;
|
||||
int yEnd = (int) ((tile.tileY + 1) << m);
|
||||
|
||||
RealmQuery<RenderEntity> realmQuery = Realm.getDefaultInstance().where(RenderEntity.class).rawPredicate("tileX>=" + xStart + " and tileX<=" + xEnd + " and tileY>=" + yStart + " and tileY<=" + yEnd);
|
||||
// 筛选不显示的数据
|
||||
if (Constant.HAD_LAYER_INVISIABLE_ARRAY != null && Constant.HAD_LAYER_INVISIABLE_ARRAY.length > 0) {
|
||||
realmQuery.beginGroup();
|
||||
for (String type : Constant.HAD_LAYER_INVISIABLE_ARRAY) {
|
||||
realmQuery.notEqualTo("table", type);
|
||||
}
|
||||
realmQuery.endGroup();
|
||||
}
|
||||
List<RenderEntity> listResult = realmQuery/*.distinct("id")*/.findAll();
|
||||
if (!listResult.isEmpty()) {
|
||||
mThreadLocalDecoders.get().decode(tile, mapDataSink, listResult);
|
||||
}
|
||||
mapDataSink.completed(QueryResult.SUCCESS);
|
||||
|
||||
} else {
|
||||
mapDataSink.completed(QueryResult.SUCCESS);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
4. 如上面代码所示,query查询结束后,会调用对应的OMDBDataDecoder.decode解析指定数据,解析结束后,会将对应的数据全部转换为统一的MapElement对象,MapElement就是渲染引擎渲染数据的输入源类型,此后开始针对不同的MapElement调用对应的解析方法。
|
||||
5. 接下来调用VectorTileLoader的process方法,如下面代码所示:
|
||||
|
||||
```java {.line-numbers}
|
||||
public void process(MapElement element) {
|
||||
if (isCanceled() || !mTile.state(LOADING))
|
||||
return;
|
||||
|
||||
if (mTileLayer.callProcessHooks(mTile, mBuckets, element))
|
||||
return;
|
||||
|
||||
TagSet tags = filterTags(element.tags);
|
||||
if (tags == null)
|
||||
return;
|
||||
|
||||
mElement = element;
|
||||
|
||||
/* get and apply render instructions */
|
||||
if (element.type == GeometryType.POINT) {
|
||||
renderNode(renderTheme.matchElement(element.type, tags, mTile.zoomLevel));
|
||||
} else {
|
||||
mCurBucket = getValidLayer(element.layer) * renderTheme.getLevels() * (element.level > 0 ? element.level : 1);
|
||||
renderWay(renderTheme.matchElement(element.type, tags, mTile.zoomLevel));
|
||||
}
|
||||
clearState();
|
||||
}
|
||||
```
|
||||
注意上面的renderTheme.matchElement方法,这里会使用初始化地图阶段setTheme设置的Theme规则匹配当前的element要素。
|
||||

|
||||
匹配到的要素会返回对应的RenderStyle数组,每个renderStyle会对应不同的渲染。
|
||||
|
||||
那么Theme从哪里来的呢?
|
||||
当前我们是通过map.setTheme方法指定对应的theme文件,将地图上所有的layer都设置为对应文件的theme规则。查VectorTileLoader的上下文,renderTheme对象正是来自于当前layer对应的theme,也就是地图上setTheme方法设置到图层的Theme。而对theme的解析就是在map.setTheme方法调用时执行的。
|
||||
|
||||
|
||||
6. 对于被匹配到的规则,随后会调用style的renderNode或renderWay方法,开始渲染指定marker或线、面数据。
|
||||
|
||||
|
||||
### 点限速的样式配置
|
||||
|
||||
接下来以点限速为例,讲解样式文件xml的具体配置和注意问题。
|
||||
|
||||
1. 首先依然以获取到的OMDB数据为例,如下所示,OMDB数据在转换导入到Realm中后,json中的所有字段都会以k-v形式存储在RenderEntity的properties中,而properties将会是渲染引擎用来匹配样式规则的数据依据。
|
||||
|
||||
```json {.line-numbers}
|
||||
{
|
||||
"angle": 154.77928694048447,
|
||||
"flag": 0,
|
||||
"geometry": "POINT(116.32239664126244 40.033977905512685 0)",
|
||||
"inDirect": 2,
|
||||
"linkIn": "84208764957432729",
|
||||
"linkOut": "84208515299875817",
|
||||
"mesh": "20596467",
|
||||
"outDirect": 1,
|
||||
"restrictionPid": "109677",
|
||||
"sequenceNo": ""
|
||||
}
|
||||
```
|
||||
|
||||
同样因为需要渲染方向箭头以及原始坐标和显示坐标的连接线,这条数据还会在辅助渲染表中生成两条辅助渲染数据,1条用来渲染起终点的线,1条用来渲染方向图标
|
||||
|
||||
```json {.line-numbers}
|
||||
{
|
||||
"geometry": "LINESTRING(116.32239664126244 40.033977905512685 0, 116.32239544126244 40.033966905512685 0)",
|
||||
"qi_table": "OMDB_RESTRICTION",
|
||||
"type": "s_2_e" // 起终点
|
||||
}
|
||||
|
||||
{
|
||||
"geometry": "LINESTRING(116.32239664126244 40.033977905512685 0, 116.32239544126244 40.033966905512685 0)",
|
||||
"qi_table": "OMDB_RESTRICTION",
|
||||
"type": "angle" // 角度
|
||||
}
|
||||
```
|
||||
2. 需要说明的是,在VTM中,点要素的marker绘制和线、面的marker不尽相同,点要素的marker在修改地图旋转角度、改变地图倾斜角时,不跟随地图旋转或倾斜,而线上的marker则会跟随地图旋转及倾斜。
|
||||
因此在渲染方向图标时,程序在预处理环节根据数据给出的角度angle计算出一个与显示坐标成angle角度,距离为5米的终点,形成一条line,作为显示方向图标的依据。否则如果直接使用源数据给出的点坐标和angle,会导致方向图标永远保持不变,在旋转地图时方向错误的问题。
|
||||
3. 接下来进入到theme的xml配置中,查看普通交限的xml配置:
|
||||
|
||||
```xml {.line-numbers}
|
||||
<!--普通交限-->
|
||||
<m k="qi_table">
|
||||
<m v="OMDB_RESTRICTION">
|
||||
<m k="angle">
|
||||
<symbol src="assets:omdb/icon_4006_0.svg" repeat="false" symbol-width="46" symbol-height="46" ></symbol>
|
||||
</m>
|
||||
<m k="type" v="angle">
|
||||
<symbol src="assets:omdb/icon_arrow_right.svg" repeat-start="0" repeat-gap="2000" symbol-width="46" symbol-height="46" repeat="false" rotate="true"></symbol>
|
||||
</m>
|
||||
<m k="type" v="s_2_e">
|
||||
<line stroke="#14582c" width="0.1" dasharray="1,1" repeat-gap="3" repeat-start="0"/>
|
||||
</m>
|
||||
</m>
|
||||
</m>
|
||||
```
|
||||
如上所示,当数据中的key包含qi_table,且value是OMDB_RESTRICTION,都会适配到这条规则,因此上面三条数据都会被适配。
|
||||
|
||||
> **注意:当k和v分别被定义在两个m标签时,即使有父子关系,数据匹配上并不会将这套k-v关联为一个组合,而是在数据中分别匹配是否存在对应的k和v**
|
||||
|
||||
接着继续寻找包含key是angle的数据,这样只有主显示表的数据才能匹配到,而下面的k="type" v="angle"则匹配到了参考表中用来绘制方向箭头的数据,最下面的k="type" v="s_2_e"则匹配到了绘制起终点的辅助表中的数据。
|
||||
|
||||
然后分析主显示表配置的symbol标签,主显示表geometry是一个点类型的数据,src指定了在这个点上显示的图片资源,repeat指定了该图标是否会重复绘制(点要素类型默认为false),symbol-width和symbol-height指定了图标的像素大小(不指定的话默认为20个像素)。
|
||||
|
||||
第二个symbol标签描述了方向箭头的绘制方式,辅助表中该方向箭头的geometry是一条线。repeat-start指定了在线上渲染图标的起始位置(不设置默认是在30%长度后开始渲染),repeat-gap为线上图标的间隔距离,这里设置一个超大的值,则只会渲染1个图标。rotate指定了线上的图标是否要根据线的方向旋转,这个属性在道路方向箭头的绘制上十分有用,多数情况下在线上的图标都应该随线的方向旋转对应的方向(默认向右的图标为角度为0的图标)。
|
||||
|
||||
最后的line标签描述了起终点的线的绘制方式。stroke指定了线的主色,width为stroke的宽度,dasharray指定线段的长度渲染方式,它的值是一个由逗号分割的最多4个int类型的字符串,指定了虚线中线段和间隔部分的相对长度对比。
|
||||
|
||||
更多的xml配置,还需要参考官方default.xml以及rendertheme.xsd的类型限制和说明来完成。
|
||||
|
||||
## 后续问题 TODOList
|
||||
|
||||
目前使用vtm绘制点、线、面的功能可以基本完成大部分的业务需求,然而针对一些特殊渲染要求,以及后续的3D化场景需求,也要求我们可以对VTM引擎做到按需修改。这里整理了目前业务上要求以及后续的修改项:
|
||||
1. 业务上需要对已有经纬度做定向的偏移,目前是通过数据预处理实现,后续可考虑修改引擎实现配置xml自动偏移的功能。
|
||||
2. 面要素纹理贴图,目前引擎支持指定src实现贴图,但是图片大小不会随面的大小而改变,导致地图放大后图片出现重复贴图的情况,后续需解决贴图与面大小一致的问题。
|
||||
3. 3D化渲染,目前引擎在MapElement读取geometry时忽略了Z轴数据,导致后续渲染环节均是在2D上完成,3D building是根据osm数据规格,在2D面上增加楼层信息后对面进行拔高。后续实现全3D化渲染,首先需要解决MapElement读取Z数据的问题,之后对OpenGL ES源码做一定修改。
|
@ -2,3 +2,4 @@
|
||||
* [Spatialite教程](Spatialite教程.md)
|
||||
* [Spatialite-Cookbook](Spatialite-Cookbook.md)
|
||||
* [VTM引擎使用指南](VTM引擎使用指南.md)
|
||||
* [VTM要素渲染流程解析](VTM要素渲染流程解析.md)
|
||||
|
Loading…
x
Reference in New Issue
Block a user