基于ArcGIS JS API实现的两种距离和面积测量方式

前言

在一些地图地图应用中,距离、面积测量属于基础功能。ArcGIS API for JavaScript有单独提供一个测量的微件,就像鹰眼、比例尺那样拿来就可以用,但是具体效果不是我想要的。之前在项目中有测量这方面的需求,在网上直接找了代码就粘上去了,后来测试的时候发现不能用,经过对比官方API文档,发现其对坐标系还有些限制。因此,根据ArcGIS提供的接口进一步封装了一个测量类,里面提供两种测量方式,一种是采用几何服务(GeometryServer),适用于具备ArcGIS Server环境的项目;另一种是GeometryEngine类,适用于坐标系为**[4326,3857,102100,任意平面坐标系]**之一的项目,这个主要是那些不使用ArcGIS平台,但采用ArcGIS JS API的项目用到。个人更加推荐第一种,不需要考虑坐标系问题,只要有个几何服务(装ArcGIS Server的时候就已经带着了,或者可以使用ArcGIS官方的一个地址,就是有点慢)地址就可以了。

开发思路

  1. 封装一个通用的测量类,提供距离测量、面积测量、结果清除功能;
  2. 提供两种测量方式,当参数中不包含几何服务地址时,采用GeometryEngine测量;
  3. 需要用到Draw工具类,距离测量时需要用到地图单击监听事件,通过记录单击点的坐标计算距离,面积测量双击结束触发draw-complete监听事件,记录绘制的多边形(geometry)计算面积;
  4. 绘制完毕要关掉Draw工具;

主要代码

Measure.js

