ArcGIS Maritime Server 开发教程Maritime Service 开发实践

Posted 李远祥

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ArcGIS Maritime Server 开发教程Maritime Service 开发实践相关的知识,希望对你有一定的参考价值。

ArcGIS Maritime Server 开发教程(四)Maritime Service 开发实践

本章导读:Maritime Service 属于 MapService 的一个扩展,大部分功能继承 MapService ,但海图的特性也是非常明显。上一章已对比过期功能的差异,本章直接进入到代码模式,通过代码方式进行调用和操作。本教程只针对 Maritime Service 的操作进行讲解,涉及到 ArcGIS javascript API 基础部分不在此处进行讲述,建议读者先学习 ArcGIS JavaScript API ,熟悉之后才开始阅读下面的内容。By 李远祥

预览 Maritime Service

Maritime Service 在 ArcGIS 的 rest 页面中是不提供 JavaScript 的预览方式,因此,如果发布了 Maritime Service ,则需要编写一个简单的代码进行调用。

既然无法通过 rest 页面进行预览,那么就可以编写一个页面进行调用。这个页面功能非常简单,就是引用 ArcGIS JavaScript API ,利用 ArcGISDynamicMapServiceLayer 接口进行加载。其核代码如下

<!DOCTYPE html>
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  <meta name="viewport" content="initial-scale=1, maximum-scale=1,user-scalable=no" />
  <title>加载海图服务</title>
  <link rel="stylesheet" href="http://localhost/arcgis_js_api/3.19/esri/css/esri.css">
  <style>
    html,
    body,
    #map 
      height: 100%;
      margin: 0;
      padding: 0;
    
  </style>
  <script src="http://localhost/arcgis_js_api/3.19/init.js"></script>
  <script>
    var map;
    require(["esri/map",
      "esri/layers/ArcGISDynamicMapServiceLayer",
      "esri/geometry/Extent",
      "dojo/domReady!"], function (Map, DynamicLayer, Extent) 
        var initexten = new Extent( "xmin": 113.42, "ymin": 22.15, "xmax": 113.58, "ymax": 22.26, "spatialReference":  "wkid": 4326  );
        map = new Map("map",  extent: initexten );
        //加载 WGS 84 服务
        var basemap = new DynamicLayer("http://localhost:6080/arcgis/rest/services/SampleWorldCities/MapServer");
        map.addLayer(basemap);
        //加载海图服务
        var enc84 = new DynamicLayer("http://localhost:6080/arcgis/rest/services/SampleWorldCities/MapServer/exts/MaritimeChartService/MapServer");
        map.addLayer(enc84);
      );
  </script>
</head>
<body>
  <div id="map"></div>
</body>
</html>

从上述的代码例子可以餐刀,其加载方式完全使用了 ArcGIS JavaScript API 中的 ArcGISDynamicMapServiceLayer 接口,用法保持一致。由于海图数据基本上是在海上,一般情况下先加载一个底图,再叠加海图服务,不然在服务加载的时候很难看到对应的位置。最好给 map 对象加入初始化的 extent 值,直接显示海图所在的范围。

下图是使用了全球范围的地图,加载了一个非常小的海图数据,可以看出在初始显示的时候肉眼几乎没能找到其数据所在的位置。如下图,绿色箭头部分就是数据所在位置。因此,初始化地图的时候给 extent 值是非常重要的。

加入了 extent 值之后,其效果基本上就是在底图上叠加显示了海图服务。

ArcGIS 在 B/S 模式下没有专门的 Maritime Service 的 API ,所以,如果从事 Maritime 的开发,则是使用 ArcGIS JavaScript API 传统的开发接口。笔者这里的代码例子全部采用 3.19 版本的 API ,如果后续有三维的需求,建议开发人员及早从 4.x API 开始构建应用程序。

Maritime Service 的切片服务加载

