Skip to content

Commit

Permalink
H3_POLYFILL center intersects and contains mode working
Browse files Browse the repository at this point in the history
  • Loading branch information
DeanSherwin committed Mar 31, 2024
1 parent f2920b7 commit acf41ef
Show file tree
Hide file tree
Showing 6 changed files with 161 additions and 75 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ import { h3Distance } from '../src/h3/h3_kring_distances/h3core_custom';

export default {
h3Distance
};
};
12 changes: 8 additions & 4 deletions clouds/snowflake/libraries/javascript/libs/h3_polyfill.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { polyfill } from '../src/h3/h3_polyfill/h3core_custom';
import { bboxClip } from '@turf/turf';
import { booleanContains, booleanIntersects, geometryCollection, intersect, polygon } from '@turf/turf';
import { h3ToGeoBoundary } from '../src/h3/h3_polyfill/h3core_custom';

export default {
bboxClip,
polyfill
booleanContains,
booleanIntersects,
geometryCollection,
intersect,
polygon,
h3ToGeoBoundary
};
5 changes: 0 additions & 5 deletions clouds/snowflake/libraries/javascript/test/h3.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ const compactLib = require('../build/h3_compact');
const isValidLib = require('../build/h3_isvalid');
const hexRingLib = require('../build/h3_hexring');
const isPentagonLib = require('../build/h3_ispentagon');
const h3PolyfillLib = require('../build/h3_polyfill');
const boundaryLib = require('../build/h3_boundary');
const kringDistancesLib = require('../build/h3_kring_distances');
const uncompactLib = require('../build/h3_uncompact');

Expand All @@ -13,9 +11,6 @@ test('h3 library defined', () => {
expect(hexRingLib.hexRing).toBeDefined();
expect(hexRingLib.h3IsValid).toBeDefined();
expect(isPentagonLib.h3IsPentagon).toBeDefined();
expect(h3PolyfillLib.polyfill).toBeDefined();
expect(boundaryLib.h3ToGeoBoundary).toBeDefined();
expect(boundaryLib.h3IsValid).toBeDefined();
expect(kringDistancesLib.h3Distance).toBeDefined();
expect(uncompactLib.uncompact).toBeDefined();
});
2 changes: 1 addition & 1 deletion clouds/snowflake/modules/doc/h3/H3_POLYFILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ H3_POLYFILL(geography, resolution)

Returns an array with all the H3 cell indexes **with centers** contained in a given polygon. It will return `null` on error (invalid geography type or resolution out of bounds).

