前段時間接了一個項目,涉及到了空間信息三維可視化的工作。之前在網上查找無意中看到ArcGIS API for JavaScript(以下簡稱“ArcGIS API”或“該API”)可以在網頁上制作三維可視化圖。好在有友人在國外幫我把整個文檔和API下載下來了,於是就着手學習了一下這個API。
簡介
做GIS的肯定清楚ArcGIS是什么,包括一系列的ArcMap、ArcScence、ArcEngine等。ArcGIS推出了這套JavaScript API,現在有4.2版本,該版本可以創建二維、三維的網頁應用程序。
下面是官網給出的一些三維可視化的示例。
ArcGIS制作可視化圖的大體結構為:View 包含 Map 包含 Layer(s) 包含 Graphic(s)。帶(s)表示是一個數組。其中
- View(視圖):對應HTML上的一個元素,在該元素中顯示底圖以及底圖上的所有圖層。
- Map(底圖):可以為多種類型:街道圖、衛星圖、大洋圖、地表圖等。
- Layer(圖層):有多種類型,如FeatureLayer、GraphicLayer、PointCloudLayer,甚至有CSVLayer等。對於圖層的解釋,ArcGIS官方文檔指出:
圖層是 ArcMap、ArcGlobe 和 ArcScene 中地理數據集的顯示機制。一個圖層引用一個數據集,並指定如何利用符號和文本標注繪制該數據集。向地圖添加圖層時,要指定它的引用數據集並設定地圖符號和標注屬性。
- Graphic(圖形):圖形在官方文檔中的解釋是:“圖形是一個矢量圖,代表了真實世界的地理事物或地理現象。它可以包含geometry(幾何)、symbol(符號)、attributes(特性)。”
A Graphic is a vector representation of real world geographic phenomena. It can contain geometry, a symbol, and attributes.A Graphic is displayed in the GraphicsLayer.
制作可視化圖
使用ArcGIS API,需要引用各個組件(Map, SenceView, FeatureLayer ...),各個組件引用的路徑在官網文檔上會標注。引用時使用require()函數(這里使用TypeScript語法表示變量的類型)
require(modules: Array<String>, callback: function) => void
即可將各個組件引用到應用程序中。其中
- modules:是一個字符串數組,每一項代表了一個組件的引用路徑。例如,組件Map的地址是 “esri/Map” 。
- callback:是一個回調函數,表示各個組件加載完成后執行的函數操作。這個函數的的參數比較特別,參數個數與modules中引用的組件的個數相同,每個參數與modules中的組件路徑一一對應,表示引用的各個組件的一個對象。
於是,腳本整體上類似於:
require([ "esri/Map", "esri/views/SceneView", "dojo/domReady!" ], function(Map, SceneView) { // Code to create the map and view will go here });
當然這些腳本要寫在一對script標簽里。
添加底圖
在ArcGIS API中,所有地圖要素都是以對象的形式存在的。要在地圖上添加地理地圖,就需要創建地理地圖對象。Map類的對象就表示一個地理地圖。當引用了MA類組件之后,就可以創建其一個Map類的對象了。
var map = new Map({ basemap: "streets", ground: "world-elevation" });
Map類的屬性有這些:
- basemap:地理底圖的類型。ArcGIS提供了多種類型的地理底圖,包括了OpenStreetMap(osm)。
- layers:包含了地圖上展示的要素所在的圖層,這個圖層是“可操作的”,包括FeatureLayers、WebTileLayers和GraphicsLayers,其中不包含basemaps,也就是無法通過訪問layers屬性訪問basemap。
- allLayers:與layers屬性不同的是,這個屬性包含了basemap圖層、ground圖層以及“可操作圖層”。
- ground:這個屬性只在使用三維視角的時候有用。ground屬性是一個Ground類的對象,它將真實世界的地形或地勢渲染到底圖上。它包含了一組圖層來顯示地圖。在創建地理底圖的時候,ground屬性可以包含一組layer,也可以僅僅賦予一個字符串 world-elevation ,通過操作ground中的layer屬性來對底圖進行操作。示例見Toggle ground elevation。
通過調節這些屬性,就可以實現我們想要的地圖。
添加視圖
我們需要一個視圖來對我們所制作的地圖進行觀察,包括對底圖的觀察以及其他所有要素的觀察。在ArcGIS軟件中,如果制作二維地圖或進行二維分析,就使用ArcMap軟件;如果制作三維地圖或進行三維分析,就使用ArcScene軟件。在ArcGIS API中也是一樣的。我們如果制作二維地圖,就使用MapView類的對象創建視圖;如果制作三維地圖,就使用SceneView類的對象創建視圖。
創建一個視圖可以的方法與創建底圖類似,如
var view = new SceneView({ container: "viewDiv", map: map });
這個SceneView的屬性非常多,日后有空一一列舉,與我們制作的三維地圖關系較大的有:
- container:是一個DOM元素的id,地圖將渲染在該DOM元素內。ArcGIS API制作的地圖可以自適應。
- map:顯示的底圖。制作好底圖后,在這里引用制作的底圖實例,即可在地圖上顯示。
- camera:視圖攝像機。攝像機代表了三位觀察的位置和方向。該屬性是一個Camera類的實例,使用三個參數確定了攝像機的位置和方向:
- position:攝像機的位置。是一個Point類的對象,可以使用經(longitude)緯(latitude)度作為平面位置的參數,也可以使用大地坐標(x, y)作為平面位置的參數。高程都是用z屬性來指定的。還有一個spatialReference的屬性,用於指定參考系。
- heading:攝像機朝向的方位角。
- tilt:攝像機的“垂直角”,這個定義與測量學中的垂直角(天頂距)定義不同,當攝像機豎直向下時,該角度為0°。
一旦創建了視圖對象,就會在選定的DOM元素中進行渲染。
添加圖層
ArcGIS API提供了豐富的圖層可以使用,但是不同的圖層代表了不同的含義。這里只分析一下FeatureLayer和GraphicLayer的區別:當圖層與某一地理實體相對應時,最好使用FeatureLayer,表示是地理要素的圖層,具有實體含義;否則使用GraphicLayer,表示僅僅是一些幾何元素,沒有地理含義。
我所拿到的項目要可視化的內容具有地理實體含義,所以使用FeatureLayer。一個FeatureLayer對象包含很多屬性,可以通過REST服務創建,也可以本地創建。使用REST服務創建需要發布REST服務,然后在url屬性上填入REST服務的地址。這里介紹從本地創建FeatureLayer中的要素,官網示例見Sample - Create a FeatureLayer with GeoJSON data。
如果要從本地創建要素,需要同時設置FeatureLayer的五個屬性:fields、objectIdField、spatialReference、geometryType和source。
- fields:是一個對象數組,相當於ArcMap中的屬性表各個字段的配置,每一個對象表示了屬性表中的一個字段。每個對象有以下幾個常用屬性
- name:字段名。
- type:字段的數據類型,與ArcMap中一樣,有small-integer、integer、single、double、string、date、oid、geometry、blob、raster、guid、global-id、xml等。
- alias:字段替用名。
- length:字段長度。
- nullable:字段是否可空。
- editable:字段時候可編輯。
- objectIdField:指定fields中那個字段代表了要素的ObjectId。ObjectId是每個要素的唯一標識符。
- spatialReference:指定地理參考系。
- geometryType:表示要素的幾何類型,有point、mulitpoint、polyline、polygon等類型。
- source:是一個Graphic對象的集合。每個Graphic對象包括三個部分:geometry、symbol和attribute。
- geometry:是一個Geometry類的對象,定義了Graphic對象的地理位置。Geometry類的派生類有Point(點)、MultiPoint(多點)、Polyline(折線)、Polygon(折線)等。
- symbol:該要素顯示時的符號,代表了可視化方式。在創建Graphic對象的時候直接指定其symbol屬性不是一個比較好的做法。比較好的做法是使用Renderer(渲染器)來對圖層中的所有要素進行統一渲染。
- attribute:是一個對象數組,每個對象要包含fields中聲明的所有不可空字段。
除此之外,還有一些屬性是非常有用的:
- renderer:渲染器。是一個Renderer類的對象,表示對要素的geometry和attribute如何進行渲染。
- popupTemplate:彈出框的模板,是一個PopupTemplate對象,可以用來顯示要素的數據。
當我們獲取到了可視化的數據,首先創建一個Graphic數組,在官網示例中,是這樣的
return arrayUtils.map(geoJson.features, function(feature, i) { return { geometry: new Point({ x: feature.geometry.coordinates[0], y: feature.geometry.coordinates[1] }), // select only the attributes you care about attributes: { ObjectID: i, title: feature.properties.title, type: feature.properties.type, place: feature.properties.place, depth: feature.geometry.coordinates[2] + " km", time: feature.properties.time, mag: feature.properties.mag, mmi: feature.properties.mmi, felt: feature.properties.felt, sig: feature.properties.sig, url: feature.properties.url } }; });
這里使用了arrayUtils的方法將一個數組映射為另一個數組。也可以使用foreach循環來完成這件事。將Griphic數組用一個變量保存起來。
fields也需要我們進行創建,官網的示例中創建了如下的屬性表:
var fields = [ {name: "ObjectID",alias: "ObjectID",type: "oid"}, {name: "title",alias: "title",type: "string"}, {name: "type",alias: "type",type: "string"}, {name: "place",alias: "place",type: "string"}, {name: "depth",alias: "depth",type: "string"}, {name: "time",alias: "time",type: "date"}, {name: "mag",alias: "Magnitude",type: "double"}, {name: "url",alias: "url",type: "string"}, {name: "mmi",alias: "intensity",type: "double"}, {name: "felt",alias: "Number of felt reports",type: "double"}, {name: "sig",alias: "significance",type: "double"} ];
我們現在就可以創建FeatureLayer了,示例代碼如下:
var lyr = new FeatureLayer({ source: graphics, // autocast as an array of esri/Graphic // create an instance of esri/layers/support/Field for each field object fields: fields, // This is required when creating a layer from Graphics objectIdField: "ObjectID", // This must be defined when creating a layer from Graphics renderer: quakesRenderer, // set the visualization on the layer spatialReference: { wkid: 4326 }, geometryType: "point", // Must be set when creating a layer from Graphics popupTemplate: pTemplate }); map.add(lyr);
最后一行通過map類對象的add()方法,將該要素圖層添加到地圖上。其中,quakesRenderer是創建的渲染器,下小節中會詳細講解。
設計渲染器
渲染器是地圖顯示符號的方法,相當於Echarts中的VisualMap配置項。ArcGIS API中有很多渲染器,我們這里對點符號的渲染可以使用SimpleRenderer,有幾個屬性
- label:渲染器標簽。
- symbol:渲染用的符號。是Symbol類的一個對象。Symbol的派生類包含了豐富的可視化類型,有二維的和三維的。如果使用三維的可視化符號,使用Symbol3D類,包括二維可視化符號的三維版本和一些三維可視化特有的符號,如MeshSymbol3D。
- visualVariables:視覺變量,是一個對象數組。效果類似於Echarts中的VisualMap配置項,可以根據某一屬性值對顏色、尺寸、透明度、旋轉角度進行映射調整。每一個元素都是以下幾種類型:ColorVisualVariable、SizeVisualVariable、OpacityVisualVariable、RotationVisualVariable。
官網設計的渲染器如下所示:
var quakesRenderer = new SimpleRenderer({ symbol: new SimpleMarkerSymbol({ style: "circle", size: 20, color: [211, 255, 0, 0], outline: { width: 1, color: "#FF0055", style: "solid" } }), visualVariables: [ { type: "size", field: "mag", // earthquake magnitude valueUnit: "unknown", minDataValue: 2, maxDataValue: 7, // Define size of mag 2 quakes based on scale minSize: { type: "size", expression: "view.scale", stops: [ {value: 1128,size: 12}, {value: 36111,size: 12}, {value: 9244649,size: 6}, {value: 73957191,size: 4}, {value: 591657528,size: 2}] }, // Define size of mag 7 quakes based on scale maxSize: { type: "size", expression: "view.scale", stops: [ {value: 1128,size: 80}, {value: 36111,size: 60}, {value: 9244649,size: 50}, {value: 73957191,size: 50}, {value: 591657528,size: 25}] } }] });
這個渲染器的效果如下所示
整體代碼
1 var arcgis_groupLayer; 2 3 function ArcGIS_Map_Init() { 4 require([ 5 "esri/layers/GroupLayer", 6 "esri/layers/FeatureLayer", 7 "esri/Map", 8 "esri/views/SceneView", 9 "esri/widgets/LayerList", 10 "esri/layers/support/Field", 11 "esri/geometry/Point", 12 "esri/renderers/SimpleRenderer", 13 "esri/symbols/PointSymbol3D", 14 "esri/symbols/ObjectSymbol3DLayer", 15 "esri/request", 16 "dojo/_base/array", 17 "dojo/dom", 18 "dojo/on", 19 "dojo/domReady!" 20 ], function (GroupLayer, FeatureLayer, Map, SceneView, LayerList, Field, Point, SimpleRenderer, PointSymbol3D, ObjectSymbol3DLayer, esriRequest, 21 arrayUtils, dom, on) { 22 23 var arcgis_fields = [ 24 {name: "ObjectID", alias: "ObjectID", type: "oid"}, 25 {name: "title", alias: "title", type: "string"}, 26 {name: "num", alias: "num", type: "integer"} 27 ]; 28 29 arcgis_groupLayer = new GroupLayer({ 30 title: "消防數據", 31 visibility: true, 32 visibilityMode: "exclusive" 33 }) 34 35 var arcgis_arcgismap = new Map({ 36 basemap: "osm", 37 layers: [arcgis_groupLayer] 38 }); 39 40 var arcgis_initCam = { 41 position: { 42 x: 120.61, 43 y: 30.50, 44 z: 100000, 45 spatialReference: { 46 wkid: 4326 47 } 48 }, 49 heading: 15, 50 tilt: 60 51 }; 52 53 var arcgis_view = new SceneView({ 54 map: arcgis_arcgismap, 55 container: "arcgismap", 56 camera: arcgis_initCam 57 }); 58 59 arcgis_view.then(function () { 60 arcgis_layerList = new LayerList({ 61 view: arcgis_view 62 }) 63 64 arcgis_view.ui.add(arcgis_layerList, "top-right"); 65 }) 66 67 $.getJSON("js/json/fire.json", function (data) { 68 var graphics = []; 69 70 data.forEach(function(iSender, i) { 71 graphics.push({ 72 geometry: new Point({ 73 longitude: iSender.longitude, 74 latitude: iSender.latitude 75 }), 76 attributes: { 77 ObjectID: iSender.senderId, 78 title: iSender.senderName, 79 num: iSender.num 80 } 81 }) 82 }, this); 83 84 var arcgis_fireRenderer = new SimpleRenderer({ 85 symbol: new PointSymbol3D({ 86 symbolLayers: [new ObjectSymbol3DLayer({ 87 resource: { 88 primitive: "cube" 89 }, 90 width: 500, 91 depth: 500, 92 material: {color: "#e6b600"} 93 })] 94 }), 95 label: "火警數", 96 visualVariables: [{ 97 type: "size", 98 field: "num", 99 axis: "height" 100 },{ 101 type: "size", 102 axis: "width-and-depth", 103 useSymbolValue: true, 104 }] 105 }); 106 107 var arcgis_fireLayer = new FeatureLayer({ 108 source: graphics, 109 fields: arcgis_fields, 110 objectIdField: "ObjectID", 111 renderer: arcgis_fireRenderer, 112 spatialReference: { 113 wkid: 4326 114 }, 115 geometryType: "point", 116 popupTemplate: { 117 title: "{title}", 118 content: [{ 119 type: "fields", 120 fieldInfos: [{ 121 fieldName: "num", 122 label: "火警數", 123 visible: true 124 }] 125 }] 126 }, 127 title: "火警數", 128 id: "fireLayer" 129 }) 130 131 arcgis_groupLayer.add(arcgis_fireLayer, 0); 132 }) 133 134 $.getJSON("js/json/fault.json", function (data) { 135 var graphics = []; 136 137 data.forEach(function(iSender, i) { 138 graphics.push({ 139 geometry: new Point({ 140 longitude: iSender.longitude, 141 latitude: iSender.latitude 142 }), 143 attributes: { 144 ObjectID: iSender.senderId, 145 title: iSender.senderName, 146 num: iSender.num 147 } 148 }) 149 }, this); 150 151 var arcgis_faultRenderer = new SimpleRenderer({ 152 symbol: new PointSymbol3D({ 153 symbolLayers: [new ObjectSymbol3DLayer({ 154 resource: { 155 primitive: "cube" 156 }, 157 width: 500, 158 depth: 500, 159 material: {color: "#0098d9"} 160 })] 161 }), 162 label: "故障數", 163 visualVariables: [{ 164 type: "size", 165 field: "num", 166 axis: "height" 167 },{ 168 type: "size", 169 axis: "width-and-depth", 170 useSymbolValue: true, 171 }] 172 }); 173 174 var arcgis_faultLayer = new FeatureLayer({ 175 source: graphics, 176 fields: arcgis_fields, 177 objectIdField: "ObjectID", 178 renderer: arcgis_faultRenderer, 179 spatialReference: { 180 wkid: 4326 181 }, 182 geometryType: "point", 183 popupTemplate: { 184 title: "{title}", 185 content: [{ 186 type: "fields", 187 fieldInfos: [{ 188 fieldName: "num", 189 label: "故障數", 190 visible: true 191 }] 192 }] 193 }, 194 title: "故障數", 195 id: "faultLayer", 196 visible: false 197 }) 198 199 arcgis_groupLayer.add(arcgis_faultLayer, 1); 200 }) 201 202 $.getJSON("js/json/linkage.json", function (data) { 203 var graphics = []; 204 205 data.forEach(function(iSender, i) { 206 graphics.push({ 207 geometry: new Point({ 208 longitude: iSender.longitude, 209 latitude: iSender.latitude 210 }), 211 attributes: { 212 ObjectID: iSender.senderId, 213 title: iSender.senderName, 214 num: iSender.num 215 } 216 }) 217 }, this); 218 219 var arcgis_linkageRenderer = new SimpleRenderer({ 220 symbol: new PointSymbol3D({ 221 symbolLayers: [new ObjectSymbol3DLayer({ 222 resource: { 223 primitive: "cube" 224 }, 225 width: 500, 226 depth: 500, 227 material: {color: "#2b821d"} 228 })] 229 }), 230 label: "聯動數", 231 visualVariables: [{ 232 type: "size", 233 field: "num", 234 axis: "height" 235 },{ 236 type: "size", 237 axis: "width-and-depth", 238 useSymbolValue: true, 239 }] 240 }); 241 242 var arcgis_linkageLayer = new FeatureLayer({ 243 source: graphics, 244 fields: arcgis_fields, 245 objectIdField: "ObjectID", 246 renderer: arcgis_linkageRenderer, 247 spatialReference: { 248 wkid: 4326 249 }, 250 geometryType: "point", 251 popupTemplate: { 252 title: "{title}", 253 content: [{ 254 type: "fields", 255 fieldInfos: [{ 256 fieldName: "num", 257 label: "聯動數", 258 visible: true 259 }] 260 }] 261 }, 262 title: "聯動數", 263 id: "linkageLayer", 264 visible: false 265 }) 266 267 arcgis_groupLayer.add(arcgis_linkageLayer, 2); 268 }) 269 }) 270 } 271 272 spatial_echarts_instant.arcgismap = { 273 init: function () { 274 ArcGIS_Map_Init(); 275 }, 276 clear: function (params) { 277 arcgis_view = {}; 278 }, 279 resize: function (params) { 280 281 }, 282 setOption: function (params) { 283 require([ 284 "esri/views/SceneView" 285 ], function (SceneView) { 286 arcgis_view = new SceneView({ 287 map: arcgis_arcgismap, 288 container: "arcgismap", 289 camera: arcgis_initCam 290 }); 291 }) 292 }, 293 changeLayer: function (layerIndex) { 294 var layerId = null; 295 switch (layerIndex) { 296 case 1: 297 layerId = "fireLayer"; 298 break; 299 case 2: 300 layerId = "faultLayer"; 301 break; 302 case 3: 303 layerId = "linkageLayer"; 304 break; 305 default: 306 break; 307 } 308 if (layerId) { 309 var layer = arcgis_groupLayer.findLayerById(layerId); 310 layer.visible = true; 311 } 312 } 313 } 314 315 $(function () { 316 ArcGIS_Map_Init(); 317 })