严格来说,Maritime 产品是没有切片地图服务的,切片服务是属于传统的 ArcGIS Server 的切片服务,是通过 ArcGISTiledMapServiceLayer 接口进行调用。
Maritime Server 提供了 tpk 生成工具来生成地图切片。该工具是在 Martime Server 的安装目录下的 Bin 文件夹中。原则上是使用桌面小工具的方式进行切片包生成,生成的 tpk 切片包则可以被所有的 ArcGIS 软件加载。

Maritime Service 切片服务的制作分为 Server 方式和 Portal 方式。ArcGIS Server 方式比较麻烦,必须是非常了解 ArcGIS Server 服务机制的人员才知道怎么发布。笔者在之前的文章 《ArcGIS Maritime 发布海图切片服务详解》有着非常详细的论述。
Portal 方式则是非常简单直接。先用 tpk 工具生成切片包,在 Portal 的资源上传页面中选择该切片包,勾选发布为切片服务即可。

下面是海图切片服务通过传统的 JavaScript 脚本预览。

以下的代码例子是一个 WGS84 的地图,叠加一个海图切片服务(姑且这么称呼它,实际上切片之后跟海图的数据没有任何关系,也不是针对海图的服务)

<!DOCTYPE html>
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  <meta name="viewport" content="initial-scale=1, maximum-scale=1,user-scalable=no" />
  <title>加载wgs84坐标系海图切片</title>
  <link rel="stylesheet" href="http://localhost/arcgis_js_api/3.19/esri/css/esri.css">
  <style>
    html,
    body,
    #map 
      height: 100%;
      margin: 0;
      padding: 0;
    
  </style>
  <script src="http://localhost/arcgis_js_api/3.19/init.js"></script>
  <script>
    var map;
    require(["esri/map",
      "esri/layers/ArcGISDynamicMapServiceLayer",
      "esri/layers/ArcGISTiledMapServiceLayer",
      "esri/SpatialReference",
      "esri/geometry/Extent",
      "dojo/domReady!"], function (Map, DynamicLayer,
        ArcGISTiledMapServiceLayer,
        SpatialReference, Extent) 
        var initexten = new Extent( "xmin": 113.42, "ymin": 22.15, "xmax": 113.58, "ymax": 22.26, "spatialReference":  "wkid": 4326  );
        map = new Map("map",  extent: initexten );
        //测试动态投影,目前 海图切片只支持 web 墨卡托投影
        //ArcGIS 中坐标系是以第一个加进的服务为主
        var haituTile = new ArcGISTiledMapServiceLayer("http://localhost:6080/arcgis/rest/services/haitu/MapServer");
        map.addLayer(haituTile);
        map.removeLayer(haituTile);
        //加载基础服务WGS 84
        var basemap = new DynamicLayer("http://localhost:6080/arcgis/rest/services/SampleWorldCities/MapServer");
        map.addLayer(basemap);
        //重新加载海图服务
        map.addLayer(haituTile);
      );
  </script>
<body>
  <div id="map"></div>
</body>

</html>

目前 Maritime Server 自带的 mcstpk 工具只能生成基于 Web 墨卡托坐标系,同时只支持谷歌微软等标准的地图切片方案。尽管在 ServerConfiguration.xml 文件中可以定义 wkid ,但从生成的切片的结构来看,还是 web 墨卡托投影。所以,在生成切片包的操作之前,必须将 ServerConfiguration.xml 的 wkid 修改为 102100 。

以下是从 tpk 包解压后提取的 config 文件,在修改了 ServerConfiguration.xml 的 wkid 为 4326 之后,其结构依然为 Web 墨卡托投影坐标。

所以,上面的代码笔者是先加载一个完海图切片服务,然后删除掉,确保第一个加载进去的服务的坐标为 web 墨卡托(ArcGIS 默认第一个加载进去的服务的坐标系为地图的坐标系,如果其他服务为动态服务,则会自动投影到第一个坐标系中)。

Maritime Service 属性查找

