作者:MR
紧接着上一篇,本篇介绍扩展资源的具体实现,即,处理输入得到输出的过程。
###一、准备
为了程序的健全性,需要做些别的工作,比如html表述、本地化的报错信息、日志记录等。这里简单分两点介绍下。
####1.1 初步检查参数是否合法
检查输入参数是否合法,首先当然是请求体是否合法,其次具体参数主要是四个/组参数的检查:mode字符串不能为空(后面运行算法时再得到有效mode进一步检查);tolerance不能空和小于0;pName(之后会进一步检查)和points必须至少一个有效;lName和lines必须至少一个有效。不清楚这些参数含义的请参考上一篇。
####1.2 本地化报错和日志
iServer使用com.supermap.services.util.ResourceManager
类管理本地化的报错信息;使用org.slf4j.cal10n.LocLogger
类设置和记录不同日志级别的日志输出。具体使用请参考iServer 6R帮助文档的说明,中英文词典文件可以自己定义,这里就直接用resource.DataRestResourceResource
里面的条目了(iServer-all-{版本}.jar//resource/DataRestResourceResource.properties
),所以tomcat窗口和日志记录的报错提示的是featureResults资源相关(我用的条目是这个)。
具体代码见工程,这里不再贴出。
###二、得到有效输入
首先需要得到每个输入对象(LCPInputFormat类型)有效mode,使用正则表达式即可,字符串中只保留PALDBU|这几个字符,没有有效mode则抛出没有有效mode的异常,runArithmetic方法如下:
@ Override
public LCPOutputFormat runArithmetic( @ SuppressWarnings( "rawtypes" ) Map atrithParamMapping ) throws HttpException
{
// 记个时
long start = System.currentTimeMillis( );
if ( atrithParamMapping.isEmpty( ) )
{
throwHttpException( DataRestResource.FEATURERESULTS_GETENTITY_ENTITYTOOBJ_FAIL, new Object[ 0 ],
Status.CLIENT_ERROR_BAD_REQUEST );
}
LCPInputFormat[ ] InPut = (LCPInputFormat[ ]) atrithParamMapping.get( PARAMS );
LCPOutputFormat OutPut = new LCPOutputFormat( );
// 输出一定和输入一样多
OutPut.results = new resultFormat[ InPut.length ];
try
{
for ( int i = 0; i < InPut.length; ++i )
{
LCPInputFormat IPT = InPut[ i ];
// 得到有效的mode
String vaildmode = "";
Matcher mcr = Pattern.compile( "P|A|L|D|B|U|\\|" ).matcher( IPT.mode );
while ( mcr.find( ) )
{
vaildmode += mcr.group( );
}
if ( vaildmode.length( ) > 0 && !"|".equals( vaildmode ) )
{
IPT.mode = vaildmode;
OutPut.results[ i ] = new resultFormat( );
modeRecognition( new LCPUtil( IPT, new DataUtil( this ) ), OutPut, i );
} else
{
throwHttpException( DataRestResource.ERRORMESSAGE_IS,
new Object[ ] { "all mode options are invalid" }, Status.CLIENT_ERROR_BAD_REQUEST );
}
}
} catch ( DataException e )
{
// 日志调试级别记录信息
locLogger.debug( "LineCapturePoint DataException ", e );
// 抛出500错误
throwHttpException( DataRestResource.ERRORMESSAGE_IS, new Object[ ] { e.getMessage( ) },
Status.SERVER_ERROR_INTERNAL );
}
OutPut.msg += " Cost:" + ( System.currentTimeMillis( ) - start ) + "ms";
return OutPut;
}
其中DataRestResource
、locLogger
即前面提到的ResourceManager
、LocLogger
对象;LCPUtil
为处理得到用于计算的输入类,DataUtil
为iServer提供的工具类,可以使用它来获取数据服务组件;modeRecognition
为处理一个具体输入的方法。另外,之后的处理几乎都是复用和直接修改原始对象,以减少内存占用、提高执行效率,所以代码可读性要降低一些,下一节再具体介绍。
LCPUtil类用来得到有效的输入,主要是容限的设置、使用点线数据集的处理、进一步检查参数是否合法,比如查询的点线数据集是否存在、数据集类型是否符合要求、上传点和查询到的点及上传线和查询线数据集是否都为空等,LCPUtil public属性如下:
/**
* Feature[ ] 参与计算的上传点(可能会被转坐标系)
*/
public Feature[ ] points;
/**
* List< Feature > 参与计算的查询点,转数组费时
*/
public List< Feature > dspoints;
/**
* Feature[ ] 参与计算的上传线(打断的情况需要修改)
*/
public Feature[ ] lines;
/**
* Double 捕捉容限(成功的容限,上传线,只读)
*/
public final Double tolerance;
/**
* Double 搜索容限(失败的容限,上传线,只读),未设置则=tolerance
*/
public final Double maxtolerance;
/**
* Double 捕捉容限(成功的容限,数据集,只读)
*/
public final Double dstolerance;
/**
* Double 搜索容限(失败的容限,数据集,只读),未设置则=dstolerance
*/
public final Double dsmaxtolerance;
/**
* String 成功返回模式(只读)
*/
public final String sucessMode;
/**
* String 失败返回模式(只读)
*/
public final String failMode;
/**
* Boolean 是否存在查询线数据集,只读
*/
public final Boolean hasLineDataset;
经过LCPUtil类的处理,得到了可以用于计算的对象,其中若有设置使用数据集的点参与计算,则获取服务组件通过设置的条件查询点(List
);线数据集不直接全部查询回来,LCPUtil提供一个方法,根据传入的查询点、容限构造查询范围再使用范围查询查询线,详见iServer帮助文档数据服务组件类的getFeature方法。查询点的代码片段如下:
// SQL查点
if ( input.pName != null && !"".equals( input.pName ) && dataUtil != null )
{
String[ ] nemes = input.pName.split( ":" );
String datasourceName = nemes[ 0 ];
String datasetNames = nemes[ 1 ];
// 获取数据服务组件
Data datacomp = dataUtil.getDataComponent( datasourceName );
DatasetInfo dsinfo = datacomp.getDatasetInfo( datasourceName, datasetNames );
if ( dsinfo.type != DatasetType.POINT )
{
throw new DataException( datasetNames + " not a point dataset!" );
} else
{
// 查询参数
QueryParameter queryParam = new QueryParameter( );
queryParam.name = datasetNames;
if ( !( input.pFilter == null || "".equals( input.pFilter ) || "null".equals( input.pFilter ) ) )
{
queryParam.attributeFilter = input.pFilter;
}
if ( input.pFields != null && input.pFields.length != 0 )
{
queryParam.fields = input.pFields;
}
// 根据条件查询点
this.dspoints = datacomp.getFeature( datasourceName, queryParam );
//点数据集投影
this.dspointsPrj = dsinfo.prjCoordSys;
}
} else
{
this.dspoints = null;
}
###三、关于精度的考虑以及容限的处理
设计输入容限和返回距离的单位都是米,那么怎么计算?
这里选择了精度稍欠(经纬度下容限的判断、返回单位为米的距离小数点后几位的精度以及捕获点小数点后几位的精度),但速度较快的方式:先把设置的容限处理成目标线数据集和线的容限值(即,和线的单位一致),比如,目标线是经纬度,设置的容限为111319米左右,那么计算时实际对比的容限大概为1,线数据集的容限和上传点的容限处理后分开保存。具体计算的方式,采用的是,先把(0,0)( tc, 0 )(墨卡托坐标系,EPSG:3857)两点转换为目标坐标系,再根据两点距离公式(dis=Math.sqrt( dx * dx + dy * dy )
)计算实际对比的容限;返回计算实际距离时交换坐标系。原理是投影坐标系(单位:米)上,任意两点之间的实际距离,就是这两点坐标使用距离公式计算出来的距离。计算实际使用的容限(以下简称对比容限及对比搜索容限),方法如下:
private Double computeTolerance( Double tc, PrjCoordSys targetPrj )
{
// 输入单位是米,转成数据集单位的距离
// 思路;先把(0.0)(根号2/2*tolerance,根号2/2*tolerance)转成目标坐标系再计算转后两点距离
// 但是不同位置经纬度一度的距离不是一样的,非得精确些就得每次计算都转成3857然后计算,那效率...
// 根号2/2*tolerance,比用(0,0)(tolerance,0)/(0,tolerance)稍精确一点点
// double xyDelta = 0.70710678 * tc;
Geometry geometry = new Geometry( );
geometry.parts = new int[ ] { 2 };
geometry.points = new Point2D[ 2 ];
geometry.points[ 0 ] = new Point2D( 0, 0 );
// geometry.points[ 1 ] = new Point2D( xyDelta, xyDelta );
// 只有x或y计算速度快些
geometry.points[ 1 ] = new Point2D( tc, 0 );
// 转换不会修改传入的geometry对象
geometry = CoordinateConversionTool.convert( geometry, this.Prj3857, targetPrj );
Double dx = geometry.points[ 0 ].x - geometry.points[ 1 ].x;
Double dy = geometry.points[ 0 ].y - geometry.points[ 1 ].y;
return Math.sqrt( dx * dx + dy * dy );
}
CoordinateConversionTool
是iServer提供的一个投影转换工具类,用于投影转换Geometry
对象。
最高精度的做法是:
首先所有加减乘除等运算都使用BigDecimal
;其次,不再这么处理容限,而是直接计算到点到所有线的最小距离,转为3857坐标系计算实际距离(也就是不管是否返回距离都得算实际距离),再与设置的容限对比,可优化的空间不多,线节点比较多的情况下,可以先将线上相邻线段排下序,再执行计算,就像之前js版实现的一样。
这里采取的做法是,先按照对比容限得到待捕获点的捕获范围,计算过程中用该范围去判断线上相邻线段是否与该范围相交(范围查线,线本身的范围肯定和捕获范围相交;上传的线不带范围,用geometry的方法获取范围也得遍历一遍线),相交的才计算距离(不计算实际距离,好理解的说法是这个距离的单位和线的一致,也即,和对比容限单位一致),判断相交的方法很简单,只是加减及判断,后面会介绍。
###四、基础算法
现在开始具体计算。点、线、对比容限都拿到了,需要解决的问题就是:如何从一堆线中找到离点最近的线并计算垂足?
上一节有提到,具体是:
遍历这些线,每条线遍历其上相邻的两点组成的线段(线都是点串的形式存储,参见iServer
Geometry类),若线段与搜索范围相交则计算点到线段距离及垂足(这俩都得算),找到一条线上最小距离的;然后找到所有线里最小距离的线。
前面说过效率问题,首先,数据集范围查询(范围查询的执行效率高,体现在数据集每个点线面等都有存储其范围,另,若数据集创建了空间索引,查询速度还能一定程度上进一步提高,特别是点线面等数量较大时,可以去iDesktop里验证)的线是线范围已经和查询范围相交的线,要找到最近的线,只能每条线遍历,没有可以跳过的;上传的线不带范围(也可以客户端构建请求体时手动添加,但没意义),Geometry获取范围时,若不存在范围,也要进行遍历的,所以和数据集做相同处理就好,即,每条线都遍历,因为没有比遍历更高效率的对比、排除线的方法。计算过程草图如下(真的很糙):
遍历线的点串,对比相邻两点构成线段与搜索范围是否相交,相交则计算距离、垂足。只能从线头遍历到线尾,不能第一个相交计算完距离、垂足就跳过后面的,比如:道路转弯处,不见得第一个范围相交的线段离的最近,极端点的还有葫芦型的线等,所以只能从头到尾算一遍,因为处理的线没有一定的规律性,就是一堆有顺序的点串而已,也排除使用折半查找(得先拆线段并排序,效率自然没图示这样高,图示只需从头到尾(点串长度减1)遍历一次,计算也不复杂)之类的办法。
对比范围是否相交的方法如下:
/**
* 判断 点及容限构成的矩形范围内 和 线段的矩形范围 是否相交
*
* @param point
* 待捕获点
* @param tc
* 逮捕获点容限
* @param segp1
* 线段点
* @param segp2
* 线段点
* @return 范围是否相交
*/
private Boolean isIntersected( Point2D point, double tc, Point2D segp1, Point2D segp2 )
{
Boolean flag = false;
// Double d1 = ( segp1.x > segp2.x ? segp1.x : segp2.x ) - ( point.x -
// tc );
// Double d2 = ( segp1.y > segp2.y ? segp1.y : segp2.y ) - ( point.y -
// tc );
// Double d3 = point.x + tc - ( segp2.x > segp1.x ? segp1.x : segp2.x );
// Double d4 = point.y + tc - ( segp2.y > segp1.y ? segp1.y : segp2.y );
// 一个为false后面就不用计算了
if ( ( ( segp1.x > segp2.x ? segp1.x : segp2.x ) + tc - point.x ) >= 0.0
&& ( point.y + tc - ( segp2.y > segp1.y ? segp1.y : segp2.y ) ) >= 0.0
&& ( point.x + tc - ( segp2.x > segp1.x ? segp1.x : segp2.x ) ) >= 0.0
&& ( ( segp1.y > segp2.y ? segp1.y : segp2.y ) + tc - point.y ) >= 0.0 )
{
flag = true;
}
return flag;
}
只要a范围、b范围相交,那么,一定有:a的Xmax-b的Xmin>=0;a的Ymax-b的Ymin>=0;反之,b-a的也是。
计算点到线段距离、垂足方法如下:
/**
* 计算点到线段距离,返回距离(判断容限用,数据集单位)和垂足
*
* @param pt0
* 待计算垂足的点
* @param pt1
* 线段的起点
* @param pt2
* 线段的终点
* @param startpos
* 线段起始位置
* @param result
* 存结果(直接修改对象)
*/
private void distanceToSegment( Point2D pt0, Point2D pt1, Point2D pt2, int startpos, PedalModel result )
{
Double dx0 = pt0.x - pt1.x;
Double dy0 = pt0.y - pt1.y;
Double dx = pt2.x - pt1.x;
Double dy = pt2.y - pt1.y;
Double along = ( dx * dx0 + dy * dy0 ) / ( dx * dx + dy * dy );
// 垂足及位置
if ( along <= 0.0 )
{
result.Pedal.x = pt1.x;
result.Pedal.y = pt1.y;
result.insertpos = startpos;
} else if ( along >= 1.0 )
{
result.Pedal.x = pt2.x;
result.Pedal.y = pt2.y;
result.insertpos = startpos + 1;
} else
{
result.Pedal.x = pt1.x + along * dx;
result.Pedal.y = pt1.y + along * dy;
result.insertpos = startpos;
}
// 两点距离
Double dxr = result.Pedal.x - pt0.x;
Double dyr = result.Pedal.y - pt0.y;
result.distance = Math.sqrt( dxr * dxr + dyr * dyr );
}
startpos
之后打断线时会使用到,PedalModel
如下:
/**
* 计算点到线垂足返回
*/
class PedalModel
{
Point2D Pedal; // 垂足
Double distance; // 距离(数据集单位)
int insertpos; // 断点插入位置(此位置后)
}
这里复用PedalModel
对象,不再每次计算都new,类似地,别的方法也复用一些对象,所以处理的时时候要注意代码执行顺序,边写边考虑复用的对象有没有被修改、修改了的话之后的过程需要的是否是修改后的等。
###五、根据输入得到输出
有了前面的计算思路,接下来就好处理了,就是遍历每个点、每个点(会先将点(上传的点,若之后的点不带有效转换投影则使用第一个点的投影)转换成上传线(取第一条线)的投影或数据集的投影;投影都有效且不同才会真正执行转换,点个数较多执行转换会比不转换更耗时)遍历所有线,得到每个点的结果。
计算一个点到一条线的最短距离、垂足的方法如下:
/**
* 返回点到线最短距离及垂足 若需要返回失败点距离则传最大容限,否则传容限
*
* @param pt
* 待捕获的点
* @param ln
* 待计算的线
* @param result
* 存结果(直接改对象)
*/
private void computeOnePedal( Feature pt, Feature ln, double tolerance, PedalModel result )
{
// 不再检查点线是否空和类型了
PedalModel tmpresult = new PedalModel( );
tmpresult.Pedal = new Point2D( );
// point、line只读
Point2D point = pt.geometry.points[ 0 ];
Geometry line = ln.geometry;
int startpos = 0;
for ( int i : line.parts ) // 可能是多线
{
if ( i < 2 ) // 跳过不合法线,几率微小,预防为主
{
startpos += i == 1 ? 1 : 0;
continue;
}
int endpos = startpos + i - 1;
while ( startpos < endpos ) // 一条线
{
// 判断跳过不计算点的方式得比distanceToSegment简单才有意义
// 范围有相交才执行计算
if ( isIntersected( point, tolerance, line.points[ startpos ], line.points[ startpos + 1 ] ) )
{
distanceToSegment( point, line.points[ startpos ], line.points[ startpos + 1 ], startpos,tmpresult );
if ( result.distance > tmpresult.distance )
{
result.distance = tmpresult.distance;
result.insertpos = tmpresult.insertpos;
result.Pedal.x = tmpresult.Pedal.x;
result.Pedal.y = tmpresult.Pedal.y;
if ( result.distance <= 0.0 )
{
break;
}
}
}
++startpos;
}
}
}
iServer的Geometry对象,type代表几何对象类型;parts (int[])数组长度表示该对象有几个组成部分,数组每一项表示该组成部分有多少个点;比如,type=“LINE”,parts=[5,4]表示该(多)线对象由两条线组成,第一条5个点(对应points数组下标0到4的点),第二条4个点(对应points数组下标5到8的点)。iClient有转换该对象(服务端返回该对象的表述,比如json格式,则返回iServer的Geometry对象转json后的字符串)为iClient自己的Geometry对象的方法,这里不讨论客户端对接服务,下一篇再说。
以此类推,我们就能得到一个点最终找到的最近的线及垂足、插入位置;所有参与计算的点的结果;所有LCPOutputFormat
项对应的结果。
####返回结果
分析下mode的设置,只要有设置,那么我们都得计算,我这里的策略是,对于数据集线,尽量使用对比容限去查询,所以,这里失败的返回模式为"“或者只有"P”,都只用对比容限去查询数据集及计算;否则使用搜索容限。
拿到了一个点的处理结果,接下来就是根据设置的模式返回了。这里的设计是,根据模式中有无打断的操作将结果放到不同的resultFormat
属性里:成功(PedalModel.distance <= 对比容限)并且打断(“B”、“U"至少一个)放到breakLines
;失败并且打断(对比容限 < PedalModel.distance <= 搜索容限 还是能捕获到最近线,除了失败返回为”"或"P"时的线数据集)放到breakFailed
;成功并且不打断时放到sucessPoints
;失败并且不打断时放到failedPoints
。其中只有U单独处理下,避免返回数组有null项等;就不贴代码了,可以下载查看源码。
下面介绍打断线的操作。考虑到支持更新(直接使用数据服务组件的updateFeatures 方法)打断后的线(上传的线和数据集的线,只要打断了,都可以更新到指定数据集),以及返回结果的处理,这里不采用new很多Feature返回的方式(会导致算法流程需要重新设计),直接将原来的Geometry拆一下,比如单线变多线(两条线)、多线变多线+1条线,因为可能很多个点的垂足都在同一条线上,每个点找到垂足,打断时直接修改原线对象比较划算省事并且效率较高、内存使用较低(不想重新设计也是重要原因,多线原来的属性没变,客户端也能把多线的每条线取出来)。已经得到最近线、垂足及插入位置,那么只要按照Geometry的存储方式,修改下parts数组和Points数组就行了,需要注意的是,虽然几率比较低,但是为了避免同一个坐标的点在线中重复出现(比如,a、b两点的垂足都是同一个点,特别是线的节点,因为前面计算垂足的distanceToSegment方法得到的垂足只会在线段上,线段端点,也就是线的节点的可能挺大),需要判断垂足点坐标是否和插入位置一样;据此决定点数组增加一个点或两个点,先来草图:
如图的一条线,若数组下标20的点与插入点坐标一致,那么点数组只需插入一个点,线变成:0到20(都包含,下同)为第一条线,插入点到之后的为第二条线;若数组下标20的点与插入点坐标不一致,则点数组只需插入两个点,线变成:0到20加插入点为第一条线,插入点到之后的为第二条线;至于parts数组,都只需要增加一项,根据上面的情况,计算线上点的数量略有不同。
打断线的方法如下:
/**
* 打断线,插入点,增加parts(变成多线)
*
* @param result
* 最近结果
* @param line
* 最近线(修改原对象)
* @return Boolean 是否打断
*/
private Boolean breakLine( PedalModel result, Feature line )
{
if ( result == null || line == null )
{
return false;
}
// 线数量
int plen = line.geometry.parts.length;
// 点数组长度
int ptlen = line.geometry.points.length;
// 不合法的线和插入位置
if ( plen < 1 || ptlen < 2 || result.insertpos >= ptlen - 1 )
{
return false;
}
// 插入点是否是线上的点(就不在PedalModel类加属性了,虽然刚开始加过...)
Boolean isLineComp = line.geometry.points[ result.insertpos ].x == result.Pedal.x
&& line.geometry.points[ result.insertpos ].y == result.Pedal.y;
int pos = -1;
// 在每条线的首尾端点则不打断
for ( int i = 0; i < plen; ++i ) // 每条线
{
pos += line.geometry.parts[ i ];
// 插入位置在每条线的尾部 或者 是线上点且在每条线头部
if ( result.insertpos == pos || ( result.insertpos == pos + 1 - line.geometry.parts[ i ] && isLineComp ) )
{
return false;
} else if ( result.insertpos < pos ) // 找到插入位置
{
// line.geometry.parts长度+1,线parts每个值应该大于2
int[ ] parts = new int[ plen + 1 ];
// 复制前面的
if ( i > 0 )
{
System.arraycopy( line.geometry.parts, 0, parts, 0, i );
}
Point2D[ ] points;
// 判断插入情况
// 插入点是线上的节点
if ( isLineComp )
{
parts[ i ] = line.geometry.parts[ i ] - ( pos - result.insertpos );
// 增加一个点
points = new Point2D[ ptlen + 1 ];
// 复制前面的
System.arraycopy( line.geometry.points, 0, points, 0, result.insertpos + 1 );
points[ result.insertpos + 1 ] = result.Pedal;
// 复制后面的
int num = ptlen - 1 - result.insertpos;
if ( num > 0 )
{
System.arraycopy( line.geometry.points, result.insertpos + 1, points, result.insertpos + 2,num );
}
} else // 插入点不是线节点
{
parts[ i ] = line.geometry.parts[ i ] - ( pos - result.insertpos ) + 1;
// 增加两个点
points = new Point2D[ ptlen + 2 ];
// 复制前面的
System.arraycopy( line.geometry.points, 0, points, 0, result.insertpos + 1 );
// 加两次(引用没问题)
points[ result.insertpos + 1 ] = result.Pedal;
points[ result.insertpos + 2 ] = result.Pedal;
// 复制后面的
int num = ptlen - 1 - result.insertpos;
if ( num > 0 )
{
System.arraycopy( line.geometry.points, result.insertpos + 1, points, result.insertpos + 3,num );
}
}
parts[ i + 1 ] = pos - result.insertpos + 1;
// 复制后面的
int num = plen - 1 - i;
if ( num > 0 )
{
System.arraycopy( line.geometry.parts, i + 1, parts, i + 2, num );
}
line.geometry.parts = parts;
line.geometry.points = points;
return true;
}
}
return false;
}
###六、完成并测试验证
可以边写边测上传点和线的情况,查询点和线只能放到iServer查看结果,可以远程调试(见iServer帮助文档)。验证情况如下图:
到这里服务端的开发已经完成,编译(JDk1.8)结果及工程源码点此,或复制链接: http://download.csdn.net/detail/supermapsupport/9824911
欢迎传播和围观。 下一篇 介绍扩展iClient for JavaScript对此服务进行对接。