diff --git a/build.gradle b/build.gradle index 28d55e76e6..f08eacce92 100644 --- a/build.gradle +++ b/build.gradle @@ -136,6 +136,8 @@ publishing { // Dependencies //****************************************************************************/ dependencies { + implementation "org.opensearch.plugin:geo:${opensearch_version}" + api project(":libs:h3") yamlRestTestRuntimeOnly "org.apache.logging.log4j:log4j-core:${versions.log4j}" testImplementation "org.hamcrest:hamcrest:${versions.hamcrest}" testImplementation 'org.json:json:20211205' diff --git a/libs/build.gradle b/libs/build.gradle new file mode 100644 index 0000000000..32bdf69d2b --- /dev/null +++ b/libs/build.gradle @@ -0,0 +1,7 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +subprojects { + apply plugin: 'opensearch.build' +} diff --git a/libs/h3/LICENSE.txt b/libs/h3/LICENSE.txt new file mode 100644 index 0000000000..3ab280ebc8 --- /dev/null +++ b/libs/h3/LICENSE.txt @@ -0,0 +1,204 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +This project is based on a modification of https://github.com/uber/h3 which is licensed under the Apache 2.0 License. diff --git a/libs/h3/NOTICE.txt b/libs/h3/NOTICE.txt new file mode 100644 index 0000000000..5201d8e6e2 --- /dev/null +++ b/libs/h3/NOTICE.txt @@ -0,0 +1,25 @@ +OpenSearch (https://opensearch.org/) +Copyright OpenSearch Contributors + +-- +Elastic-hex + +Copyright 2022 Elasticsearch B.V. + +-- + +This project is based on a modification of https://github.com/uber/h3 which is licensed under the Apache 2.0 License. + +Copyright 2017-2021 Uber Technologies, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/libs/h3/build.gradle b/libs/h3/build.gradle new file mode 100644 index 0000000000..597aa0e53a --- /dev/null +++ b/libs/h3/build.gradle @@ -0,0 +1,74 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +apply plugin: 'opensearch.build' +apply plugin: 'opensearch.publish' + +tasks.named('forbiddenApisMain').configure { + replaceSignatureFiles 'jdk-signatures' +} + +repositories { + mavenLocal() + maven { url "https://aws.oss.sonatype.org/content/repositories/snapshots" } + mavenCentral() + maven { url "https://plugins.gradle.org/m2/" } +} + +dependencies { + api "org.apache.logging.log4j:log4j-api:${versions.log4j}" + api "org.apache.logging.log4j:log4j-core:${versions.log4j}" + testImplementation "org.opensearch.test:framework:${opensearch_version}" + testImplementation "org.apache.commons:commons-compress:1.21" +} +licenseFile = "LICENSE.txt" +noticeFile = "NOTICE.txt" + +project.dependencyLicenses.enabled = false +project.thirdPartyAudit.enabled = false +project.loggerUsageCheck.enabled = false +project.forbiddenApis.ignoreFailures = true + +publishing { + publications { + pluginZip(MavenPublication) { publication -> + pom { + name = "opensearch-geospatial-h3" + description = 'OpenSearch Geospatial H3 library' + licenses { + license { + name = "The Apache License, Version 2.0" + url = "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + } + developers { + developer { + name = "OpenSearch" + url = "https://github.com/opensearch-project/geospatial/libs/h3" + } + } + } + } + } +} + + diff --git a/libs/h3/src/main/java/org/opensearch/geospatial/h3/BaseCells.java b/libs/h3/src/main/java/org/opensearch/geospatial/h3/BaseCells.java new file mode 100644 index 0000000000..5e5bd93c4b --- /dev/null +++ b/libs/h3/src/main/java/org/opensearch/geospatial/h3/BaseCells.java @@ -0,0 +1,656 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * This project is based on a modification of https://github.com/uber/h3 which is licensed under the Apache 2.0 License. + * + * Copyright 2016-2018 Uber Technologies, Inc. + */ +package org.opensearch.geospatial.h3; + +/** + * Base cell related lookup tables and access functions. + */ +final class BaseCells { + + private static class BaseCellData { + // "home" face and normalized ijk coordinates on that face + final int homeFace; + final int homeI; + final int homeJ; + final int homeK; + // is this base cell a pentagon? + final boolean isPentagon; + // if a pentagon, what are its two clockwise offset + final int[] cwOffsetPent; + + /// faces? + BaseCellData(int homeFace, int homeI, int homeJ, int homeK, boolean isPentagon, int[] cwOffsetPent) { + this.homeFace = homeFace; + this.homeI = homeI; + this.homeJ = homeJ; + this.homeK = homeK; + this.isPentagon = isPentagon; + this.cwOffsetPent = cwOffsetPent; + } + } + + /** + * Resolution 0 base cell data table. + *

+ * For each base cell, gives the "home" face and ijk+ coordinates on that face, + * whether or not the base cell is a pentagon. Additionally, if the base cell + * is a pentagon, the two cw offset rotation adjacent faces are given (-1 + * indicates that no cw offset rotation faces exist for this base cell). + */ + private static final BaseCellData[] baseCellData = new BaseCellData[] { + new BaseCellData(1, 1, 0, 0, false, new int[] { 0, 0 }), // base cell 0 + new BaseCellData(2, 1, 1, 0, false, new int[] { 0, 0 }), // base cell 1 + new BaseCellData(1, 0, 0, 0, false, new int[] { 0, 0 }), // base cell 2 + new BaseCellData(2, 1, 0, 0, false, new int[] { 0, 0 }), // base cell 3 + new BaseCellData(0, 2, 0, 0, true, new int[] { -1, -1 }), // base cell 4 + new BaseCellData(1, 1, 1, 0, false, new int[] { 0, 0 }), // base cell 5 + new BaseCellData(1, 0, 0, 1, false, new int[] { 0, 0 }), // base cell 6 + new BaseCellData(2, 0, 0, 0, false, new int[] { 0, 0 }), // base cell 7 + new BaseCellData(0, 1, 0, 0, false, new int[] { 0, 0 }), // base cell 8 + new BaseCellData(2, 0, 1, 0, false, new int[] { 0, 0 }), // base cell 9 + new BaseCellData(1, 0, 1, 0, false, new int[] { 0, 0 }), // base cell 10 + new BaseCellData(1, 0, 1, 1, false, new int[] { 0, 0 }), // base cell 11 + new BaseCellData(3, 1, 0, 0, false, new int[] { 0, 0 }), // base cell 12 + new BaseCellData(3, 1, 1, 0, false, new int[] { 0, 0 }), // base cell 13 + new BaseCellData(11, 2, 0, 0, true, new int[] { 2, 6 }), // base cell 14 + new BaseCellData(4, 1, 0, 0, false, new int[] { 0, 0 }), // base cell 15 + new BaseCellData(0, 0, 0, 0, false, new int[] { 0, 0 }), // base cell 16 + new BaseCellData(6, 0, 1, 0, false, new int[] { 0, 0 }), // base cell 17 + new BaseCellData(0, 0, 0, 1, false, new int[] { 0, 0 }), // base cell 18 + new BaseCellData(2, 0, 1, 1, false, new int[] { 0, 0 }), // base cell 19 + new BaseCellData(7, 0, 0, 1, false, new int[] { 0, 0 }), // base cell 20 + new BaseCellData(2, 0, 0, 1, false, new int[] { 0, 0 }), // base cell 21 + new BaseCellData(0, 1, 1, 0, false, new int[] { 0, 0 }), // base cell 22 + new BaseCellData(6, 0, 0, 1, false, new int[] { 0, 0 }), // base cell 23 + new BaseCellData(10, 2, 0, 0, true, new int[] { 1, 5 }), // base cell 24 + new BaseCellData(6, 0, 0, 0, false, new int[] { 0, 0 }), // base cell 25 + new BaseCellData(3, 0, 0, 0, false, new int[] { 0, 0 }), // base cell 26 + new BaseCellData(11, 1, 0, 0, false, new int[] { 0, 0 }), // base cell 27 + new BaseCellData(4, 1, 1, 0, false, new int[] { 0, 0 }), // base cell 28 + new BaseCellData(3, 0, 1, 0, false, new int[] { 0, 0 }), // base cell 29 + new BaseCellData(0, 0, 1, 1, false, new int[] { 0, 0 }), // base cell 30 + new BaseCellData(4, 0, 0, 0, false, new int[] { 0, 0 }), // base cell 31 + new BaseCellData(5, 0, 1, 0, false, new int[] { 0, 0 }), // base cell 32 + new BaseCellData(0, 0, 1, 0, false, new int[] { 0, 0 }), // base cell 33 + new BaseCellData(7, 0, 1, 0, false, new int[] { 0, 0 }), // base cell 34 + new BaseCellData(11, 1, 1, 0, false, new int[] { 0, 0 }), // base cell 35 + new BaseCellData(7, 0, 0, 0, false, new int[] { 0, 0 }), // base cell 36 + new BaseCellData(10, 1, 0, 0, false, new int[] { 0, 0 }), // base cell 37 + new BaseCellData(12, 2, 0, 0, true, new int[] { 3, 7 }), // base cell 38 + new BaseCellData(6, 1, 0, 1, false, new int[] { 0, 0 }), // base cell 39 + new BaseCellData(7, 1, 0, 1, false, new int[] { 0, 0 }), // base cell 40 + new BaseCellData(4, 0, 0, 1, false, new int[] { 0, 0 }), // base cell 41 + new BaseCellData(3, 0, 0, 1, false, new int[] { 0, 0 }), // base cell 42 + new BaseCellData(3, 0, 1, 1, false, new int[] { 0, 0 }), // base cell 43 + new BaseCellData(4, 0, 1, 0, false, new int[] { 0, 0 }), // base cell 44 + new BaseCellData(6, 1, 0, 0, false, new int[] { 0, 0 }), // base cell 45 + new BaseCellData(11, 0, 0, 0, false, new int[] { 0, 0 }), // base cell 46 + new BaseCellData(8, 0, 0, 1, false, new int[] { 0, 0 }), // base cell 47 + new BaseCellData(5, 0, 0, 1, false, new int[] { 0, 0 }), // base cell 48 + new BaseCellData(14, 2, 0, 0, true, new int[] { 0, 9 }), // base cell 49 + new BaseCellData(5, 0, 0, 0, false, new int[] { 0, 0 }), // base cell 50 + new BaseCellData(12, 1, 0, 0, false, new int[] { 0, 0 }), // base cell 51 + new BaseCellData(10, 1, 1, 0, false, new int[] { 0, 0 }), // base cell 52 + new BaseCellData(4, 0, 1, 1, false, new int[] { 0, 0 }), // base cell 53 + new BaseCellData(12, 1, 1, 0, false, new int[] { 0, 0 }), // base cell 54 + new BaseCellData(7, 1, 0, 0, false, new int[] { 0, 0 }), // base cell 55 + new BaseCellData(11, 0, 1, 0, false, new int[] { 0, 0 }), // base cell 56 + new BaseCellData(10, 0, 0, 0, false, new int[] { 0, 0 }), // base cell 57 + new BaseCellData(13, 2, 0, 0, true, new int[] { 4, 8 }), // base cell 58 + new BaseCellData(10, 0, 0, 1, false, new int[] { 0, 0 }), // base cell 59 + new BaseCellData(11, 0, 0, 1, false, new int[] { 0, 0 }), // base cell 60 + new BaseCellData(9, 0, 1, 0, false, new int[] { 0, 0 }), // base cell 61 + new BaseCellData(8, 0, 1, 0, false, new int[] { 0, 0 }), // base cell 62 + new BaseCellData(6, 2, 0, 0, true, new int[] { 11, 15 }), // base cell 63 + new BaseCellData(8, 0, 0, 0, false, new int[] { 0, 0 }), // base cell 64 + new BaseCellData(9, 0, 0, 1, false, new int[] { 0, 0 }), // base cell 65 + new BaseCellData(14, 1, 0, 0, false, new int[] { 0, 0 }), // base cell 66 + new BaseCellData(5, 1, 0, 1, false, new int[] { 0, 0 }), // base cell 67 + new BaseCellData(16, 0, 1, 1, false, new int[] { 0, 0 }), // base cell 68 + new BaseCellData(8, 1, 0, 1, false, new int[] { 0, 0 }), // base cell 69 + new BaseCellData(5, 1, 0, 0, false, new int[] { 0, 0 }), // base cell 70 + new BaseCellData(12, 0, 0, 0, false, new int[] { 0, 0 }), // base cell 71 + new BaseCellData(7, 2, 0, 0, true, new int[] { 12, 16 }), // base cell 72 + new BaseCellData(12, 0, 1, 0, false, new int[] { 0, 0 }), // base cell 73 + new BaseCellData(10, 0, 1, 0, false, new int[] { 0, 0 }), // base cell 74 + new BaseCellData(9, 0, 0, 0, false, new int[] { 0, 0 }), // base cell 75 + new BaseCellData(13, 1, 0, 0, false, new int[] { 0, 0 }), // base cell 76 + new BaseCellData(16, 0, 0, 1, false, new int[] { 0, 0 }), // base cell 77 + new BaseCellData(15, 0, 1, 1, false, new int[] { 0, 0 }), // base cell 78 + new BaseCellData(15, 0, 1, 0, false, new int[] { 0, 0 }), // base cell 79 + new BaseCellData(16, 0, 1, 0, false, new int[] { 0, 0 }), // base cell 80 + new BaseCellData(14, 1, 1, 0, false, new int[] { 0, 0 }), // base cell 81 + new BaseCellData(13, 1, 1, 0, false, new int[] { 0, 0 }), // base cell 82 + new BaseCellData(5, 2, 0, 0, true, new int[] { 10, 19 }), // base cell 83 + new BaseCellData(8, 1, 0, 0, false, new int[] { 0, 0 }), // base cell 84 + new BaseCellData(14, 0, 0, 0, false, new int[] { 0, 0 }), // base cell 85 + new BaseCellData(9, 1, 0, 1, false, new int[] { 0, 0 }), // base cell 86 + new BaseCellData(14, 0, 0, 1, false, new int[] { 0, 0 }), // base cell 87 + new BaseCellData(17, 0, 0, 1, false, new int[] { 0, 0 }), // base cell 88 + new BaseCellData(12, 0, 0, 1, false, new int[] { 0, 0 }), // base cell 89 + new BaseCellData(16, 0, 0, 0, false, new int[] { 0, 0 }), // base cell 90 + new BaseCellData(17, 0, 1, 1, false, new int[] { 0, 0 }), // base cell 91 + new BaseCellData(15, 0, 0, 1, false, new int[] { 0, 0 }), // base cell 92 + new BaseCellData(16, 1, 0, 1, false, new int[] { 0, 0 }), // base cell 93 + new BaseCellData(9, 1, 0, 0, false, new int[] { 0, 0 }), // base cell 94 + new BaseCellData(15, 0, 0, 0, false, new int[] { 0, 0 }), // base cell 95 + new BaseCellData(13, 0, 0, 0, false, new int[] { 0, 0 }), // base cell 96 + new BaseCellData(8, 2, 0, 0, true, new int[] { 13, 17 }), // base cell 97 + new BaseCellData(13, 0, 1, 0, false, new int[] { 0, 0 }), // base cell 98 + new BaseCellData(17, 1, 0, 1, false, new int[] { 0, 0 }), // base cell 99 + new BaseCellData(19, 0, 1, 0, false, new int[] { 0, 0 }), // base cell 100 + new BaseCellData(14, 0, 1, 0, false, new int[] { 0, 0 }), // base cell 101 + new BaseCellData(19, 0, 1, 1, false, new int[] { 0, 0 }), // base cell 102 + new BaseCellData(17, 0, 1, 0, false, new int[] { 0, 0 }), // base cell 103 + new BaseCellData(13, 0, 0, 1, false, new int[] { 0, 0 }), // base cell 104 + new BaseCellData(17, 0, 0, 0, false, new int[] { 0, 0 }), // base cell 105 + new BaseCellData(16, 1, 0, 0, false, new int[] { 0, 0 }), // base cell 106 + new BaseCellData(9, 2, 0, 0, true, new int[] { 14, 18 }), // base cell 107 + new BaseCellData(15, 1, 0, 1, false, new int[] { 0, 0 }), // base cell 108 + new BaseCellData(15, 1, 0, 0, false, new int[] { 0, 0 }), // base cell 109 + new BaseCellData(18, 0, 1, 1, false, new int[] { 0, 0 }), // base cell 110 + new BaseCellData(18, 0, 0, 1, false, new int[] { 0, 0 }), // base cell 111 + new BaseCellData(19, 0, 0, 1, false, new int[] { 0, 0 }), // base cell 112 + new BaseCellData(17, 1, 0, 0, false, new int[] { 0, 0 }), // base cell 113 + new BaseCellData(19, 0, 0, 0, false, new int[] { 0, 0 }), // base cell 114 + new BaseCellData(18, 0, 1, 0, false, new int[] { 0, 0 }), // base cell 115 + new BaseCellData(18, 1, 0, 1, false, new int[] { 0, 0 }), // base cell 116 + new BaseCellData(19, 2, 0, 0, true, new int[] { -1, -1 }), // base cell 117 + new BaseCellData(19, 1, 0, 0, false, new int[] { 0, 0 }), // base cell 118 + new BaseCellData(18, 0, 0, 0, false, new int[] { 0, 0 }), // base cell 119 + new BaseCellData(19, 1, 0, 1, false, new int[] { 0, 0 }), // base cell 120 + new BaseCellData(18, 1, 0, 0, false, new int[] { 0, 0 }) // base cell 121 + }; + + /** + * base cell at a given ijk and required rotations into its system + */ + private static class BaseCellRotation { + final int baseCell; // base cell number + final int ccwRot60; // number of ccw 60 degree rotations relative to current + /// face + + BaseCellRotation(int baseCell, int ccwRot60) { + this.baseCell = baseCell; + this.ccwRot60 = ccwRot60; + } + } + + /** @brief Resolution 0 base cell lookup table for each face. + * + * Given the face number and a resolution 0 ijk+ coordinate in that face's + * face-centered ijk coordinate system, gives the base cell located at that + * coordinate and the number of 60 ccw rotations to rotate into that base + * cell's orientation. + * + * Valid lookup coordinates are from (0, 0, 0) to (2, 2, 2). + * + * This table can be accessed using the functions `_faceIjkToBaseCell` and + * `_faceIjkToBaseCellCCWrot60` + */ + private static final BaseCellRotation[][][][] faceIjkBaseCells = new BaseCellRotation[][][][] { + {// face 0 + { + // i 0 + { new BaseCellRotation(16, 0), new BaseCellRotation(18, 0), new BaseCellRotation(24, 0) }, // j 0 + { new BaseCellRotation(33, 0), new BaseCellRotation(30, 0), new BaseCellRotation(32, 3) }, // j 1 + { new BaseCellRotation(49, 1), new BaseCellRotation(48, 3), new BaseCellRotation(50, 3) } // j 2 + }, + { + // i 1 + { new BaseCellRotation(8, 0), new BaseCellRotation(5, 5), new BaseCellRotation(10, 5) }, // j 0 + { new BaseCellRotation(22, 0), new BaseCellRotation(16, 0), new BaseCellRotation(18, 0) }, // j 1 + { new BaseCellRotation(41, 1), new BaseCellRotation(33, 0), new BaseCellRotation(30, 0) } // j 2 + }, + { + // i 2 + { new BaseCellRotation(4, 0), new BaseCellRotation(0, 5), new BaseCellRotation(2, 5) }, // j 0 + { new BaseCellRotation(15, 1), new BaseCellRotation(8, 0), new BaseCellRotation(5, 5) }, // j 1 + { new BaseCellRotation(31, 1), new BaseCellRotation(22, 0), new BaseCellRotation(16, 0) } // j 2 + } }, + {// face 1 + { + // i 0 + { new BaseCellRotation(2, 0), new BaseCellRotation(6, 0), new BaseCellRotation(14, 0) }, // j 0 + { new BaseCellRotation(10, 0), new BaseCellRotation(11, 0), new BaseCellRotation(17, 3) }, // j 1 + { new BaseCellRotation(24, 1), new BaseCellRotation(23, 3), new BaseCellRotation(25, 3) } // j 2 + }, + { + // i 1 + { new BaseCellRotation(0, 0), new BaseCellRotation(1, 5), new BaseCellRotation(9, 5) }, // j 0 + { new BaseCellRotation(5, 0), new BaseCellRotation(2, 0), new BaseCellRotation(6, 0) }, // j 1 + { new BaseCellRotation(18, 1), new BaseCellRotation(10, 0), new BaseCellRotation(11, 0) } // j 2 + }, + { + // i 2 + { new BaseCellRotation(4, 1), new BaseCellRotation(3, 5), new BaseCellRotation(7, 5) }, // j 0 + { new BaseCellRotation(8, 1), new BaseCellRotation(0, 0), new BaseCellRotation(1, 5) }, // j 1 + { new BaseCellRotation(16, 1), new BaseCellRotation(5, 0), new BaseCellRotation(2, 0) } // j 2 + } }, + {// face 2 + { + // i 0 + { new BaseCellRotation(7, 0), new BaseCellRotation(21, 0), new BaseCellRotation(38, 0) }, // j 0 + { new BaseCellRotation(9, 0), new BaseCellRotation(19, 0), new BaseCellRotation(34, 3) }, // j 1 + { new BaseCellRotation(14, 1), new BaseCellRotation(20, 3), new BaseCellRotation(36, 3) } // j 2 + }, + { + // i 1 + { new BaseCellRotation(3, 0), new BaseCellRotation(13, 5), new BaseCellRotation(29, 5) }, // j 0 + { new BaseCellRotation(1, 0), new BaseCellRotation(7, 0), new BaseCellRotation(21, 0) }, // j 1 + { new BaseCellRotation(6, 1), new BaseCellRotation(9, 0), new BaseCellRotation(19, 0) } // j 2 + }, + { + // i 2 + { new BaseCellRotation(4, 2), new BaseCellRotation(12, 5), new BaseCellRotation(26, 5) }, // j 0 + { new BaseCellRotation(0, 1), new BaseCellRotation(3, 0), new BaseCellRotation(13, 5) }, // j 1 + { new BaseCellRotation(2, 1), new BaseCellRotation(1, 0), new BaseCellRotation(7, 0) } // j 2 + } }, + {// face 3 + { + // i 0 + { new BaseCellRotation(26, 0), new BaseCellRotation(42, 0), new BaseCellRotation(58, 0) }, // j 0 + { new BaseCellRotation(29, 0), new BaseCellRotation(43, 0), new BaseCellRotation(62, 3) }, // j 1 + { new BaseCellRotation(38, 1), new BaseCellRotation(47, 3), new BaseCellRotation(64, 3) } // j 2 + }, + { + // i 1 + { new BaseCellRotation(12, 0), new BaseCellRotation(28, 5), new BaseCellRotation(44, 5) }, // j 0 + { new BaseCellRotation(13, 0), new BaseCellRotation(26, 0), new BaseCellRotation(42, 0) }, // j 1 + { new BaseCellRotation(21, 1), new BaseCellRotation(29, 0), new BaseCellRotation(43, 0) } // j 2 + }, + { + // i 2 + { new BaseCellRotation(4, 3), new BaseCellRotation(15, 5), new BaseCellRotation(31, 5) }, // j 0 + { new BaseCellRotation(3, 1), new BaseCellRotation(12, 0), new BaseCellRotation(28, 5) }, // j 1 + { new BaseCellRotation(7, 1), new BaseCellRotation(13, 0), new BaseCellRotation(26, 0) } // j 2 + } }, + {// face 4 + { + // i 0 + { new BaseCellRotation(31, 0), new BaseCellRotation(41, 0), new BaseCellRotation(49, 0) }, // j 0 + { new BaseCellRotation(44, 0), new BaseCellRotation(53, 0), new BaseCellRotation(61, 3) }, // j 1 + { new BaseCellRotation(58, 1), new BaseCellRotation(65, 3), new BaseCellRotation(75, 3) } // j 2 + }, + { + // i 1 + { new BaseCellRotation(15, 0), new BaseCellRotation(22, 5), new BaseCellRotation(33, 5) }, // j 0 + { new BaseCellRotation(28, 0), new BaseCellRotation(31, 0), new BaseCellRotation(41, 0) }, // j 1 + { new BaseCellRotation(42, 1), new BaseCellRotation(44, 0), new BaseCellRotation(53, 0) } // j 2 + }, + { + // i 2 + { new BaseCellRotation(4, 4), new BaseCellRotation(8, 5), new BaseCellRotation(16, 5) }, // j 0 + { new BaseCellRotation(12, 1), new BaseCellRotation(15, 0), new BaseCellRotation(22, 5) }, // j 1 + { new BaseCellRotation(26, 1), new BaseCellRotation(28, 0), new BaseCellRotation(31, 0) } // j 2 + } }, + {// face 5 + { + // i 0 + { new BaseCellRotation(50, 0), new BaseCellRotation(48, 0), new BaseCellRotation(49, 3) }, // j 0 + { new BaseCellRotation(32, 0), new BaseCellRotation(30, 3), new BaseCellRotation(33, 3) }, // j 1 + { new BaseCellRotation(24, 3), new BaseCellRotation(18, 3), new BaseCellRotation(16, 3) } // j 2 + }, + { + // i 1 + { new BaseCellRotation(70, 0), new BaseCellRotation(67, 0), new BaseCellRotation(66, 3) }, // j 0 + { new BaseCellRotation(52, 3), new BaseCellRotation(50, 0), new BaseCellRotation(48, 0) }, // j 1 + { new BaseCellRotation(37, 3), new BaseCellRotation(32, 0), new BaseCellRotation(30, 3) } // j 2 + }, + { + // i 2 + { new BaseCellRotation(83, 0), new BaseCellRotation(87, 3), new BaseCellRotation(85, 3) }, // j 0 + { new BaseCellRotation(74, 3), new BaseCellRotation(70, 0), new BaseCellRotation(67, 0) }, // j 1 + { new BaseCellRotation(57, 1), new BaseCellRotation(52, 3), new BaseCellRotation(50, 0) } // j 2 + } }, + {// face 6 + { + // i 0 + { new BaseCellRotation(25, 0), new BaseCellRotation(23, 0), new BaseCellRotation(24, 3) }, // j 0 + { new BaseCellRotation(17, 0), new BaseCellRotation(11, 3), new BaseCellRotation(10, 3) }, // j 1 + { new BaseCellRotation(14, 3), new BaseCellRotation(6, 3), new BaseCellRotation(2, 3) } // j 2 + }, + { + // i 1 + { new BaseCellRotation(45, 0), new BaseCellRotation(39, 0), new BaseCellRotation(37, 3) }, // j 0 + { new BaseCellRotation(35, 3), new BaseCellRotation(25, 0), new BaseCellRotation(23, 0) }, // j 1 + { new BaseCellRotation(27, 3), new BaseCellRotation(17, 0), new BaseCellRotation(11, 3) } // j 2 + }, + { + // i 2 + { new BaseCellRotation(63, 0), new BaseCellRotation(59, 3), new BaseCellRotation(57, 3) }, // j 0 + { new BaseCellRotation(56, 3), new BaseCellRotation(45, 0), new BaseCellRotation(39, 0) }, // j 1 + { new BaseCellRotation(46, 3), new BaseCellRotation(35, 3), new BaseCellRotation(25, 0) } // j 2 + } }, + {// face 7 + { + // i 0 + { new BaseCellRotation(36, 0), new BaseCellRotation(20, 0), new BaseCellRotation(14, 3) }, // j 0 + { new BaseCellRotation(34, 0), new BaseCellRotation(19, 3), new BaseCellRotation(9, 3) }, // j 1 + { new BaseCellRotation(38, 3), new BaseCellRotation(21, 3), new BaseCellRotation(7, 3) } // j 2 + }, + { + // i 1 + { new BaseCellRotation(55, 0), new BaseCellRotation(40, 0), new BaseCellRotation(27, 3) }, // j 0 + { new BaseCellRotation(54, 3), new BaseCellRotation(36, 0), new BaseCellRotation(20, 0) }, // j 1 + { new BaseCellRotation(51, 3), new BaseCellRotation(34, 0), new BaseCellRotation(19, 3) } // j 2 + }, + { + // i 2 + { new BaseCellRotation(72, 0), new BaseCellRotation(60, 3), new BaseCellRotation(46, 3) }, // j 0 + { new BaseCellRotation(73, 3), new BaseCellRotation(55, 0), new BaseCellRotation(40, 0) }, // j 1 + { new BaseCellRotation(71, 3), new BaseCellRotation(54, 3), new BaseCellRotation(36, 0) } // j 2 + } }, + {// face 8 + { + // i 0 + { new BaseCellRotation(64, 0), new BaseCellRotation(47, 0), new BaseCellRotation(38, 3) }, // j 0 + { new BaseCellRotation(62, 0), new BaseCellRotation(43, 3), new BaseCellRotation(29, 3) }, // j 1 + { new BaseCellRotation(58, 3), new BaseCellRotation(42, 3), new BaseCellRotation(26, 3) } // j 2 + }, + { + // i 1 + { new BaseCellRotation(84, 0), new BaseCellRotation(69, 0), new BaseCellRotation(51, 3) }, // j 0 + { new BaseCellRotation(82, 3), new BaseCellRotation(64, 0), new BaseCellRotation(47, 0) }, // j 1 + { new BaseCellRotation(76, 3), new BaseCellRotation(62, 0), new BaseCellRotation(43, 3) } // j 2 + }, + { + // i 2 + { new BaseCellRotation(97, 0), new BaseCellRotation(89, 3), new BaseCellRotation(71, 3) }, // j 0 + { new BaseCellRotation(98, 3), new BaseCellRotation(84, 0), new BaseCellRotation(69, 0) }, // j 1 + { new BaseCellRotation(96, 3), new BaseCellRotation(82, 3), new BaseCellRotation(64, 0) } // j 2 + } }, + {// face 9 + { + // i 0 + { new BaseCellRotation(75, 0), new BaseCellRotation(65, 0), new BaseCellRotation(58, 3) }, // j 0 + { new BaseCellRotation(61, 0), new BaseCellRotation(53, 3), new BaseCellRotation(44, 3) }, // j 1 + { new BaseCellRotation(49, 3), new BaseCellRotation(41, 3), new BaseCellRotation(31, 3) } // j 2 + }, + { + // i 1 + { new BaseCellRotation(94, 0), new BaseCellRotation(86, 0), new BaseCellRotation(76, 3) }, // j 0 + { new BaseCellRotation(81, 3), new BaseCellRotation(75, 0), new BaseCellRotation(65, 0) }, // j 1 + { new BaseCellRotation(66, 3), new BaseCellRotation(61, 0), new BaseCellRotation(53, 3) } // j 2 + }, + { + // i 2 + { new BaseCellRotation(107, 0), new BaseCellRotation(104, 3), new BaseCellRotation(96, 3) }, // j 0 + { new BaseCellRotation(101, 3), new BaseCellRotation(94, 0), new BaseCellRotation(86, 0) }, // j 1 + { new BaseCellRotation(85, 3), new BaseCellRotation(81, 3), new BaseCellRotation(75, 0) } // j 2 + } }, + {// face 10 + { + // i 0 + { new BaseCellRotation(57, 0), new BaseCellRotation(59, 0), new BaseCellRotation(63, 3) }, // j 0 + { new BaseCellRotation(74, 0), new BaseCellRotation(78, 3), new BaseCellRotation(79, 3) }, // j 1 + { new BaseCellRotation(83, 3), new BaseCellRotation(92, 3), new BaseCellRotation(95, 3) } // j 2 + }, + { + // i 1 + { new BaseCellRotation(37, 0), new BaseCellRotation(39, 3), new BaseCellRotation(45, 3) }, // j 0 + { new BaseCellRotation(52, 0), new BaseCellRotation(57, 0), new BaseCellRotation(59, 0) }, // j 1 + { new BaseCellRotation(70, 3), new BaseCellRotation(74, 0), new BaseCellRotation(78, 3) } // j 2 + }, + { + // i 2 + { new BaseCellRotation(24, 0), new BaseCellRotation(23, 3), new BaseCellRotation(25, 3) }, // j 0 + { new BaseCellRotation(32, 3), new BaseCellRotation(37, 0), new BaseCellRotation(39, 3) }, // j 1 + { new BaseCellRotation(50, 3), new BaseCellRotation(52, 0), new BaseCellRotation(57, 0) } // j 2 + } }, + {// face 11 + { + // i 0 + { new BaseCellRotation(46, 0), new BaseCellRotation(60, 0), new BaseCellRotation(72, 3) }, // j 0 + { new BaseCellRotation(56, 0), new BaseCellRotation(68, 3), new BaseCellRotation(80, 3) }, // j 1 + { new BaseCellRotation(63, 3), new BaseCellRotation(77, 3), new BaseCellRotation(90, 3) } // j 2 + }, + { + // i 1 + { new BaseCellRotation(27, 0), new BaseCellRotation(40, 3), new BaseCellRotation(55, 3) }, // j 0 + { new BaseCellRotation(35, 0), new BaseCellRotation(46, 0), new BaseCellRotation(60, 0) }, // j 1 + { new BaseCellRotation(45, 3), new BaseCellRotation(56, 0), new BaseCellRotation(68, 3) } // j 2 + }, + { + // i 2 + { new BaseCellRotation(14, 0), new BaseCellRotation(20, 3), new BaseCellRotation(36, 3) }, // j 0 + { new BaseCellRotation(17, 3), new BaseCellRotation(27, 0), new BaseCellRotation(40, 3) }, // j 1 + { new BaseCellRotation(25, 3), new BaseCellRotation(35, 0), new BaseCellRotation(46, 0) } // j 2 + } }, + {// face 12 + { + // i 0 + { new BaseCellRotation(71, 0), new BaseCellRotation(89, 0), new BaseCellRotation(97, 3) }, // j 0 + { new BaseCellRotation(73, 0), new BaseCellRotation(91, 3), new BaseCellRotation(103, 3) }, // j 1 + { new BaseCellRotation(72, 3), new BaseCellRotation(88, 3), new BaseCellRotation(105, 3) } // j 2 + }, + { + // i 1 + { new BaseCellRotation(51, 0), new BaseCellRotation(69, 3), new BaseCellRotation(84, 3) }, // j 0 + { new BaseCellRotation(54, 0), new BaseCellRotation(71, 0), new BaseCellRotation(89, 0) }, // j 1 + { new BaseCellRotation(55, 3), new BaseCellRotation(73, 0), new BaseCellRotation(91, 3) } // j 2 + }, + { + // i 2 + { new BaseCellRotation(38, 0), new BaseCellRotation(47, 3), new BaseCellRotation(64, 3) }, // j 0 + { new BaseCellRotation(34, 3), new BaseCellRotation(51, 0), new BaseCellRotation(69, 3) }, // j 1 + { new BaseCellRotation(36, 3), new BaseCellRotation(54, 0), new BaseCellRotation(71, 0) } // j 2 + } }, + {// face 13 + { + // i 0 + { new BaseCellRotation(96, 0), new BaseCellRotation(104, 0), new BaseCellRotation(107, 3) }, // j 0 + { new BaseCellRotation(98, 0), new BaseCellRotation(110, 3), new BaseCellRotation(115, 3) }, // j 1 + { new BaseCellRotation(97, 3), new BaseCellRotation(111, 3), new BaseCellRotation(119, 3) } // j 2 + }, + { + // i 1 + { new BaseCellRotation(76, 0), new BaseCellRotation(86, 3), new BaseCellRotation(94, 3) }, // j 0 + { new BaseCellRotation(82, 0), new BaseCellRotation(96, 0), new BaseCellRotation(104, 0) }, // j 1 + { new BaseCellRotation(84, 3), new BaseCellRotation(98, 0), new BaseCellRotation(110, 3) } // j 2 + }, + { + // i 2 + { new BaseCellRotation(58, 0), new BaseCellRotation(65, 3), new BaseCellRotation(75, 3) }, // j 0 + { new BaseCellRotation(62, 3), new BaseCellRotation(76, 0), new BaseCellRotation(86, 3) }, // j 1 + { new BaseCellRotation(64, 3), new BaseCellRotation(82, 0), new BaseCellRotation(96, 0) } // j 2 + } }, + {// face 14 + { + // i 0 + { new BaseCellRotation(85, 0), new BaseCellRotation(87, 0), new BaseCellRotation(83, 3) }, // j 0 + { new BaseCellRotation(101, 0), new BaseCellRotation(102, 3), new BaseCellRotation(100, 3) }, // j 1 + { new BaseCellRotation(107, 3), new BaseCellRotation(112, 3), new BaseCellRotation(114, 3) } // j 2 + }, + { + // i 1 + { new BaseCellRotation(66, 0), new BaseCellRotation(67, 3), new BaseCellRotation(70, 3) }, // j 0 + { new BaseCellRotation(81, 0), new BaseCellRotation(85, 0), new BaseCellRotation(87, 0) }, // j 1 + { new BaseCellRotation(94, 3), new BaseCellRotation(101, 0), new BaseCellRotation(102, 3) } // j 2 + }, + { + // i 2 + { new BaseCellRotation(49, 0), new BaseCellRotation(48, 3), new BaseCellRotation(50, 3) }, // j 0 + { new BaseCellRotation(61, 3), new BaseCellRotation(66, 0), new BaseCellRotation(67, 3) }, // j 1 + { new BaseCellRotation(75, 3), new BaseCellRotation(81, 0), new BaseCellRotation(85, 0) } // j 2 + } }, + {// face 15 + { + // i 0 + { new BaseCellRotation(95, 0), new BaseCellRotation(92, 0), new BaseCellRotation(83, 0) }, // j 0 + { new BaseCellRotation(79, 0), new BaseCellRotation(78, 0), new BaseCellRotation(74, 3) }, // j 1 + { new BaseCellRotation(63, 1), new BaseCellRotation(59, 3), new BaseCellRotation(57, 3) } // j 2 + }, + { + // i 1 + { new BaseCellRotation(109, 0), new BaseCellRotation(108, 0), new BaseCellRotation(100, 5) }, // j 0 + { new BaseCellRotation(93, 1), new BaseCellRotation(95, 0), new BaseCellRotation(92, 0) }, // j 1 + { new BaseCellRotation(77, 1), new BaseCellRotation(79, 0), new BaseCellRotation(78, 0) } // j 2 + }, + { + // i 2 + { new BaseCellRotation(117, 4), new BaseCellRotation(118, 5), new BaseCellRotation(114, 5) }, // j 0 + { new BaseCellRotation(106, 1), new BaseCellRotation(109, 0), new BaseCellRotation(108, 0) }, // j 1 + { new BaseCellRotation(90, 1), new BaseCellRotation(93, 1), new BaseCellRotation(95, 0) } // j 2 + } }, + {// face 16 + { + // i 0 + { new BaseCellRotation(90, 0), new BaseCellRotation(77, 0), new BaseCellRotation(63, 0) }, // j 0 + { new BaseCellRotation(80, 0), new BaseCellRotation(68, 0), new BaseCellRotation(56, 3) }, // j 1 + { new BaseCellRotation(72, 1), new BaseCellRotation(60, 3), new BaseCellRotation(46, 3) } // j 2 + }, + { + // i 1 + { new BaseCellRotation(106, 0), new BaseCellRotation(93, 0), new BaseCellRotation(79, 5) }, // j 0 + { new BaseCellRotation(99, 1), new BaseCellRotation(90, 0), new BaseCellRotation(77, 0) }, // j 1 + { new BaseCellRotation(88, 1), new BaseCellRotation(80, 0), new BaseCellRotation(68, 0) } // j 2 + }, + { + // i 2 + { new BaseCellRotation(117, 3), new BaseCellRotation(109, 5), new BaseCellRotation(95, 5) }, // j 0 + { new BaseCellRotation(113, 1), new BaseCellRotation(106, 0), new BaseCellRotation(93, 0) }, // j 1 + { new BaseCellRotation(105, 1), new BaseCellRotation(99, 1), new BaseCellRotation(90, 0) } // j 2 + } }, + {// face 17 + { + // i 0 + { new BaseCellRotation(105, 0), new BaseCellRotation(88, 0), new BaseCellRotation(72, 0) }, // j 0 + { new BaseCellRotation(103, 0), new BaseCellRotation(91, 0), new BaseCellRotation(73, 3) }, // j 1 + { new BaseCellRotation(97, 1), new BaseCellRotation(89, 3), new BaseCellRotation(71, 3) } // j 2 + }, + { + // i 1 + { new BaseCellRotation(113, 0), new BaseCellRotation(99, 0), new BaseCellRotation(80, 5) }, // j 0 + { new BaseCellRotation(116, 1), new BaseCellRotation(105, 0), new BaseCellRotation(88, 0) }, // j 1 + { new BaseCellRotation(111, 1), new BaseCellRotation(103, 0), new BaseCellRotation(91, 0) } // j 2 + }, + { + // i 2 + { new BaseCellRotation(117, 2), new BaseCellRotation(106, 5), new BaseCellRotation(90, 5) }, // j 0 + { new BaseCellRotation(121, 1), new BaseCellRotation(113, 0), new BaseCellRotation(99, 0) }, // j 1 + { new BaseCellRotation(119, 1), new BaseCellRotation(116, 1), new BaseCellRotation(105, 0) } // j 2 + } }, + {// face 18 + { + // i 0 + { new BaseCellRotation(119, 0), new BaseCellRotation(111, 0), new BaseCellRotation(97, 0) }, // j 0 + { new BaseCellRotation(115, 0), new BaseCellRotation(110, 0), new BaseCellRotation(98, 3) }, // j 1 + { new BaseCellRotation(107, 1), new BaseCellRotation(104, 3), new BaseCellRotation(96, 3) } // j 2 + }, + { + // i 1 + { new BaseCellRotation(121, 0), new BaseCellRotation(116, 0), new BaseCellRotation(103, 5) }, // j 0 + { new BaseCellRotation(120, 1), new BaseCellRotation(119, 0), new BaseCellRotation(111, 0) }, // j 1 + { new BaseCellRotation(112, 1), new BaseCellRotation(115, 0), new BaseCellRotation(110, 0) } // j 2 + }, + { + // i 2 + { new BaseCellRotation(117, 1), new BaseCellRotation(113, 5), new BaseCellRotation(105, 5) }, // j 0 + { new BaseCellRotation(118, 1), new BaseCellRotation(121, 0), new BaseCellRotation(116, 0) }, // j 1 + { new BaseCellRotation(114, 1), new BaseCellRotation(120, 1), new BaseCellRotation(119, 0) } // j 2 + } }, + {// face 19 + { + // i 0 + { new BaseCellRotation(114, 0), new BaseCellRotation(112, 0), new BaseCellRotation(107, 0) }, // j 0 + { new BaseCellRotation(100, 0), new BaseCellRotation(102, 0), new BaseCellRotation(101, 3) }, // j 1 + { new BaseCellRotation(83, 1), new BaseCellRotation(87, 3), new BaseCellRotation(85, 3) } // j 2 + }, + { + // i 1 + { new BaseCellRotation(118, 0), new BaseCellRotation(120, 0), new BaseCellRotation(115, 5) }, // j 0 + { new BaseCellRotation(108, 1), new BaseCellRotation(114, 0), new BaseCellRotation(112, 0) }, // j 1 + { new BaseCellRotation(92, 1), new BaseCellRotation(100, 0), new BaseCellRotation(102, 0) } // j 2 + }, + { + // i 2 + { new BaseCellRotation(117, 0), new BaseCellRotation(121, 5), new BaseCellRotation(119, 5) }, // j 0 + { new BaseCellRotation(109, 1), new BaseCellRotation(118, 0), new BaseCellRotation(120, 0) }, // j 1 + { new BaseCellRotation(95, 1), new BaseCellRotation(108, 1), new BaseCellRotation(114, 0) } // j 2 + } } }; + + /** + * Return whether or not the indicated base cell is a pentagon. + */ + public static boolean isBaseCellPentagon(int baseCell) { + if (baseCell < 0 || baseCell >= Constants.NUM_BASE_CELLS) { // LCOV_EXCL_BR_LINE + // Base cells less than zero can not be represented in an index + return false; + } + return baseCellData[baseCell].isPentagon; + } + + /** + * Return whether or not the indicated base cell is a pentagon. + */ + public static FaceIJK getBaseFaceIJK(int baseCell) { + if (baseCell < 0 || baseCell >= Constants.NUM_BASE_CELLS) { // LCOV_EXCL_BR_LINE + // Base cells less than zero can not be represented in an index + throw new IllegalArgumentException("Illegal base cell"); + } + BaseCellData cellData = baseCellData[baseCell]; + return new FaceIJK(cellData.homeFace, new CoordIJK(cellData.homeI, cellData.homeJ, cellData.homeK)); + } + + /** Find base cell given FaceIJK. + * + * Given the face number and a resolution 0 ijk+ coordinate in that face's + * face-centered ijk coordinate system, return the base cell located at that + * coordinate. + * + * Valid ijk+ lookup coordinates are from (0, 0, 0) to (2, 2, 2). + */ + public static int getBaseCell(FaceIJK faceIJK) { + return faceIjkBaseCells[faceIJK.face][faceIJK.coord.i][faceIJK.coord.j][faceIJK.coord.k].baseCell; + } + + /** Find base cell given FaceIJK. + * + * Given the face number and a resolution 0 ijk+ coordinate in that face's + * face-centered ijk coordinate system, return the number of 60' ccw rotations + * to rotate into the coordinate system of the base cell at that coordinates. + * + * Valid ijk+ lookup coordinates are from (0, 0, 0) to (2, 2, 2). + */ + public static int getBaseCellCCWrot60(FaceIJK faceIJK) { + return faceIjkBaseCells[faceIJK.face][faceIJK.coord.i][faceIJK.coord.j][faceIJK.coord.k].ccwRot60; + } + + /** Return whether or not the tested face is a cw offset face. + */ + public static boolean baseCellIsCwOffset(int baseCell, int testFace) { + return baseCellData[baseCell].cwOffsetPent[0] == testFace || baseCellData[baseCell].cwOffsetPent[1] == testFace; + } + + /** Return whether the indicated base cell is a pentagon where all + * neighbors are oriented towards it. */ + public static boolean isBaseCellPolarPentagon(int baseCell) { + return baseCell == 4 || baseCell == 117; + } + +} diff --git a/libs/h3/src/main/java/org/opensearch/geospatial/h3/CellBoundary.java b/libs/h3/src/main/java/org/opensearch/geospatial/h3/CellBoundary.java new file mode 100644 index 0000000000..72dbd83260 --- /dev/null +++ b/libs/h3/src/main/java/org/opensearch/geospatial/h3/CellBoundary.java @@ -0,0 +1,61 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * This project is based on a modification of https://github.com/uber/h3 which is licensed under the Apache 2.0 License. + * + * Copyright 2016-2021 Uber Technologies, Inc. + */ +package org.opensearch.geospatial.h3; + +/** + * cell boundary points as {@link LatLng} + */ +public final class CellBoundary { + + /** Maximum number of cell boundary vertices; worst case is pentagon: + * 5 original verts + 5 edge crossings + */ + private static final int MAX_CELL_BNDRY_VERTS = 10; + /** How many points it holds */ + private int numVertext; + /** The actual points */ + private final LatLng[] points = new LatLng[MAX_CELL_BNDRY_VERTS]; + + CellBoundary() {} + + void add(LatLng point) { + points[numVertext++] = point; + } + + /** Number of points in this boundary */ + public int numPoints() { + return numVertext; + } + + /** Return the point at the given position*/ + public LatLng getLatLon(int i) { + if (i >= numVertext) { + throw new IndexOutOfBoundsException(); + } + return points[i]; + } +} diff --git a/libs/h3/src/main/java/org/opensearch/geospatial/h3/Constants.java b/libs/h3/src/main/java/org/opensearch/geospatial/h3/Constants.java new file mode 100644 index 0000000000..6164eec8c5 --- /dev/null +++ b/libs/h3/src/main/java/org/opensearch/geospatial/h3/Constants.java @@ -0,0 +1,81 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * This project is based on a modification of https://github.com/uber/h3 which is licensed under the Apache 2.0 License. + * + * Copyright 2016-2017, 2020 Uber Technologies, Inc. + */ +package org.opensearch.geospatial.h3; + +/** + * Constants used by more than one source code file. + */ +final class Constants { + /** + * sqrt(3) / 2.0 + */ + public static double M_SQRT3_2 = 0.8660254037844386467637231707529361834714; + /** + * H3 version 1 has 16 resolutions, numbered 0 through 15 + * min H3 resolution + */ + public static int MIN_H3_RES = 0; + /** + * max H3 resolution; + */ + public static int MAX_H3_RES = 15; + /** + * The number of H3 base cells + */ + public static int NUM_BASE_CELLS = 122; + /** + * The number of vertices in a hexagon + */ + public static int NUM_HEX_VERTS = 6; + /** + * The number of vertices in a pentagon + */ + public static int NUM_PENT_VERTS = 5; + /** + * H3 index modes + */ + public static int H3_CELL_MODE = 1; + /** + * square root of 7 + */ + public static final double M_SQRT7 = 2.6457513110645905905016157536392604257102; + /** + * scaling factor from hex2d resolution 0 unit length + * (or distance between adjacent cell center points + * on the plane) to gnomonic unit length. + */ + public static double RES0_U_GNOMONIC = 0.38196601125010500003; + /** + * rotation angle between Class II and Class III resolution axes + * (asin(sqrt(3.0 / 28.0))) + */ + public static double M_AP7_ROT_RADS = 0.333473172251832115336090755351601070065900389; + /** + * threshold epsilon + */ + public static double EPSILON = 0.0000000000000001; +} diff --git a/libs/h3/src/main/java/org/opensearch/geospatial/h3/CoordIJK.java b/libs/h3/src/main/java/org/opensearch/geospatial/h3/CoordIJK.java new file mode 100644 index 0000000000..afe4dd1ba9 --- /dev/null +++ b/libs/h3/src/main/java/org/opensearch/geospatial/h3/CoordIJK.java @@ -0,0 +1,398 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * This project is based on a modification of https://github.com/uber/h3 which is licensed under the Apache 2.0 License. + * + * Copyright 2016-2018, 2020-2021 Uber Technologies, Inc. + */ +package org.opensearch.geospatial.h3; + +/** + * Mutable IJK hexagon coordinates + * + * Each axis is spaced 120 degrees apart. + * + * References two Vec2d cartesian coordinate systems: + * + * 1. gnomonic: face-centered polyhedral gnomonic projection space with + * traditional scaling and x-axes aligned with the face Class II + * i-axes. + * + * 2. hex2d: local face-centered coordinate system scaled a specific H3 grid + * resolution unit length and with x-axes aligned with the local + * i-axes + */ +final class CoordIJK { + + /** CoordIJK unit vectors corresponding to the 7 H3 digits. + */ + private static final int[][] UNIT_VECS = { + { 0, 0, 0 }, // direction 0 + { 0, 0, 1 }, // direction 1 + { 0, 1, 0 }, // direction 2 + { 0, 1, 1 }, // direction 3 + { 1, 0, 0 }, // direction 4 + { 1, 0, 1 }, // direction 5 + { 1, 1, 0 } // direction 6 + }; + + /** H3 digit representing ijk+ axes direction. + * Values will be within the lowest 3 bits of an integer. + */ + public enum Direction { + + CENTER_DIGIT(0), + K_AXES_DIGIT(1), + J_AXES_DIGIT(2), + JK_AXES_DIGIT(J_AXES_DIGIT.digit() | K_AXES_DIGIT.digit()), + I_AXES_DIGIT(4), + IK_AXES_DIGIT(I_AXES_DIGIT.digit() | K_AXES_DIGIT.digit()), + IJ_AXES_DIGIT(I_AXES_DIGIT.digit() | J_AXES_DIGIT.digit()), + INVALID_DIGIT(7), + NUM_DIGITS(INVALID_DIGIT.digit()), + PENTAGON_SKIPPED_DIGIT(K_AXES_DIGIT.digit()); + + Direction(int digit) { + this.digit = digit; + } + + private final int digit; + + public int digit() { + return digit; + } + + } + + int i; // i component + int j; // j component + int k; // k component + + CoordIJK(int i, int j, int k) { + this.i = i; + this.j = j; + this.k = k; + } + + /** + * Find the center point in 2D cartesian coordinates of a hex. + * + */ + public Vec2d ijkToHex2d() { + int i = this.i - this.k; + int j = this.j - this.k; + return new Vec2d(i - 0.5 * j, j * Constants.M_SQRT3_2); + } + + /** + * Add ijk coordinates. + * + * @param i the i coordinate + * @param j the j coordinate + * @param k the k coordinate + */ + + public void ijkAdd(int i, int j, int k) { + this.i += i; + this.j += j; + this.k += k; + } + + /** + * Subtract ijk coordinates. + * + * @param i the i coordinate + * @param j the j coordinate + * @param k the k coordinate + */ + public void ijkSub(int i, int j, int k) { + this.i -= i; + this.j -= j; + this.k -= k; + } + + /** + * Normalizes ijk coordinates by setting the ijk coordinates + * to the smallest possible values. + */ + public void ijkNormalize() { + // remove any negative values + if (i < 0) { + j -= i; + k -= i; + i = 0; + } + + if (j < 0) { + i -= j; + k -= j; + j = 0; + } + + if (k < 0) { + i -= k; + j -= k; + k = 0; + } + + // remove the min value if needed + int min = i; + if (j < min) { + min = j; + } + if (k < min) { + min = k; + } + if (min > 0) { + i -= min; + j -= min; + k -= min; + } + } + + /** + * Find the normalized ijk coordinates of the hex centered on the current + * hex at the next finer aperture 7 counter-clockwise resolution. + */ + public void downAp7() { + // res r unit vectors in res r+1 + // iVec (3, 0, 1) + // jVec (1, 3, 0) + // kVec (0, 1, 3) + final int i = this.i * 3 + this.j * 1 + this.k * 0; + final int j = this.i * 0 + this.j * 3 + this.k * 1; + final int k = this.i * 1 + this.j * 0 + this.k * 3; + this.i = i; + this.j = j; + this.k = k; + ijkNormalize(); + } + + /** + * Find the normalized ijk coordinates of the hex centered on the current + * hex at the next finer aperture 7 clockwise resolution. + */ + public void downAp7r() { + // iVec (3, 1, 0) + // jVec (0, 3, 1) + // kVec (1, 0, 3) + final int i = this.i * 3 + this.j * 0 + this.k * 1; + final int j = this.i * 1 + this.j * 3 + this.k * 0; + final int k = this.i * 0 + this.j * 1 + this.k * 3; + this.i = i; + this.j = j; + this.k = k; + ijkNormalize(); + } + + /** + * Find the normalized ijk coordinates of the hex centered on the current + * hex at the next finer aperture 3 counter-clockwise resolution. + */ + public void downAp3() { + // res r unit vectors in res r+1 + // iVec (2, 0, 1) + // jVec (1, 2, 0) + // kVec (0, 1, 2) + final int i = this.i * 2 + this.j * 1 + this.k * 0; + final int j = this.i * 0 + this.j * 2 + this.k * 1; + final int k = this.i * 1 + this.j * 0 + this.k * 2; + this.i = i; + this.j = j; + this.k = k; + ijkNormalize(); + } + + /** + * Find the normalized ijk coordinates of the hex centered on the current + * hex at the next finer aperture 3 clockwise resolution. + */ + public void downAp3r() { + // res r unit vectors in res r+1 + // iVec (2, 1, 0) + // jVec (0, 2, 1) + // kVec (1, 0, 2) + final int i = this.i * 2 + this.j * 0 + this.k * 1; + final int j = this.i * 1 + this.j * 2 + this.k * 0; + final int k = this.i * 0 + this.j * 1 + this.k * 2; + this.i = i; + this.j = j; + this.k = k; + ijkNormalize(); + } + + /** + * Rotates ijk coordinates 60 degrees clockwise. + * + */ + public void ijkRotate60cw() { + // unit vector rotations + // iVec (1, 0, 1) + // jVec (1, 1, 0) + // kVec (0, 1, 1) + final int i = this.i * 1 + this.j * 1 + this.k * 0; + final int j = this.i * 0 + this.j * 1 + this.k * 1; + final int k = this.i * 1 + this.j * 0 + this.k * 1; + this.i = i; + this.j = j; + this.k = k; + ijkNormalize(); + } + + /** + * Rotates ijk coordinates 60 degrees counter-clockwise. + */ + public void ijkRotate60ccw() { + // unit vector rotations + // iVec (1, 1, 0) + // jVec (0, 1, 1) + // kVec (1, 0, 1) + final int i = this.i * 1 + this.j * 0 + this.k * 1; + final int j = this.i * 1 + this.j * 1 + this.k * 0; + final int k = this.i * 0 + this.j * 1 + this.k * 1; + this.i = i; + this.j = j; + this.k = k; + ijkNormalize(); + } + + /** + * Find the normalized ijk coordinates of the hex in the specified digit + * direction from the current ijk coordinates. + * @param digit The digit direction from the original ijk coordinates. + */ + public void neighbor(int digit) { + if (digit > Direction.CENTER_DIGIT.digit() && digit < Direction.NUM_DIGITS.digit()) { + ijkAdd(UNIT_VECS[digit][0], UNIT_VECS[digit][1], UNIT_VECS[digit][2]); + ijkNormalize(); + } + } + + /** + * Find the normalized ijk coordinates of the indexing parent of a cell in a + * clockwise aperture 7 grid. + */ + public void upAp7r() { + i = this.i - this.k; + j = this.j - this.k; + int i = (int) Math.round((2 * this.i + this.j) / 7.0); + int j = (int) Math.round((3 * this.j - this.i) / 7.0); + this.i = i; + this.j = j; + this.k = 0; + ijkNormalize(); + } + + /** + * Find the normalized ijk coordinates of the indexing parent of a cell in a + * counter-clockwise aperture 7 grid. + * + */ + public void upAp7() { + i = this.i - this.k; + j = this.j - this.k; + int i = (int) Math.round((3 * this.i - this.j) / 7.0); + int j = (int) Math.round((this.i + 2 * this.j) / 7.0); + this.i = i; + this.j = j; + this.k = 0; + ijkNormalize(); + } + + /** + * Determines the H3 digit corresponding to a unit vector in ijk coordinates. + * + * @return The H3 digit (0-6) corresponding to the ijk unit vector, or + * INVALID_DIGIT on failure. + */ + public int unitIjkToDigit() { + ijkNormalize(); + int digit = Direction.INVALID_DIGIT.digit(); + for (int i = Direction.CENTER_DIGIT.digit(); i < Direction.NUM_DIGITS.digit(); i++) { + if (ijkMatches(UNIT_VECS[i])) { + digit = i; + break; + } + } + return digit; + } + + /** + * Returns whether or not two ijk coordinates contain exactly the same + * component values. + * + * @param c The set of ijk coordinates. + * @return true if the two addresses match, 0 if they do not. + */ + private boolean ijkMatches(int[] c) { + return (i == c[0] && j == c[1] && k == c[2]); + } + + /** + * Rotates indexing digit 60 degrees clockwise. Returns result. + * + * @param digit Indexing digit (between 1 and 6 inclusive) + */ + public static int rotate60cw(int digit) { + switch (digit) { + case 1: // K_AXES_DIGIT + return Direction.JK_AXES_DIGIT.digit(); + case 3: // JK_AXES_DIGIT: + return Direction.J_AXES_DIGIT.digit(); + case 2: // J_AXES_DIGIT: + return Direction.IJ_AXES_DIGIT.digit(); + case 6: // IJ_AXES_DIGIT + return Direction.I_AXES_DIGIT.digit(); + case 4: // I_AXES_DIGIT + return Direction.IK_AXES_DIGIT.digit(); + case 5: // IK_AXES_DIGIT + return Direction.K_AXES_DIGIT.digit(); + default: + return digit; + } + } + + /** + * Rotates indexing digit 60 degrees counter-clockwise. Returns result. + * + * @param digit Indexing digit (between 1 and 6 inclusive) + */ + public static int rotate60ccw(int digit) { + switch (digit) { + case 1: // K_AXES_DIGIT + return Direction.IK_AXES_DIGIT.digit(); + case 5: // IK_AXES_DIGIT + return Direction.I_AXES_DIGIT.digit(); + case 4: // I_AXES_DIGIT + return Direction.IJ_AXES_DIGIT.digit(); + case 6: // IJ_AXES_DIGIT + return Direction.J_AXES_DIGIT.digit(); + case 2: // J_AXES_DIGIT: + return Direction.JK_AXES_DIGIT.digit(); + case 3: // JK_AXES_DIGIT: + return Direction.K_AXES_DIGIT.digit(); + default: + return digit; + } + } + +} diff --git a/libs/h3/src/main/java/org/opensearch/geospatial/h3/FaceIJK.java b/libs/h3/src/main/java/org/opensearch/geospatial/h3/FaceIJK.java new file mode 100644 index 0000000000..7a37c23254 --- /dev/null +++ b/libs/h3/src/main/java/org/opensearch/geospatial/h3/FaceIJK.java @@ -0,0 +1,817 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * This project is based on a modification of https://github.com/uber/h3 which is licensed under the Apache 2.0 License. + * + * Copyright 2016-2021 Uber Technologies, Inc. + */ +package org.opensearch.geospatial.h3; + +/** + * Mutable face number and ijk coordinates on that face-centered coordinate system. + * + * References the Vec2d cartesian coordinate systems hex2d: local face-centered + * coordinate system scaled a specific H3 grid resolution unit length and + * with x-axes aligned with the local i-axes + */ +final class FaceIJK { + + /** enum representing overage type */ + enum Overage { + /** + * Digit representing overage type + */ + NO_OVERAGE, + /** + * On face edge (only occurs on substrate grids) + */ + FACE_EDGE, + /** + * Overage on new face interior + */ + NEW_FACE + } + + // indexes for faceNeighbors table + /** + * IJ quadrant faceNeighbors table direction + */ + private static final int IJ = 1; + /** + * KI quadrant faceNeighbors table direction + */ + private static final int KI = 2; + /** + * JK quadrant faceNeighbors table direction + */ + private static final int JK = 3; + + /** + * overage distance table + */ + private static final int[] maxDimByCIIres = { + 2, // res 0 + -1, // res 1 + 14, // res 2 + -1, // res 3 + 98, // res 4 + -1, // res 5 + 686, // res 6 + -1, // res 7 + 4802, // res 8 + -1, // res 9 + 33614, // res 10 + -1, // res 11 + 235298, // res 12 + -1, // res 13 + 1647086, // res 14 + -1, // res 15 + 11529602 // res 16 + }; + + /** + * unit scale distance table + */ + private static final int[] unitScaleByCIIres = { + 1, // res 0 + -1, // res 1 + 7, // res 2 + -1, // res 3 + 49, // res 4 + -1, // res 5 + 343, // res 6 + -1, // res 7 + 2401, // res 8 + -1, // res 9 + 16807, // res 10 + -1, // res 11 + 117649, // res 12 + -1, // res 13 + 823543, // res 14 + -1, // res 15 + 5764801 // res 16 + }; + + /** + * direction from the origin face to the destination face, relative to + * the origin face's coordinate system, or -1 if not adjacent. + */ + private static final int[][] adjacentFaceDir = new int[][] { + { 0, KI, -1, -1, IJ, JK, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 }, // face 0 + { IJ, 0, KI, -1, -1, -1, JK, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 }, // face 1 + { -1, IJ, 0, KI, -1, -1, -1, JK, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 }, // face 2 + { -1, -1, IJ, 0, KI, -1, -1, -1, JK, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 }, // face 3 + { KI, -1, -1, IJ, 0, -1, -1, -1, -1, JK, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 }, // face 4 + { JK, -1, -1, -1, -1, 0, -1, -1, -1, -1, IJ, -1, -1, -1, KI, -1, -1, -1, -1, -1 }, // face 5 + { -1, JK, -1, -1, -1, -1, 0, -1, -1, -1, KI, IJ, -1, -1, -1, -1, -1, -1, -1, -1 }, // face 6 + { -1, -1, JK, -1, -1, -1, -1, 0, -1, -1, -1, KI, IJ, -1, -1, -1, -1, -1, -1, -1 }, // face 7 + { -1, -1, -1, JK, -1, -1, -1, -1, 0, -1, -1, -1, KI, IJ, -1, -1, -1, -1, -1, -1 }, // face 8 + { -1, -1, -1, -1, JK, -1, -1, -1, -1, 0, -1, -1, -1, KI, IJ, -1, -1, -1, -1, -1 }, // face 9 + { -1, -1, -1, -1, -1, IJ, KI, -1, -1, -1, 0, -1, -1, -1, -1, JK, -1, -1, -1, -1 }, // face 10 + { -1, -1, -1, -1, -1, -1, IJ, KI, -1, -1, -1, 0, -1, -1, -1, -1, JK, -1, -1, -1 }, // face 11 + { -1, -1, -1, -1, -1, -1, -1, IJ, KI, -1, -1, -1, 0, -1, -1, -1, -1, JK, -1, -1 }, // face 12 + { -1, -1, -1, -1, -1, -1, -1, -1, IJ, KI, -1, -1, -1, 0, -1, -1, -1, -1, JK, -1 }, // face 13 + { -1, -1, -1, -1, -1, KI, -1, -1, -1, IJ, -1, -1, -1, -1, 0, -1, -1, -1, -1, JK }, // face 14 + { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, JK, -1, -1, -1, -1, 0, IJ, -1, -1, KI }, // face 15 + { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, JK, -1, -1, -1, KI, 0, IJ, -1, -1 }, // face 16 + { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, JK, -1, -1, -1, KI, 0, IJ, -1 }, // face 17 + { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, JK, -1, -1, -1, KI, 0, IJ }, // face 18 + { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, JK, IJ, -1, -1, KI, 0 } // face 19 + }; + + /** Maximum input for any component to face-to-base-cell lookup functions */ + private static final int MAX_FACE_COORD = 2; + + /** + * Information to transform into an adjacent face IJK system + */ + private static class FaceOrientIJK { + // face number + final int face; + // res 0 translation relative to primary face + final int translateI; + final int translateJ; + final int translateK; + // number of 60 degree ccw rotations relative to primary + final int ccwRot60; + + // face + FaceOrientIJK(int face, int translateI, int translateJ, int translateK, int ccwRot60) { + this.face = face; + this.translateI = translateI; + this.translateJ = translateJ; + this.translateK = translateK; + this.ccwRot60 = ccwRot60; + } + } + + /** + * Definition of which faces neighbor each other. + */ + private static final FaceOrientIJK[][] faceNeighbors = new FaceOrientIJK[][] { + { + // face 0 + new FaceOrientIJK(0, 0, 0, 0, 0), // central face + new FaceOrientIJK(4, 2, 0, 2, 1), // ij quadrant + new FaceOrientIJK(1, 2, 2, 0, 5), // ki quadrant + new FaceOrientIJK(5, 0, 2, 2, 3) // jk quadrant + }, + { + // face 1 + new FaceOrientIJK(1, 0, 0, 0, 0), // central face + new FaceOrientIJK(0, 2, 0, 2, 1), // ij quadrant + new FaceOrientIJK(2, 2, 2, 0, 5), // ki quadrant + new FaceOrientIJK(6, 0, 2, 2, 3) // jk quadrant + }, + { + // face 2 + new FaceOrientIJK(2, 0, 0, 0, 0), // central face + new FaceOrientIJK(1, 2, 0, 2, 1), // ij quadrant + new FaceOrientIJK(3, 2, 2, 0, 5), // ki quadrant + new FaceOrientIJK(7, 0, 2, 2, 3) // jk quadrant + }, + { + // face 3 + new FaceOrientIJK(3, 0, 0, 0, 0), // central face + new FaceOrientIJK(2, 2, 0, 2, 1), // ij quadrant + new FaceOrientIJK(4, 2, 2, 0, 5), // ki quadrant + new FaceOrientIJK(8, 0, 2, 2, 3) // jk quadrant + }, + { + // face 4 + new FaceOrientIJK(4, 0, 0, 0, 0), // central face + new FaceOrientIJK(3, 2, 0, 2, 1), // ij quadrant + new FaceOrientIJK(0, 2, 2, 0, 5), // ki quadrant + new FaceOrientIJK(9, 0, 2, 2, 3) // jk quadrant + }, + { + // face 5 + new FaceOrientIJK(5, 0, 0, 0, 0), // central face + new FaceOrientIJK(10, 2, 2, 0, 3), // ij quadrant + new FaceOrientIJK(14, 2, 0, 2, 3), // ki quadrant + new FaceOrientIJK(0, 0, 2, 2, 3) // jk quadrant + }, + { + // face 6 + new FaceOrientIJK(6, 0, 0, 0, 0), // central face + new FaceOrientIJK(11, 2, 2, 0, 3), // ij quadrant + new FaceOrientIJK(10, 2, 0, 2, 3), // ki quadrant + new FaceOrientIJK(1, 0, 2, 2, 3) // jk quadrant + }, + { + // face 7 + new FaceOrientIJK(7, 0, 0, 0, 0), // central face + new FaceOrientIJK(12, 2, 2, 0, 3), // ij quadrant + new FaceOrientIJK(11, 2, 0, 2, 3), // ki quadrant + new FaceOrientIJK(2, 0, 2, 2, 3) // jk quadrant + }, + { + // face 8 + new FaceOrientIJK(8, 0, 0, 0, 0), // central face + new FaceOrientIJK(13, 2, 2, 0, 3), // ij quadrant + new FaceOrientIJK(12, 2, 0, 2, 3), // ki quadrant + new FaceOrientIJK(3, 0, 2, 2, 3) // jk quadrant + }, + { + // face 9 + new FaceOrientIJK(9, 0, 0, 0, 0), // central face + new FaceOrientIJK(14, 2, 2, 0, 3), // ij quadrant + new FaceOrientIJK(13, 2, 0, 2, 3), // ki quadrant + new FaceOrientIJK(4, 0, 2, 2, 3) // jk quadrant + }, + { + // face 10 + new FaceOrientIJK(10, 0, 0, 0, 0), // central face + new FaceOrientIJK(5, 2, 2, 0, 3), // ij quadrant + new FaceOrientIJK(6, 2, 0, 2, 3), // ki quadrant + new FaceOrientIJK(15, 0, 2, 2, 3) // jk quadrant + }, + { + // face 11 + new FaceOrientIJK(11, 0, 0, 0, 0), // central face + new FaceOrientIJK(6, 2, 2, 0, 3), // ij quadrant + new FaceOrientIJK(7, 2, 0, 2, 3), // ki quadrant + new FaceOrientIJK(16, 0, 2, 2, 3) // jk quadrant + }, + { + // face 12 + new FaceOrientIJK(12, 0, 0, 0, 0), // central face + new FaceOrientIJK(7, 2, 2, 0, 3), // ij quadrant + new FaceOrientIJK(8, 2, 0, 2, 3), // ki quadrant + new FaceOrientIJK(17, 0, 2, 2, 3) // jk quadrant + }, + { + // face 13 + new FaceOrientIJK(13, 0, 0, 0, 0), // central face + new FaceOrientIJK(8, 2, 2, 0, 3), // ij quadrant + new FaceOrientIJK(9, 2, 0, 2, 3), // ki quadrant + new FaceOrientIJK(18, 0, 2, 2, 3) // jk quadrant + }, + { + // face 14 + new FaceOrientIJK(14, 0, 0, 0, 0), // central face + new FaceOrientIJK(9, 2, 2, 0, 3), // ij quadrant + new FaceOrientIJK(5, 2, 0, 2, 3), // ki quadrant + new FaceOrientIJK(19, 0, 2, 2, 3) // jk quadrant + }, + { + // face 15 + new FaceOrientIJK(15, 0, 0, 0, 0), // central face + new FaceOrientIJK(16, 2, 0, 2, 1), // ij quadrant + new FaceOrientIJK(19, 2, 2, 0, 5), // ki quadrant + new FaceOrientIJK(10, 0, 2, 2, 3) // jk quadrant + }, + { + // face 16 + new FaceOrientIJK(16, 0, 0, 0, 0), // central face + new FaceOrientIJK(17, 2, 0, 2, 1), // ij quadrant + new FaceOrientIJK(15, 2, 2, 0, 5), // ki quadrant + new FaceOrientIJK(11, 0, 2, 2, 3) // jk quadrant + }, + { + // face 17 + new FaceOrientIJK(17, 0, 0, 0, 0), // central face + new FaceOrientIJK(18, 2, 0, 2, 1), // ij quadrant + new FaceOrientIJK(16, 2, 2, 0, 5), // ki quadrant + new FaceOrientIJK(12, 0, 2, 2, 3) // jk quadrant + }, + { + // face 18 + new FaceOrientIJK(18, 0, 0, 0, 0), // central face + new FaceOrientIJK(19, 2, 0, 2, 1), // ij quadrant + new FaceOrientIJK(17, 2, 2, 0, 5), // ki quadrant + new FaceOrientIJK(13, 0, 2, 2, 3) // jk quadrant + }, + { + // face 19 + new FaceOrientIJK(19, 0, 0, 0, 0), // central face + new FaceOrientIJK(15, 2, 0, 2, 1), // ij quadrant + new FaceOrientIJK(18, 2, 2, 0, 5), // ki quadrant + new FaceOrientIJK(14, 0, 2, 2, 3) // jk quadrant + } }; + + int face; // face number + CoordIJK coord; // ijk coordinates on that face + + FaceIJK(int face, CoordIJK coord) { + this.face = face; + this.coord = coord; + } + + /** + * Adjusts this FaceIJK address so that the resulting cell address is + * relative to the correct icosahedral face. + * + * @param res The H3 resolution of the cell. + * @param pentLeading4 Whether or not the cell is a pentagon with a leading + * digit 4. + * @param substrate Whether or not the cell is in a substrate grid. + * @return 0 if on original face (no overage); 1 if on face edge (only occurs + * on substrate grids); 2 if overage on new face interior + */ + public Overage adjustOverageClassII(int res, boolean pentLeading4, boolean substrate) { + Overage overage = Overage.NO_OVERAGE; + // get the maximum dimension value; scale if a substrate grid + int maxDim = maxDimByCIIres[res]; + if (substrate) { + maxDim *= 3; + } + + // check for overage + if (substrate && this.coord.i + this.coord.j + this.coord.k == maxDim) { // on edge + overage = Overage.FACE_EDGE; + } else if (this.coord.i + this.coord.j + this.coord.k > maxDim) { // overage + overage = Overage.NEW_FACE; + final FaceOrientIJK fijkOrient; + if (this.coord.k > 0) { + if (this.coord.j > 0) { // jk "quadrant" + fijkOrient = faceNeighbors[this.face][JK]; + } else { // ik "quadrant" + fijkOrient = faceNeighbors[this.face][KI]; + // adjust for the pentagonal missing sequence + if (pentLeading4) { + // translate origin to center of pentagon + this.coord.ijkSub(maxDim, 0, 0); + // rotate to adjust for the missing sequence + this.coord.ijkRotate60cw(); + // translate the origin back to the center of the triangle + this.coord.ijkAdd(maxDim, 0, 0); + } + } + } else { // ij "quadrant" + fijkOrient = faceNeighbors[this.face][IJ]; + } + + this.face = fijkOrient.face; + + // rotate and translate for adjacent face + for (int i = 0; i < fijkOrient.ccwRot60; i++) { + this.coord.ijkRotate60ccw(); + } + + int unitScale = unitScaleByCIIres[res]; + if (substrate) { + unitScale *= 3; + } + this.coord.ijkAdd(fijkOrient.translateI * unitScale, fijkOrient.translateJ * unitScale, fijkOrient.translateK * unitScale); + this.coord.ijkNormalize(); + + // overage points on pentagon boundaries can end up on edges + if (substrate && this.coord.i + this.coord.j + this.coord.k == maxDim) { // on edge + overage = Overage.FACE_EDGE; + } + } + return overage; + } + + /** + * Computes the center point in spherical coordinates of a cell given by + * a FaceIJK address at a specified resolution. + * + * @param res The H3 resolution of the cell. + */ + public LatLng faceIjkToGeo(int res) { + Vec2d v = coord.ijkToHex2d(); + return v.hex2dToGeo(face, res, false); + } + + /** + * Computes the cell boundary in spherical coordinates for a pentagonal cell + * for this FaceIJK address at a specified resolution. + * + * @param res The H3 resolution of the cell. + * @param start The first topological vertex to return. + * @param length The number of topological vertexes to return. + */ + public CellBoundary faceIjkPentToCellBoundary(int res, int start, int length) { + FaceIJK[] fijkVerts = new FaceIJK[Constants.NUM_PENT_VERTS]; + int adjRes = faceIjkPentToVerts(res, fijkVerts); + + // If we're returning the entire loop, we need one more iteration in case + // of a distortion vertex on the last edge + int additionalIteration = length == Constants.NUM_PENT_VERTS ? 1 : 0; + + // convert each vertex to lat/lng + // adjust the face of each vertex as appropriate and introduce + // edge-crossing vertices as needed + CellBoundary boundary = new CellBoundary(); + FaceIJK lastFijk = null; + for (int vert = start; vert < start + length + additionalIteration; vert++) { + int v = vert % Constants.NUM_PENT_VERTS; + + FaceIJK fijk = fijkVerts[v]; + + fijk.adjustPentVertOverage(adjRes); + + // all Class III pentagon edges cross icosa edges + // note that Class II pentagons have vertices on the edge, + // not edge intersections + if (H3Index.isResolutionClassIII(res) && vert > start) { + // find hex2d of the two vertexes on the last face + FaceIJK tmpFijk = new FaceIJK(fijk.face, new CoordIJK(fijk.coord.i, fijk.coord.j, fijk.coord.k)); + + Vec2d orig2d0 = lastFijk.coord.ijkToHex2d(); + + int currentToLastDir = adjacentFaceDir[tmpFijk.face][lastFijk.face]; + + FaceOrientIJK fijkOrient = faceNeighbors[tmpFijk.face][currentToLastDir]; + + tmpFijk.face = fijkOrient.face; + CoordIJK ijk = tmpFijk.coord; + + // rotate and translate for adjacent face + for (int i = 0; i < fijkOrient.ccwRot60; i++) { + ijk.ijkRotate60ccw(); + } + + int unitScale = unitScaleByCIIres[adjRes] * 3; + ijk.ijkAdd(fijkOrient.translateI * unitScale, fijkOrient.translateJ * unitScale, fijkOrient.translateK * unitScale); + ijk.ijkNormalize(); + + Vec2d orig2d1 = ijk.ijkToHex2d(); + + // find the appropriate icosa face edge vertexes + int maxDim = maxDimByCIIres[adjRes]; + Vec2d v0 = new Vec2d(3.0 * maxDim, 0.0); + Vec2d v1 = new Vec2d(-1.5 * maxDim, 3.0 * Constants.M_SQRT3_2 * maxDim); + Vec2d v2 = new Vec2d(-1.5 * maxDim, -3.0 * Constants.M_SQRT3_2 * maxDim); + + Vec2d edge0; + Vec2d edge1; + switch (adjacentFaceDir[tmpFijk.face][fijk.face]) { + case IJ: + edge0 = v0; + edge1 = v1; + break; + case JK: + edge0 = v1; + edge1 = v2; + break; + case KI: + default: + assert (adjacentFaceDir[tmpFijk.face][fijk.face] == KI); + edge0 = v2; + edge1 = v0; + break; + } + + // find the intersection and add the lat/lng point to the result + Vec2d inter = Vec2d.v2dIntersect(orig2d0, orig2d1, edge0, edge1); + LatLng point = inter.hex2dToGeo(tmpFijk.face, adjRes, true); + boundary.add(point); + } + + // convert vertex to lat/lng and add to the result + // vert == start + NUM_PENT_VERTS is only used to test for possible + // intersection on last edge + if (vert < start + Constants.NUM_PENT_VERTS) { + Vec2d vec = fijk.coord.ijkToHex2d(); + LatLng point = vec.hex2dToGeo(fijk.face, adjRes, true); + boundary.add(point); + } + + lastFijk = fijk; + } + return boundary; + } + + /** + * Generates the cell boundary in spherical coordinates for a cell given by this + * FaceIJK address at a specified resolution. + * + * @param res The H3 resolution of the cell. + * @param start The first topological vertex to return. + * @param length The number of topological vertexes to return. + */ + public CellBoundary faceIjkToCellBoundary(int res, int start, int length) { + FaceIJK fijkVerts[] = new FaceIJK[Constants.NUM_HEX_VERTS]; + int adjRes = faceIjkToVerts(res, fijkVerts); + // If we're returning the entire loop, we need one more iteration in case + // of a distortion vertex on the last edge + int additionalIteration = length == Constants.NUM_HEX_VERTS ? 1 : 0; + + // convert each vertex to lat/lng + // adjust the face of each vertex as appropriate and introduce + // edge-crossing vertices as needed + CellBoundary boundary = new CellBoundary(); + int lastFace = -1; + Overage lastOverage = Overage.NO_OVERAGE; + for (int vert = start; vert < start + length + additionalIteration; vert++) { + int v = vert % Constants.NUM_HEX_VERTS; + + FaceIJK fijk = new FaceIJK(fijkVerts[v].face, new CoordIJK(fijkVerts[v].coord.i, fijkVerts[v].coord.j, fijkVerts[v].coord.k)); + + // + final boolean pentLeading4 = false; // may change in c code when calling method + Overage overage = fijk.adjustOverageClassII(adjRes, pentLeading4, true); + + /* + Check for edge-crossing. Each face of the underlying icosahedron is a + different projection plane. So if an edge of the hexagon crosses an + icosahedron edge, an additional vertex must be introduced at that + intersection point. Then each half of the cell edge can be projected + to geographic coordinates using the appropriate icosahedron face + projection. Note that Class II cell edges have vertices on the face + edge, with no edge line intersections. + */ + if (H3Index.isResolutionClassIII(res) && vert > start && fijk.face != lastFace && lastOverage != Overage.FACE_EDGE) { + // find hex2d of the two vertexes on original face + int lastV = (v + 5) % Constants.NUM_HEX_VERTS; + Vec2d orig2d0 = fijkVerts[lastV].coord.ijkToHex2d(); + Vec2d orig2d1 = fijkVerts[v].coord.ijkToHex2d(); + + // find the appropriate icosa face edge vertexes + int maxDim = maxDimByCIIres[adjRes]; + Vec2d v0 = new Vec2d(3.0 * maxDim, 0.0); + Vec2d v1 = new Vec2d(-1.5 * maxDim, 3.0 * Constants.M_SQRT3_2 * maxDim); + Vec2d v2 = new Vec2d(-1.5 * maxDim, -3.0 * Constants.M_SQRT3_2 * maxDim); + + int face2 = ((lastFace == this.face) ? fijk.face : lastFace); + final Vec2d edge0; + final Vec2d edge1; + switch (adjacentFaceDir[this.face][face2]) { + case IJ: + edge0 = v0; + edge1 = v1; + break; + case JK: + edge0 = v1; + edge1 = v2; + break; + // case KI: + default: + assert (adjacentFaceDir[this.face][face2] == KI); + edge0 = v2; + edge1 = v0; + break; + } + + // find the intersection and add the lat/lng point to the result + Vec2d inter = Vec2d.v2dIntersect(orig2d0, orig2d1, edge0, edge1); + /* + If a point of intersection occurs at a hexagon vertex, then each + adjacent hexagon edge will lie completely on a single icosahedron + face, and no additional vertex is required. + */ + boolean isIntersectionAtVertex = orig2d0.equals(inter) || orig2d1.equals(inter); + if (isIntersectionAtVertex == false) { + LatLng point = inter.hex2dToGeo(this.face, adjRes, true); + boundary.add(point); + } + } + + // convert vertex to lat/lng and add to the result + // vert == start + NUM_HEX_VERTS is only used to test for possible + // intersection on last edge + if (vert < start + Constants.NUM_HEX_VERTS) { + Vec2d vec = fijk.coord.ijkToHex2d(); + LatLng point = vec.hex2dToGeo(fijk.face, adjRes, true); + boundary.add(point); + } + lastFace = fijk.face; + lastOverage = overage; + } + return boundary; + } + + /** + * compute the corresponding H3Index. + * @param res The cell resolution. + * @return The encoded H3Index (or H3_NULL on failure). + */ + public long faceIjkToH3(int res) { + // initialize the index + long h = H3Index.H3_INIT; + h = H3Index.H3_set_mode(h, Constants.H3_CELL_MODE); + h = H3Index.H3_set_resolution(h, res); + + // check for res 0/base cell + if (res == 0) { + if (coord.i > MAX_FACE_COORD || coord.j > MAX_FACE_COORD || coord.k > MAX_FACE_COORD) { + // out of range input + throw new IllegalArgumentException(" out of range input"); + } + + return H3Index.H3_set_base_cell(h, BaseCells.getBaseCell(this)); + } + + // we need to find the correct base cell FaceIJK for this H3 index; + // start with the passed in face and resolution res ijk coordinates + // in that face's coordinate system + + // build the H3Index from finest res up + // adjust r for the fact that the res 0 base cell offsets the indexing + // digits + for (int r = res - 1; r >= 0; r--) { + int lastI = coord.i; + int lastJ = coord.j; + int lastK = coord.k; + CoordIJK lastCenter; + if (H3Index.isResolutionClassIII(r + 1)) { + // rotate ccw + coord.upAp7(); + lastCenter = new CoordIJK(coord.i, coord.j, coord.k); + lastCenter.downAp7(); + } else { + // rotate cw + coord.upAp7r(); + lastCenter = new CoordIJK(coord.i, coord.j, coord.k); + lastCenter.downAp7r(); + } + + CoordIJK diff = new CoordIJK(lastI - lastCenter.i, lastJ - lastCenter.j, lastK - lastCenter.k); + diff.ijkNormalize(); + h = H3Index.H3_set_index_digit(h, r + 1, diff.unitIjkToDigit()); + } + + // we should now hold the IJK of the base cell in the + // coordinate system of the current face + + if (coord.i > MAX_FACE_COORD || coord.j > MAX_FACE_COORD || coord.k > MAX_FACE_COORD) { + // out of range input + throw new IllegalArgumentException(" out of range input"); + } + + // lookup the correct base cell + int baseCell = BaseCells.getBaseCell(this); + h = H3Index.H3_set_base_cell(h, baseCell); + + // rotate if necessary to get canonical base cell orientation + // for this base cell + int numRots = BaseCells.getBaseCellCCWrot60(this); + if (BaseCells.isBaseCellPentagon(baseCell)) { + // force rotation out of missing k-axes sub-sequence + if (H3Index.h3LeadingNonZeroDigit(h) == CoordIJK.Direction.K_AXES_DIGIT.digit()) { + // check for a cw/ccw offset face; default is ccw + if (BaseCells.baseCellIsCwOffset(baseCell, face)) { + h = H3Index.h3Rotate60cw(h); + } else { + h = H3Index.h3Rotate60ccw(h); + } + } + + for (int i = 0; i < numRots; i++) { + h = H3Index.h3RotatePent60ccw(h); + } + } else { + for (int i = 0; i < numRots; i++) { + h = H3Index.h3Rotate60ccw(h); + } + } + + return h; + } + + /** + * Populate the vertices of this cell as substrate FaceIJK addresses. + * + * @param res The H3 resolution of the cell. This may be adjusted if + * necessary for the substrate grid resolution. + */ + private int faceIjkToVerts(int res, FaceIJK[] fijkVerts) { + // get the correct set of substrate vertices for this resolution + CoordIJK[] verts; + if (H3Index.isResolutionClassIII(res)) { + // the vertexes of an origin-centered cell in a Class III resolution on a + // substrate grid with aperture sequence 33r7r. The aperture 3 gets us the + // vertices, and the 3r7r gets us to Class II. + // vertices listed ccw from the i-axes + verts = new CoordIJK[] { + new CoordIJK(5, 4, 0), // 0 + new CoordIJK(1, 5, 0), // 1 + new CoordIJK(0, 5, 4), // 2 + new CoordIJK(0, 1, 5), // 3 + new CoordIJK(4, 0, 5), // 4 + new CoordIJK(5, 0, 1) // 5 + }; + } else { + // the vertexes of an origin-centered cell in a Class II resolution on a + // substrate grid with aperture sequence 33r. The aperture 3 gets us the + // vertices, and the 3r gets us back to Class II. + // vertices listed ccw from the i-axes + verts = new CoordIJK[] { + new CoordIJK(2, 1, 0), // 0 + new CoordIJK(1, 2, 0), // 1 + new CoordIJK(0, 2, 1), // 2 + new CoordIJK(0, 1, 2), // 3 + new CoordIJK(1, 0, 2), // 4 + new CoordIJK(2, 0, 1) // 5 + }; + } + + // adjust the center point to be in an aperture 33r substrate grid + // these should be composed for speed + this.coord.downAp3(); + this.coord.downAp3r(); + + // if res is Class III we need to add a cw aperture 7 to get to + // icosahedral Class II + if (H3Index.isResolutionClassIII(res)) { + this.coord.downAp7r(); + res += 1; + } + + // The center point is now in the same substrate grid as the origin + // cell vertices. Add the center point substate coordinates + // to each vertex to translate the vertices to that cell. + + for (int v = 0; v < Constants.NUM_HEX_VERTS; v++) { + verts[v].ijkAdd(this.coord.i, this.coord.j, this.coord.k); + verts[v].ijkNormalize(); + fijkVerts[v] = new FaceIJK(this.face, verts[v]); + } + return res; + } + + /** + * Populate the vertices of this pentagon cell as substrate FaceIJK addresses + * + * @param res The H3 resolution of the cell. This may be adjusted if + * necessary for the substrate grid resolution. + */ + private int faceIjkPentToVerts(int res, FaceIJK[] fijkVerts) { + // get the correct set of substrate vertices for this resolution + CoordIJK[] verts; + if (H3Index.isResolutionClassIII(res)) { + // the vertexes of an origin-centered pentagon in a Class II resolution on a + // substrate grid with aperture sequence 33r. The aperture 3 gets us the + // vertices, and the 3r gets us back to Class II. + // vertices listed ccw from the i-axes + verts = new CoordIJK[] { + new CoordIJK(5, 4, 0), // 0 + new CoordIJK(1, 5, 0), // 1 + new CoordIJK(0, 5, 4), // 2 + new CoordIJK(0, 1, 5), // 3 + new CoordIJK(4, 0, 5) // 4 + }; + } else { + // the vertexes of an origin-centered pentagon in a Class III resolution on + // a substrate grid with aperture sequence 33r7r. The aperture 3 gets us the + // vertices, and the 3r7r gets us to Class II. vertices listed ccw from the + // i-axes + verts = new CoordIJK[] { + new CoordIJK(2, 1, 0), // 0 + new CoordIJK(1, 2, 0), // 1 + new CoordIJK(0, 2, 1), // 2 + new CoordIJK(0, 1, 2), // 3 + new CoordIJK(1, 0, 2) // 4 + }; + } + + // adjust the center point to be in an aperture 33r substrate grid + // these should be composed for speed + this.coord.downAp3(); + this.coord.downAp3r(); + + // if res is Class III we need to add a cw aperture 7 to get to + // icosahedral Class II + if (H3Index.isResolutionClassIII(res)) { + this.coord.downAp7r(); + res += 1; + } + + // The center point is now in the same substrate grid as the origin + // cell vertices. Add the center point substate coordinates + // to each vertex to translate the vertices to that cell. + for (int v = 0; v < Constants.NUM_PENT_VERTS; v++) { + verts[v].ijkAdd(this.coord.i, this.coord.j, this.coord.k); + verts[v].ijkNormalize(); + fijkVerts[v] = new FaceIJK(this.face, verts[v]); + } + return res; + } + + /** + * Adjusts a FaceIJK address for a pentagon vertex in a substrate grid in + * place so that the resulting cell address is relative to the correct + * icosahedral face. + * + * @param res The H3 resolution of the cell. + */ + private Overage adjustPentVertOverage(int res) { + Overage overage; + do { + overage = adjustOverageClassII(res, false, true); + } while (overage == Overage.NEW_FACE); + return overage; + } +} diff --git a/libs/h3/src/main/java/org/opensearch/geospatial/h3/H3.java b/libs/h3/src/main/java/org/opensearch/geospatial/h3/H3.java new file mode 100644 index 0000000000..ad55d00a15 --- /dev/null +++ b/libs/h3/src/main/java/org/opensearch/geospatial/h3/H3.java @@ -0,0 +1,311 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * This project is based on a modification of https://github.com/uber/h3 which is licensed under the Apache 2.0 License. + * + * Copyright 2016-2021 Uber Technologies, Inc. + */ +package org.opensearch.geospatial.h3; + +import java.util.Arrays; + +import static java.lang.Math.toRadians; + +/** + * Defines the public API of the H3 library. + */ +public final class H3 { + + public static int MIN_H3_RES = Constants.MIN_H3_RES; + public static int MAX_H3_RES = Constants.MAX_H3_RES; + + /** + * Converts from long representation of an index to String representation. + */ + public static String h3ToString(long h3) { + return Long.toHexString(h3); + } + + /** + * Converts from String representation of an index to long representation. + */ + public static long stringToH3(String h3Address) { + return Long.parseUnsignedLong(h3Address, 16); + } + + /** determines if an H3 cell is a pentagon */ + public static boolean isPentagon(long h3) { + return H3Index.H3_is_pentagon(h3); + } + + /** determines if an H3 cell in string format is a pentagon */ + public static boolean isPentagon(String h3Address) { + return isPentagon(stringToH3(h3Address)); + } + + /** Returns true if this is a valid H3 index */ + public static boolean h3IsValid(long h3) { + if (H3Index.H3_get_high_bit(h3) != 0) { + return false; + } + + if (H3Index.H3_get_mode(h3) != Constants.H3_CELL_MODE) { + return false; + } + + if (H3Index.H3_get_reserved_bits(h3) != 0) { + return false; + } + + int baseCell = H3Index.H3_get_base_cell(h3); + if (baseCell < 0 || baseCell >= Constants.NUM_BASE_CELLS) { // LCOV_EXCL_BR_LINE + // Base cells less than zero can not be represented in an index + return false; + } + + int res = H3Index.H3_get_resolution(h3); + if (res < Constants.MIN_H3_RES || res > Constants.MAX_H3_RES) { // LCOV_EXCL_BR_LINE + // Resolutions less than zero can not be represented in an index + return false; + } + + boolean foundFirstNonZeroDigit = false; + for (int r = 1; r <= res; r++) { + int digit = H3Index.H3_get_index_digit(h3, r); + + if (foundFirstNonZeroDigit == false && digit != CoordIJK.Direction.CENTER_DIGIT.digit()) { + foundFirstNonZeroDigit = true; + if (BaseCells.isBaseCellPentagon(baseCell) && digit == CoordIJK.Direction.K_AXES_DIGIT.digit()) { + return false; + } + } + + if (digit < CoordIJK.Direction.CENTER_DIGIT.digit() || digit >= CoordIJK.Direction.NUM_DIGITS.digit()) { + return false; + } + } + + for (int r = res + 1; r <= Constants.MAX_H3_RES; r++) { + int digit = H3Index.H3_get_index_digit(h3, r); + if (digit != CoordIJK.Direction.INVALID_DIGIT.digit()) { + return false; + } + } + return true; + } + + /** Returns true if this is a valid H3 index */ + public static boolean h3IsValid(String h3Address) { + return h3IsValid(stringToH3(h3Address)); + } + + /** + * Return all base cells + */ + public static long[] getLongRes0Cells() { + long[] cells = new long[Constants.NUM_BASE_CELLS]; + for (int bc = 0; bc < Constants.NUM_BASE_CELLS; bc++) { + long baseCell = H3Index.H3_INIT; + baseCell = H3Index.H3_set_mode(baseCell, Constants.H3_CELL_MODE); + baseCell = H3Index.H3_set_base_cell(baseCell, bc); + cells[bc] = baseCell; + } + return cells; + } + + /** + * Return all base cells + */ + public static String[] getStringRes0Cells() { + return h3ToStringList(getLongRes0Cells()); + } + + /** + * Find the {@link LatLng} center point of the cell. + */ + public static LatLng h3ToLatLng(long h3) { + final FaceIJK fijk = H3Index.h3ToFaceIjk(h3); + return fijk.faceIjkToGeo(H3Index.H3_get_resolution(h3)); + } + + /** + * Find the {@link LatLng} center point of the cell. + */ + public static LatLng h3ToLatLng(String h3Address) { + return h3ToLatLng(stringToH3(h3Address)); + } + + /** + * Find the cell {@link CellBoundary} coordinates for the cell + */ + public static CellBoundary h3ToGeoBoundary(long h3) { + FaceIJK fijk = H3Index.h3ToFaceIjk(h3); + if (H3Index.H3_is_pentagon(h3)) { + return fijk.faceIjkPentToCellBoundary(H3Index.H3_get_resolution(h3), 0, Constants.NUM_PENT_VERTS); + } else { + return fijk.faceIjkToCellBoundary(H3Index.H3_get_resolution(h3), 0, Constants.NUM_HEX_VERTS); + } + } + + /** + * Find the cell {@link CellBoundary} coordinates for the cell + */ + public static CellBoundary h3ToGeoBoundary(String h3Address) { + return h3ToGeoBoundary(stringToH3(h3Address)); + } + + /** + * Find the H3 index of the resolution res cell containing the lat/lon (in degrees) + * + * @param lat Latitude in degrees. + * @param lng Longitude in degrees. + * @param res Resolution, 0 <= res <= 15 + * @return The H3 index. + * @throws IllegalArgumentException latitude, longitude, or resolution are out of range. + */ + public static long geoToH3(double lat, double lng, int res) { + checkResolution(res); + return new LatLng(toRadians(lat), toRadians(lng)).geoToFaceIJK(res).faceIjkToH3(res); + } + + /** + * Find the H3 index of the resolution res cell containing the lat/lon (in degrees) + * + * @param lat Latitude in degrees. + * @param lng Longitude in degrees. + * @param res Resolution, 0 <= res <= 15 + * @return The H3 index. + * @throws IllegalArgumentException Latitude, longitude, or resolution is out of range. + */ + public static String geoToH3Address(double lat, double lng, int res) { + return h3ToString(geoToH3(lat, lng, res)); + } + + /** + * Returns the parent of the given index. + */ + public static long h3ToParent(long h3) { + int childRes = H3Index.H3_get_resolution(h3); + if (childRes == 0) { + throw new IllegalArgumentException("Input is a base cell"); + } + long parentH = H3Index.H3_set_resolution(h3, childRes - 1); + return H3Index.H3_set_index_digit(parentH, childRes, H3Index.H3_DIGIT_MASK); + } + + /** + * Returns the parent of the given index. + */ + public static String h3ToParent(String h3Address) { + long parent = h3ToParent(stringToH3(h3Address)); + return h3ToString(parent); + } + + /** + * Returns the children of the given index. + */ + public static long[] h3ToChildren(long h3) { + long[] children = new long[cellToChildrenSize(h3)]; + int res = H3Index.H3_get_resolution(h3); + Iterator.IterCellsChildren it = Iterator.iterInitParent(h3, res + 1); + int pos = 0; + while (it.h != Iterator.H3_NULL) { + children[pos++] = it.h; + Iterator.iterStepChild(it); + } + return children; + } + + /** + * Transforms a list of H3 indexes in long form to a list of H3 + * indexes in string form. + */ + public static String[] h3ToChildren(String h3Address) { + return h3ToStringList(h3ToChildren(stringToH3(h3Address))); + } + + public static String[] hexRing(String h3Address) { + return h3ToStringList(hexRing(stringToH3(h3Address))); + } + + /** + * Returns the neighbor indexes. + * + * @param h3 Origin index + * @return All neighbor indexes from the origin + */ + public static long[] hexRing(long h3) { + return HexRing.hexRing(h3); + } + + /** + * cellToChildrenSize returns the exact number of children for a cell at a + * given child resolution. + * + * @param h H3Index to find the number of children of + * + * @return int Exact number of children (handles hexagons and pentagons + * correctly) + */ + private static int cellToChildrenSize(long h) { + int n = 1; + if (H3Index.H3_is_pentagon(h)) { + return (1 + 5 * (_ipow(7, n) - 1) / 6); + } else { + return _ipow(7, n); + } + } + + /** + * _ipow does integer exponentiation efficiently. Taken from StackOverflow. + * + * @param base the integer base (can be positive or negative) + * @param exp the integer exponent (should be nonnegative) + * + * @return the exponentiated value + */ + private static int _ipow(int base, int exp) { + int result = 1; + while (exp != 0) { + if ((exp & 1) != 0) { + result *= base; + } + exp >>= 1; + base *= base; + } + + return result; + } + + private static String[] h3ToStringList(long[] h3s) { + return Arrays.stream(h3s).mapToObj(H3::h3ToString).toArray(String[]::new); + } + + /** + * @throws IllegalArgumentException res is not a valid H3 resolution. + */ + private static void checkResolution(int res) { + if (res < 0 || res > Constants.MAX_H3_RES) { + throw new IllegalArgumentException("resolution [" + res + "] is out of range (must be 0 <= res <= 15)"); + } + } +} diff --git a/libs/h3/src/main/java/org/opensearch/geospatial/h3/H3Index.java b/libs/h3/src/main/java/org/opensearch/geospatial/h3/H3Index.java new file mode 100644 index 0000000000..f12fcac199 --- /dev/null +++ b/libs/h3/src/main/java/org/opensearch/geospatial/h3/H3Index.java @@ -0,0 +1,337 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * This project is based on a modification of https://github.com/uber/h3 which is licensed under the Apache 2.0 License. + * + * Copyright 2016-2018, 2020 Uber Technologies, Inc. + */ +package org.opensearch.geospatial.h3; + +/** + * Functions that can be applied to an H3 index. + */ +final class H3Index { + + /** + * Gets the integer base cell of h3. + */ + public static int H3_get_base_cell(long h3) { + return ((int) ((((h3) & H3_BC_MASK) >> H3_BC_OFFSET))); + } + + /** + * Returns true if this index is one of twelve pentagons per resolution. + */ + public static boolean H3_is_pentagon(long h3) { + return BaseCells.isBaseCellPentagon(H3Index.H3_get_base_cell(h3)) && H3Index.h3LeadingNonZeroDigit(h3) == 0; + } + + public static long H3_INIT = 35184372088831L; + + /** + * The bit offset of the mode in an H3 index. + */ + public static int H3_MODE_OFFSET = 59; + + /** + * 1's in the 4 mode bits, 0's everywhere else. + */ + public static long H3_MODE_MASK = 15L << H3_MODE_OFFSET; + + /** + * 0's in the 4 mode bits, 1's everywhere else. + */ + public static long H3_MODE_MASK_NEGATIVE = ~H3_MODE_MASK; + + public static long H3_set_mode(long h3, long mode) { + return (h3 & H3_MODE_MASK_NEGATIVE) | (mode << H3_MODE_OFFSET); + } + + /** + * The bit offset of the base cell in an H3 index. + */ + public static int H3_BC_OFFSET = 45; + /** + * 1's in the 7 base cell bits, 0's everywhere else. + */ + public static long H3_BC_MASK = 127L << H3_BC_OFFSET; + + /** + * 0's in the 7 base cell bits, 1's everywhere else. + */ + public static long H3_BC_MASK_NEGATIVE = ~H3_BC_MASK; + + /** + * Sets the integer base cell of h3 to bc. + */ + public static long H3_set_base_cell(long h3, long bc) { + return (h3 & H3_BC_MASK_NEGATIVE) | (bc << H3_BC_OFFSET); + } + + public static int H3_RES_OFFSET = 52; + /** + * 1's in the 4 resolution bits, 0's everywhere else. + */ + public static long H3_RES_MASK = 15L << H3_RES_OFFSET; + + /** + * 0's in the 4 resolution bits, 1's everywhere else. + */ + public static long H3_RES_MASK_NEGATIVE = ~H3_RES_MASK; + + /** + * The bit offset of the max resolution digit in an H3 index. + */ + public static int H3_MAX_OFFSET = 63; + + /** + * 1 in the highest bit, 0's everywhere else. + */ + public static long H3_HIGH_BIT_MASK = (1L << H3_MAX_OFFSET); + + /** + * Gets the highest bit of the H3 index. + */ + public static int H3_get_high_bit(long h3) { + return ((int) ((((h3) & H3_HIGH_BIT_MASK) >> H3_MAX_OFFSET))); + } + + /** + * Sets the long resolution of h3. + */ + public static long H3_set_resolution(long h3, long res) { + return (((h3) & H3_RES_MASK_NEGATIVE) | (((res)) << H3_RES_OFFSET)); + } + + /** + * The bit offset of the reserved bits in an H3 index. + */ + public static int H3_RESERVED_OFFSET = 56; + + /** + * 1's in the 3 reserved bits, 0's everywhere else. + */ + public static long H3_RESERVED_MASK = (7L << H3_RESERVED_OFFSET); + + /** + * Gets a value in the reserved space. Should always be zero for valid indexes. + */ + public static int H3_get_reserved_bits(long h3) { + return ((int) ((((h3) & H3_RESERVED_MASK) >> H3_RESERVED_OFFSET))); + } + + public static int H3_get_mode(long h3) { + return ((int) ((((h3) & H3_MODE_MASK) >> H3_MODE_OFFSET))); + } + + /** + * Gets the integer resolution of h3. + */ + public static int H3_get_resolution(long h3) { + return (int) ((h3 & H3_RES_MASK) >> H3_RES_OFFSET); + } + + /** + * The number of bits in a single H3 resolution digit. + */ + public static int H3_PER_DIGIT_OFFSET = 3; + + /** + * 1's in the 3 bits of res 15 digit bits, 0's everywhere else. + */ + public static long H3_DIGIT_MASK = 7L; + + /** + * Gets the resolution res integer digit (0-7) of h3. + */ + public static int H3_get_index_digit(long h3, int res) { + return ((int) ((((h3) >> ((Constants.MAX_H3_RES - (res)) * H3_PER_DIGIT_OFFSET)) & H3_DIGIT_MASK))); + } + + /** + * Sets the resolution res digit of h3 to the integer digit (0-7) + */ + public static long H3_set_index_digit(long h3, int res, long digit) { + int x = (Constants.MAX_H3_RES - res) * H3_PER_DIGIT_OFFSET; + return (((h3) & ~((H3_DIGIT_MASK << (x)))) | (((digit)) << x)); + } + + /** + * Returns whether or not a resolution is a Class III grid. Note that odd + * resolutions are Class III and even resolutions are Class II. + * @param res The H3 resolution. + * @return 1 if the resolution is a Class III grid, and 0 if the resolution is + * a Class II grid. + */ + public static boolean isResolutionClassIII(int res) { + return res % 2 != 0; + } + + /** + * Convert an H3Index to a FaceIJK address. + * @param h3 The H3Index. + */ + public static FaceIJK h3ToFaceIjk(long h3) { + int baseCell = H3Index.H3_get_base_cell(h3); + if (baseCell < 0 || baseCell >= Constants.NUM_BASE_CELLS) { // LCOV_EXCL_BR_LINE + // Base cells less than zero can not be represented in an index + // To prevent reading uninitialized memory, we zero the output. + throw new IllegalArgumentException(); + } + // adjust for the pentagonal missing sequence; all of sub-sequence 5 needs + // to be adjusted (and some of sub-sequence 4 below) + if (BaseCells.isBaseCellPentagon(baseCell) && h3LeadingNonZeroDigit(h3) == 5) { + h3 = h3Rotate60cw(h3); + } + + // start with the "home" face and ijk+ coordinates for the base cell of c + FaceIJK fijk = BaseCells.getBaseFaceIJK(baseCell); + if (h3ToFaceIjkWithInitializedFijk(h3, fijk) == false) { + return fijk; // no overage is possible; h lies on this face + } + // if we're here we have the potential for an "overage"; i.e., it is + // possible that c lies on an adjacent face + + CoordIJK origIJK = new CoordIJK(fijk.coord.i, fijk.coord.j, fijk.coord.k); + + // if we're in Class III, drop into the next finer Class II grid + int res = H3Index.H3_get_resolution(h3); + if (isResolutionClassIII(res)) { + // Class III + fijk.coord.downAp7r(); + res++; + } + + // adjust for overage if needed + // a pentagon base cell with a leading 4 digit requires special handling + boolean pentLeading4 = (BaseCells.isBaseCellPentagon(baseCell) && h3LeadingNonZeroDigit(h3) == 4); + if (fijk.adjustOverageClassII(res, pentLeading4, false) != FaceIJK.Overage.NO_OVERAGE) { + // if the base cell is a pentagon we have the potential for secondary + // overages + if (BaseCells.isBaseCellPentagon(baseCell)) { + FaceIJK.Overage overage; + do { + overage = fijk.adjustOverageClassII(res, false, false); + } while (overage != FaceIJK.Overage.NO_OVERAGE); + } + + if (res != H3Index.H3_get_resolution(h3)) { + fijk.coord.upAp7r(); + } + } else if (res != H3Index.H3_get_resolution(h3)) { + fijk.coord = origIJK; + } + return fijk; + } + + /** + * Returns the highest resolution non-zero digit in an H3Index. + * @param h The H3Index. + * @return The highest resolution non-zero digit in the H3Index. + */ + public static int h3LeadingNonZeroDigit(long h) { + for (int r = 1; r <= H3Index.H3_get_resolution(h); r++) { + final int dir = H3Index.H3_get_index_digit(h, r); + if (dir != CoordIJK.Direction.CENTER_DIGIT.digit()) { + return dir; + } + } + // if we're here it's all 0's + return CoordIJK.Direction.CENTER_DIGIT.digit(); + } + + /** + * Convert an H3Index to the FaceIJK address on a specified icosahedral face. + * @param h The H3Index. + * @param fijk The FaceIJK address, initialized with the desired face + * and normalized base cell coordinates. + * @return Returns true if the possibility of overage exists, otherwise false. + */ + private static boolean h3ToFaceIjkWithInitializedFijk(long h, FaceIJK fijk) { + + final int res = H3Index.H3_get_resolution(h); + + // center base cell hierarchy is entirely on this face + final boolean possibleOverage = BaseCells.isBaseCellPentagon(H3_get_base_cell(h)) != false + || (res != 0 && (fijk.coord.i != 0 || fijk.coord.j != 0 || fijk.coord.k != 0)); + + for (int r = 1; r <= res; r++) { + if (isResolutionClassIII(r)) { + // Class III == rotate ccw + fijk.coord.downAp7(); + } else { + // Class II == rotate cw + fijk.coord.downAp7r(); + } + fijk.coord.neighbor(H3_get_index_digit(h, r)); + } + + return possibleOverage; + } + + /** + * Rotate an H3Index 60 degrees clockwise. + * @param h The H3Index. + */ + public static long h3Rotate60cw(long h) { + for (int r = 1, res = H3_get_resolution(h); r <= res; r++) { + h = H3_set_index_digit(h, r, CoordIJK.rotate60cw(H3_get_index_digit(h, r))); + } + return h; + } + + /** + * Rotate an H3Index 60 degrees counter-clockwise. + * @param h The H3Index. + */ + public static long h3Rotate60ccw(long h) { + for (int r = 1, res = H3_get_resolution(h); r <= res; r++) { + h = H3_set_index_digit(h, r, CoordIJK.rotate60ccw(H3_get_index_digit(h, r))); + } + return h; + } + + /** + * Rotate an H3Index 60 degrees counter-clockwise about a pentagonal center. + * @param h The H3Index. + */ + public static long h3RotatePent60ccw(long h) { + // skips any leading 1 digits (k-axis) + boolean foundFirstNonZeroDigit = false; + for (int r = 1, res = H3_get_resolution(h); r <= res; r++) { + // rotate this digit + h = H3_set_index_digit(h, r, CoordIJK.rotate60ccw(H3_get_index_digit(h, r))); + + // look for the first non-zero digit so we + // can adjust for deleted k-axes sequence + // if necessary + if (foundFirstNonZeroDigit == false && H3_get_index_digit(h, r) != 0) { + foundFirstNonZeroDigit = true; + + // adjust for deleted k-axes sequence + if (h3LeadingNonZeroDigit(h) == CoordIJK.Direction.K_AXES_DIGIT.digit()) h = h3Rotate60ccw(h); + } + } + return h; + } + +} diff --git a/libs/h3/src/main/java/org/opensearch/geospatial/h3/HexRing.java b/libs/h3/src/main/java/org/opensearch/geospatial/h3/HexRing.java new file mode 100644 index 0000000000..9f95f1a66c --- /dev/null +++ b/libs/h3/src/main/java/org/opensearch/geospatial/h3/HexRing.java @@ -0,0 +1,760 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * This project is based on a modification of https://github.com/uber/h3 which is licensed under the Apache 2.0 License. + * + * Copyright 2016-2021 Uber Technologies, Inc. + */ +package org.opensearch.geospatial.h3; + +/** + * Computes the neighbour H3 index from a given index. + */ +final class HexRing { + + private static final int INVALID_BASE_CELL = 127; + + /** Neighboring base cell ID in each IJK direction. + * + * For each base cell, for each direction, the neighboring base + * cell ID is given. 127 indicates there is no neighbor in that direction. + */ + private static final int[][] baseCellNeighbors = new int[][] { + { 0, 1, 5, 2, 4, 3, 8 }, // base cell 0 + { 1, 7, 6, 9, 0, 3, 2 }, // base cell 1 + { 2, 6, 10, 11, 0, 1, 5 }, // base cell 2 + { 3, 13, 1, 7, 4, 12, 0 }, // base cell 3 + { 4, INVALID_BASE_CELL, 15, 8, 3, 0, 12 }, // base cell 4 (pentagon) + { 5, 2, 18, 10, 8, 0, 16 }, // base cell 5 + { 6, 14, 11, 17, 1, 9, 2 }, // base cell 6 + { 7, 21, 9, 19, 3, 13, 1 }, // base cell 7 + { 8, 5, 22, 16, 4, 0, 15 }, // base cell 8 + { 9, 19, 14, 20, 1, 7, 6 }, // base cell 9 + { 10, 11, 24, 23, 5, 2, 18 }, // base cell 10 + { 11, 17, 23, 25, 2, 6, 10 }, // base cell 11 + { 12, 28, 13, 26, 4, 15, 3 }, // base cell 12 + { 13, 26, 21, 29, 3, 12, 7 }, // base cell 13 + { 14, INVALID_BASE_CELL, 17, 27, 9, 20, 6 }, // base cell 14 (pentagon) + { 15, 22, 28, 31, 4, 8, 12 }, // base cell 15 + { 16, 18, 33, 30, 8, 5, 22 }, // base cell 16 + { 17, 11, 14, 6, 35, 25, 27 }, // base cell 17 + { 18, 24, 30, 32, 5, 10, 16 }, // base cell 18 + { 19, 34, 20, 36, 7, 21, 9 }, // base cell 19 + { 20, 14, 19, 9, 40, 27, 36 }, // base cell 20 + { 21, 38, 19, 34, 13, 29, 7 }, // base cell 21 + { 22, 16, 41, 33, 15, 8, 31 }, // base cell 22 + { 23, 24, 11, 10, 39, 37, 25 }, // base cell 23 + { 24, INVALID_BASE_CELL, 32, 37, 10, 23, 18 }, // base cell 24 (pentagon) + { 25, 23, 17, 11, 45, 39, 35 }, // base cell 25 + { 26, 42, 29, 43, 12, 28, 13 }, // base cell 26 + { 27, 40, 35, 46, 14, 20, 17 }, // base cell 27 + { 28, 31, 42, 44, 12, 15, 26 }, // base cell 28 + { 29, 43, 38, 47, 13, 26, 21 }, // base cell 29 + { 30, 32, 48, 50, 16, 18, 33 }, // base cell 30 + { 31, 41, 44, 53, 15, 22, 28 }, // base cell 31 + { 32, 30, 24, 18, 52, 50, 37 }, // base cell 32 + { 33, 30, 49, 48, 22, 16, 41 }, // base cell 33 + { 34, 19, 38, 21, 54, 36, 51 }, // base cell 34 + { 35, 46, 45, 56, 17, 27, 25 }, // base cell 35 + { 36, 20, 34, 19, 55, 40, 54 }, // base cell 36 + { 37, 39, 52, 57, 24, 23, 32 }, // base cell 37 + { 38, INVALID_BASE_CELL, 34, 51, 29, 47, 21 }, // base cell 38 (pentagon) + { 39, 37, 25, 23, 59, 57, 45 }, // base cell 39 + { 40, 27, 36, 20, 60, 46, 55 }, // base cell 40 + { 41, 49, 53, 61, 22, 33, 31 }, // base cell 41 + { 42, 58, 43, 62, 28, 44, 26 }, // base cell 42 + { 43, 62, 47, 64, 26, 42, 29 }, // base cell 43 + { 44, 53, 58, 65, 28, 31, 42 }, // base cell 44 + { 45, 39, 35, 25, 63, 59, 56 }, // base cell 45 + { 46, 60, 56, 68, 27, 40, 35 }, // base cell 46 + { 47, 38, 43, 29, 69, 51, 64 }, // base cell 47 + { 48, 49, 30, 33, 67, 66, 50 }, // base cell 48 + { 49, INVALID_BASE_CELL, 61, 66, 33, 48, 41 }, // base cell 49 (pentagon) + { 50, 48, 32, 30, 70, 67, 52 }, // base cell 50 + { 51, 69, 54, 71, 38, 47, 34 }, // base cell 51 + { 52, 57, 70, 74, 32, 37, 50 }, // base cell 52 + { 53, 61, 65, 75, 31, 41, 44 }, // base cell 53 + { 54, 71, 55, 73, 34, 51, 36 }, // base cell 54 + { 55, 40, 54, 36, 72, 60, 73 }, // base cell 55 + { 56, 68, 63, 77, 35, 46, 45 }, // base cell 56 + { 57, 59, 74, 78, 37, 39, 52 }, // base cell 57 + { 58, INVALID_BASE_CELL, 62, 76, 44, 65, 42 }, // base cell 58 (pentagon) + { 59, 63, 78, 79, 39, 45, 57 }, // base cell 59 + { 60, 72, 68, 80, 40, 55, 46 }, // base cell 60 + { 61, 53, 49, 41, 81, 75, 66 }, // base cell 61 + { 62, 43, 58, 42, 82, 64, 76 }, // base cell 62 + { 63, INVALID_BASE_CELL, 56, 45, 79, 59, 77 }, // base cell 63 (pentagon) + { 64, 47, 62, 43, 84, 69, 82 }, // base cell 64 + { 65, 58, 53, 44, 86, 76, 75 }, // base cell 65 + { 66, 67, 81, 85, 49, 48, 61 }, // base cell 66 + { 67, 66, 50, 48, 87, 85, 70 }, // base cell 67 + { 68, 56, 60, 46, 90, 77, 80 }, // base cell 68 + { 69, 51, 64, 47, 89, 71, 84 }, // base cell 69 + { 70, 67, 52, 50, 83, 87, 74 }, // base cell 70 + { 71, 89, 73, 91, 51, 69, 54 }, // base cell 71 + { 72, INVALID_BASE_CELL, 73, 55, 80, 60, 88 }, // base cell 72 (pentagon) + { 73, 91, 72, 88, 54, 71, 55 }, // base cell 73 + { 74, 78, 83, 92, 52, 57, 70 }, // base cell 74 + { 75, 65, 61, 53, 94, 86, 81 }, // base cell 75 + { 76, 86, 82, 96, 58, 65, 62 }, // base cell 76 + { 77, 63, 68, 56, 93, 79, 90 }, // base cell 77 + { 78, 74, 59, 57, 95, 92, 79 }, // base cell 78 + { 79, 78, 63, 59, 93, 95, 77 }, // base cell 79 + { 80, 68, 72, 60, 99, 90, 88 }, // base cell 80 + { 81, 85, 94, 101, 61, 66, 75 }, // base cell 81 + { 82, 96, 84, 98, 62, 76, 64 }, // base cell 82 + { 83, INVALID_BASE_CELL, 74, 70, 100, 87, 92 }, // base cell 83 (pentagon) + { 84, 69, 82, 64, 97, 89, 98 }, // base cell 84 + { 85, 87, 101, 102, 66, 67, 81 }, // base cell 85 + { 86, 76, 75, 65, 104, 96, 94 }, // base cell 86 + { 87, 83, 102, 100, 67, 70, 85 }, // base cell 87 + { 88, 72, 91, 73, 99, 80, 105 }, // base cell 88 + { 89, 97, 91, 103, 69, 84, 71 }, // base cell 89 + { 90, 77, 80, 68, 106, 93, 99 }, // base cell 90 + { 91, 73, 89, 71, 105, 88, 103 }, // base cell 91 + { 92, 83, 78, 74, 108, 100, 95 }, // base cell 92 + { 93, 79, 90, 77, 109, 95, 106 }, // base cell 93 + { 94, 86, 81, 75, 107, 104, 101 }, // base cell 94 + { 95, 92, 79, 78, 109, 108, 93 }, // base cell 95 + { 96, 104, 98, 110, 76, 86, 82 }, // base cell 96 + { 97, INVALID_BASE_CELL, 98, 84, 103, 89, 111 }, // base cell 97 (pentagon) + { 98, 110, 97, 111, 82, 96, 84 }, // base cell 98 + { 99, 80, 105, 88, 106, 90, 113 }, // base cell 99 + { 100, 102, 83, 87, 108, 114, 92 }, // base cell 100 + { 101, 102, 107, 112, 81, 85, 94 }, // base cell 101 + { 102, 101, 87, 85, 114, 112, 100 }, // base cell 102 + { 103, 91, 97, 89, 116, 105, 111 }, // base cell 103 + { 104, 107, 110, 115, 86, 94, 96 }, // base cell 104 + { 105, 88, 103, 91, 113, 99, 116 }, // base cell 105 + { 106, 93, 99, 90, 117, 109, 113 }, // base cell 106 + { 107, INVALID_BASE_CELL, 101, 94, 115, 104, 112 }, // base cell 107 (pentagon) + { 108, 100, 95, 92, 118, 114, 109 }, // base cell 108 + { 109, 108, 93, 95, 117, 118, 106 }, // base cell 109 + { 110, 98, 104, 96, 119, 111, 115 }, // base cell 110 + { 111, 97, 110, 98, 116, 103, 119 }, // base cell 111 + { 112, 107, 102, 101, 120, 115, 114 }, // base cell 112 + { 113, 99, 116, 105, 117, 106, 121 }, // base cell 113 + { 114, 112, 100, 102, 118, 120, 108 }, // base cell 114 + { 115, 110, 107, 104, 120, 119, 112 }, // base cell 115 + { 116, 103, 119, 111, 113, 105, 121 }, // base cell 116 + { 117, INVALID_BASE_CELL, 109, 118, 113, 121, 106 }, // base cell 117 (pentagon) + { 118, 120, 108, 114, 117, 121, 109 }, // base cell 118 + { 119, 111, 115, 110, 121, 116, 120 }, // base cell 119 + { 120, 115, 114, 112, 121, 119, 118 }, // base cell 120 + { 121, 116, 120, 119, 117, 113, 118 }, // base cell 121 + }; + + /** @brief Neighboring base cell rotations in each IJK direction. + * + * For each base cell, for each direction, the number of 60 degree + * CCW rotations to the coordinate system of the neighbor is given. + * -1 indicates there is no neighbor in that direction. + */ + private static final int[][] baseCellNeighbor60CCWRots = new int[][] { + { 0, 5, 0, 0, 1, 5, 1 }, // base cell 0 + { 0, 0, 1, 0, 1, 0, 1 }, // base cell 1 + { 0, 0, 0, 0, 0, 5, 0 }, // base cell 2 + { 0, 5, 0, 0, 2, 5, 1 }, // base cell 3 + { 0, -1, 1, 0, 3, 4, 2 }, // base cell 4 (pentagon) + { 0, 0, 1, 0, 1, 0, 1 }, // base cell 5 + { 0, 0, 0, 3, 5, 5, 0 }, // base cell 6 + { 0, 0, 0, 0, 0, 5, 0 }, // base cell 7 + { 0, 5, 0, 0, 0, 5, 1 }, // base cell 8 + { 0, 0, 1, 3, 0, 0, 1 }, // base cell 9 + { 0, 0, 1, 3, 0, 0, 1 }, // base cell 10 + { 0, 3, 3, 3, 0, 0, 0 }, // base cell 11 + { 0, 5, 0, 0, 3, 5, 1 }, // base cell 12 + { 0, 0, 1, 0, 1, 0, 1 }, // base cell 13 + { 0, -1, 3, 0, 5, 2, 0 }, // base cell 14 (pentagon) + { 0, 5, 0, 0, 4, 5, 1 }, // base cell 15 + { 0, 0, 0, 0, 0, 5, 0 }, // base cell 16 + { 0, 3, 3, 3, 3, 0, 3 }, // base cell 17 + { 0, 0, 0, 3, 5, 5, 0 }, // base cell 18 + { 0, 3, 3, 3, 0, 0, 0 }, // base cell 19 + { 0, 3, 3, 3, 0, 3, 0 }, // base cell 20 + { 0, 0, 0, 3, 5, 5, 0 }, // base cell 21 + { 0, 0, 1, 0, 1, 0, 1 }, // base cell 22 + { 0, 3, 3, 3, 0, 3, 0 }, // base cell 23 + { 0, -1, 3, 0, 5, 2, 0 }, // base cell 24 (pentagon) + { 0, 0, 0, 3, 0, 0, 3 }, // base cell 25 + { 0, 0, 0, 0, 0, 5, 0 }, // base cell 26 + { 0, 3, 0, 0, 0, 3, 3 }, // base cell 27 + { 0, 0, 1, 0, 1, 0, 1 }, // base cell 28 + { 0, 0, 1, 3, 0, 0, 1 }, // base cell 29 + { 0, 3, 3, 3, 0, 0, 0 }, // base cell 30 + { 0, 0, 0, 0, 0, 5, 0 }, // base cell 31 + { 0, 3, 3, 3, 3, 0, 3 }, // base cell 32 + { 0, 0, 1, 3, 0, 0, 1 }, // base cell 33 + { 0, 3, 3, 3, 3, 0, 3 }, // base cell 34 + { 0, 0, 3, 0, 3, 0, 3 }, // base cell 35 + { 0, 0, 0, 3, 0, 0, 3 }, // base cell 36 + { 0, 3, 0, 0, 0, 3, 3 }, // base cell 37 + { 0, -1, 3, 0, 5, 2, 0 }, // base cell 38 (pentagon) + { 0, 3, 0, 0, 3, 3, 0 }, // base cell 39 + { 0, 3, 0, 0, 3, 3, 0 }, // base cell 40 + { 0, 0, 0, 3, 5, 5, 0 }, // base cell 41 + { 0, 0, 0, 3, 5, 5, 0 }, // base cell 42 + { 0, 3, 3, 3, 0, 0, 0 }, // base cell 43 + { 0, 0, 1, 3, 0, 0, 1 }, // base cell 44 + { 0, 0, 3, 0, 0, 3, 3 }, // base cell 45 + { 0, 0, 0, 3, 0, 3, 0 }, // base cell 46 + { 0, 3, 3, 3, 0, 3, 0 }, // base cell 47 + { 0, 3, 3, 3, 0, 3, 0 }, // base cell 48 + { 0, -1, 3, 0, 5, 2, 0 }, // base cell 49 (pentagon) + { 0, 0, 0, 3, 0, 0, 3 }, // base cell 50 + { 0, 3, 0, 0, 0, 3, 3 }, // base cell 51 + { 0, 0, 3, 0, 3, 0, 3 }, // base cell 52 + { 0, 3, 3, 3, 0, 0, 0 }, // base cell 53 + { 0, 0, 3, 0, 3, 0, 3 }, // base cell 54 + { 0, 0, 3, 0, 0, 3, 3 }, // base cell 55 + { 0, 3, 3, 3, 0, 0, 3 }, // base cell 56 + { 0, 0, 0, 3, 0, 3, 0 }, // base cell 57 + { 0, -1, 3, 0, 5, 2, 0 }, // base cell 58 (pentagon) + { 0, 3, 3, 3, 3, 3, 0 }, // base cell 59 + { 0, 3, 3, 3, 3, 3, 0 }, // base cell 60 + { 0, 3, 3, 3, 3, 0, 3 }, // base cell 61 + { 0, 3, 3, 3, 3, 0, 3 }, // base cell 62 + { 0, -1, 3, 0, 5, 2, 0 }, // base cell 63 (pentagon) + { 0, 0, 0, 3, 0, 0, 3 }, // base cell 64 + { 0, 3, 3, 3, 0, 3, 0 }, // base cell 65 + { 0, 3, 0, 0, 0, 3, 3 }, // base cell 66 + { 0, 3, 0, 0, 3, 3, 0 }, // base cell 67 + { 0, 3, 3, 3, 0, 0, 0 }, // base cell 68 + { 0, 3, 0, 0, 3, 3, 0 }, // base cell 69 + { 0, 0, 3, 0, 0, 3, 3 }, // base cell 70 + { 0, 0, 0, 3, 0, 3, 0 }, // base cell 71 + { 0, -1, 3, 0, 5, 2, 0 }, // base cell 72 (pentagon) + { 0, 3, 3, 3, 0, 0, 3 }, // base cell 73 + { 0, 3, 3, 3, 0, 0, 3 }, // base cell 74 + { 0, 0, 0, 3, 0, 0, 3 }, // base cell 75 + { 0, 3, 0, 0, 0, 3, 3 }, // base cell 76 + { 0, 0, 0, 3, 0, 5, 0 }, // base cell 77 + { 0, 3, 3, 3, 0, 0, 0 }, // base cell 78 + { 0, 0, 1, 3, 1, 0, 1 }, // base cell 79 + { 0, 0, 1, 3, 1, 0, 1 }, // base cell 80 + { 0, 0, 3, 0, 3, 0, 3 }, // base cell 81 + { 0, 0, 3, 0, 3, 0, 3 }, // base cell 82 + { 0, -1, 3, 0, 5, 2, 0 }, // base cell 83 (pentagon) + { 0, 0, 3, 0, 0, 3, 3 }, // base cell 84 + { 0, 0, 0, 3, 0, 3, 0 }, // base cell 85 + { 0, 3, 0, 0, 3, 3, 0 }, // base cell 86 + { 0, 3, 3, 3, 3, 3, 0 }, // base cell 87 + { 0, 0, 0, 3, 0, 5, 0 }, // base cell 88 + { 0, 3, 3, 3, 3, 3, 0 }, // base cell 89 + { 0, 0, 0, 0, 0, 0, 1 }, // base cell 90 + { 0, 3, 3, 3, 0, 0, 0 }, // base cell 91 + { 0, 0, 0, 3, 0, 5, 0 }, // base cell 92 + { 0, 5, 0, 0, 5, 5, 0 }, // base cell 93 + { 0, 0, 3, 0, 0, 3, 3 }, // base cell 94 + { 0, 0, 0, 0, 0, 0, 1 }, // base cell 95 + { 0, 0, 0, 3, 0, 3, 0 }, // base cell 96 + { 0, -1, 3, 0, 5, 2, 0 }, // base cell 97 (pentagon) + { 0, 3, 3, 3, 0, 0, 3 }, // base cell 98 + { 0, 5, 0, 0, 5, 5, 0 }, // base cell 99 + { 0, 0, 1, 3, 1, 0, 1 }, // base cell 100 + { 0, 3, 3, 3, 0, 0, 3 }, // base cell 101 + { 0, 3, 3, 3, 0, 0, 0 }, // base cell 102 + { 0, 0, 1, 3, 1, 0, 1 }, // base cell 103 + { 0, 3, 3, 3, 3, 3, 0 }, // base cell 104 + { 0, 0, 0, 0, 0, 0, 1 }, // base cell 105 + { 0, 0, 1, 0, 3, 5, 1 }, // base cell 106 + { 0, -1, 3, 0, 5, 2, 0 }, // base cell 107 (pentagon) + { 0, 5, 0, 0, 5, 5, 0 }, // base cell 108 + { 0, 0, 1, 0, 4, 5, 1 }, // base cell 109 + { 0, 3, 3, 3, 0, 0, 0 }, // base cell 110 + { 0, 0, 0, 3, 0, 5, 0 }, // base cell 111 + { 0, 0, 0, 3, 0, 5, 0 }, // base cell 112 + { 0, 0, 1, 0, 2, 5, 1 }, // base cell 113 + { 0, 0, 0, 0, 0, 0, 1 }, // base cell 114 + { 0, 0, 1, 3, 1, 0, 1 }, // base cell 115 + { 0, 5, 0, 0, 5, 5, 0 }, // base cell 116 + { 0, -1, 1, 0, 3, 4, 2 }, // base cell 117 (pentagon) + { 0, 0, 1, 0, 0, 5, 1 }, // base cell 118 + { 0, 0, 0, 0, 0, 0, 1 }, // base cell 119 + { 0, 5, 0, 0, 5, 5, 0 }, // base cell 120 + { 0, 0, 1, 0, 1, 5, 1 }, // base cell 121 + }; + + private static final int E_SUCCESS = 0; // Success (no error) + private static final int E_PENTAGON = 9; // Pentagon distortion was encountered which the algorithm + private static final int E_CELL_INVALID = 5; // `H3Index` cell argument was not valid + private static final int E_FAILED = 1; // The operation failed but a more specific error is not available + + /** + * Directions used for traversing a hexagonal ring counterclockwise around + * {1, 0, 0} + * + *

+     *      _
+     *    _/ \\_
+     *   / \\5/ \\
+     *   \\0/ \\4/
+     *   / \\_/ \\
+     *   \\1/ \\3/
+     *     \\2/
+     * 
+ */ + private static final CoordIJK.Direction[] DIRECTIONS = new CoordIJK.Direction[] { + CoordIJK.Direction.J_AXES_DIGIT, + CoordIJK.Direction.JK_AXES_DIGIT, + CoordIJK.Direction.K_AXES_DIGIT, + CoordIJK.Direction.IK_AXES_DIGIT, + CoordIJK.Direction.I_AXES_DIGIT, + CoordIJK.Direction.IJ_AXES_DIGIT }; + + /** + * New digit when traversing along class II grids. + * + * Current digit -> direction -> new digit. + */ + private static final CoordIJK.Direction[][] NEW_DIGIT_II = new CoordIJK.Direction[][] { + { + CoordIJK.Direction.CENTER_DIGIT, + CoordIJK.Direction.K_AXES_DIGIT, + CoordIJK.Direction.J_AXES_DIGIT, + CoordIJK.Direction.JK_AXES_DIGIT, + CoordIJK.Direction.I_AXES_DIGIT, + CoordIJK.Direction.IK_AXES_DIGIT, + CoordIJK.Direction.IJ_AXES_DIGIT }, + { + CoordIJK.Direction.K_AXES_DIGIT, + CoordIJK.Direction.I_AXES_DIGIT, + CoordIJK.Direction.JK_AXES_DIGIT, + CoordIJK.Direction.IJ_AXES_DIGIT, + CoordIJK.Direction.IK_AXES_DIGIT, + CoordIJK.Direction.J_AXES_DIGIT, + CoordIJK.Direction.CENTER_DIGIT }, + { + CoordIJK.Direction.J_AXES_DIGIT, + CoordIJK.Direction.JK_AXES_DIGIT, + CoordIJK.Direction.K_AXES_DIGIT, + CoordIJK.Direction.I_AXES_DIGIT, + CoordIJK.Direction.IJ_AXES_DIGIT, + CoordIJK.Direction.CENTER_DIGIT, + CoordIJK.Direction.IK_AXES_DIGIT }, + { + CoordIJK.Direction.JK_AXES_DIGIT, + CoordIJK.Direction.IJ_AXES_DIGIT, + CoordIJK.Direction.I_AXES_DIGIT, + CoordIJK.Direction.IK_AXES_DIGIT, + CoordIJK.Direction.CENTER_DIGIT, + CoordIJK.Direction.K_AXES_DIGIT, + CoordIJK.Direction.J_AXES_DIGIT }, + { + CoordIJK.Direction.I_AXES_DIGIT, + CoordIJK.Direction.IK_AXES_DIGIT, + CoordIJK.Direction.IJ_AXES_DIGIT, + CoordIJK.Direction.CENTER_DIGIT, + CoordIJK.Direction.J_AXES_DIGIT, + CoordIJK.Direction.JK_AXES_DIGIT, + CoordIJK.Direction.K_AXES_DIGIT }, + { + CoordIJK.Direction.IK_AXES_DIGIT, + CoordIJK.Direction.J_AXES_DIGIT, + CoordIJK.Direction.CENTER_DIGIT, + CoordIJK.Direction.K_AXES_DIGIT, + CoordIJK.Direction.JK_AXES_DIGIT, + CoordIJK.Direction.IJ_AXES_DIGIT, + CoordIJK.Direction.I_AXES_DIGIT }, + { + CoordIJK.Direction.IJ_AXES_DIGIT, + CoordIJK.Direction.CENTER_DIGIT, + CoordIJK.Direction.IK_AXES_DIGIT, + CoordIJK.Direction.J_AXES_DIGIT, + CoordIJK.Direction.K_AXES_DIGIT, + CoordIJK.Direction.I_AXES_DIGIT, + CoordIJK.Direction.JK_AXES_DIGIT } }; + + /** + * New traversal direction when traversing along class II grids. + * + * Current digit -> direction -> new ap7 move (at coarser level). + */ + private static final CoordIJK.Direction[][] NEW_ADJUSTMENT_II = new CoordIJK.Direction[][] { + { + CoordIJK.Direction.CENTER_DIGIT, + CoordIJK.Direction.CENTER_DIGIT, + CoordIJK.Direction.CENTER_DIGIT, + CoordIJK.Direction.CENTER_DIGIT, + CoordIJK.Direction.CENTER_DIGIT, + CoordIJK.Direction.CENTER_DIGIT, + CoordIJK.Direction.CENTER_DIGIT }, + { + CoordIJK.Direction.CENTER_DIGIT, + CoordIJK.Direction.K_AXES_DIGIT, + CoordIJK.Direction.CENTER_DIGIT, + CoordIJK.Direction.K_AXES_DIGIT, + CoordIJK.Direction.CENTER_DIGIT, + CoordIJK.Direction.IK_AXES_DIGIT, + CoordIJK.Direction.CENTER_DIGIT }, + { + CoordIJK.Direction.CENTER_DIGIT, + CoordIJK.Direction.CENTER_DIGIT, + CoordIJK.Direction.J_AXES_DIGIT, + CoordIJK.Direction.JK_AXES_DIGIT, + CoordIJK.Direction.CENTER_DIGIT, + CoordIJK.Direction.CENTER_DIGIT, + CoordIJK.Direction.J_AXES_DIGIT }, + { + CoordIJK.Direction.CENTER_DIGIT, + CoordIJK.Direction.K_AXES_DIGIT, + CoordIJK.Direction.JK_AXES_DIGIT, + CoordIJK.Direction.JK_AXES_DIGIT, + CoordIJK.Direction.CENTER_DIGIT, + CoordIJK.Direction.CENTER_DIGIT, + CoordIJK.Direction.CENTER_DIGIT }, + { + CoordIJK.Direction.CENTER_DIGIT, + CoordIJK.Direction.CENTER_DIGIT, + CoordIJK.Direction.CENTER_DIGIT, + CoordIJK.Direction.CENTER_DIGIT, + CoordIJK.Direction.I_AXES_DIGIT, + CoordIJK.Direction.I_AXES_DIGIT, + CoordIJK.Direction.IJ_AXES_DIGIT }, + { + CoordIJK.Direction.CENTER_DIGIT, + CoordIJK.Direction.IK_AXES_DIGIT, + CoordIJK.Direction.CENTER_DIGIT, + CoordIJK.Direction.CENTER_DIGIT, + CoordIJK.Direction.I_AXES_DIGIT, + CoordIJK.Direction.IK_AXES_DIGIT, + CoordIJK.Direction.CENTER_DIGIT }, + { + CoordIJK.Direction.CENTER_DIGIT, + CoordIJK.Direction.CENTER_DIGIT, + CoordIJK.Direction.J_AXES_DIGIT, + CoordIJK.Direction.CENTER_DIGIT, + CoordIJK.Direction.IJ_AXES_DIGIT, + CoordIJK.Direction.CENTER_DIGIT, + CoordIJK.Direction.IJ_AXES_DIGIT } }; + + /** + * New traversal direction when traversing along class III grids. + * + * Current digit -> direction -> new ap7 move (at coarser level). + */ + private static final CoordIJK.Direction[][] NEW_DIGIT_III = new CoordIJK.Direction[][] { + { + CoordIJK.Direction.CENTER_DIGIT, + CoordIJK.Direction.K_AXES_DIGIT, + CoordIJK.Direction.J_AXES_DIGIT, + CoordIJK.Direction.JK_AXES_DIGIT, + CoordIJK.Direction.I_AXES_DIGIT, + CoordIJK.Direction.IK_AXES_DIGIT, + CoordIJK.Direction.IJ_AXES_DIGIT }, + { + CoordIJK.Direction.K_AXES_DIGIT, + CoordIJK.Direction.J_AXES_DIGIT, + CoordIJK.Direction.JK_AXES_DIGIT, + CoordIJK.Direction.I_AXES_DIGIT, + CoordIJK.Direction.IK_AXES_DIGIT, + CoordIJK.Direction.IJ_AXES_DIGIT, + CoordIJK.Direction.CENTER_DIGIT }, + { + CoordIJK.Direction.J_AXES_DIGIT, + CoordIJK.Direction.JK_AXES_DIGIT, + CoordIJK.Direction.I_AXES_DIGIT, + CoordIJK.Direction.IK_AXES_DIGIT, + CoordIJK.Direction.IJ_AXES_DIGIT, + CoordIJK.Direction.CENTER_DIGIT, + CoordIJK.Direction.K_AXES_DIGIT }, + { + CoordIJK.Direction.JK_AXES_DIGIT, + CoordIJK.Direction.I_AXES_DIGIT, + CoordIJK.Direction.IK_AXES_DIGIT, + CoordIJK.Direction.IJ_AXES_DIGIT, + CoordIJK.Direction.CENTER_DIGIT, + CoordIJK.Direction.K_AXES_DIGIT, + CoordIJK.Direction.J_AXES_DIGIT }, + { + CoordIJK.Direction.I_AXES_DIGIT, + CoordIJK.Direction.IK_AXES_DIGIT, + CoordIJK.Direction.IJ_AXES_DIGIT, + CoordIJK.Direction.CENTER_DIGIT, + CoordIJK.Direction.K_AXES_DIGIT, + CoordIJK.Direction.J_AXES_DIGIT, + CoordIJK.Direction.JK_AXES_DIGIT }, + { + CoordIJK.Direction.IK_AXES_DIGIT, + CoordIJK.Direction.IJ_AXES_DIGIT, + CoordIJK.Direction.CENTER_DIGIT, + CoordIJK.Direction.K_AXES_DIGIT, + CoordIJK.Direction.J_AXES_DIGIT, + CoordIJK.Direction.JK_AXES_DIGIT, + CoordIJK.Direction.I_AXES_DIGIT }, + { + CoordIJK.Direction.IJ_AXES_DIGIT, + CoordIJK.Direction.CENTER_DIGIT, + CoordIJK.Direction.K_AXES_DIGIT, + CoordIJK.Direction.J_AXES_DIGIT, + CoordIJK.Direction.JK_AXES_DIGIT, + CoordIJK.Direction.I_AXES_DIGIT, + CoordIJK.Direction.IK_AXES_DIGIT } }; + + /** + * New traversal direction when traversing along class III grids. + * + * Current digit -> direction -> new ap7 move (at coarser level). + */ + private static final CoordIJK.Direction[][] NEW_ADJUSTMENT_III = new CoordIJK.Direction[][] { + { + CoordIJK.Direction.CENTER_DIGIT, + CoordIJK.Direction.CENTER_DIGIT, + CoordIJK.Direction.CENTER_DIGIT, + CoordIJK.Direction.CENTER_DIGIT, + CoordIJK.Direction.CENTER_DIGIT, + CoordIJK.Direction.CENTER_DIGIT, + CoordIJK.Direction.CENTER_DIGIT }, + { + CoordIJK.Direction.CENTER_DIGIT, + CoordIJK.Direction.K_AXES_DIGIT, + CoordIJK.Direction.CENTER_DIGIT, + CoordIJK.Direction.JK_AXES_DIGIT, + CoordIJK.Direction.CENTER_DIGIT, + CoordIJK.Direction.K_AXES_DIGIT, + CoordIJK.Direction.CENTER_DIGIT }, + { + CoordIJK.Direction.CENTER_DIGIT, + CoordIJK.Direction.CENTER_DIGIT, + CoordIJK.Direction.J_AXES_DIGIT, + CoordIJK.Direction.J_AXES_DIGIT, + CoordIJK.Direction.CENTER_DIGIT, + CoordIJK.Direction.CENTER_DIGIT, + CoordIJK.Direction.IJ_AXES_DIGIT }, + { + CoordIJK.Direction.CENTER_DIGIT, + CoordIJK.Direction.JK_AXES_DIGIT, + CoordIJK.Direction.J_AXES_DIGIT, + CoordIJK.Direction.JK_AXES_DIGIT, + CoordIJK.Direction.CENTER_DIGIT, + CoordIJK.Direction.CENTER_DIGIT, + CoordIJK.Direction.CENTER_DIGIT }, + { + CoordIJK.Direction.CENTER_DIGIT, + CoordIJK.Direction.CENTER_DIGIT, + CoordIJK.Direction.CENTER_DIGIT, + CoordIJK.Direction.CENTER_DIGIT, + CoordIJK.Direction.I_AXES_DIGIT, + CoordIJK.Direction.IK_AXES_DIGIT, + CoordIJK.Direction.I_AXES_DIGIT }, + { + CoordIJK.Direction.CENTER_DIGIT, + CoordIJK.Direction.K_AXES_DIGIT, + CoordIJK.Direction.CENTER_DIGIT, + CoordIJK.Direction.CENTER_DIGIT, + CoordIJK.Direction.IK_AXES_DIGIT, + CoordIJK.Direction.IK_AXES_DIGIT, + CoordIJK.Direction.CENTER_DIGIT }, + { + CoordIJK.Direction.CENTER_DIGIT, + CoordIJK.Direction.CENTER_DIGIT, + CoordIJK.Direction.IJ_AXES_DIGIT, + CoordIJK.Direction.CENTER_DIGIT, + CoordIJK.Direction.I_AXES_DIGIT, + CoordIJK.Direction.CENTER_DIGIT, + CoordIJK.Direction.IJ_AXES_DIGIT } }; + + /** + * Produce all neighboring cells. For Hexagons there will be 6 neighbors while + * for pentagon just 5. + * Output is placed in the provided array in no particular order. + * + * @param origin origin cell + */ + public static long[] hexRing(long origin) { + final long[] out = H3Index.H3_is_pentagon(origin) ? new long[5] : new long[6]; + int idx = 0; + long previous = -1; + for (int i = 0; i < 6; i++) { + int[] rotations = new int[] { 0 }; + long[] nextNeighbor = new long[] { 0 }; + int neighborResult = h3NeighborRotations(origin, DIRECTIONS[i].digit(), rotations, nextNeighbor); + if (neighborResult != E_PENTAGON) { + // E_PENTAGON is an expected case when trying to traverse off of + // pentagons. + if (neighborResult != E_SUCCESS) { + throw new IllegalArgumentException(); + } + if (previous != nextNeighbor[0]) { + out[idx++] = nextNeighbor[0]; + previous = nextNeighbor[0]; + } + } + } + assert idx == out.length; + return out; + } + + /** + * Returns the hexagon index neighboring the origin, in the direction dir. + * + * Implementation note: The only reachable case where this returns 0 is if the + * origin is a pentagon and the translation is in the k direction. Thus, + * 0 can only be returned if origin is a pentagon. + * + * @param origin Origin index + * @param dir Direction to move in + * @param rotations Number of ccw rotations to perform to reorient the + * translation vector. Will be modified to the new number of + * rotations to perform (such as when crossing a face edge.) + * @param out H3Index of the specified neighbor if succesful + * @return E_SUCCESS on success + */ + private static int h3NeighborRotations(long origin, int dir, int[] rotations, long[] out) { + long current = origin; + + for (int i = 0; i < rotations[0]; i++) { + dir = CoordIJK.rotate60ccw(dir); + } + + int newRotations = 0; + int oldBaseCell = H3Index.H3_get_base_cell(current); + if (oldBaseCell < 0 || oldBaseCell >= Constants.NUM_BASE_CELLS) { // LCOV_EXCL_BR_LINE + // Base cells less than zero can not be represented in an index + return E_CELL_INVALID; + } + int oldLeadingDigit = H3Index.h3LeadingNonZeroDigit(current); + + // Adjust the indexing digits and, if needed, the base cell. + int r = H3Index.H3_get_resolution(current) - 1; + while (true) { + if (r == -1) { + current = H3Index.H3_set_base_cell(current, baseCellNeighbors[oldBaseCell][dir]); + newRotations = baseCellNeighbor60CCWRots[oldBaseCell][dir]; + + if (H3Index.H3_get_base_cell(current) == INVALID_BASE_CELL) { + // Adjust for the deleted k vertex at the base cell level. + // This edge actually borders a different neighbor. + current = H3Index.H3_set_base_cell(current, baseCellNeighbors[oldBaseCell][CoordIJK.Direction.IK_AXES_DIGIT.digit()]); + newRotations = baseCellNeighbor60CCWRots[oldBaseCell][CoordIJK.Direction.IK_AXES_DIGIT.digit()]; + + // perform the adjustment for the k-subsequence we're skipping + // over. + current = H3Index.h3Rotate60ccw(current); + rotations[0] = rotations[0] + 1; + } + + break; + } else { + int oldDigit = H3Index.H3_get_index_digit(current, r + 1); + int nextDir; + if (oldDigit == CoordIJK.Direction.INVALID_DIGIT.digit()) { + // Only possible on invalid input + return E_CELL_INVALID; + } else if (H3Index.isResolutionClassIII(r + 1)) { + current = H3Index.H3_set_index_digit(current, r + 1, NEW_DIGIT_II[oldDigit][dir].digit()); + nextDir = NEW_ADJUSTMENT_II[oldDigit][dir].digit(); + } else { + current = H3Index.H3_set_index_digit(current, r + 1, NEW_DIGIT_III[oldDigit][dir].digit()); + nextDir = NEW_ADJUSTMENT_III[oldDigit][dir].digit(); + } + + if (nextDir != CoordIJK.Direction.CENTER_DIGIT.digit()) { + dir = nextDir; + r--; + } else { + // No more adjustment to perform + break; + } + } + } + + int newBaseCell = H3Index.H3_get_base_cell(current); + if (BaseCells.isBaseCellPentagon(newBaseCell)) { + boolean alreadyAdjustedKSubsequence = false; + + // force rotation out of missing k-axes sub-sequence + if (H3Index.h3LeadingNonZeroDigit(current) == CoordIJK.Direction.K_AXES_DIGIT.digit()) { + if (oldBaseCell != newBaseCell) { + // in this case, we traversed into the deleted + // k subsequence of a pentagon base cell. + // We need to rotate out of that case depending + // on how we got here. + // check for a cw/ccw offset face; default is ccw + + if (BaseCells.baseCellIsCwOffset(newBaseCell, BaseCells.getBaseFaceIJK(oldBaseCell).face)) { + current = H3Index.h3Rotate60cw(current); + } else { + // See cwOffsetPent in testGridDisk.c for why this is + // unreachable. + current = H3Index.h3Rotate60ccw(current); // LCOV_EXCL_LINE + } + alreadyAdjustedKSubsequence = true; + } else { + // In this case, we traversed into the deleted + // k subsequence from within the same pentagon + // base cell. + if (oldLeadingDigit == CoordIJK.Direction.CENTER_DIGIT.digit()) { + // Undefined: the k direction is deleted from here + return E_PENTAGON; + } else if (oldLeadingDigit == CoordIJK.Direction.JK_AXES_DIGIT.digit()) { + // Rotate out of the deleted k subsequence + // We also need an additional change to the direction we're + // moving in + current = H3Index.h3Rotate60ccw(current); + rotations[0] = rotations[0] + 1; + } else if (oldLeadingDigit == CoordIJK.Direction.IK_AXES_DIGIT.digit()) { + // Rotate out of the deleted k subsequence + // We also need an additional change to the direction we're + // moving in + current = H3Index.h3Rotate60cw(current); + rotations[0] = rotations[0] + 5; + } else { + // Should never occur + return E_FAILED; // LCOV_EXCL_LINE + } + } + } + + for (int i = 0; i < newRotations; i++) + current = H3Index.h3RotatePent60ccw(current); + + // Account for differing orientation of the base cells (this edge + // might not follow properties of some other edges.) + if (oldBaseCell != newBaseCell) { + if (BaseCells.isBaseCellPolarPentagon(newBaseCell)) { + // 'polar' base cells behave differently because they have all + // i neighbors. + if (oldBaseCell != 118 + && oldBaseCell != 8 + && H3Index.h3LeadingNonZeroDigit(current) != CoordIJK.Direction.JK_AXES_DIGIT.digit()) { + rotations[0] = rotations[0] + 1; + } + } else if (H3Index.h3LeadingNonZeroDigit(current) == CoordIJK.Direction.IK_AXES_DIGIT.digit() + && alreadyAdjustedKSubsequence == false) { + // account for distortion introduced to the 5 neighbor by the + // deleted k subsequence. + rotations[0] = rotations[0] + 1; + } + } + } else { + for (int i = 0; i < newRotations; i++) + current = H3Index.h3Rotate60ccw(current); + } + + rotations[0] = (rotations[0] + newRotations) % 6; + out[0] = current; + + return E_SUCCESS; + } + +} diff --git a/libs/h3/src/main/java/org/opensearch/geospatial/h3/Iterator.java b/libs/h3/src/main/java/org/opensearch/geospatial/h3/Iterator.java new file mode 100644 index 0000000000..ad21842d19 --- /dev/null +++ b/libs/h3/src/main/java/org/opensearch/geospatial/h3/Iterator.java @@ -0,0 +1,310 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * This project is based on a modification of https://github.com/uber/h3 which is licensed under the Apache 2.0 License. + * + * Copyright 2021 Uber Technologies, Inc. + */ +package org.opensearch.geospatial.h3; + +/** + * Iterator structures and functions for the children of a cell. + */ +final class Iterator { + /** + * Invalid index used to indicate an error from latLngToCell and related + * functions or missing data in arrays of H3 indices. Analogous to NaN in + * floating point. + */ + public static final long H3_NULL = 0; + + /** + * The number of bits in a single H3 resolution digit. + */ + private static final int H3_PER_DIGIT_OFFSET = 3; + + /** + * IterCellsChildren: struct for iterating through the descendants of + * a given cell. + *

+ * Constructors: + *

+ * Initialize with either `iterInitParent` or `iterInitBaseCellNum`. + * `iterInitParent` sets up an iterator for all the children of a given + * parent cell at a given resolution. + *

+ * `iterInitBaseCellNum` sets up an iterator for children cells, given + * a base cell number (0--121). + *

+ * Iteration: + *

+ * Step iterator with `iterStepChild`. + * During the lifetime of the `IterCellsChildren`, the current iterate + * is accessed via the `IterCellsChildren.h` member. + * When the iterator is exhausted or if there was an error in initialization, + * `IterCellsChildren.h` will be `H3_NULL` even after calling `iterStepChild`. + */ + static class IterCellsChildren { + long h; + int _parentRes; // parent resolution + int _skipDigit; // this digit skips `1` for pentagons + + IterCellsChildren(long h, int _parentRes, int _skipDigit) { + this.h = h; + this._parentRes = _parentRes; + this._skipDigit = _skipDigit; + } + } + + /** + * Create a fully nulled-out child iterator for when an iterator is exhausted. + * This helps minimize the chance that a user will depend on the iterator + * internal state after it's exhausted, like the child resolution, for + * example. + */ + private static IterCellsChildren nullIter() { + return new IterCellsChildren(H3_NULL, -1, -1); + } + + /** + ## Logic for iterating through the children of a cell + We'll describe the logic for .... + - normal (non pentagon iteration) + - pentagon iteration. define "pentagon digit" + ### Cell Index Component Diagrams + The lower 56 bits of an H3 Cell Index describe the following index components: + - the cell resolution (4 bits) + - the base cell number (7 bits) + - the child cell digit for each resolution from 1 to 15 (3*15 = 45 bits) + These are the bits we'll be focused on when iterating through child cells. + To help describe the iteration logic, we'll use diagrams displaying the + (decimal) values for each component like: + child digit for resolution 2 + / + | res | base cell # | 1 | 2 | 3 | 4 | 5 | 6 | ... | + |-----|-------------|---|---|---|---|---|---|-----| + | 9 | 17 | 5 | 3 | 0 | 6 | 2 | 1 | ... | + ### Iteration through children of a hexagon (but not a pentagon) + Iteration through the children of a *hexagon* (but not a pentagon) + simply involves iterating through all the children values (0--6) + for each child digit (up to the child's resolution). + For example, suppose a resolution 3 hexagon index has the following + components: + parent resolution + / + | res | base cell # | 1 | 2 | 3 | 4 | 5 | 6 | ... | + |-----|-------------|---|---|---|---|---|---|-----| + | 3 | 17 | 3 | 5 | 1 | 7 | 7 | 7 | ... | + The iteration through all children of resolution 6 would look like: + parent res child res + / / + | res | base cell # | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | ... | + |-----|-------------|---|---|---|---|---|---|---|---|-----| + | 6 | 17 | 3 | 5 | 1 | 0 | 0 | 0 | 7 | 7 | ... | + | 6 | 17 | 3 | 5 | 1 | 0 | 0 | 1 | 7 | 7 | ... | + | ... | | | | | | | | | | | + | 6 | 17 | 3 | 5 | 1 | 0 | 0 | 6 | 7 | 7 | ... | + | 6 | 17 | 3 | 5 | 1 | 0 | 1 | 0 | 7 | 7 | ... | + | 6 | 17 | 3 | 5 | 1 | 0 | 1 | 1 | 7 | 7 | ... | + | ... | | | | | | | | | | | + | 6 | 17 | 3 | 5 | 1 | 6 | 6 | 6 | 7 | 7 | ... | + ### Step sequence on a *pentagon* cell + Pentagon cells have a base cell number (e.g., 97) corresponding to a + resolution 0 pentagon, and have all zeros from digit 1 to the digit + corresponding to the cell's resolution. + (We'll drop the ellipses from now on, knowing that digits should contain + 7's beyond the cell resolution.) + parent res child res + / / + | res | base cell # | 1 | 2 | 3 | 4 | 5 | 6 | + |-----|-------------|---|---|---|---|---|---| + | 6 | 97 | 0 | 0 | 0 | 0 | 0 | 0 | + Iteration through children of a *pentagon* is almost the same + as *hexagon* iteration, except that we skip the *first* 1 value + that appears in the "skip digit". This corresponds to the fact + that a pentagon only has 6 children, which are denoted with + the numbers {0,2,3,4,5,6}. + The skip digit starts at the child resolution position. + When iterating through children more than one resolution below + the parent, we move the skip digit to the left + (up to the next coarser resolution) each time we skip the 1 value + in that digit. + Iteration would start like: + parent res child res + / / + | res | base cell # | 1 | 2 | 3 | 4 | 5 | 6 | + |-----|-------------|---|---|---|---|---|---| + | 6 | 97 | 0 | 0 | 0 | 0 | 0 | 0 | + \ + skip digit + Noticing we skip the 1 value and move the skip digit, + the next iterate would be: + | res | base cell # | 1 | 2 | 3 | 4 | 5 | 6 | + |-----|-------------|---|---|---|---|---|---| + | 6 | 97 | 0 | 0 | 0 | 0 | 0 | 2 | + \ + skip digit + Iteration continues normally until we get to: + | res | base cell # | 1 | 2 | 3 | 4 | 5 | 6 | + |-----|-------------|---|---|---|---|---|---| + | 6 | 97 | 0 | 0 | 0 | 0 | 0 | 6 | + \ + skip digit + which is followed by (skipping the 1): + | res | base cell # | 1 | 2 | 3 | 4 | 5 | 6 | + |-----|-------------|---|---|---|---|---|---| + | 6 | 97 | 0 | 0 | 0 | 0 | 2 | 0 | + \ + skip digit + For the next iterate, we won't skip the `1` in the previous digit + because it is no longer the skip digit: + | res | base cell # | 1 | 2 | 3 | 4 | 5 | 6 | + |-----|-------------|---|---|---|---|---|---| + | 6 | 97 | 0 | 0 | 0 | 0 | 2 | 1 | + \ + skip digit + Iteration continues normally until we're right before the next skip + digit: + | res | base cell # | 1 | 2 | 3 | 4 | 5 | 6 | + |-----|-------------|---|---|---|---|---|---| + | 6 | 97 | 0 | 0 | 0 | 0 | 6 | 6 | + \ + skip digit + Which is followed by + | res | base cell # | 1 | 2 | 3 | 4 | 5 | 6 | + |-----|-------------|---|---|---|---|---|---| + | 6 | 97 | 0 | 0 | 0 | 2 | 0 | 0 | + \ + skip digit + and so on. + */ + + /** + * Initialize a IterCellsChildren struct representing the sequence giving + * the children of cell `h` at resolution `childRes`. + *

+ * At any point in the iteration, starting once + * the struct is initialized, IterCellsChildren.h gives the current child. + *

+ * Also, IterCellsChildren.h == H3_NULL when all the children have been iterated + * through, or if the input to `iterInitParent` was invalid. + */ + public static IterCellsChildren iterInitParent(long h, int childRes) { + + int parentRes = H3Index.H3_get_resolution(h); + + if (childRes < parentRes || childRes > Constants.MAX_H3_RES || h == H3_NULL) { + return nullIter(); + } + + long newH = zeroIndexDigits(h, parentRes + 1, childRes); + newH = H3Index.H3_set_resolution(newH, childRes); + + int _skipDigit; + if (H3Index.H3_is_pentagon(newH)) { + // The skip digit skips `1` for pentagons. + // The "_skipDigit" moves to the left as we count up from the + // child resolution to the parent resolution. + _skipDigit = childRes; + } else { + // if not a pentagon, we can ignore "skip digit" logic + _skipDigit = -1; + } + + return new IterCellsChildren(newH, parentRes, _skipDigit); + } + + /** + * Step a IterCellsChildren to the next child cell. + * When the iteration is over, IterCellsChildren.h will be H3_NULL. + * Handles iterating through hexagon and pentagon cells. + */ + public static void iterStepChild(IterCellsChildren it) { + // once h == H3_NULL, the iterator returns an infinite sequence of H3_NULL + if (it.h == H3_NULL) return; + + int childRes = H3Index.H3_get_resolution(it.h); + + incrementResDigit(it, childRes); + + for (int i = childRes; i >= it._parentRes; i--) { + if (i == it._parentRes) { + // if we're modifying the parent resolution digit, then we're done + // *it = _null_iter(); + it.h = H3_NULL; + return; + } + + // PENTAGON_SKIPPED_DIGIT == 1 + if (i == it._skipDigit && getResDigit(it, i) == CoordIJK.Direction.PENTAGON_SKIPPED_DIGIT.digit()) { + // Then we are iterating through the children of a pentagon cell. + // All children of a pentagon have the property that the first + // nonzero digit between the parent and child resolutions is + // not 1. + // I.e., we never see a sequence like 00001. + // Thus, we skip the `1` in this digit. + incrementResDigit(it, i); + it._skipDigit -= 1; + return; + } + + // INVALID_DIGIT == 7 + if (getResDigit(it, i) == CoordIJK.Direction.INVALID_DIGIT.digit()) { + incrementResDigit(it, i); // zeros out it[i] and increments it[i-1] by 1 + } else { + break; + } + } + } + + // extract the `res` digit (0--7) of the current cell + private static int getResDigit(IterCellsChildren it, int res) { + return H3Index.H3_get_index_digit(it.h, res); + } + + /** + * Zero out index digits from start to end, inclusive. + * No-op if start > end. + */ + private static long zeroIndexDigits(long h, int start, int end) { + if (start > end) { + return h; + } + + long m = 0; + + m = ~m; + m <<= H3_PER_DIGIT_OFFSET * (end - start + 1); + m = ~m; + m <<= H3_PER_DIGIT_OFFSET * (Constants.MAX_H3_RES - end); + m = ~m; + + return h & m; + } + + // increment the digit (0--7) at location `res` + private static void incrementResDigit(IterCellsChildren it, int res) { + long val = 1; + val <<= H3_PER_DIGIT_OFFSET * (Constants.MAX_H3_RES - res); + it.h += val; + } +} diff --git a/libs/h3/src/main/java/org/opensearch/geospatial/h3/LatLng.java b/libs/h3/src/main/java/org/opensearch/geospatial/h3/LatLng.java new file mode 100644 index 0000000000..427e1e3d8f --- /dev/null +++ b/libs/h3/src/main/java/org/opensearch/geospatial/h3/LatLng.java @@ -0,0 +1,125 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * This project is based on a modification of https://github.com/uber/h3 which is licensed under the Apache 2.0 License. + * + * Copyright 2016-2021 Uber Technologies, Inc. + */ +package org.opensearch.geospatial.h3; + +/** pair of latitude/longitude */ +public final class LatLng { + + // lat / lon in radians + private final double lon; + private final double lat; + + LatLng(double lat, double lon) { + this.lon = lon; + this.lat = lat; + } + + /** Returns latitude in radians */ + public double getLatRad() { + return lat; + } + + /** Returns longitude in radians */ + public double getLonRad() { + return lon; + } + + /** Returns latitude in degrees */ + public double getLatDeg() { + return Math.toDegrees(getLatRad()); + } + + /** Returns longitude in degrees */ + public double getLonDeg() { + return Math.toDegrees(getLonRad()); + } + + /** + * Encodes a coordinate on the sphere to the corresponding icosahedral face and + * containing 2D hex coordinates relative to that face center. + * + * @param res The desired H3 resolution for the encoding. + */ + FaceIJK geoToFaceIJK(int res) { + Vec3d v3d = new Vec3d(this); + + // determine the icosahedron face + int face = 0; + double sqd = v3d.pointSquareDist(Vec3d.faceCenterPoint[0]); + for (int i = 1; i < Vec3d.faceCenterPoint.length; i++) { + double sqdT = v3d.pointSquareDist(Vec3d.faceCenterPoint[i]); + if (sqdT < sqd) { + face = i; + sqd = sqdT; + } + } + // cos(r) = 1 - 2 * sin^2(r/2) = 1 - 2 * (sqd / 4) = 1 - sqd/2 + double r = Math.acos(1 - sqd / 2); + + if (r < Constants.EPSILON) { + return new FaceIJK(face, new Vec2d(0.0, 0.0).hex2dToCoordIJK()); + } + + // now have face and r, now find CCW theta from CII i-axis + double theta = Vec2d.posAngleRads( + Vec2d.faceAxesAzRadsCII[face][0] - Vec2d.posAngleRads(Vec2d.faceCenterGeo[face].geoAzimuthRads(this)) + ); + + // adjust theta for Class III (odd resolutions) + if (H3Index.isResolutionClassIII(res)) { + theta = Vec2d.posAngleRads(theta - Constants.M_AP7_ROT_RADS); + } + + // perform gnomonic scaling of r + r = Math.tan(r); + + // scale for current resolution length u + r /= Constants.RES0_U_GNOMONIC; + for (int i = 0; i < res; i++) { + r *= Constants.M_SQRT7; + } + + // we now have (r, theta) in hex2d with theta ccw from x-axes + + // convert to local x,y + Vec2d vec2d = new Vec2d(r * Math.cos(theta), r * Math.sin(theta)); + return new FaceIJK(face, vec2d.hex2dToCoordIJK()); + } + + /** + * Determines the azimuth to the provided LatLng in radians. + * + * @param p The spherical coordinates. + * @return The azimuth in radians. + */ + private double geoAzimuthRads(LatLng p) { + return Math.atan2( + Math.cos(p.lat) * Math.sin(p.lon - lon), + Math.cos(lat) * Math.sin(p.lat) - Math.sin(lat) * Math.cos(p.lat) * Math.cos(p.lon - lon) + ); + } +} diff --git a/libs/h3/src/main/java/org/opensearch/geospatial/h3/Vec2d.java b/libs/h3/src/main/java/org/opensearch/geospatial/h3/Vec2d.java new file mode 100644 index 0000000000..aefd3ce8ca --- /dev/null +++ b/libs/h3/src/main/java/org/opensearch/geospatial/h3/Vec2d.java @@ -0,0 +1,408 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * This project is based on a modification of https://github.com/uber/h3 which is licensed under the Apache 2.0 License. + * + * Copyright 2016-2017 Uber Technologies, Inc. + */ +package org.opensearch.geospatial.h3; + +import java.util.Objects; + +/** + * 2D floating-point vector + */ +final class Vec2d { + + /** sin(60') */ + private static final double M_SIN60 = Constants.M_SQRT3_2; + + /** + * icosahedron face centers in lat/lng radians + */ + public static final LatLng[] faceCenterGeo = new LatLng[] { + new LatLng(0.803582649718989942, 1.248397419617396099), // face 0 + new LatLng(1.307747883455638156, 2.536945009877921159), // face 1 + new LatLng(1.054751253523952054, -1.347517358900396623), // face 2 + new LatLng(0.600191595538186799, -0.450603909469755746), // face 3 + new LatLng(0.491715428198773866, 0.401988202911306943), // face 4 + new LatLng(0.172745327415618701, 1.678146885280433686), // face 5 + new LatLng(0.605929321571350690, 2.953923329812411617), // face 6 + new LatLng(0.427370518328979641, -1.888876200336285401), // face 7 + new LatLng(-0.079066118549212831, -0.733429513380867741), // face 8 + new LatLng(-0.230961644455383637, 0.506495587332349035), // face 9 + new LatLng(0.079066118549212831, 2.408163140208925497), // face 10 + new LatLng(0.230961644455383637, -2.635097066257444203), // face 11 + new LatLng(-0.172745327415618701, -1.463445768309359553), // face 12 + new LatLng(-0.605929321571350690, -0.187669323777381622), // face 13 + new LatLng(-0.427370518328979641, 1.252716453253507838), // face 14 + new LatLng(-0.600191595538186799, 2.690988744120037492), // face 15 + new LatLng(-0.491715428198773866, -2.739604450678486295), // face 16 + new LatLng(-0.803582649718989942, -1.893195233972397139), // face 17 + new LatLng(-1.307747883455638156, -0.604647643711872080), // face 18 + new LatLng(-1.054751253523952054, 1.794075294689396615), // face 19 + }; + + /** + * icosahedron face ijk axes as azimuth in radians from face center to + * vertex 0/1/2 respectively + */ + public static final double[][] faceAxesAzRadsCII = new double[][] { + { 5.619958268523939882, 3.525563166130744542, 1.431168063737548730 }, // face 0 + { 5.760339081714187279, 3.665943979320991689, 1.571548876927796127 }, // face 1 + { 0.780213654393430055, 4.969003859179821079, 2.874608756786625655 }, // face 2 + { 0.430469363979999913, 4.619259568766391033, 2.524864466373195467 }, // face 3 + { 6.130269123335111400, 4.035874020941915804, 1.941478918548720291 }, // face 4 + { 2.692877706530642877, 0.598482604137447119, 4.787272808923838195 }, // face 5 + { 2.982963003477243874, 0.888567901084048369, 5.077358105870439581 }, // face 6 + { 3.532912002790141181, 1.438516900396945656, 5.627307105183336758 }, // face 7 + { 3.494305004259568154, 1.399909901866372864, 5.588700106652763840 }, // face 8 + { 3.003214169499538391, 0.908819067106342928, 5.097609271892733906 }, // face 9 + { 5.930472956509811562, 3.836077854116615875, 1.741682751723420374 }, // face 10 + { 0.138378484090254847, 4.327168688876645809, 2.232773586483450311 }, // face 11 + { 0.448714947059150361, 4.637505151845541521, 2.543110049452346120 }, // face 12 + { 0.158629650112549365, 4.347419854898940135, 2.253024752505744869 }, // face 13 + { 5.891865957979238535, 3.797470855586042958, 1.703075753192847583 }, // face 14 + { 2.711123289609793325, 0.616728187216597771, 4.805518392002988683 }, // face 15 + { 3.294508837434268316, 1.200113735041072948, 5.388903939827463911 }, // face 16 + { 3.804819692245439833, 1.710424589852244509, 5.899214794638635174 }, // face 17 + { 3.664438879055192436, 1.570043776661997111, 5.758833981448388027 }, // face 18 + { 2.361378999196363184, 0.266983896803167583, 4.455774101589558636 }, // face 19 + }; + + /** + * pi + */ + private static double M_PI = 3.14159265358979323846; + /** + * pi / 2.0 + */ + private static double M_PI_2 = 1.5707963267948966; + /** + * 2.0 * PI + */ + public static double M_2PI = 6.28318530717958647692528676655900576839433; + + private final double x; /// < x component + private final double y; /// < y component + + Vec2d(double x, double y) { + this.x = x; + this.y = y; + } + + /** + * Determines the center point in spherical coordinates of a cell given by 2D + * hex coordinates on a particular icosahedral face. + * + * @param face The icosahedral face upon which the 2D hex coordinate system is + * centered. + * @param res The H3 resolution of the cell. + * @param substrate Indicates whether or not this grid is actually a substrate + * grid relative to the specified resolution. + */ + public LatLng hex2dToGeo(int face, int res, boolean substrate) { + // calculate (r, theta) in hex2d + double r = v2dMag(); + + if (r < Constants.EPSILON) { + return faceCenterGeo[face]; + } + + double theta = Math.atan2(y, x); + + // scale for current resolution length u + for (int i = 0; i < res; i++) { + r /= Constants.M_SQRT7; + } + + // scale accordingly if this is a substrate grid + if (substrate) { + r /= 3.0; + if (H3Index.isResolutionClassIII(res)) { + r /= Constants.M_SQRT7; + } + } + + r *= Constants.RES0_U_GNOMONIC; + + // perform inverse gnomonic scaling of r + r = Math.atan(r); + + // adjust theta for Class III + // if a substrate grid, then it's already been adjusted for Class III + if (substrate == false && H3Index.isResolutionClassIII(res)) theta = posAngleRads(theta + Constants.M_AP7_ROT_RADS); + + // find theta as an azimuth + theta = posAngleRads(faceAxesAzRadsCII[face][0] - theta); + + // now find the point at (r,theta) from the face center + return geoAzDistanceRads(faceCenterGeo[face], theta, r); + } + + /** + * Determine the containing hex in ijk+ coordinates for a 2D cartesian + * coordinate vector (from DGGRID). + * + */ + public CoordIJK hex2dToCoordIJK() { + double a1, a2; + double x1, x2; + int m1, m2; + double r1, r2; + + // quantize into the ij system and then normalize + int k = 0; + int i; + int j; + + a1 = Math.abs(x); + a2 = Math.abs(y); + + // first do a reverse conversion + x2 = a2 / M_SIN60; + x1 = a1 + x2 / 2.0; + + // check if we have the center of a hex + m1 = (int) x1; + m2 = (int) x2; + + // otherwise round correctly + r1 = x1 - m1; + r2 = x2 - m2; + + if (r1 < 0.5) { + if (r1 < 1.0 / 3.0) { + if (r2 < (1.0 + r1) / 2.0) { + i = m1; + j = m2; + } else { + i = m1; + j = m2 + 1; + } + } else { + if (r2 < (1.0 - r1)) { + j = m2; + } else { + j = m2 + 1; + } + + if ((1.0 - r1) <= r2 && r2 < (2.0 * r1)) { + i = m1 + 1; + } else { + i = m1; + } + } + } else { + if (r1 < 2.0 / 3.0) { + if (r2 < (1.0 - r1)) { + j = m2; + } else { + j = m2 + 1; + } + + if ((2.0 * r1 - 1.0) < r2 && r2 < (1.0 - r1)) { + i = m1; + } else { + i = m1 + 1; + } + } else { + if (r2 < (r1 / 2.0)) { + i = m1 + 1; + j = m2; + } else { + i = m1 + 1; + j = m2 + 1; + } + } + } + + // now fold across the axes if necessary + + if (x < 0.0) { + if ((j % 2) == 0) // even + { + int axisi = j / 2; + int diff = i - axisi; + i = i - 2 * diff; + } else { + int axisi = (j + 1) / 2; + int diff = i - axisi; + i = i - (2 * diff + 1); + } + } + + if (y < 0.0) { + i = i - (2 * j + 1) / 2; + j = -1 * j; + } + CoordIJK coordIJK = new CoordIJK(i, j, k); + coordIJK.ijkNormalize(); + return coordIJK; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Vec2d vec2d = (Vec2d) o; + return Double.compare(vec2d.x, x) == 0 && Double.compare(vec2d.y, y) == 0; + } + + @Override + public int hashCode() { + return Objects.hash(x, y); + } + + /** + * Finds the intersection between two lines. Assumes that the lines intersect + * and that the intersection is not at an endpoint of either line. + * + * @param p0 The first endpoint of the first line. + * @param p1 The second endpoint of the first line. + * @param p2 The first endpoint of the second line. + * @param p3 The second endpoint of the second line. + */ + public static Vec2d v2dIntersect(Vec2d p0, Vec2d p1, Vec2d p2, Vec2d p3) { + double[] s1 = new double[2], s2 = new double[2]; + s1[0] = p1.x - p0.x; + s1[1] = p1.y - p0.y; + s2[0] = p3.x - p2.x; + s2[1] = p3.y - p2.y; + + float t; + t = (float) ((s2[0] * (p0.y - p2.y) - s2[1] * (p0.x - p2.x)) / (-s2[0] * s1[1] + s1[0] * s2[1])); + + return new Vec2d(p0.x + (t * s1[0]), p0.y + (t * s1[1])); + } + + /** + * Calculates the magnitude of a 2D cartesian vector. + * + * @return The magnitude of the vector. + */ + private double v2dMag() { + return Math.sqrt(x * x + y * y); + } + + /** + * Normalizes radians to a value between 0.0 and two PI. + * + * @param rads The input radians value. + * @return The normalized radians value. + */ + static double posAngleRads(double rads) { + double tmp = ((rads < 0.0) ? rads + M_2PI : rads); + if (rads >= M_2PI) tmp -= M_2PI; + return tmp; + } + + /** + * Computes the point on the sphere a specified azimuth and distance from + * another point. + * + * @param p1 The first spherical coordinates. + * @param az The desired azimuth from p1. + * @param distance The desired distance from p1, must be non-negative. + * p1. + */ + private static LatLng geoAzDistanceRads(LatLng p1, double az, double distance) { + if (distance < Constants.EPSILON) { + return p1; + } + + double sinlat, sinlng, coslng; + + az = posAngleRads(az); + + double lat, lon; + + // check for due north/south azimuth + if (az < Constants.EPSILON || Math.abs(az - M_PI) < Constants.EPSILON) { + if (az < Constants.EPSILON) {// due north + lat = p1.getLatRad() + distance; + } else { // due south + lat = p1.getLatRad() - distance; + } + if (Math.abs(lat - M_PI_2) < Constants.EPSILON) { // north pole + lat = M_PI_2; + lon = 0.0; + } else if (Math.abs(lat + M_PI_2) < Constants.EPSILON) { // south pole + lat = -M_PI_2; + lon = 0.0; + } else { + lon = constrainLng(p1.getLonRad()); + } + } else { // not due north or south + sinlat = Math.sin(p1.getLatRad()) * Math.cos(distance) + Math.cos(p1.getLatRad()) * Math.sin(distance) * Math.cos(az); + if (sinlat > 1.0) { + sinlat = 1.0; + } + if (sinlat < -1.0) { + sinlat = -1.0; + } + lat = Math.asin(sinlat); + if (Math.abs(lat - M_PI_2) < Constants.EPSILON) // north pole + { + lat = M_PI_2; + lon = 0.0; + } else if (Math.abs(lat + M_PI_2) < Constants.EPSILON) // south pole + { + lat = -M_PI_2; + lon = 0.0; + } else { + sinlng = Math.sin(az) * Math.sin(distance) / Math.cos(lat); + coslng = (Math.cos(distance) - Math.sin(p1.getLatRad()) * Math.sin(lat)) / Math.cos(p1.getLatRad()) / Math.cos(lat); + if (sinlng > 1.0) { + sinlng = 1.0; + } + if (sinlng < -1.0) { + sinlng = -1.0; + } + if (coslng > 1.0) { + coslng = 1.0; + } + if (coslng < -1.0) { + coslng = -1.0; + } + lon = constrainLng(p1.getLonRad() + Math.atan2(sinlng, coslng)); + } + } + return new LatLng(lat, lon); + } + + /** + * constrainLng makes sure longitudes are in the proper bounds + * + * @param lng The origin lng value + * @return The corrected lng value + */ + private static double constrainLng(double lng) { + while (lng > M_PI) { + lng = lng - (2 * M_PI); + } + while (lng < -M_PI) { + lng = lng + (2 * M_PI); + } + return lng; + } +} diff --git a/libs/h3/src/main/java/org/opensearch/geospatial/h3/Vec3d.java b/libs/h3/src/main/java/org/opensearch/geospatial/h3/Vec3d.java new file mode 100644 index 0000000000..f4b75ef8f9 --- /dev/null +++ b/libs/h3/src/main/java/org/opensearch/geospatial/h3/Vec3d.java @@ -0,0 +1,87 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * This project is based on a modification of https://github.com/uber/h3 which is licensed under the Apache 2.0 License. + * + * Copyright 2018, 2020-2021 Uber Technologies, Inc. + */ + +package org.opensearch.geospatial.h3; + +final class Vec3d { + + /** icosahedron face centers in x/y/z on the unit sphere */ + public static final double[][] faceCenterPoint = new double[][] { + { 0.2199307791404606, 0.6583691780274996, 0.7198475378926182 }, // face 0 + { -0.2139234834501421, 0.1478171829550703, 0.9656017935214205 }, // face 1 + { 0.1092625278784797, -0.4811951572873210, 0.8697775121287253 }, // face 2 + { 0.7428567301586791, -0.3593941678278028, 0.5648005936517033 }, // face 3 + { 0.8112534709140969, 0.3448953237639384, 0.4721387736413930 }, // face 4 + { -0.1055498149613921, 0.9794457296411413, 0.1718874610009365 }, // face 5 + { -0.8075407579970092, 0.1533552485898818, 0.5695261994882688 }, // face 6 + { -0.2846148069787907, -0.8644080972654206, 0.4144792552473539 }, // face 7 + { 0.7405621473854482, -0.6673299564565524, -0.0789837646326737 }, // face 8 + { 0.8512303986474293, 0.4722343788582681, -0.2289137388687808 }, // face 9 + { -0.7405621473854481, 0.6673299564565524, 0.0789837646326737 }, // face 10 + { -0.8512303986474292, -0.4722343788582682, 0.2289137388687808 }, // face 11 + { 0.1055498149613919, -0.9794457296411413, -0.1718874610009365 }, // face 12 + { 0.8075407579970092, -0.1533552485898819, -0.5695261994882688 }, // face 13 + { 0.2846148069787908, 0.8644080972654204, -0.4144792552473539 }, // face 14 + { -0.7428567301586791, 0.3593941678278027, -0.5648005936517033 }, // face 15 + { -0.8112534709140971, -0.3448953237639382, -0.4721387736413930 }, // face 16 + { -0.2199307791404607, -0.6583691780274996, -0.7198475378926182 }, // face 17 + { 0.2139234834501420, -0.1478171829550704, -0.9656017935214205 }, // face 18 + { -0.1092625278784796, 0.4811951572873210, -0.8697775121287253 }, // face 19 + }; + + private final double x; + private final double y; + private final double z; + + Vec3d(LatLng latLng) { + double r = Math.cos(latLng.getLatRad()); + this.z = Math.sin(latLng.getLatRad()); + this.x = Math.cos(latLng.getLonRad()) * r; + this.y = Math.sin(latLng.getLonRad()) * r; + } + + /** + * Calculate the square of the distance between two 3D coordinates. + * + * @param v The first 3D coordinate. + * @return The square of the distance between the given points. + */ + public double pointSquareDist(double[] v) { + return square(x - v[0]) + square(y - v[1]) + square(z - v[2]); + } + + /** + * Square of a number + * + * @param x The input number. + * @return The square of the input number. + */ + private double square(double x) { + return x * x; + } + +} diff --git a/libs/h3/src/test/java/org/opensearch/geospatial/h3/CellBoundaryTests.java b/libs/h3/src/test/java/org/opensearch/geospatial/h3/CellBoundaryTests.java new file mode 100644 index 0000000000..82c2263528 --- /dev/null +++ b/libs/h3/src/test/java/org/opensearch/geospatial/h3/CellBoundaryTests.java @@ -0,0 +1,182 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.opensearch.geospatial.h3; + +import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream; +import org.opensearch.test.OpenSearchTestCase; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.StringTokenizer; + +public class CellBoundaryTests extends OpenSearchTestCase { + + public void testRes0() throws Exception { + processFile("res00cells.txt"); + } + + public void testRes1() throws Exception { + processFile("res01cells.txt"); + } + + public void testRes2() throws Exception { + processFile("res02cells.txt"); + } + + public void testRes3() throws Exception { + processFile("res03cells.txt"); + } + + public void testBc05r08cells() throws Exception { + processFile("bc05r08cells.txt"); + } + + public void testBc05r09cells() throws Exception { + processFile("bc05r09cells.txt"); + } + + public void testBc05r10cells() throws Exception { + processFile("bc05r10cells.txt"); + } + + public void testBc05r11cells() throws Exception { + processFile("bc05r11cells.txt"); + } + + public void testBc05r12cells() throws Exception { + processFile("bc05r12cells.txt"); + } + + public void testBc05r13cells() throws Exception { + processFile("bc05r13cells.txt"); + } + + public void testBc05r05cells() throws Exception { + processFile("bc05r14cells.txt"); + } + + public void testBc05r15cells() throws Exception { + processFile("bc05r15cells.txt"); + } + + public void testBc14r08cells() throws Exception { + processFile("bc14r08cells.txt"); + } + + public void testBc14r09cells() throws Exception { + processFile("bc14r09cells.txt"); + } + + public void testBc14r10cells() throws Exception { + processFile("bc14r10cells.txt"); + } + + public void testBc14r11cells() throws Exception { + processFile("bc14r11cells.txt"); + } + + public void testBc14r12cells() throws Exception { + processFile("bc14r12cells.txt"); + } + + public void testBc14r13cells() throws Exception { + processFile("bc14r13cells.txt"); + } + + public void testBc14r14cells() throws Exception { + processFile("bc14r14cells.txt"); + } + + public void testBc14r15cells() throws Exception { + processFile("bc14r15cells.txt"); + } + + public void testBc19r08cells() throws Exception { + processFile("bc19r08cells.txt"); + } + + public void testBc19r09cells() throws Exception { + processFile("bc19r09cells.txt"); + } + + public void testBc19r10cells() throws Exception { + processFile("bc19r10cells.txt"); + } + + public void testBc19r11cells() throws Exception { + processFile("bc19r11cells.txt"); + } + + public void testBc19r12cells() throws Exception { + processFile("bc19r12cells.txt"); + } + + public void testBc19r13cells() throws Exception { + processFile("bc19r13cells.txt"); + } + + public void testBc19r14cells() throws Exception { + processFile("bc19r14cells.txt"); + } + + private void processFile(String file) throws IOException { + InputStream fis = getClass().getResourceAsStream(file + ".gz"); + BufferedReader reader = new BufferedReader(new InputStreamReader(new GzipCompressorInputStream(fis), StandardCharsets.UTF_8)); + String h3Address = reader.readLine(); + while (h3Address != null) { + assertEquals(true, H3.h3IsValid(h3Address)); + long h3 = H3.stringToH3(h3Address); + assertEquals(true, H3.h3IsValid(h3)); + processOne(h3Address, reader); + h3Address = reader.readLine(); + } + } + + private void processOne(String h3Address, BufferedReader reader) throws IOException { + String line = reader.readLine(); + if ("{".equals(line) == false) { + throw new IllegalArgumentException(); + } + line = reader.readLine(); + List points = new ArrayList<>(); + while ("}".equals(line) == false) { + StringTokenizer tokens = new StringTokenizer(line, " "); + assertEquals(2, tokens.countTokens()); + double lat = Double.parseDouble(tokens.nextToken()); + double lon = Double.parseDouble(tokens.nextToken()); + points.add(new double[] { lat, lon }); + line = reader.readLine(); + } + CellBoundary boundary = H3.h3ToGeoBoundary(h3Address); + assert boundary.numPoints() == points.size(); + for (int i = 0; i < boundary.numPoints(); i++) { + assertEquals(h3Address, points.get(i)[0], boundary.getLatLon(i).getLatDeg(), 1e-8); + assertEquals(h3Address, points.get(i)[1], boundary.getLatLon(i).getLonDeg(), 1e-8); + } + } +} diff --git a/libs/h3/src/test/java/org/opensearch/geospatial/h3/CellCenterTests.java b/libs/h3/src/test/java/org/opensearch/geospatial/h3/CellCenterTests.java new file mode 100644 index 0000000000..1d7b026dc4 --- /dev/null +++ b/libs/h3/src/test/java/org/opensearch/geospatial/h3/CellCenterTests.java @@ -0,0 +1,187 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.opensearch.geospatial.h3; + +import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream; +import org.opensearch.test.OpenSearchTestCase; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.StringTokenizer; + +public class CellCenterTests extends OpenSearchTestCase { + + public void testRes0() throws Exception { + processFile("res00ic.txt"); + } + + public void testRes1() throws Exception { + processFile("res01ic.txt"); + } + + public void testRes2() throws Exception { + processFile("res02ic.txt"); + } + + public void testRes3() throws Exception { + processFile("res03ic.txt"); + } + + public void testBc05r08centers() throws Exception { + processFile("bc05r08centers.txt"); + } + + public void testBc05r09centers() throws Exception { + processFile("bc05r09centers.txt"); + } + + public void testBc05r10centers() throws Exception { + processFile("bc05r10centers.txt"); + } + + public void testBc05r11centers() throws Exception { + processFile("bc05r11centers.txt"); + } + + public void testBc05r12centers() throws Exception { + processFile("bc05r12centers.txt"); + } + + public void testBc05r13centers() throws Exception { + processFile("bc05r13centers.txt"); + } + + public void testBc05r05centers() throws Exception { + processFile("bc05r14centers.txt"); + } + + public void testBc05r15centers() throws Exception { + processFile("bc05r15centers.txt"); + } + + public void testBc14r08centers() throws Exception { + processFile("bc14r08centers.txt"); + } + + public void testBc14r09centers() throws Exception { + processFile("bc14r09centers.txt"); + } + + public void testBc14r10centers() throws Exception { + processFile("bc14r10centers.txt"); + } + + public void testBc14r11centers() throws Exception { + processFile("bc14r11centers.txt"); + } + + public void testBc14r12centers() throws Exception { + processFile("bc14r12centers.txt"); + } + + public void testBc14r13centers() throws Exception { + processFile("bc14r13centers.txt"); + } + + public void testBc14r14centers() throws Exception { + processFile("bc14r14centers.txt"); + } + + public void testBc14r15centers() throws Exception { + processFile("bc14r15centers.txt"); + } + + public void testBc19r08centers() throws Exception { + processFile("bc19r08centers.txt"); + } + + public void testBc19r09centers() throws Exception { + processFile("bc19r09centers.txt"); + } + + public void testBc19r10centers() throws Exception { + processFile("bc19r10centers.txt"); + } + + public void testBc19r11centers() throws Exception { + processFile("bc19r11centers.txt"); + } + + public void testBc19r12centers() throws Exception { + processFile("bc19r12centers.txt"); + } + + public void testBc19r13centers() throws Exception { + processFile("bc19r13centers.txt"); + } + + public void testBc19r14centers() throws Exception { + processFile("bc19r14centers.txt"); + } + + public void testBc19r15centers() throws Exception { + processFile("bc19r15centers.txt"); + } + + private void processFile(String file) throws IOException { + InputStream fis = getClass().getResourceAsStream(file + ".gz"); + BufferedReader reader = new BufferedReader(new InputStreamReader(new GzipCompressorInputStream(fis), StandardCharsets.UTF_8)); + String line = reader.readLine(); + while (line != null) { + StringTokenizer tokenizer = new StringTokenizer(line, " "); + assertEquals(3, tokenizer.countTokens()); + String h3Address = tokenizer.nextToken(); + assertEquals(h3Address, true, H3.h3IsValid(h3Address)); + double lat = Double.parseDouble(tokenizer.nextToken()); + double lon = Double.parseDouble(tokenizer.nextToken()); + assertH3ToLatLng(h3Address, lat, lon); + assertGeoToH3(h3Address, lat, lon); + assertHexRing(h3Address); + line = reader.readLine(); + } + } + + private void assertH3ToLatLng(String h3Address, double lat, double lon) { + LatLng latLng = H3.h3ToLatLng(h3Address); + assertEquals(h3Address, lat, latLng.getLatDeg(), 1e-6); + assertEquals(h3Address, lon, latLng.getLonDeg(), 1e-6); + } + + private void assertGeoToH3(String h3Address, double lat, double lon) { + String computedH3Address = H3.geoToH3Address(lat, lon, H3Index.H3_get_resolution(H3.stringToH3(h3Address))); + assertEquals(h3Address, computedH3Address); + assertEquals(h3Address, computedH3Address); + } + + private void assertHexRing(String h3Address) { + String[] neighbors = H3.hexRing(h3Address); + long center = H3.stringToH3(h3Address); + for (String neighbor : neighbors) { + long l = H3.stringToH3(neighbor); + assertEquals(H3Index.H3_get_resolution(center), H3Index.H3_get_resolution(l)); + } + } +} diff --git a/libs/h3/src/test/java/org/opensearch/geospatial/h3/GeoToH3Tests.java b/libs/h3/src/test/java/org/opensearch/geospatial/h3/GeoToH3Tests.java new file mode 100644 index 0000000000..2f55983f64 --- /dev/null +++ b/libs/h3/src/test/java/org/opensearch/geospatial/h3/GeoToH3Tests.java @@ -0,0 +1,60 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.opensearch.geospatial.h3; + +import org.apache.lucene.tests.geo.GeoTestUtil; +import org.opensearch.test.OpenSearchTestCase; + +public class GeoToH3Tests extends OpenSearchTestCase { + + public void testRandomPoints() { + for (int i = 0; i < 50; i++) { + // avoid points close to the poles + double lat = randomValueOtherThanMany(d -> d > 60 || d < -60, GeoTestUtil::nextLatitude); + // avoid points close to the dateline + double lon = randomValueOtherThanMany(d -> d > 150 || d < -150, GeoTestUtil::nextLongitude); + testPoint(lat, lon); + } + } + + private void testPoint(double lat, double lon) { + for (int i = 0; i < Constants.MAX_H3_RES; i++) { + String h3Address = H3.geoToH3Address(lat, lon, i); + CellBoundary cellBoundary = H3.h3ToGeoBoundary(h3Address); + double minLat = cellBoundary.getLatLon(0).getLatDeg(); + double maxLat = cellBoundary.getLatLon(0).getLatDeg(); + double minLon = cellBoundary.getLatLon(0).getLonDeg(); + double maxLon = cellBoundary.getLatLon(0).getLonDeg(); + for (int j = 0; j < cellBoundary.numPoints(); j++) { + minLat = Math.min(minLat, cellBoundary.getLatLon(j).getLatDeg()); + maxLat = Math.max(maxLat, cellBoundary.getLatLon(j).getLatDeg()); + minLon = Math.min(minLon, cellBoundary.getLatLon(j).getLonDeg()); + maxLon = Math.max(maxLon, cellBoundary.getLatLon(j).getLonDeg()); + } + assertTrue(minLat <= lat); + assertTrue(maxLat >= lat); + assertTrue(minLon <= lon); + assertTrue(maxLon >= lon); + } + } +} diff --git a/libs/h3/src/test/java/org/opensearch/geospatial/h3/ParentChildNavigationTests.java b/libs/h3/src/test/java/org/opensearch/geospatial/h3/ParentChildNavigationTests.java new file mode 100644 index 0000000000..a0a1a3a209 --- /dev/null +++ b/libs/h3/src/test/java/org/opensearch/geospatial/h3/ParentChildNavigationTests.java @@ -0,0 +1,72 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.opensearch.geospatial.h3; + +import com.carrotsearch.randomizedtesting.generators.RandomPicks; + +import org.opensearch.test.OpenSearchTestCase; + +public class ParentChildNavigationTests extends OpenSearchTestCase { + + public void testParentChild() { + String[] h3Addresses = H3.getStringRes0Cells(); + String h3Address = RandomPicks.randomFrom(random(), h3Addresses); + String[] values = new String[H3.MAX_H3_RES]; + values[0] = h3Address; + for (int i = 1; i < H3.MAX_H3_RES; i++) { + h3Addresses = H3.h3ToChildren(h3Address); + h3Address = RandomPicks.randomFrom(random(), h3Addresses); + values[i] = h3Address; + } + h3Addresses = H3.h3ToChildren(h3Address); + h3Address = RandomPicks.randomFrom(random(), h3Addresses); + for (int i = H3.MAX_H3_RES - 1; i >= H3.MIN_H3_RES; i--) { + h3Address = H3.h3ToParent(h3Address); + assertEquals(values[i], h3Address); + } + } + + public void testHexRing() { + String[] h3Addresses = H3.getStringRes0Cells(); + String h3Address = RandomPicks.randomFrom(random(), h3Addresses); + for (int i = 1; i < H3.MAX_H3_RES; i++) { + h3Addresses = H3.h3ToChildren(h3Address); + assertHexRing(i, h3Address, h3Addresses); + h3Address = RandomPicks.randomFrom(random(), h3Addresses); + } + } + + private static final int[] HEX_RING_POSITIONS = new int[] { 2, 0, 1, 4, 3, 5 }; + private static final int[] PENT_RING_POSITIONS = new int[] { 0, 1, 3, 2, 4 }; + + private void assertHexRing(int res, String h3Address, String[] children) { + LatLng latLng = H3.h3ToLatLng(h3Address); + String centerChild = H3.geoToH3Address(latLng.getLatDeg(), latLng.getLonDeg(), res); + assertEquals(children[0], centerChild); + String[] ring = H3.hexRing(centerChild); + int[] positions = H3.isPentagon(centerChild) ? PENT_RING_POSITIONS : HEX_RING_POSITIONS; + for (int i = 1; i < children.length; i++) { + assertEquals(children[i], ring[positions[i - 1]]); + } + } +} diff --git a/libs/h3/src/test/resources/org/opensearch/geospatial/h3/NOTICE.txt b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/NOTICE.txt new file mode 100644 index 0000000000..50caeb4a70 --- /dev/null +++ b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/NOTICE.txt @@ -0,0 +1,4 @@ +The files under this directory come from the input test files from Uber's h3 repository +(https://github.com/uber/h3/tree/master/tests/inputfiles) and are made available here +under the same Apache 2 license. + diff --git a/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc05r08cells.txt.gz b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc05r08cells.txt.gz new file mode 100644 index 0000000000..e8904b41cf Binary files /dev/null and b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc05r08cells.txt.gz differ diff --git a/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc05r08centers.txt.gz b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc05r08centers.txt.gz new file mode 100644 index 0000000000..9efeadc839 Binary files /dev/null and b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc05r08centers.txt.gz differ diff --git a/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc05r09cells.txt.gz b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc05r09cells.txt.gz new file mode 100644 index 0000000000..dbb2e59344 Binary files /dev/null and b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc05r09cells.txt.gz differ diff --git a/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc05r09centers.txt.gz b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc05r09centers.txt.gz new file mode 100644 index 0000000000..9cbeae277c Binary files /dev/null and b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc05r09centers.txt.gz differ diff --git a/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc05r10cells.txt.gz b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc05r10cells.txt.gz new file mode 100644 index 0000000000..f91ba38794 Binary files /dev/null and b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc05r10cells.txt.gz differ diff --git a/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc05r10centers.txt.gz b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc05r10centers.txt.gz new file mode 100644 index 0000000000..405a1e3650 Binary files /dev/null and b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc05r10centers.txt.gz differ diff --git a/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc05r11cells.txt.gz b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc05r11cells.txt.gz new file mode 100644 index 0000000000..ebffe35712 Binary files /dev/null and b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc05r11cells.txt.gz differ diff --git a/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc05r11centers.txt.gz b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc05r11centers.txt.gz new file mode 100644 index 0000000000..04c8ac2ff8 Binary files /dev/null and b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc05r11centers.txt.gz differ diff --git a/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc05r12cells.txt.gz b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc05r12cells.txt.gz new file mode 100644 index 0000000000..8462076718 Binary files /dev/null and b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc05r12cells.txt.gz differ diff --git a/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc05r12centers.txt.gz b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc05r12centers.txt.gz new file mode 100644 index 0000000000..ae9cc56994 Binary files /dev/null and b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc05r12centers.txt.gz differ diff --git a/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc05r13cells.txt.gz b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc05r13cells.txt.gz new file mode 100644 index 0000000000..d8da966a35 Binary files /dev/null and b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc05r13cells.txt.gz differ diff --git a/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc05r13centers.txt.gz b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc05r13centers.txt.gz new file mode 100644 index 0000000000..e3e98279ac Binary files /dev/null and b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc05r13centers.txt.gz differ diff --git a/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc05r14cells.txt.gz b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc05r14cells.txt.gz new file mode 100644 index 0000000000..27be76a9ae Binary files /dev/null and b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc05r14cells.txt.gz differ diff --git a/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc05r14centers.txt.gz b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc05r14centers.txt.gz new file mode 100644 index 0000000000..0695444fb4 Binary files /dev/null and b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc05r14centers.txt.gz differ diff --git a/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc05r15cells.txt.gz b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc05r15cells.txt.gz new file mode 100644 index 0000000000..73e1244722 Binary files /dev/null and b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc05r15cells.txt.gz differ diff --git a/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc05r15centers.txt.gz b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc05r15centers.txt.gz new file mode 100644 index 0000000000..1ac7986927 Binary files /dev/null and b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc05r15centers.txt.gz differ diff --git a/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc14r08cells.txt.gz b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc14r08cells.txt.gz new file mode 100644 index 0000000000..5b0dc4330f Binary files /dev/null and b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc14r08cells.txt.gz differ diff --git a/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc14r08centers.txt.gz b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc14r08centers.txt.gz new file mode 100644 index 0000000000..911f7e4c71 Binary files /dev/null and b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc14r08centers.txt.gz differ diff --git a/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc14r09cells.txt.gz b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc14r09cells.txt.gz new file mode 100644 index 0000000000..f4a614bf13 Binary files /dev/null and b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc14r09cells.txt.gz differ diff --git a/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc14r09centers.txt.gz b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc14r09centers.txt.gz new file mode 100644 index 0000000000..28be0951ad Binary files /dev/null and b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc14r09centers.txt.gz differ diff --git a/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc14r10cells.txt.gz b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc14r10cells.txt.gz new file mode 100644 index 0000000000..95941dc11b Binary files /dev/null and b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc14r10cells.txt.gz differ diff --git a/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc14r10centers.txt.gz b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc14r10centers.txt.gz new file mode 100644 index 0000000000..da6b7d2987 Binary files /dev/null and b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc14r10centers.txt.gz differ diff --git a/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc14r11cells.txt.gz b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc14r11cells.txt.gz new file mode 100644 index 0000000000..e05bb28b59 Binary files /dev/null and b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc14r11cells.txt.gz differ diff --git a/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc14r11centers.txt.gz b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc14r11centers.txt.gz new file mode 100644 index 0000000000..636d4d149c Binary files /dev/null and b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc14r11centers.txt.gz differ diff --git a/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc14r12cells.txt.gz b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc14r12cells.txt.gz new file mode 100644 index 0000000000..5fc28a11bb Binary files /dev/null and b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc14r12cells.txt.gz differ diff --git a/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc14r12centers.txt.gz b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc14r12centers.txt.gz new file mode 100644 index 0000000000..e0dd3f4395 Binary files /dev/null and b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc14r12centers.txt.gz differ diff --git a/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc14r13cells.txt.gz b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc14r13cells.txt.gz new file mode 100644 index 0000000000..8c49ce4f37 Binary files /dev/null and b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc14r13cells.txt.gz differ diff --git a/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc14r13centers.txt.gz b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc14r13centers.txt.gz new file mode 100644 index 0000000000..2ea6219f01 Binary files /dev/null and b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc14r13centers.txt.gz differ diff --git a/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc14r14cells.txt.gz b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc14r14cells.txt.gz new file mode 100644 index 0000000000..caf54eb6d7 Binary files /dev/null and b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc14r14cells.txt.gz differ diff --git a/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc14r14centers.txt.gz b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc14r14centers.txt.gz new file mode 100644 index 0000000000..2a96ded412 Binary files /dev/null and b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc14r14centers.txt.gz differ diff --git a/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc14r15cells.txt.gz b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc14r15cells.txt.gz new file mode 100644 index 0000000000..2a64b905e8 Binary files /dev/null and b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc14r15cells.txt.gz differ diff --git a/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc14r15centers.txt.gz b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc14r15centers.txt.gz new file mode 100644 index 0000000000..963b44b952 Binary files /dev/null and b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc14r15centers.txt.gz differ diff --git a/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc19r08cells.txt.gz b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc19r08cells.txt.gz new file mode 100644 index 0000000000..201c6d2cea Binary files /dev/null and b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc19r08cells.txt.gz differ diff --git a/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc19r08centers.txt.gz b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc19r08centers.txt.gz new file mode 100644 index 0000000000..f1011e0783 Binary files /dev/null and b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc19r08centers.txt.gz differ diff --git a/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc19r09cells.txt.gz b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc19r09cells.txt.gz new file mode 100644 index 0000000000..51bf7405fd Binary files /dev/null and b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc19r09cells.txt.gz differ diff --git a/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc19r09centers.txt.gz b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc19r09centers.txt.gz new file mode 100644 index 0000000000..6e8dba126b Binary files /dev/null and b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc19r09centers.txt.gz differ diff --git a/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc19r10cells.txt.gz b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc19r10cells.txt.gz new file mode 100644 index 0000000000..1bb9cd27ac Binary files /dev/null and b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc19r10cells.txt.gz differ diff --git a/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc19r10centers.txt.gz b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc19r10centers.txt.gz new file mode 100644 index 0000000000..c20c3029c6 Binary files /dev/null and b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc19r10centers.txt.gz differ diff --git a/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc19r11cells.txt.gz b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc19r11cells.txt.gz new file mode 100644 index 0000000000..23d923d820 Binary files /dev/null and b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc19r11cells.txt.gz differ diff --git a/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc19r11centers.txt.gz b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc19r11centers.txt.gz new file mode 100644 index 0000000000..5a0a57a9c4 Binary files /dev/null and b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc19r11centers.txt.gz differ diff --git a/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc19r12cells.txt.gz b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc19r12cells.txt.gz new file mode 100644 index 0000000000..16631958ef Binary files /dev/null and b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc19r12cells.txt.gz differ diff --git a/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc19r12centers.txt.gz b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc19r12centers.txt.gz new file mode 100644 index 0000000000..77d2cb9d0b Binary files /dev/null and b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc19r12centers.txt.gz differ diff --git a/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc19r13cells.txt.gz b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc19r13cells.txt.gz new file mode 100644 index 0000000000..74d5d33beb Binary files /dev/null and b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc19r13cells.txt.gz differ diff --git a/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc19r13centers.txt.gz b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc19r13centers.txt.gz new file mode 100644 index 0000000000..961b5a4c3e Binary files /dev/null and b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc19r13centers.txt.gz differ diff --git a/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc19r14cells.txt.gz b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc19r14cells.txt.gz new file mode 100644 index 0000000000..8c3c6ead1b Binary files /dev/null and b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc19r14cells.txt.gz differ diff --git a/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc19r14centers.txt.gz b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc19r14centers.txt.gz new file mode 100644 index 0000000000..114ff52644 Binary files /dev/null and b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc19r14centers.txt.gz differ diff --git a/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc19r15cells.txt.gz b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc19r15cells.txt.gz new file mode 100644 index 0000000000..bb1394d19b Binary files /dev/null and b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc19r15cells.txt.gz differ diff --git a/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc19r15centers.txt.gz b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc19r15centers.txt.gz new file mode 100644 index 0000000000..0b3a1ac52e Binary files /dev/null and b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/bc19r15centers.txt.gz differ diff --git a/libs/h3/src/test/resources/org/opensearch/geospatial/h3/res00cells.txt.gz b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/res00cells.txt.gz new file mode 100644 index 0000000000..5b45234c7d Binary files /dev/null and b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/res00cells.txt.gz differ diff --git a/libs/h3/src/test/resources/org/opensearch/geospatial/h3/res00ic.txt.gz b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/res00ic.txt.gz new file mode 100644 index 0000000000..498d8de3b7 Binary files /dev/null and b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/res00ic.txt.gz differ diff --git a/libs/h3/src/test/resources/org/opensearch/geospatial/h3/res01cells.txt.gz b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/res01cells.txt.gz new file mode 100644 index 0000000000..f6437ad724 Binary files /dev/null and b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/res01cells.txt.gz differ diff --git a/libs/h3/src/test/resources/org/opensearch/geospatial/h3/res01ic.txt.gz b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/res01ic.txt.gz new file mode 100644 index 0000000000..3d8f456950 Binary files /dev/null and b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/res01ic.txt.gz differ diff --git a/libs/h3/src/test/resources/org/opensearch/geospatial/h3/res02cells.txt.gz b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/res02cells.txt.gz new file mode 100644 index 0000000000..0c63e3bbee Binary files /dev/null and b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/res02cells.txt.gz differ diff --git a/libs/h3/src/test/resources/org/opensearch/geospatial/h3/res02ic.txt.gz b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/res02ic.txt.gz new file mode 100644 index 0000000000..9cfdb2b17e Binary files /dev/null and b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/res02ic.txt.gz differ diff --git a/libs/h3/src/test/resources/org/opensearch/geospatial/h3/res03cells.txt.gz b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/res03cells.txt.gz new file mode 100644 index 0000000000..13cdb00539 Binary files /dev/null and b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/res03cells.txt.gz differ diff --git a/libs/h3/src/test/resources/org/opensearch/geospatial/h3/res03ic.txt.gz b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/res03ic.txt.gz new file mode 100644 index 0000000000..d72ae7a290 Binary files /dev/null and b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/res03ic.txt.gz differ diff --git a/libs/h3/src/test/resources/org/opensearch/geospatial/h3/res04ic.txt.gz b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/res04ic.txt.gz new file mode 100644 index 0000000000..be6b73d103 Binary files /dev/null and b/libs/h3/src/test/resources/org/opensearch/geospatial/h3/res04ic.txt.gz differ diff --git a/settings.gradle b/settings.gradle index be8ff3ba98..72251801a7 100644 --- a/settings.gradle +++ b/settings.gradle @@ -8,3 +8,6 @@ */ rootProject.name = 'geospatial' + +include ":libs" +include ":libs:h3" \ No newline at end of file diff --git a/src/main/java/org/opensearch/geospatial/plugin/GeospatialPlugin.java b/src/main/java/org/opensearch/geospatial/plugin/GeospatialPlugin.java index 9dc7d6e8ac..56349622f0 100644 --- a/src/main/java/org/opensearch/geospatial/plugin/GeospatialPlugin.java +++ b/src/main/java/org/opensearch/geospatial/plugin/GeospatialPlugin.java @@ -34,6 +34,8 @@ import org.opensearch.geospatial.index.query.xyshape.XYShapeQueryBuilder; import org.opensearch.geospatial.processor.FeatureProcessor; import org.opensearch.geospatial.rest.action.upload.geojson.RestUploadGeoJSONAction; +import org.opensearch.geospatial.search.aggregations.bucket.geogrid.GeoHexGrid; +import org.opensearch.geospatial.search.aggregations.bucket.geogrid.GeoHexGridAggregationBuilder; import org.opensearch.geospatial.stats.upload.RestUploadStatsAction; import org.opensearch.geospatial.stats.upload.UploadStats; import org.opensearch.geospatial.stats.upload.UploadStatsAction; @@ -120,4 +122,19 @@ public List> getQueries() { // Register XYShapeQuery Builder to be delegated for query type: xy_shape return List.of(new QuerySpec<>(XYShapeQueryBuilder.NAME, XYShapeQueryBuilder::new, XYShapeQueryBuilder::fromXContent)); } + + /** + * Registering {@link GeoHexGrid} aggregation on GeoPoint field. + */ + @Override + public List getAggregations() { + + final var geoHexGridSpec = new AggregationSpec( + GeoHexGridAggregationBuilder.NAME, + GeoHexGridAggregationBuilder::new, + GeoHexGridAggregationBuilder.PARSER + ).addResultReader(GeoHexGrid::new).setAggregatorRegistrar(GeoHexGridAggregationBuilder::registerAggregators); + + return List.of(geoHexGridSpec); + } } diff --git a/src/main/java/org/opensearch/geospatial/search/aggregations/bucket/geogrid/GeoHexGrid.java b/src/main/java/org/opensearch/geospatial/search/aggregations/bucket/geogrid/GeoHexGrid.java new file mode 100644 index 0000000000..9b336f86da --- /dev/null +++ b/src/main/java/org/opensearch/geospatial/search/aggregations/bucket/geogrid/GeoHexGrid.java @@ -0,0 +1,64 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.geospatial.search.aggregations.bucket.geogrid; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import org.opensearch.common.io.stream.StreamInput; +import org.opensearch.geo.search.aggregations.bucket.geogrid.BaseGeoGrid; +import org.opensearch.geo.search.aggregations.bucket.geogrid.BaseGeoGridBucket; +import org.opensearch.search.aggregations.InternalAggregations; + +/** + * Represents a grid of cells where each cell's location is determined by a h3 cell address. + * All h3CellAddress in a grid are of the same precision + */ +public final class GeoHexGrid extends BaseGeoGrid { + + public GeoHexGrid(StreamInput in) throws IOException { + super(in); + } + + @Override + public BaseGeoGrid create(List list) { + return new GeoHexGrid(name, requiredSize, buckets, metadata); + } + + @Override + public BaseGeoGridBucket createBucket(InternalAggregations internalAggregations, BaseGeoGridBucket baseGeoGridBucket) { + return new GeoHexGridBucket(baseGeoGridBucket.hashAsLong(), baseGeoGridBucket.getDocCount(), internalAggregations); + } + + @Override + public String getWriteableName() { + return GeoHexGridAggregationBuilder.NAME; + } + + protected GeoHexGrid(String name, int requiredSize, List buckets, Map metadata) { + super(name, requiredSize, buckets, metadata); + } + + @Override + protected Reader getBucketReader() { + return GeoHexGridBucket::new; + } + + @Override + protected BaseGeoGrid create(String name, int requiredSize, List buckets, Map metadata) { + return new GeoHexGrid(name, requiredSize, buckets, metadata); + } + + @Override + protected GeoHexGridBucket createBucket(long address, long docCount, InternalAggregations internalAggregations) { + return new GeoHexGridBucket(address, docCount, internalAggregations); + } + + int getRequiredSize() { + return requiredSize; + } +} diff --git a/src/main/java/org/opensearch/geospatial/search/aggregations/bucket/geogrid/GeoHexGridAggregationBuilder.java b/src/main/java/org/opensearch/geospatial/search/aggregations/bucket/geogrid/GeoHexGridAggregationBuilder.java new file mode 100644 index 0000000000..c1016261c8 --- /dev/null +++ b/src/main/java/org/opensearch/geospatial/search/aggregations/bucket/geogrid/GeoHexGridAggregationBuilder.java @@ -0,0 +1,135 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.geospatial.search.aggregations.bucket.geogrid; + +import static org.opensearch.geospatial.search.aggregations.bucket.geogrid.GeoHexHelper.checkPrecisionRange; + +import java.io.IOException; +import java.util.Map; + +import org.opensearch.OpenSearchParseException; +import org.opensearch.common.geo.GeoBoundingBox; +import org.opensearch.common.io.stream.StreamInput; +import org.opensearch.common.xcontent.ObjectParser; +import org.opensearch.common.xcontent.XContentParser; +import org.opensearch.common.xcontent.support.XContentMapValues; +import org.opensearch.geo.search.aggregations.bucket.geogrid.GeoGridAggregationBuilder; +import org.opensearch.geo.search.aggregations.metrics.GeoGridAggregatorSupplier; +import org.opensearch.index.query.QueryShardContext; +import org.opensearch.search.aggregations.AggregationBuilder; +import org.opensearch.search.aggregations.AggregatorFactories; +import org.opensearch.search.aggregations.AggregatorFactory; +import org.opensearch.search.aggregations.support.ValuesSourceAggregatorFactory; +import org.opensearch.search.aggregations.support.ValuesSourceConfig; +import org.opensearch.search.aggregations.support.ValuesSourceRegistry; + +/** + * Aggregation Builder for geo hex grid + */ +public class GeoHexGridAggregationBuilder extends GeoGridAggregationBuilder { + + /** + * Aggregation context name + */ + public static final String NAME = "geohex_grid"; + public static final ValuesSourceRegistry.RegistryKey REGISTRY_KEY = new ValuesSourceRegistry.RegistryKey<>( + NAME, + GeoGridAggregatorSupplier.class + ); + public static final ObjectParser PARSER = createParser( + NAME, + GeoHexGridAggregationBuilder::parsePrecision, + GeoHexGridAggregationBuilder::new + ); + private static final int DEFAULT_MAX_NUM_CELLS = 10000; + private static final int DEFAULT_PRECISION = 5; + private static final int DEFAULT_SHARD_SIZE = -1; + + public GeoHexGridAggregationBuilder(String name) { + super(name); + precision(DEFAULT_PRECISION); + size(DEFAULT_MAX_NUM_CELLS); + shardSize = DEFAULT_SHARD_SIZE; + } + + public GeoHexGridAggregationBuilder(StreamInput in) throws IOException { + super(in); + } + + @Override + public String getType() { + return NAME; + } + + /** + * Register's Geo Hex Aggregation + * @param builder Builder to register new Aggregation + */ + public static void registerAggregators(final ValuesSourceRegistry.Builder builder) { + GeoHexGridAggregatorFactory.registerAggregators(builder); + } + + @Override + public GeoGridAggregationBuilder precision(int precision) { + checkPrecisionRange(precision); + this.precision = precision; + return this; + } + + protected GeoHexGridAggregationBuilder( + GeoGridAggregationBuilder clone, + AggregatorFactories.Builder factoriesBuilder, + Map metadata + ) { + super(clone, factoriesBuilder, metadata); + } + + @Override + protected ValuesSourceAggregatorFactory createFactory( + String name, + ValuesSourceConfig config, + int precision, + int requiredSize, + int shardSize, + GeoBoundingBox geoBoundingBox, + QueryShardContext queryShardContext, + AggregatorFactory aggregatorFactory, + AggregatorFactories.Builder builder, + Map metadata + ) throws IOException { + return new GeoHexGridAggregatorFactory( + name, + config, + precision, + requiredSize, + shardSize, + geoBoundingBox, + queryShardContext, + aggregatorFactory, + builder, + metadata + ); + } + + @Override + protected ValuesSourceRegistry.RegistryKey getRegistryKey() { + return REGISTRY_KEY; + } + + @Override + protected AggregationBuilder shallowCopy(AggregatorFactories.Builder builder, Map metadata) { + return new GeoHexGridAggregationBuilder(this, builder, metadata); + } + + private static int parsePrecision(final XContentParser parser) throws IOException, OpenSearchParseException { + final var token = parser.currentToken(); + if (token.equals(XContentParser.Token.VALUE_NUMBER)) { + return XContentMapValues.nodeIntegerValue(parser.intValue()); + } + final var precision = parser.text(); + return XContentMapValues.nodeIntegerValue(precision); + } +} diff --git a/src/main/java/org/opensearch/geospatial/search/aggregations/bucket/geogrid/GeoHexGridAggregator.java b/src/main/java/org/opensearch/geospatial/search/aggregations/bucket/geogrid/GeoHexGridAggregator.java new file mode 100644 index 0000000000..813575c924 --- /dev/null +++ b/src/main/java/org/opensearch/geospatial/search/aggregations/bucket/geogrid/GeoHexGridAggregator.java @@ -0,0 +1,48 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.geospatial.search.aggregations.bucket.geogrid; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import org.opensearch.geo.search.aggregations.bucket.geogrid.BaseGeoGridBucket; +import org.opensearch.geo.search.aggregations.bucket.geogrid.GeoGridAggregator; +import org.opensearch.search.aggregations.Aggregator; +import org.opensearch.search.aggregations.AggregatorFactories; +import org.opensearch.search.aggregations.CardinalityUpperBound; +import org.opensearch.search.aggregations.support.ValuesSource; +import org.opensearch.search.internal.SearchContext; + +/** + * Aggregates data expressed as H3 Cell ID. + */ +public class GeoHexGridAggregator extends GeoGridAggregator { + + public GeoHexGridAggregator( + String name, + AggregatorFactories factories, + ValuesSource.Numeric valuesSource, + int requiredSize, + int shardSize, + SearchContext aggregationContext, + Aggregator parent, + CardinalityUpperBound cardinality, + Map metadata + ) throws IOException { + super(name, factories, valuesSource, requiredSize, shardSize, aggregationContext, parent, cardinality, metadata); + } + + @Override + protected GeoHexGrid buildAggregation(String name, int requiredSize, List buckets, Map metadata) { + return new GeoHexGrid(name, requiredSize, buckets, metadata); + } + + @Override + protected BaseGeoGridBucket newEmptyBucket() { + return new GeoHexGridBucket(0, 0, null); + } +} diff --git a/src/main/java/org/opensearch/geospatial/search/aggregations/bucket/geogrid/GeoHexGridAggregatorFactory.java b/src/main/java/org/opensearch/geospatial/search/aggregations/bucket/geogrid/GeoHexGridAggregatorFactory.java new file mode 100644 index 0000000000..78fb0edc9a --- /dev/null +++ b/src/main/java/org/opensearch/geospatial/search/aggregations/bucket/geogrid/GeoHexGridAggregatorFactory.java @@ -0,0 +1,129 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.geospatial.search.aggregations.bucket.geogrid; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import org.opensearch.common.geo.GeoBoundingBox; +import org.opensearch.geo.search.aggregations.bucket.geogrid.CellIdSource; +import org.opensearch.index.query.QueryShardContext; +import org.opensearch.search.aggregations.Aggregator; +import org.opensearch.search.aggregations.AggregatorFactories; +import org.opensearch.search.aggregations.AggregatorFactory; +import org.opensearch.search.aggregations.CardinalityUpperBound; +import org.opensearch.search.aggregations.InternalAggregation; +import org.opensearch.search.aggregations.NonCollectingAggregator; +import org.opensearch.search.aggregations.support.CoreValuesSourceType; +import org.opensearch.search.aggregations.support.ValuesSource; +import org.opensearch.search.aggregations.support.ValuesSourceAggregatorFactory; +import org.opensearch.search.aggregations.support.ValuesSourceConfig; +import org.opensearch.search.aggregations.support.ValuesSourceRegistry; +import org.opensearch.search.internal.SearchContext; + +/** + * Aggregation Factory for geohex_grid agg + */ +public class GeoHexGridAggregatorFactory extends ValuesSourceAggregatorFactory { + private final int precision; + private final int requiredSize; + private final int shardSize; + private final GeoBoundingBox geoBoundingBox; + + GeoHexGridAggregatorFactory( + String name, + ValuesSourceConfig config, + int precision, + int requiredSize, + int shardSize, + GeoBoundingBox geoBoundingBox, + QueryShardContext queryShardContext, + AggregatorFactory parent, + AggregatorFactories.Builder subFactoriesBuilder, + Map metadata + ) throws IOException { + super(name, config, queryShardContext, parent, subFactoriesBuilder, metadata); + this.precision = precision; + this.requiredSize = requiredSize; + this.shardSize = shardSize; + this.geoBoundingBox = geoBoundingBox; + } + + @Override + protected Aggregator createUnmapped(SearchContext searchContext, Aggregator aggregator, Map map) throws IOException { + final var aggregation = new GeoHexGrid(name, requiredSize, List.of(), metadata); + + return new NonCollectingAggregator(name, searchContext, aggregator, factories, metadata) { + @Override + public InternalAggregation buildEmptyAggregation() { + return aggregation; + } + }; + } + + @Override + protected Aggregator doCreateInternal( + SearchContext searchContext, + Aggregator aggregator, + CardinalityUpperBound cardinalityUpperBound, + Map map + ) throws IOException { + return queryShardContext.getValuesSourceRegistry() + .getAggregator(GeoHexGridAggregationBuilder.REGISTRY_KEY, config) + .build( + name, + factories, + config.getValuesSource(), + precision, + geoBoundingBox, + requiredSize, + shardSize, + searchContext, + aggregator, + cardinalityUpperBound, + metadata + ); + } + + static void registerAggregators(final ValuesSourceRegistry.Builder builder) { + builder.register( + GeoHexGridAggregationBuilder.REGISTRY_KEY, + CoreValuesSourceType.GEOPOINT, + ( + name, + factories, + valuesSource, + precision, + geoBoundingBox, + requiredSize, + shardSize, + aggregationContext, + parent, + cardinality, + metadata) -> { + CellIdSource cellIdSource = new CellIdSource( + (ValuesSource.GeoPoint) valuesSource, + precision, + geoBoundingBox, + GeoHexHelper::longEncode + ); + return new GeoHexGridAggregator( + name, + factories, + cellIdSource, + requiredSize, + shardSize, + aggregationContext, + parent, + cardinality, + metadata + ); + }, + true + ); + } +} diff --git a/src/main/java/org/opensearch/geospatial/search/aggregations/bucket/geogrid/GeoHexGridBucket.java b/src/main/java/org/opensearch/geospatial/search/aggregations/bucket/geogrid/GeoHexGridBucket.java new file mode 100644 index 0000000000..29c5b80ae8 --- /dev/null +++ b/src/main/java/org/opensearch/geospatial/search/aggregations/bucket/geogrid/GeoHexGridBucket.java @@ -0,0 +1,44 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.geospatial.search.aggregations.bucket.geogrid; + +import static org.opensearch.geospatial.search.aggregations.bucket.geogrid.GeoHexHelper.h3ToGeoPoint; + +import java.io.IOException; + +import org.opensearch.common.io.stream.StreamInput; +import org.opensearch.geo.search.aggregations.bucket.geogrid.BaseGeoGridBucket; +import org.opensearch.geospatial.h3.H3; +import org.opensearch.search.aggregations.InternalAggregations; + +/** + * Implementation of geohex grid bucket + */ +public class GeoHexGridBucket extends BaseGeoGridBucket { + + public GeoHexGridBucket(long hashAsLong, long docCount, InternalAggregations aggregations) { + super(hashAsLong, docCount, aggregations); + } + + /** + * Read from a Stream + * @param in {@link StreamInput} contains GridBucket + * @throws IOException + */ + public GeoHexGridBucket(StreamInput in) throws IOException { + super(in); + } + + @Override + public Object getKey() { + return h3ToGeoPoint(hashAsLong); + } + + @Override + public String getKeyAsString() { + return H3.h3ToString(hashAsLong); + } +} diff --git a/src/main/java/org/opensearch/geospatial/search/aggregations/bucket/geogrid/GeoHexHelper.java b/src/main/java/org/opensearch/geospatial/search/aggregations/bucket/geogrid/GeoHexHelper.java new file mode 100644 index 0000000000..ee074bf26e --- /dev/null +++ b/src/main/java/org/opensearch/geospatial/search/aggregations/bucket/geogrid/GeoHexHelper.java @@ -0,0 +1,78 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.geospatial.search.aggregations.bucket.geogrid; + +import static org.opensearch.geospatial.h3.H3.MAX_H3_RES; +import static org.opensearch.geospatial.h3.H3.MIN_H3_RES; +import static org.opensearch.geospatial.h3.H3.geoToH3; +import static org.opensearch.geospatial.h3.H3.h3IsValid; +import static org.opensearch.geospatial.h3.H3.h3ToLatLng; +import static org.opensearch.geospatial.h3.H3.stringToH3; + +import java.util.Locale; + +import lombok.NonNull; + +import org.opensearch.common.geo.GeoPoint; + +/** + * Helper class for H3 library + */ +public class GeoHexHelper { + + /** + * Checks whether given precision is within H3 Precision range + * @param precision H3 index precision + */ + public static void checkPrecisionRange(int precision) { + if ((precision < MIN_H3_RES) || (precision > MAX_H3_RES)) { + throw new IllegalArgumentException( + String.format( + Locale.getDefault(), + "Invalid precision of %d . Must be between %d and %d.", + precision, + MIN_H3_RES, + MAX_H3_RES + ) + ); + } + } + + /** + * Converts from long representation of an index to {@link GeoPoint} representation. + * @param h3CellID H3 Cell Id + * @throws IllegalArgumentException if invalid h3CellID is provided + */ + public static GeoPoint h3ToGeoPoint(long h3CellID) { + if (h3IsValid(h3CellID) == false) { + throw new IllegalArgumentException(String.format(Locale.getDefault(), "Invalid H3 Cell address: %d", h3CellID)); + } + final var position = h3ToLatLng(h3CellID); + return new GeoPoint(position.getLatDeg(), position.getLonDeg()); + } + + /** + * Converts from {@link String} representation of an index to {@link GeoPoint} representation. + * @param h3CellID H3 Cell Id + * @throws IllegalArgumentException if invalid h3CellID is provided + */ + public static GeoPoint h3ToGeoPoint(@NonNull String h3CellID) { + return h3ToGeoPoint(stringToH3(h3CellID)); + } + + /** + * Encodes longitude/latitude into H3 Cell Address for given precision + * + * @param latitude Latitude in degrees. + * @param longitude Longitude in degrees. + * @param precision Precision, 0 <= res <= 15 + * @return The H3 index. + * @throws IllegalArgumentException latitude, longitude, or precision are out of range. + */ + public static long longEncode(double longitude, double latitude, int precision) { + return geoToH3(latitude, longitude, precision); + } +} diff --git a/src/main/java/org/opensearch/geospatial/search/aggregations/bucket/geogrid/ParsedGeoHexGrid.java b/src/main/java/org/opensearch/geospatial/search/aggregations/bucket/geogrid/ParsedGeoHexGrid.java new file mode 100644 index 0000000000..7e0dcda2d0 --- /dev/null +++ b/src/main/java/org/opensearch/geospatial/search/aggregations/bucket/geogrid/ParsedGeoHexGrid.java @@ -0,0 +1,33 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.geospatial.search.aggregations.bucket.geogrid; + +import java.io.IOException; + +import lombok.NoArgsConstructor; + +import org.opensearch.common.xcontent.ObjectParser; +import org.opensearch.common.xcontent.XContentParser; +import org.opensearch.geo.search.aggregations.bucket.geogrid.ParsedGeoGrid; + +@NoArgsConstructor +public class ParsedGeoHexGrid extends ParsedGeoGrid { + private static final ObjectParser PARSER = createParser( + ParsedGeoHexGrid::new, + ParsedGeoHexGridBucket::fromXContent, + ParsedGeoHexGridBucket::fromXContent + ); + + public static ParsedGeoGrid fromXContent(XContentParser parser, String name) throws IOException { + final var parsedGeoGrid = PARSER.parse(parser, null); + parsedGeoGrid.setName(name); + return parsedGeoGrid; + } + + public String getType() { + return GeoHexGridAggregationBuilder.NAME; + } +} diff --git a/src/main/java/org/opensearch/geospatial/search/aggregations/bucket/geogrid/ParsedGeoHexGridBucket.java b/src/main/java/org/opensearch/geospatial/search/aggregations/bucket/geogrid/ParsedGeoHexGridBucket.java new file mode 100644 index 0000000000..9b409f6936 --- /dev/null +++ b/src/main/java/org/opensearch/geospatial/search/aggregations/bucket/geogrid/ParsedGeoHexGridBucket.java @@ -0,0 +1,30 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.geospatial.search.aggregations.bucket.geogrid; + +import java.io.IOException; + +import lombok.NoArgsConstructor; + +import org.opensearch.common.geo.GeoPoint; +import org.opensearch.common.xcontent.XContentParser; +import org.opensearch.geo.search.aggregations.bucket.geogrid.ParsedGeoGridBucket; + +@NoArgsConstructor +public class ParsedGeoHexGridBucket extends ParsedGeoGridBucket { + + public GeoPoint getKey() { + return GeoHexHelper.h3ToGeoPoint(this.hashAsString); + } + + public String getKeyAsString() { + return this.hashAsString; + } + + static ParsedGeoHexGridBucket fromXContent(XContentParser parser) throws IOException { + return parseXContent(parser, false, ParsedGeoHexGridBucket::new, (p, bucket) -> { bucket.hashAsString = p.textOrNull(); }); + } +} diff --git a/src/test/java/org/opensearch/geospatial/GeospatialRestTestCase.java b/src/test/java/org/opensearch/geospatial/GeospatialRestTestCase.java index 6b71b696c7..c27cef9d83 100644 --- a/src/test/java/org/opensearch/geospatial/GeospatialRestTestCase.java +++ b/src/test/java/org/opensearch/geospatial/GeospatialRestTestCase.java @@ -15,6 +15,8 @@ import static org.opensearch.geospatial.action.upload.geojson.UploadGeoJSONRequestContent.FIELD_DATA; import static org.opensearch.geospatial.shared.URLBuilder.getPluginURLPrefix; import static org.opensearch.index.query.AbstractGeometryQueryBuilder.DEFAULT_SHAPE_FIELD_NAME; +import static org.opensearch.rest.action.search.RestSearchAction.TYPED_KEYS_PARAM; +import static org.opensearch.search.aggregations.Aggregations.AGGREGATIONS_FIELD; import java.io.IOException; import java.util.Collections; @@ -69,6 +71,7 @@ public abstract class GeospatialRestTestCase extends OpenSearchSecureRestTestCas public static final String SHAPE_INDEX_FIELD = "index"; public static final String SHAPE_ID_FIELD = "id"; public static final String SHAPE_INDEX_PATH_FIELD = "path"; + public static final String QUERY_PARAM_TOKEN = "?"; private static String buildPipelinePath(String name) { return String.join(URL_DELIMITER, "_ingest", "pipeline", name); @@ -251,7 +254,16 @@ public String buildContentAsString(CheckedConsumer return Strings.toString(builder); } - public String buildSearchBodyAsString( + public String buildSearchAggregationsBodyAsString(CheckedConsumer aggregationsBuilder) + throws IOException { + return buildContentAsString(builder -> { + builder.startObject(AGGREGATIONS_FIELD); + aggregationsBuilder.accept(builder); + builder.endObject(); + }); + } + + public String buildSearchQueryBodyAsString( CheckedConsumer searchQueryBuilder, String queryType, String fieldName @@ -264,9 +276,12 @@ public String buildSearchBodyAsString( }); } - public SearchResponse searchIndex(String indexName, String entity) throws Exception { - String path = String.join(URL_DELIMITER, indexName, SEARCH); - final Request request = new Request("GET", path); + public SearchResponse searchIndex(String indexName, String entity, boolean includeType) throws Exception { + var urlPathBuilder = new StringBuilder().append(indexName).append(URL_DELIMITER).append(SEARCH); + if (includeType) { + urlPathBuilder.append(QUERY_PARAM_TOKEN).append(TYPED_KEYS_PARAM); + } + final Request request = new Request("GET", urlPathBuilder.toString()); request.setJsonEntity(entity); final Response response = client().performRequest(request); return SearchResponse.fromXContent(createParser(XContentType.JSON.xContent(), EntityUtils.toString(response.getEntity()))); @@ -299,13 +314,13 @@ public String indexDocumentUsingGeoJSON(String indexName, String fieldName, Geom public SearchResponse searchUsingShapeRelation(String indexName, String fieldName, Geometry geometry, ShapeRelation shapeRelation) throws Exception { - String searchEntity = buildSearchBodyAsString(builder -> { + String searchEntity = buildSearchQueryBodyAsString(builder -> { builder.field(DEFAULT_SHAPE_FIELD_NAME); GeoJson.toXContent(geometry, builder, EMPTY_PARAMS); builder.field(SHAPE_RELATION, shapeRelation.getRelationName()); }, XYShapeQueryBuilder.NAME, fieldName); - return searchIndex(indexName, searchEntity); + return searchIndex(indexName, searchEntity, false); } public void createIndexedShapeIndex() throws IOException { @@ -321,7 +336,7 @@ public SearchResponse searchUsingIndexedShapeIndex( String docId, String fieldName ) throws Exception { - String searchEntity = buildSearchBodyAsString(builder -> { + String searchEntity = buildSearchQueryBodyAsString(builder -> { builder.startObject(INDEXED_SHAPE_FIELD); builder.field(SHAPE_INDEX_FIELD, indexedShapeIndex); builder.field(SHAPE_ID_FIELD, docId); @@ -329,7 +344,7 @@ public SearchResponse searchUsingIndexedShapeIndex( builder.endObject(); }, XYShapeQueryBuilder.NAME, fieldName); - return searchIndex(indexName, searchEntity); + return searchIndex(indexName, searchEntity, false); } } diff --git a/src/test/java/org/opensearch/geospatial/GeospatialTestHelper.java b/src/test/java/org/opensearch/geospatial/GeospatialTestHelper.java index a52a4c8200..9486ab0ec4 100644 --- a/src/test/java/org/opensearch/geospatial/GeospatialTestHelper.java +++ b/src/test/java/org/opensearch/geospatial/GeospatialTestHelper.java @@ -42,6 +42,7 @@ import org.opensearch.common.collect.Tuple; import org.opensearch.geospatial.action.upload.geojson.ContentBuilder; import org.opensearch.geospatial.action.upload.geojson.UploadGeoJSONRequestContent; +import org.opensearch.geospatial.h3.H3; import org.opensearch.geospatial.stats.upload.UploadMetric; import org.opensearch.index.shard.ShardId; import org.opensearch.test.OpenSearchTestCase; @@ -166,4 +167,8 @@ public static double[] toDoubleArray(float[] input) { return IntStream.range(0, input.length).mapToDouble(i -> input[i]).toArray(); } + public static int randomHexGridPrecision() { + return randomIntBetween(H3.MIN_H3_RES, H3.MAX_H3_RES); + } + } diff --git a/src/test/java/org/opensearch/geospatial/index/query/xyshape/XYShapeQueryIT.java b/src/test/java/org/opensearch/geospatial/index/query/xyshape/XYShapeQueryIT.java index 8435d0ae6d..f1fc9189da 100644 --- a/src/test/java/org/opensearch/geospatial/index/query/xyshape/XYShapeQueryIT.java +++ b/src/test/java/org/opensearch/geospatial/index/query/xyshape/XYShapeQueryIT.java @@ -56,12 +56,12 @@ public void testIndexPointsFilterRectangleWithDefaultRelation() throws Exception indexDocumentUsingGeoJSON(indexName, xyShapeFieldName, new Point(-45, -50)); Rectangle rectangle = new Rectangle(-45, 45, 45, -45); - String searchEntity = buildSearchBodyAsString(builder -> { + String searchEntity = buildSearchQueryBodyAsString(builder -> { builder.field(DEFAULT_SHAPE_FIELD_NAME); GeoJson.toXContent(rectangle, builder, EMPTY_PARAMS); }, XYShapeQueryBuilder.NAME, xyShapeFieldName); - final SearchResponse searchResponse = searchIndex(indexName, searchEntity); + final SearchResponse searchResponse = searchIndex(indexName, searchEntity, false); assertSearchResponse(searchResponse); assertHitCount(searchResponse, 1); MatcherAssert.assertThat(searchResponse.getHits().getAt(0).getId(), equalTo(firstDocumentID)); diff --git a/src/test/java/org/opensearch/geospatial/search/aggregations/bucket/geogrid/GeoHexAggregationIT.java b/src/test/java/org/opensearch/geospatial/search/aggregations/bucket/geogrid/GeoHexAggregationIT.java new file mode 100644 index 0000000000..c364d220bd --- /dev/null +++ b/src/test/java/org/opensearch/geospatial/search/aggregations/bucket/geogrid/GeoHexAggregationIT.java @@ -0,0 +1,172 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.geospatial.search.aggregations.bucket.geogrid; + +import static java.util.function.Function.identity; +import static java.util.stream.Collectors.toMap; +import static org.hamcrest.Matchers.containsString; +import static org.opensearch.geo.GeometryTestUtils.randomPoint; +import static org.opensearch.geospatial.GeospatialTestHelper.randomHexGridPrecision; +import static org.opensearch.geospatial.GeospatialTestHelper.randomLowerCaseString; +import static org.opensearch.geospatial.h3.H3.geoToH3Address; +import static org.opensearch.test.hamcrest.OpenSearchAssertions.assertSearchResponse; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import org.hamcrest.MatcherAssert; +import org.opensearch.client.ResponseException; +import org.opensearch.cluster.ClusterModule; +import org.opensearch.common.ParseField; +import org.opensearch.common.settings.Settings; +import org.opensearch.common.xcontent.ContextParser; +import org.opensearch.common.xcontent.NamedXContentRegistry; +import org.opensearch.geometry.Point; +import org.opensearch.geospatial.GeospatialRestTestCase; +import org.opensearch.geospatial.h3.H3; +import org.opensearch.index.mapper.GeoPointFieldMapper; +import org.opensearch.search.aggregations.Aggregation; +import org.opensearch.search.aggregations.bucket.MultiBucketsAggregation; + +public class GeoHexAggregationIT extends GeospatialRestTestCase { + + private static final String FIELD = "field"; + private static final String FIELD_PRECISION = "precision"; + private static final String FIELD_SIZE = "size"; + private static int MAX_DOCUMENTS = 15; + private static int MIN_DOCUMENTS = 2; + private String indexName; + private String geospatialFieldName; + + @Override + public void setUp() throws Exception { + super.setUp(); + indexName = randomLowerCaseString(); + geospatialFieldName = randomLowerCaseString(); + } + + public void testGeoHexGridBucket() throws Exception { + // Step 1: Create an index + createIndex(indexName, Settings.EMPTY, Map.of(geospatialFieldName, GeoPointFieldMapper.CONTENT_TYPE)); + + // Generate metadata for Test data + final var randomDocumentsForTesting = randomIntBetween(MIN_DOCUMENTS, MAX_DOCUMENTS); + final var randomPrecision = randomHexGridPrecision(); + + // Generate Test data + final Map pointStringMap = generateRandomPointH3CellMap(randomDocumentsForTesting, randomPrecision); + for (var point : pointStringMap.keySet()) { + indexDocumentUsingWKT(indexName, geospatialFieldName, point.toString()); + } + + // do in-memory aggregation for comparison + final Map expectedAggregationMap = pointStringMap.values() + .stream() + .collect(Collectors.groupingBy(identity(), Collectors.counting())); + + // build test aggregation search query + var context = randomLowerCaseString(); + var content = buildSearchAggregationsBodyAsString(builder -> { + builder.startObject(context) + .startObject(GeoHexGridAggregationBuilder.NAME) + .field(FIELD, geospatialFieldName) + .field(FIELD_PRECISION, randomPrecision) + .field(FIELD_SIZE, expectedAggregationMap.size()) + .endObject() + .endObject(); + }); + + // execute Aggregation + final var searchResponse = searchIndex(indexName, content, true); + // Assert Search succeeded + assertSearchResponse(searchResponse); + // Fetch Aggregation + final var aggregation = searchResponse.getAggregations().asMap().get(context); + assertNotNull(aggregation); + + // Assert Results + assertTrue(aggregation instanceof MultiBucketsAggregation); + final var multiBucketsAggregation = (MultiBucketsAggregation) aggregation; + + // Assert size before checking contents + assertEquals(expectedAggregationMap.size(), multiBucketsAggregation.getBuckets().size()); + final Map actualAggregationMap = multiBucketsAggregation.getBuckets() + .stream() + .collect(toMap(MultiBucketsAggregation.Bucket::getKeyAsString, MultiBucketsAggregation.Bucket::getDocCount)); + + // compare in-memory aggregation with cluster aggregation + assertEquals(expectedAggregationMap, actualAggregationMap); + + } + + public void testSizeIsZero() throws Exception { + + // build test aggregation search query + var context = randomLowerCaseString(); + var content = buildSearchAggregationsBodyAsString(builder -> { + builder.startObject(context) + .startObject(GeoHexGridAggregationBuilder.NAME) + .field(FIELD, geospatialFieldName) + .field(FIELD_PRECISION, randomHexGridPrecision()) + .field(FIELD_SIZE, 0) + .endObject() + .endObject(); + }); + + // execute Aggregation + ResponseException exception = expectThrows(ResponseException.class, () -> searchIndex(indexName, content, true)); + MatcherAssert.assertThat(exception.getMessage(), containsString("[size] must be greater than 0.")); + } + + public void testInvalidPrecision() throws Exception { + + // build test aggregation search query + var invalidPrecision = H3.MAX_H3_RES + 1; + var content = buildSearchAggregationsBodyAsString(builder -> { + builder.startObject(randomLowerCaseString()) + .startObject(GeoHexGridAggregationBuilder.NAME) + .field(FIELD, geospatialFieldName) + .field(FIELD_PRECISION, invalidPrecision) + .endObject() + .endObject(); + }); + + // execute Aggregation + ResponseException exception = expectThrows(ResponseException.class, () -> searchIndex(indexName, content, true)); + MatcherAssert.assertThat( + exception.getMessage(), + containsString( + String.format( + Locale.getDefault(), + "Invalid precision of %d . Must be between %d and %d", + invalidPrecision, + H3.MIN_H3_RES, + H3.MAX_H3_RES + ) + ) + ); + } + + @Override + protected NamedXContentRegistry xContentRegistry() { + final List namedXContents = new ArrayList<>(ClusterModule.getNamedXWriteables()); + final ContextParser hexGridParser = (p, c) -> ParsedGeoHexGrid.fromXContent(p, (String) c); + namedXContents.add( + new NamedXContentRegistry.Entry(Aggregation.class, new ParseField(GeoHexGridAggregationBuilder.NAME), hexGridParser) + ); + return new NamedXContentRegistry(namedXContents); + } + + private Map generateRandomPointH3CellMap(int size, int randomPrecision) { + return IntStream.range(0, size) + .mapToObj(unUsed -> randomPoint()) + .collect(toMap(identity(), point -> geoToH3Address(point.getLat(), point.getLon(), randomPrecision))); + } +} diff --git a/src/test/java/org/opensearch/geospatial/search/aggregations/bucket/geogrid/GeoHexGridAggregatorTests.java b/src/test/java/org/opensearch/geospatial/search/aggregations/bucket/geogrid/GeoHexGridAggregatorTests.java new file mode 100644 index 0000000000..354fd94ae2 --- /dev/null +++ b/src/test/java/org/opensearch/geospatial/search/aggregations/bucket/geogrid/GeoHexGridAggregatorTests.java @@ -0,0 +1,344 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.geospatial.search.aggregations.bucket.geogrid; + +import static org.hamcrest.Matchers.equalTo; +import static org.opensearch.geospatial.GeospatialTestHelper.randomLowerCaseString; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.function.Consumer; +import java.util.function.Function; + +import org.apache.lucene.document.LatLonDocValuesField; +import org.apache.lucene.document.SortedSetDocValuesField; +import org.apache.lucene.geo.GeoEncodingUtils; +import org.apache.lucene.index.DirectoryReader; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.IndexableField; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.search.MatchAllDocsQuery; +import org.apache.lucene.search.Query; +import org.apache.lucene.store.Directory; +import org.apache.lucene.tests.index.RandomIndexWriter; +import org.apache.lucene.util.BytesRef; +import org.hamcrest.MatcherAssert; +import org.opensearch.common.CheckedConsumer; +import org.opensearch.common.geo.GeoBoundingBox; +import org.opensearch.common.geo.GeoPoint; +import org.opensearch.common.geo.GeoUtils; +import org.opensearch.geo.GeometryTestUtils; +import org.opensearch.geo.search.aggregations.bucket.geogrid.BaseGeoGridBucket; +import org.opensearch.geo.search.aggregations.bucket.geogrid.GeoGrid; +import org.opensearch.geo.search.aggregations.bucket.geogrid.GeoGridAggregationBuilder; +import org.opensearch.geometry.Rectangle; +import org.opensearch.geospatial.h3.H3; +import org.opensearch.geospatial.plugin.GeospatialPlugin; +import org.opensearch.index.mapper.GeoPointFieldMapper; +import org.opensearch.index.mapper.MappedFieldType; +import org.opensearch.plugins.SearchPlugin; +import org.opensearch.search.aggregations.Aggregation; +import org.opensearch.search.aggregations.Aggregator; +import org.opensearch.search.aggregations.AggregatorTestCase; +import org.opensearch.search.aggregations.MultiBucketConsumerService; +import org.opensearch.search.aggregations.bucket.terms.StringTerms; +import org.opensearch.search.aggregations.bucket.terms.TermsAggregationBuilder; + +/** + * This class is modified from https://github.com/opensearch-project/OpenSearch/blob/main/modules/geo/src/test/java/org/opensearch/geo/search/aggregations/bucket/geogrid/GeoGridAggregatorTestCase.java + * to keep relevant test case required for GeoHex Grid Aggregation. + */ +public class GeoHexGridAggregatorTests extends AggregatorTestCase { + + private static final String GEO_POINT_FIELD_NAME = "location"; + private static final double TOLERANCE = 1E-5D; + + public void testNoDocs() throws IOException { + testCase( + new MatchAllDocsQuery(), + GEO_POINT_FIELD_NAME, + randomPrecision(), + null, + geoGrid -> { assertEquals(0, geoGrid.getBuckets().size()); }, + iw -> { + // Intentionally not writing any docs + } + ); + } + + public void testUnmapped() throws IOException { + testCase( + new MatchAllDocsQuery(), + randomLowerCaseString(), + randomPrecision(), + null, + geoGrid -> { assertEquals(0, geoGrid.getBuckets().size()); }, + iw -> { iw.addDocument(Collections.singleton(new LatLonDocValuesField(GEO_POINT_FIELD_NAME, 10D, 10D))); } + ); + } + + public void testUnmappedMissing() throws IOException { + GeoGridAggregationBuilder builder = createBuilder(randomLowerCaseString()).field(randomLowerCaseString()) + .missing("53.69437,6.475031"); + testCase( + new MatchAllDocsQuery(), + randomPrecision(), + null, + geoGrid -> assertEquals(1, geoGrid.getBuckets().size()), + iw -> iw.addDocument(Collections.singleton(new LatLonDocValuesField(GEO_POINT_FIELD_NAME, 10D, 10D))), + builder + ); + + } + + public void testWithSeveralDocs() throws IOException { + int precision = randomPrecision(); + int numPoints = randomIntBetween(8, 128); + Map expectedCountPerGeoHex = new HashMap<>(); + testCase(new MatchAllDocsQuery(), GEO_POINT_FIELD_NAME, precision, null, geoHexGrid -> { + assertEquals(expectedCountPerGeoHex.size(), geoHexGrid.getBuckets().size()); + for (GeoGrid.Bucket bucket : geoHexGrid.getBuckets()) { + assertEquals((long) expectedCountPerGeoHex.get(bucket.getKeyAsString()), bucket.getDocCount()); + } + assertTrue(hasValue(geoHexGrid)); + }, iw -> { + List points = new ArrayList<>(); + Set distinctAddressPerDoc = new HashSet<>(); + for (int pointId = 0; pointId < numPoints; pointId++) { + double[] latLng = randomLatLng(); + points.add(new LatLonDocValuesField(GEO_POINT_FIELD_NAME, latLng[0], latLng[1])); + String address = h3AddressAsString(latLng[1], latLng[0], precision); + if (distinctAddressPerDoc.contains(address) == false) { + expectedCountPerGeoHex.put(address, expectedCountPerGeoHex.getOrDefault(address, 0) + 1); + } + distinctAddressPerDoc.add(address); + if (usually()) { + iw.addDocument(points); + points.clear(); + distinctAddressPerDoc.clear(); + } + } + if (points.size() != 0) { + iw.addDocument(points); + } + }); + } + + public void testAsSubAgg() throws IOException { + int precision = randomPrecision(); + Map> expectedCountPerTPerGeoHex = new TreeMap<>(); + List> docs = new ArrayList<>(); + for (int i = 0; i < 30; i++) { + String t = randomAlphaOfLength(1); + double[] latLng = randomLatLng(); + + List doc = new ArrayList<>(); + docs.add(doc); + doc.add(new LatLonDocValuesField(GEO_POINT_FIELD_NAME, latLng[0], latLng[1])); + doc.add(new SortedSetDocValuesField("t", new BytesRef(t))); + + String address = h3AddressAsString(latLng[1], latLng[0], precision); + Map expectedCountPerGeoHex = expectedCountPerTPerGeoHex.get(t); + if (expectedCountPerGeoHex == null) { + expectedCountPerGeoHex = new TreeMap<>(); + expectedCountPerTPerGeoHex.put(t, expectedCountPerGeoHex); + } + expectedCountPerGeoHex.put(address, expectedCountPerGeoHex.getOrDefault(address, 0L) + 1); + } + CheckedConsumer buildIndex = iw -> iw.addDocuments(docs); + String aggregation = randomLowerCaseString(); + TermsAggregationBuilder aggregationBuilder = new TermsAggregationBuilder("t").field("t") + .size(expectedCountPerTPerGeoHex.size()) + .subAggregation(createBuilder(aggregation).field(GEO_POINT_FIELD_NAME).precision(precision)); + Consumer verify = (terms) -> { + Map> actual = new TreeMap<>(); + for (StringTerms.Bucket tb : terms.getBuckets()) { + GeoHexGrid gg = tb.getAggregations().get(aggregation); + Map sub = new TreeMap<>(); + for (BaseGeoGridBucket ggb : gg.getBuckets()) { + sub.put(ggb.getKeyAsString(), ggb.getDocCount()); + } + actual.put(tb.getKeyAsString(), sub); + } + MatcherAssert.assertThat(actual, equalTo(expectedCountPerTPerGeoHex)); + }; + testCase(aggregationBuilder, new MatchAllDocsQuery(), buildIndex, verify, keywordField("t"), geoPointField(GEO_POINT_FIELD_NAME)); + } + + public void testBounds() throws IOException { + final int numDocs = randomIntBetween(64, 256); + final GeoHexGridAggregationBuilder builder = createBuilder("_name"); + + expectThrows(IllegalArgumentException.class, () -> builder.precision(-1)); + expectThrows(IllegalArgumentException.class, () -> builder.precision(30)); + + // only consider bounding boxes that are at least TOLERANCE wide and have quantized coordinates + GeoBoundingBox bbox = randomValueOtherThanMany( + (b) -> Math.abs(GeoUtils.normalizeLon(b.right()) - GeoUtils.normalizeLon(b.left())) < TOLERANCE, + GeoHexGridAggregatorTests::randomBBox + ); + Function encodeDecodeLat = (lat) -> GeoEncodingUtils.decodeLatitude(GeoEncodingUtils.encodeLatitude(lat)); + Function encodeDecodeLon = (lon) -> GeoEncodingUtils.decodeLongitude(GeoEncodingUtils.encodeLongitude(lon)); + bbox.topLeft().reset(encodeDecodeLat.apply(bbox.top()), encodeDecodeLon.apply(bbox.left())); + bbox.bottomRight().reset(encodeDecodeLat.apply(bbox.bottom()), encodeDecodeLon.apply(bbox.right())); + + int in = 0, out = 0; + List docs = new ArrayList<>(); + while (in + out < numDocs) { + if (bbox.left() > bbox.right()) { + if (randomBoolean()) { + double lonWithin = randomBoolean() + ? randomDoubleBetween(bbox.left(), 180.0, true) + : randomDoubleBetween(-180.0, bbox.right(), true); + double latWithin = randomDoubleBetween(bbox.bottom(), bbox.top(), true); + in++; + docs.add(new LatLonDocValuesField(GEO_POINT_FIELD_NAME, latWithin, lonWithin)); + } else { + double lonOutside = randomDoubleBetween(bbox.left(), bbox.right(), true); + double latOutside = randomDoubleBetween(bbox.top(), -90, false); + out++; + docs.add(new LatLonDocValuesField(GEO_POINT_FIELD_NAME, latOutside, lonOutside)); + } + } else { + if (randomBoolean()) { + double lonWithin = randomDoubleBetween(bbox.left(), bbox.right(), true); + double latWithin = randomDoubleBetween(bbox.bottom(), bbox.top(), true); + in++; + docs.add(new LatLonDocValuesField(GEO_POINT_FIELD_NAME, latWithin, lonWithin)); + } else { + double lonOutside = GeoUtils.normalizeLon(randomDoubleBetween(bbox.right(), 180.001, false)); + double latOutside = GeoUtils.normalizeLat(randomDoubleBetween(bbox.top(), 90.001, false)); + out++; + docs.add(new LatLonDocValuesField(GEO_POINT_FIELD_NAME, latOutside, lonOutside)); + } + } + + } + + final long numDocsInBucket = in; + final int precision = randomPrecision(); + + testCase(new MatchAllDocsQuery(), GEO_POINT_FIELD_NAME, precision, bbox, geoGrid -> { + assertTrue(hasValue(geoGrid)); + long docCount = 0; + for (int i = 0; i < geoGrid.getBuckets().size(); i++) { + docCount += geoGrid.getBuckets().get(i).getDocCount(); + } + MatcherAssert.assertThat(docCount, equalTo(numDocsInBucket)); + }, iw -> { + for (LatLonDocValuesField docField : docs) { + iw.addDocument(Collections.singletonList(docField)); + } + }); + } + + @Override + public void doAssertReducedMultiBucketConsumer(Aggregation agg, MultiBucketConsumerService.MultiBucketConsumer bucketConsumer) { + /* + * No-op. + */ + } + + /** + * Overriding the Search Plugins list with {@link GeospatialPlugin} so that the testcase will know that this plugin is + * to be loaded during the tests. + * @return List of {@link SearchPlugin} + */ + @Override + protected List getSearchPlugins() { + return Collections.singletonList(new GeospatialPlugin()); + } + + private double[] randomLatLng() { + double lat = (180d * randomDouble()) - 90d; + double lng = (360d * randomDouble()) - 180d; + + // Precision-adjust longitude/latitude to avoid wrong bucket placement + // Internally, lat/lng get converted to 32 bit integers, loosing some precision. + // This does not affect geo hex because it also uses the same algorithm, + // but it does affect other bucketing algos, thus we need to do the same steps here. + lng = GeoEncodingUtils.decodeLongitude(GeoEncodingUtils.encodeLongitude(lng)); + lat = GeoEncodingUtils.decodeLatitude(GeoEncodingUtils.encodeLatitude(lat)); + + return new double[] { lat, lng }; + } + + private void testCase( + Query query, + String field, + int precision, + GeoBoundingBox geoBoundingBox, + Consumer verify, + CheckedConsumer buildIndex + ) throws IOException { + testCase(query, precision, geoBoundingBox, verify, buildIndex, createBuilder("_name").field(field)); + } + + private void testCase( + Query query, + int precision, + GeoBoundingBox geoBoundingBox, + Consumer verify, + CheckedConsumer buildIndex, + GeoGridAggregationBuilder aggregationBuilder + ) throws IOException { + Directory directory = newDirectory(); + RandomIndexWriter indexWriter = new RandomIndexWriter(random(), directory); + buildIndex.accept(indexWriter); + indexWriter.close(); + + IndexReader indexReader = DirectoryReader.open(directory); + IndexSearcher indexSearcher = newSearcher(indexReader, true, true); + + aggregationBuilder.precision(precision); + if (geoBoundingBox != null) { + aggregationBuilder.setGeoBoundingBox(geoBoundingBox); + MatcherAssert.assertThat(aggregationBuilder.geoBoundingBox(), equalTo(geoBoundingBox)); + } + + MappedFieldType fieldType = new GeoPointFieldMapper.GeoPointFieldType(GEO_POINT_FIELD_NAME); + + Aggregator aggregator = createAggregator(aggregationBuilder, indexSearcher, fieldType); + aggregator.preCollection(); + indexSearcher.search(query, aggregator); + aggregator.postCollection(); + verify.accept((GeoHexGrid) aggregator.buildTopLevel()); + + indexReader.close(); + directory.close(); + } + + private int randomPrecision() { + return randomIntBetween(H3.MIN_H3_RES, H3.MAX_H3_RES); + } + + private static boolean hasValue(GeoHexGrid agg) { + return agg.getBuckets().stream().anyMatch(bucket -> bucket.getDocCount() > 0); + } + + private static GeoBoundingBox randomBBox() { + Rectangle rectangle = GeometryTestUtils.randomRectangle(); + return new GeoBoundingBox( + new GeoPoint(rectangle.getMaxLat(), rectangle.getMinLon()), + new GeoPoint(rectangle.getMinLat(), rectangle.getMaxLon()) + ); + } + + private String h3AddressAsString(double lng, double lat, int precision) { + return H3.geoToH3Address(lat, lng, precision); + } + + private GeoHexGridAggregationBuilder createBuilder(String name) { + return new GeoHexGridAggregationBuilder(name); + } +} diff --git a/src/test/java/org/opensearch/geospatial/search/aggregations/bucket/geogrid/GeoHexGridParserTests.java b/src/test/java/org/opensearch/geospatial/search/aggregations/bucket/geogrid/GeoHexGridParserTests.java new file mode 100644 index 0000000000..7b62a29b47 --- /dev/null +++ b/src/test/java/org/opensearch/geospatial/search/aggregations/bucket/geogrid/GeoHexGridParserTests.java @@ -0,0 +1,143 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.geospatial.search.aggregations.bucket.geogrid; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.instanceOf; +import static org.opensearch.geospatial.GeospatialTestHelper.randomHexGridPrecision; +import static org.opensearch.geospatial.GeospatialTestHelper.randomLowerCaseString; +import static org.opensearch.geospatial.search.aggregations.bucket.geogrid.GeoHexGridAggregationBuilder.NAME; +import static org.opensearch.geospatial.search.aggregations.bucket.geogrid.GeoHexGridAggregationBuilder.PARSER; + +import java.util.Locale; + +import org.hamcrest.MatcherAssert; +import org.opensearch.common.xcontent.XContentParseException; +import org.opensearch.common.xcontent.XContentParser; +import org.opensearch.common.xcontent.json.JsonXContent; +import org.opensearch.geo.GeometryTestUtils; +import org.opensearch.geometry.Rectangle; +import org.opensearch.geospatial.h3.H3; +import org.opensearch.test.OpenSearchTestCase; + +public class GeoHexGridParserTests extends OpenSearchTestCase { + + private final static int MAX_SIZE = 100; + private final static int MIN_SIZE = 1; + private final static int MAX_SHARD_SIZE = 100; + private final static int MIN_SHARD_SIZE = 1; + + public void testParseValidFromInts() throws Exception { + XContentParser stParser = createParser( + JsonXContent.jsonXContent, + buildAggregation( + randomLowerCaseString(), + randomHexGridPrecision(), + randomIntBetween(MIN_SIZE, MAX_SIZE), + randomIntBetween(MIN_SHARD_SIZE, MAX_SHARD_SIZE) + ) + ); + XContentParser.Token token = stParser.nextToken(); + assertSame(XContentParser.Token.START_OBJECT, token); + // can create a factory + assertNotNull(PARSER.parse(stParser, NAME)); + } + + public void testParseValidFromStrings() throws Exception { + XContentParser stParser = createParser( + JsonXContent.jsonXContent, + buildAggregation( + randomLowerCaseString(), + randomHexGridPrecision(), + randomIntBetween(MIN_SIZE, MAX_SIZE), + randomIntBetween(MIN_SHARD_SIZE, MAX_SHARD_SIZE) + ) + ); + XContentParser.Token token = stParser.nextToken(); + assertSame(XContentParser.Token.START_OBJECT, token); + // can create a factory + assertNotNull(PARSER.parse(stParser, NAME)); + } + + public void testParseInvalidUnitPrecision() throws Exception { + XContentParser stParser = createParser(JsonXContent.jsonXContent, "{\"field\":\"my_loc\", \"precision\": \"10kg\"}"); + XContentParser.Token token = stParser.nextToken(); + assertSame(XContentParser.Token.START_OBJECT, token); + XContentParseException ex = expectThrows(XContentParseException.class, () -> PARSER.parse(stParser, NAME)); + MatcherAssert.assertThat(ex.getMessage(), containsString("failed to parse field [precision]")); + MatcherAssert.assertThat(ex.getCause(), instanceOf(NumberFormatException.class)); + assertEquals("For input string: \"10kg\"", ex.getCause().getMessage()); + } + + public void testParseErrorOnBooleanPrecision() throws Exception { + XContentParser stParser = createParser(JsonXContent.jsonXContent, "{\"field\":\"my_loc\", \"precision\":false}"); + XContentParser.Token token = stParser.nextToken(); + assertSame(XContentParser.Token.START_OBJECT, token); + XContentParseException e = expectThrows(XContentParseException.class, () -> PARSER.parse(stParser, NAME)); + MatcherAssert.assertThat(e.getMessage(), containsString("precision doesn't support values of type: VALUE_BOOLEAN")); + } + + public void testParseErrorOnPrecisionOutOfRange() throws Exception { + int invalidPrecision = H3.MAX_H3_RES + 1; + XContentParser stParser = createParser( + JsonXContent.jsonXContent, + buildAggregation( + randomLowerCaseString(), + invalidPrecision, + randomIntBetween(MIN_SIZE, MAX_SIZE), + randomIntBetween(MIN_SHARD_SIZE, MAX_SHARD_SIZE) + ) + ); + XContentParser.Token token = stParser.nextToken(); + assertSame(XContentParser.Token.START_OBJECT, token); + try { + PARSER.parse(stParser, NAME); + fail(); + } catch (XContentParseException ex) { + MatcherAssert.assertThat(ex.getCause(), instanceOf(IllegalArgumentException.class)); + assertEquals( + String.format( + Locale.getDefault(), + "Invalid precision of %d . Must be between %d and %d.", + invalidPrecision, + H3.MIN_H3_RES, + H3.MAX_H3_RES + ), + ex.getCause().getMessage() + ); + } + } + + public void testParseValidBounds() throws Exception { + Rectangle bbox = GeometryTestUtils.randomRectangle(); + XContentParser stParser = createParser( + JsonXContent.jsonXContent, + String.format( + Locale.getDefault(), + "{\"field\":\"my_loc\", \"precision\": 5, \"size\": 500, \"shard_size\": 550,\"bounds\": { \"top\": %s,\"bottom\": %s,\"left\": %s,\"right\": %s}}", + bbox.getMaxY(), + bbox.getMinY(), + bbox.getMinX(), + bbox.getMaxX() + ) + ); + XContentParser.Token token = stParser.nextToken(); + assertSame(XContentParser.Token.START_OBJECT, token); + // can create a factory + assertNotNull(PARSER.parse(stParser, NAME)); + } + + private String buildAggregation(String fieldName, int precision, int size, int shardSize) { + return String.format( + Locale.getDefault(), + "{\"field\":\"%s\", \"precision\":%d, \"size\": %d, \"shard_size\": %d}", + fieldName, + precision, + size, + shardSize + ); + } +} diff --git a/src/test/java/org/opensearch/geospatial/search/aggregations/bucket/geogrid/GeoHexGridTests.java b/src/test/java/org/opensearch/geospatial/search/aggregations/bucket/geogrid/GeoHexGridTests.java new file mode 100644 index 0000000000..32a248abcc --- /dev/null +++ b/src/test/java/org/opensearch/geospatial/search/aggregations/bucket/geogrid/GeoHexGridTests.java @@ -0,0 +1,186 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.geospatial.search.aggregations.bucket.geogrid; + +import static org.hamcrest.Matchers.equalTo; +import static org.opensearch.geospatial.GeospatialTestHelper.randomHexGridPrecision; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.lucene.index.IndexWriter; +import org.hamcrest.MatcherAssert; +import org.opensearch.common.ParseField; +import org.opensearch.common.xcontent.ContextParser; +import org.opensearch.common.xcontent.NamedXContentRegistry; +import org.opensearch.geo.search.aggregations.bucket.geogrid.BaseGeoGrid; +import org.opensearch.geo.search.aggregations.bucket.geogrid.BaseGeoGridBucket; +import org.opensearch.geo.search.aggregations.bucket.geogrid.GeoGrid; +import org.opensearch.geospatial.h3.H3; +import org.opensearch.geospatial.plugin.GeospatialPlugin; +import org.opensearch.plugins.SearchPlugin; +import org.opensearch.search.aggregations.Aggregation; +import org.opensearch.search.aggregations.InternalAggregations; +import org.opensearch.search.aggregations.ParsedMultiBucketAggregation; +import org.opensearch.test.InternalMultiBucketAggregationTestCase; + +/** + * This class is modified from https://github.com/opensearch-project/OpenSearch/blob/main/modules/geo/src/test/java/org/opensearch/geo/search/aggregations/bucket/geogrid/GeoGridTestCase.java + * to keep relevant test case required for GeoHex Grid. + */ +public class GeoHexGridTests extends InternalMultiBucketAggregationTestCase { + + private static final double LATITUDE_MIN = -90.0; + private static final double LATITUDE_MAX = 90.0; + private static final double LONGITUDE_MIN = -180.0; + private static final double LONGITUDE_MAX = 180.0; + private static final int MIN_BUCKET_SIZE = 1; + private static final int MAX_BUCKET_SIZE = 3; + + public void testCreateFromBuckets() { + BaseGeoGrid original = createTestInstance(); + MatcherAssert.assertThat(original, equalTo(original.create(original.getBuckets()))); + } + + @Override + protected int minNumberOfBuckets() { + return MIN_BUCKET_SIZE; + } + + @Override + protected int maxNumberOfBuckets() { + return MAX_BUCKET_SIZE; + } + + /** + * Overriding the method so that tests can get the aggregation specs for namedWriteable. + * + * @return GeoSpatialPlugin + */ + @Override + protected SearchPlugin registerPlugin() { + return new GeospatialPlugin(); + } + + /** + * Overriding with the {@link ParsedGeoHexGrid} so that it can be parsed. We need to do this as {@link GeospatialPlugin} + * is registering this Aggregation. + * + * @return a List of {@link NamedXContentRegistry.Entry} + */ + @Override + protected List getNamedXContents() { + final List namedXContents = new ArrayList<>(getDefaultNamedXContents()); + final ContextParser hexGridParser = (p, c) -> ParsedGeoHexGrid.fromXContent(p, (String) c); + namedXContents.add( + new NamedXContentRegistry.Entry(Aggregation.class, new ParseField(GeoHexGridAggregationBuilder.NAME), hexGridParser) + ); + return namedXContents; + } + + @Override + protected GeoHexGrid createTestInstance(String name, Map metadata, InternalAggregations aggregations) { + final int precision = randomHexGridPrecision(); + int size = randomNumberOfBuckets(); + List buckets = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + double latitude = randomDoubleBetween(LATITUDE_MIN, LATITUDE_MAX, false); + double longitude = randomDoubleBetween(LONGITUDE_MIN, LONGITUDE_MAX, false); + + long addressAsLong = longEncode(longitude, latitude, precision); + buckets.add(createInternalGeoGridBucket(addressAsLong, randomInt(IndexWriter.MAX_DOCS), aggregations)); + } + return createInternalGeoGrid(name, size, buckets, metadata); + } + + @Override + protected void assertReduced(GeoHexGrid reduced, List inputs) { + Map> map = new HashMap<>(); + for (GeoHexGrid input : inputs) { + for (GeoGrid.Bucket bucketBase : input.getBuckets()) { + GeoHexGridBucket bucket = (GeoHexGridBucket) bucketBase; + List buckets = map.computeIfAbsent(bucket.hashAsLong(), k -> new ArrayList<>()); + buckets.add(bucket); + } + } + List expectedBuckets = new ArrayList<>(); + for (Map.Entry> entry : map.entrySet()) { + long docCount = 0; + for (GeoHexGridBucket bucket : entry.getValue()) { + docCount += bucket.getDocCount(); + } + expectedBuckets.add(createInternalGeoGridBucket(entry.getKey(), docCount, InternalAggregations.EMPTY)); + } + expectedBuckets.sort((first, second) -> { + int cmp = Long.compare(second.getDocCount(), first.getDocCount()); + if (cmp == 0) { + return second.compareTo(first); + } + return cmp; + }); + int requestedSize = inputs.get(0).getRequiredSize(); + expectedBuckets = expectedBuckets.subList(0, Math.min(requestedSize, expectedBuckets.size())); + assertEquals(expectedBuckets.size(), reduced.getBuckets().size()); + for (int i = 0; i < reduced.getBuckets().size(); i++) { + GeoGrid.Bucket expected = expectedBuckets.get(i); + GeoGrid.Bucket actual = reduced.getBuckets().get(i); + assertEquals(expected.getDocCount(), actual.getDocCount()); + assertEquals(expected.getKey(), actual.getKey()); + } + } + + @Override + protected Class implementationClass() { + return ParsedGeoHexGrid.class; + } + + @Override + protected GeoHexGrid mutateInstance(GeoHexGrid instance) { + String name = instance.getName(); + int size = instance.getRequiredSize(); + List buckets = instance.getBuckets(); + Map metadata = instance.getMetadata(); + switch (between(0, 3)) { + case 0: + name += randomAlphaOfLength(5); + break; + case 1: + buckets = new ArrayList<>(buckets); + buckets.add( + createInternalGeoGridBucket(randomNonNegativeLong(), randomInt(IndexWriter.MAX_DOCS), InternalAggregations.EMPTY) + ); + break; + case 2: + size = size + between(1, 10); + break; + case 3: + if (metadata == null) { + metadata = new HashMap<>(1); + } else { + metadata = new HashMap<>(instance.getMetadata()); + } + metadata.put(randomAlphaOfLength(15), randomInt()); + break; + default: + throw new AssertionError("Illegal randomisation branch"); + } + return createInternalGeoGrid(name, size, buckets, metadata); + } + + private GeoHexGrid createInternalGeoGrid(String name, int size, List buckets, Map metadata) { + return new GeoHexGrid(name, size, buckets, metadata); + } + + private GeoHexGridBucket createInternalGeoGridBucket(Long key, long docCount, InternalAggregations aggregations) { + return new GeoHexGridBucket(key, docCount, aggregations); + } + + private long longEncode(double lng, double lat, int precision) { + return H3.geoToH3(lng, lat, precision); + } +} diff --git a/src/yamlRestTest/resources/rest-api-spec/test/20_geohex_grid.yml b/src/yamlRestTest/resources/rest-api-spec/test/20_geohex_grid.yml new file mode 100644 index 0000000000..da68982620 --- /dev/null +++ b/src/yamlRestTest/resources/rest-api-spec/test/20_geohex_grid.yml @@ -0,0 +1,61 @@ +setup: + - do: + indices.create: + index: cities + body: + settings: + number_of_replicas: 0 + mappings: + properties: + location: + type: geo_point + +--- +"Basic test": + - do: + bulk: + refresh: true + body: + - index: + _index: cities + _id: 1 + - location: "52.374081,4.912350" + - index: + _index: cities + _id: 2 + - location: "52.369219,4.901618" + - index: + _index: cities + _id: 3 + - location: "52.371667,4.914722" + - index: + _index: cities + _id: 4 + - location: "51.222900,4.405200" + - index: + _index: cities + _id: 5 + - location: "48.861111,2.336389" + - index: + _index: cities + _id: 6 + - location: "48.860000,2.327000" + + - do: + search: + rest_total_hits_as_int: true + body: + aggregations: + grid: + geohex_grid: + field: location + precision: 4 + + + - match: { hits.total: 6 } + - match: { aggregations.grid.buckets.0.key: 841969dffffffff } + - match: { aggregations.grid.buckets.0.doc_count: 3 } + - match: { aggregations.grid.buckets.1.key: 841fb47ffffffff } + - match: { aggregations.grid.buckets.1.doc_count: 2 } + - match: { aggregations.grid.buckets.2.key: 841fa4dffffffff } + - match: { aggregations.grid.buckets.2.doc_count: 1 }