作者:MR
之前的一篇博客(SuperMap iServer REST资源(Restlet)扩展机制简介)我们对iServer资源扩展有了些了解,现在我们开始做一个常见功能的扩展——点靠近、打断线。由于篇幅比较长,会分成三篇博客,分别介绍扩展模块选择以及功能和输入输出设计等(本篇)、具体业务逻辑实现部分、前端js脚本对接使用扩展的服务部分。最后会上传工程源码及编译结果、js文件。
###应用场景:
一张草图简单说明:
(草图很糙请见谅)
**具体功能是:**找到离上图中每个红点最近的对应的线,并且返回每个红点到最近线的垂足、每个红点到最近线的距离等。
**常见应用场景:**获取到人员定位点后,找到最近的一条路及到这条路的最短直线距离;行驶过程中纠正车辆的位置始终在线上等。打断线的功能可以方便地通过web页面对数据进行一些编辑,当然,加上它也是因为,已经算出垂足了,从垂足打断这条线为两段只是顺便就能完成的事。
当然,我之前的一篇博客 iClient for JavaScript求两线交点、线线打断、点打断线,已经给出了在前端使用JavaScript代码去实现批量求两线交点、点打断线、线线打断等,但是有前端计算能力有限、距离计算结果数值不能设置单位、需要在JavaScript里已经拿到点和线的几何对象等限制,应用场景还比较单一,所以这次就来个服务端进行计算的版本。
另:iClient for JavaScript Geometry的distanceTo方法即可计算两个几何对象的最短距离及垂足点,实现了该方法的Geometry子类有效,基于坐标数值。
###具体功能设计:
- 支持GET、POST请求
- 支持查询点数据集和线数据集
- 支持上传点和线进行分析
- 支持自动转换坐标,实现点捕捉到线的容限、返回点到线距离的单位都为米
- 支持设置不同返回模式
- 当然,核心,计算垂足
###扩展模块选择:
出于更新数据集的考虑,当然选扩展到数据服务了。数据服务组件提供了范围查询要素的方法,我们可以用它查询距离点多少范围的线,选择范围查询更重要的原因是,数据集的线都是有存储线的范围的,加上给线数据集建立空间索引,那么查询的效率是很高的,这样直接就能判断捕捉容限内(以待捕捉点位中心,边长为两倍容限的正方形)有没有线和利用这些线进行计算(很遗憾iServer的数据服务组件并没有提供一步到位的方法,至少API里没有这样的方法)。
其它已经排除的选项。
**地图服务:**地图服务组件有个最近距离距离查询功能可以直接用,也就是查到距离某点最近的线,可惜的是并不会返回距离和垂足,而且如果是上传的线,那么还是需要自己实现,最后,地图服务组件没提供更新数据集的相关方法。
其它方式,扩展领域服务,比如这篇博客的介绍:扩展iServer实现坐标投影转换 。任何一个public类都能被发布为服务(服务列出里面的public方法)。但是它的问题是不通用,可能是查写死的工作空间路径下的数据源(文件型数据源嗨可能存在独占的问题),否则,请求参数就得带上很东西,使用和实现都不便;要么就得扩展领域服务的三层结构(一般可能只需要扩展服务组件及其服务资源就行)了。
总之基于以上考虑,这里扩展一个数据服务的资源,然后iServer现有的数据服务下的资源有datasources、featureResults,功能分别是数据源、数据集要素的操作,比如基础的查询和增删改(datasource的子资源)和各种方式的数据集查询,比如各种空间查询等,而它的子资源就是它创建的结果。然后我们的功能,层次上应该是要和这俩资源同一级的,可以理解为和工作空间同一级,我们要获取的可能是整个工作空间下任意数据源下的任意数据集的要素。当然,其实放哪一级无所谓,放哪你都能获取到服务组件的能力,区别只是层次感和怎么设计和获取url参数及请求体等,还是建议按照iServer的层次划分来。
与我们要实现的功能最类似的是featureResults资源,扩展这种资源应该继承AlgorithmResultSetResource
类,这篇博客给出了一个扩展算法结果集资源的空白模板:扩展一个iServer REST资源(Restlet机制)的简单模板工程
###资源输入输出设计
####输入:
考虑到实现的方便,上传的点和线都可以带属性,然后都是com.supermap.services.components.commontypes.Feature
类,这样上传的点、线和通过服务组件查询到的是同一类型,另外就是方便iClient for JavaScript对接、服务端解析。这里设计的输入为数组。最终的输入设计如下({“params”:LCPInputFormat[].class}):
package com.supermap.services.rest.InOutputFormat;
import com.supermap.services.components.commontypes.Feature;
/**
* @author MR
* @description LineCapturePoint计算资源输入(数组)
*/
public class LCPInputFormat implements java.io.Serializable
{
private static final long serialVersionUID = 1L;
/**
* 点数据集名,与points至少要有一个,格式:"{数据源名}:{数据集名}"
*/
public String pName;
public String pFilter; // 点数据集过滤条件(可选,不写整个点数据集参与计算)
public String[ ] pFields; // 指定返回的点数据集字段(可选)
public Feature[ ] points; // 上传的待捕获的点,属性会被原样返回
/**
* 线数据集名,与lines至少要有一个,格式:"{数据源名}:{数据集名}"
*/
public String lName;
public String lFilter; // 线数据集过滤条件(可选,不写整个线数据集参与计算)
public String[ ] lFields; // 指定返回的线数据集(可选)
public Feature[ ] lines; // 上传的线,属性会被原样返回
public Double tolerance; // 捕获容限,失败点输出到failed字段
/**
* 搜索容限,超过该容限的点不处理,直接输出到failed字段,不填则=tolerance
*/
public Double maxtolerance;
/**
* 字母组合(全大写),两段,{成功点设置}|{失败点设置}
*
* @ P,返回捕获的点或大于搜索容限时返回原始点
* @ A,返回最近线指定属性
* @ L,返回最近线geometry
* @ D,返回点到线距离
* @ B,返回打断后的线
* @ U,打断线并更新线数据集
*/
public String mode;
}
需要注意的是,输入输出类最好需要实现java.io.Serializable
接口,方便iServer临时资源缓存机制发挥作用,否则会有警告信息(不影响效果实现)。
####输出:
考虑到打断线的需求,输出分为打断线的情况和不打断线的情况;打断线时若要返回打断后的线,则按线存储,每条线打断后的多线对象以及对应的这条线上的捕捉点。最终的输出设计如下:
package com.supermap.services.rest.InOutputFormat;
/**
* @author MR
* @description LineCapturePoint计算资源输出 最少会返回sucessCount及failedCount
* 打断线返回breakLines,不打断返回sucessPoints
*/
public class LCPOutputFormat implements java.io.Serializable
{
private static final long serialVersionUID = 1L;
public resultFormat[ ] results;
public int totalSucessCount = 0; // 捕获成功的点总数
public int totalFailedCount = 0; // 捕获失败的点总数
public String msg = ""; // 消息
}
//-------------------文件分割线----------------------
package com.supermap.services.rest.InOutputFormat;
/**
* @author MR
* @description LineCapturePoint计算资源输出results字段
*/
public class resultFormat implements java.io.Serializable
{
private static final long serialVersionUID = 1L;
public List< OutputLP > breakLines; // 打断成功的线
public List< OutputLP > breakFailedPoints; // 打断失败的点
public List< OutputPL > sucessPoints; // 捕获点成功的结果(未打断线)
public List< OutputPL > failedPoints; // 失败的捕获点
public int sucessCount = 0; // 捕获成功的点数量
public int failedCount = 0; // 捕获失败的点数量
}
//-------------------文件分割线----------------------
package com.supermap.services.rest.InOutputFormat;
import com.supermap.services.components.commontypes.Feature;
/**
* @author MR
* @description LineCapturePoint计算资源输出results字段按点输出的结果
*/
public class OutputPL implements java.io.Serializable
{
private static final long serialVersionUID = 1L;
public Feature point = null; // 捕获的点,带输入点的属性
public Double distance = -1.0; // 点到线距离
public Feature line = null; // 捕获点的线
}
//-------------------文件分割线----------------------
package com.supermap.services.rest.InOutputFormat;
import com.supermap.services.components.commontypes.Feature;
/**
* @author MR
* @description LineCapturePoint计算资源输出results字段按线存输出
*/
public class OutputLP implements java.io.Serializable
{
private static final long serialVersionUID = 1L;
public Feature lines = null; // 打断后的线(多线对象)
public List<pwd> points = null; // 线上的捕获点
}
//-------------------文件分割线----------------------
package com.supermap.services.rest.InOutputFormat;
import com.supermap.services.components.commontypes.Feature;
import com.supermap.services.rest.DefaultMethodHandler;
/**
* @author MR
* @description LineCapturePoint计算资源输出results字段按线存输出OutputLP的points属性
*/
public class pwd extends DefaultMethodHandler implements java.io.Serializable
{
private static final long serialVersionUID = 1L;
public int pos = -1; // 点在线/多线的点数组中的位置
public Feature point = null; // 捕获的点,带输入点的属性
public Double distance = -1.0; // 点到线距离
}
###测试输入输出是否合法
package com.supermap.services.rest.resources.impl;
import com.alibaba.fastjson.JSON;
import com.supermap.services.rest.InOutputFormat.LCPInputFormat;
public class Test
{
public static String inputJSON = "[{\"points\":[{\"fieldNames\":[\"pid\"],\"fieldValues\":[\"point0\"],\"geometry\":{\"id\":0,\"style\":null,\"parts\":[1],\"points\":[{\"id\":\"SuperMap.Geometry.Point_37\",\"x\":-120,\"y\":54.142,\"type\":\"NONE\",\"tag\":null,\"bounds\":null,\"SRID\":null}],\"type\":\"POINT\",\"prjCoordSys\":{\"epsgCode\":null}}},{\"fieldNames\":[\"pid\"],\"fieldValues\":[\"point1\"],\"geometry\":{\"id\":0,\"style\":null,\"parts\":[1],\"points\":[{\"id\":\"SuperMap.Geometry.Point_38\",\"x\":-110,\"y\":40,\"type\":\"NONE\",\"tag\":null,\"bounds\":null,\"SRID\":null}],\"type\":\"POINT\",\"prjCoordSys\":{\"epsgCode\":null}}},{\"fieldNames\":[\"pid\"],\"fieldValues\":[\"point2\"],\"geometry\":{\"id\":0,\"style\":null,\"parts\":[1],\"points\":[{\"id\":\"SuperMap.Geometry.Point_39\",\"x\":-120,\"y\":25.857,\"type\":\"NONE\",\"tag\":null,\"bounds\":null,\"SRID\":null}],\"type\":\"POINT\",\"prjCoordSys\":{\"epsgCode\":null}}},{\"fieldNames\":[\"pid\"],\"fieldValues\":[\"point3\"],\"geometry\":{\"id\":0,\"style\":null,\"parts\":[1],\"points\":[{\"id\":\"SuperMap.Geometry.Point_40\",\"x\":-140,\"y\":25.857,\"type\":\"NONE\",\"tag\":null,\"bounds\":null,\"SRID\":null}],\"type\":\"POINT\",\"prjCoordSys\":{\"epsgCode\":null}}},{\"fieldNames\":[\"pid\"],\"fieldValues\":[\"point4\"],\"geometry\":{\"id\":0,\"style\":null,\"parts\":[1],\"points\":[{\"id\":\"SuperMap.Geometry.Point_41\",\"x\":-150,\"y\":40,\"type\":\"NONE\",\"tag\":null,\"bounds\":null,\"SRID\":null}],\"type\":\"POINT\",\"prjCoordSys\":{\"epsgCode\":null}}},{\"fieldNames\":[\"pid\"],\"fieldValues\":[\"point5\"],\"geometry\":{\"id\":0,\"style\":null,\"parts\":[1],\"points\":[{\"id\":\"SuperMap.Geometry.Point_42\",\"x\":-140,\"y\":54.142,\"type\":\"NONE\",\"tag\":null,\"bounds\":null,\"SRID\":null}],\"type\":\"POINT\",\"prjCoordSys\":{\"epsgCode\":null}}},{\"fieldNames\":[\"pid\"],\"fieldValues\":[\"point6\"],\"geometry\":{\"id\":0,\"style\":null,\"parts\":[1,1],\"points\":[{\"id\":\"SuperMap.Geometry.Point_43\",\"x\":-150,\"y\":40,\"type\":\"NONE\",\"tag\":null,\"bounds\":null,\"SRID\":null},{\"id\":\"SuperMap.Geometry.Point_44\",\"x\":-140,\"y\":54.142,\"type\":\"NONE\",\"tag\":null,\"bounds\":null,\"SRID\":null}],\"type\":\"POINT\",\"prjCoordSys\":{\"epsgCode\":null}}}],\"lines\":[{\"fieldNames\":[\"lid\"],\"fieldValues\":[\"line0\"],\"geometry\":{\"id\":0,\"style\":null,\"parts\":[2],\"points\":[{\"id\":\"SuperMap.Geometry.Point_45\",\"x\":-120,\"y\":54.142,\"type\":\"NONE\",\"tag\":null,\"bounds\":null,\"SRID\":null},{\"id\":\"SuperMap.Geometry.Point_46\",\"x\":-120,\"y\":25.857,\"type\":\"NONE\",\"tag\":null,\"bounds\":null,\"SRID\":null}],\"type\":\"LINE\",\"prjCoordSys\":{\"epsgCode\":null}}},{\"fieldNames\":[\"lid\"],\"fieldValues\":[\"line1\"],\"geometry\":{\"id\":0,\"style\":null,\"parts\":[2],\"points\":[{\"id\":\"SuperMap.Geometry.Point_47\",\"x\":-110,\"y\":40,\"type\":\"NONE\",\"tag\":null,\"bounds\":null,\"SRID\":null},{\"id\":\"SuperMap.Geometry.Point_48\",\"x\":-140,\"y\":25.857,\"type\":\"NONE\",\"tag\":null,\"bounds\":null,\"SRID\":null}],\"type\":\"LINE\",\"prjCoordSys\":{\"epsgCode\":null}}},{\"fieldNames\":[\"lid\"],\"fieldValues\":[\"line2\"],\"geometry\":{\"id\":0,\"style\":null,\"parts\":[2,2],\"points\":[{\"id\":\"SuperMap.Geometry.Point_49\",\"x\":-120,\"y\":54.142,\"type\":\"NONE\",\"tag\":null,\"bounds\":null,\"SRID\":null},{\"id\":\"SuperMap.Geometry.Point_50\",\"x\":-120,\"y\":25.857,\"type\":\"NONE\",\"tag\":null,\"bounds\":null,\"SRID\":null},{\"id\":\"SuperMap.Geometry.Point_51\",\"x\":-110,\"y\":40,\"type\":\"NONE\",\"tag\":null,\"bounds\":null,\"SRID\":null},{\"id\":\"SuperMap.Geometry.Point_52\",\"x\":-140,\"y\":25.857,\"type\":\"NONE\",\"tag\":null,\"bounds\":null,\"SRID\":null}],\"type\":\"LINE\",\"prjCoordSys\":{\"epsgCode\":null}}}],\"tolerance\":0.0001,\"mode\":\"PALDBU|PAD\",\"pName\":null,\"lName\":\"Interpolation:SamplesP\",\"pFilter\":\"\",\"lFilter\":\"SMID>0\",\"maxtolerance\":0.0008,\"pFields\":null,\"lFields\":[\"lid\"]},{\"points\":[{\"fieldNames\":[\"pid\"],\"fieldValues\":[\"point0\"],\"geometry\":{\"id\":0,\"style\":null,\"parts\":[1],\"points\":[{\"id\":\"SuperMap.Geometry.Point_37\",\"x\":-120,\"y\":54.142,\"type\":\"NONE\",\"tag\":null,\"bounds\":null,\"SRID\":null}],\"type\":\"POINT\",\"prjCoordSys\":{\"epsgCode\":null}}},{\"fieldNames\":[\"pid\"],\"fieldValues\":[\"point1\"],\"geometry\":{\"id\":0,\"style\":null,\"parts\":[1],\"points\":[{\"id\":\"SuperMap.Geometry.Point_38\",\"x\":-110,\"y\":40,\"type\":\"NONE\",\"tag\":null,\"bounds\":null,\"SRID\":null}],\"type\":\"POINT\",\"prjCoordSys\":{\"epsgCode\":null}}},{\"fieldNames\":[\"pid\"],\"fieldValues\":[\"point2\"],\"geometry\":{\"id\":0,\"style\":null,\"parts\":[1],\"points\":[{\"id\":\"SuperMap.Geometry.Point_39\",\"x\":-120,\"y\":25.857,\"type\":\"NONE\",\"tag\":null,\"bounds\":null,\"SRID\":null}],\"type\":\"POINT\",\"prjCoordSys\":{\"epsgCode\":null}}},{\"fieldNames\":[\"pid\"],\"fieldValues\":[\"point3\"],\"geometry\":{\"id\":0,\"style\":null,\"parts\":[1],\"points\":[{\"id\":\"SuperMap.Geometry.Point_40\",\"x\":-140,\"y\":25.857,\"type\":\"NONE\",\"tag\":null,\"bounds\":null,\"SRID\":null}],\"type\":\"POINT\",\"prjCoordSys\":{\"epsgCode\":null}}},{\"fieldNames\":[\"pid\"],\"fieldValues\":[\"point4\"],\"geometry\":{\"id\":0,\"style\":null,\"parts\":[1],\"points\":[{\"id\":\"SuperMap.Geometry.Point_41\",\"x\":-150,\"y\":40,\"type\":\"NONE\",\"tag\":null,\"bounds\":null,\"SRID\":null}],\"type\":\"POINT\",\"prjCoordSys\":{\"epsgCode\":null}}},{\"fieldNames\":[\"pid\"],\"fieldValues\":[\"point5\"],\"geometry\":{\"id\":0,\"style\":null,\"parts\":[1],\"points\":[{\"id\":\"SuperMap.Geometry.Point_42\",\"x\":-140,\"y\":54.142,\"type\":\"NONE\",\"tag\":null,\"bounds\":null,\"SRID\":null}],\"type\":\"POINT\",\"prjCoordSys\":{\"epsgCode\":null}}},{\"fieldNames\":[\"pid\"],\"fieldValues\":[\"point6\"],\"geometry\":{\"id\":0,\"style\":null,\"parts\":[1,1],\"points\":[{\"id\":\"SuperMap.Geometry.Point_43\",\"x\":-150,\"y\":40,\"type\":\"NONE\",\"tag\":null,\"bounds\":null,\"SRID\":null},{\"id\":\"SuperMap.Geometry.Point_44\",\"x\":-140,\"y\":54.142,\"type\":\"NONE\",\"tag\":null,\"bounds\":null,\"SRID\":null}],\"type\":\"POINT\",\"prjCoordSys\":{\"epsgCode\":null}}}],\"lines\":[{\"fieldNames\":[\"lid\"],\"fieldValues\":[\"line0\"],\"geometry\":{\"id\":0,\"style\":null,\"parts\":[2],\"points\":[{\"id\":\"SuperMap.Geometry.Point_45\",\"x\":-120,\"y\":54.142,\"type\":\"NONE\",\"tag\":null,\"bounds\":null,\"SRID\":null},{\"id\":\"SuperMap.Geometry.Point_46\",\"x\":-120,\"y\":25.857,\"type\":\"NONE\",\"tag\":null,\"bounds\":null,\"SRID\":null}],\"type\":\"LINE\",\"prjCoordSys\":{\"epsgCode\":null}}},{\"fieldNames\":[\"lid\"],\"fieldValues\":[\"line1\"],\"geometry\":{\"id\":0,\"style\":null,\"parts\":[2],\"points\":[{\"id\":\"SuperMap.Geometry.Point_47\",\"x\":-110,\"y\":40,\"type\":\"NONE\",\"tag\":null,\"bounds\":null,\"SRID\":null},{\"id\":\"SuperMap.Geometry.Point_48\",\"x\":-140,\"y\":25.857,\"type\":\"NONE\",\"tag\":null,\"bounds\":null,\"SRID\":null}],\"type\":\"LINE\",\"prjCoordSys\":{\"epsgCode\":null}}},{\"fieldNames\":[\"lid\"],\"fieldValues\":[\"line2\"],\"geometry\":{\"id\":0,\"style\":null,\"parts\":[2,2],\"points\":[{\"id\":\"SuperMap.Geometry.Point_49\",\"x\":-120,\"y\":54.142,\"type\":\"NONE\",\"tag\":null,\"bounds\":null,\"SRID\":null},{\"id\":\"SuperMap.Geometry.Point_50\",\"x\":-120,\"y\":25.857,\"type\":\"NONE\",\"tag\":null,\"bounds\":null,\"SRID\":null},{\"id\":\"SuperMap.Geometry.Point_51\",\"x\":-110,\"y\":40,\"type\":\"NONE\",\"tag\":null,\"bounds\":null,\"SRID\":null},{\"id\":\"SuperMap.Geometry.Point_52\",\"x\":-140,\"y\":25.857,\"type\":\"NONE\",\"tag\":null,\"bounds\":null,\"SRID\":null}],\"type\":\"LINE\",\"prjCoordSys\":{\"epsgCode\":null}}}],\"tolerance\":0.0001,\"mode\":\"PALDB|PAD\",\"pName\":null,\"lName\":\"Interpolation:SamplesP\",\"pFilter\":\"\",\"lFilter\":\"SMID>0\",\"maxtolerance\":0.0008,\"pFields\":[\"pid\"],\"lFields\":[\"lid\"]}]";
public static void main( String[ ] args )
{
long start = System.currentTimeMillis( );
LCPInputFormat[ ] INPUT = JSON.parseObject( inputJSON, LCPInputFormat[ ].class );
System.out.println( "JSON to Object cost: " + ( System.currentTimeMillis( ) - start ) + "ms" );
}
对接这个服务的基于iClient for JavaScript的对接代码在三里面介绍。
结果:
JSON to Object cost: 335ms
没有报错,断点检查INPUT对象没问题即说明输入设计合法,可以根据输入进行计算了。输出类似。当然,将工程导出jar包,放到iServer运行和远程调试(见iServer帮助文档,之前一篇博客也有介绍)也没问题。
欢迎传播和围观。 下一篇 介绍如何将输入处理成输出,即具体计算的过程。