├── .gitignore ├── Gruntfile.js ├── bower.json ├── example ├── app1.js ├── app2.js ├── app3.js ├── app4.js ├── app5.js ├── app6.js ├── canvas_example5.html ├── gmaps_example1.html ├── gmaps_example2.html ├── gmaps_example3.html ├── gmaps_example4.html ├── gmaps_example6.html └── main.css ├── graham_scan-qunit.jstd ├── graham_scan.min.js ├── license.txt ├── package.json ├── qunit-lib ├── QUnitAdapter.js └── equiv.js ├── readme.md ├── src └── graham_scan.js └── test └── graham_scan.Test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | bower_components 3 | .idea 4 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | var path = require('path'); 3 | 4 | grunt.loadNpmTasks('grunt-contrib-uglify'); 5 | 6 | grunt.initConfig({ 7 | 8 | uglify: { 9 | options: { 10 | preserveComments: function (info, comment) { 11 | // Only keep the banner comment. 12 | return comment.pos === 0; 13 | } 14 | }, 15 | graham_scan: { 16 | files: { 17 | 'graham_scan.min.js': path.join('src', 'graham_scan.js') 18 | } 19 | } 20 | } 21 | 22 | }); 23 | 24 | grunt.registerTask('build', 'Builds the app into a distributable package.', function() { 25 | grunt.task.run('uglify:graham_scan'); 26 | }); 27 | 28 | }; 29 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graham_scan", 3 | "version": "1.0.5", 4 | "main": "graham_scan.min.js", 5 | "license": "MIT", 6 | "ignore": [ 7 | "**/.*", 8 | "test", 9 | "*.md", 10 | "*.jstd", 11 | ".gitignore", 12 | "example", 13 | "qunit-lib" 14 | ], 15 | "keywords": [ 16 | "convex", 17 | "hull" 18 | ], 19 | "homepage": "http://brian3kb.github.io/graham_scan_js", 20 | "authors": [ 21 | "Brian Barnett " 22 | ], 23 | "description": "Implementation of the Graham Scan algorithm to calculate a convex hull from a given array of x, y coordinates." 24 | } 25 | -------------------------------------------------------------------------------- /example/app1.js: -------------------------------------------------------------------------------- 1 | function initialize() { 2 | 3 | var coords = [ 4 | {'lat' : '48.890609', 'lon' : '11.184313'}, 5 | {'lat' : '48.8167', 'lon' : '11.3667'}, 6 | {'lat' : '48.8333', 'lon' : '11.2167'}, 7 | {'lat' : '48.8', 'lon' : '11.3'}, 8 | {'lat' : '48.7833', 'lon' : '11.2333'}, 9 | {'lat' : '48.8167', 'lon' : '11.3167'}, 10 | {'lat' : '48.85', 'lon' : '11.3167'}, 11 | {'lat' : '48.8', 'lon' : '11.2333'}, 12 | {'lat' : '48.95', 'lon' : '11.2'}, 13 | {'lat' : '48.9', 'lon' : '11.1'}, 14 | {'lat' : '49', 'lon' : '11.2167'}, 15 | {'lat' : '48.9167', 'lon' : '11.3'}, 16 | {'lat' : '48.8333', 'lon' : '11.4167'}, 17 | {'lat' : '48.8667', 'lon' : '11.0667'}, 18 | {'lat' : '48.7667', 'lon' : '11.0667'}, 19 | {'lat' : '48.893175', 'lon' : '10.990565'}, 20 | {'lat' : '48.8833', 'lon' : '11'}, 21 | {'lat' : '48.8', 'lon' : '11.1'}, 22 | {'lat' : '48.88636', 'lon' : '11.198945'}, 23 | {'lat' : '48.872829', 'lon' : '11.373385'}, 24 | {'lat' : '48.86946', 'lon' : '11.00602'} 25 | ]; 26 | 27 | 28 | var centrePoint = new google.maps.LatLng(48.85, 11.25); 29 | 30 | var mapOptions = { 31 | zoom: 10, 32 | center: centrePoint, 33 | mapTypeId: google.maps.MapTypeId.ROADMAP 34 | }; 35 | 36 | var map = new google.maps.Map(document.getElementById('map-canvas'), mapOptions); 37 | 38 | var poly; 39 | var polyHull; 40 | var convexHull = new ConvexHullGrahamScan(); 41 | 42 | 43 | poly = new google.maps.Polygon({ 44 | paths: coords.map(function(item){ 45 | return new google.maps.LatLng(item.lat, item.lon); 46 | }), 47 | strokeColor: '#000', 48 | strokeOpacity: 0.2, 49 | strokeWeight: 2, 50 | fillColor: '#000', 51 | fillOpacity: 0.1 52 | }); 53 | 54 | 55 | coords.forEach(function (item) { 56 | var marker = new google.maps.Marker({ 57 | position: new google.maps.LatLng(item.lat, item.lon), 58 | map: map 59 | }); 60 | convexHull.addPoint(item.lon, item.lat); 61 | }); 62 | 63 | 64 | if (convexHull.points.length > 0) { 65 | var hullPoints = convexHull.getHull(); 66 | 67 | 68 | 69 | //Convert to google latlng objects 70 | hullPoints = hullPoints.map(function (item) { 71 | return new google.maps.LatLng(item.y, item.x); 72 | }); 73 | 74 | console.log(hullPoints); 75 | 76 | polyHull = new google.maps.Polygon({ 77 | paths: hullPoints, 78 | strokeColor: '#000', 79 | strokeOpacity: 0.8, 80 | strokeWeight: 2, 81 | fillColor: '#000', 82 | fillOpacity: 0.35 83 | }); 84 | 85 | polyHull.setMap(map); 86 | 87 | } 88 | } 89 | 90 | google.maps.event.addDomListener(window, 'load', initialize); -------------------------------------------------------------------------------- /example/app2.js: -------------------------------------------------------------------------------- 1 | function initialize() { 2 | 3 | var coords = [ 4 | {'lat' : '50.35', 'lon' : '6.5'}, 5 | {'lat' : '50.2333', 'lon' : '6.41667'}, 6 | {'lat' : '50.1667', 'lon' : '6.38333'}, 7 | {'lat' : '50.1833', 'lon' : '6.35'}, 8 | {'lat' : '50.1833', 'lon' : '6.36667'}, 9 | {'lat' : '50.2333', 'lon' : '6.46667'}, 10 | {'lat' : '50.1333', 'lon' : '6.3'}, 11 | {'lat' : '50.2167', 'lon' : '6.5'}, 12 | {'lat' : '50.1833', 'lon' : '6.26667'}, 13 | {'lat' : '50.1667', 'lon' : '6.51667'}, 14 | {'lat' : '50.301225', 'lon' : '6.33757'}, 15 | {'lat' : '50.1167', 'lon' : '6.35'}, 16 | {'lat' : '50.2667', 'lon' : '6.48333'}, 17 | {'lat' : '50.1333', 'lon' : '6.36667'}, 18 | {'lat' : '50.15', 'lon' : '6.3'}, 19 | {'lat' : '50.15', 'lon' : '6.38333'}, 20 | {'lat' : '50.127949', 'lon' : '6.34825'}, 21 | {'lat' : '50.2833', 'lon' : '6.46667'}, 22 | {'lat' : '50.2667', 'lon' : '6.46667'}, 23 | {'lat' : '50.3333', 'lon' : '6.45'}, 24 | {'lat' : '50.1667', 'lon' : '6.33333'}, 25 | {'lat' : '50.3', 'lon' : '6.48333'}, 26 | {'lat' : '50.2', 'lon' : '6.45'}, 27 | {'lat' : '50.15', 'lon' : '6.5'}, 28 | {'lat' : '50.1333', 'lon' : '6.31667'}, 29 | {'lat' : '50.2', 'lon' : '6.53333'}, 30 | {'lat' : '50.2333', 'lon' : '6.28333'}, 31 | {'lat' : '50.2167', 'lon' : '6.31667'}, 32 | {'lat' : '50.2333', 'lon' : '6.31667'}, 33 | {'lat' : '50.25', 'lon' : '6.3'}, 34 | {'lat' : '50.25', 'lon' : '6.26667'}, 35 | {'lat' : '50.2333', 'lon' : '6.36667'}, 36 | {'lat' : '50.2333', 'lon' : '6.25'}, 37 | {'lat' : '50.35', 'lon' : '6.43333'}, 38 | {'lat' : '50.35', 'lon' : '6.41667'}, 39 | {'lat' : '50.15', 'lon' : '6.43333'}, 40 | {'lat' : '50.1833', 'lon' : '6.45'}, 41 | {'lat' : '50.1333', 'lon' : '6.43333'}, 42 | {'lat' : '50.1667', 'lon' : '6.43333'}, 43 | {'lat' : '50.1333', 'lon' : '6.45'}, 44 | {'lat' : '50.1667', 'lon' : '6.41667'}, 45 | {'lat' : '50.15', 'lon' : '6.41667'}, 46 | {'lat' : '50.213686', 'lon' : '6.212561'}, 47 | {'lat' : '50.1333', 'lon' : '6.16667'}, 48 | {'lat' : '50.1167', 'lon' : '6.16667'}, 49 | {'lat' : '50.136235', 'lon' : '6.253813'}, 50 | {'lat' : '50.1166667', 'lon' : '6.3833333'}, 51 | {'lat' : '50.147869', 'lon' : '6.24628'}, 52 | {'lat' : '50.1167', 'lon' : '6.2'}, 53 | {'lat' : '50.1833', 'lon' : '6.21667'}, 54 | {'lat' : '50.1333', 'lon' : '6.21667'}, 55 | {'lat' : '50.15', 'lon' : '6.23333'}, 56 | {'lat' : '50.137051', 'lon' : '6.21823'}, 57 | {'lat' : '50.1167', 'lon' : '6.3'}, 58 | {'lat' : '50.1', 'lon' : '6.2'}, 59 | {'lat' : '50.1167', 'lon' : '6.18333'}, 60 | {'lat' : '50.1167', 'lon' : '6.21667'}, 61 | {'lat' : '50.151539', 'lon' : '6.24935'}, 62 | {'lat' : '50.1167', 'lon' : '6.38333'}, 63 | {'lat' : '50.1333', 'lon' : '6.38333'}, 64 | {'lat' : '50.0833', 'lon' : '6.33333'}, 65 | {'lat' : '50.091938', 'lon' : '6.363765'}, 66 | {'lat' : '50.1', 'lon' : '6.35'}, 67 | {'lat' : '50.1167', 'lon' : '6.36667'}, 68 | {'lat' : '50.1', 'lon' : '6.36667'}, 69 | {'lat' : '50.05', 'lon' : '6.28333'}, 70 | {'lat' : '49.9667', 'lon' : '6.2'}, 71 | {'lat' : '50.0333', 'lon' : '6.33333'}, 72 | {'lat' : '49.9833', 'lon' : '6.23333'}, 73 | {'lat' : '50', 'lon' : '6.18333'}, 74 | {'lat' : '50.05', 'lon' : '6.26667'}, 75 | {'lat' : '50.0667', 'lon' : '6.16667'}, 76 | {'lat' : '50.057331', 'lon' : '6.29292'}, 77 | {'lat' : '50.0167', 'lon' : '6.23333'}, 78 | {'lat' : '49.9667', 'lon' : '6.16667'}, 79 | {'lat' : '49.995602', 'lon' : '6.24571'}, 80 | {'lat' : '50.0667', 'lon' : '6.33333'}, 81 | {'lat' : '50.02454', 'lon' : '6.26061'}, 82 | {'lat' : '49.9833', 'lon' : '6.26667'}, 83 | {'lat' : '49.9667', 'lon' : '6.25'}, 84 | {'lat' : '50.0167', 'lon' : '6.3'}, 85 | {'lat' : '50.0333', 'lon' : '6.28333'}, 86 | {'lat' : '50.010163', 'lon' : '6.295068'}, 87 | {'lat' : '49.9833', 'lon' : '6.2'}, 88 | {'lat' : '50.0667', 'lon' : '6.21667'}, 89 | {'lat' : '50.03672', 'lon' : '6.15229'}, 90 | {'lat' : '50.0833', 'lon' : '6.18333'}, 91 | {'lat' : '50.0667', 'lon' : '6.25'}, 92 | {'lat' : '50.21764', 'lon' : '6.418925'}, 93 | {'lat' : '50.301225', 'lon' : '6.33757'}, 94 | {'lat' : '50.139037', 'lon' : '6.347223'}, 95 | {'lat' : '50.308023', 'lon' : '6.384857'}, 96 | {'lat' : '50.204374', 'lon' : '6.527217'}, 97 | {'lat' : '50.218556', 'lon' : '6.252401'}, 98 | {'lat' : '50.253096', 'lon' : '6.262949'}, 99 | {'lat' : '50.156949', 'lon' : '6.465449'}, 100 | {'lat' : '50.147959', 'lon' : '6.186598'}, 101 | {'lat' : '50.157677', 'lon' : '6.214198'}, 102 | {'lat' : '50.136235', 'lon' : '6.253813'}, 103 | {'lat' : '49.96503', 'lon' : '6.206825'}, 104 | {'lat' : '49.982153', 'lon' : '6.232356'}, 105 | {'lat' : '49.988421', 'lon' : '6.161605'}, 106 | {'lat' : '50.001905', 'lon' : '6.223443'}, 107 | {'lat' : '50.014097', 'lon' : '6.251456'}, 108 | {'lat' : '50.027203', 'lon' : '6.179071'}, 109 | {'lat' : '50.001262', 'lon' : '6.148231'} 110 | 111 | ]; 112 | 113 | var centrePoint = new google.maps.LatLng(50.15, 6.41); 114 | 115 | var mapOptions = { 116 | zoom: 10, 117 | center: centrePoint, 118 | mapTypeId: google.maps.MapTypeId.ROADMAP 119 | }; 120 | 121 | var map = new google.maps.Map(document.getElementById('map-canvas'), mapOptions); 122 | 123 | var poly; 124 | var polyHull; 125 | var convexHull = new ConvexHullGrahamScan(); 126 | 127 | 128 | poly = new google.maps.Polygon({ 129 | paths: coords.map(function(item){ 130 | return new google.maps.LatLng(item.lat, item.lon); 131 | }), 132 | strokeColor: '#000', 133 | strokeOpacity: 0.2, 134 | strokeWeight: 2, 135 | fillColor: '#000', 136 | fillOpacity: 0.1 137 | }); 138 | 139 | 140 | coords.forEach(function (item) { 141 | var marker = new google.maps.Marker({ 142 | position: new google.maps.LatLng(item.lat, item.lon), 143 | map: map 144 | }); 145 | convexHull.addPoint(item.lon, item.lat); 146 | }); 147 | 148 | 149 | if (convexHull.points.length > 0) { 150 | var hullPoints = convexHull.getHull(); 151 | 152 | 153 | 154 | //Convert to google latlng objects 155 | hullPoints = hullPoints.map(function (item) { 156 | console.log('x:'+item.x + ', y:' + item.y); 157 | return new google.maps.LatLng(item.y, item.x); 158 | }); 159 | 160 | console.log(hullPoints); 161 | 162 | polyHull = new google.maps.Polygon({ 163 | paths: hullPoints, 164 | strokeColor: '#000', 165 | strokeOpacity: 0.8, 166 | strokeWeight: 2, 167 | fillColor: '#000', 168 | fillOpacity: 0.35 169 | }); 170 | 171 | polyHull.setMap(map); 172 | 173 | } 174 | } 175 | 176 | google.maps.event.addDomListener(window, 'load', initialize); -------------------------------------------------------------------------------- /example/app3.js: -------------------------------------------------------------------------------- 1 | function initialize() { 2 | 3 | var coords = [ 4 | {'lat' : '-19.909134', 'lon' : '-43.951838'}, 5 | {'lat' : '-19.909453', 'lon' : '-43.951822'}, 6 | {'lat' : '-19.909564', 'lon' : '-43.952082'}, 7 | {'lat' : '-19.909923', 'lon' : '-43.952578'}, 8 | {'lat' : '-19.909723', 'lon' : '-43.952738'}, 9 | {'lat' : '-19.909943', 'lon' : '-43.952844'}, 10 | {'lat' : '-19.91011', 'lon' : '-43.953016'}, 11 | {'lat' : '-19.910004', 'lon' : '-43.953185'}, 12 | {'lat' : '-19.91009', 'lon' : '-43.953567'}, 13 | {'lat' : '-19.91042', 'lon' : '-43.953295'}, 14 | {'lat' : '-19.910397', 'lon' : '-43.952985'}, 15 | {'lat' : '-19.910472', 'lon' : '-43.952794'}, 16 | {'lat' : '-19.910648', 'lon' : '-43.953102'}, 17 | {'lat' : '-19.910807', 'lon' : '-43.952894'}, 18 | {'lat' : '-19.91116', 'lon' : '-43.952878'}, 19 | {'lat' : '-19.911679', 'lon' : '-43.952534'}, 20 | {'lat' : '-19.911509', 'lon' : '-43.952155'}, 21 | {'lat' : '-19.911337', 'lon' : '-43.95231'}, 22 | {'lat' : '-19.911167', 'lon' : '-43.952136'}, 23 | {'lat' : '-19.911099', 'lon' : '-43.952459'}, 24 | {'lat' : '-19.910926', 'lon' : '-43.952563'}, 25 | {'lat' : '-19.910733', 'lon' : '-43.952234'}, 26 | {'lat' : '-19.910537', 'lon' : '-43.95229'}, 27 | {'lat' : '-19.910503', 'lon' : '-43.95251'}, 28 | {'lat' : '-19.910335', 'lon' : '-43.952121'}, 29 | {'lat' : '-19.909931', 'lon' : '-43.952002'} 30 | ]; 31 | 32 | 33 | var centrePoint = new google.maps.LatLng(-19.910519, -43.952694); 34 | 35 | var mapOptions = { 36 | zoom: 17, 37 | center: centrePoint, 38 | mapTypeId: google.maps.MapTypeId.ROADMAP 39 | }; 40 | 41 | var map = new google.maps.Map(document.getElementById('map-canvas'), mapOptions); 42 | 43 | var poly; 44 | var polyHull; 45 | var convexHull = new ConvexHullGrahamScan(); 46 | 47 | 48 | poly = new google.maps.Polygon({ 49 | paths: coords.map(function(item){ 50 | return new google.maps.LatLng(item.lat, item.lon); 51 | }), 52 | strokeColor: '#000', 53 | strokeOpacity: 0.2, 54 | strokeWeight: 2, 55 | fillColor: '#000', 56 | fillOpacity: 0.1 57 | }); 58 | 59 | 60 | coords.forEach(function (item) { 61 | var marker = new google.maps.Marker({ 62 | position: new google.maps.LatLng(item.lat, item.lon), 63 | map: map 64 | }); 65 | convexHull.addPoint(item.lon, item.lat); 66 | }); 67 | 68 | 69 | if (convexHull.points.length > 0) { 70 | var hullPoints = convexHull.getHull(); 71 | 72 | 73 | 74 | //Convert to google latlng objects 75 | hullPoints = hullPoints.map(function (item) { 76 | return new google.maps.LatLng(item.y, item.x); 77 | }); 78 | 79 | console.log(hullPoints); 80 | 81 | polyHull = new google.maps.Polygon({ 82 | paths: hullPoints, 83 | strokeColor: '#000', 84 | strokeOpacity: 0.8, 85 | strokeWeight: 2, 86 | fillColor: '#000', 87 | fillOpacity: 0.35 88 | }); 89 | 90 | polyHull.setMap(map); 91 | 92 | } 93 | } 94 | 95 | google.maps.event.addDomListener(window, 'load', initialize); -------------------------------------------------------------------------------- /example/app4.js: -------------------------------------------------------------------------------- 1 | function initialize() { 2 | 3 | var coords = [ 4 | {'lat' : '50.157913235507706', 'lon' : '29.900512524414125'}, 5 | {'lat' : '50.74029471119741', 'lon' : '31.146087475586'}, 6 | {'lat' : '50.74029471119741', 'lon' : '29.900512524414125'}, 7 | {'lat' : '50.15791323550770611', 'lon' : '31.146087475586'} 8 | ]; 9 | 10 | var centrePoint = new google.maps.LatLng(50.5, 30.0); 11 | 12 | var mapOptions = { 13 | zoom: 8, 14 | center: centrePoint, 15 | mapTypeId: google.maps.MapTypeId.ROADMAP 16 | }; 17 | 18 | var map = new google.maps.Map(document.getElementById('map-canvas'), mapOptions); 19 | 20 | var poly; 21 | var polyHull; 22 | var convexHull = new ConvexHullGrahamScan(); 23 | 24 | 25 | poly = new google.maps.Polygon({ 26 | paths: coords.map(function(item){ 27 | return new google.maps.LatLng(item.lat, item.lon); 28 | }), 29 | strokeColor: '#000', 30 | strokeOpacity: 0.2, 31 | strokeWeight: 2, 32 | fillColor: '#000', 33 | fillOpacity: 0.1 34 | }); 35 | 36 | 37 | coords.forEach(function (item) { 38 | var marker = new google.maps.Marker({ 39 | position: new google.maps.LatLng(item.lat, item.lon), 40 | map: map 41 | }); 42 | convexHull.addPoint(item.lon, item.lat); 43 | }); 44 | 45 | 46 | if (convexHull.points.length > 0) { 47 | var hullPoints = convexHull.getHull(); 48 | 49 | 50 | 51 | //Convert to google latlng objects 52 | hullPoints = hullPoints.map(function (item) { 53 | return new google.maps.LatLng(item.y, item.x); 54 | }); 55 | 56 | console.log(hullPoints); 57 | 58 | polyHull = new google.maps.Polygon({ 59 | paths: hullPoints, 60 | strokeColor: '#000', 61 | strokeOpacity: 0.8, 62 | strokeWeight: 2, 63 | fillColor: '#000', 64 | fillOpacity: 0.35 65 | }); 66 | 67 | polyHull.setMap(map); 68 | 69 | } 70 | } 71 | 72 | google.maps.event.addDomListener(window, 'load', initialize); -------------------------------------------------------------------------------- /example/app5.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var canvas = document.getElementById('hullCanvas'), 3 | ctx = canvas.getContext('2d'), 4 | convexHull = new ConvexHullGrahamScan(), 5 | points = [ 6 | [2,2], [2,-2], [-2,-2], [-2,2], 7 | [3,3], [3,-3], [-3,-3], [-3,3], 8 | [4,4], [4,-4], [-4,-4], [-4,4], 9 | [5,5], [5,-5], [-5,-5], [-5,5] 10 | ], 11 | hull, 12 | drawPoint = function(point, fill) { 13 | ctx.beginPath(); 14 | ctx.arc((point.x*20)+200, (point.y*20)+200, 3, 0, 3* Math.PI); 15 | if (fill){ 16 | ctx.fillStyle = fill; 17 | ctx.fill(); 18 | } else { 19 | ctx.stroke(); 20 | } 21 | }; 22 | 23 | points.forEach(function(p) { 24 | convexHull.addPoint(p[0], p[1]); 25 | drawPoint({x:p[0], y:p[1]}); 26 | }); 27 | 28 | hull = convexHull.getHull(); 29 | console.log(hull); 30 | 31 | ctx.beginPath(); 32 | ctx.moveTo((hull[0].x*20)+200, (hull[0].y*20)+200); 33 | hull.forEach(function(p) { 34 | ctx.lineTo((p.x*20)+200, (p.y*20)+200); 35 | }); 36 | ctx.lineTo((hull[0].x*20)+200, (hull[0].y*20)+200); 37 | ctx.stroke(); 38 | ctx.closePath(); 39 | 40 | hull.forEach(function(p) { 41 | drawPoint(p, 'green'); 42 | }); 43 | 44 | 45 | })(); -------------------------------------------------------------------------------- /example/app6.js: -------------------------------------------------------------------------------- 1 | function initialize() { 2 | 3 | const coords = [ 4 | { 5 | 'lon': -89.04826539999999, 6 | 'lat': 40.4572814, 7 | }, 8 | { 9 | 'lon': -89.04826539999999, 10 | 'lat': 40.4572814, 11 | }, 12 | { 13 | 'lon': -89.04632170000001, 14 | 'lat': 40.4528266, 15 | }, 16 | { 17 | 'lon': -89.0462646, 18 | 'lat': 40.4524364, 19 | }, 20 | { 21 | 'lon': -89.0462652, 22 | 'lat': 40.4521928, 23 | }, 24 | { 25 | 'lon': -89.0461206, 26 | 'lat': 40.45196019999999, 27 | }, 28 | { 29 | 'lon': -89.0460007, 30 | 'lat': 40.4516839, 31 | }, 32 | { 33 | 'lon': -89.045872, 34 | 'lat': 40.4514006, 35 | }, 36 | { 37 | 'lon': -89.0460978, 38 | 'lat': 40.4530237, 39 | }, 40 | { 41 | 'lon': -89.04557539999999, 42 | 'lat': 40.4529, 43 | }, 44 | { 45 | 'lon': -89.045594, 46 | 'lat': 40.4526814, 47 | }, 48 | { 49 | 'lon': -89.04566489999999, 50 | 'lat': 40.4523262, 51 | }, 52 | { 53 | 'lon': -89.04552729999999, 54 | 'lat': 40.4520635, 55 | }, 56 | { 57 | 'lon': -89.0454006, 58 | 'lat': 40.4518228, 59 | }, 60 | { 61 | 'lon': -89.04035069999999, 62 | 'lat': 40.4511462, 63 | }, 64 | { 65 | 'lon': -89.03999619999999, 66 | 'lat': 40.4512826, 67 | }, 68 | { 69 | 'lon': -89.04089189999999, 70 | 'lat': 40.4515287, 71 | }, 72 | { 73 | 'lon': -89.0401569, 74 | 'lat': 40.4516308, 75 | }, 76 | { 77 | 'lon': -89.0410225, 78 | 'lat': 40.4512711, 79 | }, 80 | { 81 | 'lon': -89.0440267, 82 | 'lat': 40.4530663, 83 | }, 84 | { 85 | 'lon': -89.0434483, 86 | 'lat': 40.4528427, 87 | }, 88 | { 89 | 'lon': -89.04296169999999, 90 | 'lat': 40.45276339999999, 91 | }, 92 | { 93 | 'lon': -89.04245619999999, 94 | 'lat': 40.4526677, 95 | }, 96 | { 97 | 'lon': -89.0419369, 98 | 'lat': 40.4525717, 99 | }, 100 | { 101 | 'lon': -89.0410412, 102 | 'lat': 40.452246, 103 | }, 104 | { 105 | 'lon': -89.04081719999999, 106 | 'lat': 40.4520054, 107 | }, 108 | { 109 | 'lon': -89.0440267, 110 | 'lat': 40.4533845, 111 | }, 112 | { 113 | 'lon': -89.0426086, 114 | 'lat': 40.4532137, 115 | }, 116 | { 117 | 'lon': -89.0422354, 118 | 'lat': 40.4531311, 119 | }, 120 | { 121 | 'lon': -89.04178759999999, 122 | 'lat': 40.4530478, 123 | }, 124 | { 125 | 'lon': -89.0413397, 126 | 'lat': 40.45296460000001, 127 | }, 128 | { 129 | 'lon': -89.0409852, 130 | 'lat': 40.452902, 131 | }, 132 | { 133 | 'lon': -89.0406493, 134 | 'lat': 40.45274, 135 | }, 136 | { 137 | 'lon': -89.04029469999999, 138 | 'lat': 40.4524785, 139 | }, 140 | { 141 | 'lon': -89.0401455, 142 | 'lat': 40.4522386, 143 | }, 144 | { 145 | 'lon': -89.0452517, 146 | 'lat': 40.4514698, 147 | }, 148 | { 149 | 'lon': -89.04486209999999, 150 | 'lat': 40.4510384, 151 | }, 152 | { 153 | 'lon': -89.04441849999999, 154 | 'lat': 40.4511798, 155 | }, 156 | { 157 | 'lon': -89.044176, 158 | 'lat': 40.4510784, 159 | }, 160 | { 161 | 'lon': -89.0440423, 162 | 'lat': 40.4506969, 163 | }, 164 | { 165 | 'lon': -89.04353669999999, 166 | 'lat': 40.4507898, 167 | }, 168 | { 169 | 'lon': -89.0432398, 170 | 'lat': 40.4506543, 171 | }, 172 | { 173 | 'lon': -89.04353669999999, 174 | 'lat': 40.4507898, 175 | }, 176 | { 177 | 'lon': -89.0435677, 178 | 'lat': 40.4504348, 179 | }, 180 | { 181 | 'lon': -89.04395799999999, 182 | 'lat': 40.4502574, 183 | }, 184 | { 185 | 'lon': -89.045814, 186 | 'lat': 40.4511397, 187 | }, 188 | { 189 | 'lon': -89.04547749999999, 190 | 'lat': 40.4508854, 191 | }, 192 | { 193 | 'lon': -89.0456906, 194 | 'lat': 40.4505941, 195 | }, 196 | { 197 | 'lon': -89.0456572, 198 | 'lat': 40.4503141, 199 | }, 200 | { 201 | 'lon': -89.0456143, 202 | 'lat': 40.4500338, 203 | }, 204 | { 205 | 'lon': -89.0451496, 206 | 'lat': 40.4508172, 207 | }, 208 | { 209 | 'lon': -89.0451136, 210 | 'lat': 40.4505815, 211 | }, 212 | { 213 | 'lon': -89.0450795, 214 | 'lat': 40.4503586, 215 | }, 216 | { 217 | 'lon': -89.0442044, 218 | 'lat': 40.4504944, 219 | }, 220 | { 221 | 'lon': -89.044265, 222 | 'lat': 40.4503412, 223 | }, 224 | { 225 | 'lon': -89.0481501, 226 | 'lat': 40.4509718, 227 | }, 228 | { 229 | 'lon': -89.04124639999999, 230 | 'lat': 40.4504773, 231 | }, 232 | { 233 | 'lon': -89.041433, 234 | 'lat': 40.45024009999999, 235 | }, 236 | { 237 | 'lon': -89.0416383, 238 | 'lat': 40.450023, 239 | }, 240 | { 241 | 'lon': -89.0417129, 242 | 'lat': 40.4497054, 243 | }, 244 | { 245 | 'lon': -89.04178759999999, 246 | 'lat': 40.44946729999999, 247 | }, 248 | { 249 | 'lon': -89.0418622, 250 | 'lat': 40.4491496, 251 | }, 252 | { 253 | 'lon': -89.0409852, 254 | 'lat': 40.4507536, 255 | }, 256 | { 257 | 'lon': -89.0404476, 258 | 'lat': 40.4502308, 259 | }, 260 | { 261 | 'lon': -89.04081719999999, 262 | 'lat': 40.4500162, 263 | }, 264 | { 265 | 'lon': -89.04094789999999, 266 | 'lat': 40.4497587, 267 | }, 268 | { 269 | 'lon': -89.0410971, 270 | 'lat': 40.4495212, 271 | }, 272 | { 273 | 'lon': -89.0411158, 274 | 'lat': 40.449223, 275 | }, 276 | { 277 | 'lon': -89.0411904, 278 | 'lat': 40.4489849, 279 | }, 280 | { 281 | 'lon': -89.0411904, 282 | 'lat': 40.4486666, 283 | }, 284 | { 285 | 'lon': -89.0392497, 286 | 'lat': 40.4488893, 287 | }, 288 | { 289 | 'lon': -89.039399, 290 | 'lat': 40.4485722, 291 | }, 292 | { 293 | 'lon': -89.0393244, 294 | 'lat': 40.4483329, 295 | }, 296 | { 297 | 'lon': -89.0394737, 298 | 'lat': 40.4480159, 299 | }, 300 | { 301 | 'lon': -89.0395297, 302 | 'lat': 40.4477577, 303 | }, 304 | { 305 | 'lon': -89.0391751, 306 | 'lat': 40.4494457, 307 | }, 308 | { 309 | 'lon': -89.0392262, 310 | 'lat': 40.4491256, 311 | }, 312 | { 313 | 'lon': -89.0392497, 314 | 'lat': 40.4488893, 315 | }, 316 | { 317 | 'lon': -89.039399, 318 | 'lat': 40.4485722, 319 | }, 320 | { 321 | 'lon': -89.0393244, 322 | 'lat': 40.4483329, 323 | }, 324 | { 325 | 'lon': -89.0394737, 326 | 'lat': 40.4480159, 327 | }, 328 | { 329 | 'lon': -89.0395297, 330 | 'lat': 40.4474792, 331 | }, 332 | { 333 | 'lon': -89.03857789999999, 334 | 'lat': 40.4496794, 335 | }, 336 | { 337 | 'lon': -89.0391751, 338 | 'lat': 40.4494457, 339 | }, 340 | ]; 341 | 342 | const centrePoint = new google.maps.LatLng(40.453, -89.04); 343 | 344 | const mapOptions = { 345 | zoom: 16, 346 | center: centrePoint, 347 | mapTypeId: google.maps.MapTypeId.ROADMAP 348 | }; 349 | 350 | const map = new google.maps.Map(document.getElementById('map-canvas'), 351 | mapOptions); 352 | 353 | const convexHull = new ConvexHullGrahamScan(); 354 | 355 | new google.maps.Polygon({ 356 | paths: coords.map(function(item){ 357 | return new google.maps.LatLng(item.lat, item.lon); 358 | }), 359 | strokeColor: '#000', 360 | strokeOpacity: 0.2, 361 | strokeWeight: 2, 362 | fillColor: '#000', 363 | fillOpacity: 0.1 364 | }); 365 | 366 | coords.forEach( item => { 367 | new google.maps.Marker({ 368 | position: new google.maps.LatLng(item.lat, item.lon), 369 | map: map 370 | }); 371 | convexHull.addPoint(item.lon, item.lat); 372 | }); 373 | 374 | if (convexHull.points.length > 0) { 375 | let hullPoints = convexHull.getHull().map( 376 | item => new google.maps.LatLng(item.y, item.x) 377 | ); 378 | 379 | console.log(hullPoints); 380 | 381 | new google.maps.Polygon({ 382 | paths: hullPoints, 383 | strokeColor: '#000', 384 | strokeOpacity: 0.8, 385 | strokeWeight: 2, 386 | fillColor: '#000', 387 | fillOpacity: 0.35 388 | }).setMap(map); 389 | 390 | } 391 | } 392 | 393 | google.maps.event.addDomListener(window, 'load', initialize); 394 | 395 | -------------------------------------------------------------------------------- /example/canvas_example5.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Graham's Scan Canvas Example 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /example/gmaps_example1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Graham's Scan Google Maps Example 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /example/gmaps_example2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Graham's Scan Google Maps Example 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /example/gmaps_example3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Graham's Scan Google Maps Example 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /example/gmaps_example4.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Graham's Scan Google Maps Example 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /example/gmaps_example6.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Graham's Scan Google Maps Example 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /example/main.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | height: 100%; 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | #map-canvas, #map_canvas { 8 | height: 100%; 9 | font-size: 11px; 10 | } 11 | 12 | @media print { 13 | html, body { 14 | height: auto; 15 | } 16 | 17 | #map_canvas { 18 | height: 650px; 19 | } 20 | } -------------------------------------------------------------------------------- /graham_scan-qunit.jstd: -------------------------------------------------------------------------------- 1 | load: 2 | - qunit-lib/equiv.js 3 | - qunit-lib/QUnitAdapter.js 4 | - src/graham_scan.js 5 | 6 | test: 7 | - test/graham_scan.Test.js -------------------------------------------------------------------------------- /graham_scan.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Graham's Scan Convex Hull Algorithm 3 | * @desc An implementation of the Graham's Scan Convex Hull algorithm in JavaScript. 4 | * @author Brian Barnett, brian@3kb.co.uk, http://brianbar.net/ || http://3kb.co.uk/ 5 | * @version 1.0.5 6 | */ 7 | function ConvexHullGrahamScan(){this.anchorPoint=void 0,this.reverse=!1,this.points=[]}ConvexHullGrahamScan.prototype={constructor:ConvexHullGrahamScan,Point:function(a,b){this.x=a,this.y=b},_findPolarAngle:function(a,b){var c,d,e=57.295779513082;if(!a||!b)return 0;if(c=b.x-a.x,d=b.y-a.y,0==c&&0==d)return 0;var f=Math.atan2(d,c)*e;return this.reverse?0>=f&&(f+=360):f>=0&&(f+=360),f},addPoint:function(a,b){var c=void 0===this.anchorPoint||this.anchorPoint.y>b||this.anchorPoint.y===b&&this.anchorPoint.x>a;c?(void 0!==this.anchorPoint&&this.points.push(new this.Point(this.anchorPoint.x,this.anchorPoint.y)),this.anchorPoint=new this.Point(a,b)):this.points.push(new this.Point(a,b))},_sortPoints:function(){var a=this;return this.points.sort(function(b,c){var d=a._findPolarAngle(a.anchorPoint,b),e=a._findPolarAngle(a.anchorPoint,c);return e>d?-1:d>e?1:0})},_checkPoints:function(a,b,c){var d,e=this._findPolarAngle(a,b),f=this._findPolarAngle(a,c);return e>f?(d=e-f,!(d>180)):f>e?(d=f-e,d>180):!0},getHull:function(){var a,b,c=[];if(this.reverse=this.points.every(function(a){return a.x<0&&a.y<0}),a=this._sortPoints(),b=a.length,3>b)return a.unshift(this.anchorPoint),a;for(c.push(a.shift(),a.shift());;){var d,e,f;if(c.push(a.shift()),d=c[c.length-3],e=c[c.length-2],f=c[c.length-1],this._checkPoints(d,e,f)&&c.splice(c.length-2,1),0==a.length){if(b==c.length){var g=this.anchorPoint;return c=c.filter(function(a){return!!a}),c.some(function(a){return a.x==g.x&&a.y==g.y})||c.unshift(this.anchorPoint),c}a=c,b=a.length,c=[],c.push(a.shift(),a.shift())}}}},"function"==typeof define&&define.amd&&define(function(){return ConvexHullGrahamScan}),"undefined"!=typeof module&&(module.exports=ConvexHullGrahamScan); 8 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Brian Barnett 4 | http://brianbar.net/ 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graham_scan", 3 | "version": "1.0.5", 4 | "description": "Implementation of the Graham Scan algorithm to calculate a convex hull from a given array of x, y coordinates.", 5 | "license": "MIT", 6 | "directories": { 7 | "lib": "src", 8 | "example": "example", 9 | "test": "test" 10 | }, 11 | "keywords": [ 12 | "convex", 13 | "hull" 14 | ], 15 | "homepage": "http://brian3kb.github.io/graham_scan_js", 16 | "authors": [ 17 | "Brian Barnett " 18 | ], 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/brian3kb/graham_scan_js.git" 22 | }, 23 | "devDependencies": { 24 | "bower": "~1.8.8", 25 | "grunt": "^0.4.5", 26 | "grunt-cli": "~0.1.12", 27 | "grunt-contrib-uglify": "^0.5.1" 28 | }, 29 | "bugs": { 30 | "url": "https://github.com/brian3kb/graham_scan_js/issues" 31 | }, 32 | "main": "graham_scan.min.js", 33 | "scripts": { 34 | "test": "graham_scan-qunit.jstd" 35 | }, 36 | "author": "Brian Barnett " 37 | } 38 | -------------------------------------------------------------------------------- /qunit-lib/QUnitAdapter.js: -------------------------------------------------------------------------------- 1 | /* 2 | QUnitAdapter 3 | 4 | Run qunit tests using Google's JS Test Driver. Maps async methods stop() and start(). 5 | 6 | This provides almost the same api as qunit. Extended from original adapter by Karl Okeeffe. 7 | */ 8 | (function() { 9 | if(!(window.equiv)) { 10 | throw new Error("QUnitAdapter.js - Unable to find equiv function. Ensure you have added equiv.js to the load section of your jsTestDriver.conf"); 11 | } 12 | 13 | var QUnitTestCase, lifecycle; 14 | 15 | window.module = function(name, lifecycle_) { 16 | QUnitTestCase = AsyncTestCase(name); 17 | lifecycle = lifecycle_ || {}; 18 | }; 19 | 20 | window.test = function(name, expected, test) { 21 | QUnitTestCase.prototype['test ' + name] = (function(lifecycle) { 22 | return function(q) { 23 | var context = {}, 24 | origStop = window.stop; 25 | 26 | // setup 27 | if (lifecycle.setup) { 28 | lifecycle.setup.call(context); 29 | } 30 | 31 | // expected is an optional argument 32 | if(expected.constructor === Number) { 33 | expectAsserts(expected); 34 | } else { 35 | test = expected; 36 | } 37 | 38 | window.stop = function() { 39 | var capturedAssertions = [], 40 | originalAssertions = [], 41 | assertions = ['ok', 'equal', 'notEqual', 'deepEqual', 'notDeepEqual', 42 | 'strictEqual', 'notStrictEqual', 'raises']; 43 | 44 | for (var i = 0; i < assertions.length; i++) { 45 | originalAssertions[assertions[i]] = window[assertions[i]]; 46 | } 47 | 48 | window.ok = function() { capturedAssertions.push(['ok', arguments]) }; 49 | window.equal = function() { capturedAssertions.push(['equal', arguments]) }; 50 | window.notEqual = function() { capturedAssertions.push(['notEqual', arguments]) }; 51 | window.deepEqual = function() { capturedAssertions.push(['deepEqual', arguments]) }; 52 | window.notDeepEqual = function() { capturedAssertions.push(['notDeepEqual', arguments]) }; 53 | window.strictEqual = function() { capturedAssertions.push(['strictEqual', arguments]) }; 54 | window.notStrictEqual = function() { capturedAssertions.push(['notStrictEqual', arguments]) }; 55 | window.raises = function() { capturedAssertions.push(['raises', arguments]) }; 56 | 57 | // This could be a more efficient way of doing the above, but can't achieve correct scope 58 | // capturedAssertions.push() is called upon function call rather than upon setup, 59 | // so actual values never enter the array, only 'undefined' 60 | /* 61 | for (var i = 0; i < assertions.length; i += 1) { 62 | window[assertions[i]] = function() { 63 | capturedAssertions.push([assertions[i], arguments]); 64 | }; 65 | } 66 | */ 67 | 68 | // Sets up test to resume when `start()` is called. 69 | q.defer('start()', function(pool) { 70 | var origStart = window.start; 71 | 72 | window.start = pool.add(function() { 73 | window.start = origStart; 74 | }); 75 | }); 76 | 77 | // Assertions made in async tests must run in a `defer()` callback. 78 | q.defer('async assertions', function(pool) { 79 | var assertion; 80 | 81 | for (var i = 0; i < assertions.length; i++) { 82 | window[assertions[i]] = originalAssertions[assertions[i]]; 83 | } 84 | 85 | for (var i = 0; i < capturedAssertions.length; i++) { 86 | assertion = capturedAssertions[i]; 87 | window[assertion[0]].apply(null, assertion[1]); 88 | } 89 | }); 90 | }; 91 | 92 | test.call(context); 93 | 94 | window.stop = origStop; 95 | 96 | // teardown 97 | if (lifecycle.teardown) { 98 | lifecycle.teardown.call(context); 99 | } 100 | }; 101 | })(lifecycle); // capture current value of `lifecycle` in new scope 102 | }; 103 | 104 | // wrapper to provide async functionality 105 | window.asyncTest = function(name, expected, test) { 106 | var testFn = function() { 107 | window.stop(); 108 | // expected is an optional argument 109 | test = !test ? expected : test; 110 | test.call(this); 111 | }; 112 | 113 | if (!test) { 114 | window.test(name, testFn); 115 | } else { 116 | window.test(name, expected, testFn); 117 | } 118 | }; 119 | 120 | window.expect = function(count) { 121 | expectAsserts(count); 122 | }; 123 | 124 | window.ok = function(actual, msg) { 125 | assertTrue(msg ? msg : '', !!actual); 126 | }; 127 | 128 | window.equal = function(a, b, msg) { 129 | assertEquals(msg ? msg : '', b, a); 130 | }; 131 | 132 | window.notEqual = function(a, b, msg) { 133 | assertNotEquals(msg ? msg : '', b, a); 134 | }; 135 | 136 | window.deepEqual = function(a, b, msg) { 137 | assertTrue(msg ? msg : '', window.equiv(b, a)); 138 | }; 139 | 140 | window.notDeepEqual = function(a, b, msg) { 141 | assertTrue(msg ? msg : '', !window.equiv(b, a)); 142 | }; 143 | 144 | window.strictEqual = function(a, b, msg) { 145 | assertSame(msg ? msg : '', b, a); 146 | }; 147 | 148 | window.notStrictEqual = function(a, b, msg) { 149 | assertNotSame(msg ? msg : '', b, a); 150 | }; 151 | 152 | // error argument must be a function 153 | window.raises = function(callback, error, msg) { 154 | if(!msg) { 155 | assertException(error, callback); 156 | } else { 157 | assertException(msg, callback, error); 158 | } 159 | } 160 | 161 | // support for depreciated QUnit methods 162 | window.equals = window.equal; 163 | window.same = window.deepEqual; 164 | 165 | window.reset = function() { 166 | fail('reset method is not available when using JS Test Driver'); 167 | }; 168 | 169 | window.isLocal = function() { 170 | return false; 171 | }; 172 | 173 | window.QUnit = { 174 | equiv: window.equiv, 175 | ok: window.ok 176 | }; 177 | 178 | // we need at least a single module to prevent jsTestDriver erroring out with exception 179 | module('Default Module'); 180 | 181 | })(); -------------------------------------------------------------------------------- /qunit-lib/equiv.js: -------------------------------------------------------------------------------- 1 | 2 | // Tests for equality any JavaScript type and structure without unexpected results. 3 | // Discussions and reference: http://philrathe.com/articles/equiv 4 | // Test suites: http://philrathe.com/tests/equiv 5 | // Author: Philippe Rathé 6 | window.equiv = function () { 7 | 8 | var innerEquiv; // the real equiv function 9 | var callers = []; // stack to decide between skip/abort functions 10 | 11 | // Determine what is o. 12 | function hoozit(o) { 13 | if (typeof o === "string") { 14 | return "string"; 15 | 16 | } else if (typeof o === "boolean") { 17 | return "boolean"; 18 | 19 | } else if (typeof o === "number") { 20 | 21 | if (isNaN(o)) { 22 | return "nan"; 23 | } else { 24 | return "number"; 25 | } 26 | 27 | } else if (typeof o === "undefined") { 28 | return "undefined"; 29 | 30 | // consider: typeof null === object 31 | } else if (o === null) { 32 | return "null"; 33 | 34 | // consider: typeof [] === object 35 | } else if (o instanceof Array) { 36 | return "array"; 37 | 38 | // consider: typeof new Date() === object 39 | } else if (o instanceof Date) { 40 | return "date"; 41 | 42 | // consider: /./ instanceof Object; 43 | // /./ instanceof RegExp; 44 | // typeof /./ === "function"; // => false in IE and Opera, 45 | // true in FF and Safari 46 | } else if (o instanceof RegExp) { 47 | return "regexp"; 48 | 49 | } else if (typeof o === "object") { 50 | return "object"; 51 | 52 | } else if (o instanceof Function) { 53 | return "function"; 54 | } 55 | } 56 | 57 | // Call the o related callback with the given arguments. 58 | function bindCallbacks(o, callbacks, args) { 59 | var prop = hoozit(o); 60 | if (prop) { 61 | if (hoozit(callbacks[prop]) === "function") { 62 | return callbacks[prop].apply(callbacks, args); 63 | } else { 64 | return callbacks[prop]; // or undefined 65 | } 66 | } 67 | } 68 | 69 | var callbacks = function () { 70 | 71 | // for string, boolean, number and null 72 | function useStrictEquality(b, a) { 73 | return a === b; 74 | } 75 | 76 | return { 77 | "string": useStrictEquality, 78 | "boolean": useStrictEquality, 79 | "number": useStrictEquality, 80 | "null": useStrictEquality, 81 | "undefined": useStrictEquality, 82 | 83 | "nan": function (b) { 84 | return isNaN(b); 85 | }, 86 | 87 | "date": function (b, a) { 88 | return hoozit(b) === "date" && a.valueOf() === b.valueOf(); 89 | }, 90 | 91 | "regexp": function (b, a) { 92 | return hoozit(b) === "regexp" && 93 | a.source === b.source && // the regex itself 94 | a.global === b.global && // and its modifers (gmi) ... 95 | a.ignoreCase === b.ignoreCase && 96 | a.multiline === b.multiline; 97 | }, 98 | 99 | // - skip when the property is a method of an instance (OOP) 100 | // - abort otherwise, 101 | // initial === would have catch identical references anyway 102 | "function": function () { 103 | var caller = callers[callers.length - 1]; 104 | return caller !== Object && 105 | typeof caller !== "undefined"; 106 | }, 107 | 108 | "array": function (b, a) { 109 | var i; 110 | var len; 111 | 112 | // b could be an object literal here 113 | if ( ! (hoozit(b) === "array")) { 114 | return false; 115 | } 116 | 117 | len = a.length; 118 | if (len !== b.length) { // safe and faster 119 | return false; 120 | } 121 | for (i = 0; i < len; i++) { 122 | if( ! innerEquiv(a[i], b[i])) { 123 | return false; 124 | } 125 | } 126 | return true; 127 | }, 128 | 129 | "object": function (b, a) { 130 | var i; 131 | var eq = true; // unless we can proove it 132 | var aProperties = [], bProperties = []; // collection of strings 133 | 134 | // comparing constructors is more strict than using instanceof 135 | if ( a.constructor !== b.constructor) { 136 | return false; 137 | } 138 | 139 | // stack constructor before traversing properties 140 | callers.push(a.constructor); 141 | 142 | for (i in a) { // be strict: don't ensures hasOwnProperty and go deep 143 | 144 | aProperties.push(i); // collect a's properties 145 | 146 | if ( ! innerEquiv(a[i], b[i])) { 147 | eq = false; 148 | } 149 | } 150 | 151 | callers.pop(); // unstack, we are done 152 | 153 | for (i in b) { 154 | bProperties.push(i); // collect b's properties 155 | } 156 | 157 | // Ensures identical properties name 158 | return eq && innerEquiv(aProperties.sort(), bProperties.sort()); 159 | } 160 | }; 161 | }(); 162 | 163 | innerEquiv = function () { // can take multiple arguments 164 | var args = Array.prototype.slice.apply(arguments); 165 | if (args.length < 2) { 166 | return true; // end transition 167 | } 168 | 169 | return (function (a, b) { 170 | if (a === b) { 171 | return true; // catch the most you can 172 | 173 | } else if (typeof a !== typeof b || a === null || b === null || typeof a === "undefined" || typeof b === "undefined") { 174 | return false; // don't lose time with error prone cases 175 | 176 | } else { 177 | return bindCallbacks(a, callbacks, [b, a]); 178 | } 179 | 180 | // apply transition with (1..n) arguments 181 | })(args[0], args[1]) && arguments.callee.apply(this, args.splice(1, args.length -1)); 182 | }; 183 | 184 | return innerEquiv; 185 | }(); // equiv -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ## JavaScript Graham's Scan Convex Hull Algorithm 2 | 3 | I required a simple implementation to calculate a convex hull from a given array of x, y coordinates, 4 | the convex hull's in js I found either were a little buggy, or required dependencies on other libraries. 5 | This implementation just takes the x,y coordinates, no other libraries are needed. 6 | 7 | These four examples show how to utilise with Google Maps: 8 | 9 | [Example 1](http://brian3kb.github.io/graham_scan_js/pages/gmaps_example1.html) 10 | [Example 2](http://brian3kb.github.io/graham_scan_js/pages/gmaps_example2.html) 11 | [Example 3](http://brian3kb.github.io/graham_scan_js/pages/gmaps_example3.html) 12 | [Example 4](http://brian3kb.github.io/graham_scan_js/pages/gmaps_example4.html) 13 | 14 | View [GitHub pages](http://brian3kb.github.io/graham_scan_js) 15 | 16 | ### Building 17 | 18 | This produces `graham_scan.min.js`: 19 | 20 | npm install 21 | grunt build 22 | 23 | ### Testing 24 | 25 | The source is tested with qUnit, tests executed with Google's JS Test Driver. 26 | 27 | ### Usage 28 | 29 | //Create a new instance. 30 | var convexHull = new ConvexHullGrahamScan(); 31 | 32 | //add points (needs to be done for each point, a foreach loop on the input array can be used.) 33 | convexHull.addPoint(x, y); 34 | 35 | //getHull() returns the array of points that make up the convex hull. 36 | var hullPoints = convexHull.getHull(); 37 | 38 | ### Algorithm 39 | 40 | GRAHAM_SCAN(Q) 41 | Find p0 in Q with minimum y-coordinate (and minimum x-coordinate if there are ties). 42 | Sort the remaining points of Q (that is, Q − {p0}) by polar angle in counterclockwise order with respect to p0. 43 | TOP [S] = 0 ▷ Lines 3-6 initialize the stack to contain, from bottom to top, first three points. 44 | PUSH (p0, S) 45 | PUSH (p1, S) 46 | PUSH (p2, S) 47 | for i = 3 to m ▷ Perform test for each point p3, ..., pm. 48 | do while the angle between NEXT_TO_TOP[S], TOP[S], and pi makes a non-left turn ▷ remove if not a vertex 49 | do POP(S) 50 | PUSH (S, pi) 51 | return S 52 | 53 | ### References 54 | 55 | * http://www.personal.kent.edu/~rmuhamma/Compgeometry/MyCG/ConvexHull/GrahamScan/grahamScan.htm 56 | * http://en.wikipedia.org/wiki/Graham_scan 57 | 58 | ### License 59 | 60 | MIT License 61 | -------------------------------------------------------------------------------- /src/graham_scan.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Graham's Scan Convex Hull Algorithm 3 | * @desc An implementation of the Graham's Scan Convex Hull algorithm in JavaScript. 4 | * @author Brian Barnett, brian@3kb.co.uk, http://brianbar.net/ || http://3kb.co.uk/ 5 | * @version 1.0.5 6 | */ 7 | function ConvexHullGrahamScan() { 8 | this.anchorPoint = undefined; 9 | this.reverse = false; 10 | this.points = []; 11 | } 12 | 13 | ConvexHullGrahamScan.prototype = { 14 | 15 | constructor: ConvexHullGrahamScan, 16 | 17 | Point: function (x, y) { 18 | this.x = x; 19 | this.y = y; 20 | }, 21 | 22 | _findPolarAngle: function (a, b) { 23 | var ONE_RADIAN = 57.295779513082; 24 | var deltaX, deltaY; 25 | 26 | //if the points are undefined, return a zero difference angle. 27 | if (!a || !b) return 0; 28 | 29 | deltaX = (b.x - a.x); 30 | deltaY = (b.y - a.y); 31 | 32 | if (deltaX == 0 && deltaY == 0) { 33 | return 0; 34 | } 35 | 36 | var angle = Math.atan2(deltaY, deltaX) * ONE_RADIAN; 37 | 38 | if (this.reverse){ 39 | if (angle <= 0) { 40 | angle += 360; 41 | } 42 | }else{ 43 | if (angle >= 0) { 44 | angle += 360; 45 | } 46 | } 47 | 48 | return angle; 49 | }, 50 | 51 | addPoint: function (x, y) { 52 | //Check for a new anchor 53 | var newAnchor = 54 | (this.anchorPoint === undefined) || 55 | ( this.anchorPoint.y > y ) || 56 | ( this.anchorPoint.y === y && this.anchorPoint.x > x ); 57 | 58 | if ( newAnchor ) { 59 | if ( this.anchorPoint !== undefined ) { 60 | this.points.push(new this.Point(this.anchorPoint.x, this.anchorPoint.y)); 61 | } 62 | this.anchorPoint = new this.Point(x, y); 63 | } else { 64 | this.points.push(new this.Point(x, y)); 65 | } 66 | }, 67 | 68 | _sortPoints: function () { 69 | var self = this; 70 | 71 | return this.points.sort(function (a, b) { 72 | var polarA = self._findPolarAngle(self.anchorPoint, a); 73 | var polarB = self._findPolarAngle(self.anchorPoint, b); 74 | 75 | if (polarA < polarB) { 76 | return -1; 77 | } 78 | if (polarA > polarB) { 79 | return 1; 80 | } 81 | 82 | return 0; 83 | }); 84 | }, 85 | 86 | _checkPoints: function (p0, p1, p2) { 87 | var difAngle; 88 | var cwAngle = this._findPolarAngle(p0, p1); 89 | var ccwAngle = this._findPolarAngle(p0, p2); 90 | 91 | if (cwAngle > ccwAngle) { 92 | 93 | difAngle = cwAngle - ccwAngle; 94 | 95 | return !(difAngle > 180); 96 | 97 | } else if (cwAngle < ccwAngle) { 98 | 99 | difAngle = ccwAngle - cwAngle; 100 | 101 | return (difAngle > 180); 102 | 103 | } 104 | 105 | return true; 106 | }, 107 | 108 | getHull: function () { 109 | var hullPoints = [], 110 | points, 111 | pointsLength; 112 | 113 | this.reverse = this.points.every(function(point){ 114 | return (point.x < 0 && point.y < 0); 115 | }); 116 | 117 | points = this._sortPoints(); 118 | pointsLength = points.length; 119 | 120 | //If there are less than 3 points, joining these points creates a correct hull. 121 | if (pointsLength < 3) { 122 | points.unshift(this.anchorPoint); 123 | return points; 124 | } 125 | 126 | //move first two points to output array 127 | hullPoints.push(points.shift(), points.shift()); 128 | 129 | //scan is repeated until no concave points are present. 130 | while (true) { 131 | var p0, 132 | p1, 133 | p2; 134 | 135 | hullPoints.push(points.shift()); 136 | 137 | p0 = hullPoints[hullPoints.length - 3]; 138 | p1 = hullPoints[hullPoints.length - 2]; 139 | p2 = hullPoints[hullPoints.length - 1]; 140 | 141 | if (this._checkPoints(p0, p1, p2)) { 142 | hullPoints.splice(hullPoints.length - 2, 1); 143 | } 144 | 145 | if (points.length == 0) { 146 | if (pointsLength == hullPoints.length) { 147 | //check for duplicate anchorPoint edge-case, if not found, add the anchorpoint as the first item. 148 | var ap = this.anchorPoint; 149 | //remove any udefined elements in the hullPoints array. 150 | hullPoints = hullPoints.filter(function(p) { return !!p; }); 151 | if (!hullPoints.some(function(p){ 152 | return(p.x == ap.x && p.y == ap.y); 153 | })) { 154 | hullPoints.unshift(this.anchorPoint); 155 | } 156 | return hullPoints; 157 | } 158 | points = hullPoints; 159 | pointsLength = points.length; 160 | hullPoints = []; 161 | hullPoints.push(points.shift(), points.shift()); 162 | } 163 | } 164 | } 165 | }; 166 | 167 | // EXPORTS 168 | 169 | if (typeof define === 'function' && define.amd) { 170 | define(function() { 171 | return ConvexHullGrahamScan; 172 | }); 173 | } 174 | if (typeof module !== 'undefined') { 175 | module.exports = ConvexHullGrahamScan; 176 | } 177 | -------------------------------------------------------------------------------- /test/graham_scan.Test.js: -------------------------------------------------------------------------------- 1 | module('Point methods'); 2 | 3 | test('Add a single new point',2 , function() { 4 | var testGSHull = new ConvexHullGrahamScan(); 5 | 6 | testGSHull.addPoint(11, 50); 7 | console.log(testGSHull.anchorPoint); 8 | console.log(new testGSHull.Point(11, 50)); 9 | equal(testGSHull.anchorPoint, new testGSHull.Point(11, 50), 10 | 'Tests a point has been added to the points array correctly.'); 11 | testGSHull.addPoint(10, 50); 12 | equal(testGSHull.anchorPoint, new testGSHull.Point(10, 50), 13 | 'Tests that same y value then checks x value for comparison.'); 14 | }); 15 | 16 | test('Sort points',2, function(){ 17 | var samplePoints = [ 18 | {'y' : '48.8', 'x' : '11.3'}, 19 | {'y' : '48.8167', 'x' : '11.3667'}, 20 | {'y' : '48.1', 'x' : '11.1'}, 21 | {'y' : '48.9', 'x' : '11.7'}, 22 | {'y' : '48.7833', 'x' : '11.2333'}]; 23 | 24 | var sortedPoints = [ 25 | {'y' : '48.1', 'x' : '11.1'}, 26 | {'y' : '48.7833', 'x' : '11.2333'}, 27 | {'y' : '48.8', 'x' : '11.3'}, 28 | {'y' : '48.8167', 'x' : '11.3667'}, 29 | {'y' : '48.9', 'x' : '11.7'}]; 30 | 31 | var testGSHull = new ConvexHullGrahamScan(); 32 | testGSHull.points = samplePoints; 33 | testGSHull.anchorPoint = {'x' : 11.1, 'y' : 49.8}; 34 | 35 | testGSHull._sortPoints(); 36 | equal(testGSHull.points, sortedPoints, 37 | 'Tests a collection of points is correctly sorted.'); 38 | 39 | testGSHull.points = [{'x':0,'y':0},{'x':0,'y':0}]; 40 | testGSHull._sortPoints(); 41 | ok(testGSHull.points, 'Test handling points with zero values.'); 42 | }); 43 | 44 | test('Find polar angle outputs a correct calculation',1, function(){ 45 | var testGSHull = new ConvexHullGrahamScan(); 46 | 47 | equal(testGSHull._findPolarAngle({'x': 11.1, 'y': 48.1}, {'x': 11.3 ,'y': 48.8}), 434.0546040990765, 48 | 'Tests the polar angle calculation method is correct.'); 49 | }); 50 | 51 | test('Polar angle point comparison check.',3, function(){ 52 | var testGSHull = new ConvexHullGrahamScan(); 53 | 54 | ok(testGSHull._checkPoints( {'y' : '48.1', 'x' : '11.1'}, 55 | {'y' : '48.7833', 'x' : '11.2333'}, 56 | {'y' : '48.8', 'x' : '11.3'}), 57 | 'Check if last point added results in a concave.'); 58 | 59 | equal(testGSHull._checkPoints( {'y' : '48.1', 'x' : '11.1'}, 60 | {'y' : '48.7833', 'x' : '12.2333'}, 61 | {'y' : '48.8', 'x' : '11.3'}), 62 | false, 63 | 'Check if last point added results in a concave.'); 64 | 65 | equal(testGSHull._checkPoints( {'y' : '48.1', 'x' : '11.1'}, 66 | {'y' : '48.1', 'x' : '-11.1'}, 67 | {'y' : '-48.1', 'x' : '11.1'}), 68 | false, 69 | 'Check if last point added results in a concave.'); 70 | }); 71 | 72 | 73 | module('hull scan'); 74 | 75 | test('Test handling less than 4 points.',1, function(){ 76 | var expectedHull = [{'y' : '48.1', 'x' : '11.1'}, 77 | {'y' : '48.8', 'x' : '11.3'}, 78 | {'y' : '48.7833', 'x' : '11.2333'}]; 79 | var testGSHull = new ConvexHullGrahamScan(); 80 | testGSHull.anchorPoint = {'y' : '48.1', 'x' : '11.1'}; 81 | testGSHull.points = [{'y' : '48.1', 'x' : '11.1'}, 82 | {'y' : '48.8', 'x' : '11.3'}, 83 | {'y' : '48.7833', 'x' : '11.2333'}]; 84 | 85 | equal(testGSHull.getHull(), expectedHull, 'Check same array is returned.'); 86 | }); 87 | 88 | test('Test handling 4 points including a concave point.',1, function(){ 89 | var expectedHull = [{"y":211.41796875,"x":29.2265625},{"y":214.66796875,"x":53.6015625},{"y":223.25,"x":30}]; 90 | var testGSHull = new ConvexHullGrahamScan(); 91 | testGSHull.anchorPoint = {'y' : 223.25, 'x' : 30}; 92 | testGSHull.points = [{'y' : 223.25, 'x' : 30}, 93 | {'y' : 214.66796875, 'x' : 53.6015625}, 94 | {'y' : 213.79296875, 'x' : 38.6015625}, 95 | {'y' : 211.41796875, 'x' : 29.2265625}]; 96 | 97 | equal(testGSHull.getHull(), expectedHull, 'Check output hull is as expected (4 points inc 1 concave).'); 98 | }); 99 | 100 | test('Test hull calculation.',1,function(){ 101 | var expectedHull = [ 102 | {'y':48.7833,'x':11.2333}, 103 | {'y':48.8,'x':11.3}, 104 | {'y':48.8167,'x':11.3667}, 105 | {'y':48.8333,'x':11.4167}, 106 | {'y':48.872829,'x':11.373385}, 107 | {'y':49,'x':11.2167}, 108 | {'y':48.893175,'x':10.990565}, 109 | {'y':48.86946,'x':11.00602}, 110 | {'y':48.8,'x':11.1} 111 | ]; 112 | var testGSHull = new ConvexHullGrahamScan(); 113 | testGSHull.anchorPoint = {'y':48.7833,'x':11.2333}; 114 | testGSHull.points = [ 115 | {'y':48.7833,'x':11.2333}, 116 | {'y':48.8,'x':11.3}, 117 | {'y':48.8167,'x':11.3667}, 118 | {'y':48.8333,'x':11.4167}, 119 | {'y':48.8167,'x':11.3167}, 120 | {'y':48.872829,'x':11.373385}, 121 | {'y':48.85,'x':11.3167}, 122 | {'y':48.9167,'x':11.3}, 123 | {'y':48.8,'x':11.2333}, 124 | {'y':49,'x':11.2167}, 125 | {'y':48.95,'x':11.2}, 126 | {'y':48.8333,'x':11.2167}, 127 | {'y':48.88636,'x':11.198945}, 128 | {'y':48.890609,'x':11.184313}, 129 | {'y':48.9,'x':11.1}, 130 | {'y':48.8667,'x':11.0667}, 131 | {'y':48.893175,'x':10.990565}, 132 | {'y':48.8833,'x':11}, 133 | {'y':48.86946,'x':11.00602}, 134 | {'y':48.8,'x':11.1} 135 | ]; 136 | 137 | equal(testGSHull.getHull(), expectedHull, 'Check output hull is as expected.'); 138 | }); 139 | 140 | test('Test handling 4 points rectangular.',1, function(){ 141 | var expectedHull = [ 142 | {'y':50.157913235507706,'x':29.900512524414125}, 143 | {'y':50.157913235507706,'x':31.146087475586}, 144 | {'y':50.74029471119741,'x':31.146087475586}, 145 | {'y':50.74029471119741,'x':29.900512524414125} 146 | ]; 147 | var testGSHull = new ConvexHullGrahamScan(); 148 | testGSHull.anchorPoint = {'y':50.157913235507706,'x':29.900512524414125}; 149 | testGSHull.points = [ 150 | {'y' : '50.157913235507706', 'x' : '29.900512524414125'}, 151 | {'y' : '50.15791323550770611', 'x' : '31.146087475586'}, 152 | {'y' : '50.74029471119741', 'x' : '31.146087475586'}, 153 | {'y' : '50.74029471119741', 'x' : '29.900512524414125'} 154 | ]; 155 | 156 | equal(testGSHull.getHull(), expectedHull, 'Check output hull is as expected.'); 157 | }); 158 | 159 | test('that collinear points sharing the same polar angle are removed from resultant hull.',1, function(){ 160 | var expectedHull = [ 161 | {'y':-5,'x':-5}, 162 | {'y':-5,'x':5}, 163 | {'y':5,'x':5}, 164 | {'y':5,'x':-5} 165 | ]; 166 | var testGSHull = new ConvexHullGrahamScan(); 167 | testGSHull.anchorPoint = {'y':-5,'x':-5}; 168 | testGSHull.points = [ 169 | {'y' : '2', 'x' : '2'}, 170 | {'y' : '-2', 'x' : '2'}, 171 | {'y' : '-2', 'x' : '-2'}, 172 | {'y' : '2', 'x' : '-2'}, 173 | {'y' : '3', 'x' : '3'}, 174 | {'y' : '-3', 'x' : '3'}, 175 | {'y' : '-3', 'x' : '-3'}, 176 | {'y' : '3', 'x' : '-3'}, 177 | {'y' : '4', 'x' : '4'}, 178 | {'y' : '-4', 'x' : '4'}, 179 | {'y' : '-4', 'x' : '-4'}, 180 | {'y' : '4', 'x' : '-4'}, 181 | {'y' : '5', 'x' : '5'}, 182 | {'y' : '-5', 'x' : '5'}, 183 | {'y' : '-5', 'x' : '-5'}, 184 | {'y' : '5', 'x' : '-5'} 185 | ]; 186 | 187 | equal(testGSHull.getHull(), expectedHull, 'Check output hull is as expected.'); 188 | }); 189 | 190 | test('hull culculation for a larger set of bigger numbers.', 1, function() { 191 | var points = [ 192 | {'x': 466, 'y': 231},{'x': 469, 'y': 228},{'x': 472, 'y': 226},{'x': 476, 'y': 223},{'x': 479, 'y': 221}, 193 | {'x': 483, 'y': 219},{'x': 486, 'y': 216},{'x': 489, 'y': 214},{'x': 492, 'y': 211},{'x': 495, 'y': 209}, 194 | {'x': 499, 'y': 207},{'x': 503, 'y': 205},{'x': 506, 'y': 203},{'x': 510, 'y': 201},{'x': 513, 'y': 200}, 195 | {'x': 517, 'y': 199},{'x': 521, 'y': 197},{'x': 525, 'y': 196},{'x': 529, 'y': 194},{'x': 532, 'y': 193}, 196 | {'x': 536, 'y': 191},{'x': 540, 'y': 190},{'x': 544, 'y': 189},{'x': 548, 'y': 188},{'x': 552, 'y': 187}, 197 | {'x': 556, 'y': 187},{'x': 563, 'y': 185},{'x': 567, 'y': 184},{'x': 571, 'y': 184},{'x': 575, 'y': 183}, 198 | {'x': 579, 'y': 183},{'x': 583, 'y': 183},{'x': 587, 'y': 183},{'x': 591, 'y': 183},{'x': 595, 'y': 184}, 199 | {'x': 599, 'y': 185},{'x': 602, 'y': 187},{'x': 606, 'y': 189},{'x': 610, 'y': 191},{'x': 613, 'y': 193}, 200 | {'x': 617, 'y': 195},{'x': 620, 'y': 198},{'x': 623, 'y': 200},{'x': 627, 'y': 202},{'x': 629, 'y': 205}, 201 | {'x': 633, 'y': 207},{'x': 636, 'y': 210},{'x': 639, 'y': 213},{'x': 642, 'y': 215},{'x': 647, 'y': 219}, 202 | {'x': 650, 'y': 222},{'x': 653, 'y': 225},{'x': 656, 'y': 228},{'x': 658, 'y': 231},{'x': 661, 'y': 234}, 203 | {'x': 663, 'y': 237},{'x': 666, 'y': 240},{'x': 667, 'y': 244},{'x': 669, 'y': 248},{'x': 671, 'y': 251}, 204 | {'x': 673, 'y': 252},{'x': 674, 'y': 256},{'x': 675, 'y': 258} 205 | ]; 206 | 207 | var expectedHull = [ 208 | {'x': 575, 'y': 183},{'x': 587, 'y': 183},{'x': 591, 'y': 183},{'x': 599, 'y': 185},{'x': 610, 'y': 191}, 209 | {'x': 617, 'y': 195},{'x': 627, 'y': 202},{'x': 633, 'y': 207},{'x': 647, 'y': 219},{'x': 656, 'y': 228}, 210 | {'x': 666, 'y': 240},{'x': 673, 'y': 252},{'x': 675, 'y': 258},{'x': 466, 'y': 231},{'x': 469, 'y': 228}, 211 | {'x': 492, 'y': 211},{'x': 495, 'y': 209},{'x': 506, 'y': 203},{'x': 510, 'y': 201},{'x': 536, 'y': 191}, 212 | {'x': 552, 'y': 187},{'x': 567, 'y': 184} 213 | ]; 214 | 215 | var testGSHull = new ConvexHullGrahamScan(); 216 | 217 | points.forEach( function(p){ testGSHull.addPoint(p['x'], p['y']); } ); 218 | 219 | equal(testGSHull.getHull(), expectedHull, 'Check output hull is as expected.'); 220 | }); 221 | --------------------------------------------------------------------------------