ArcGIS for JavaScript开发Cluster思路解析

来源:互联网 发布:看源码有什么好处 编辑:程序博客网 时间:2024/05/09 14:50

曾经接到一位朋友的咨询,在Web端我怎么能够加载大量的点数据,而且不至于影响效率。说实话这个问题本身就是一个矛盾,对B/S架构的程序,非常忌讳的就是大数据量的加载,这样势必会影响前段展示的效率,但是如果非要较真这个问题,我觉得可以从以下几个方面入手:

1:建议地理数据库为只读数据,如果是版本化肯定会影响效率,所以需要用户重建空间索引,分析数据,其实也可以在ArcGIS Desktop对相关数据进行测试,确定在桌面端显示速度还是比较能够接受为之,但是要记得,在Web显示肯定比桌面显示效率要低。

2:保证网络的流畅传输,那么带宽和延迟也是需要考虑的

3:如果需要前段渲染,建议尽可能的使用简单符号,毕竟越复杂的符号越影响效率

4:使用Cluster方式变相显示,也就是本博客的主题,Cluster是用点符号的形式显示一个cluster数值,点开这个cluster就会将包含的数值的真实数据进行显示,这样无疑在满足用户业务需求的情况下也可以平衡效率。


当然,大家都知道ArcGIS for Server为了防止用户的大数据量传输,针对每个服务都有1000条记录的限制,测试之前,我们可以将这个限制进行修改。



环境:ArcGIS 10.2.2 for Server、ArcGIS for JS 3.9、ArcSDE10.1

数据:只要包括稍微大量要素的点即可。

该程序代码也是以Esri提供的Sample为例,进行相关的修改。


思路解析:

Sample中Esri提供了一个已经写好的ClusterLayer类,如果熟悉JS的朋友也可以对该类进行扩展,该类的构造函数主要包括以下几个部分:

      //     Optional. Number of pixels to shift a cluster label vertically. Defaults to -5 to align labels with circle symbols. Does not work in IE.      //   resolution:  Number。分辨率,必选      //     Required. Width of a pixel in map coordinates. Example of how to calculate:       //     map.extent.getWidth() / map.width      //   showSingles:  Boolean?点击cluster对象是否显示具体信息,默认为true      //     Optional. Whether or graphics should be displayed when a cluster graphic is clicked. Default is true.      //   singleSymbol:  MarkerSymbol?      //     Marker Symbol (picture or simple). Optional. Symbol to use for graphics that represent single points. Default is a small gray SimpleMarkerSymbol.      //   singleTemplate:  PopupTemplate?      //     PopupTemplate</a>. Optional. Popup template used to format attributes for graphics that represent single points. Default shows all attributes as "attribute = value" (not recommended).      //   maxSingles:  Number?      //     Optional. Threshold for whether or not to show graphics for points in a cluster. Default is 1000.      //   webmap:  Boolean?      //     Optional. Whether or not the map is from an ArcGIS.com webmap. Default is false.      //   spatialReference:  SpatialReference?      //     Optional. Spatial reference for all graphics in the layer. This has to match the spatial reference of the map. Default is 102100. Omit this if the map uses basemaps in web mercator.


相关源代码如下