define([
    "dojo/_base/lang",
    "dojo/_base/declare",
    "dojo/number",
    "esri/layers/GraphicsLayer",
    "esri/toolbars/draw",
    "esri/symbols/TextSymbol",
    "esri/symbols/SimpleMarkerSymbol",
    "esri/symbols/SimpleLineSymbol",
    "esri/symbols/SimpleFillSymbol",
    "esri/symbols/Font",
    "esri/Color",
    "esri/graphic",
    "esri/geometry/Polyline",
    "esri/geometry/Point",
    "esri/geometry/Polygon",
    "esri/geometry/geometryEngine",
    "esri/tasks/AreasAndLengthsParameters",
    "esri/tasks/LengthsParameters",
    "esri/tasks/GeometryService",
], function (
    lang,
    declare,
    number,
    GraphicsLayer,
    Draw,
    TextSymbol,
    SimpleMarkerSymbol,
    SimpleLineSymbol,
    SimpleFillSymbol,
    Font,
    Color,
    Graphic,
    Polyline,
    Point,
    Polygon,
    geometryEngine,
    AreasAndLengthsParameters,
    LengthsParameters,
    GeometryService,
    ) {
        return declare(null, {
            _map: null,
            _measureMethod: "GeometryServer",//默认测量方式为几何服务
            _distanceMeasure: false,//距离测量flag
            _areaMeasure: false,//面积测量
            _options: {
                map: this._map,
                geometryServiceUrl: ''
            },
            _mapClickListener: null,
            _inputPoints: [],
            _totalDistance: 0.00,
            _totalGraphic: null,

            constructor: function (options) {
                lang.mixin(this._options, options);
                this._checkParams(options);
                this._map = this._options.map;
                this._geometryServiceUrl = this._options.geometryServiceUrl;
                //监听地图单击事件
                this._mapClickListener = this._map.on('click', this._mapClickHandler.bind(this));
                this._drawBar = new Draw(this._map);
                this._drawBar.on('draw-complete', this._drawEnd.bind(this))
                this._map.addLayer(new GraphicsLayer({
                    id: "measureLayer"
                }));
                this._font = new Font('12px').setWeight(Font.WEIGHT_BOLD);
                this._defaultMarkerSymbol = new SimpleMarkerSymbol(SimpleMarkerSymbol.STYLE_CIRCLE, 7, 
                    new SimpleLineSymbol(SimpleLineSymbol.STYLE_SOLID, 
                        new Color([255, 0, 0]), 1), new Color([255, 0, 0]));
            },
            //测量距离
            measureDistance: function () {
                this.clearMeasureActionAndGraphics();
                this._distanceMeasure = true;//激活距离测量
                this._totalDistance = 0.00;//总长度重置为0
                this._inputPoints = [];//输入点数组置为空
                this._drawBar.activate(Draw.POLYLINE);
            },
            //地图单击事件处理
            _mapClickHandler: function (evt) {
                if (this._distanceMeasure) {
                    this._distanceMeasureHandler(evt.mapPoint);
                }
            },
            /**
             * 距离测量处理
             * @param mapPoint 单击的点
             */
            _distanceMeasureHandler: function (mapPoint) {
                let me = this;
                this._inputPoints.push(mapPoint);//地图上添加鼠标点击处的点            
                //添加起点
                let textSymbol;
                if (this._inputPoints.length === 1) { //记录第一个点
                    textSymbol = new TextSymbol("起点", this._font, new Color([204, 102, 51]));
                    textSymbol.setOffset(0, -20);
                    this._map.getLayer("measureLayer").add(new Graphic(mapPoint, textSymbol));
                }
                //鼠标点击处添加点,并设置其样式
                this._map.getLayer("measureLayer").add(new Graphic(mapPoint, this._defaultMarkerSymbol));
                if (this._inputPoints.length >= 2) {
                    if (this._measureMethod === "GeometryServer") {
                        //方式一:利用ArcGIS Server的GeometryService服务,适用于具备ArcGIS Server环境的项目
                        let lengthParams = new LengthsParameters();
                        let url = this._geometryServiceUrl;
                        let geometryService = new GeometryService(url);
                        lengthParams.distanceUnit = GeometryService.UNIT_METER;
                        lengthParams.calculationType = "preserveShape";
                        let p1 = this._inputPoints[this._inputPoints.length - 2];
                        let p2 = this._inputPoints[this._inputPoints.length - 1];
                        //同一个点,解决重复添加的bug
                        if (p1.x == p2.x && p1.y == p2.y)
                            return;
                        //在两点之间画线将两点连接起来
                        let polyline = new Polyline(this._map.spatialReference);
                        polyline.addPath([p1, p2]);
                        lengthParams.polylines = [polyline];
                        //根据参数,动态计算长度
                        geometryService.lengths(lengthParams, function (distance) {
                            let _distance = number.format(distance.lengths[0]);//长度单位改为米
                            // debugger;
                            _distance = _distance.replace(",", '');//返回值每3位','隔开
                            me._totalDistance += parseFloat(_distance);//计算总长度
                            let beetwentDistances = _distance + "米";
                            let tdistance = new TextSymbol(beetwentDistances, me._font, new Color([204, 102, 51]));
                            tdistance.setOffset(40, -3);
                            me._map.getLayer("measureLayer").add(new Graphic(p2, tdistance));
                            if (me._totalGraphic) //如果总长度存在的话,就删除总长度Graphic
                                me._map.getLayer("measureLayer").remove(me._totalGraphic);
                            let total = number.format(me._totalDistance, { pattern: "#.000" });//保留三位小数
                            //设置总长度显示样式,并添加到地图上
                            let totalSymbol = new TextSymbol("总长度:" + total + "米", me._font, new Color([204, 102, 51]));
                            totalSymbol.setOffset(40, -20);
                            me._totalGraphic = me._map.getLayer("measureLayer").add(new Graphic(p2, totalSymbol));
                        });
                    } else {
                        //方式二:利用ArcGIS API中自带的GeometryEngine类,适用于地图坐标系(wkid)为3857或4326或平面投影坐标系
                        //设置距离测量的参数
                        let p1 = this._inputPoints[this._inputPoints.length - 2];
                        let p2 = this._inputPoints[this._inputPoints.length - 1];
                        //同一个点,解决重复添加的bug
                        if (p1.x == p2.x && p1.y == p2.y)
                            return;
                        //在两点之间画线将两点连接起来
                        let polyline = new Polyline(this._map.spatialReference);
                        polyline.addPath([p1, p2]);
                        let distance = 0;
                        //根据参数,动态计算长度
                        if (this._map.spatialReference.wkid == "3857" || (this._map.spatialReference.wkid == "102100") || this._map.spatialReference.wkid == "4326") {//在web麦卡托投影和WGS84坐标系下的计算方法
                            distance = geometryEngine.geodesicLength(polyline, "meters");//geodesicArea适用坐标系见官网API
                        } else {//在其他投影坐标系下的计算方法
                            distance = geometryEngine.planarLength(polyline, "meters");//planarArea适用于平面投影坐标系
                        }
        
                        let _distance = number.format(distance / 1000);
                        this._totalDistance += parseFloat(_distance);//计算总长度
                        let beetwentDistances = _distance + "千米";
                        let tdistance = new TextSymbol(beetwentDistances, this._font, new Color([204, 102, 51]));
                        tdistance.setOffset(40, -3);
                        this._map.getLayer("measureLayer").add(new Graphic(p2, tdistance));
                        if (this._totalGraphic) //如果总长度存在的话,就删除总长度Graphic
                            this._map.getLayer("measureLayer").remove(this._totalGraphic);
                        let total = number.format(this._totalDistance, { pattern: "#.000" });//保留三位小数
                        //设置总长度显示样式,并添加到地图上
                        let totalSymbol = new TextSymbol("总长度:" + total + "千米", this._font, new Color([204, 102, 51]));
                        totalSymbol.setOffset(40, -20);
                        this._totalGraphic = this._map.getLayer("measureLayer").add(Graphic(p2, totalSymbol));
                    }
                }
            },
            /**
             * 面积测量,对外暴露
             */
            measureArea: function () {
                this.clearMeasureActionAndGraphics();
                this._areaMeasure = true;
                this._drawBar.activate(Draw.POLYGON);
            },
            /**
             * 绘制结束监听事件
             */
            _drawEnd: function (evt) {
                if (this._distanceMeasure) {
                    this._inputPoints = [];
                }
                if (this._areaMeasure) {
                    let me = this;
                    let geometry = evt.geometry;
                    if (this._measureMethod === "GeometryServer") {
                        //方式一:利用ArcGIS Server的GeometryService服务,适用于具备ArcGIS Server环境的项目
                        let areasAndLengthParams = new AreasAndLengthsParameters();
                        let url = this._geometryServiceUrl;
                        let geometryService = new GeometryService(url);
                        areasAndLengthParams.lengthUnit = GeometryService.UNIT_METER;
                        areasAndLengthParams.areaUnit = GeometryService.UNIT_SQUARE_METERS;//单位改为平方米
                        areasAndLengthParams.calculationType = 'preserveShape';
                        geometryService.simplify([geometry], function (simplifiedGeometries) {
                            areasAndLengthParams.polygons = simplifiedGeometries;
                            geometryService.areasAndLengths(areasAndLengthParams, function (result) {
                                let font = new Font("16px", Font.STYLE_NORMAL, Font.VARIANT_NORMAL, Font.WEIGHT_BOLDER);
                                let lengthsResult = new TextSymbol(number.format(result.areas[0], {
                                    pattern: '#.000'
                                }) + "平方米", font, new Color([204, 102, 51]));
                                let spoint = new Point(evt.geometry.getExtent().getCenter().x, evt.geometry.getExtent().getCenter().y, me._map.spatialReference);
                                me._map.getLayer("measureLayer").add(new Graphic(spoint, lengthsResult)); //在地图上显示测量的面积
                            });
                        });
                    } else {
                        //方式二:利用ArcGIS API中自带的GeometryEngine类,适用于地图坐标系(wkid)为3857或4326或平面投影坐标系
                        let area = 0;
                        if ((geometry.spatialReference.wkid == "4326") || (geometry.spatialReference.wkid == "3857") || (geometry.spatialReference.wkid == "102100")) {
                            area = geometryEngine.geodesicArea(evt.geometry, "square-kilometers");//geodesicArea适用坐标系见官网API
                        } else {
                            area = geometryEngine.planarArea(evt.geometry, "square-kilometers");//planarArea适用于平面投影坐标系
                        }
                        this.drawArea+=parseFloat(area.toFixed(3)); 
                        let font = new Font("18px", Font.STYLE_NORMAL, Font.VARIANT_NORMAL,
                                                            Font.WEIGHT_BOLDER);
                        let areaResult = new TextSymbol(number.format(area, { pattern: '#.000' }) + 
                                                            "平方千米", font, new Color([204, 102, 51]));
                        let spoint = new Point(geometry.getExtent().getCenter().x, geometry.getExtent().getCenter().y, this._map.spatialReference);
                        this._map.getLayer("measureLayer").add(new Graphic(spoint, areaResult));//在地图上显示测量的面积
                    } 
                }
                let resultSymbol;
                switch (evt.geometry.type) {
                    case 'polyline':
                        resultSymbol = new SimpleLineSymbol(SimpleLineSymbol.STYLE_SOLID, new Color([255,0,0,0.8]), 2);
                        break;
                    case 'polygon':
                        resultSymbol = new SimpleFillSymbol(SimpleFillSymbol.STYLE_SOLID, new SimpleLineSymbol(SimpleLineSymbol.STYLE_SOLID, new Color([255, 0, 0]), 2), new Color([255, 255, 0, 0.25]));
                        break;
                }
                this._map.getLayer("measureLayer").add(new Graphic(evt.geometry, resultSymbol));
                this._distanceMeasure = false;
                this._areaMeasure = false;
                this._drawBar.deactivate();
            },
            /**
             * 清除测量动作及图上图形
             */
            clearMeasureActionAndGraphics: function () {
                this._distanceMeasure = false;
                this._areaMeasure = false;
                this._map.getLayer("measureLayer").clear();
                this._drawBar.deactivate();
            },
            /**
             * 参数校验
             */
            _checkParams: function (options) {
                if (!options.map) {
                    throw new Error("参数中必须包含map对象,参数格式:{'map': map}");
                }
                if (!options.geometryServiceUrl) {
                    this._measureMethod = "GeometryEngine";//未传入几何服务地址,改用GeometryEngine类进行测量
                    console.warn("未传入参数'geometryServiceUrl',采用方式2计算,若地图坐标系非['4326','3857','任意平面投影坐标系']之一,可能测量失败");
                }
            },

        })

    });

