Skip to content

Commit

Permalink
Merge pull request #19 from uber/ensure-output
Browse files Browse the repository at this point in the history
* Adds an ensureOutput option to featureToH3Set, offering a fallback to index the centroid when a polygon contains no cells
* Ensures that the output of featureToH3Set is actually a set (i.e. contains no duplicate indexes)
  • Loading branch information
nrabinowitz committed Apr 21, 2021
2 parents 275c038 + 141d7d2 commit b33003c
Show file tree
Hide file tree
Showing 7 changed files with 119 additions and 17 deletions.
7 changes: 7 additions & 0 deletions .prettierrc.js
@@ -0,0 +1,7 @@
module.exports = {
printWidth: 100,
semi: true,
singleQuote: true,
trailingComma: 'none',
bracketSpacing: false
};
6 changes: 6 additions & 0 deletions README.md
Expand Up @@ -62,6 +62,11 @@ fall within the feature will be included.* Note that conversion from GeoJSON
is lossy; the resulting hexagon set only approximately describes the original
shape, at a level of precision determined by the hexagon resolution.

If the polygon is small in comparison with the chosen resolution, there may be
no cell whose center lies within it, resulting in an empty set. To fall back
to a single H3 cell representing the centroid of the polygon in this case, use
the `ensureOutput` option.

![featureToH3Set](./doc-files/featureToH3Set.png)

**Kind**: static method of [<code>geojson2h3</code>](#module_geojson2h3)
Expand All @@ -71,6 +76,7 @@ shape, at a level of precision determined by the hexagon resolution.
| --- | --- | --- |
| feature | <code>Object</code> | Input GeoJSON: type must be either `Feature` or `FeatureCollection`, and geometry type must be either `Polygon` or `MultiPolygon` |
| resolution | <code>Number</code> | Resolution of hexagons, between 0 and 15 |
| [options.ensureOutput] | <code>Boolean</code> | Whether to ensure that at least one cell is returned in the set |


* * *
Expand Down
4 changes: 2 additions & 2 deletions package.json
Expand Up @@ -31,7 +31,7 @@
"istanbul": "^0.4.3",
"jsdoc": "^3.6.6",
"jsdoc-to-markdown": "^6.0.1",
"prettier": "^1.12.1",
"prettier": "^1.19.1",
"tape": "^4.8.0",
"typescript": "^4.1.5"
},
Expand All @@ -52,7 +52,7 @@
"dist-test": "yarn dist && buble -i test -o dist/test",
"benchmarks": "yarn dist-test && node dist/test/benchmarks.js",
"prepublish": "yarn dist",
"prettier": "prettier --write --single-quote --no-bracket-spacing --print-width=100 'src/**/*.js' 'test/**/*.js' '*.d.ts'"
"prettier": "prettier --write 'src/**/*.js' 'test/**/*.js' '*.d.ts'"
},
"engines": {
"node": ">=4",
Expand Down
42 changes: 39 additions & 3 deletions src/geojson2h3.js
Expand Up @@ -46,7 +46,26 @@ function flatten(arrays) {
out = arrays[i];
}
}
return out;
return Array.from(new Set(out));
}

/**
* Utility to compute the centroid of a polygon, based on @turf/centroid
* @private
* @param {Number[][][]} polygon Polygon, as an array of loops
* @return {Number[]} lngLat Lng/lat centroid
*/
function centroid(polygon) {
let lngSum = 0;
let latSum = 0;
let count = 0;
const loop = polygon[0];
for (let i = 0; i < loop.length; i++) {
lngSum += loop[i][0];
latSum += loop[i][1];
count++;
}
return [lngSum / count, latSum / count];
}