define([  "dojo/_base/declare",  "dojo/_base/array",  "esri/Color",  "dojo/_base/connect",  "esri/SpatialReference",  "esri/geometry/Point",  "esri/graphic",  "esri/symbols/SimpleMarkerSymbol",  "esri/symbols/TextSymbol",  "esri/dijit/PopupTemplate",  "esri/layers/GraphicsLayer"], function (  declare, arrayUtils, Color, connect,  SpatialReference, Point, Graphic, SimpleMarkerSymbol, TextSymbol,   PopupTemplate, GraphicsLayer) {  return declare([GraphicsLayer], {    constructor: function(options) {      // options:      //   data:  Object[]      //     Array of objects. Required. Object are required to have properties named x, y and attributes. The x and y coordinates have to be numbers that represent a points coordinates.      //   distance:  Number?      //     Optional. The max number of pixels between points to group points in the same cluster. Default value is 50.      //   labelColor:  String?      //     Optional. Hex string or array of rgba values used as the color for cluster labels. Default value is #fff (white).      //   labelOffset:  String?      //     Optional. Number of pixels to shift a cluster label vertically. Defaults to -5 to align labels with circle symbols. Does not work in IE.      //   resolution:  Number      //     Required. Width of a pixel in map coordinates. Example of how to calculate:       //     map.extent.getWidth() / map.width      //   showSingles:  Boolean?      //     Optional. Whether or graphics should be displayed when a cluster graphic is clicked. Default is true.      //   singleSymbol:  MarkerSymbol?      //     Marker Symbol (picture or simple). Optional. Symbol to use for graphics that represent single points. Default is a small gray SimpleMarkerSymbol.      //   singleTemplate:  PopupTemplate?      //     PopupTemplate</a>. Optional. Popup template used to format attributes for graphics that represent single points. Default shows all attributes as "attribute = value" (not recommended).      //   maxSingles:  Number?      //     Optional. Threshold for whether or not to show graphics for points in a cluster. Default is 1000.      //   webmap:  Boolean?      //     Optional. Whether or not the map is from an ArcGIS.com webmap. Default is false.      //   spatialReference:  SpatialReference?      //     Optional. Spatial reference for all graphics in the layer. This has to match the spatial reference of the map. Default is 102100. Omit this if the map uses basemaps in web mercator.            this._clusterTolerance = options.distance || 50;      this._clusterData = options.data || [];      this._clusters = [];      this._clusterLabelColor = options.labelColor || "#000";      // labelOffset can be zero so handle it differently      this._clusterLabelOffset = (options.hasOwnProperty("labelOffset")) ? options.labelOffset : -5;      // graphics that represent a single point      this._singles = []; // populated when a graphic is clicked      this._showSingles = options.hasOwnProperty("showSingles") ? options.showSingles : true;      // symbol for single graphics      var sms = SimpleMarkerSymbol;      this._singleSym = options.singleSymbol || new sms("circle", 6, null, new Color("#888"));      this._singleTemplate = options.singleTemplate || new PopupTemplate({ "title": "", "description": "{*}" });      this._maxSingles = options.maxSingles || 1000;      this._webmap = options.hasOwnProperty("webmap") ? options.webmap : false;      this._sr = options.spatialReference || new SpatialReference({ "wkid": 102100 });      this._zoomEnd = null;    },    // override esri/layers/GraphicsLayer methods     _setMap: function(map, surface) {      // calculate and set the initial resolution      this._clusterResolution = map.extent.getWidth() / map.width; // probably a bad default...      this._clusterGraphics();      // connect to onZoomEnd so data is re-clustered when zoom level changes      this._zoomEnd = connect.connect(map, "onZoomEnd", this, function() {        // update resolution        this._clusterResolution = this._map.extent.getWidth() / this._map.width;        this.clear();        this._clusterGraphics();      });      // GraphicsLayer will add its own listener here      var div = this.inherited(arguments);      return div;    },    _unsetMap: function() {      this.inherited(arguments);      connect.disconnect(this._zoomEnd);    },    // public ClusterLayer methods    add: function(p) {      // Summary:  The argument is a data point to be added to an existing cluster. If the data point falls within an existing cluster, it is added to that cluster and the cluster's label is updated. If the new point does not fall within an existing cluster, a new cluster is created.      //      // if passed a graphic, use the GraphicsLayer's add method      if ( p.declaredClass ) {        this.inherited(arguments);        return;      }      // add the new data to _clusterData so that it's included in clusters      // when the map level changes      this._clusterData.push(p);      var clustered = false;      // look for an existing cluster for the new point      for ( var i = 0; i < this._clusters.length; i++ ) {        var c = this._clusters[i];        if ( this._clusterTest(p, c) ) {          // add the point to an existing cluster          this._clusterAddPoint(p, c);          // update the cluster's geometry          this._updateClusterGeometry(c);          // update the label          this._updateLabel(c);          clustered = true;          break;        }      }      if ( ! clustered ) {        this._clusterCreate(p);        p.attributes.clusterCount = 1;        this._showCluster(p);      }    },    clear: function() {      // Summary:  Remove all clusters and data points.      this.inherited(arguments);      this._clusters.length = 0;    },    clearSingles: function(singles) {      // Summary:  Remove graphics that represent individual data points.      var s = singles || this._singles;      arrayUtils.forEach(s, function(g) {        this.remove(g);      }, this);      this._singles.length = 0;    },    onClick: function(e) {      // remove any previously showing single features      this.clearSingles(this._singles);      // find single graphics that make up the cluster that was clicked      // would be nice to use filter but performance tanks with large arrays in IE      var singles = [];      for ( var i = 0, il = this._clusterData.length; i < il; i++) {        if ( e.graphic.attributes.clusterId == this._clusterData[i].attributes.clusterId ) {          singles.push(this._clusterData[i]);        }      }      if ( singles.length > this._maxSingles ) {        alert("Sorry, that cluster contains more than " + this._maxSingles + " points. Zoom in for more detail.");        return;      } else {        // stop the click from bubbling to the map        e.stopPropagation();        this._map.infoWindow.show(e.graphic.geometry);        this._addSingles(singles);      }    },    // internal methods     _clusterGraphics: function() {      // first time through, loop through the points      for ( var j = 0, jl = this._clusterData.length; j < jl; j++ ) {        // see if the current feature should be added to a cluster        var point = this._clusterData[j];        var clustered = false;        var numClusters = this._clusters.length;        for ( var i = 0; i < this._clusters.length; i++ ) {          var c = this._clusters[i];          if ( this._clusterTest(point, c) ) {            this._clusterAddPoint(point, c);            clustered = true;            break;          }        }        if ( ! clustered ) {          this._clusterCreate(point);        }      }      this._showAllClusters();    },    _clusterTest: function(p, cluster) {      var distance = (        Math.sqrt(          Math.pow((cluster.x - p.x), 2) + Math.pow((cluster.y - p.y), 2)        ) / this._clusterResolution      );      return (distance <= this._clusterTolerance);    },    // points passed to clusterAddPoint should be included     // in an existing cluster    // also give the point an attribute called clusterId     // that corresponds to its cluster    _clusterAddPoint: function(p, cluster) {      // average in the new point to the cluster geometry      var count, x, y;      count = cluster.attributes.clusterCount;      x = (p.x + (cluster.x * count)) / (count + 1);      y = (p.y + (cluster.y * count)) / (count + 1);      cluster.x = x;      cluster.y = y;      // build an extent that includes all points in a cluster      // extents are for debug/testing only...not used by the layer      if ( p.x < cluster.attributes.extent[0] ) {        cluster.attributes.extent[0] = p.x;      } else if ( p.x > cluster.attributes.extent[2] ) {        cluster.attributes.extent[2] = p.x;      }      if ( p.y < cluster.attributes.extent[1] ) {        cluster.attributes.extent[1] = p.y;      } else if ( p.y > cluster.attributes.extent[3] ) {        cluster.attributes.extent[3] = p.y;      }      // increment the count      cluster.attributes.clusterCount++;      // attributes might not exist      if ( ! p.hasOwnProperty("attributes") ) {        p.attributes = {};      }      // give the graphic a cluster id      p.attributes.clusterId = cluster.attributes.clusterId;    },    // point passed to clusterCreate isn't within the     // clustering distance specified for the layer so    // create a new cluster for it    _clusterCreate: function(p) {      var clusterId = this._clusters.length + 1;      // console.log("cluster create, id is: ", clusterId);      // p.attributes might be undefined      if ( ! p.attributes ) {        p.attributes = {};      }      p.attributes.clusterId = clusterId;      // create the cluster      var cluster = {         "x": p.x,        "y": p.y,        "attributes" : {          "clusterCount": 1,          "clusterId": clusterId,          "extent": [ p.x, p.y, p.x, p.y ]        }      };      this._clusters.push(cluster);    },    _showAllClusters: function() {      for ( var i = 0, il = this._clusters.length; i < il; i++ ) {        var c = this._clusters[i];        this._showCluster(c);      }    },    _showCluster: function(c) {      var point = new Point(c.x, c.y, this._sr);      this.add(        new Graphic(          point,           null,           c.attributes        )      );      // code below is used to not label clusters with a single point      if ( c.attributes.clusterCount == 1 ) {        return;      }      // show number of points in the cluster      var label = new TextSymbol(c.attributes.clusterCount)        .setColor(new Color(this._clusterLabelColor))        .setOffset(0, this._clusterLabelOffset);      this.add(        new Graphic(          point,          label,          c.attributes        )      );    },    _addSingles: function(singles) {      // add single graphics to the map      arrayUtils.forEach(singles, function(p) {        var g = new Graphic(          new Point(p.x, p.y, this._sr),          this._singleSym,          p.attributes,          this._singleTemplate        );        this._singles.push(g);        if ( this._showSingles ) {          this.add(g);        }      }, this);      this._map.infoWindow.setFeatures(this._singles);    },    _updateClusterGeometry: function(c) {      // find the cluster graphic      var cg = arrayUtils.filter(this.graphics, function(g) {        return ! g.symbol &&               g.attributes.clusterId == c.attributes.clusterId;      });      if ( cg.length == 1 ) {        cg[0].geometry.update(c.x, c.y);      } else {        console.log("didn't find exactly one cluster geometry to update: ", cg);      }    },    _updateLabel: function(c) {      // find the existing label      var label = arrayUtils.filter(this.graphics, function(g) {        return g.symbol &&                g.symbol.declaredClass == "esri.symbol.TextSymbol" &&               g.attributes.clusterId == c.attributes.clusterId;      });      if ( label.length == 1 ) {        // console.log("update label...found: ", label);        this.remove(label[0]);        var newLabel = new TextSymbol(c.attributes.clusterCount)          .setColor(new Color(this._clusterLabelColor))          .setOffset(0, this._clusterLabelOffset);        this.add(          new Graphic(            new Point(c.x, c.y, this._sr),            newLabel,            c.attributes          )        );        // console.log("updated the label");      } else {        console.log("didn't find exactly one label: ", label);      }    },    // debug only...never called by the layer    _clusterMeta: function() {      // print total number of features      console.log("Total:  ", this._clusterData.length);      // add up counts and print it      var count = 0;      arrayUtils.forEach(this._clusters, function(c) {        count += c.attributes.clusterCount;      });      console.log("In clusters:  ", count);    }  });});

------------------------------------------------------------------

版权所有,文章允许转载,但必须以链接方式注明源地址,否则追究法律责任!

建议看到转载,请直接访问正版链接获得最新的ArcGIS技术文章

Blog:               http://blog.csdn.net/linghe301 

------------------------------------------------------------------


既然最主要的参数是一个带有XY坐标和属性的数组,那么我们从发布的MapService中进行Query,获得相关的记录进行循环处理,生成符合ClusterLayer要求的数组对象即可。

在生成ClusterLayer对象后,使用分级进行渲染,比如不同的颜色代表不同的数据量。


相关源代码如下

<!doctype html><html><head>    <meta charset="utf-8">    <meta name="viewport" content="initial-scale=1, maximum-scale=1,user-scalable=no">    <title>Cluster</title>    <link rel="stylesheet" href="http://js.arcgis.com/3.10/js/dojo/dijit/themes/tundra/tundra.css">    <link rel="stylesheet" href="http://js.arcgis.com/3.10/js/esri/css/esri.css">    <style>        html, body {            height: 100%;            width: 100%;            margin: 0;            padding: 0;        }        #map {            margin: 0;            padding: 0;        }        /* center the image in the popup */        .esriViewPopup .gallery {            margin: 0 auto !important;        }    </style>    <script>        // helpful for understanding dojoConfig.packages vs. dojoConfig.paths:        var dojoConfig = {            paths: {                extras: location.pathname.replace(/\/[^/]+$/, "") + "/extras"            }        };    </script>    <script src="http://js.arcgis.com/3.10/"></script>    <script>        var map;        require([          "dojo/parser",          "dojo/ready",          "dojo/_base/array",          "esri/Color",          "dojo/dom-style",          "dojo/query",          "esri/map",          "esri/request",          "esri/graphic",          "esri/geometry/Extent",          "esri/tasks/query",          "esri/tasks/QueryTask",          "esri/symbols/SimpleMarkerSymbol",          "esri/symbols/SimpleFillSymbol",          "esri/symbols/PictureMarkerSymbol",          "esri/renderers/ClassBreaksRenderer",          "esri/layers/GraphicsLayer",          "esri/dijit/PopupTemplate",          "esri/geometry/Point",          "esri/config",          "extras/ClusterLayer",          "dijit/layout/BorderContainer",          "dijit/layout/ContentPane",          "dojo/domReady!"        ], function (          parser, ready, arrayUtils, Color, domStyle, query,          Map, esriRequest, Graphic, Extent,Query, QueryTask,          SimpleMarkerSymbol, SimpleFillSymbol, PictureMarkerSymbol, ClassBreaksRenderer,          GraphicsLayer, PopupTemplate, Point,esriConfig,          ClusterLayer        ) {            ready(function () {                esriConfig.defaults.io.proxyUrl = "/proxy";                parser.parse();                var clusterLayer;                             map = new Map("map", {                    basemap: "streets",                    center: [116.408386, 39.91395],                    zoom: 12,                });                map.on("load", function () {//使用Query 进行查询要素                    var queryTask = new QueryTask("http://localhost:6080/arcgis/rest/services/JS/MapServer/1");                    var query = new Query();                    query.returnGeometry = true;                    query.outFields = ["*"];                    query.outSpatialReference = { wkid: 102100 };                    query.where = "ObjectID<10000";                    queryTask.execute(query, addClusters);                });                function addClusters(resp) {                    var photoInfo = {};                   //将查询的要素安装ClusterLayer要求的数组对象进行重新组合                        photoInfo.data = arrayUtils.map(resp.features, function (p) {                            var attributes = {                                "address": p.attributes["ADDRESS"],                                "districtna": p.attributes["DISTRICTNA"],                                "name": p.attributes["NAME"],                                "type": p.attributes["TYPE"]                            };                            return {                                "x": p.geometry.x,                                "y": p.geometry.y,                                "attributes": attributes                            };                        });                   // cluster layer that uses OpenLayers style clustering                    clusterLayer = new ClusterLayer({                        "data": photoInfo.data,                        "distance": 100,                        "id": "clusters",                        "labelColor": "#fff",                        "labelOffset": 10,                        "resolution": map.extent.getWidth() / map.width,                        "singleColor": "#888"                                            });                    var defaultSym = new SimpleMarkerSymbol().setSize(4);                    var renderer = new ClassBreaksRenderer(defaultSym, "clusterCount");                    var picBaseUrl = "http://static.arcgis.com/images/Symbols/Shapes/";                    var blue = new PictureMarkerSymbol(picBaseUrl + "BluePin1LargeB.png", 32, 32).setOffset(0, 15);                    var green = new PictureMarkerSymbol(picBaseUrl + "GreenPin1LargeB.png", 64, 64).setOffset(0, 15);                    var red = new PictureMarkerSymbol(picBaseUrl + "RedPin1LargeB.png", 72, 72).setOffset(0, 15);                    renderer.addBreak(0, 2, blue);                    renderer.addBreak(2, 50, green);                    renderer.addBreak(51, 1001, red);                    clusterLayer.setRenderer(renderer);                    map.addLayer(clusterLayer);                    // close the info window when the map is clicked                    map.on("click", cleanUp);                    // close the info window when esc is pressed                    map.on("key-down", function (e) {                        if (e.keyCode === 27) {                            cleanUp();                        }                    });                }                function cleanUp() {                    map.infoWindow.hide();                    clusterLayer.clearSingles();                }                function error(err) {                    console.log("something failed: ", err);                }                 });        });       </script></head><body>    <div data-dojo-type="dijit/layout/BorderContainer"         data-dojo-props="design:'headline',gutters:false"         style="width: 100%; height: 100%; margin: 0;">        <div id="map"             data-dojo-type="dijit/layout/ContentPane"             data-dojo-props="region:'center'">        </div>    </div></body></html>


该代码有几个点需要注意:

1:引用外部JS文件(dojo)

有两种方法都可以引入外部JS

关于两种方法的比较请参考:http://www.sitepen.com/blog/2013/06/20/dojo-faq-what-is-the-difference-packages-vs-paths-vs-aliases/

A:dojoConfig.pahts

 var dojoConfig = {            paths: {                extras: location.pathname.replace(/\/[^/]+$/, "") + "/extras"            }        };
使用location.pathname.replace(/\/[^/]+$/, "")这种正则表达式代表该程序的路径,然后我的js文件放在/extras文件夹中,该js文件以extras标识。那么在引用该js的clusterlayer时,就需要使用 "extras/ClusterLayer",


B:dojoConfig.Packages

 var lo = location.pathname.replace(/\/[^/]+$/, '');        var dojoConfig = {            //baseU            parseOnLoad: true,            packages: [        {            "name": "extras",            "location": lo + "/extras"        }            ]        };
这个其实跟paths比较类似。


2:如果你是经纬度坐标,需要进行转换为平面坐标

var photoInfo = {};            var wgs = new SpatialReference({              "wkid": 4326            });            photoInfo.data = arrayUtils.map(resp, function(p) {              var latlng = new  Point(parseFloat(p.lng), parseFloat(p.lat), wgs);              var webMercator = webMercatorUtils.geographicToWebMercator(latlng);              var attributes = {                "Caption": p.caption,                "Name": p.full_name,                "Image": p.image,                "Link": p.link              };              return {                "x": webMercator.x,                "y": webMercator.y,                "attributes": attributes              };            });

最终的显示效果图



------------------------------------------------------------------

版权所有,文章允许转载,但必须以链接方式注明源地址,否则追究法律责任!

建议看到转载,请直接访问正版链接获得最新的ArcGIS技术文章

Blog:               http://blog.csdn.net/linghe301 

                欢迎添加微信公众号:ArcGIS技术分享(arcgis_share),直接回复1就可以在移动端获取最新技术文章



------------------------------------------------------------------



0 0