1,实现流程
- 创建GPServer,使用ArcgisPro添加GP工具运行,然后使用共享web服务发布运行成功的GP任务
- 根据发布成功的GPServer发布地址,解析出GP服务的输入参数和输出参数
- 前端输入gp服务需要的参数,发送给后端来异步提交
- 后端提交后创建轮询任务等待执行结果
- 收到执行结果后解析,根据输出结果类型(表格、矢量、栅格)分别处理和保存
- 后端将需要添加样式的矢量或栅格数据重新发布为MapServer
- 前端展示表格数据,渲染带样式的GP结果的MapServer
2,GPServer的发布
发布gp工具,使用arcgis pro登录要发布服务的门户
在gispro中点击share——share web tool,然后选择刚刚成果运行的分析记录,点击ok
发布gp分析服务
2,解析GP服务
上一步发布成功的话,使用rest服务在web页面上应该能找到刚发布的服务,地址类似这样https://aaa.server.com:6443/arcgis/rest/services
前端使用该gp的url就能解析出输入参数和输出参数,并确定每个参数的类型;
- GPRasterDataLayer栅格数据
- GPFeatureRecordSetLayer矢量图层(属性和坐标信息)
- GPRecordSet 属性表格
如果输入参数类型是矢量应该支持geojson、shp、数据库的空间表
如果输出参数是矢量和栅格,可以在提交GP分析时一并把渲染样式一起提交到后端
一个gp分析可能会返回多个output,每个output都可能是矢量和栅格,也就是都可以作为图层支持添加样式
一般前后端访问rest接口都只需要解析json数据,不需要html内容,所以请求url都追加 “?f=json”
3,提交GP分析
考虑某些GP分析可能很耗时,如果分析数据巨大可能一天,需要注意两点:
1,矢量数据尽量不要使用geojson或具体文件,而是使用已经发布到arcgis的发布url,可以参考空间表发布到arcgis
2,提交请求使用post异步提交,轮询执行结果(需要考虑终止轮询)
因为链路太长,周期长,所以建议每一步都保存执行进度,提交GP分析的代码示例如下:
/**
* 提交GP分析
*
* @param param
*/
public void submitJob(GpParam param) {
//重新执行时清理正在运行的轮询
cleanOldGP(param.getGpId());
param.setStatus(1);
param.setBeginTime(LocalDateTime.now());
mongoTemplate.insert(param, "app_gp_param");
//后续操作后台运行taskExecutor
CompletableFuture.runAsync(() -> submitJobAsync(param), taskExecutor);
}
/**
* 异步处理gp任务
*
* @param param
*/
@Async("taskExecutor")
public void submitJobAsync(GpParam param) {
String gpId = param.getGpId();
Map<String, Object> paramMap = new HashMap<>();
//指定返回格式为json
paramMap.put("f", "json");
//指定输出坐标系
paramMap.put("env:outSR", "4490");
log.info("开始解析gp参数{}", gpId);
for (String key : param.getGpParams().keySet()) {
//区分是数据库空间表还是基本数据类型,数据库空间表这里我用@符号分割了数据库id和表名
String val = param.getGpParams().get(key) + "";
if (val.indexOf("@") > 0) {
//替换为数据库的发布地址作为数据源
String[] tableInfo = val.split("@");
TableInfoDO tableInfoDO = mongoTemplate.findOne(
new Query(Criteria.where("dbId").is(tableInfo[0])
.and("name").is(tableInfo[1])), TableInfoDO.class);
if (tableInfoDO == null) {
log.error("空间表不存在,{}", val);
updateStatus(gpId, 3, "空间表不存在," + val);
return;
}
String a = "";
if (StrUtil.isEmpty(tableInfoDO.getGpUrl())) {
log.warn("数据库表未发布arcgis:{}", val);
Update update = new Update();
update.set("startTime", LocalDateTime.now());
//单表发布到arcgis的代码参考我ArcGis系列下的另一篇文章
。。。。。。。
a = tablePubUrl;
if (a.contains("error")) {
log.error("空间表发布到arcgis失败,url:{},失败信息:{}", tablePubUrl, a);
updateStatus(gpId, 3, "空间表发布到arcgis失败," + tablePubUrl);
return;
}
//更新表发布地址
update.set("gpUrl", tablePubUrl);
update.set("endTime", LocalDateTime.now());
mongoTemplate.updateFirst(new Query(Criteria.where("_id").is(tableInfoDO.getId())), update, "db_database_table");
} else {
a = tableInfoDO.getGpUrl();
}
//以url形式提交gp参数 http://aaa.server.com/server/rest/services/Hosted/CONTOUR1/FeatureServer/0
JSONObject object = new JSONObject();
object.put("url", a);
paramMap.put(key, JSONObject.toJSONString(object));
} else {
paramMap.put(key, val);
}
}
String url = param.getGpUrl() + "/submitJob?f=json";
log.info("提交分析url:{},参数:{}", url, paramMap.toString());
String groupStr = HttpRequest.post(url)
.form(paramMap)
.contentType("application/x-www-form-urlencoded").timeout(60000)
.execute().body();
//{"jobId":"jf10c44e3286f47f989abbe1a99f0c3ba","jobStatus":"esriJobSubmitted"}
log.info("提交分析返回:{}", groupStr);
if (groupStr.contains("error")) {
//保存GP分析数据
Update update = new Update();
update.set("status", 3);
update.set("error", groupStr);
mongoTemplate.updateFirst(new Query(Criteria.where("gpId").is(gpId)), update, GpParam.class);
return;
}
JSONObject jsonObject = JSON.parseObject(groupStr);
String jobId = jsonObject.get("jobId") + "";
//保存GP分析数据
param.setJobId(jobId);
Update update = new Update();
update.set("jobId", jobId);
mongoTemplate.updateFirst(new Query(Criteria.where("gpId").is(gpId)), update, GpParam.class);
// 创建轮询任务
String pageId = param.getPageId();
log.info("创建轮询任务,每5s查询一次GP分析结果");
ScheduledFuture<?> future = executor.scheduleAtFixedRate(() -> {
if (taskStateMap.get(gpId) != null && taskStateMap.get(gpId)) {
return;
}
taskStateMap.put(gpId, true);
// 查询GP分析结果
boolean res = executeGPTask(url, jobId, pageId, gpId);
// 判断是否需要终止轮询任务
if (res) {
// 取消轮询任务
taskStateMap.remove(gpId);
if (taskMap.get(gpId) != null) {
taskMap.get(gpId).cancel(false);
//从任务列表中删除该用户的轮询任务
taskMap.remove(gpId);
}
} else {
taskStateMap.put(gpId, false);
}
}, 0, 5, TimeUnit.SECONDS); // 延迟0秒开始执行,并每隔5秒执行一次
// 将任务添加到任务列表中
taskMap.put(gpId, future);
}
4,轮询GP分析结果
很多刚接触的同事可能在这一步一直获取提交失败的错误,建议先在gp分析的web界面下方点击submit调试,确保页面上能正常拿到数据再来编码
- 执行状态有已提交,执行中,执行失败,执行成功,
- 后两种状态获取到就终止轮询,并修改gp的执行状态,
- 执行成功时解析分析结果并处理(加样式发布为图层)
- 因为矢量和表格数据都是json数据,所以都保存为json,栅格是tif的单独保存
/**
* 轮询gp执行状态
*
* @param url http://127.0.0.1:6080/arcgis/rest/services/test/Model5/GPServer/landconflicts/submitJob?f=json
* @param jobId
* @param pageId
* @param gpId
* @return
*/
private boolean executeGPTask(String url, String jobId, String pageId, String gpId) {
//http://127.0.0.1:6080/arcgis/rest/services/test/Model5/GPServer/landconflicts/jobs/j7d80b44b27834e428a5da531fdfcb1c9?f=json
url = url.replace("submitJob", "jobs/" + jobId);
String res = HttpUtil.get(url);
if (res.contains("error")) {
//保存异常信息
updateStatus(gpId, 3, res);
//异常信息,中断轮询
return true;
}
//执行结束,解析结果
JSONObject jsonObject = JSON.parseObject(res);
String state = jsonObject.get("jobStatus") + "";
//已提交 | 执行中
if ("esriJobSubmitted".equals(state) || "esriJobExecuting".equals(state)) {
return false;
} else if ("esriJobSucceeded".equals(state)) {
//执行成功
JSONObject jsonObj = jsonObject.getJSONObject("results");
//创建目录
new File(layerParamLocation + gpId + "\\json").mkdirs();
new File(layerParamLocation + gpId + "\\tif").mkdirs();
for (String outputName : jsonObj.keySet()) {
try {
GpResult gpResult = new GpResult();
gpResult.setOutputName(outputName);
gpResult.setJobId(jobId);
gpResult.setGpId(gpId);
gpResult.setPageId(pageId);
gpResult.setEndTime(LocalDateTime.now());
//查询outputName输出结果
String newUrl = url.replace("?f=json", "/results/" + outputName + "?f=pjson");
//保存分析结果到json文件 gpResult.setGpResult(HttpUtil.get(newUrl));
log.info("查询output结果,{}", newUrl);
gpResult.setGpResult(newUrl);
String resStr = getFeatureJson(newUrl);
JSONObject resJson = JSON.parseObject(resStr);
if (resJson.get("error") != null) {
//保存异常信息
log.error("获取执行结果异常,{}", newUrl);
updateStatus(gpId, 3, "获取执行结果异常," + newUrl);
return true;
}
String resFilePath = "";
//按照output类型分别存储矢量json和栅格url
if ("GPRasterDataLayer".equals(resJson.getString("dataType"))) {
//栅格数据下载tif到本地 https:/127.0.0.1:6443/arcgis/rest/directories/arcgisjobs/gp/demcreate04_gpserver/j6b911eade85d4ed88748abeef0c3a0ac/scratch/dem2.tif
//表格GPRecordSet和矢量GPFeatureRecordSetLayer
resFilePath = layerParamLocation + gpId + "\\tif\\" + outputName + ".tif";
String tifUrl = resJson.getJSONObject("value").getString("url");
FileUtils.downloadImage2(tifUrl, resFilePath);
//gpResult.setGpResult(tifUrl);
} else {
//表格GPRecordSet和矢量GPFeatureRecordSetLayer
resFilePath = layerParamLocation + gpId + "\\json\\" + outputName + ".json";
FileUtils.saveStringToFile(resJson.getJSONObject("value").toString(), resFilePath);
}
mongoTemplate.remove(new Query(Criteria.where("gpId").is(gpId)
.and("outputName").is(outputName)), GpResult.class);
mongoTemplate.insert(gpResult, "app_gp_result");
log.info("保存gp结果{}完成,{}", outputName, resFilePath);
} catch (Exception e) {
//保存异常信息
log.error("保存执行结果异常:{}", e.toString());
updateStatus(gpId, 3, "保存执行结果异常," + url);
return true;
}
}
//将执行结果追加样式并发布
publishGPResult(gpId);
return true;
} else if ("esriJobFailed".equals(state)) {
//执行失败
Update update = new Update();
update.set("status", 3);
update.set("endTime", LocalDateTime.now());
update.set("error", res);
mongoTemplate.updateFirst(new Query(Criteria.where("jobId").is(jobId)), update, GpParam.class);
return true;
} else {
log.error("gp执行未知状态:{},完整报文:{}", state, res);
return true;
}
}
/**
* 获取gp分析结果的矢量数据,主要针对结果数据量很大或网络波动导致的读取超时
* @param url
* @return
*/
public String getFeatureJson(String url) {
String result = HttpUtil.createGet(url)
.setConnectionTimeout(30000)
.setReadTimeout(30000)
.execute().body();
return result;
}
5,将GP分析结果发布为Map
将gp结果发布到arcgis同样需要调用python脚本,构建本地项目创建草稿并上传发布
- 发布图层需要的矢量和栅格数据从上一步保存的json和tif获取,需要的样式文件从前端提交gp时的保存的路径取
- 发布gp结果的详细代码放在同系列的另一篇文章中( ̄▽ ̄)*
/**
* 给GP分析结果添加样式并重新发布
*
* @param gpId
*/
private void publishGPResult(String gpId) {
Update update = new Update();
GpParam gpParam = mongoTemplate.findOne(new Query(Criteria.where("gpId").is(gpId)), GpParam.class);
// 图层参数临时目录
String layerParamDir = layerParamLocation + gpId;
log.info("开始遍历发布gp结果:{}", layerParamDir);
String url = null;
try {
url = pythonExecutor.publishLayerToArcgis(layerParamDir, getBaseUrl(gpParam.getGpUrl()), "lzwpro", "xxxxxx", gpId);
} catch (Exception e) {
log.error("发布gp结果异常:{},{}", gpId, e.toString());
updateStatus(gpId, 3, e.toString());
return;
}
update.set("status", 2);
update.set("publishUrl", url);
//更新GP任务状态
update.set("endTime", LocalDateTime.now());
mongoTemplate.updateFirst(new Query(Criteria.where("gpId").is(gpId)), update, GpParam.class);
}