Skip to content

Commit 39558d9

Browse files
authored
Improve H3#hexRing logic and add H3#areNeighborCells method (#91140)
Clean up the logic as we are allowing only first neighbours so we can simplify it a bit and remove some unnecessary allocations. In addition we ported the method H3#areNeighborCells which can be useful for example for aggregations over geo_shape.
1 parent d5fb604 commit 39558d9

File tree

4 files changed

+196
-58
lines changed

4 files changed

+196
-58
lines changed

docs/changelog/91140.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 91140
2+
summary: Improve H3#hexRing logic and add H3#areNeighborCells method
3+
area: Geo
4+
type: enhancement
5+
issues: []

libs/h3/src/main/java/org/elasticsearch/h3/H3.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,12 @@ public static String[] h3ToChildren(String h3Address) {
248248
return h3ToStringList(h3ToChildren(stringToH3(h3Address)));
249249
}
250250

251+
/**
252+
* Returns the neighbor indexes.
253+
*
254+
* @param h3Address Origin index
255+
* @return All neighbor indexes from the origin
256+
*/
251257
public static String[] hexRing(String h3Address) {
252258
return h3ToStringList(hexRing(stringToH3(h3Address)));
253259
}
@@ -262,6 +268,28 @@ public static long[] hexRing(long h3) {
262268
return HexRing.hexRing(h3);
263269
}
264270

