diff --git a/CHANGELOG.md b/CHANGELOG.md index 01f90597..8d10bc7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,14 @@ file [H3Core.java](./src/main/java/com/uber/h3core/H3Core.java), and support for the Linux x64 and Darwin x64 platforms. ## Unreleased Changes + +## [4.4.0] - 2025-12-12 +### Added +- `constructCell`, `isValidIndex`, and `getIndexDigit` functions. + ### Changed - Restored benchmark target to the build script. (#188) +- Upgraded the core library to v4.4.1. (#192) ## [4.3.2] - 2025-10-10 ### Changed diff --git a/README.md b/README.md index 16e50e68..f1ec6935 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![Coverage Status](https://coveralls.io/repos/github/uber/h3-java/badge.svg?branch=master)](https://coveralls.io/github/uber/h3-java?branch=master) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE) [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.uber/h3/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.uber/h3) -[![H3 Version](https://img.shields.io/badge/h3-v4.3.0-blue.svg)](https://github.com/uber/h3/releases/tag/v4.3.0) +[![H3 Version](https://img.shields.io/badge/h3-v4.4.1-blue.svg)](https://github.com/uber/h3/releases/tag/v4.4.1) This library provides Java bindings for the [H3 Core Library](https://github.com/uber/h3). For API reference, please see the [H3 Documentation](https://h3geo.org/). @@ -18,14 +18,14 @@ Add it to your pom.xml: com.uber h3 - 4.3.2 + 4.4.0 ``` Or, using Gradle: ```gradle -compile("com.uber:h3:4.3.2") +compile("com.uber:h3:4.4.0") ``` Encode a location into a hexagon address: diff --git a/build.gradle b/build.gradle index e7f9c05d..1bac2923 100644 --- a/build.gradle +++ b/build.gradle @@ -133,7 +133,7 @@ spotless { } jacoco { - toolVersion = '0.8.12' + toolVersion = '0.8.14' } jacocoTestReport { diff --git a/gradle.properties b/gradle.properties index c9e4e23f..73f39e04 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,4 +5,4 @@ org.gradle.configuration-cache=false # No spaces on the following line, needed by release.yml: -version=4.3.3-SNAPSHOT +version=4.4.0 diff --git a/h3version.properties b/h3version.properties index ac2953c8..af844af8 100644 --- a/h3version.properties +++ b/h3version.properties @@ -1 +1 @@ -h3.git.reference=v4.3.0 +h3.git.reference=v4.4.1 diff --git a/src/main/c/h3-java/src/jniapi.c b/src/main/c/h3-java/src/jniapi.c index 342fe585..37d42b89 100644 --- a/src/main/c/h3-java/src/jniapi.c +++ b/src/main/c/h3-java/src/jniapi.c @@ -218,6 +218,33 @@ void DestroyGeoPolygon(JNIEnv *env, jdoubleArray verts, } } +/* + * Class: com_uber_h3core_NativeMethods + * Method: constructCell + * Signature: (II[I)J + */ +JNIEXPORT jlong JNICALL Java_com_uber_h3core_NativeMethods_constructCell( + JNIEnv *env, jobject thiz, jint res, jint baseCell, jintArray digits) { + H3Index result = 0; + jint *digitsElements = (**env).GetIntArrayElements(env, digits, 0); + + if (digitsElements != NULL) { + // if sz is too small, bad things will happen + // note: We assume int can at least contain `jint` on the current + // platform. This may not be true if sizeof(int) < 32, but we don't + // support any platforms where this would be the case. + H3Error err = constructCell(res, baseCell, digitsElements, &result); + + (**env).ReleaseIntArrayElements(env, digits, digitsElements, 0); + if (err) { + ThrowH3Exception(env, err); + } + } else { + ThrowOutOfMemoryError(env); + } + return result; +} + /* * Class: com_uber_h3core_NativeMethods * Method: isValidCell @@ -228,6 +255,16 @@ JNIEXPORT jboolean JNICALL Java_com_uber_h3core_NativeMethods_isValidCell( return isValidCell(h3); } +/* + * Class: com_uber_h3core_NativeMethods + * Method: isValidIndex + * Signature: (J)Z + */ +JNIEXPORT jboolean JNICALL Java_com_uber_h3core_NativeMethods_isValidIndex( + JNIEnv *env, jobject thiz, jlong h3) { + return isValidIndex(h3); +} + /* * Class: com_uber_h3core_NativeMethods * Method: getBaseCellNumber diff --git a/src/main/java/com/uber/h3core/H3Core.java b/src/main/java/com/uber/h3core/H3Core.java index e5135a8a..eb4f3426 100644 --- a/src/main/java/com/uber/h3core/H3Core.java +++ b/src/main/java/com/uber/h3core/H3Core.java @@ -103,16 +103,59 @@ private H3Core(NativeMethods h3Api) { this.h3Api = h3Api; } - /** Returns true if this is a valid H3 index. */ + /** Returns true if this is a valid H3 cell index. */ public boolean isValidCell(long h3) { return h3Api.isValidCell(h3); } - /** Returns true if this is a valid H3 index. */ + /** Returns true if this is a valid H3 cell index. */ public boolean isValidCell(String h3Address) { return isValidCell(stringToH3(h3Address)); } + /** Returns true if this is a valid H3 index. */ + public boolean isValidIndex(long h3) { + return h3Api.isValidIndex(h3); + } + + /** Returns true if this is a valid H3 index. */ + public boolean isValidIndex(String h3Address) { + return isValidIndex(stringToH3(h3Address)); + } + + /** Construct a cell index from component parts */ + public long constructCell(int baseCellNumber, List digits, int res) { + int[] digitsArray = digits.stream().mapToInt(Integer::intValue).toArray(); + if (digitsArray.length != res) { + throw new IllegalArgumentException( + String.format( + "Number of provided digits is incorrect, must be %d, was %d", + res, digitsArray.length)); + } + if (digitsArray.length > 15) { + throw new IllegalArgumentException( + String.format( + "Additional unused digits provided, must be at most 15 but was %d", + digitsArray.length)); + } + return h3Api.constructCell(res, baseCellNumber, digitsArray); + } + + /** Construct a cell index from component parts */ + public long constructCell(int baseCellNumber, List digits) { + return constructCell(baseCellNumber, digits, digits.size()); + } + + /** Construct a cell index from component parts */ + public String constructCellAddress(int baseCellNumber, List digits) { + return h3ToString(constructCell(baseCellNumber, digits, digits.size())); + } + + /** Construct a cell index from component parts */ + public String constructCellAddress(int baseCellNumber, List digits, int res) { + return h3ToString(constructCell(baseCellNumber, digits, res)); + } + /** Returns the base cell number for this index. */ public int getBaseCellNumber(long h3) { return h3Api.getBaseCellNumber(h3); @@ -727,11 +770,37 @@ public int getResolution(String h3Address) { return getResolution(stringToH3(h3Address)); } - /** Returns the resolution of the provided index */ + /** Returns the resolution of the provided index. */ public int getResolution(long h3) { return (int) ((h3 & H3_RES_MASK) >> H3_RES_OFFSET); } + /** + * Returns the indexing digit of the index at `res` + * + * @param h3 H3 index. + * @param res Resolution of the digit, 1 <= res <= 15 + * @throws IllegalArgumentException res is not between 0 and 15, inclusive. + */ + public int getIndexDigit(String h3Address, int res) { + return getIndexDigit(stringToH3(h3Address), res); + } + + /** + * Returns the indexing digit of the index at `res` + * + * @param h3 H3 index. + * @param res Resolution of the digit, 1 <= res <= 15 + * @throws IllegalArgumentException res is not between 0 and 15, inclusive. + */ + public int getIndexDigit(long h3, int res) { + if (res < 1 || res > 15) { + throw new IllegalArgumentException( + String.format("resolution %d is out of range (must be 1 <= res <= 15)", res)); + } + return (int) ((h3 >> ((15 - res) * 3)) & 7); + } + /** * Returns the parent of the index at the given resolution. * diff --git a/src/main/java/com/uber/h3core/NativeMethods.java b/src/main/java/com/uber/h3core/NativeMethods.java index 06825c98..fb5845b9 100644 --- a/src/main/java/com/uber/h3core/NativeMethods.java +++ b/src/main/java/com/uber/h3core/NativeMethods.java @@ -34,8 +34,12 @@ final class NativeMethods { native long cellToCenterChild(long h3, int childRes); + native long constructCell(int res, int baseCell, int[] digits); + native boolean isValidCell(long h3); + native boolean isValidIndex(long h3); + native int getBaseCellNumber(long h3); native boolean isPentagon(long h3); diff --git a/src/main/java/com/uber/h3core/exceptions/H3Exception.java b/src/main/java/com/uber/h3core/exceptions/H3Exception.java index 8396de43..159ed5c4 100644 --- a/src/main/java/com/uber/h3core/exceptions/H3Exception.java +++ b/src/main/java/com/uber/h3core/exceptions/H3Exception.java @@ -68,6 +68,14 @@ public static String codeToMessage(int code) { return "Bounds of provided memory were insufficient"; case 15: return "Mode or flags argument was not valid"; + case 16: + return "Index argument was not valid"; + case 17: + return "Base cell number was outside of acceptable range"; + case 18: + return "Child indexing digits invalid"; + case 19: + return "Child indexing digits refer to a deleted subsequence"; default: return "Unknown error"; } diff --git a/src/test/java/com/uber/h3core/TestDirectedEdges.java b/src/test/java/com/uber/h3core/TestDirectedEdges.java index 3a6b1c0c..b7243e4c 100644 --- a/src/test/java/com/uber/h3core/TestDirectedEdges.java +++ b/src/test/java/com/uber/h3core/TestDirectedEdges.java @@ -43,6 +43,8 @@ void unidirectionalEdges() { assertTrue(h3.isValidDirectedEdge(edge)); assertFalse(h3.isValidDirectedEdge(start)); + assertTrue(h3.isValidIndex(edge)); + assertFalse(h3.isValidCell(edge)); assertEquals(start, h3.getDirectedEdgeOrigin(edge)); assertEquals(adjacent, h3.getDirectedEdgeDestination(edge)); diff --git a/src/test/java/com/uber/h3core/TestInspection.java b/src/test/java/com/uber/h3core/TestInspection.java index dbf62007..3de77f5b 100644 --- a/src/test/java/com/uber/h3core/TestInspection.java +++ b/src/test/java/com/uber/h3core/TestInspection.java @@ -17,8 +17,11 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import com.google.common.collect.ImmutableList; +import com.uber.h3core.exceptions.H3Exception; import java.util.Collection; import java.util.HashSet; import java.util.Set; @@ -27,7 +30,7 @@ /** Tests for index inspection and description functions. */ class TestInspection extends BaseTestH3Core { @Test - void h3IsValid() { + void h3IsValidCell() { assertTrue(h3.isValidCell(22758474429497343L | (1L << 59L))); assertFalse(h3.isValidCell(-1L)); assertTrue(h3.isValidCell("8f28308280f18f2")); @@ -38,6 +41,173 @@ void h3IsValid() { assertFalse(h3.isValidCell(0x8f28308280f18f2L | (1L << 58L))); } + @Test + void h3IsValidIndex() { + assertTrue(h3.isValidIndex(22758474429497343L | (1L << 59L))); + assertFalse(h3.isValidIndex(-1L)); + assertTrue(h3.isValidIndex("8f28308280f18f2")); + assertTrue(h3.isValidIndex("8F28308280F18F2")); + assertTrue(h3.isValidIndex("08f28308280f18f2")); + + assertFalse(h3.isValidIndex(0x8f28308280f18f2L | (1L << 63L))); + assertFalse(h3.isValidIndex(0x8f28308280f18f2L | (1L << 58L))); + } + + @Test + void getIndexDigit() { + assertEquals(h3.getIndexDigit("822d57fffffffff", 1), 5); + assertEquals(h3.getIndexDigit("822d57fffffffff", 2), 2); + assertThrows( + IllegalArgumentException.class, + () -> + // Invalid res + h3.getIndexDigit("822d57fffffffff", -1)); + assertThrows( + IllegalArgumentException.class, + () -> + // Invalid res + h3.getIndexDigit("822d57fffffffff", 0)); + assertThrows( + IllegalArgumentException.class, + () -> + // Invalid res + h3.getIndexDigit("822d57fffffffff", 20)); + assertEquals(h3.getIndexDigit("822d57fffffffff", 3), 7); + } + + @Test + void constructCell() { + assertEquals(h3.constructCell(0, ImmutableList.of()), 576495936675512319L); + assertEquals(h3.constructCell(0, ImmutableList.of(), 0), 576495936675512319L); + assertThrows( + IllegalArgumentException.class, + () -> + // Invalid res + h3.constructCell(0, ImmutableList.of(), -1)); + assertThrows( + H3Exception.class, + () -> + // Invalid base cell + h3.constructCell(-1, ImmutableList.of())); + assertThrows( + H3Exception.class, + () -> + // Invalid base cell + h3.constructCell(-1, ImmutableList.of(), 0)); + assertThrows( + IllegalArgumentException.class, + () -> + // Invalid digit list + h3.constructCell(5, ImmutableList.of(), 1)); + assertThrows( + IllegalArgumentException.class, + () -> + // Invalid digit list + h3.constructCell(5, ImmutableList.of(1, 2), 1)); + assertEquals(h3.constructCell(5, ImmutableList.of(1)), 581149069884260351L); + assertEquals(h3.constructCell(5, ImmutableList.of(1), 1), 581149069884260351L); + assertThrows( + H3Exception.class, + () -> + // Deleted subsequence + h3.constructCell(4, ImmutableList.of(1))); + assertThrows( + H3Exception.class, + () -> + // Deleted subsequence + h3.constructCell(4, ImmutableList.of(1), 1)); + assertThrows( + IllegalArgumentException.class, + () -> + // Too few digits + h3.constructCell(20, ImmutableList.of(1, 2, 3, 4, 5, 6), 7)); + assertEquals( + h3.constructCell(100, ImmutableList.of(1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6, 1, 2, 3)), + 0x8fc8539714e5c53L); + assertEquals( + h3.constructCell(100, ImmutableList.of(1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6, 1, 2, 3), 15), + 0x8fc8539714e5c53L); + assertThrows( + IllegalArgumentException.class, + () -> + // Too many digits + h3.constructCell( + 20, ImmutableList.of(1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6, 1, 2, 3, 0, 0, 0))); + assertThrows( + IllegalArgumentException.class, + () -> + // Too many digits + h3.constructCell( + 20, ImmutableList.of(1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6, 1, 2, 3, 0, 0, 0), 15)); + } + + @Test + void constructCellAddress() { + assertEquals(h3.constructCellAddress(0, ImmutableList.of()), "8001fffffffffff"); + assertEquals(h3.constructCellAddress(0, ImmutableList.of(), 0), "8001fffffffffff"); + assertThrows( + IllegalArgumentException.class, + () -> + // Invalid res + h3.constructCellAddress(0, ImmutableList.of(), -1)); + assertThrows( + H3Exception.class, + () -> + // Invalid base cell + h3.constructCellAddress(-1, ImmutableList.of())); + assertThrows( + H3Exception.class, + () -> + // Invalid base cell + h3.constructCellAddress(-1, ImmutableList.of(), 0)); + assertThrows( + IllegalArgumentException.class, + () -> + // Invalid digit list + h3.constructCellAddress(5, ImmutableList.of(), 1)); + assertThrows( + IllegalArgumentException.class, + () -> + // Invalid digit list + h3.constructCellAddress(5, ImmutableList.of(1, 2), 1)); + assertEquals(h3.constructCellAddress(5, ImmutableList.of(1)), "810a7ffffffffff"); + assertEquals(h3.constructCellAddress(5, ImmutableList.of(1), 1), "810a7ffffffffff"); + assertThrows( + H3Exception.class, + () -> + // Deleted subsequence + h3.constructCellAddress(4, ImmutableList.of(1))); + assertThrows( + H3Exception.class, + () -> + // Deleted subsequence + h3.constructCellAddress(4, ImmutableList.of(1), 1)); + assertThrows( + IllegalArgumentException.class, + () -> + // Too few digits + h3.constructCellAddress(20, ImmutableList.of(1, 2, 3, 4, 5, 6), 7)); + assertEquals( + h3.constructCellAddress(100, ImmutableList.of(1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6, 1, 2, 3)), + "8fc8539714e5c53"); + assertEquals( + h3.constructCellAddress( + 100, ImmutableList.of(1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6, 1, 2, 3), 15), + "8fc8539714e5c53"); + assertThrows( + IllegalArgumentException.class, + () -> + // Too many digits + h3.constructCellAddress( + 20, ImmutableList.of(1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6, 1, 2, 3, 0, 0, 0))); + assertThrows( + IllegalArgumentException.class, + () -> + // Too many digits + h3.constructCellAddress( + 20, ImmutableList.of(1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6, 1, 2, 3, 0, 0, 0), 15)); + } + @Test void h3GetResolution() { assertEquals(0, h3.getResolution(0x8029fffffffffffL)); diff --git a/src/test/java/com/uber/h3core/TestRegion.java b/src/test/java/com/uber/h3core/TestRegion.java index a41fa41c..dbbb6d17 100644 --- a/src/test/java/com/uber/h3core/TestRegion.java +++ b/src/test/java/com/uber/h3core/TestRegion.java @@ -119,6 +119,140 @@ void polyfillExperimental() { assertTrue(hexagons.size() > 1000); } + @Test + void polyfillExperimentalAddressesCenter() { + List hexagons = + h3.polygonToCellAddressesExperimental( + ImmutableList.of( + new LatLng(37.813318999983238, -122.4089866999972145), + new LatLng(37.7866302000007224, -122.3805436999997056), + new LatLng(37.7198061999978478, -122.3544736999993603), + new LatLng(37.7076131999975672, -122.5123436999983966), + new LatLng(37.7835871999971715, -122.5247187000021967), + new LatLng(37.8151571999998453, -122.4798767000009008)), + null, + 9, + PolygonToCellsFlags.containment_center); + + assertTrue(hexagons.size() > 1000); + } + + @Test + void polyfillExperimentalAddressesFull() { + List hexagons = + h3.polygonToCellAddressesExperimental( + ImmutableList.of( + new LatLng(37.813318999983238, -122.4089866999972145), + new LatLng(37.7866302000007224, -122.3805436999997056), + new LatLng(37.7198061999978478, -122.3544736999993603), + new LatLng(37.7076131999975672, -122.5123436999983966), + new LatLng(37.7835871999971715, -122.5247187000021967), + new LatLng(37.8151571999998453, -122.4798767000009008)), + null, + 9, + PolygonToCellsFlags.containment_full); + + assertTrue(hexagons.size() > 1000); + } + + @Test + void polyfillExperimentalAddressesOverlapping() { + List hexagons = + h3.polygonToCellAddressesExperimental( + ImmutableList.of( + new LatLng(37.813318999983238, -122.4089866999972145), + new LatLng(37.7866302000007224, -122.3805436999997056), + new LatLng(37.7198061999978478, -122.3544736999993603), + new LatLng(37.7076131999975672, -122.5123436999983966), + new LatLng(37.7835871999971715, -122.5247187000021967), + new LatLng(37.8151571999998453, -122.4798767000009008)), + null, + 9, + PolygonToCellsFlags.containment_overlapping); + + assertTrue(hexagons.size() > 1000); + } + + @Test + void polyfillExperimentalAddressesOverlappingBbox() { + List hexagons = + h3.polygonToCellAddressesExperimental( + ImmutableList.of( + new LatLng(37.813318999983238, -122.4089866999972145), + new LatLng(37.7866302000007224, -122.3805436999997056), + new LatLng(37.7198061999978478, -122.3544736999993603), + new LatLng(37.7076131999975672, -122.5123436999983966), + new LatLng(37.7835871999971715, -122.5247187000021967), + new LatLng(37.8151571999998453, -122.4798767000009008)), + null, + 9, + PolygonToCellsFlags.containment_overlapping_bbox); + + assertTrue(hexagons.size() > 1000); + } + + @Test + void polyfillExperimentalAddresses() { + List hexagons = + h3.polygonToCellAddressesExperimental( + ImmutableList.of( + new LatLng(37.813318999983238, -122.4089866999972145), + new LatLng(37.7866302000007224, -122.3805436999997056), + new LatLng(37.7198061999978478, -122.3544736999993603), + new LatLng(37.7076131999975672, -122.5123436999983966), + new LatLng(37.7835871999971715, -122.5247187000021967), + new LatLng(37.8151571999998453, -122.4798767000009008)), + null, + 9, + PolygonToCellsFlags.containment_center); + + assertTrue(hexagons.size() > 1000); + } + + @Test + void polyfillExperimentalWithHole() { + List hexagons = + h3.polygonToCellsExperimental( + ImmutableList.of( + new LatLng(37.813318999983238, -122.4089866999972145), + new LatLng(37.7866302000007224, -122.3805436999997056), + new LatLng(37.7198061999978478, -122.3544736999993603), + new LatLng(37.7076131999975672, -122.5123436999983966), + new LatLng(37.7835871999971715, -122.5247187000021967), + new LatLng(37.8151571999998453, -122.4798767000009008)), + ImmutableList.>of( + ImmutableList.of( + new LatLng(37.7869802, -122.4471197), + new LatLng(37.7664102, -122.4590777), + new LatLng(37.7710682, -122.4137097))), + 9, + PolygonToCellsFlags.containment_center); + + assertTrue(hexagons.size() > 1000); + } + + @Test + void polyfillExperimentalAddressesWithHole() { + List hexagons = + h3.polygonToCellAddressesExperimental( + ImmutableList.of( + new LatLng(37.813318999983238, -122.4089866999972145), + new LatLng(37.7866302000007224, -122.3805436999997056), + new LatLng(37.7198061999978478, -122.3544736999993603), + new LatLng(37.7076131999975672, -122.5123436999983966), + new LatLng(37.7835871999971715, -122.5247187000021967), + new LatLng(37.8151571999998453, -122.4798767000009008)), + ImmutableList.>of( + ImmutableList.of( + new LatLng(37.7869802, -122.4471197), + new LatLng(37.7664102, -122.4590777), + new LatLng(37.7710682, -122.4137097))), + 9, + PolygonToCellsFlags.containment_center); + + assertTrue(hexagons.size() > 1000); + } + @Test void polyfill() { List hexagons = diff --git a/src/test/java/com/uber/h3core/exceptions/TestH3Exception.java b/src/test/java/com/uber/h3core/exceptions/TestH3Exception.java index bcb00e38..31f59656 100644 --- a/src/test/java/com/uber/h3core/exceptions/TestH3Exception.java +++ b/src/test/java/com/uber/h3core/exceptions/TestH3Exception.java @@ -26,7 +26,7 @@ class TestH3Exception { @Test void test() { Set messages = new HashSet<>(); - int maxErrorCode = 16; + int maxErrorCode = 20; for (int i = 0; i < maxErrorCode + 10; i++) { H3Exception e = new H3Exception(i); messages.add(e.getMessage());