在上一章中介绍过 Maritime Service 的查询接口只有 find ,没有任何的 图层 Query 操作。为什么会这样?如果从 S-57 数据本身来看,可以从中发现一些问题。
S-57 数据是以对象方式来存储的,没有所谓的图层概念,例如 水深点和锚地,都放在同一个区域中,有点像一个杂乱的仓库,东西随便对方,只能辨别出它是属于什么类型。
而 GIS 数据是以层的形式进行组织的,同样是一个仓库作为比喻,GIS 会将仓库划分为好多个储物间,水深放一个储物间,锚地放另一个储物间,有严格的划分,不能放错。所以 GIS 的查找可以直接到对应的储物间(图层)中精确获取所需的各种要素。
所以,在检索方面,Maritime Service 由于数据的原因,包括使用多个分幅来组织,导致不可能使用 Query 来操作。

那么,同样是 find 的方式进行检索,二者有什么本质的区别。下面来看看 Maritime Service 的检索的核心代码。

//查找地图
        function findNow() 
          var findtask1 = new FindTask("http://localhost:6080/arcgis/rest/services/SampleWorldCities/MapServer/exts/MaritimeChartService/MapServer");
          var fParam = new FindParameters();
          fParam.returnGeometry = true;
          fParam.searchFields = ["*"];
          var findTxt = dojo.byId("ftxt").value;
          fParam.searchText = findTxt;
          findtask1.execute(fParam, getResult);
        

        function getResult(results) 
          map.graphics.clear();
          dojo.forEach(results, function (result) 
            setGrapic(result.feature);
          
          );
        

从代码中看到,调用的方式同样是通过 FindTask 以及 FindParameters 组合来检索,也支持检索结果返回的字段和图形。但这个结果的结果是没法在服务器端进行结果过滤的。
从 getResult 方法可以看到其返回的结果其实一堆的 graphic 对象。通过graphic 接口可以获取到对应的属性值以及使用 symbol 对 graphic 进行渲染。这都跟传统的 find 方法是一样的。以下是完整的代码,包括从结果查询到结果返回到结果渲染和地图跳转等。