效果测试

下面通过一个单页面测试一下封装的测量类。

效果图

distance

area

测试页面

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>基于ArcGIS JS API实现的两种距离和面积测量方式</title>
    <link rel="stylesheet" href="https://js.arcgis.com/3.27/esri/css/esri.css">
</head>
<style>
    html,
    body,
    #map {
        height: 100%;
        width: 100%;
        margin: 0;
        padding: 0;
    }
    .horizontal-toolbar{
        position: absolute;
        top: 30px;
        right: 75px;
        /* width: 100px; */
        height: 50px;
        overflow: hidden;
        background-color: white;
        border-radius: 4px;
        z-index: 100;
    }
    .horizontal-toolbar>li{
        position: relative;
        margin: 5px;
        list-style: none; 
        float: left;
        width: 46px;
        height: 46px;
    }
</style>
<script>
    var package_path = window.location.pathname.substring(0, window.location.pathname.lastIndexOf('/'));
    //var package_path = window.location.pathname.replace(/\/[^\/]*$/,"");
    var dojoConfig = {
        // The locationPath logic below may look confusing but all its doing is
        // enabling us to load the api from a CDN and load local modules from the correct location.
        packages: [{
            name: "modules",
            location: package_path + '/modules'
        }]
    };
</script>
<script src="https://js.arcgis.com/3.27/"></script>
<script>
    var map,measureTool;
    require([
        "esri/map",
        "modules/Measure",
        "dojo/domReady!"
    ], function (Map, Measure) {
        map = new Map("map", {
            basemap: 'topo',
            center: [117.02156, 36.65993],
            zoom: 16
        });
        //方式一:使用几何服务
        var options = {
            map: map,
            geometryServiceUrl: 'https://utility.arcgisonline.com/ArcGIS/rest/services/Geometry/GeometryServer'
        };
        //方式二:不传几何服务参数,使用GeometryEngine
        // var options = {
        //     map: map
        // };
        measureTool = new Measure(options);
    });
    function measureDistance () {
        measureTool.measureDistance();
    }
    function measureArea () {
        measureTool.measureArea();
    }
    function clearMeasureActionAndGraphics () {
        measureTool.clearMeasureActionAndGraphics();
    }
