Skip to content

Commit 79ce6e3

Browse files
authored
Improve performance of H3.h3ToGeoBoundary (#117812)
There are two clear code paths depending if a h3 bin belongs to even resolutions (class II) or uneven resolutions (class III). especializing the code paths for each type leads to an improvement in performance.
1 parent 2a30fbc commit 79ce6e3

File tree

2 files changed

+142
-105
lines changed

2 files changed

+142
-105
lines changed

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

Lines changed: 139 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -417,51 +417,72 @@ public LatLng faceIjkToGeo(int res) {
417417
* for this FaceIJK address at a specified resolution.
418418
*
419419
* @param res The H3 resolution of the cell.
420-
* @param start The first topological vertex to return.
421-
* @param length The number of topological vertexes to return.
422420
*/
423-
public CellBoundary faceIjkPentToCellBoundary(int res, int start, int length) {
421+
public CellBoundary faceIjkPentToCellBoundary(int res) {
424422
// adjust the center point to be in an aperture 33r substrate grid
425423
// these should be composed for speed
426424
this.coord.downAp3();
427425
this.coord.downAp3r();
428426
// if res is Class III we need to add a cw aperture 7 to get to
429427
// icosahedral Class II
430-
int adjRes = res;
431-
if (H3Index.isResolutionClassIII(res)) {
432-
this.coord.downAp7r();
433-
adjRes += 1;
434-
}
428+
final int adjRes = adjustRes(this.coord, res);
429+
435430
// If we're returning the entire loop, we need one more iteration in case
436431
// of a distortion vertex on the last edge
437-
final int additionalIteration = length == Constants.NUM_PENT_VERTS ? 1 : 0;
438-
final boolean isResolutionClassIII = H3Index.isResolutionClassIII(res);
439-
// convert each vertex to lat/lng
440-
// adjust the face of each vertex as appropriate and introduce
441-
// edge-crossing vertices as needed
432+
if (H3Index.isResolutionClassIII(res)) {
433+
return faceIjkPentToCellBoundaryClassIII(adjRes);
434+
} else {
435+
return faceIjkPentToCellBoundaryClassII(adjRes);
436+
}
437+
}
438+
439+
private CellBoundary faceIjkPentToCellBoundaryClassII(int adjRes) {
440+
final LatLng[] points = new LatLng[Constants.NUM_PENT_VERTS];
441+
final FaceIJK fijk = new FaceIJK(this.face, new CoordIJK(0, 0, 0));
442+
for (int vert = 0; vert < Constants.NUM_PENT_VERTS; vert++) {
443+
// The center point is now in the same substrate grid as the origin
444+
// cell vertices. Add the center point substate coordinates
445+
// to each vertex to translate the vertices to that cell.
446+
fijk.coord.reset(
447+
VERTEX_CLASSII[vert][0] + this.coord.i,
448+
VERTEX_CLASSII[vert][1] + this.coord.j,
449+
VERTEX_CLASSII[vert][2] + this.coord.k
450+
);
451+
fijk.coord.ijkNormalize();
452+
fijk.face = this.face;
453+
454+
fijk.adjustPentVertOverage(adjRes);
455+
456+
points[vert] = fijk.coord.ijkToGeo(fijk.face, adjRes, true);
457+
}
458+
return new CellBoundary(points, Constants.NUM_PENT_VERTS);
459+
}
460+
461+
private CellBoundary faceIjkPentToCellBoundaryClassIII(int adjRes) {
442462
final LatLng[] points = new LatLng[CellBoundary.MAX_CELL_BNDRY_VERTS];
443463
int numPoints = 0;
444-
final CoordIJK scratch = new CoordIJK(0, 0, 0);
445-
final FaceIJK fijk = new FaceIJK(this.face, scratch);
446-
final int[][] coord = isResolutionClassIII ? VERTEX_CLASSIII : VERTEX_CLASSII;
464+
final FaceIJK fijk = new FaceIJK(this.face, new CoordIJK(0, 0, 0));
447465
final CoordIJK lastCoord = new CoordIJK(0, 0, 0);
448466
int lastFace = this.face;
449-
for (int vert = start; vert < start + length + additionalIteration; vert++) {
467+
for (int vert = 0; vert < Constants.NUM_PENT_VERTS + 1; vert++) {
450468
final int v = vert % Constants.NUM_PENT_VERTS;
451469
// The center point is now in the same substrate grid as the origin
452470
// cell vertices. Add the center point substate coordinates
453471
// to each vertex to translate the vertices to that cell.
454-
scratch.reset(coord[v][0], coord[v][1], coord[v][2]);
455-
scratch.ijkAdd(this.coord.i, this.coord.j, this.coord.k);
456-
scratch.ijkNormalize();
472+
fijk.coord.reset(
473+
VERTEX_CLASSIII[v][0] + this.coord.i,
474+
VERTEX_CLASSIII[v][1] + this.coord.j,
475+
VERTEX_CLASSIII[v][2] + this.coord.k
476+
);
477+
fijk.coord.ijkNormalize();
457478
fijk.face = this.face;
458479

459480
fijk.adjustPentVertOverage(adjRes);
460481

461482
// all Class III pentagon edges cross icosa edges
462483
// note that Class II pentagons have vertices on the edge,
463484
// not edge intersections
464-
if (isResolutionClassIII && vert > start) {
485+
if (vert > 0) {
465486
// find hex2d of the two vertexes on the last face
466487
final Vec2d orig2d0 = lastCoord.ijkToHex2d();
467488

@@ -480,35 +501,17 @@ public CellBoundary faceIjkPentToCellBoundary(int res, int start, int length) {
480501

481502
final Vec2d orig2d1 = lastCoord.ijkToHex2d();
482503

483-
// find the appropriate icosa face edge vertexes
484-
final Vec2d edge0;
485-
final Vec2d edge1;
486-
switch (adjacentFaceDir[fijkOrient.face][fijk.face]) {
487-
case IJ -> {
488-
edge0 = maxDimByCIIVec2d[adjRes][0];
489-
edge1 = maxDimByCIIVec2d[adjRes][1];
490-
}
491-
case JK -> {
492-
edge0 = maxDimByCIIVec2d[adjRes][1];
493-
edge1 = maxDimByCIIVec2d[adjRes][2];
494-
}
495-
// case KI:
496-
default -> {
497-
assert (adjacentFaceDir[fijkOrient.face][fijk.face] == KI);
498-
edge0 = maxDimByCIIVec2d[adjRes][2];
499-
edge1 = maxDimByCIIVec2d[adjRes][0];
500-
}
501-
}
502-
503504
// find the intersection and add the lat/lng point to the result
504-
final Vec2d inter = Vec2d.v2dIntersect(orig2d0, orig2d1, edge0, edge1);
505-
points[numPoints++] = inter.hex2dToGeo(fijkOrient.face, adjRes, true);
505+
final Vec2d inter = findIntersectionPoint(orig2d0, orig2d1, adjRes, adjacentFaceDir[fijkOrient.face][fijk.face]);
506+
if (inter != null) {
507+
points[numPoints++] = inter.hex2dToGeo(fijkOrient.face, adjRes, true);
508+
}
506509
}
507510

508511
// convert vertex to lat/lng and add to the result
509512
// vert == start + NUM_PENT_VERTS is only used to test for possible
510513
// intersection on last edge
511-
if (vert < start + Constants.NUM_PENT_VERTS) {
514+
if (vert < Constants.NUM_PENT_VERTS) {
512515
points[numPoints++] = fijk.coord.ijkToGeo(fijk.face, adjRes, true);
513516
}
514517
lastFace = fijk.face;
@@ -522,43 +525,72 @@ public CellBoundary faceIjkPentToCellBoundary(int res, int start, int length) {
522525
* FaceIJK address at a specified resolution.
523526
*
524527
* @param res The H3 resolution of the cell.
525-
* @param start The first topological vertex to return.
526-
* @param length The number of topological vertexes to return.
527528
*/
528-
public CellBoundary faceIjkToCellBoundary(final int res, final int start, final int length) {
529+
public CellBoundary faceIjkToCellBoundary(final int res) {
529530
// adjust the center point to be in an aperture 33r substrate grid
530531
// these should be composed for speed
531532
this.coord.downAp3();
532533
this.coord.downAp3r();
533534

534535
// if res is Class III we need to add a cw aperture 7 to get to
535536
// icosahedral Class II
536-
int adjRes = res;
537-
if (H3Index.isResolutionClassIII(res)) {
538-
this.coord.downAp7r();
539-
adjRes += 1;
540-
}
537+
final int adjRes = adjustRes(this.coord, res);
541538

542-
// If we're returning the entire loop, we need one more iteration in case
543-
// of a distortion vertex on the last edge
544-
final int additionalIteration = length == Constants.NUM_HEX_VERTS ? 1 : 0;
545-
final boolean isResolutionClassIII = H3Index.isResolutionClassIII(res);
546539
// convert each vertex to lat/lng
547540
// adjust the face of each vertex as appropriate and introduce
548541
// edge-crossing vertices as needed
542+
if (H3Index.isResolutionClassIII(res)) {
543+
return faceIjkToCellBoundaryClassIII(adjRes);
544+
} else {
545+
return faceIjkToCellBoundaryClassII(adjRes);
546+
}
547+
}
548+
549+
private static int adjustRes(CoordIJK coord, int res) {
550+
if (H3Index.isResolutionClassIII(res)) {
551+
coord.downAp7r();
552+
res += 1;
553+
}
554+
return res;
555+
}
556+
557+
private CellBoundary faceIjkToCellBoundaryClassII(int adjRes) {
558+
final LatLng[] points = new LatLng[Constants.NUM_HEX_VERTS];
559+
final FaceIJK fijk = new FaceIJK(this.face, new CoordIJK(0, 0, 0));
560+
for (int vert = 0; vert < Constants.NUM_HEX_VERTS; vert++) {
561+
fijk.coord.reset(
562+
VERTEX_CLASSII[vert][0] + this.coord.i,
563+
VERTEX_CLASSII[vert][1] + this.coord.j,
564+
VERTEX_CLASSII[vert][2] + this.coord.k
565+
);
566+
fijk.coord.ijkNormalize();
567+
fijk.face = this.face;
568+
569+
fijk.adjustOverageClassII(adjRes, false, true);
570+
571+
// convert vertex to lat/lng and add to the result
572+
// vert == start + NUM_HEX_VERTS is only used to test for possible
573+
// intersection on last edge
574+
points[vert] = fijk.coord.ijkToGeo(fijk.face, adjRes, true);
575+
}
576+
return new CellBoundary(points, Constants.NUM_HEX_VERTS);
577+
}
578+
579+
private CellBoundary faceIjkToCellBoundaryClassIII(int adjRes) {
549580
final LatLng[] points = new LatLng[CellBoundary.MAX_CELL_BNDRY_VERTS];
550581
int numPoints = 0;
551-
final CoordIJK scratch1 = new CoordIJK(0, 0, 0);
552-
final FaceIJK fijk = new FaceIJK(this.face, scratch1);
553-
final CoordIJK scratch2 = isResolutionClassIII ? new CoordIJK(0, 0, 0) : null;
554-
final int[][] verts = isResolutionClassIII ? VERTEX_CLASSIII : VERTEX_CLASSII;
582+
final FaceIJK fijk = new FaceIJK(this.face, new CoordIJK(0, 0, 0));
583+
final CoordIJK scratch = new CoordIJK(0, 0, 0);
555584
int lastFace = -1;
556585
Overage lastOverage = Overage.NO_OVERAGE;
557-
for (int vert = start; vert < start + length + additionalIteration; vert++) {
558-
int v = vert % Constants.NUM_HEX_VERTS;
559-
scratch1.reset(verts[v][0], verts[v][1], verts[v][2]);
560-
scratch1.ijkAdd(this.coord.i, this.coord.j, this.coord.k);
561-
scratch1.ijkNormalize();
586+
for (int vert = 0; vert < Constants.NUM_HEX_VERTS + 1; vert++) {
587+
final int v = vert % Constants.NUM_HEX_VERTS;
588+
fijk.coord.reset(
589+
VERTEX_CLASSIII[v][0] + this.coord.i,
590+
VERTEX_CLASSIII[v][1] + this.coord.j,
591+
VERTEX_CLASSIII[v][2] + this.coord.k
592+
);
593+
fijk.coord.ijkNormalize();
562594
fijk.face = this.face;
563595

564596
final Overage overage = fijk.adjustOverageClassII(adjRes, false, true);
@@ -572,58 +604,28 @@ public CellBoundary faceIjkToCellBoundary(final int res, final int start, final
572604
projection. Note that Class II cell edges have vertices on the face
573605
edge, with no edge line intersections.
574606
*/
575-
if (isResolutionClassIII && vert > start && fijk.face != lastFace && lastOverage != Overage.FACE_EDGE) {
607+
if (vert > 0 && fijk.face != lastFace && lastOverage != Overage.FACE_EDGE) {
576608
// find hex2d of the two vertexes on original face
577609
final int lastV = (v + 5) % Constants.NUM_HEX_VERTS;
578610
// The center point is now in the same substrate grid as the origin
579611
// cell vertices. Add the center point substate coordinates
580612
// to each vertex to translate the vertices to that cell.
581-
final int[] vertexLast = verts[lastV];
582-
final int[] vertexV = verts[v];
583-
scratch2.reset(vertexLast[0] + this.coord.i, vertexLast[1] + this.coord.j, vertexLast[2] + this.coord.k);
584-
scratch2.ijkNormalize();
585-
final Vec2d orig2d0 = scratch2.ijkToHex2d();
586-
scratch2.reset(vertexV[0] + this.coord.i, vertexV[1] + this.coord.j, vertexV[2] + this.coord.k);
587-
scratch2.ijkNormalize();
588-
final Vec2d orig2d1 = scratch2.ijkToHex2d();
613+
final Vec2d orig2d0 = orig(scratch, VERTEX_CLASSIII[lastV]);
614+
final Vec2d orig2d1 = orig(scratch, VERTEX_CLASSIII[v]);
589615

590616
// find the appropriate icosa face edge vertexes
591617
final int face2 = ((lastFace == this.face) ? fijk.face : lastFace);
592-
final Vec2d edge0;
593-
final Vec2d edge1;
594-
switch (adjacentFaceDir[this.face][face2]) {
595-
case IJ -> {
596-
edge0 = maxDimByCIIVec2d[adjRes][0];
597-
edge1 = maxDimByCIIVec2d[adjRes][1];
598-
}
599-
case JK -> {
600-
edge0 = maxDimByCIIVec2d[adjRes][1];
601-
edge1 = maxDimByCIIVec2d[adjRes][2];
602-
}
603-
// case KI:
604-
default -> {
605-
assert (adjacentFaceDir[this.face][face2] == KI);
606-
edge0 = maxDimByCIIVec2d[adjRes][2];
607-
edge1 = maxDimByCIIVec2d[adjRes][0];
608-
}
609-
}
610618
// find the intersection and add the lat/lng point to the result
611-
final Vec2d inter = Vec2d.v2dIntersect(orig2d0, orig2d1, edge0, edge1);
612-
/*
613-
If a point of intersection occurs at a hexagon vertex, then each
614-
adjacent hexagon edge will lie completely on a single icosahedron
615-
face, and no additional vertex is required.
616-
*/
617-
final boolean isIntersectionAtVertex = orig2d0.numericallyIdentical(inter) || orig2d1.numericallyIdentical(inter);
618-
if (isIntersectionAtVertex == false) {
619+
final Vec2d inter = findIntersectionPoint(orig2d0, orig2d1, adjRes, adjacentFaceDir[this.face][face2]);
620+
if (inter != null) {
619621
points[numPoints++] = inter.hex2dToGeo(this.face, adjRes, true);
620622
}
621623
}
622624

623625
// convert vertex to lat/lng and add to the result
624626
// vert == start + NUM_HEX_VERTS is only used to test for possible
625627
// intersection on last edge
626-
if (vert < start + Constants.NUM_HEX_VERTS) {
628+
if (vert < Constants.NUM_HEX_VERTS) {
627629
points[numPoints++] = fijk.coord.ijkToGeo(fijk.face, adjRes, true);
628630
}
629631
lastFace = fijk.face;
@@ -632,6 +634,42 @@ public CellBoundary faceIjkToCellBoundary(final int res, final int start, final
632634
return new CellBoundary(points, numPoints);
633635
}
634636

637+
private Vec2d orig(CoordIJK scratch, int[] vertexLast) {
638+
scratch.reset(vertexLast[0] + this.coord.i, vertexLast[1] + this.coord.j, vertexLast[2] + this.coord.k);
639+
scratch.ijkNormalize();
640+
return scratch.ijkToHex2d();
641+
}
642+
643+
private Vec2d findIntersectionPoint(Vec2d orig2d0, Vec2d orig2d1, int adjRes, int faceDir) {
644+
// find the appropriate icosa face edge vertexes
645+
final Vec2d edge0;
646+
final Vec2d edge1;
647+
switch (faceDir) {
648+
case IJ -> {
649+
edge0 = maxDimByCIIVec2d[adjRes][0];
650+
edge1 = maxDimByCIIVec2d[adjRes][1];
651+
}
652+
case JK -> {
653+
edge0 = maxDimByCIIVec2d[adjRes][1];
654+
edge1 = maxDimByCIIVec2d[adjRes][2];
655+
}
656+
// case KI:
657+
default -> {
658+
assert (faceDir == KI);
659+
edge0 = maxDimByCIIVec2d[adjRes][2];
660+
edge1 = maxDimByCIIVec2d[adjRes][0];
661+
}
662+
}
663+
// find the intersection and add the lat/lng point to the result
664+
final Vec2d inter = Vec2d.v2dIntersect(orig2d0, orig2d1, edge0, edge1);
665+
/*
666+
If a point of intersection occurs at a hexagon vertex, then each
667+
adjacent hexagon edge will lie completely on a single icosahedron
668+
face, and no additional vertex is required.
669+
*/
670+
return orig2d0.numericallyIdentical(inter) || orig2d1.numericallyIdentical(inter) ? null : inter;
671+
}
672+
635673
/**
636674
* compute the corresponding H3Index.
637675
* @param res The cell resolution.
@@ -651,7 +689,6 @@ static long faceIjkToH3(int res, int face, CoordIJK coord) {
651689
// out of range input
652690
throw new IllegalArgumentException(" out of range input");
653691
}
654-
655692
return H3Index.H3_set_base_cell(h, BaseCells.getBaseCell(face, coord));
656693
}
657694

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -174,11 +174,11 @@ public static LatLng h3ToLatLng(String h3Address) {
174174
* Find the cell {@link CellBoundary} coordinates for the cell
175175
*/
176176
public static CellBoundary h3ToGeoBoundary(long h3) {
177-
FaceIJK fijk = H3Index.h3ToFaceIjk(h3);
177+
final FaceIJK fijk = H3Index.h3ToFaceIjk(h3);
178178
if (H3Index.H3_is_pentagon(h3)) {
179-
return fijk.faceIjkPentToCellBoundary(H3Index.H3_get_resolution(h3), 0, Constants.NUM_PENT_VERTS);
179+
return fijk.faceIjkPentToCellBoundary(H3Index.H3_get_resolution(h3));
180180
} else {
181-
return fijk.faceIjkToCellBoundary(H3Index.H3_get_resolution(h3), 0, Constants.NUM_HEX_VERTS);
181+
return fijk.faceIjkToCellBoundary(H3Index.H3_get_resolution(h3));
182182
}
183183
}
184184

0 commit comments

Comments
 (0)