<!DOCTYPE html>
<html>
<head lang="en">
  <meta charset="UTF-8">
  <title>全局查找_findTask</title>
  <link rel="stylesheet" href="http://localhost/arcgis_js_api/3.19/dijit/themes/claro/claro.css">
  <link rel="stylesheet" href="http://localhost/arcgis_js_api/3.19/esri/css/esri.css">
  <script src="http://localhost/arcgis_js_api/3.19/init.js"></script>
  <style>
    html,
    body,
    #mapDiv 
      width: 100%;
      height: 100%;
      margin: 0;
      padding: 0;
    
  </style>
  <script>
    var map;
    require([
      "esri/map",
      "esri/layers/ArcGISDynamicMapServiceLayer",
      "esri/Color", 
      "esri/tasks/FindTask",
      "esri/tasks/FindParameters",
      "esri/symbols/SimpleMarkerSymbol",
      "esri/symbols/SimpleLineSymbol",
      "esri/symbols/SimpleFillSymbol",
      "esri/geometry/Extent",
      "dojo/domReady!"
    ], function (Map, ArcGISDynamicMapServiceLayer, eColor,FindTask, FindParameters, SimpleMarkerSymbol,
      SimpleLineSymbol, SimpleFillSymbol, Extent) 
        var initexten = new Extent( "xmin": 113.42, "ymin": 22.15, "xmax": 113.58, "ymax": 22.26, "spatialReference":  "wkid": 4326  );
        map = new Map("mapDiv",  extent: initexten );
        //加载 WGS 84
        var basemap = new ArcGISDynamicMapServiceLayer("http://localhost:6080/arcgis/rest/services/SampleWorldCities/MapServer");
        map.addLayer(basemap);
        var tLayer2 = new ArcGISDynamicMapServiceLayer("http://localhost:6080/arcgis/rest/services/SampleWorldCities/MapServer/exts/MaritimeChartService/MapServer");
        map.addLayer(tLayer2);
        //注册查询事件
        dojo.connect(dojo.byId("x1"), "onclick", findNow);
        //查找地图
        function findNow() 
          var findtask1 = new FindTask("http://localhost:6080/arcgis/rest/services/SampleWorldCities/MapServer/exts/MaritimeChartService/MapServer");
          var fParam = new FindParameters();
          fParam.returnGeometry = true;
          fParam.searchFields = ["*"];
          var findTxt = dojo.byId("ftxt").value;
          fParam.searchText = findTxt;
          findtask1.execute(fParam, getResult);
        

        function getResult(results) 
          map.graphics.clear();
          dojo.forEach(results, function (result) 
            setGrapic(result.feature);
          
          );
        

        function setGrapic(gra) 
          //如果需要查看所有属性,可以参考下面的脚本
          //var featureAttributes = gra.attributes;
          //for (var attr in featureAttributes) 
          //                           
          //alert("<b>" + attr + ":</b>  " + featureAttributes[attr] + "<br>");
          //
          var pointSym = new SimpleMarkerSymbol();
          var lineSym = new SimpleLineSymbol();
          var polySym = new SimpleFillSymbol();
          var color = new eColor([255, 0, 0, 0.5]);
          var outColor = new eColor([0, 255, 0, 0.5]);
          pointSym.setColor(color);
          pointSym.setSize(12);
          pointSym.outline.setColor(outColor);
          lineSym.setColor(color);
          lineSym.setWidth(3);
          polySym.setColor(color);
          polySym.outline.setColor(outColor);
          if (gra.geometry.type == "point") 
            gra.symbol = pointSym;
           else if (gra.geometry.type == "polyline") 
            gra.symbol = lineSym;
           else if (gra.geometry.type == "polygon") 
            gra.symbol = polySym;
          
          map.graphics.add(gra);
          jumptoMap(gra);
        
        function jumptoMap(gra) 
          if (gra.geometry.type == "point") 
            map.centerAndZoom(gra.geometry);
           else 
            //map.setLevel(6);
            map.setExtent(gra.geometry.getExtent());
          
        
      
    );
  </script>
</head>

<body>
  <div>
    <input type="text" value="亚太" id="ftxt">
    <button id="x1">搜索</button>
  </div>
  <div id="mapDiv"></div>
</body>

</html>

Maritime Service identify 操作

上一章提及的 Maritime Service 的 Identify 接口只提供一个容差值得设置,但实际上不是这样的,Identify 接口还是跟传统的点查询接口是一样的,只是屏蔽了大部分的参数。下面来看看 Maritime Service 的 Identify 操作的核心代码:

//点击地图,通过屏幕上的点进行查询
        function mapIdentify(evt) 
          var point, identifyParams, identifyTask;
          point = evt.mapPoint;
          identifyTask = new IdentifyTask("http://localhost:6080/arcgis/rest/services/SampleWorldCities/MapServer/exts/MaritimeChartService/MapServer");
          identifyParams = new IdentifyParameters();
          identifyParams.tolerance = 30;
          identifyParams.returnGeometry = true;
          identifyParams.geometry = point;
          //mapExtent 必须设置,不然查询不响应
          identifyParams.mapExtent = map.extent;
          //layerOption 实际上是不生效的
          identifyParams.layerOption = IdentifyParameters.LAYER_OPTION_VISIBLE;
          identifyParams.width = map.width;
          identifyParams.height = map.height;
          identifyTask.execute(identifyParams, getResult);
        
        function getResult(results) 
          //先清空地图上的要素,便于展示结果数据
          map.graphics.clear();
          //遍历所有的查找结果
          dojo.forEach(results, function (result) 
            //对每个查找结果进行渲染
            setGrapic(result.feature);
          
          );
        