271+
/**
272+
* returns whether or not the provided hexagons border
273+
*
274+
* @param origin the first index
275+
* @param destination the second index
276+
* @return whether or not the provided hexagons border
277+
*/
278+
public static boolean areNeighborCells(String origin, String destination) {
279+
return areNeighborCells(stringToH3(origin), stringToH3(destination));
280+
}
281+
282+
/**
283+
* returns whether or not the provided hexagons border
284+
*
285+
* @param origin the first index
286+
* @param destination the second index
287+
* @return whether or not the provided hexagons border
288+
*/
289+
public static boolean areNeighborCells(long origin, long destination) {
290+
return HexRing.areNeighbours(origin, destination);
291+
}
292+
265293
/**
266294
* cellToChildrenSize returns the exact number of children for a cell at a
267295
* given child resolution.

libs/h3/src/main/java/org/elasticsearch/h3/HexRing.java

Lines changed: 111 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -569,6 +569,24 @@ final class HexRing {
569569
CoordIJK.Direction.CENTER_DIGIT,
570570
CoordIJK.Direction.IJ_AXES_DIGIT } };
571571

572+
private static final CoordIJK.Direction[] NEIGHBORSETCLOCKWISE = new CoordIJK.Direction[] {
573+
CoordIJK.Direction.CENTER_DIGIT,
574+
CoordIJK.Direction.JK_AXES_DIGIT,
575+
CoordIJK.Direction.IJ_AXES_DIGIT,
576+
CoordIJK.Direction.J_AXES_DIGIT,
577+
CoordIJK.Direction.IK_AXES_DIGIT,
578+
CoordIJK.Direction.K_AXES_DIGIT,
579+
CoordIJK.Direction.I_AXES_DIGIT };
580+
581+
private static final CoordIJK.Direction[] NEIGHBORSETCOUNTERCLOCKWISE = new CoordIJK.Direction[] {
582+
CoordIJK.Direction.CENTER_DIGIT,
583+
CoordIJK.Direction.IK_AXES_DIGIT,
584+
CoordIJK.Direction.JK_AXES_DIGIT,
585+
CoordIJK.Direction.K_AXES_DIGIT,
586+
CoordIJK.Direction.IJ_AXES_DIGIT,
587+
CoordIJK.Direction.I_AXES_DIGIT,
588+
CoordIJK.Direction.J_AXES_DIGIT };
589+
572590
/**
573591
* Produce all neighboring cells. For Hexagons there will be 6 neighbors while
574592
* for pentagon just 5.
@@ -581,52 +599,115 @@ public static long[] hexRing(long origin) {
581599
int idx = 0;
582600
long previous = -1;
583601
for (int i = 0; i < 6; i++) {
584-
int[] rotations = new int[] { 0 };
585-
long[] nextNeighbor = new long[] { 0 };
586-
int neighborResult = h3NeighborRotations(origin, DIRECTIONS[i].digit(), rotations, nextNeighbor);
587-
if (neighborResult != E_PENTAGON) {
588-
// E_PENTAGON is an expected case when trying to traverse off of
589-
// pentagons.
590-
if (neighborResult != E_SUCCESS) {
591-
throw new IllegalArgumentException();
592-
}
593-
if (previous != nextNeighbor[0]) {
594-
out[idx++] = nextNeighbor[0];
595-
previous = nextNeighbor[0];
602+
long neighbor = h3NeighborInDirection(origin, DIRECTIONS[i].digit());
603+
if (neighbor != -1) {
604+
// -1 is an expected case when trying to traverse off of pentagons.
605+
if (previous != neighbor) {
606+
out[idx++] = neighbor;
607+
previous = neighbor;
596608
}
597609
}
598610
}
599611
assert idx == out.length;
600612
return out;
601613
}
602614

615+
/**
616+
* Returns whether or not the provided H3Indexes are neighbors.
617+
* @param origin The origin H3 index.
618+
* @param destination The destination H3 index.
619+
* @return true if the indexes are neighbors, false otherwise
620+
*/
621+
public static boolean areNeighbours(long origin, long destination) {
622+
// Make sure they're hexagon indexes
623+
if (H3Index.H3_get_mode(origin) != Constants.H3_CELL_MODE) {
624+
throw new IllegalArgumentException("Invalid cell: " + origin);
625+
}
626+
627+
if (H3Index.H3_get_mode(destination) != Constants.H3_CELL_MODE) {
628+
throw new IllegalArgumentException("Invalid cell: " + destination);
629+
}
630+
631+
// Hexagons cannot be neighbors with themselves
632+
if (origin == destination) {
633+
return false;
634+
}
635+
636+
final int resolution = H3Index.H3_get_resolution(origin);
637+
// Only hexagons in the same resolution can be neighbors
638+
if (resolution != H3Index.H3_get_resolution(destination)) {
639+
return false;
640+
}
641+
642+
// H3 Indexes that share the same parent are very likely to be neighbors
643+
// Child 0 is neighbor with all of its parent's 'offspring', the other
644+
// children are neighbors with 3 of the 7 children. So a simple comparison
645+
// of origin and destination parents and then a lookup table of the children
646+
// is a super-cheap way to possibly determine they are neighbors.
647+
if (resolution > 1) {
648+
long originParent = H3.h3ToParent(origin);
649+
long destinationParent = H3.h3ToParent(destination);
650+
if (originParent == destinationParent) {
651+
int originResDigit = H3Index.H3_get_index_digit(origin, resolution);
652+
int destinationResDigit = H3Index.H3_get_index_digit(destination, resolution);
653+
if (originResDigit == CoordIJK.Direction.CENTER_DIGIT.digit()
654+
|| destinationResDigit == CoordIJK.Direction.CENTER_DIGIT.digit()) {
655+
return true;
656+
}
657+
if (originResDigit >= CoordIJK.Direction.INVALID_DIGIT.digit()) {
658+
// Prevent indexing off the end of the array below
659+
throw new IllegalArgumentException("");
660+
}
661+
if ((originResDigit == CoordIJK.Direction.K_AXES_DIGIT.digit()
662+
|| destinationResDigit == CoordIJK.Direction.K_AXES_DIGIT.digit()) && H3.isPentagon(originParent)) {
663+
// If these are invalid cells, fail rather than incorrectly
664+
// reporting neighbors. For pentagon cells that are actually
665+
// neighbors across the deleted subsequence, they will fail the
666+
// optimized check below, but they will be accepted by the
667+
// gridDisk check below that.
668+
throw new IllegalArgumentException("Undefined error checking for neighbors");
669+
}
670+
// These sets are the relevant neighbors in the clockwise
671+
// and counter-clockwise
672+
if (NEIGHBORSETCLOCKWISE[originResDigit].digit() == destinationResDigit
673+
|| NEIGHBORSETCOUNTERCLOCKWISE[originResDigit].digit() == destinationResDigit) {
674+
return true;
675+
}
676+
}
677+
}
678+
// Otherwise, we have to determine the neighbor relationship the "hard" way.
679+
for (int i = 0; i < 6; i++) {
680+
long neighbor = h3NeighborInDirection(origin, DIRECTIONS[i].digit());
681+
if (neighbor != -1) {
682+
// -1 is an expected case when trying to traverse off of
683+
// pentagons.
684+
if (destination == neighbor) {
685+
return true;
686+
}
687+
}
688+
}
689+
return false;
690+
}
691+
603692
/**
604693
* Returns the hexagon index neighboring the origin, in the direction dir.
605694
*
606-
* Implementation note: The only reachable case where this returns 0 is if the
695+
* Implementation note: The only reachable case where this returns -1 is if the
607696
* origin is a pentagon and the translation is in the k direction. Thus,
608-
* 0 can only be returned if origin is a pentagon.
697+
* -1 can only be returned if origin is a pentagon.
609698
*
610699
* @param origin Origin index
611700
* @param dir Direction to move in
612-
* @param rotations Number of ccw rotations to perform to reorient the
613-
* translation vector. Will be modified to the new number of
614-
* rotations to perform (such as when crossing a face edge.)
615-
* @param out H3Index of the specified neighbor if succesful
616-
* @return E_SUCCESS on success
701+
* @return H3Index of the specified neighbor or -1 if there is no more neighbor
617702
*/
618-
private static int h3NeighborRotations(long origin, int dir, int[] rotations, long[] out) {
703+
private static long h3NeighborInDirection(long origin, int dir) {
619704
long current = origin;
620705

621-
for (int i = 0; i < rotations[0]; i++) {
622-
dir = CoordIJK.rotate60ccw(dir);
623-
}
624-
625706
int newRotations = 0;
626707
int oldBaseCell = H3Index.H3_get_base_cell(current);
627708
if (oldBaseCell < 0 || oldBaseCell >= Constants.NUM_BASE_CELLS) { // LCOV_EXCL_BR_LINE
628709
// Base cells less than zero can not be represented in an index
629-
return E_CELL_INVALID;
710+
throw new IllegalArgumentException("Invalid base cell looking for neighbor");
630711
}
631712
int oldLeadingDigit = H3Index.h3LeadingNonZeroDigit(current);
632713

@@ -646,7 +727,6 @@ private static int h3NeighborRotations(long origin, int dir, int[] rotations, lo
646727
// perform the adjustment for the k-subsequence we're skipping
647728
// over.
648729
current = H3Index.h3Rotate60ccw(current);
649-
rotations[0] = rotations[0] + 1;
650730
}
651731

652732
break;
@@ -655,7 +735,7 @@ private static int h3NeighborRotations(long origin, int dir, int[] rotations, lo
655735
int nextDir;
656736
if (oldDigit == CoordIJK.Direction.INVALID_DIGIT.digit()) {
657737
// Only possible on invalid input
658-
return E_CELL_INVALID;
738+
throw new IllegalArgumentException();
659739
} else if (H3Index.isResolutionClassIII(r + 1)) {
660740
current = H3Index.H3_set_index_digit(current, r + 1, NEW_DIGIT_II[oldDigit][dir].digit());
661741
nextDir = NEW_ADJUSTMENT_II[oldDigit][dir].digit();
@@ -676,8 +756,6 @@ private static int h3NeighborRotations(long origin, int dir, int[] rotations, lo
676756

677757
int newBaseCell = H3Index.H3_get_base_cell(current);
678758
if (BaseCells.isBaseCellPentagon(newBaseCell)) {
679-
boolean alreadyAdjustedKSubsequence = false;
680-
681759
// force rotation out of missing k-axes sub-sequence
682760
if (H3Index.h3LeadingNonZeroDigit(current) == CoordIJK.Direction.K_AXES_DIGIT.digit()) {
683761
if (oldBaseCell != newBaseCell) {
@@ -694,63 +772,38 @@ private static int h3NeighborRotations(long origin, int dir, int[] rotations, lo
694772
// unreachable.
695773
current = H3Index.h3Rotate60ccw(current); // LCOV_EXCL_LINE
696774
}
697-
alreadyAdjustedKSubsequence = true;
698775
} else {
699776
// In this case, we traversed into the deleted
700777
// k subsequence from within the same pentagon
701778
// base cell.
702779
if (oldLeadingDigit == CoordIJK.Direction.CENTER_DIGIT.digit()) {
703780
// Undefined: the k direction is deleted from here
704-
return E_PENTAGON;
781+
return -1L;
705782
} else if (oldLeadingDigit == CoordIJK.Direction.JK_AXES_DIGIT.digit()) {
706783
// Rotate out of the deleted k subsequence
707784
// We also need an additional change to the direction we're
708785
// moving in
709786
current = H3Index.h3Rotate60ccw(current);
710-
rotations[0] = rotations[0] + 1;
711787
} else if (oldLeadingDigit == CoordIJK.Direction.IK_AXES_DIGIT.digit()) {
712788
// Rotate out of the deleted k subsequence
713789
// We also need an additional change to the direction we're
714790
// moving in
715791
current = H3Index.h3Rotate60cw(current);
716-
rotations[0] = rotations[0] + 5;
717792
} else {
718793
// Should never occur
719-
return E_FAILED; // LCOV_EXCL_LINE
794+
throw new IllegalArgumentException("Undefined error looking for neighbor"); // LCOV_EXCL_LINE
720795
}
721796
}
722797
}
723798

724-
for (int i = 0; i < newRotations; i++)
799+
for (int i = 0; i < newRotations; i++) {
725800
current = H3Index.h3RotatePent60ccw(current);
726-
727-
// Account for differing orientation of the base cells (this edge
728-
// might not follow properties of some other edges.)
729-
if (oldBaseCell != newBaseCell) {
730-
if (BaseCells.isBaseCellPolarPentagon(newBaseCell)) {
731-
// 'polar' base cells behave differently because they have all
732-
// i neighbors.
733-
if (oldBaseCell != 118
734-
&& oldBaseCell != 8
735-
&& H3Index.h3LeadingNonZeroDigit(current) != CoordIJK.Direction.JK_AXES_DIGIT.digit()) {
736-
rotations[0] = rotations[0] + 1;
737-
}
738-
} else if (H3Index.h3LeadingNonZeroDigit(current) == CoordIJK.Direction.IK_AXES_DIGIT.digit()
739-
&& alreadyAdjustedKSubsequence == false) {
740-
// account for distortion introduced to the 5 neighbor by the
741-
// deleted k subsequence.
742-
rotations[0] = rotations[0] + 1;
743-
}
744801
}
745802
} else {
746803
for (int i = 0; i < newRotations; i++)
747804
current = H3Index.h3Rotate60ccw(current);
748805
}
749-
750-
rotations[0] = (rotations[0] + newRotations) % 6;
751-
out[0] = current;
752-
753-
return E_SUCCESS;
806+
return current;
754807
}
755808

756809
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.elasticsearch.h3;
20+
21+
import org.apache.lucene.tests.geo.GeoTestUtil;
22+
import org.elasticsearch.test.ESTestCase;
23+
24+
import java.util.Arrays;
25+
26+
public class HexRingTests extends ESTestCase {
27+
28+
public void testHexRing() {
29+
for (int i = 0; i < 500; i++) {
30+
double lat = GeoTestUtil.nextLatitude();
31+
double lon = GeoTestUtil.nextLongitude();
32+
for (int res = 0; res <= Constants.MAX_H3_RES; res++) {
33+
String origin = H3.geoToH3Address(lat, lon, res);
34+
assertFalse(H3.areNeighborCells(origin, origin));
35+
String[] ring = H3.hexRing(origin);
36+
Arrays.sort(ring);
37+
for (String destination : ring) {
38+
assertTrue(H3.areNeighborCells(origin, destination));
39+
String[] newRing = H3.hexRing(destination);
40+
for (String newDestination : newRing) {
41+
if (Arrays.binarySearch(ring, newDestination) >= 0) {
42+
assertTrue(H3.areNeighborCells(origin, newDestination));
43+
} else {
44+
assertFalse(H3.areNeighborCells(origin, newDestination));
45+
}
46+
}
47+
48+
}
49+
}
50+
}
51+
}
52+
}

0 commit comments

Comments
 (0)