-
Notifications
You must be signed in to change notification settings - Fork 44
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore(sf|h3): reimplement polyfill h3 functions (#490)
Co-authored-by: vdelacruzb <[email protected]>
- Loading branch information
1 parent
30a28cd
commit d51021b
Showing
19 changed files
with
736 additions
and
437 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,4 +2,4 @@ import { h3Distance } from '../src/h3/h3_kring_distances/h3core_custom'; | |
|
||
export default { | ||
h3Distance | ||
}; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, intersect, polygon, multiPolygon } from '@turf/turf'; | ||
import { h3ToGeoBoundary } from '../src/h3/h3_polyfill/h3core_custom'; | ||
|
||
export default { | ||
bboxClip, | ||
polyfill | ||
booleanContains, | ||
booleanIntersects, | ||
intersect, | ||
polygon, | ||
multiPolygon, | ||
h3ToGeoBoundary | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
# Rebuild h3-js 3.7.2 dependency | ||
First, ensure you have yarn and docker installed. | ||
|
||
``` | ||
wget https://github.com/uber/h3-js/releases/tag/v3.7.2 | ||
unzip h3-js-3.7.2.zip | ||
cd h3-js-3.7.2 | ||
yarn docker-boot && yarn build-emscripten | ||
``` | ||
|
||
Remove all the unneeded bindings from the `lib/bindings.js` | ||
|
||
Then run: | ||
``` | ||
yarn docker-emscripten-run | ||
``` | ||
|
||
Your new library file is available at `out/a.out.js`. Copy it to the correct location with the new filename. For example: `cp out/a.out.js ~/development/analytics-toolbox/core/clouds/snowflake/libraries/javascript/src/h3/h3_polyfill/libh3_custom.js`. Ensure it is named `libh3_custom.js`. | ||
|
10 changes: 5 additions & 5 deletions
10
clouds/snowflake/libraries/javascript/src/h3/h3_polyfill/libh3_custom.js
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+135 KB
clouds/snowflake/modules/doc/h3/images/H3_POLYFILL_MODE_intersects.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,83 +1,177 @@ | ||
---------------------------- | ||
-- Copyright (C) 2021 CARTO | ||
---------------------------- | ||
-------------------------------- | ||
-- Copyright (C) 2021-2024 CARTO | ||
-------------------------------- | ||
|
||
CREATE OR REPLACE FUNCTION @@SF_SCHEMA@@._H3_POLYFILL | ||
(geojson STRING, input_resolution DOUBLE) | ||
RETURNS ARRAY | ||
CREATE OR REPLACE FUNCTION @@SF_SCHEMA@@._HAS_POLYGON_JS | ||
(geojson STRING) | ||
RETURNS BOOLEAN | ||
LANGUAGE JAVASCRIPT | ||
IMMUTABLE | ||
AS $$ | ||
if (!GEOJSON || INPUT_RESOLUTION == null) { | ||
return []; | ||
let inputGeoJSON = JSON.parse(GEOJSON); | ||
let geometries = inputGeoJSON.geometries ? inputGeoJSON.geometries : [inputGeoJSON] // geometrycollection or regular feature geometry | ||
for (let g of geometries) { | ||
if (g.type === 'Polygon' || g.type === 'MultiPolygon') { | ||
return true | ||
} | ||
} | ||
return false | ||
$$; | ||
|
||
CREATE OR REPLACE FUNCTION @@SF_SCHEMA@@._HAS_POLYGON | ||
(geog GEOGRAPHY) | ||
RETURNS BOOLEAN | ||
LANGUAGE SQL | ||
IMMUTABLE | ||
AS $$ | ||
@@SF_SCHEMA@@._HAS_POLYGON_JS(CAST(ST_ASGEOJSON(GEOG) AS STRING)) | ||
$$; | ||
|
||
CREATE OR REPLACE FUNCTION @@SF_SCHEMA@@._FILTER_GEOG_JS | ||
(geojson STRING) | ||
RETURNS STRING | ||
LANGUAGE JAVASCRIPT | ||
IMMUTABLE | ||
AS $$ | ||
// remove non-polygons and split polygons >= 180 degrees | ||
// output is a always MULTIPOLYGON | ||
|
||
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] ]]); | ||
|
||
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 []; | ||
} | ||
let polygons = []; | ||
let geometries = inputGeoJSON.geometries ? inputGeoJSON.geometries : [inputGeoJSON] | ||
|
||
if (polygonCoordinatesA.length + polygonCoordinatesB.length === 0) { | ||
return []; | ||
} | ||
geometries.forEach(g => { | ||
if (g.type === 'Polygon') { | ||
polygons.push({type: 'Feature', geometry: g}) | ||
} | ||
else if (g.type === 'MultiPolygon') { | ||
g.coordinates.forEach(ring => polygons.push({type: 'Feature', geometry: {type: 'Polygon', coordinates: ring}})) | ||
} | ||
}); | ||
|
||
|
||
let intersections = []; | ||
|
||
let intersectAndPush = (hemisphere, poly) => { | ||
const intersection = h3PolyfillLib.intersect(poly, hemisphere); | ||
if (intersection) { | ||
if (intersection.geometry.type === 'Polygon') { | ||
intersections.push(intersection); | ||
} | ||
else if (intersection.geometry.type === 'MultiPolygon') { | ||
intersection.geometry.coordinates.forEach(ring => intersections.push({type: 'Feature', geometry: {type: 'Polygon', coordinates: ring}})) | ||
} | ||
} | ||
}; | ||
|
||
polygons.forEach(p => { | ||
intersectAndPush(westernHemisphere, p); | ||
intersectAndPush(easternHemisphere, p); | ||
}) | ||
|
||
return JSON.stringify(h3PolyfillLib.multiPolygon(intersections.map(i => i.geometry.coordinates)).geometry) | ||
$$; | ||
|
||
CREATE OR REPLACE FUNCTION @@SF_SCHEMA@@._FILTER_GEOG | ||
(geog GEOGRAPHY) | ||
RETURNS GEOGRAPHY | ||
LANGUAGE SQL | ||
IMMUTABLE | ||
AS $$ | ||
TO_GEOGRAPHY(@@SF_SCHEMA@@._FILTER_GEOG_JS(CAST(ST_ASGEOJSON(GEOG) AS STRING))) | ||
$$; | ||
|
||
CREATE OR REPLACE FUNCTION @@SF_SCHEMA@@._H3_POLYFILL_CONTAINS | ||
(geojson STRING, indexes ARRAY) | ||
RETURNS ARRAY | ||
LANGUAGE JAVASCRIPT | ||
IMMUTABLE | ||
AS $$ | ||
let results = [] | ||
let inputGeoJSON = JSON.parse(GEOJSON); | ||
|
||
@@SF_LIBRARY_H3_POLYFILL@@ | ||
|
||
// @@SF_SCHEMA@@.ST_BUFFER demotes MULTIPOLYGONs to POLYGON if it only has one ring. So we check again here. | ||
let polygons = inputGeoJSON.type === 'MultiPolygon' ? inputGeoJSON.coordinates.map(ring => h3PolyfillLib.polygon(ring)) : [h3PolyfillLib.polygon(inputGeoJSON.coordinates)] | ||
|
||
|
||
INDEXES.forEach(h3Index => { | ||
polygons.some(p => { | ||
if (h3PolyfillLib.booleanContains(p, h3PolyfillLib.polygon([h3PolyfillLib.h3ToGeoBoundary(h3Index, true)]))) { | ||
results.push(h3Index) | ||
} | ||
}) | ||
}) | ||
return results | ||
$$; | ||
|
||
CREATE OR REPLACE SECURE FUNCTION @@SF_SCHEMA@@._H3_POLYFILL_INTERSECTS_FILTER | ||
(h3Indexes ARRAY, geojson STRING) | ||
RETURNS ARRAY | ||
LANGUAGE JAVASCRIPT | ||
IMMUTABLE | ||
AS $$ | ||
|
||
let results = [] | ||
let inputGeoJSON = JSON.parse(GEOJSON); | ||
|
||
@@SF_LIBRARY_H3_POLYFILL@@ | ||
|
||
// @@SF_SCHEMA@@.ST_BUFFER demotes MULTIPOLYGONs to POLYGON if it only has one ring. So we check again here. | ||
let polygons = inputGeoJSON.type === 'MultiPolygon' ? inputGeoJSON.coordinates.map(ring => h3PolyfillLib.polygon(ring)) : [h3PolyfillLib.polygon(inputGeoJSON.coordinates)] | ||
|
||
H3INDEXES.forEach(h3Index => { | ||
if (polygons.some(p => h3PolyfillLib.booleanIntersects(p, h3PolyfillLib.polygon([h3PolyfillLib.h3ToGeoBoundary(h3Index, true)])))) { | ||
results.push(h3Index) | ||
} | ||
}) | ||
return [...new Set(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@@._CHECK_TOO_WIDE(geo GEOGRAPHY) | ||
RETURNS BOOLEAN | ||
AS | ||
$$ | ||
CASE | ||
WHEN ST_XMax(geo) < ST_XMin(geo) THEN | ||
-- Adjusts for crossing the antimeridian | ||
360 + ST_XMax(geo) - ST_XMin(geo) >= 180 | ||
ELSE | ||
ST_XMax(geo) - ST_XMin(geo) >= 180 | ||
END | ||
$$; | ||
|
||
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 >= 0 AND RESOLUTION <= 15 AND @@SF_SCHEMA@@._HAS_POLYGON(GEOG), | ||
COALESCE(H3_POLYGON_TO_CELLS_STRINGS(@@SF_SCHEMA@@._FILTER_GEOG(GEOG), 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 < 0 OR RESOLUTION > 15 OR NOT @@SF_SCHEMA@@._HAS_POLYGON(GEOG) THEN [] | ||
WHEN MODE = 'center' THEN @@SF_SCHEMA@@.H3_POLYFILL(GEOG, RESOLUTION) | ||
WHEN MODE = 'intersects' THEN | ||
CASE WHEN @@SF_SCHEMA@@._CHECK_TOO_WIDE(GEOG) THEN @@SF_SCHEMA@@._H3_POLYFILL_INTERSECTS_FILTER(H3_COVERAGE_STRINGS(@@SF_SCHEMA@@._FILTER_GEOG(GEOG), RESOLUTION), CAST(ST_ASGEOJSON(GEOG) AS STRING)) | ||
ELSE H3_COVERAGE_STRINGS(@@SF_SCHEMA@@.ST_BUFFER(@@SF_SCHEMA@@._FILTER_GEOG(GEOG), CAST(0.00000001 AS DOUBLE)), RESOLUTION) | ||
END | ||
WHEN MODE = 'contains' THEN @@SF_SCHEMA@@._H3_POLYFILL_CONTAINS(CAST(ST_ASGEOJSON(@@SF_SCHEMA@@.ST_BUFFER(@@SF_SCHEMA@@._FILTER_GEOG(GEOG), CAST(0.00000001 AS DOUBLE)) ) AS STRING), @@SF_SCHEMA@@.H3_POLYFILL(GEOG, RESOLUTION)) | ||
ELSE [] | ||
END | ||
$$; |
Oops, something went wrong.