上述代码同样是使用了 IdentifyTask 和 IdentifyParameters 组合进行查询,跟传统方法一样,但这里有两个地方必须要注意的。identifyParams.layerOption 实际上是不生效的,它不能处理查询的图层是否可见图层或全部图层查询,因为 S-57 数据本身就没有层的概念。
另一个是必须设置 identifyParams.mapExtent,不然无法响应查询。因为 S-57 数据是没有空间索引的,可以将其看做是一个大文本,没有章节等划分。如果不设定该参数,默认整个地图范围所有的图幅都进行点查询计算,那效率就非常低下了。

下面是点击查询的完整代码

<!DOCTYPE html>
<html>

<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  <meta name="viewport" content="initial-scale=1, maximum-scale=1,user-scalable=no" />
  <title>点查询海图要素</title>
  <link rel="stylesheet" href="http://localhost/arcgis_js_api/3.19/esri/css/esri.css">
  <style>
    html,
    body,
    #mapDiv 
      width: 100%;
      height: 100%;
      margin: 0;
      padding: 0;
    
  </style>
  <script src="http://localhost/arcgis_js_api/3.19/init.js"></script>
  <script>
    var map;
    require(["esri/map",
      "esri/layers/ArcGISDynamicMapServiceLayer",
      "esri/geometry/Extent",
      "esri/symbols/SimpleMarkerSymbol",
      "esri/symbols/SimpleLineSymbol",
      "esri/symbols/SimpleFillSymbol",
      "esri/tasks/IdentifyTask",
      "esri/tasks/IdentifyParameters",
      "esri/tasks/IdentifyResult",
      "dojo/Deferred",
      "esri/Color", 
      "dojo/domReady!"], function (Map, DynamicLayer, Extent,
        SimpleMarkerSymbol, SimpleLineSymbol, SimpleFillSymbol,
        IdentifyTask, IdentifyParameters, IdentifyResult, Deferred,
        eColor) 
        //初始化显示范围
        var initexten = new Extent( "xmin": 113.42, "ymin": 22.15, "xmax": 113.58, "ymax": 22.26, "spatialReference":  "wkid": 4326  );
        map = new Map("mapDiv",  extent: initexten );
        //加载基础服务WGS 84
        var basemap = new DynamicLayer("http://localhost:6080/arcgis/rest/services/SampleWorldCities/MapServer");
        map.addLayer(basemap);
        //ENC WGS 84
        var enc84 = new DynamicLayer("http://localhost:6080/arcgis/rest/services/SampleWorldCities/MapServer/exts/MaritimeChartService/MapServer");
        map.addLayer(enc84);
        //注册点击查询事件
        dojo.connect(dojo.byId("identity_Btn"), "onclick", identityFun);
        function identityFun() 
          //注册map的click 事件,进入点击查询
          map.on("click", mapIdentify);
        
        //点击地图,通过屏幕上的点进行查询
        function mapIdentify(evt) 
          var point, identifyParams, identifyTask;
          point = evt.mapPoint;
          identifyTask = new IdentifyTask("http://localhost:6080/arcgis/rest/services/SampleWorldCities/MapServer/exts/MaritimeChartService/MapServer");
          identifyParams = new IdentifyParameters();
          identifyParams.tolerance = 30;
          identifyParams.returnGeometry = true;
          identifyParams.geometry = point;
          //mapExtent 必须设置,不然查询不响应
          identifyParams.mapExtent = map.extent;
          //layerOption 实际上是不生效的
          identifyParams.layerOption = IdentifyParameters.LAYER_OPTION_VISIBLE;
          identifyParams.width = map.width;
          identifyParams.height = map.height;
          identifyTask.execute(identifyParams, getResult);
        
        function getResult(results) 
          //先清空地图上的要素,便于展示结果数据
          map.graphics.clear();
          //遍历所有的查找结果
          dojo.forEach(results, function (result) 
            //对每个查找结果进行渲染
            setGrapic(result.feature);
          
          );
        
        //渲染查找结果
        function setGrapic(gra) 
          //定义点线面符号
          var pointSym = new SimpleMarkerSymbol();
          var lineSym = new SimpleLineSymbol();
          var polySym = new SimpleFillSymbol();
          var color = new eColor([255, 0, 0, 0.5]);
          var outColor = new eColor([0, 255, 0, 0.5]);
          //点符号设置
          pointSym.setColor(color);
          pointSym.setSize(12);
          pointSym.outline.setColor(outColor);
          //线符号设置
          lineSym.setColor(color);
          lineSym.setWidth(3);
          //面符号设置
          polySym.setColor(color);
          polySym.outline.setColor(outColor);
          //对返回的物标要素进行渲染
          //获取物标所有的属性,其中objectType(固定标识) 为物标的唯一标识
          var featureAttributes = gra.attributes;
          for (var attr in featureAttributes) 
            if (attr == "objectType") 
              var oType = featureAttributes[attr];
              if (oType == "TSSBND") 
                //对点线面图形设置符号
                if (gra.geometry.type == "point") 
                  gra.symbol = pointSym;
                 else if (gra.geometry.type == "polyline") 
                  gra.symbol = lineSym;
                 else if (gra.geometry.type == "polygon") 
                  gra.symbol = polySym;
                
                map.graphics.add(gra);
                //跳转到地图位置
                jumptoMap(gra);
              
            
          
        
        //跳转到要素所在的位置
        function jumptoMap(gra) 
          if (gra.geometry.type == "point") 
            map.centerAndZoom(gra.geometry);
           else 
            map.setLevel(6);
            map.setExtent(gra.geometry.getExtent());
          
        
        //跳转到要素所在的位置
        function jumptoMap(gra) 
          if (gra.geometry.type == "point") 
            map.centerAndZoom(gra.geometry);
           else 
            map.setLevel(6);
            map.setExtent(gra.geometry.getExtent());
          
        
      );
  </script>
