├── .gitignore ├── Gruntfile.js ├── package.json ├── LICENSE.md ├── test.conf.js ├── README.md ├── test └── test.js └── src └── util.calculateArea.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | 3 | // Project config 4 | grunt.initConfig({ 5 | pkg: grunt.file.readJSON('package.json'), 6 | concat: { 7 | build: { 8 | src: [ 9 | 'src/util.calculateArea.js' 10 | ], 11 | dest: 'build/util.calculateArea.js' 12 | } 13 | }, 14 | uglify: { 15 | build: { 16 | src: 'build/util.calculateArea.js', 17 | dest: 'build/util.calculateArea.min.js' 18 | } 19 | }, 20 | watch: { 21 | files: 'src/*.js', 22 | tasks: 'default' 23 | } 24 | }); 25 | 26 | // Load the plugin 27 | grunt.loadNpmTasks('grunt-contrib-concat'); 28 | grunt.loadNpmTasks('grunt-contrib-uglify'); 29 | grunt.loadNpmTasks('grunt-contrib-watch'); 30 | 31 | // Default task 32 | grunt.registerTask('default', ['concat', 'uglify', 'watch']); 33 | 34 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Pereskokova Marina ", 3 | "name": "mapsapi-area", 4 | "description": "Yandex Maps API plugin for calculating geodesic features area", 5 | "version": "0.0.1", 6 | "main": "build/util.calculateArea.js", 7 | "homepage": "https://github.com/yandex/mapsapi-area", 8 | "keywords": [ 9 | "yandex", 10 | "maps" 11 | ], 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/yandex/mapsapi-area.git" 15 | }, 16 | "devDependencies": { 17 | "chai": "4.0.2", 18 | "grunt": "1.0.1", 19 | "grunt-contrib-concat": "1.0.1", 20 | "grunt-contrib-uglify": "3.0.1", 21 | "grunt-contrib-watch": "1.0.0", 22 | "jscs": "1.8.1", 23 | "karma": "0.12.21", 24 | "karma-phantomjs-launcher": "1.0.4", 25 | "karma-mocha": "0.1.7", 26 | "mocha": "3.4.2", 27 | "phantomjs": "^1.9.18" 28 | }, 29 | "scripts": { 30 | "lint": "jscs -p yandex src", 31 | "test": "./node_modules/karma/bin/karma start test.conf.js" 32 | } 33 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017, YANDEX LLC 2 | Copyright 2005-2013 OpenLayers Contributors. All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation and/or 12 | other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY OPENLAYERS CONTRIBUTORS ``AS IS'' AND ANY EXPRESS 15 | OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 16 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 17 | SHALL COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 18 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 19 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 20 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 21 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 22 | OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 23 | ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /test.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | 3 | module.exports = function(config) { 4 | config.set({ 5 | 6 | // base path that will be used to resolve all patterns (eg. files, exclude) 7 | basePath: './', 8 | 9 | 10 | // frameworks to use 11 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 12 | frameworks: ['mocha'], 13 | 14 | 15 | // list of files / patterns to load in the browser 16 | files: [ 17 | 'node_modules/chai/chai.js', 18 | 'http://api-maps.yandex.ru/2.1/?lang=ru-RU', 19 | {pattern: 'src/*.js', include: true}, 20 | {pattern: 'test/*.js', include: true} 21 | ], 22 | 23 | 24 | // list of files to exclude 25 | exclude: [], 26 | 27 | 28 | // preprocess matching files before serving them to the browser 29 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 30 | preprocessors: {}, 31 | 32 | 33 | // test results reporter to use 34 | // possible values: 'dots', 'progress' 35 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 36 | reporters: ['progress'], 37 | 38 | 39 | // web server port 40 | port: 9876, 41 | 42 | 43 | // enable / disable colors in the output (reporters and logs) 44 | colors: true, 45 | 46 | 47 | // level of logging 48 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 49 | logLevel: config.LOG_INFO, 50 | 51 | 52 | // enable / disable watching file and executing tests whenever any file changes 53 | autoWatch: true, 54 | 55 | 56 | // start these browsers 57 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 58 | browsers: ['PhantomJS'], 59 | 60 | 61 | // Continuous Integration mode 62 | // if true, Karma captures browsers, runs the tests and exits 63 | singleRun: false 64 | }); 65 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Yandex Maps API area calculation plugin. 2 | 3 | `util.calculateArea` module allows to calculate the area of polygons, rectangles and circles. 4 | Note that plugin works only with geodesic geometries. 5 | 6 | ## Usage 7 | 8 | 1. Load both [Yandex Maps JS API 2.1](https://tech.yandex.com/maps/doc/jsapi/2.1/quick-start/index-docpage/) and module source code by adding following code into <head> section of your page 9 | ```html 10 | 11 | 12 | 13 | ``` 14 | 15 | 2. Wait for both API and module loaded 16 | ```js 17 | ymaps.ready(['util.calculateArea']).then(function () { 18 | var myPolygon = new ymaps.Polygon(someCoordinates); 19 | // You can calculate area of any type of ymaps.GeoObject. 20 | var area = ymaps.util.calculateArea(myPolygon); 21 | 22 | // Or you can calculate area of GeoJson feature. 23 | var areaFromJson = ymaps.util.calculateArea({ 24 | type: 'Feature', 25 | geometry: { 26 | type: 'Rectangle', 27 | coordinates: someRectangleCoordinates 28 | } 29 | }); 30 | }); 31 | ``` 32 | 33 | Note: module definition uses standard Yandex Maps API namespace 'ymaps'. 34 | If you are using custom namespace, you need to fork and rebuild module for your needs. 35 | 36 | ## util.calculateArea(geoObject) 37 | geoObject descibed using one of following formats: 38 | 47 | 48 | Returns geoObject area in square meters. 49 | 50 | ## For contributors 51 | 52 | If you want to make a pull-request, run tests and check code style first. 53 | 54 | ```js 55 | npm install 56 | npm run-script lint 57 | npm test 58 | ``` 59 | 60 | ## Third party components 61 | 62 | The views and conclusions contained in the software and documentation are those 63 | of the authors and should not be interpreted as representing official policies, 64 | either expressed or implied, of OpenLayers Contributors. 65 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | var expect = chai.expect; 2 | 3 | describe('util.calculateArea', function() { 4 | var coordinates = [[55.76, 37.64]], 5 | polygonSideLength = 600, 6 | polygonArea = Math.pow(polygonSideLength, 2); 7 | 8 | before(function (done) { 9 | ymaps.ready(['util.calculateArea']).then(function () { 10 | coordinates.push(moveToDistance(coordinates[0], [1, 0], polygonSideLength)); 11 | coordinates.push(moveToDistance(coordinates[1], [0, 1], polygonSideLength)); 12 | coordinates.push(moveToDistance(coordinates[2], [-1, 0], polygonSideLength)); 13 | coordinates.push(moveToDistance(coordinates[3], [0, -1], polygonSideLength)); 14 | coordinates.push(coordinates[0]); 15 | done(); 16 | }) 17 | }); 18 | 19 | it('should calculate ymaps.Polygon area', function() { 20 | var polygon = new ymaps.Polygon([coordinates]), 21 | area = ymaps.util.calculateArea(polygon); 22 | expect(getRelativeErrorPercent(area, polygonArea)).to.be.below(0.5); 23 | }); 24 | 25 | it('should calculate ymaps.Rectangle area', function() { 26 | var rectangle = new ymaps.Rectangle([coordinates[0], coordinates[2]]), 27 | area = ymaps.util.calculateArea(rectangle); 28 | expect(getRelativeErrorPercent(area, polygonArea)).to.be.below(0.5); 29 | }); 30 | 31 | it('should calculate polygon feature area', function () { 32 | var polygon = { 33 | type: 'Feature', 34 | geometry: { 35 | type: 'Polygon', 36 | coordinates: [coordinates] 37 | } 38 | }, 39 | area = ymaps.util.calculateArea(polygon); 40 | expect(getRelativeErrorPercent(area, polygonArea)).to.be.below(0.5); 41 | }); 42 | 43 | it('should calculate rectangle feature area', function () { 44 | var polygon = { 45 | type: 'Feature', 46 | geometry: { 47 | type: 'Rectangle', 48 | coordinates: [coordinates[0], coordinates[2]] 49 | } 50 | }, 51 | area = ymaps.util.calculateArea(polygon); 52 | expect(getRelativeErrorPercent(area, polygonArea)).to.be.below(0.5); 53 | }); 54 | 55 | it('should return 0 for lineString', function () { 56 | var area = ymaps.util.calculateArea({ 57 | type: 'Feature', 58 | geometry: { 59 | coordinates: coordinates, 60 | type: 'LineString' 61 | } 62 | }); 63 | expect(area).to.be.eql(0); 64 | }); 65 | 66 | it('should return geojson circle area', function () { 67 | var area = ymaps.util.calculateArea({ 68 | type: 'Feature', 69 | geometry: { 70 | coordinates: coordinates[0], 71 | type: 'Circle', 72 | radius: 200 73 | } 74 | }); 75 | expect(area).to.be.eql(Math.PI * Math.pow(200, 2)); 76 | }); 77 | 78 | it('should return circle area', function () { 79 | var area = ymaps.util.calculateArea(new ymaps.Circle([coordinates[0], 200])); 80 | expect(area).to.be.eql(Math.PI * Math.pow(200, 2)); 81 | }); 82 | 83 | function moveToDistance(basePoint, direction, distance) { 84 | return ymaps.coordSystem.geo.solveDirectProblem(basePoint, direction, distance).endPoint; 85 | } 86 | 87 | function getRelativeErrorPercent(result, standard) { 88 | return Math.abs((result - standard) / standard) * 100; 89 | } 90 | }); -------------------------------------------------------------------------------- /src/util.calculateArea.js: -------------------------------------------------------------------------------- 1 | ymaps.modules.define('util.calculateArea', [], function (provide) { 2 | // Equatorial radius of Earth 3 | var RADIUS = 6378137; 4 | 5 | function calculateArea(feature) { 6 | var geoJsonGeometry = getGeoJsonGeometry(feature); 7 | return calculateJsonGeometryArea(geoJsonGeometry); 8 | } 9 | 10 | function getGeoJsonGeometry(feature) { 11 | if (feature.type == 'Feature') { 12 | return feature.geometry; 13 | } else if (feature.geometry && feature.geometry.getType) { 14 | if (feature.geometry.getType() == 'Circle') { 15 | return { 16 | type: 'Circle', 17 | coordinates: feature.geometry.getCoordinates(), 18 | radius: feature.geometry.getRadius() 19 | } 20 | } 21 | return { 22 | type: feature.geometry.getType(), 23 | coordinates: feature.geometry.getCoordinates() 24 | } 25 | } else { 26 | throw new Error('util.calculateArea: Unknown input object.'); 27 | } 28 | } 29 | 30 | function calculateJsonGeometryArea(geometry) { 31 | var area = 0; 32 | var i; 33 | switch (geometry.type) { 34 | case 'Polygon': 35 | return polygonArea(geometry.coordinates); 36 | case 'MultiPolygon': 37 | for (i = 0; i < geometry.coordinates.length; i++) { 38 | area += polygonArea(geometry.coordinates[i]); 39 | } 40 | return area; 41 | case 'Rectangle': 42 | return polygonArea([[ 43 | geometry.coordinates[0], 44 | [geometry.coordinates[0][0], geometry.coordinates[1][1]], 45 | geometry.coordinates[1], 46 | [geometry.coordinates[1][0], geometry.coordinates[0][1]], 47 | geometry.coordinates[0] 48 | ]]); 49 | case 'Circle': 50 | return Math.PI * Math.pow(geometry.radius, 2); 51 | case 'Point': 52 | case 'MultiPoint': 53 | case 'LineString': 54 | case 'MultiLineString': 55 | return 0; 56 | } 57 | } 58 | 59 | function polygonArea(coords) { 60 | var area = 0; 61 | if (coords && coords.length > 0) { 62 | area += Math.abs(ringArea(coords[0])); 63 | for (var i = 1; i < coords.length; i++) { 64 | area -= Math.abs(ringArea(coords[i])); 65 | } 66 | } 67 | return area; 68 | } 69 | 70 | /** 71 | * Modified version of https://github.com/mapbox/geojson-area 72 | * Calculate the approximate area of the polygon were it projected onto 73 | * the earth. Note that this area will be positive if ring is oriented 74 | * clockwise, otherwise it will be negative. 75 | * 76 | * Reference: 77 | * Robert. G. Chamberlain and William H. Duquette, "Some Algorithms for 78 | * Polygons on a Sphere", JPL Publication 07-03, Jet Propulsion 79 | * Laboratory, Pasadena, CA, June 2007 https://trs.jpl.nasa.gov/handle/2014/40409 80 | * 81 | * Returns: 82 | * {Number} The approximate signed geodesic area of the polygon in square 83 | * meters. 84 | */ 85 | 86 | function ringArea(coords) { 87 | var p1; 88 | var p2; 89 | var p3; 90 | var lowerIndex; 91 | var middleIndex; 92 | var upperIndex; 93 | var i; 94 | var area = 0; 95 | var coordsLength = coords.length; 96 | var longitude = ymaps.meta.coordinatesOrder == 'latlong' ? 1 : 0; 97 | var latitude = ymaps.meta.coordinatesOrder == 'latlong' ? 0 : 1; 98 | 99 | if (coordsLength > 2) { 100 | for (i = 0; i < coordsLength; i++) { 101 | // i = N-2 102 | if (i === coordsLength - 2) { 103 | lowerIndex = coordsLength - 2; 104 | middleIndex = coordsLength - 1; 105 | upperIndex = 0; 106 | } else if (i === coordsLength - 1) { 107 | // i = N-1 108 | lowerIndex = coordsLength - 1; 109 | middleIndex = 0; 110 | upperIndex = 1; 111 | } else { 112 | // i = 0 to N-3 113 | lowerIndex = i; 114 | middleIndex = i + 1; 115 | upperIndex = i + 2; 116 | } 117 | p1 = coords[lowerIndex]; 118 | p2 = coords[middleIndex]; 119 | p3 = coords[upperIndex]; 120 | area += (rad(p3[longitude]) - rad(p1[longitude])) * Math.sin(rad(p2[latitude])); 121 | } 122 | 123 | area = area * RADIUS * RADIUS / 2; 124 | } 125 | 126 | return area; 127 | } 128 | 129 | function rad(_) { 130 | return _ * Math.PI / 180; 131 | } 132 | 133 | provide(calculateArea); 134 | }); 135 | --------------------------------------------------------------------------------