</script>
<body>
    <div id="map" style="position:relative;">
        <ul class="horizontal-toolbar">
            <li onclick="measureDistance()">
                <img src="./icons/distance.png" alt="测量距离" title="测量距离">
            </li>
            <li onclick="measureArea()">
                <img src="./icons/area.png" alt="测量面积" title="测量面积">
            </li>
            <li onclick="clearMeasureActionAndGraphics()">
                <img src="./icons/clear.png" alt="清除" title="清除">
            </li>
        </ul>
    </div>
</body>
</html>

开发总结

  1. ArcGIS Server有自带的几何服务,若使用官方几何服务地址,由于网络原因结果可能出来的很慢或直接请求失败;

tool

  1. 使用几何服务距离测量时注意设置calculationType,包括三种:planar(平面),geodesic(曲面),preserveShape(通用),面积测量类似;
  2. 使用GeometryEngine测量时注意区分geodesicArea,planarArea,前者适用于wkid为[4326、3857、102100],后者适用于平面坐标系,如果地图坐标系与使用的测量方法匹配不起来可能会出现错误;

Calculates the area of the input geometry. As opposed to planarArea(), geodesicArea takes into account the curvature of the earth when performing this calculation. Therefore, when using input geometries with a spatial reference of either WGS-84 (wkid: 4326) or Web Mercator Auxiliary Sphere (wkid: 3857), it is best practice to calculate areas using geodesicArea(). If the input geometries have a projected coordinate system other than Web Mercator, use planarArea() instead.