</head>
<body>
  <div>
    <button id="identity_Btn">点击此按钮然后点选地图查询(此处代码限定为TSSBND)</button>
  </div>
  <div id="mapDiv"></div>
</body>
</html>

留意关键部分,由于 S-57 没有图层,只有分类,那么结果返回的是所有的对象,必须通过 S-57 的标准规范对结果进行判定。例如点击一个地方要返回该区域的某一类对象,如 TSSBND ,其做法是通过结果判定 graphic 的 objectType 属性值,如果是 TSSBND ,才获取渲染。完整代码的最终结果如下图

identity 方式都是效率比较低的检索,而且只能从返回结果去做判定过滤,而不能通过服务端请求去过滤。原因还是在 S-57 数据本身。这种情况估计会在 S-100 标准推广之后会得到改善。

本章总结

Maritime Service 的开发绝大部分都跟传统 MapService 是一致的,所以,要在开始功能开发之前,必须先掌握好 ArcGIS JavaScript API ,并主要海图数据引起的服务能力的异同。如果熟悉 ArcGIS JavaScript API ,再懂一点海图数据的规范,那么基于 Maritime Service 开发应用就事半功倍了。

更多的GIS主流和非主流技术,可以持续关注CSDN的GIS制图乐园,以及微信公众号【GIS制图乐园】。BY 李远祥

以上是关于ArcGIS Maritime Server 开发教程Maritime Service 开发实践的主要内容,如果未能解决你的问题,请参考以下文章

ArcGIS Maritime Server 开发教程Maritime Server 正确的开发模式

ArcGIS Maritime Server 开发教程Maritime Server 正确的开发模式

ArcGIS Maritime Server 开发教程Maritime Server 正确的开发模式

ArcGIS Maritime Server 开发教程Maritime Service 开发技巧

ArcGIS Maritime Server 开发教程Maritime Service 开发技巧

ArcGIS Maritime Server 开发教程Maritime Service 开发实践