├── .travis.yml ├── .gitignore ├── fixtures └── line.geojson ├── package.json ├── LICENSE ├── README.md └── index.js /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.8" 4 | - "0.10" 5 | before_install: 6 | - npm install -g npm@~1.4.6 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Deployed apps should consider commenting this line out: 24 | # see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git 25 | node_modules 26 | -------------------------------------------------------------------------------- /fixtures/line.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "features": [ 4 | { 5 | "type": "Feature", 6 | "properties": {}, 7 | "geometry": { 8 | "type": "LineString", 9 | "coordinates": [ 10 | [ 11 | -100.5084228515625, 12 | 37.400710068740565 13 | ], 14 | [ 15 | -100.60729980468749, 16 | 38.655488159953 17 | ], 18 | [ 19 | -98.7176513671875, 20 | 40.28371627054261 21 | ], 22 | [ 23 | -96.5423583984375, 24 | 40.07807142745009 25 | ] 26 | ] 27 | } 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "geojson-cover", 3 | "version": "0.1.5", 4 | "description": "generates an s2 cover from geojson", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "node test.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/mapbox/geojson-cover.git" 12 | }, 13 | "keywords": [ 14 | "s2", 15 | "node-s2", 16 | "cover", 17 | "geojson" 18 | ], 19 | "author": "", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/mapbox/geojson-cover/issues" 23 | }, 24 | "homepage": "https://github.com/mapbox/geojson-cover", 25 | "dependencies": { 26 | "geojson-extent": "^0.3.0", 27 | "geojson-normalize": "0.0.0", 28 | "s2": "^0.3.8", 29 | "turf-buffer": "^0.1.0" 30 | }, 31 | "devDependencies": { 32 | "queue-async": "^1.0.7", 33 | "tape": "^2.14.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Morgan Herlocker 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # geojson-cover 2 | 3 | [![build status](https://secure.travis-ci.org/mapbox/geojson-cover.png)](http://travis-ci.org/mapbox/geojson-cover) 4 | 5 | generates an s2 cover from geojson 6 | 7 | ## API 8 | 9 | ```js 10 | geojsonCover.bboxQueryIndexes(bbox, range); // -> cells 11 | geojsonCover.bboxCellGeoJSON(bbox); // -> geojson 12 | geojsonCover.geometryIndexes(geojson); // -> cells 13 | geojsonCover.geometryGeoJSON(geojson); // -> geojson 14 | 15 | // options 16 | geojsonCover.bboxQueryIndexes(bbox, false, {max_index_cells: 50}); // -> cells 17 | ``` 18 | 19 | ###options 20 | 21 | name|default|description 22 | ---|---|--- 23 | max_query_cells | 100 | the maximum number of S2 cells used for any query coverage. 24 | query_min_level | 1 | The largest size of a cell permissable in a query. 25 | query_max_level | 8 | The smallest size of a cell permissable in a query. 26 | max_index_cells | 100 | the maximum number of S2 cells used for any index coverage. 27 | index_min_level | 8 | The largest size of a cell permissable in an index. 28 | index_max_level | 12 | The smallest size of a cell permissable in an index. 29 | index_point_level | 15 | The index level for point features only. 30 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var s2 = require('s2'), 2 | normalize = require('geojson-normalize'), 3 | buffer = require('turf-buffer'), 4 | geojsonExtent = require('geojson-extent'); 5 | 6 | var serialization = 'toToken'; 7 | 8 | module.exports.bboxQueryIndexes = function(bbox, range, opts) { 9 | opts = setOptions(opts); 10 | if (range === undefined) range = true; 11 | var latLngRect = new s2.S2LatLngRect( 12 | new s2.S2LatLng(bbox[1], bbox[0]), 13 | new s2.S2LatLng(bbox[3], bbox[2])); 14 | 15 | var cover_options = { 16 | min: opts.query_min_level, 17 | max: opts.query_max_level, 18 | max_cells: opts.max_index_cells 19 | }; 20 | 21 | return s2.getCover(latLngRect, cover_options).map(function(cell) { 22 | if (range) { 23 | return [ 24 | cell.id().rangeMin().toToken(), 25 | cell.id().rangeMax().toToken() 26 | ]; 27 | } else { 28 | return cell.id().toToken(); 29 | } 30 | }); 31 | }; 32 | 33 | module.exports.bboxCellGeoJSON = function(bbox, opts) { 34 | opts = setOptions(opts); 35 | var latLngRect = new s2.S2LatLngRect( 36 | new s2.S2LatLng(bbox[1], bbox[0]), 37 | new s2.S2LatLng(bbox[3], bbox[2])); 38 | 39 | var cover_options = { 40 | min: opts.query_min_level, 41 | max: opts.query_max_level, 42 | max_cells: opts.max_index_cells 43 | }; 44 | return s2.getCover(latLngRect, cover_options).map(function(c) { 45 | return c.toGeoJSON(); 46 | }); 47 | }; 48 | 49 | module.exports.geometryIndexes = function(input, opts) { 50 | opts = setOptions(opts); 51 | var geom = normalize(input).features[0].geometry; 52 | switch (geom.type) { 53 | case 'Point': 54 | return pointIndex(geom.coordinates, opts); 55 | case 'LineString': 56 | return linestringIndex(geom, opts); 57 | case 'Polygon': 58 | return polygonIndex(geom, opts); 59 | case 'MultiPolygon': 60 | return multipolygonIndex(geom, opts); 61 | default: 62 | return []; 63 | } 64 | }; 65 | 66 | module.exports.geometryGeoJSON = function(input, opts) { 67 | opts = setOptions(opts); 68 | var geom = normalize(input).features[0].geometry; 69 | switch (geom.type) { 70 | case 'Point': 71 | return pointGeoJSON(geom.coordinates, opts); 72 | case 'LineString': 73 | return linestringGeoJSON(geom, opts); 74 | case 'Polygon': 75 | return polygonGeoJSON(geom, opts); 76 | case 'MultiPolygon': 77 | return multipolygonGeoJSON(geom, opts); 78 | default: 79 | return []; 80 | } 81 | }; 82 | 83 | // GeometryIndex 84 | 85 | function pointIndex(coords, opts) { 86 | var id = new s2.S2CellId(new s2.S2LatLng(coords[1], coords[0])); 87 | return [id.parent(opts.index_point_level).toToken()]; 88 | } 89 | 90 | function linestringIndex(geometry, opts) { 91 | var feature = { 92 | type:'Feature', 93 | geometry: geometry 94 | }; 95 | geometry = buffer(feature, 0.00001, 'miles').features[0].geometry; 96 | var rings = geometry.coordinates; 97 | 98 | var cover_options = { 99 | min: opts.index_min_level, 100 | max: opts.index_max_level, 101 | max_cells: opts.max_index_cells, 102 | type: 'polygon' 103 | }; 104 | 105 | var llRings = rings.map(function(ring) { 106 | return ring.map(function(c) { 107 | var latLng = (new s2.S2LatLng(c[1], c[0])).normalized(); 108 | return latLng.toPoint(); 109 | }).slice(1); 110 | }); 111 | 112 | return s2.getCover(llRings, cover_options).map(function(cell) { 113 | return cell.id().toToken(); 114 | }); 115 | } 116 | 117 | function polygonIndex(geometry, opts) { 118 | var rings = geometry.coordinates; 119 | 120 | var cover_options = { 121 | min: opts.index_min_level, 122 | max: opts.index_max_level, 123 | max_cells: opts.max_index_cells, 124 | type: 'polygon' 125 | }; 126 | 127 | var llRings = rings.map(function(ring) { 128 | return ring.map(function(c) { 129 | var latLng = (new s2.S2LatLng(c[1], c[0])).normalized(); 130 | return latLng.toPoint(); 131 | }).slice(1); 132 | }); 133 | 134 | return s2.getCover(llRings, cover_options).map(function(cell) { 135 | return cell.id().toToken(); 136 | }); 137 | } 138 | 139 | function multipolygonIndex(geometry, opts) { 140 | var polygons = geometry.coordinates; 141 | 142 | var cover_options = { 143 | min: opts.index_min_level, 144 | max: opts.index_max_level, 145 | max_cells: opts.max_index_cells, 146 | type: 'multipolygon' 147 | }; 148 | 149 | var llRings = polygons.map(function(rings) { 150 | return rings.map(function(ring) { 151 | return ring.map(function(c){ 152 | var latLng = (new s2.S2LatLng(c[1], c[0])).normalized(); 153 | return latLng.toPoint(); 154 | }).slice(1); 155 | }); 156 | }); 157 | 158 | return s2.getCover(llRings, cover_options).map(function(cell) { 159 | return cell.id().toToken(); 160 | }); 161 | } 162 | 163 | // GeometryGeoJSON 164 | 165 | function linestringGeoJSON(geometry, opts) { 166 | var feature = { 167 | type:'Feature', 168 | geometry: geometry 169 | } 170 | geometry = buffer(feature, 0.00001, 'miles').features[0].geometry; 171 | var rings = geometry.coordinates; 172 | 173 | var cover_options = { 174 | min: opts.index_min_level, 175 | max: opts.index_max_level, 176 | max_cells: opts.max_index_cells, 177 | type: 'polygon' 178 | }; 179 | 180 | var llRings = rings.map(function(ring) { 181 | return ring.map(function(c) { 182 | var latLng = (new s2.S2LatLng(c[1], c[0])).normalized(); 183 | return latLng.toPoint(); 184 | }).slice(1); 185 | }); 186 | var features = s2.getCover(llRings, cover_options).map(function(cell,i) { 187 | var geojson = cell.toGeoJSON(); 188 | return { 189 | type: 'Feature', 190 | geometry: geojson, 191 | properties: {} 192 | }; 193 | }); 194 | 195 | return { 196 | type: 'FeatureCollection', 197 | features: features 198 | } 199 | } 200 | 201 | function polygonGeoJSON(geometry, opts) { 202 | var rings = geometry.coordinates; 203 | 204 | var cover_options = { 205 | min: opts.index_min_level, 206 | max: opts.index_max_level, 207 | max_cells: opts.max_index_cells, 208 | type: 'polygon' 209 | }; 210 | 211 | var llRings = rings.map(function(ring) { 212 | return ring.map(function(c) { 213 | var latLng = (new s2.S2LatLng(c[1], c[0])).normalized(); 214 | return latLng.toPoint(); 215 | }).slice(1); 216 | }); 217 | var features = s2.getCover(llRings, cover_options).map(function(cell, i) { 218 | var geojson = cell.toGeoJSON(); 219 | return { 220 | type: 'Feature', 221 | geometry: geojson, 222 | properties: {} 223 | }; 224 | }); 225 | 226 | return { 227 | type: 'FeatureCollection', 228 | features: features 229 | } 230 | } 231 | 232 | function multipolygonGeoJSON(geometry, opts) { 233 | var polygons = geometry.coordinates; 234 | 235 | var cover_options = { 236 | min: opts.index_min_level, 237 | max: opts.index_max_level, 238 | max_cells: opts.max_index_cells, 239 | type: 'multipolygon' 240 | }; 241 | 242 | var llRings = polygons.map(function(rings) { 243 | return rings.map(function(ring) { 244 | return ring.map(function(c){ 245 | var latLng = (new s2.S2LatLng(c[1], c[0])).normalized(); 246 | return latLng.toPoint(); 247 | }).slice(1); 248 | }); 249 | }); 250 | var features = s2.getCover(llRings, cover_options).map(function(cell, i) { 251 | var geojson = cell.toGeoJSON(); 252 | return { 253 | type: 'Feature', 254 | geometry: geojson, 255 | properties: {} 256 | }; 257 | }); 258 | 259 | return { 260 | type: 'FeatureCollection', 261 | features: features 262 | } 263 | } 264 | 265 | function setOptions(opts) { 266 | if (!opts) { 267 | opts = {} 268 | } 269 | 270 | // the maximum number of S2 cells used for any query coverage. 271 | // - More = more complex queries 272 | // - Fewer = less accurate queries 273 | if (opts.max_query_cells === undefined) { 274 | opts.max_query_cells = 100 275 | } 276 | 277 | // The largest size of a cell permissable in a query. 278 | if (opts.query_min_level === undefined) { 279 | opts.query_min_level = 1; 280 | } 281 | 282 | // The smallest size of a cell permissable in a query. 283 | // - This must be >= index_max_level 284 | if (opts.query_max_level === undefined) { 285 | opts.query_max_level = 8; 286 | } 287 | 288 | // the maximum number of S2 cells used for any index coverage. 289 | // - More = more accurate indexes 290 | // - Fewer = more compact queries 291 | if (opts.max_index_cells === undefined) { 292 | opts.max_index_cells = 100; 293 | } 294 | 295 | // The largest size of a cell permissable in an index. 296 | // - This must be <= query_min_level 297 | if (opts.index_min_level === undefined) { 298 | opts.index_min_level = 8; 299 | } 300 | 301 | // The smallest size of a cell permissable in an index. 302 | if (opts.index_max_level === undefined) { 303 | opts.index_max_level = 12; 304 | } 305 | 306 | // The index level for point features only. 307 | if (opts.index_point_level === undefined) { 308 | opts.index_point_level = 15; 309 | } 310 | 311 | if (!(opts.query_max_level >= opts.index_min_level)) { 312 | throw new Error('query level and index level must correspond'); 313 | } 314 | 315 | return opts; 316 | } 317 | --------------------------------------------------------------------------------