This method only works with WGS-84 (wkid: 4326) and Web Mercator (wkid: 3857) spatial references.

  1. 3857102100表示的是同一个投影坐标系,只是wkid有所不同,3857是较新版本的(latestWkid);
3857 WGS_1984_Web_Mercator_Auxiliary_Sphere

PROJCS[“WGS_1984_Web_Mercator_Auxiliary_Sphere”,GEOGCS[“GCS_WGS_1984”,DATUM[“D_WGS_1984”,SPHEROID[“WGS_1984”,6378137.0,298.257223563]],PRIMEM[“Greenwich”,0.0],UNIT[“Degree”,0.0174532925199433]],PROJECTION[“Mercator_Auxiliary_Sphere”],PARAMETER[“False_Easting”,0.0],PARAMETER[“False_Northing”,0.0],PARAMETER[“Central_Meridian”,0.0],PARAMETER[“Standard_Parallel_1”,0.0],PARAMETER[“Auxiliary_Sphere_Type”,0.0],UNIT[“Meter”,1.0]]

102100 WGS_1984_Web_Mercator_Auxiliary_Sphere

PROJCS[“WGS_1984_Web_Mercator_Auxiliary_Sphere”,GEOGCS[“GCS_WGS_1984”,DATUM[“D_WGS_1984”,SPHEROID[“WGS_1984”,6378137.0,298.257223563]],PRIMEM[“Greenwich”,0.0],UNIT[“Degree”,0.0174532925199433]],PROJECTION[“Mercator_Auxiliary_Sphere”],PARAMETER[“False_Easting”,0.0],PARAMETER[“False_Northing”,0.0],PARAMETER[“Central_Meridian”,0.0],PARAMETER[“Standard_Parallel_1”,0.0],PARAMETER[“Auxiliary_Sphere_Type”,0.0],UNIT[“Meter”,1.0]]

参考链接

  1. ArcGIS官方测量微件:https://developers.arcgis.com/javascript/3/sandbox/sandbox.html?sample=widget_measurement
  2. LengthsParameters计算类型:https://developers.arcgis.com/javascript/3/jsapi/lengthsparameters-amd.html#calculationtype
  3. 坐标系大全:https://developers.arcgis.com/javascript/3/jshelp/pcs.html
  4. https://developers.arcgis.com/javascript/3/jsapi/esri.geometry.geometryengine-amd.html#geodesiclength

源文件下载地址:https://download.csdn.net/download/wml00000/11114680

猜你喜欢

转载自blog.csdn.net/wml00000/article/details/89298114