* `geography`: `GEOGRAPHY` **polygon** or **multipolygon** representing the shape to cover.
* `geography`: `GEOGRAPHY` **polygon** or **multipolygon** representing the shape to cover. **GeometryCollections** are also allowed but they should contain **polygon** or **multipolygon** geographies. Non-Polygon types will not raise an error but will be ignored instead.
* `resolution`: `INT` number between 0 and 15 with the [H3 resolution](https://h3geo.org/docs/core-library/restable).

**Return type**
Expand Down
207 changes: 147 additions & 60 deletions clouds/snowflake/modules/sql/h3/H3_POLYFILL.sql
Original file line number Diff line number Diff line change
Expand Up @@ -2,82 +2,169 @@
-- Copyright (C) 2021 CARTO
----------------------------

CREATE OR REPLACE FUNCTION @@SF_SCHEMA@@._H3_POLYFILL
(geojson STRING, input_resolution DOUBLE)
RETURNS ARRAY
CREATE OR REPLACE FUNCTION @@SF_SCHEMA@@._HEMI_SPLIT
(geojson STRING)
RETURNS STRING
LANGUAGE JAVASCRIPT
IMMUTABLE
AS $$
if (!GEOJSON || INPUT_RESOLUTION == null) {
return [];
}

let inputGeoJSON = JSON.parse(GEOJSON);

@@SF_LIBRARY_H3_POLYFILL@@

const resolution = Number(INPUT_RESOLUTION);
if (resolution < 0 || resolution > 15) {
return [];
}
const westernHemisphere = h3PolyfillLib.polygon([[ [-180, 90], [0, 90], [0, -90], [-180, -90], [-180, 90]]]);
const easternHemisphere = h3PolyfillLib.polygon([[ [0, 90], [180, 90], [180, -90], [0, -90], [0, 90] ]]);

let polygons = [];

const bboxA = [-180, -90, 0, 90]
const bboxB = [0, -90, 180, 90]
const featureGeometry = JSON.parse(GEOJSON)
let polygonCoordinatesA = [];
let polygonCoordinatesB = [];
switch(featureGeometry.type) {
case 'GeometryCollection':
featureGeometry.geometries.forEach(function (geom) {
if (geom.type === 'MultiPolygon') {
var clippedGeometryA = h3PolyfillLib.bboxClip(geom, bboxA).geometry;
polygonCoordinatesA = polygonCoordinatesA.concat(clippedGeometryA.coordinates);
var clippedGeometryB = h3PolyfillLib.bboxClip(geom, bboxB).geometry;
polygonCoordinatesB = polygonCoordinatesB.concat(clippedGeometryB.coordinates);
} else if (geom.type === 'Polygon') {
var clippedGeometryA = h3PolyfillLib.bboxClip(geom, bboxA).geometry;
polygonCoordinatesA = polygonCoordinatesA.concat([clippedGeometryA.coordinates]);
var clippedGeometryB = h3PolyfillLib.bboxClip(geom, bboxB).geometry;
polygonCoordinatesB = polygonCoordinatesB.concat([clippedGeometryB.coordinates]);
}
});
break;
case 'MultiPolygon':
var clippedGeometryA = h3PolyfillLib.bboxClip(featureGeometry, bboxA).geometry;
polygonCoordinatesA = clippedGeometryA.coordinates;
var clippedGeometryB = h3PolyfillLib.bboxClip(featureGeometry, bboxB).geometry;
polygonCoordinatesB = clippedGeometryB.coordinates;
break;
case 'Polygon':
var clippedGeometryA = h3PolyfillLib.bboxClip(featureGeometry, bboxA).geometry;
polygonCoordinatesA = [clippedGeometryA.coordinates];
var clippedGeometryB = h3PolyfillLib.bboxClip(featureGeometry, bboxB).geometry;
polygonCoordinatesB = [clippedGeometryB.coordinates];
break;
default:
return [];
if (inputGeoJSON.type == "GeometryCollection") {
inputGeoJSON.geometries.forEach(g => {
if (g.type === 'Polygon'|| g.type === 'MultiPolygon') {
polygons.push(g)
}
});
}
else if (inputGeoJSON.type === 'Polygon' || inputGeoJSON.type === 'MultiPolygon') {
polygons.push(inputGeoJSON)
}

if (polygonCoordinatesA.length + polygonCoordinatesB.length === 0) {
return [];
let intersections = [];

let intersectAndPush = (hemisphere, poly) => {
const intersection = h3PolyfillLib.intersect(poly, hemisphere);
if (intersection) {
intersections.push(intersection);
}
};

polygons.forEach(p => {
intersectAndPush(westernHemisphere, p);
intersectAndPush(easternHemisphere, p);
})

return JSON.stringify(h3PolyfillLib.geometryCollection(intersections.map(i => i.geometry)));
$$;

CREATE OR REPLACE FUNCTION @@SF_SCHEMA@@._H3_POLYFILL_CONTAINS
(geojson STRING, indicies ARRAY)
RETURNS ARRAY
LANGUAGE JAVASCRIPT
IMMUTABLE
AS $$

let results = []
let inputGeoJSON = JSON.parse(GEOJSON);

@@SF_LIBRARY_H3_POLYFILL@@
@@SF_LIBRARY_H3_BOUNDARY@@

let polygons = [];

// TODO - I think this will always be one polygon? Does it get _HEMI_SPLIT?
if (inputGeoJSON.type == "GeometryCollection") {
inputGeoJSON.geometries.forEach(g => {
if (g.type === 'Polygon'|| g.type === 'MultiPolygon') {
polygons.push(g)
}
});
}
else if (inputGeoJSON.type === 'Polygon' || inputGeoJSON.type === 'MultiPolygon') {
polygons.push(inputGeoJSON)
}
INDICIES.forEach(h3Index => {
polygons.some(p => {
if (h3PolyfillLib.booleanContains(p, h3PolyfillLib.polygon([h3BoundaryLib.h3ToGeoBoundary(h3Index, true)]))) {
results.push(h3Index)
}
})
})
return results
$$;

let hexesA = polygonCoordinatesA.reduce(
(acc, coordinates) => acc.concat(h3PolyfillLib.polyfill(coordinates, resolution, true)),
[]
).filter(h => h != null);
let hexesB = polygonCoordinatesB.reduce(
(acc, coordinates) => acc.concat(h3PolyfillLib.polyfill(coordinates, resolution, true)),
[]
).filter(h => h != null);
hexes = [...hexesA, ...hexesB];
hexes = [...new Set(hexes)];

return hexes;
CREATE OR REPLACE FUNCTION @@SF_SCHEMA@@._H3_AVG_M_DIAMETER
(resolution DOUBLE)
RETURNS DOUBLE
LANGUAGE JAVASCRIPT
IMMUTABLE
AS $$
return parseInt([
1281.256011 * 2 * 1000,
483.0568391 * 2 * 1000,
182.5129565 * 2 * 1000,
68.97922179 * 2 * 1000,
26.07175968 * 2 * 1000,
9.854090990 * 2 * 1000,
3.724532667 * 2 * 1000,
1.406475763 * 2 * 1000,
0.531414010 * 2 * 1000,
0.200786148 * 2 * 1000,
0.075863783 * 2 * 1000,
0.028663897 * 2 * 1000,
0.010830188 * 2 * 1000,
0.004092010 * 2 * 1000,
0.001546100 * 2 * 1000,
0.000584169 * 2 * 1000
][RESOLUTION])
$$;

CREATE OR REPLACE SECURE FUNCTION @@SF_SCHEMA@@._H3_POLYFILL_INTERSECTS_FILTER
(h3Indicies ARRAY, geojson STRING)
RETURNS ARRAY
LANGUAGE JAVASCRIPT
IMMUTABLE
AS $$

let results = []
let inputGeoJSON = JSON.parse(GEOJSON);

@@SF_LIBRARY_H3_POLYFILL@@
@@SF_LIBRARY_H3_BOUNDARY@@

let polygons = [];

if (inputGeoJSON.type == "GeometryCollection") {
inputGeoJSON.geometries.forEach(g => {
if (g.type === 'Polygon'|| g.type === 'MultiPolygon') {
polygons.push(g)
}
});
}
else if (inputGeoJSON.type === 'Polygon' || inputGeoJSON.type === 'MultiPolygon') {
polygons.push(inputGeoJSON)
}
H3INDICIES.forEach(h3Index => {
polygons.some(p => {
if (h3PolyfillLib.booleanIntersects(p, h3PolyfillLib.polygon([h3BoundaryLib.h3ToGeoBoundary(h3Index, true)]))) {
results.push(h3Index)
}
})
})
return results
$$;


CREATE OR REPLACE SECURE FUNCTION @@SF_SCHEMA@@.H3_POLYFILL
(geog GEOGRAPHY, resolution INT)
RETURNS ARRAY
IMMUTABLE
AS $$
@@SF_SCHEMA@@._H3_POLYFILL(CAST(ST_ASGEOJSON(GEOG) AS STRING), CAST(RESOLUTION AS DOUBLE))
IFF(
GEOG IS NOT NULL AND RESOLUTION BETWEEN 0 AND 15,
COALESCE(H3_POLYGON_TO_CELLS_STRINGS(TO_GEOGRAPHY(@@SF_SCHEMA@@._HEMI_SPLIT(CAST(ST_ASGEOJSON(GEOG) AS STRING))), RESOLUTION), []),
[]
)
$$;

CREATE OR REPLACE SECURE FUNCTION @@SF_SCHEMA@@.H3_POLYFILL
(geog GEOGRAPHY, resolution INT, mode STRING)
RETURNS ARRAY
IMMUTABLE
AS $$
CASE WHEN GEOG IS NULL OR RESOLUTION NOT BETWEEN 0 AND 15 THEN []
WHEN MODE = 'center' THEN @@SF_SCHEMA@@.H3_POLYFILL(GEOG, RESOLUTION)
WHEN MODE = 'intersects' THEN @@SF_SCHEMA@@._H3_POLYFILL_INTERSECTS_FILTER(H3_COVERAGE_STRINGS(TO_GEOGRAPHY(@@SF_SCHEMA@@._HEMI_SPLIT(CAST(ST_ASGEOJSON(GEOG) AS STRING))), RESOLUTION), CAST(ST_ASGEOJSON(GEOG) AS STRING))
WHEN MODE = 'contains' THEN @@SF_SCHEMA@@._H3_POLYFILL_CONTAINS(CAST(ST_ASGEOJSON(GEOG) AS STRING), @@SF_SCHEMA@@.H3_POLYFILL(GEOG, RESOLUTION))
ELSE []
END
$$;
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ test('H3_POLYFILL returns the expected values', async () => {
SELECT
resolution,
H3_FROMGEOGPOINT(geog, resolution) AS hex_id,
H3_BOUNDARY(H3_FROMGEOGPOINT(geog, resolution)) AS boundary
H3_CELL_TO_BOUNDARY(H3_FROMGEOGPOINT(geog, resolution)) AS boundary
FROM points
),
polyfill AS
Expand Down Expand Up @@ -110,7 +110,7 @@ test('H3_POLYFILL returns the expected values', async () => {
SELECT
resolution,
H3_FROMGEOGPOINT(geog, resolution) AS hex_id,
H3_BOUNDARY(H3_FROMGEOGPOINT(geog, resolution)) AS boundary
H3_CELL_TO_BOUNDARY(H3_FROMGEOGPOINT(geog, resolution)) AS boundary
FROM points
),
polyfill AS
Expand Down Expand Up @@ -141,7 +141,7 @@ test('H3_POLYFILL returns the expected values', async () => {
SELECT
resolution,
H3_FROMGEOGPOINT(geog, resolution) AS hex_id,
H3_BOUNDARY(H3_FROMGEOGPOINT(geog, resolution)) AS boundary
H3_CELL_TO_BOUNDARY(H3_FROMGEOGPOINT(geog, resolution)) AS boundary
FROM points
),
polyfill AS
Expand All @@ -160,4 +160,4 @@ test('H3_POLYFILL returns the expected values', async () => {
`;
rows = await runQuery(query);
expect(rows.length).toEqual(0);
});
});

0 comments on commit acf41ef

Please sign in to comment.