/**
Expand Down Expand Up @@ -74,15 +93,22 @@ function featureCollectionToH3Set(featureCollection, resolution) {
* is lossy; the resulting hexagon set only approximately describes the original
* shape, at a level of precision determined by the hexagon resolution.
*
* If the polygon is small in comparison with the chosen resolution, there may be
* no cell whose center lies within it, resulting in an empty set. To fall back
* to a single H3 cell representing the centroid of the polygon in this case, use
* the `ensureOutput` option.
*
* ![featureToH3Set](./doc-files/featureToH3Set.png)
* @static
* @param {Object} feature Input GeoJSON: type must be either `Feature` or
* `FeatureCollection`, and geometry type must be
* either `Polygon` or `MultiPolygon`
* @param {Number} resolution Resolution of hexagons, between 0 and 15
* @param {Boolean} [options.ensureOutput] Whether to ensure that at least one
* cell is returned in the set
* @return {String[]} H3 indexes
*/
function featureToH3Set(feature, resolution) {
function featureToH3Set(feature, resolution, options = {}) {
const {type, geometry} = feature;
const geometryType = geometry && geometry.type;

Expand All @@ -101,7 +127,17 @@ function featureToH3Set(feature, resolution) {
const polygons = geometryType === POLYGON ? [geometry.coordinates] : geometry.coordinates;

// Polyfill each polygon and flatten the results
return flatten(polygons.map(polygon => h3.polyfill(polygon, resolution, true)));
return flatten(
polygons.map(polygon => {
const result = h3.polyfill(polygon, resolution, true);
if (result.length || !options.ensureOutput) {
return result;
}
// If we got no results, index the centroid
const [lng, lat] = centroid(polygon);
return [h3.geoToH3(lat, lng, resolution)];
})
);
}

/**
Expand Down
69 changes: 59 additions & 10 deletions test/geojson2h3.spec.js
Expand Up @@ -229,26 +229,75 @@ test('featureToH3Set - one contained hex', assert => {
assert.end();
});

const SMALL_POLY = {
type: 'Feature',
properties: {},
geometry: {
type: 'Polygon',
coordinates: [
[
[-122.26985598997341, 37.83598006884068],
[-122.26836960154117, 37.83702107154188],
[-122.26741606933939, 37.835426338014386],
[-122.26985598997341, 37.83598006884068]
]
]
}
};

test('featureToH3Set - no contained hex centers', assert => {
assert.deepEqual(featureToH3Set(SMALL_POLY, 8), [], 'featureToH3Set matches expected');
assert.end();
});

test('featureToH3Set - no contained hex centers, ensureOutput', assert => {
const hexagons = ['8828308137fffff'];
assert.deepEqual(
featureToH3Set(SMALL_POLY, 8, {ensureOutput: true}),
hexagons,
'featureToH3Set matches expected'
);
assert.end();
});

test('featureToH3Set - Polygon', assert => {
const feature = {
type: 'Feature',
properties: {},
geometry: {
type: 'Polygon',
coordinates: [
[
[-122.26985598997341, 37.83598006884068],
[-122.26836960154117, 37.83702107154188],
[-122.26741606933939, 37.835426338014386],
[-122.26985598997341, 37.83598006884068]
]
]
coordinates: POLYGON
}
};

const hexagons = [];
const hexagons = ['89283081347ffff', '89283081343ffff', '8928308134fffff', '8928308137bffff'];

assert.deepEqual(
featureToH3Set(feature, DEFAULT_RES).sort(),
hexagons.sort(),
'featureToH3Set matches expected'
);

assert.deepEqual(featureToH3Set(feature, 8), hexagons, 'featureToH3Set matches expected');
assert.end();
});

test('featureToH3Set - MultiPolygon, duplicates', assert => {
const feature = {
type: 'Feature',
properties: {},
geometry: {
type: 'MultiPolygon',
coordinates: [POLYGON, POLYGON]
}
};

const hexagons = ['89283081347ffff', '89283081343ffff', '8928308134fffff', '8928308137bffff'];

assert.deepEqual(
featureToH3Set(feature, DEFAULT_RES).sort(),
hexagons.sort(),
'featureToH3Set matches expected'
);

assert.end();
});
Expand Down
6 changes: 5 additions & 1 deletion types.d.ts
Expand Up @@ -7,7 +7,11 @@ declare module 'geojson2h3' {
* is lossy; the resulting hexagon set only approximately describes the original
* shape, at a level of precision determined by the hexagon resolution.
*/
export function featureToH3Set(feature: Feature, resolution: number): string[];
export function featureToH3Set(
feature: Feature | FeatureCollection,
resolution: number,
options?: {ensureOutput?: boolean}
): string[];

/**
* Convert a single H3 hexagon to a GeoJSON `Polygon` feature
Expand Down
2 changes: 1 addition & 1 deletion yarn.lock
Expand Up @@ -1382,7 +1382,7 @@ prelude-ls@~1.1.2:
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=

prettier@^1.12.1:
prettier@^1.19.1:
version "1.19.1"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb"
integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==
Expand Down

0 comments on commit b33003c

Please sign in to comment.