From 6f879716a8b1189b6650ff6c4ea5c99283cc145c Mon Sep 17 00:00:00 2001 From: Linus Wallin Date: Thu, 22 Feb 2024 11:38:50 +0100 Subject: [PATCH 01/13] docs: Added md file that describes the tags for our commits COMMIT.md contains the structure for how we should add tags to our commits to make them easier to understand. --- COMMIT.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 COMMIT.md diff --git a/COMMIT.md b/COMMIT.md new file mode 100644 index 00000000..af6db84f --- /dev/null +++ b/COMMIT.md @@ -0,0 +1,14 @@ +Document for Commit Structure +=========================================================== + +* `feat` - a new feature is introduced with the changes +* `fix`- a bug fix has occurred +* `chore` - changes that do not relate to a fix or feature and don't modify src or test files (for example updating dependencies) +* `refactor` - refactored code that neither fixes a bug nor adds a feature +* `docs` - updates to documentation such as a the README or other markdown files +* `style` - changes that do not affect the meaning of the code, likely related to code formatting such as white-space, missing semi-colons, and so on. +* `test` - including new or correcting previous tests +* `perf` - performance improvements +* `ci` - continuous integration related +* `build` - changes that affect the build system or external dependencies +* `revert` - reverts a previous commit \ No newline at end of file From b836d1e76af97e146b2d6745445ec56e6b1b216e Mon Sep 17 00:00:00 2001 From: karlsb <36365664+karlsb@users.noreply.github.com> Date: Thu, 22 Feb 2024 11:52:39 +0100 Subject: [PATCH 02/13] Revert "docs: Added md file that describes the tags for our commits" --- COMMIT.md | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 COMMIT.md diff --git a/COMMIT.md b/COMMIT.md deleted file mode 100644 index af6db84f..00000000 --- a/COMMIT.md +++ /dev/null @@ -1,14 +0,0 @@ -Document for Commit Structure -=========================================================== - -* `feat` - a new feature is introduced with the changes -* `fix`- a bug fix has occurred -* `chore` - changes that do not relate to a fix or feature and don't modify src or test files (for example updating dependencies) -* `refactor` - refactored code that neither fixes a bug nor adds a feature -* `docs` - updates to documentation such as a the README or other markdown files -* `style` - changes that do not affect the meaning of the code, likely related to code formatting such as white-space, missing semi-colons, and so on. -* `test` - including new or correcting previous tests -* `perf` - performance improvements -* `ci` - continuous integration related -* `build` - changes that affect the build system or external dependencies -* `revert` - reverts a previous commit \ No newline at end of file From a56f6ae18ceae1f9fdf9ee24931677953dfee590 Mon Sep 17 00:00:00 2001 From: wenjj2000 Date: Mon, 4 Mar 2024 10:20:21 +0100 Subject: [PATCH 03/13] Squashed commit of the following: commit b29091aa3386ba717d94dd23a4ffb20049a35930 Merge: 9b64610 9aaaf4c Author: Wen Jun Jie <132931864+WenJJ2000@users.noreply.github.com> Date: Mon Mar 4 10:09:16 2024 +0100 Merge pull request #55 from DD2480-Group-3/54-coveringGeohash 54 covering geohash commit 9aaaf4cd1222a16a5f89ba64a3ea1a6087dc81dd Author: wenjj2000 Date: Mon Mar 4 10:00:46 2024 +0100 Squashed commit of the following: commit 279212f7e85e923793eaace978cbbcb666508096 Author: Linus Wallin Date: Mon Mar 4 09:46:08 2024 +0100 fix: solves bug where precision is allowed to be too high Limits the precision in containingGeohash function to 6 from 24 since the toGeohash function has been updated to only work for highest precision 6. commit 2aa650f094120e01c3aa9b2329698b3b88b8dfe0 Merge: 263f8e2 dcdb58d Author: Linus Wallin Date: Mon Mar 4 09:45:04 2024 +0100 Merge branch '213-geometry-to-geohash' of github.com:DD2480-Group-3/geometry-api-java into 54-coveringGeohash commit 263f8e2a10d3ee5cb4815548d30abe6b19be26d1 Author: Linus Wallin Date: Sun Mar 3 19:47:34 2024 +0100 refactor: removed missed empty line commit 72b84f42b3e101f9aadc0b336f602cbe30bdfe9a Author: Linus Wallin Date: Sun Mar 3 19:47:05 2024 +0100 refactor: removed empty line commit 6fef31241f5724a43805781e0062256056ad45b5 Author: Linus Wallin Date: Sun Mar 3 19:45:34 2024 +0100 refactor: removed empty lines commit 1287b2c1ea051537ffc11f29f200ea88dfa47e99 Author: Linus Wallin Date: Sun Mar 3 19:39:49 2024 +0100 test: changed envelope to encompass 4 different parts of the geo grid The envelope was previously assigned wrong max x and y values, which resulted in errors as the coveringGeohash function didn't return the expected amount of geo hashes. commit 69ca9c0ffbfba83d59d6f4528bb3230c596aac1c Author: Linus Wallin Date: Sun Mar 3 19:30:38 2024 +0100 fix: solves bug which resulted in wrong geohashes commit 2a8d13169998e8dda67b8c737623c09ccd01438b Author: Linus Wallin Date: Sun Mar 3 19:24:32 2024 +0100 test: updated tests to match the changes of the function in last commit commit bb45871770a7d31861a166d0d6a3ae52892b5866 Author: Linus Wallin Date: Sun Mar 3 19:24:02 2024 +0100 refactor: made the return string array dynamic commit 708542e0eb04197d2049e6e46dae3722f0f4c048 Author: Linus Wallin Date: Sun Mar 3 18:42:15 2024 +0100 refactor: removes indentation error caused by merge commit a216691f06191097ed230c771c15e739d8ee32d5 Merge: 4420765 62f7d5f Author: Linus Wallin Date: Sun Mar 3 18:41:36 2024 +0100 Merge branch '213-geometry-to-geohash' of github.com:DD2480-Group-3/geometry-api-java into 54-coveringGeohash commit 4420765466991ff31b9ffd31fa321740db3b8502 Author: Linus Wallin Date: Sun Mar 3 18:37:30 2024 +0100 test: added test cases for coveringGeohash function commit 2c319798289907fa33f165df62cf5b7dcc41f2b0 Author: Linus Wallin Date: Sun Mar 3 18:15:40 2024 +0100 refactor: removes unnecessary if statement Removes if statement which didn't change the outcome of the program. commit e0b04d3959305a7bb5fb5ab22c05ce6f102283bb Author: Linus Wallin Date: Sun Mar 3 17:39:37 2024 +0100 feat: coveringGeohash funciton added Adds funtion which given an envelope returns up to four geohashes which cover the envelope. commit 9b64610a2fbb3095c6c14cd31cb2cc21086ebd9d Author: wenjj2000 Date: Mon Mar 4 09:24:34 2024 +0100 docs : update doucmentation for helper function and removed commented code commit dcdb58d2fc8ad30d0a9fdfa2bc7be89f3790800b Author: wenjj2000 Date: Mon Mar 4 07:52:31 2024 +0100 refactor : Changed toGeoHash function to use bitwise operations instead of strings to improve time complexity commit 80aa9326d9926285e13b544c7d9ebd6730e01012 Author: wenjj2000 Date: Mon Mar 4 00:33:36 2024 +0100 refactor : changing helper functions to private instead of public and removing their tests commit 62f7d5fbad86104efddd766ce7bb5b8f65314a65 Author: --replace-all Date: Sun Mar 3 16:48:48 2024 +0100 Test: added some tests for containingGeohash commit 7191907c0ab0b8f489cef21435483963a168eaad Author: --replace-all Date: Sun Mar 3 16:47:58 2024 +0100 Fix: lat and lon were inverted in test for toGeohash commit b21e5b9323ffc14c0fc2ed73d75fa7f99a11f8ab Author: --replace-all Date: Sun Mar 3 16:45:28 2024 +0100 Feat: Implemented containingGeohash commit c30e1e56bacc270954921b8a6b50b9fec0d306ff Merge: 84ef58e dbe750f Author: --replace-all Date: Sun Mar 3 16:44:34 2024 +0100 Fix: lat and lon were inverted in tooGeoHash commit 84ef58e8b5f5dc4d1ff208a769a3e7a13c1640d1 Author: --replace-all Date: Sun Mar 3 11:02:32 2024 +0100 test: Added some tests for toGeohash #213 commit dbe750f63f3c8983d06ac6f66dddb1c6da7e8592 Author: wenjj2000 Date: Sun Mar 3 00:21:25 2024 +0100 tests : added test for TestToGeoHash commit e4adae40b08e0c03f504df464ead57d08fa3aaea Author: wenjj2000 Date: Sat Mar 2 23:32:43 2024 +0100 tests : added test for binaryToBase32 and TestCovertToBinary commit 42b5287f84c7d8e640927d129b459957ab4f7a76 Author: wenjj2000 Date: Sat Mar 2 23:14:06 2024 +0100 feat : Added toGeoHash function with 2 helper function BinaryToBase32 and converyTobinary commit 5fd7cb0d8072816e26267ed45ffa1d07d2082090 Author: --replace-all Date: Sat Mar 2 17:33:31 2024 +0100 Feat: Implemented geohashToEnvelope #213 commit cac7dfdc4ebeb639fa34e572a4bc55e6b4228add Author: --replace-all Date: Sat Mar 2 17:30:08 2024 +0100 test: added some tests for geohashToEnvelope #213 commit 8461a3c9dac312f5fff7b9b70fdbf70bc2ab9797 Author: --replace-all Date: Sat Mar 2 16:15:02 2024 +0100 Test: Created a test file for geohash #213 commit af667b61e44816756e5cb632ce72aeece838e337 Author: --replace-all Date: Sat Mar 2 14:45:05 2024 +0100 Feat: Created Geohash class and its skeletton #213 --- .../java/com/esri/core/geometry/Geohash.java | 252 ++++++++++++++++++ .../com/esri/core/geometry/TestGeohash.java | 182 +++++++++++++ 2 files changed, 434 insertions(+) create mode 100644 src/main/java/com/esri/core/geometry/Geohash.java create mode 100644 src/test/java/com/esri/core/geometry/TestGeohash.java diff --git a/src/main/java/com/esri/core/geometry/Geohash.java b/src/main/java/com/esri/core/geometry/Geohash.java new file mode 100644 index 00000000..33987207 --- /dev/null +++ b/src/main/java/com/esri/core/geometry/Geohash.java @@ -0,0 +1,252 @@ +package com.esri.core.geometry; + +import java.security.InvalidParameterException; + +/** + * Helper class to work with geohash + */ +public class Geohash { + + private static final String base32 = "0123456789bcdefghjkmnpqrstuvwxyz"; + + private static final String INVALID_CHARACTER_MESSAGE = + "Invalid character in geohash: "; + private static final String GEOHASH_EXCEED_MAX_PRECISION_MESSAGE = + "Precision to high in geohash (max 24)"; + + /** + * Create an evelope from a given geohash + * @param geoHash + * @return The envelope that corresponds to the geohash + * @throws InvalidParameterException if the precision of geoHash is greater than 24 characters + */ + public static Envelope2D geohashToEnvelope(String geoHash) { + if (geoHash.length() > 24) { + throw new InvalidParameterException(GEOHASH_EXCEED_MAX_PRECISION_MESSAGE); + } + + long latBits = 0; + long lonBits = 0; + for (int i = 0; i < geoHash.length(); i++) { + int pos = base32.indexOf(geoHash.charAt(i)); + if (pos == -1) { + throw new InvalidParameterException( + new StringBuilder(INVALID_CHARACTER_MESSAGE) + .append('\'') + .append(geoHash.charAt(i)) + .append('\'') + .toString() + ); + } + + if (i % 2 == 0) { + lonBits = + (lonBits << 3) | ((pos >> 2) & 4) | ((pos >> 1) & 2) | (pos & 1); + latBits = (latBits << 2) | ((pos >> 2) & 2) | ((pos >> 1) & 1); + } else { + latBits = + (latBits << 3) | ((pos >> 2) & 4) | ((pos >> 1) & 2) | (pos & 1); + lonBits = (lonBits << 2) | ((pos >> 2) & 2) | ((pos >> 1) & 1); + } + } + + int lonBitsSize = (int) Math.ceil(geoHash.length() * 5 / 2.0); + int latBitsSize = geoHash.length() * 5 - lonBitsSize; + + double lat = -90; + double latPrecision = 90; + for (int i = 0; i < latBitsSize; i++) { + if (((1 << (latBitsSize - 1 - i)) & latBits) != 0) { + lat += latPrecision; + } + latPrecision /= 2; + } + + double lon = -180; + double lonPrecision = 180; + for (int i = 0; i < lonBitsSize; i++) { + if (((1 << (lonBitsSize - 1 - i)) & lonBits) != 0) { + lon += lonPrecision; + } + lonPrecision /= 2; + } + + return new Envelope2D( + lon, + lat, + lon + lonPrecision * 2, + lat + latPrecision * 2 + ); + } + + /** + * Computes the geohash that contains a point at a certain precision + * @param pt A point represented as lat/long pair + * @param characterLength - The precision of the geohash + * @return The geohash of containing pt as a String + */ + public static String toGeohash(Point2D pt, int characterLength) { + if (characterLength < 1) { + throw new InvalidParameterException( + "CharacterLength cannot be less than 1" + ); + } + if (characterLength > 6) { + throw new InvalidParameterException("Max characterLength of 6"); + } + int precision = characterLength * 5; + double lat = pt.y; + double lon = pt.x; + long latBit = Geohash.convertToBinary( + lat, + new double[] { -90, 90 }, + precision + ); + + long lonBit = Geohash.convertToBinary( + lon, + new double[] { -180, 180 }, + precision + ); + + long interwovenBin = 1; + for (int i = precision - 1; i >= 0; i--) { + long currLon = (lonBit >>> i) & 1; + long currLat = (latBit >>> i) & 1; + interwovenBin <<= 1; + interwovenBin |= currLon; + interwovenBin <<= 1; + interwovenBin |= currLat; + } + + return Geohash + .binaryToBase32(interwovenBin, precision * 2) + .substring(0, characterLength); + } + + /** + * Computes the base32 value of the binary string given + * @param binStr (long) Binary number that is to be converted to a base32 string + * @param len (int) number of bits + * @return base32 string of the binStr in chunks of 5 binary digits + */ + + private static String binaryToBase32(long binStr, int len) { + StringBuilder base32Str = new StringBuilder(); + + for (int i = len - 5; i >= 0; i -= 5) { + // Extract a group of 5 bits + int group = (int) (binStr >>> i) & 0x1F; + + // Use the extracted group as an index to fetch the corresponding base32 character + base32Str.append(base32.charAt(group)); + } + + return base32Str.toString(); + } + + /** + * Converts the value given to a binary string with the given precision and range + * @param value (double) The value to be converted to a binary number + * @param r (double[]) The range at which the value is to be compared with + * @param precision (int) The Precision (number of bits) that the binary number needs + * @return (String) A binary number representation of the value with the given range and precision + */ + + private static long convertToBinary(double value, double[] r, int precision) { + int binVal = 1; + for (int i = 0; i < precision; i++) { + double mid = (r[0] + r[1]) / 2; + if (value >= mid) { + binVal = binVal << 1; + binVal = binVal | 1; + r[0] = mid; + } else { + binVal = binVal << 1; + r[1] = mid; + } + } + return binVal; + } + + /** + * Compute the longest geohash that contains the envelope + * @param envelope + * @return the geohash as a string + */ + public static String containingGeohash(Envelope2D envelope) { + double posMinX = envelope.xmin + 180; + double posMaxX = envelope.xmax + 180; + double posMinY = envelope.ymin + 90; + double posMaxY = envelope.ymax + 90; + int chars = 0; + double xmin = 0; + double xmax = 0; + double ymin = 0; + double ymax = 0; + double deltaLon = 360; + double deltaLat = 180; + + while (xmin == xmax && ymin == ymax && chars < 7) { + if (chars % 2 == 0) { + deltaLon = deltaLon / 8; + deltaLat = deltaLat / 4; + } else { + deltaLon = deltaLon / 4; + deltaLat = deltaLat / 8; + } + + xmin = Math.floor(posMinX / deltaLon); + xmax = Math.floor(posMaxX / deltaLon); + ymin = Math.floor(posMinY / deltaLat); + ymax = Math.floor(posMaxY / deltaLat); + + chars++; + } + + if (chars == 1) return ""; + + return toGeohash(new Point2D(envelope.xmin, envelope.ymin), chars - 1); + } + + /** + * + * @param envelope + * @return up to four geohashes that completely cover given envelope + */ + public static String[] coveringGeohash(Envelope2D envelope) { + double xmin = envelope.xmin; + double ymin = envelope.ymin; + double xmax = envelope.xmax; + double ymax = envelope.ymax; + + if (NumberUtils.isNaN(xmax)) { + return new String[] {""}; + } + String[] geoHash = {containingGeohash(envelope)}; + if (geoHash[0] != ""){ + return geoHash; + } + + int grid = 45; + int gridMaxLon = (int)Math.floor(xmax/grid); + int gridMinLon = (int)Math.floor(xmin/grid); + int gridMaxLat = (int)Math.floor(ymax/grid); + int gridMinLat = (int)Math.floor(ymin/grid); + int deltaLon = gridMaxLon - gridMinLon + 1; + int deltaLat = gridMaxLat - gridMinLat + 1; + String[] geoHashes = new String[deltaLon * deltaLat]; + + if (deltaLon * deltaLat > 4){ + return new String[] {""}; + } else { + for (int i = 0; i < deltaLon; i++){ + for (int j = 0; j < deltaLat; j++){ + Point2D p = new Point2D(xmin + i * grid, ymin + j * grid); + geoHashes[i*deltaLat + j] = toGeohash(p, 1); + } + } + } + return geoHashes; + } +} diff --git a/src/test/java/com/esri/core/geometry/TestGeohash.java b/src/test/java/com/esri/core/geometry/TestGeohash.java new file mode 100644 index 00000000..ab09d327 --- /dev/null +++ b/src/test/java/com/esri/core/geometry/TestGeohash.java @@ -0,0 +1,182 @@ +package com.esri.core.geometry; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +public class TestGeohash { + + /** + * Check if the center of the new envelope is well placed + */ + @Test + public void testGeohashToEnvelopeWellCentered() { + double delta = 0.00000001; + + String geohash1 = "ghgh"; + + double lat1 = 72.50976563; + double lon1 = -40.60546875; + Envelope2D env1 = Geohash.geohashToEnvelope(geohash1); + double centerX1 = (env1.xmax + env1.xmin) * 0.5; + double centerY1 = (env1.ymax + env1.ymin) * 0.5; + + assertEquals(lon1, centerX1, delta); + assertEquals(lat1, centerY1, delta); + + String geohash2 = "p"; + + double lat2 = -67.50000000; + double lon2 = 157.50000000; + Envelope2D env2 = Geohash.geohashToEnvelope(geohash2); + double centerX2 = (env2.xmax + env2.xmin) * 0.5; + double centerY2 = (env2.ymax + env2.ymin) * 0.5; + + assertEquals(lon2, centerX2, delta); + assertEquals(lat2, centerY2, delta); + } + + /** + * Check if the dimension of the new envelope is correct for low precision + */ + @Test + public void testGeohashToEnvelopeGoodDimensions() { + double delta = 0.00000001; + + double latDiff = 180 / 4; + double lonDiff = 360 / 8; + + String geohash = "h"; + + Envelope2D env = Geohash.geohashToEnvelope(geohash); + + assertEquals(lonDiff, env.xmax - env.xmin, delta); + assertEquals(latDiff, env.ymax - env.ymin, delta); + } + + /** + * Check if the dimension of the new envelope is correct for higher precision + */ + @Test + public void testGeohashToEnvelopeGoodDimensions2() { + double delta = 0.00000001; + + double latDiff = 180.0 / 32768; + double lonDiff = 360.0 / 32768; + + String geohash = "hggggg"; + + Envelope2D env = Geohash.geohashToEnvelope(geohash); + + assertEquals(lonDiff, env.xmax - env.xmin, delta); + assertEquals(latDiff, env.ymax - env.ymin, delta); + } + + @Test + public void testToGeoHash() { + Point2D p0 = new Point2D(0, 0); + + Point2D p1 = new Point2D(-4.329, 48.669); + Point2D p2 = new Point2D(-30.382, 70.273); + Point2D p3 = new Point2D(14.276, 37.691); + Point2D p4 = new Point2D(-143.923, 48.669); + Point2D p5 = new Point2D(-143.923, 48.669); + + int chrLen = 5; + + String p0Hash = Geohash.toGeohash(p0, 1); + + String p1Hash = Geohash.toGeohash(p1, chrLen); + String p2Hash = Geohash.toGeohash(p2, chrLen); + String p3Hash = Geohash.toGeohash(p3, chrLen); + String p4Hash = Geohash.toGeohash(p4, chrLen); + String p5Hash = Geohash.toGeohash(p5, 6); + + assertEquals("s", p0Hash); + assertEquals("gbsuv", p1Hash); + assertEquals("gk6ru", p2Hash); + assertEquals("sqdnk", p3Hash); + assertEquals("bb9su", p4Hash); + assertEquals("bb9sug", p5Hash); + } + + @Test + public void testToGeohashHasGoodPrecision() { + Point2D point = new Point2D(18.068581, 59.329323); + assertEquals(6, Geohash.toGeohash(point, 6).length()); + } + + @Test + public void testToGeohash2() { + String expected = "u6sce"; + Point2D point = new Point2D(18.068581, 59.329323); + String geoHash = Geohash.toGeohash(point, 5); + + assertEquals(expected, geoHash); + } + + @Test + public void testContainingGeohashWithHugeValues() { + Envelope2D envelope = new Envelope2D(-179, -89, 179, 89); + assertEquals("", Geohash.containingGeohash(envelope)); + } + + @Test + public void testContainingGeohash() { + Envelope2D envelope = new Envelope2D(-179, -89, -140, -50); + assertEquals("0", Geohash.containingGeohash(envelope)); + } + + @Test + public void testContainingGeohash2() { + Envelope2D envelope = new Envelope2D(18.078, 59.3564, 18.1, 59.3344); + assertEquals("u6sce", Geohash.containingGeohash(envelope)); + } + + @Test + public void testCoveringGeohashEmptyEnvelope() { + Envelope2D emptyEnv = new Envelope2D(); + String [] coverage = Geohash.coveringGeohash(emptyEnv); + } + + @Test + public void testCoveringGeohashOneGeohash() { + Envelope2D env = new Envelope2D(-180, -90, -149, -49); + String [] coverage = Geohash.coveringGeohash(env); + assertEquals("0", coverage[0]); + } + + @Test + public void testCoveringGeohashPoint() { + Envelope2D env = new Envelope2D(180,90,180,90); + String [] coverage = Geohash.coveringGeohash(env); + assertEquals("zzzzzz", coverage[0]); + } + + @Test + public void testCoveringGeohashTwoGeohashes() { + Envelope2D env = new Envelope2D(-180, -90, -180, -35); + String [] coverage = Geohash.coveringGeohash(env); + assertEquals("0", coverage[0]); + assertEquals("2", coverage[1]); + } + + @Test + public void testCoveringGeohashThreeGeohashes() { + Envelope2D env = new Envelope2D(-180, -90, -180, 5); + String [] coverage = Geohash.coveringGeohash(env); + assertEquals("0", coverage[0]); + assertEquals("2", coverage[1]); + assertEquals("8", coverage[2]); + } + + @Test + public void testCoveringGeohashFourGeohashes() { + Envelope2D env = new Envelope2D(-180, -90, -130, -40); + String [] coverage = Geohash.coveringGeohash(env); + assertEquals("0", coverage[0]); + assertEquals("2", coverage[1]); + assertEquals("1", coverage[2]); + assertEquals("3", coverage[3]); + } +} \ No newline at end of file From 404dfa505c5fbc47bc38340112cde4c5bcfef88e Mon Sep 17 00:00:00 2001 From: wenjj2000 Date: Mon, 4 Mar 2024 10:39:23 +0100 Subject: [PATCH 04/13] Docs : added GeoHash patch file --- GeoHashPatch.patch | 447 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 447 insertions(+) create mode 100644 GeoHashPatch.patch diff --git a/GeoHashPatch.patch b/GeoHashPatch.patch new file mode 100644 index 00000000..83d41e18 --- /dev/null +++ b/GeoHashPatch.patch @@ -0,0 +1,447 @@ +diff --git a/src/main/java/com/esri/core/geometry/Geohash.java b/src/main/java/com/esri/core/geometry/Geohash.java +new file mode 100644 +index 0000000..3398720 +--- /dev/null ++++ b/src/main/java/com/esri/core/geometry/Geohash.java +@@ -0,0 +1,252 @@ ++package com.esri.core.geometry; ++ ++import java.security.InvalidParameterException; ++ ++/** ++ * Helper class to work with geohash ++ */ ++public class Geohash { ++ ++ private static final String base32 = "0123456789bcdefghjkmnpqrstuvwxyz"; ++ ++ private static final String INVALID_CHARACTER_MESSAGE = ++ "Invalid character in geohash: "; ++ private static final String GEOHASH_EXCEED_MAX_PRECISION_MESSAGE = ++ "Precision to high in geohash (max 24)"; ++ ++ /** ++ * Create an evelope from a given geohash ++ * @param geoHash ++ * @return The envelope that corresponds to the geohash ++ * @throws InvalidParameterException if the precision of geoHash is greater than 24 characters ++ */ ++ public static Envelope2D geohashToEnvelope(String geoHash) { ++ if (geoHash.length() > 24) { ++ throw new InvalidParameterException(GEOHASH_EXCEED_MAX_PRECISION_MESSAGE); ++ } ++ ++ long latBits = 0; ++ long lonBits = 0; ++ for (int i = 0; i < geoHash.length(); i++) { ++ int pos = base32.indexOf(geoHash.charAt(i)); ++ if (pos == -1) { ++ throw new InvalidParameterException( ++ new StringBuilder(INVALID_CHARACTER_MESSAGE) ++ .append('\'') ++ .append(geoHash.charAt(i)) ++ .append('\'') ++ .toString() ++ ); ++ } ++ ++ if (i % 2 == 0) { ++ lonBits = ++ (lonBits << 3) | ((pos >> 2) & 4) | ((pos >> 1) & 2) | (pos & 1); ++ latBits = (latBits << 2) | ((pos >> 2) & 2) | ((pos >> 1) & 1); ++ } else { ++ latBits = ++ (latBits << 3) | ((pos >> 2) & 4) | ((pos >> 1) & 2) | (pos & 1); ++ lonBits = (lonBits << 2) | ((pos >> 2) & 2) | ((pos >> 1) & 1); ++ } ++ } ++ ++ int lonBitsSize = (int) Math.ceil(geoHash.length() * 5 / 2.0); ++ int latBitsSize = geoHash.length() * 5 - lonBitsSize; ++ ++ double lat = -90; ++ double latPrecision = 90; ++ for (int i = 0; i < latBitsSize; i++) { ++ if (((1 << (latBitsSize - 1 - i)) & latBits) != 0) { ++ lat += latPrecision; ++ } ++ latPrecision /= 2; ++ } ++ ++ double lon = -180; ++ double lonPrecision = 180; ++ for (int i = 0; i < lonBitsSize; i++) { ++ if (((1 << (lonBitsSize - 1 - i)) & lonBits) != 0) { ++ lon += lonPrecision; ++ } ++ lonPrecision /= 2; ++ } ++ ++ return new Envelope2D( ++ lon, ++ lat, ++ lon + lonPrecision * 2, ++ lat + latPrecision * 2 ++ ); ++ } ++ ++ /** ++ * Computes the geohash that contains a point at a certain precision ++ * @param pt A point represented as lat/long pair ++ * @param characterLength - The precision of the geohash ++ * @return The geohash of containing pt as a String ++ */ ++ public static String toGeohash(Point2D pt, int characterLength) { ++ if (characterLength < 1) { ++ throw new InvalidParameterException( ++ "CharacterLength cannot be less than 1" ++ ); ++ } ++ if (characterLength > 6) { ++ throw new InvalidParameterException("Max characterLength of 6"); ++ } ++ int precision = characterLength * 5; ++ double lat = pt.y; ++ double lon = pt.x; ++ long latBit = Geohash.convertToBinary( ++ lat, ++ new double[] { -90, 90 }, ++ precision ++ ); ++ ++ long lonBit = Geohash.convertToBinary( ++ lon, ++ new double[] { -180, 180 }, ++ precision ++ ); ++ ++ long interwovenBin = 1; ++ for (int i = precision - 1; i >= 0; i--) { ++ long currLon = (lonBit >>> i) & 1; ++ long currLat = (latBit >>> i) & 1; ++ interwovenBin <<= 1; ++ interwovenBin |= currLon; ++ interwovenBin <<= 1; ++ interwovenBin |= currLat; ++ } ++ ++ return Geohash ++ .binaryToBase32(interwovenBin, precision * 2) ++ .substring(0, characterLength); ++ } ++ ++ /** ++ * Computes the base32 value of the binary string given ++ * @param binStr (long) Binary number that is to be converted to a base32 string ++ * @param len (int) number of bits ++ * @return base32 string of the binStr in chunks of 5 binary digits ++ */ ++ ++ private static String binaryToBase32(long binStr, int len) { ++ StringBuilder base32Str = new StringBuilder(); ++ ++ for (int i = len - 5; i >= 0; i -= 5) { ++ // Extract a group of 5 bits ++ int group = (int) (binStr >>> i) & 0x1F; ++ ++ // Use the extracted group as an index to fetch the corresponding base32 character ++ base32Str.append(base32.charAt(group)); ++ } ++ ++ return base32Str.toString(); ++ } ++ ++ /** ++ * Converts the value given to a binary string with the given precision and range ++ * @param value (double) The value to be converted to a binary number ++ * @param r (double[]) The range at which the value is to be compared with ++ * @param precision (int) The Precision (number of bits) that the binary number needs ++ * @return (String) A binary number representation of the value with the given range and precision ++ */ ++ ++ private static long convertToBinary(double value, double[] r, int precision) { ++ int binVal = 1; ++ for (int i = 0; i < precision; i++) { ++ double mid = (r[0] + r[1]) / 2; ++ if (value >= mid) { ++ binVal = binVal << 1; ++ binVal = binVal | 1; ++ r[0] = mid; ++ } else { ++ binVal = binVal << 1; ++ r[1] = mid; ++ } ++ } ++ return binVal; ++ } ++ ++ /** ++ * Compute the longest geohash that contains the envelope ++ * @param envelope ++ * @return the geohash as a string ++ */ ++ public static String containingGeohash(Envelope2D envelope) { ++ double posMinX = envelope.xmin + 180; ++ double posMaxX = envelope.xmax + 180; ++ double posMinY = envelope.ymin + 90; ++ double posMaxY = envelope.ymax + 90; ++ int chars = 0; ++ double xmin = 0; ++ double xmax = 0; ++ double ymin = 0; ++ double ymax = 0; ++ double deltaLon = 360; ++ double deltaLat = 180; ++ ++ while (xmin == xmax && ymin == ymax && chars < 7) { ++ if (chars % 2 == 0) { ++ deltaLon = deltaLon / 8; ++ deltaLat = deltaLat / 4; ++ } else { ++ deltaLon = deltaLon / 4; ++ deltaLat = deltaLat / 8; ++ } ++ ++ xmin = Math.floor(posMinX / deltaLon); ++ xmax = Math.floor(posMaxX / deltaLon); ++ ymin = Math.floor(posMinY / deltaLat); ++ ymax = Math.floor(posMaxY / deltaLat); ++ ++ chars++; ++ } ++ ++ if (chars == 1) return ""; ++ ++ return toGeohash(new Point2D(envelope.xmin, envelope.ymin), chars - 1); ++ } ++ ++ /** ++ * ++ * @param envelope ++ * @return up to four geohashes that completely cover given envelope ++ */ ++ public static String[] coveringGeohash(Envelope2D envelope) { ++ double xmin = envelope.xmin; ++ double ymin = envelope.ymin; ++ double xmax = envelope.xmax; ++ double ymax = envelope.ymax; ++ ++ if (NumberUtils.isNaN(xmax)) { ++ return new String[] {""}; ++ } ++ String[] geoHash = {containingGeohash(envelope)}; ++ if (geoHash[0] != ""){ ++ return geoHash; ++ } ++ ++ int grid = 45; ++ int gridMaxLon = (int)Math.floor(xmax/grid); ++ int gridMinLon = (int)Math.floor(xmin/grid); ++ int gridMaxLat = (int)Math.floor(ymax/grid); ++ int gridMinLat = (int)Math.floor(ymin/grid); ++ int deltaLon = gridMaxLon - gridMinLon + 1; ++ int deltaLat = gridMaxLat - gridMinLat + 1; ++ String[] geoHashes = new String[deltaLon * deltaLat]; ++ ++ if (deltaLon * deltaLat > 4){ ++ return new String[] {""}; ++ } else { ++ for (int i = 0; i < deltaLon; i++){ ++ for (int j = 0; j < deltaLat; j++){ ++ Point2D p = new Point2D(xmin + i * grid, ymin + j * grid); ++ geoHashes[i*deltaLat + j] = toGeohash(p, 1); ++ } ++ } ++ } ++ return geoHashes; ++ } ++} +diff --git a/src/test/java/com/esri/core/geometry/TestGeohash.java b/src/test/java/com/esri/core/geometry/TestGeohash.java +new file mode 100644 +index 0000000..ab09d32 +--- /dev/null ++++ b/src/test/java/com/esri/core/geometry/TestGeohash.java +@@ -0,0 +1,182 @@ ++package com.esri.core.geometry; ++ ++import static org.junit.Assert.assertEquals; ++ ++import org.junit.Test; ++ ++public class TestGeohash { ++ ++ /** ++ * Check if the center of the new envelope is well placed ++ */ ++ @Test ++ public void testGeohashToEnvelopeWellCentered() { ++ double delta = 0.00000001; ++ ++ String geohash1 = "ghgh"; ++ ++ double lat1 = 72.50976563; ++ double lon1 = -40.60546875; ++ Envelope2D env1 = Geohash.geohashToEnvelope(geohash1); ++ double centerX1 = (env1.xmax + env1.xmin) * 0.5; ++ double centerY1 = (env1.ymax + env1.ymin) * 0.5; ++ ++ assertEquals(lon1, centerX1, delta); ++ assertEquals(lat1, centerY1, delta); ++ ++ String geohash2 = "p"; ++ ++ double lat2 = -67.50000000; ++ double lon2 = 157.50000000; ++ Envelope2D env2 = Geohash.geohashToEnvelope(geohash2); ++ double centerX2 = (env2.xmax + env2.xmin) * 0.5; ++ double centerY2 = (env2.ymax + env2.ymin) * 0.5; ++ ++ assertEquals(lon2, centerX2, delta); ++ assertEquals(lat2, centerY2, delta); ++ } ++ ++ /** ++ * Check if the dimension of the new envelope is correct for low precision ++ */ ++ @Test ++ public void testGeohashToEnvelopeGoodDimensions() { ++ double delta = 0.00000001; ++ ++ double latDiff = 180 / 4; ++ double lonDiff = 360 / 8; ++ ++ String geohash = "h"; ++ ++ Envelope2D env = Geohash.geohashToEnvelope(geohash); ++ ++ assertEquals(lonDiff, env.xmax - env.xmin, delta); ++ assertEquals(latDiff, env.ymax - env.ymin, delta); ++ } ++ ++ /** ++ * Check if the dimension of the new envelope is correct for higher precision ++ */ ++ @Test ++ public void testGeohashToEnvelopeGoodDimensions2() { ++ double delta = 0.00000001; ++ ++ double latDiff = 180.0 / 32768; ++ double lonDiff = 360.0 / 32768; ++ ++ String geohash = "hggggg"; ++ ++ Envelope2D env = Geohash.geohashToEnvelope(geohash); ++ ++ assertEquals(lonDiff, env.xmax - env.xmin, delta); ++ assertEquals(latDiff, env.ymax - env.ymin, delta); ++ } ++ ++ @Test ++ public void testToGeoHash() { ++ Point2D p0 = new Point2D(0, 0); ++ ++ Point2D p1 = new Point2D(-4.329, 48.669); ++ Point2D p2 = new Point2D(-30.382, 70.273); ++ Point2D p3 = new Point2D(14.276, 37.691); ++ Point2D p4 = new Point2D(-143.923, 48.669); ++ Point2D p5 = new Point2D(-143.923, 48.669); ++ ++ int chrLen = 5; ++ ++ String p0Hash = Geohash.toGeohash(p0, 1); ++ ++ String p1Hash = Geohash.toGeohash(p1, chrLen); ++ String p2Hash = Geohash.toGeohash(p2, chrLen); ++ String p3Hash = Geohash.toGeohash(p3, chrLen); ++ String p4Hash = Geohash.toGeohash(p4, chrLen); ++ String p5Hash = Geohash.toGeohash(p5, 6); ++ ++ assertEquals("s", p0Hash); ++ assertEquals("gbsuv", p1Hash); ++ assertEquals("gk6ru", p2Hash); ++ assertEquals("sqdnk", p3Hash); ++ assertEquals("bb9su", p4Hash); ++ assertEquals("bb9sug", p5Hash); ++ } ++ ++ @Test ++ public void testToGeohashHasGoodPrecision() { ++ Point2D point = new Point2D(18.068581, 59.329323); ++ assertEquals(6, Geohash.toGeohash(point, 6).length()); ++ } ++ ++ @Test ++ public void testToGeohash2() { ++ String expected = "u6sce"; ++ Point2D point = new Point2D(18.068581, 59.329323); ++ String geoHash = Geohash.toGeohash(point, 5); ++ ++ assertEquals(expected, geoHash); ++ } ++ ++ @Test ++ public void testContainingGeohashWithHugeValues() { ++ Envelope2D envelope = new Envelope2D(-179, -89, 179, 89); ++ assertEquals("", Geohash.containingGeohash(envelope)); ++ } ++ ++ @Test ++ public void testContainingGeohash() { ++ Envelope2D envelope = new Envelope2D(-179, -89, -140, -50); ++ assertEquals("0", Geohash.containingGeohash(envelope)); ++ } ++ ++ @Test ++ public void testContainingGeohash2() { ++ Envelope2D envelope = new Envelope2D(18.078, 59.3564, 18.1, 59.3344); ++ assertEquals("u6sce", Geohash.containingGeohash(envelope)); ++ } ++ ++ @Test ++ public void testCoveringGeohashEmptyEnvelope() { ++ Envelope2D emptyEnv = new Envelope2D(); ++ String [] coverage = Geohash.coveringGeohash(emptyEnv); ++ } ++ ++ @Test ++ public void testCoveringGeohashOneGeohash() { ++ Envelope2D env = new Envelope2D(-180, -90, -149, -49); ++ String [] coverage = Geohash.coveringGeohash(env); ++ assertEquals("0", coverage[0]); ++ } ++ ++ @Test ++ public void testCoveringGeohashPoint() { ++ Envelope2D env = new Envelope2D(180,90,180,90); ++ String [] coverage = Geohash.coveringGeohash(env); ++ assertEquals("zzzzzz", coverage[0]); ++ } ++ ++ @Test ++ public void testCoveringGeohashTwoGeohashes() { ++ Envelope2D env = new Envelope2D(-180, -90, -180, -35); ++ String [] coverage = Geohash.coveringGeohash(env); ++ assertEquals("0", coverage[0]); ++ assertEquals("2", coverage[1]); ++ } ++ ++ @Test ++ public void testCoveringGeohashThreeGeohashes() { ++ Envelope2D env = new Envelope2D(-180, -90, -180, 5); ++ String [] coverage = Geohash.coveringGeohash(env); ++ assertEquals("0", coverage[0]); ++ assertEquals("2", coverage[1]); ++ assertEquals("8", coverage[2]); ++ } ++ ++ @Test ++ public void testCoveringGeohashFourGeohashes() { ++ Envelope2D env = new Envelope2D(-180, -90, -130, -40); ++ String [] coverage = Geohash.coveringGeohash(env); ++ assertEquals("0", coverage[0]); ++ assertEquals("2", coverage[1]); ++ assertEquals("1", coverage[2]); ++ assertEquals("3", coverage[3]); ++ } ++} +\ No newline at end of file From 1bd948629db52e21d6218c6fe67cd2d88abd6e66 Mon Sep 17 00:00:00 2001 From: Linus Wallin <73783914+LinusWallin@users.noreply.github.com> Date: Mon, 4 Mar 2024 11:05:39 +0100 Subject: [PATCH 05/13] feat: Geohash class (#1) * docs: Added md file that describes the tags for our commits COMMIT.md contains the structure for how we should add tags to our commits to make them easier to understand. * Revert "docs: Added md file that describes the tags for our commits" * Squashed commit of the following: commit b29091aa3386ba717d94dd23a4ffb20049a35930 Merge: 9b64610 9aaaf4c Author: Wen Jun Jie <132931864+WenJJ2000@users.noreply.github.com> Date: Mon Mar 4 10:09:16 2024 +0100 Merge pull request #55 from DD2480-Group-3/54-coveringGeohash 54 covering geohash commit 9aaaf4cd1222a16a5f89ba64a3ea1a6087dc81dd Author: wenjj2000 Date: Mon Mar 4 10:00:46 2024 +0100 Squashed commit of the following: commit 279212f7e85e923793eaace978cbbcb666508096 Author: Linus Wallin Date: Mon Mar 4 09:46:08 2024 +0100 fix: solves bug where precision is allowed to be too high Limits the precision in containingGeohash function to 6 from 24 since the toGeohash function has been updated to only work for highest precision 6. commit 2aa650f094120e01c3aa9b2329698b3b88b8dfe0 Merge: 263f8e2 dcdb58d Author: Linus Wallin Date: Mon Mar 4 09:45:04 2024 +0100 Merge branch '213-geometry-to-geohash' of github.com:DD2480-Group-3/geometry-api-java into 54-coveringGeohash commit 263f8e2a10d3ee5cb4815548d30abe6b19be26d1 Author: Linus Wallin Date: Sun Mar 3 19:47:34 2024 +0100 refactor: removed missed empty line commit 72b84f42b3e101f9aadc0b336f602cbe30bdfe9a Author: Linus Wallin Date: Sun Mar 3 19:47:05 2024 +0100 refactor: removed empty line commit 6fef31241f5724a43805781e0062256056ad45b5 Author: Linus Wallin Date: Sun Mar 3 19:45:34 2024 +0100 refactor: removed empty lines commit 1287b2c1ea051537ffc11f29f200ea88dfa47e99 Author: Linus Wallin Date: Sun Mar 3 19:39:49 2024 +0100 test: changed envelope to encompass 4 different parts of the geo grid The envelope was previously assigned wrong max x and y values, which resulted in errors as the coveringGeohash function didn't return the expected amount of geo hashes. commit 69ca9c0ffbfba83d59d6f4528bb3230c596aac1c Author: Linus Wallin Date: Sun Mar 3 19:30:38 2024 +0100 fix: solves bug which resulted in wrong geohashes commit 2a8d13169998e8dda67b8c737623c09ccd01438b Author: Linus Wallin Date: Sun Mar 3 19:24:32 2024 +0100 test: updated tests to match the changes of the function in last commit commit bb45871770a7d31861a166d0d6a3ae52892b5866 Author: Linus Wallin Date: Sun Mar 3 19:24:02 2024 +0100 refactor: made the return string array dynamic commit 708542e0eb04197d2049e6e46dae3722f0f4c048 Author: Linus Wallin Date: Sun Mar 3 18:42:15 2024 +0100 refactor: removes indentation error caused by merge commit a216691f06191097ed230c771c15e739d8ee32d5 Merge: 4420765 62f7d5f Author: Linus Wallin Date: Sun Mar 3 18:41:36 2024 +0100 Merge branch '213-geometry-to-geohash' of github.com:DD2480-Group-3/geometry-api-java into 54-coveringGeohash commit 4420765466991ff31b9ffd31fa321740db3b8502 Author: Linus Wallin Date: Sun Mar 3 18:37:30 2024 +0100 test: added test cases for coveringGeohash function commit 2c319798289907fa33f165df62cf5b7dcc41f2b0 Author: Linus Wallin Date: Sun Mar 3 18:15:40 2024 +0100 refactor: removes unnecessary if statement Removes if statement which didn't change the outcome of the program. commit e0b04d3959305a7bb5fb5ab22c05ce6f102283bb Author: Linus Wallin Date: Sun Mar 3 17:39:37 2024 +0100 feat: coveringGeohash funciton added Adds funtion which given an envelope returns up to four geohashes which cover the envelope. commit 9b64610a2fbb3095c6c14cd31cb2cc21086ebd9d Author: wenjj2000 Date: Mon Mar 4 09:24:34 2024 +0100 docs : update doucmentation for helper function and removed commented code commit dcdb58d2fc8ad30d0a9fdfa2bc7be89f3790800b Author: wenjj2000 Date: Mon Mar 4 07:52:31 2024 +0100 refactor : Changed toGeoHash function to use bitwise operations instead of strings to improve time complexity commit 80aa9326d9926285e13b544c7d9ebd6730e01012 Author: wenjj2000 Date: Mon Mar 4 00:33:36 2024 +0100 refactor : changing helper functions to private instead of public and removing their tests commit 62f7d5fbad86104efddd766ce7bb5b8f65314a65 Author: --replace-all Date: Sun Mar 3 16:48:48 2024 +0100 Test: added some tests for containingGeohash commit 7191907c0ab0b8f489cef21435483963a168eaad Author: --replace-all Date: Sun Mar 3 16:47:58 2024 +0100 Fix: lat and lon were inverted in test for toGeohash commit b21e5b9323ffc14c0fc2ed73d75fa7f99a11f8ab Author: --replace-all Date: Sun Mar 3 16:45:28 2024 +0100 Feat: Implemented containingGeohash commit c30e1e56bacc270954921b8a6b50b9fec0d306ff Merge: 84ef58e dbe750f Author: --replace-all Date: Sun Mar 3 16:44:34 2024 +0100 Fix: lat and lon were inverted in tooGeoHash commit 84ef58e8b5f5dc4d1ff208a769a3e7a13c1640d1 Author: --replace-all Date: Sun Mar 3 11:02:32 2024 +0100 test: Added some tests for toGeohash #213 commit dbe750f63f3c8983d06ac6f66dddb1c6da7e8592 Author: wenjj2000 Date: Sun Mar 3 00:21:25 2024 +0100 tests : added test for TestToGeoHash commit e4adae40b08e0c03f504df464ead57d08fa3aaea Author: wenjj2000 Date: Sat Mar 2 23:32:43 2024 +0100 tests : added test for binaryToBase32 and TestCovertToBinary commit 42b5287f84c7d8e640927d129b459957ab4f7a76 Author: wenjj2000 Date: Sat Mar 2 23:14:06 2024 +0100 feat : Added toGeoHash function with 2 helper function BinaryToBase32 and converyTobinary commit 5fd7cb0d8072816e26267ed45ffa1d07d2082090 Author: --replace-all Date: Sat Mar 2 17:33:31 2024 +0100 Feat: Implemented geohashToEnvelope #213 commit cac7dfdc4ebeb639fa34e572a4bc55e6b4228add Author: --replace-all Date: Sat Mar 2 17:30:08 2024 +0100 test: added some tests for geohashToEnvelope #213 commit 8461a3c9dac312f5fff7b9b70fdbf70bc2ab9797 Author: --replace-all Date: Sat Mar 2 16:15:02 2024 +0100 Test: Created a test file for geohash #213 commit af667b61e44816756e5cb632ce72aeece838e337 Author: --replace-all Date: Sat Mar 2 14:45:05 2024 +0100 Feat: Created Geohash class and its skeletton #213 * Docs : added GeoHash patch file --------- Co-authored-by: karlsb <36365664+karlsb@users.noreply.github.com> Co-authored-by: wenjj2000 --- GeoHashPatch.patch | 447 ++++++++++++++++++ .../java/com/esri/core/geometry/Geohash.java | 252 ++++++++++ .../com/esri/core/geometry/TestGeohash.java | 182 +++++++ 3 files changed, 881 insertions(+) create mode 100644 GeoHashPatch.patch create mode 100644 src/main/java/com/esri/core/geometry/Geohash.java create mode 100644 src/test/java/com/esri/core/geometry/TestGeohash.java diff --git a/GeoHashPatch.patch b/GeoHashPatch.patch new file mode 100644 index 00000000..83d41e18 --- /dev/null +++ b/GeoHashPatch.patch @@ -0,0 +1,447 @@ +diff --git a/src/main/java/com/esri/core/geometry/Geohash.java b/src/main/java/com/esri/core/geometry/Geohash.java +new file mode 100644 +index 0000000..3398720 +--- /dev/null ++++ b/src/main/java/com/esri/core/geometry/Geohash.java +@@ -0,0 +1,252 @@ ++package com.esri.core.geometry; ++ ++import java.security.InvalidParameterException; ++ ++/** ++ * Helper class to work with geohash ++ */ ++public class Geohash { ++ ++ private static final String base32 = "0123456789bcdefghjkmnpqrstuvwxyz"; ++ ++ private static final String INVALID_CHARACTER_MESSAGE = ++ "Invalid character in geohash: "; ++ private static final String GEOHASH_EXCEED_MAX_PRECISION_MESSAGE = ++ "Precision to high in geohash (max 24)"; ++ ++ /** ++ * Create an evelope from a given geohash ++ * @param geoHash ++ * @return The envelope that corresponds to the geohash ++ * @throws InvalidParameterException if the precision of geoHash is greater than 24 characters ++ */ ++ public static Envelope2D geohashToEnvelope(String geoHash) { ++ if (geoHash.length() > 24) { ++ throw new InvalidParameterException(GEOHASH_EXCEED_MAX_PRECISION_MESSAGE); ++ } ++ ++ long latBits = 0; ++ long lonBits = 0; ++ for (int i = 0; i < geoHash.length(); i++) { ++ int pos = base32.indexOf(geoHash.charAt(i)); ++ if (pos == -1) { ++ throw new InvalidParameterException( ++ new StringBuilder(INVALID_CHARACTER_MESSAGE) ++ .append('\'') ++ .append(geoHash.charAt(i)) ++ .append('\'') ++ .toString() ++ ); ++ } ++ ++ if (i % 2 == 0) { ++ lonBits = ++ (lonBits << 3) | ((pos >> 2) & 4) | ((pos >> 1) & 2) | (pos & 1); ++ latBits = (latBits << 2) | ((pos >> 2) & 2) | ((pos >> 1) & 1); ++ } else { ++ latBits = ++ (latBits << 3) | ((pos >> 2) & 4) | ((pos >> 1) & 2) | (pos & 1); ++ lonBits = (lonBits << 2) | ((pos >> 2) & 2) | ((pos >> 1) & 1); ++ } ++ } ++ ++ int lonBitsSize = (int) Math.ceil(geoHash.length() * 5 / 2.0); ++ int latBitsSize = geoHash.length() * 5 - lonBitsSize; ++ ++ double lat = -90; ++ double latPrecision = 90; ++ for (int i = 0; i < latBitsSize; i++) { ++ if (((1 << (latBitsSize - 1 - i)) & latBits) != 0) { ++ lat += latPrecision; ++ } ++ latPrecision /= 2; ++ } ++ ++ double lon = -180; ++ double lonPrecision = 180; ++ for (int i = 0; i < lonBitsSize; i++) { ++ if (((1 << (lonBitsSize - 1 - i)) & lonBits) != 0) { ++ lon += lonPrecision; ++ } ++ lonPrecision /= 2; ++ } ++ ++ return new Envelope2D( ++ lon, ++ lat, ++ lon + lonPrecision * 2, ++ lat + latPrecision * 2 ++ ); ++ } ++ ++ /** ++ * Computes the geohash that contains a point at a certain precision ++ * @param pt A point represented as lat/long pair ++ * @param characterLength - The precision of the geohash ++ * @return The geohash of containing pt as a String ++ */ ++ public static String toGeohash(Point2D pt, int characterLength) { ++ if (characterLength < 1) { ++ throw new InvalidParameterException( ++ "CharacterLength cannot be less than 1" ++ ); ++ } ++ if (characterLength > 6) { ++ throw new InvalidParameterException("Max characterLength of 6"); ++ } ++ int precision = characterLength * 5; ++ double lat = pt.y; ++ double lon = pt.x; ++ long latBit = Geohash.convertToBinary( ++ lat, ++ new double[] { -90, 90 }, ++ precision ++ ); ++ ++ long lonBit = Geohash.convertToBinary( ++ lon, ++ new double[] { -180, 180 }, ++ precision ++ ); ++ ++ long interwovenBin = 1; ++ for (int i = precision - 1; i >= 0; i--) { ++ long currLon = (lonBit >>> i) & 1; ++ long currLat = (latBit >>> i) & 1; ++ interwovenBin <<= 1; ++ interwovenBin |= currLon; ++ interwovenBin <<= 1; ++ interwovenBin |= currLat; ++ } ++ ++ return Geohash ++ .binaryToBase32(interwovenBin, precision * 2) ++ .substring(0, characterLength); ++ } ++ ++ /** ++ * Computes the base32 value of the binary string given ++ * @param binStr (long) Binary number that is to be converted to a base32 string ++ * @param len (int) number of bits ++ * @return base32 string of the binStr in chunks of 5 binary digits ++ */ ++ ++ private static String binaryToBase32(long binStr, int len) { ++ StringBuilder base32Str = new StringBuilder(); ++ ++ for (int i = len - 5; i >= 0; i -= 5) { ++ // Extract a group of 5 bits ++ int group = (int) (binStr >>> i) & 0x1F; ++ ++ // Use the extracted group as an index to fetch the corresponding base32 character ++ base32Str.append(base32.charAt(group)); ++ } ++ ++ return base32Str.toString(); ++ } ++ ++ /** ++ * Converts the value given to a binary string with the given precision and range ++ * @param value (double) The value to be converted to a binary number ++ * @param r (double[]) The range at which the value is to be compared with ++ * @param precision (int) The Precision (number of bits) that the binary number needs ++ * @return (String) A binary number representation of the value with the given range and precision ++ */ ++ ++ private static long convertToBinary(double value, double[] r, int precision) { ++ int binVal = 1; ++ for (int i = 0; i < precision; i++) { ++ double mid = (r[0] + r[1]) / 2; ++ if (value >= mid) { ++ binVal = binVal << 1; ++ binVal = binVal | 1; ++ r[0] = mid; ++ } else { ++ binVal = binVal << 1; ++ r[1] = mid; ++ } ++ } ++ return binVal; ++ } ++ ++ /** ++ * Compute the longest geohash that contains the envelope ++ * @param envelope ++ * @return the geohash as a string ++ */ ++ public static String containingGeohash(Envelope2D envelope) { ++ double posMinX = envelope.xmin + 180; ++ double posMaxX = envelope.xmax + 180; ++ double posMinY = envelope.ymin + 90; ++ double posMaxY = envelope.ymax + 90; ++ int chars = 0; ++ double xmin = 0; ++ double xmax = 0; ++ double ymin = 0; ++ double ymax = 0; ++ double deltaLon = 360; ++ double deltaLat = 180; ++ ++ while (xmin == xmax && ymin == ymax && chars < 7) { ++ if (chars % 2 == 0) { ++ deltaLon = deltaLon / 8; ++ deltaLat = deltaLat / 4; ++ } else { ++ deltaLon = deltaLon / 4; ++ deltaLat = deltaLat / 8; ++ } ++ ++ xmin = Math.floor(posMinX / deltaLon); ++ xmax = Math.floor(posMaxX / deltaLon); ++ ymin = Math.floor(posMinY / deltaLat); ++ ymax = Math.floor(posMaxY / deltaLat); ++ ++ chars++; ++ } ++ ++ if (chars == 1) return ""; ++ ++ return toGeohash(new Point2D(envelope.xmin, envelope.ymin), chars - 1); ++ } ++ ++ /** ++ * ++ * @param envelope ++ * @return up to four geohashes that completely cover given envelope ++ */ ++ public static String[] coveringGeohash(Envelope2D envelope) { ++ double xmin = envelope.xmin; ++ double ymin = envelope.ymin; ++ double xmax = envelope.xmax; ++ double ymax = envelope.ymax; ++ ++ if (NumberUtils.isNaN(xmax)) { ++ return new String[] {""}; ++ } ++ String[] geoHash = {containingGeohash(envelope)}; ++ if (geoHash[0] != ""){ ++ return geoHash; ++ } ++ ++ int grid = 45; ++ int gridMaxLon = (int)Math.floor(xmax/grid); ++ int gridMinLon = (int)Math.floor(xmin/grid); ++ int gridMaxLat = (int)Math.floor(ymax/grid); ++ int gridMinLat = (int)Math.floor(ymin/grid); ++ int deltaLon = gridMaxLon - gridMinLon + 1; ++ int deltaLat = gridMaxLat - gridMinLat + 1; ++ String[] geoHashes = new String[deltaLon * deltaLat]; ++ ++ if (deltaLon * deltaLat > 4){ ++ return new String[] {""}; ++ } else { ++ for (int i = 0; i < deltaLon; i++){ ++ for (int j = 0; j < deltaLat; j++){ ++ Point2D p = new Point2D(xmin + i * grid, ymin + j * grid); ++ geoHashes[i*deltaLat + j] = toGeohash(p, 1); ++ } ++ } ++ } ++ return geoHashes; ++ } ++} +diff --git a/src/test/java/com/esri/core/geometry/TestGeohash.java b/src/test/java/com/esri/core/geometry/TestGeohash.java +new file mode 100644 +index 0000000..ab09d32 +--- /dev/null ++++ b/src/test/java/com/esri/core/geometry/TestGeohash.java +@@ -0,0 +1,182 @@ ++package com.esri.core.geometry; ++ ++import static org.junit.Assert.assertEquals; ++ ++import org.junit.Test; ++ ++public class TestGeohash { ++ ++ /** ++ * Check if the center of the new envelope is well placed ++ */ ++ @Test ++ public void testGeohashToEnvelopeWellCentered() { ++ double delta = 0.00000001; ++ ++ String geohash1 = "ghgh"; ++ ++ double lat1 = 72.50976563; ++ double lon1 = -40.60546875; ++ Envelope2D env1 = Geohash.geohashToEnvelope(geohash1); ++ double centerX1 = (env1.xmax + env1.xmin) * 0.5; ++ double centerY1 = (env1.ymax + env1.ymin) * 0.5; ++ ++ assertEquals(lon1, centerX1, delta); ++ assertEquals(lat1, centerY1, delta); ++ ++ String geohash2 = "p"; ++ ++ double lat2 = -67.50000000; ++ double lon2 = 157.50000000; ++ Envelope2D env2 = Geohash.geohashToEnvelope(geohash2); ++ double centerX2 = (env2.xmax + env2.xmin) * 0.5; ++ double centerY2 = (env2.ymax + env2.ymin) * 0.5; ++ ++ assertEquals(lon2, centerX2, delta); ++ assertEquals(lat2, centerY2, delta); ++ } ++ ++ /** ++ * Check if the dimension of the new envelope is correct for low precision ++ */ ++ @Test ++ public void testGeohashToEnvelopeGoodDimensions() { ++ double delta = 0.00000001; ++ ++ double latDiff = 180 / 4; ++ double lonDiff = 360 / 8; ++ ++ String geohash = "h"; ++ ++ Envelope2D env = Geohash.geohashToEnvelope(geohash); ++ ++ assertEquals(lonDiff, env.xmax - env.xmin, delta); ++ assertEquals(latDiff, env.ymax - env.ymin, delta); ++ } ++ ++ /** ++ * Check if the dimension of the new envelope is correct for higher precision ++ */ ++ @Test ++ public void testGeohashToEnvelopeGoodDimensions2() { ++ double delta = 0.00000001; ++ ++ double latDiff = 180.0 / 32768; ++ double lonDiff = 360.0 / 32768; ++ ++ String geohash = "hggggg"; ++ ++ Envelope2D env = Geohash.geohashToEnvelope(geohash); ++ ++ assertEquals(lonDiff, env.xmax - env.xmin, delta); ++ assertEquals(latDiff, env.ymax - env.ymin, delta); ++ } ++ ++ @Test ++ public void testToGeoHash() { ++ Point2D p0 = new Point2D(0, 0); ++ ++ Point2D p1 = new Point2D(-4.329, 48.669); ++ Point2D p2 = new Point2D(-30.382, 70.273); ++ Point2D p3 = new Point2D(14.276, 37.691); ++ Point2D p4 = new Point2D(-143.923, 48.669); ++ Point2D p5 = new Point2D(-143.923, 48.669); ++ ++ int chrLen = 5; ++ ++ String p0Hash = Geohash.toGeohash(p0, 1); ++ ++ String p1Hash = Geohash.toGeohash(p1, chrLen); ++ String p2Hash = Geohash.toGeohash(p2, chrLen); ++ String p3Hash = Geohash.toGeohash(p3, chrLen); ++ String p4Hash = Geohash.toGeohash(p4, chrLen); ++ String p5Hash = Geohash.toGeohash(p5, 6); ++ ++ assertEquals("s", p0Hash); ++ assertEquals("gbsuv", p1Hash); ++ assertEquals("gk6ru", p2Hash); ++ assertEquals("sqdnk", p3Hash); ++ assertEquals("bb9su", p4Hash); ++ assertEquals("bb9sug", p5Hash); ++ } ++ ++ @Test ++ public void testToGeohashHasGoodPrecision() { ++ Point2D point = new Point2D(18.068581, 59.329323); ++ assertEquals(6, Geohash.toGeohash(point, 6).length()); ++ } ++ ++ @Test ++ public void testToGeohash2() { ++ String expected = "u6sce"; ++ Point2D point = new Point2D(18.068581, 59.329323); ++ String geoHash = Geohash.toGeohash(point, 5); ++ ++ assertEquals(expected, geoHash); ++ } ++ ++ @Test ++ public void testContainingGeohashWithHugeValues() { ++ Envelope2D envelope = new Envelope2D(-179, -89, 179, 89); ++ assertEquals("", Geohash.containingGeohash(envelope)); ++ } ++ ++ @Test ++ public void testContainingGeohash() { ++ Envelope2D envelope = new Envelope2D(-179, -89, -140, -50); ++ assertEquals("0", Geohash.containingGeohash(envelope)); ++ } ++ ++ @Test ++ public void testContainingGeohash2() { ++ Envelope2D envelope = new Envelope2D(18.078, 59.3564, 18.1, 59.3344); ++ assertEquals("u6sce", Geohash.containingGeohash(envelope)); ++ } ++ ++ @Test ++ public void testCoveringGeohashEmptyEnvelope() { ++ Envelope2D emptyEnv = new Envelope2D(); ++ String [] coverage = Geohash.coveringGeohash(emptyEnv); ++ } ++ ++ @Test ++ public void testCoveringGeohashOneGeohash() { ++ Envelope2D env = new Envelope2D(-180, -90, -149, -49); ++ String [] coverage = Geohash.coveringGeohash(env); ++ assertEquals("0", coverage[0]); ++ } ++ ++ @Test ++ public void testCoveringGeohashPoint() { ++ Envelope2D env = new Envelope2D(180,90,180,90); ++ String [] coverage = Geohash.coveringGeohash(env); ++ assertEquals("zzzzzz", coverage[0]); ++ } ++ ++ @Test ++ public void testCoveringGeohashTwoGeohashes() { ++ Envelope2D env = new Envelope2D(-180, -90, -180, -35); ++ String [] coverage = Geohash.coveringGeohash(env); ++ assertEquals("0", coverage[0]); ++ assertEquals("2", coverage[1]); ++ } ++ ++ @Test ++ public void testCoveringGeohashThreeGeohashes() { ++ Envelope2D env = new Envelope2D(-180, -90, -180, 5); ++ String [] coverage = Geohash.coveringGeohash(env); ++ assertEquals("0", coverage[0]); ++ assertEquals("2", coverage[1]); ++ assertEquals("8", coverage[2]); ++ } ++ ++ @Test ++ public void testCoveringGeohashFourGeohashes() { ++ Envelope2D env = new Envelope2D(-180, -90, -130, -40); ++ String [] coverage = Geohash.coveringGeohash(env); ++ assertEquals("0", coverage[0]); ++ assertEquals("2", coverage[1]); ++ assertEquals("1", coverage[2]); ++ assertEquals("3", coverage[3]); ++ } ++} +\ No newline at end of file diff --git a/src/main/java/com/esri/core/geometry/Geohash.java b/src/main/java/com/esri/core/geometry/Geohash.java new file mode 100644 index 00000000..33987207 --- /dev/null +++ b/src/main/java/com/esri/core/geometry/Geohash.java @@ -0,0 +1,252 @@ +package com.esri.core.geometry; + +import java.security.InvalidParameterException; + +/** + * Helper class to work with geohash + */ +public class Geohash { + + private static final String base32 = "0123456789bcdefghjkmnpqrstuvwxyz"; + + private static final String INVALID_CHARACTER_MESSAGE = + "Invalid character in geohash: "; + private static final String GEOHASH_EXCEED_MAX_PRECISION_MESSAGE = + "Precision to high in geohash (max 24)"; + + /** + * Create an evelope from a given geohash + * @param geoHash + * @return The envelope that corresponds to the geohash + * @throws InvalidParameterException if the precision of geoHash is greater than 24 characters + */ + public static Envelope2D geohashToEnvelope(String geoHash) { + if (geoHash.length() > 24) { + throw new InvalidParameterException(GEOHASH_EXCEED_MAX_PRECISION_MESSAGE); + } + + long latBits = 0; + long lonBits = 0; + for (int i = 0; i < geoHash.length(); i++) { + int pos = base32.indexOf(geoHash.charAt(i)); + if (pos == -1) { + throw new InvalidParameterException( + new StringBuilder(INVALID_CHARACTER_MESSAGE) + .append('\'') + .append(geoHash.charAt(i)) + .append('\'') + .toString() + ); + } + + if (i % 2 == 0) { + lonBits = + (lonBits << 3) | ((pos >> 2) & 4) | ((pos >> 1) & 2) | (pos & 1); + latBits = (latBits << 2) | ((pos >> 2) & 2) | ((pos >> 1) & 1); + } else { + latBits = + (latBits << 3) | ((pos >> 2) & 4) | ((pos >> 1) & 2) | (pos & 1); + lonBits = (lonBits << 2) | ((pos >> 2) & 2) | ((pos >> 1) & 1); + } + } + + int lonBitsSize = (int) Math.ceil(geoHash.length() * 5 / 2.0); + int latBitsSize = geoHash.length() * 5 - lonBitsSize; + + double lat = -90; + double latPrecision = 90; + for (int i = 0; i < latBitsSize; i++) { + if (((1 << (latBitsSize - 1 - i)) & latBits) != 0) { + lat += latPrecision; + } + latPrecision /= 2; + } + + double lon = -180; + double lonPrecision = 180; + for (int i = 0; i < lonBitsSize; i++) { + if (((1 << (lonBitsSize - 1 - i)) & lonBits) != 0) { + lon += lonPrecision; + } + lonPrecision /= 2; + } + + return new Envelope2D( + lon, + lat, + lon + lonPrecision * 2, + lat + latPrecision * 2 + ); + } + + /** + * Computes the geohash that contains a point at a certain precision + * @param pt A point represented as lat/long pair + * @param characterLength - The precision of the geohash + * @return The geohash of containing pt as a String + */ + public static String toGeohash(Point2D pt, int characterLength) { + if (characterLength < 1) { + throw new InvalidParameterException( + "CharacterLength cannot be less than 1" + ); + } + if (characterLength > 6) { + throw new InvalidParameterException("Max characterLength of 6"); + } + int precision = characterLength * 5; + double lat = pt.y; + double lon = pt.x; + long latBit = Geohash.convertToBinary( + lat, + new double[] { -90, 90 }, + precision + ); + + long lonBit = Geohash.convertToBinary( + lon, + new double[] { -180, 180 }, + precision + ); + + long interwovenBin = 1; + for (int i = precision - 1; i >= 0; i--) { + long currLon = (lonBit >>> i) & 1; + long currLat = (latBit >>> i) & 1; + interwovenBin <<= 1; + interwovenBin |= currLon; + interwovenBin <<= 1; + interwovenBin |= currLat; + } + + return Geohash + .binaryToBase32(interwovenBin, precision * 2) + .substring(0, characterLength); + } + + /** + * Computes the base32 value of the binary string given + * @param binStr (long) Binary number that is to be converted to a base32 string + * @param len (int) number of bits + * @return base32 string of the binStr in chunks of 5 binary digits + */ + + private static String binaryToBase32(long binStr, int len) { + StringBuilder base32Str = new StringBuilder(); + + for (int i = len - 5; i >= 0; i -= 5) { + // Extract a group of 5 bits + int group = (int) (binStr >>> i) & 0x1F; + + // Use the extracted group as an index to fetch the corresponding base32 character + base32Str.append(base32.charAt(group)); + } + + return base32Str.toString(); + } + + /** + * Converts the value given to a binary string with the given precision and range + * @param value (double) The value to be converted to a binary number + * @param r (double[]) The range at which the value is to be compared with + * @param precision (int) The Precision (number of bits) that the binary number needs + * @return (String) A binary number representation of the value with the given range and precision + */ + + private static long convertToBinary(double value, double[] r, int precision) { + int binVal = 1; + for (int i = 0; i < precision; i++) { + double mid = (r[0] + r[1]) / 2; + if (value >= mid) { + binVal = binVal << 1; + binVal = binVal | 1; + r[0] = mid; + } else { + binVal = binVal << 1; + r[1] = mid; + } + } + return binVal; + } + + /** + * Compute the longest geohash that contains the envelope + * @param envelope + * @return the geohash as a string + */ + public static String containingGeohash(Envelope2D envelope) { + double posMinX = envelope.xmin + 180; + double posMaxX = envelope.xmax + 180; + double posMinY = envelope.ymin + 90; + double posMaxY = envelope.ymax + 90; + int chars = 0; + double xmin = 0; + double xmax = 0; + double ymin = 0; + double ymax = 0; + double deltaLon = 360; + double deltaLat = 180; + + while (xmin == xmax && ymin == ymax && chars < 7) { + if (chars % 2 == 0) { + deltaLon = deltaLon / 8; + deltaLat = deltaLat / 4; + } else { + deltaLon = deltaLon / 4; + deltaLat = deltaLat / 8; + } + + xmin = Math.floor(posMinX / deltaLon); + xmax = Math.floor(posMaxX / deltaLon); + ymin = Math.floor(posMinY / deltaLat); + ymax = Math.floor(posMaxY / deltaLat); + + chars++; + } + + if (chars == 1) return ""; + + return toGeohash(new Point2D(envelope.xmin, envelope.ymin), chars - 1); + } + + /** + * + * @param envelope + * @return up to four geohashes that completely cover given envelope + */ + public static String[] coveringGeohash(Envelope2D envelope) { + double xmin = envelope.xmin; + double ymin = envelope.ymin; + double xmax = envelope.xmax; + double ymax = envelope.ymax; + + if (NumberUtils.isNaN(xmax)) { + return new String[] {""}; + } + String[] geoHash = {containingGeohash(envelope)}; + if (geoHash[0] != ""){ + return geoHash; + } + + int grid = 45; + int gridMaxLon = (int)Math.floor(xmax/grid); + int gridMinLon = (int)Math.floor(xmin/grid); + int gridMaxLat = (int)Math.floor(ymax/grid); + int gridMinLat = (int)Math.floor(ymin/grid); + int deltaLon = gridMaxLon - gridMinLon + 1; + int deltaLat = gridMaxLat - gridMinLat + 1; + String[] geoHashes = new String[deltaLon * deltaLat]; + + if (deltaLon * deltaLat > 4){ + return new String[] {""}; + } else { + for (int i = 0; i < deltaLon; i++){ + for (int j = 0; j < deltaLat; j++){ + Point2D p = new Point2D(xmin + i * grid, ymin + j * grid); + geoHashes[i*deltaLat + j] = toGeohash(p, 1); + } + } + } + return geoHashes; + } +} diff --git a/src/test/java/com/esri/core/geometry/TestGeohash.java b/src/test/java/com/esri/core/geometry/TestGeohash.java new file mode 100644 index 00000000..ab09d327 --- /dev/null +++ b/src/test/java/com/esri/core/geometry/TestGeohash.java @@ -0,0 +1,182 @@ +package com.esri.core.geometry; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +public class TestGeohash { + + /** + * Check if the center of the new envelope is well placed + */ + @Test + public void testGeohashToEnvelopeWellCentered() { + double delta = 0.00000001; + + String geohash1 = "ghgh"; + + double lat1 = 72.50976563; + double lon1 = -40.60546875; + Envelope2D env1 = Geohash.geohashToEnvelope(geohash1); + double centerX1 = (env1.xmax + env1.xmin) * 0.5; + double centerY1 = (env1.ymax + env1.ymin) * 0.5; + + assertEquals(lon1, centerX1, delta); + assertEquals(lat1, centerY1, delta); + + String geohash2 = "p"; + + double lat2 = -67.50000000; + double lon2 = 157.50000000; + Envelope2D env2 = Geohash.geohashToEnvelope(geohash2); + double centerX2 = (env2.xmax + env2.xmin) * 0.5; + double centerY2 = (env2.ymax + env2.ymin) * 0.5; + + assertEquals(lon2, centerX2, delta); + assertEquals(lat2, centerY2, delta); + } + + /** + * Check if the dimension of the new envelope is correct for low precision + */ + @Test + public void testGeohashToEnvelopeGoodDimensions() { + double delta = 0.00000001; + + double latDiff = 180 / 4; + double lonDiff = 360 / 8; + + String geohash = "h"; + + Envelope2D env = Geohash.geohashToEnvelope(geohash); + + assertEquals(lonDiff, env.xmax - env.xmin, delta); + assertEquals(latDiff, env.ymax - env.ymin, delta); + } + + /** + * Check if the dimension of the new envelope is correct for higher precision + */ + @Test + public void testGeohashToEnvelopeGoodDimensions2() { + double delta = 0.00000001; + + double latDiff = 180.0 / 32768; + double lonDiff = 360.0 / 32768; + + String geohash = "hggggg"; + + Envelope2D env = Geohash.geohashToEnvelope(geohash); + + assertEquals(lonDiff, env.xmax - env.xmin, delta); + assertEquals(latDiff, env.ymax - env.ymin, delta); + } + + @Test + public void testToGeoHash() { + Point2D p0 = new Point2D(0, 0); + + Point2D p1 = new Point2D(-4.329, 48.669); + Point2D p2 = new Point2D(-30.382, 70.273); + Point2D p3 = new Point2D(14.276, 37.691); + Point2D p4 = new Point2D(-143.923, 48.669); + Point2D p5 = new Point2D(-143.923, 48.669); + + int chrLen = 5; + + String p0Hash = Geohash.toGeohash(p0, 1); + + String p1Hash = Geohash.toGeohash(p1, chrLen); + String p2Hash = Geohash.toGeohash(p2, chrLen); + String p3Hash = Geohash.toGeohash(p3, chrLen); + String p4Hash = Geohash.toGeohash(p4, chrLen); + String p5Hash = Geohash.toGeohash(p5, 6); + + assertEquals("s", p0Hash); + assertEquals("gbsuv", p1Hash); + assertEquals("gk6ru", p2Hash); + assertEquals("sqdnk", p3Hash); + assertEquals("bb9su", p4Hash); + assertEquals("bb9sug", p5Hash); + } + + @Test + public void testToGeohashHasGoodPrecision() { + Point2D point = new Point2D(18.068581, 59.329323); + assertEquals(6, Geohash.toGeohash(point, 6).length()); + } + + @Test + public void testToGeohash2() { + String expected = "u6sce"; + Point2D point = new Point2D(18.068581, 59.329323); + String geoHash = Geohash.toGeohash(point, 5); + + assertEquals(expected, geoHash); + } + + @Test + public void testContainingGeohashWithHugeValues() { + Envelope2D envelope = new Envelope2D(-179, -89, 179, 89); + assertEquals("", Geohash.containingGeohash(envelope)); + } + + @Test + public void testContainingGeohash() { + Envelope2D envelope = new Envelope2D(-179, -89, -140, -50); + assertEquals("0", Geohash.containingGeohash(envelope)); + } + + @Test + public void testContainingGeohash2() { + Envelope2D envelope = new Envelope2D(18.078, 59.3564, 18.1, 59.3344); + assertEquals("u6sce", Geohash.containingGeohash(envelope)); + } + + @Test + public void testCoveringGeohashEmptyEnvelope() { + Envelope2D emptyEnv = new Envelope2D(); + String [] coverage = Geohash.coveringGeohash(emptyEnv); + } + + @Test + public void testCoveringGeohashOneGeohash() { + Envelope2D env = new Envelope2D(-180, -90, -149, -49); + String [] coverage = Geohash.coveringGeohash(env); + assertEquals("0", coverage[0]); + } + + @Test + public void testCoveringGeohashPoint() { + Envelope2D env = new Envelope2D(180,90,180,90); + String [] coverage = Geohash.coveringGeohash(env); + assertEquals("zzzzzz", coverage[0]); + } + + @Test + public void testCoveringGeohashTwoGeohashes() { + Envelope2D env = new Envelope2D(-180, -90, -180, -35); + String [] coverage = Geohash.coveringGeohash(env); + assertEquals("0", coverage[0]); + assertEquals("2", coverage[1]); + } + + @Test + public void testCoveringGeohashThreeGeohashes() { + Envelope2D env = new Envelope2D(-180, -90, -180, 5); + String [] coverage = Geohash.coveringGeohash(env); + assertEquals("0", coverage[0]); + assertEquals("2", coverage[1]); + assertEquals("8", coverage[2]); + } + + @Test + public void testCoveringGeohashFourGeohashes() { + Envelope2D env = new Envelope2D(-180, -90, -130, -40); + String [] coverage = Geohash.coveringGeohash(env); + assertEquals("0", coverage[0]); + assertEquals("2", coverage[1]); + assertEquals("1", coverage[2]); + assertEquals("3", coverage[3]); + } +} \ No newline at end of file From 74308108fa43338427fa4f6a220b3d8646f035ea Mon Sep 17 00:00:00 2001 From: Wen Jun Jie <132931864+WenJJ2000@users.noreply.github.com> Date: Mon, 4 Mar 2024 14:35:42 +0100 Subject: [PATCH 06/13] Delete GeoHashPatch.patch --- GeoHashPatch.patch | 447 --------------------------------------------- 1 file changed, 447 deletions(-) delete mode 100644 GeoHashPatch.patch diff --git a/GeoHashPatch.patch b/GeoHashPatch.patch deleted file mode 100644 index 83d41e18..00000000 --- a/GeoHashPatch.patch +++ /dev/null @@ -1,447 +0,0 @@ -diff --git a/src/main/java/com/esri/core/geometry/Geohash.java b/src/main/java/com/esri/core/geometry/Geohash.java -new file mode 100644 -index 0000000..3398720 ---- /dev/null -+++ b/src/main/java/com/esri/core/geometry/Geohash.java -@@ -0,0 +1,252 @@ -+package com.esri.core.geometry; -+ -+import java.security.InvalidParameterException; -+ -+/** -+ * Helper class to work with geohash -+ */ -+public class Geohash { -+ -+ private static final String base32 = "0123456789bcdefghjkmnpqrstuvwxyz"; -+ -+ private static final String INVALID_CHARACTER_MESSAGE = -+ "Invalid character in geohash: "; -+ private static final String GEOHASH_EXCEED_MAX_PRECISION_MESSAGE = -+ "Precision to high in geohash (max 24)"; -+ -+ /** -+ * Create an evelope from a given geohash -+ * @param geoHash -+ * @return The envelope that corresponds to the geohash -+ * @throws InvalidParameterException if the precision of geoHash is greater than 24 characters -+ */ -+ public static Envelope2D geohashToEnvelope(String geoHash) { -+ if (geoHash.length() > 24) { -+ throw new InvalidParameterException(GEOHASH_EXCEED_MAX_PRECISION_MESSAGE); -+ } -+ -+ long latBits = 0; -+ long lonBits = 0; -+ for (int i = 0; i < geoHash.length(); i++) { -+ int pos = base32.indexOf(geoHash.charAt(i)); -+ if (pos == -1) { -+ throw new InvalidParameterException( -+ new StringBuilder(INVALID_CHARACTER_MESSAGE) -+ .append('\'') -+ .append(geoHash.charAt(i)) -+ .append('\'') -+ .toString() -+ ); -+ } -+ -+ if (i % 2 == 0) { -+ lonBits = -+ (lonBits << 3) | ((pos >> 2) & 4) | ((pos >> 1) & 2) | (pos & 1); -+ latBits = (latBits << 2) | ((pos >> 2) & 2) | ((pos >> 1) & 1); -+ } else { -+ latBits = -+ (latBits << 3) | ((pos >> 2) & 4) | ((pos >> 1) & 2) | (pos & 1); -+ lonBits = (lonBits << 2) | ((pos >> 2) & 2) | ((pos >> 1) & 1); -+ } -+ } -+ -+ int lonBitsSize = (int) Math.ceil(geoHash.length() * 5 / 2.0); -+ int latBitsSize = geoHash.length() * 5 - lonBitsSize; -+ -+ double lat = -90; -+ double latPrecision = 90; -+ for (int i = 0; i < latBitsSize; i++) { -+ if (((1 << (latBitsSize - 1 - i)) & latBits) != 0) { -+ lat += latPrecision; -+ } -+ latPrecision /= 2; -+ } -+ -+ double lon = -180; -+ double lonPrecision = 180; -+ for (int i = 0; i < lonBitsSize; i++) { -+ if (((1 << (lonBitsSize - 1 - i)) & lonBits) != 0) { -+ lon += lonPrecision; -+ } -+ lonPrecision /= 2; -+ } -+ -+ return new Envelope2D( -+ lon, -+ lat, -+ lon + lonPrecision * 2, -+ lat + latPrecision * 2 -+ ); -+ } -+ -+ /** -+ * Computes the geohash that contains a point at a certain precision -+ * @param pt A point represented as lat/long pair -+ * @param characterLength - The precision of the geohash -+ * @return The geohash of containing pt as a String -+ */ -+ public static String toGeohash(Point2D pt, int characterLength) { -+ if (characterLength < 1) { -+ throw new InvalidParameterException( -+ "CharacterLength cannot be less than 1" -+ ); -+ } -+ if (characterLength > 6) { -+ throw new InvalidParameterException("Max characterLength of 6"); -+ } -+ int precision = characterLength * 5; -+ double lat = pt.y; -+ double lon = pt.x; -+ long latBit = Geohash.convertToBinary( -+ lat, -+ new double[] { -90, 90 }, -+ precision -+ ); -+ -+ long lonBit = Geohash.convertToBinary( -+ lon, -+ new double[] { -180, 180 }, -+ precision -+ ); -+ -+ long interwovenBin = 1; -+ for (int i = precision - 1; i >= 0; i--) { -+ long currLon = (lonBit >>> i) & 1; -+ long currLat = (latBit >>> i) & 1; -+ interwovenBin <<= 1; -+ interwovenBin |= currLon; -+ interwovenBin <<= 1; -+ interwovenBin |= currLat; -+ } -+ -+ return Geohash -+ .binaryToBase32(interwovenBin, precision * 2) -+ .substring(0, characterLength); -+ } -+ -+ /** -+ * Computes the base32 value of the binary string given -+ * @param binStr (long) Binary number that is to be converted to a base32 string -+ * @param len (int) number of bits -+ * @return base32 string of the binStr in chunks of 5 binary digits -+ */ -+ -+ private static String binaryToBase32(long binStr, int len) { -+ StringBuilder base32Str = new StringBuilder(); -+ -+ for (int i = len - 5; i >= 0; i -= 5) { -+ // Extract a group of 5 bits -+ int group = (int) (binStr >>> i) & 0x1F; -+ -+ // Use the extracted group as an index to fetch the corresponding base32 character -+ base32Str.append(base32.charAt(group)); -+ } -+ -+ return base32Str.toString(); -+ } -+ -+ /** -+ * Converts the value given to a binary string with the given precision and range -+ * @param value (double) The value to be converted to a binary number -+ * @param r (double[]) The range at which the value is to be compared with -+ * @param precision (int) The Precision (number of bits) that the binary number needs -+ * @return (String) A binary number representation of the value with the given range and precision -+ */ -+ -+ private static long convertToBinary(double value, double[] r, int precision) { -+ int binVal = 1; -+ for (int i = 0; i < precision; i++) { -+ double mid = (r[0] + r[1]) / 2; -+ if (value >= mid) { -+ binVal = binVal << 1; -+ binVal = binVal | 1; -+ r[0] = mid; -+ } else { -+ binVal = binVal << 1; -+ r[1] = mid; -+ } -+ } -+ return binVal; -+ } -+ -+ /** -+ * Compute the longest geohash that contains the envelope -+ * @param envelope -+ * @return the geohash as a string -+ */ -+ public static String containingGeohash(Envelope2D envelope) { -+ double posMinX = envelope.xmin + 180; -+ double posMaxX = envelope.xmax + 180; -+ double posMinY = envelope.ymin + 90; -+ double posMaxY = envelope.ymax + 90; -+ int chars = 0; -+ double xmin = 0; -+ double xmax = 0; -+ double ymin = 0; -+ double ymax = 0; -+ double deltaLon = 360; -+ double deltaLat = 180; -+ -+ while (xmin == xmax && ymin == ymax && chars < 7) { -+ if (chars % 2 == 0) { -+ deltaLon = deltaLon / 8; -+ deltaLat = deltaLat / 4; -+ } else { -+ deltaLon = deltaLon / 4; -+ deltaLat = deltaLat / 8; -+ } -+ -+ xmin = Math.floor(posMinX / deltaLon); -+ xmax = Math.floor(posMaxX / deltaLon); -+ ymin = Math.floor(posMinY / deltaLat); -+ ymax = Math.floor(posMaxY / deltaLat); -+ -+ chars++; -+ } -+ -+ if (chars == 1) return ""; -+ -+ return toGeohash(new Point2D(envelope.xmin, envelope.ymin), chars - 1); -+ } -+ -+ /** -+ * -+ * @param envelope -+ * @return up to four geohashes that completely cover given envelope -+ */ -+ public static String[] coveringGeohash(Envelope2D envelope) { -+ double xmin = envelope.xmin; -+ double ymin = envelope.ymin; -+ double xmax = envelope.xmax; -+ double ymax = envelope.ymax; -+ -+ if (NumberUtils.isNaN(xmax)) { -+ return new String[] {""}; -+ } -+ String[] geoHash = {containingGeohash(envelope)}; -+ if (geoHash[0] != ""){ -+ return geoHash; -+ } -+ -+ int grid = 45; -+ int gridMaxLon = (int)Math.floor(xmax/grid); -+ int gridMinLon = (int)Math.floor(xmin/grid); -+ int gridMaxLat = (int)Math.floor(ymax/grid); -+ int gridMinLat = (int)Math.floor(ymin/grid); -+ int deltaLon = gridMaxLon - gridMinLon + 1; -+ int deltaLat = gridMaxLat - gridMinLat + 1; -+ String[] geoHashes = new String[deltaLon * deltaLat]; -+ -+ if (deltaLon * deltaLat > 4){ -+ return new String[] {""}; -+ } else { -+ for (int i = 0; i < deltaLon; i++){ -+ for (int j = 0; j < deltaLat; j++){ -+ Point2D p = new Point2D(xmin + i * grid, ymin + j * grid); -+ geoHashes[i*deltaLat + j] = toGeohash(p, 1); -+ } -+ } -+ } -+ return geoHashes; -+ } -+} -diff --git a/src/test/java/com/esri/core/geometry/TestGeohash.java b/src/test/java/com/esri/core/geometry/TestGeohash.java -new file mode 100644 -index 0000000..ab09d32 ---- /dev/null -+++ b/src/test/java/com/esri/core/geometry/TestGeohash.java -@@ -0,0 +1,182 @@ -+package com.esri.core.geometry; -+ -+import static org.junit.Assert.assertEquals; -+ -+import org.junit.Test; -+ -+public class TestGeohash { -+ -+ /** -+ * Check if the center of the new envelope is well placed -+ */ -+ @Test -+ public void testGeohashToEnvelopeWellCentered() { -+ double delta = 0.00000001; -+ -+ String geohash1 = "ghgh"; -+ -+ double lat1 = 72.50976563; -+ double lon1 = -40.60546875; -+ Envelope2D env1 = Geohash.geohashToEnvelope(geohash1); -+ double centerX1 = (env1.xmax + env1.xmin) * 0.5; -+ double centerY1 = (env1.ymax + env1.ymin) * 0.5; -+ -+ assertEquals(lon1, centerX1, delta); -+ assertEquals(lat1, centerY1, delta); -+ -+ String geohash2 = "p"; -+ -+ double lat2 = -67.50000000; -+ double lon2 = 157.50000000; -+ Envelope2D env2 = Geohash.geohashToEnvelope(geohash2); -+ double centerX2 = (env2.xmax + env2.xmin) * 0.5; -+ double centerY2 = (env2.ymax + env2.ymin) * 0.5; -+ -+ assertEquals(lon2, centerX2, delta); -+ assertEquals(lat2, centerY2, delta); -+ } -+ -+ /** -+ * Check if the dimension of the new envelope is correct for low precision -+ */ -+ @Test -+ public void testGeohashToEnvelopeGoodDimensions() { -+ double delta = 0.00000001; -+ -+ double latDiff = 180 / 4; -+ double lonDiff = 360 / 8; -+ -+ String geohash = "h"; -+ -+ Envelope2D env = Geohash.geohashToEnvelope(geohash); -+ -+ assertEquals(lonDiff, env.xmax - env.xmin, delta); -+ assertEquals(latDiff, env.ymax - env.ymin, delta); -+ } -+ -+ /** -+ * Check if the dimension of the new envelope is correct for higher precision -+ */ -+ @Test -+ public void testGeohashToEnvelopeGoodDimensions2() { -+ double delta = 0.00000001; -+ -+ double latDiff = 180.0 / 32768; -+ double lonDiff = 360.0 / 32768; -+ -+ String geohash = "hggggg"; -+ -+ Envelope2D env = Geohash.geohashToEnvelope(geohash); -+ -+ assertEquals(lonDiff, env.xmax - env.xmin, delta); -+ assertEquals(latDiff, env.ymax - env.ymin, delta); -+ } -+ -+ @Test -+ public void testToGeoHash() { -+ Point2D p0 = new Point2D(0, 0); -+ -+ Point2D p1 = new Point2D(-4.329, 48.669); -+ Point2D p2 = new Point2D(-30.382, 70.273); -+ Point2D p3 = new Point2D(14.276, 37.691); -+ Point2D p4 = new Point2D(-143.923, 48.669); -+ Point2D p5 = new Point2D(-143.923, 48.669); -+ -+ int chrLen = 5; -+ -+ String p0Hash = Geohash.toGeohash(p0, 1); -+ -+ String p1Hash = Geohash.toGeohash(p1, chrLen); -+ String p2Hash = Geohash.toGeohash(p2, chrLen); -+ String p3Hash = Geohash.toGeohash(p3, chrLen); -+ String p4Hash = Geohash.toGeohash(p4, chrLen); -+ String p5Hash = Geohash.toGeohash(p5, 6); -+ -+ assertEquals("s", p0Hash); -+ assertEquals("gbsuv", p1Hash); -+ assertEquals("gk6ru", p2Hash); -+ assertEquals("sqdnk", p3Hash); -+ assertEquals("bb9su", p4Hash); -+ assertEquals("bb9sug", p5Hash); -+ } -+ -+ @Test -+ public void testToGeohashHasGoodPrecision() { -+ Point2D point = new Point2D(18.068581, 59.329323); -+ assertEquals(6, Geohash.toGeohash(point, 6).length()); -+ } -+ -+ @Test -+ public void testToGeohash2() { -+ String expected = "u6sce"; -+ Point2D point = new Point2D(18.068581, 59.329323); -+ String geoHash = Geohash.toGeohash(point, 5); -+ -+ assertEquals(expected, geoHash); -+ } -+ -+ @Test -+ public void testContainingGeohashWithHugeValues() { -+ Envelope2D envelope = new Envelope2D(-179, -89, 179, 89); -+ assertEquals("", Geohash.containingGeohash(envelope)); -+ } -+ -+ @Test -+ public void testContainingGeohash() { -+ Envelope2D envelope = new Envelope2D(-179, -89, -140, -50); -+ assertEquals("0", Geohash.containingGeohash(envelope)); -+ } -+ -+ @Test -+ public void testContainingGeohash2() { -+ Envelope2D envelope = new Envelope2D(18.078, 59.3564, 18.1, 59.3344); -+ assertEquals("u6sce", Geohash.containingGeohash(envelope)); -+ } -+ -+ @Test -+ public void testCoveringGeohashEmptyEnvelope() { -+ Envelope2D emptyEnv = new Envelope2D(); -+ String [] coverage = Geohash.coveringGeohash(emptyEnv); -+ } -+ -+ @Test -+ public void testCoveringGeohashOneGeohash() { -+ Envelope2D env = new Envelope2D(-180, -90, -149, -49); -+ String [] coverage = Geohash.coveringGeohash(env); -+ assertEquals("0", coverage[0]); -+ } -+ -+ @Test -+ public void testCoveringGeohashPoint() { -+ Envelope2D env = new Envelope2D(180,90,180,90); -+ String [] coverage = Geohash.coveringGeohash(env); -+ assertEquals("zzzzzz", coverage[0]); -+ } -+ -+ @Test -+ public void testCoveringGeohashTwoGeohashes() { -+ Envelope2D env = new Envelope2D(-180, -90, -180, -35); -+ String [] coverage = Geohash.coveringGeohash(env); -+ assertEquals("0", coverage[0]); -+ assertEquals("2", coverage[1]); -+ } -+ -+ @Test -+ public void testCoveringGeohashThreeGeohashes() { -+ Envelope2D env = new Envelope2D(-180, -90, -180, 5); -+ String [] coverage = Geohash.coveringGeohash(env); -+ assertEquals("0", coverage[0]); -+ assertEquals("2", coverage[1]); -+ assertEquals("8", coverage[2]); -+ } -+ -+ @Test -+ public void testCoveringGeohashFourGeohashes() { -+ Envelope2D env = new Envelope2D(-180, -90, -130, -40); -+ String [] coverage = Geohash.coveringGeohash(env); -+ assertEquals("0", coverage[0]); -+ assertEquals("2", coverage[1]); -+ assertEquals("1", coverage[2]); -+ assertEquals("3", coverage[3]); -+ } -+} -\ No newline at end of file From a80aa6addc818705a6033405c8ea40a282705c76 Mon Sep 17 00:00:00 2001 From: wenjj2000 Date: Tue, 5 Mar 2024 18:22:13 +0100 Subject: [PATCH 07/13] fix: improved max character length for toGeoHash in Geohash.java --- .../java/com/esri/core/geometry/Geohash.java | 78 ++++++++++--------- .../com/esri/core/geometry/TestGeohash.java | 25 +++--- 2 files changed, 55 insertions(+), 48 deletions(-) diff --git a/src/main/java/com/esri/core/geometry/Geohash.java b/src/main/java/com/esri/core/geometry/Geohash.java index 33987207..f7abbd9c 100644 --- a/src/main/java/com/esri/core/geometry/Geohash.java +++ b/src/main/java/com/esri/core/geometry/Geohash.java @@ -91,10 +91,10 @@ public static String toGeohash(Point2D pt, int characterLength) { "CharacterLength cannot be less than 1" ); } - if (characterLength > 6) { - throw new InvalidParameterException("Max characterLength of 6"); + if (characterLength > 25) { + throw new InvalidParameterException("Max characterLength of 25"); } - int precision = characterLength * 5; + int precision = 63; double lat = pt.y; double lon = pt.x; long latBit = Geohash.convertToBinary( @@ -109,37 +109,43 @@ public static String toGeohash(Point2D pt, int characterLength) { precision ); - long interwovenBin = 1; - for (int i = precision - 1; i >= 0; i--) { - long currLon = (lonBit >>> i) & 1; - long currLat = (latBit >>> i) & 1; - interwovenBin <<= 1; - interwovenBin |= currLon; - interwovenBin <<= 1; - interwovenBin |= currLat; - } - return Geohash - .binaryToBase32(interwovenBin, precision * 2) + .binaryToBase32(lonBit, latBit, precision) .substring(0, characterLength); } /** * Computes the base32 value of the binary string given - * @param binStr (long) Binary number that is to be converted to a base32 string + * @param lonBits (long) longtitude bits + * @param latBits (long) latitude bits * @param len (int) number of bits * @return base32 string of the binStr in chunks of 5 binary digits */ - private static String binaryToBase32(long binStr, int len) { + private static String binaryToBase32(long lonBits, long latBits, int len) { StringBuilder base32Str = new StringBuilder(); + int i = len - 1; + long curr = 1; + int currLen = 0; + while (i >= 0) { + long currLon = (lonBits >>> i) & 1; + long currLat = (latBits >>> i) & 1; + if (currLen >= 5) { + base32Str.append(base32.charAt((int) (curr & 0x1F))); + curr = 1; + currLen = 0; + } + curr = (curr << 1) | currLon; + currLen++; + if (currLen >= 5) { + base32Str.append(base32.charAt((int) (curr & 0x1F))); + curr = 1; + currLen = 0; + } + curr = (curr << 1) | currLat; + currLen++; - for (int i = len - 5; i >= 0; i -= 5) { - // Extract a group of 5 bits - int group = (int) (binStr >>> i) & 0x1F; - - // Use the extracted group as an index to fetch the corresponding base32 character - base32Str.append(base32.charAt(group)); + i--; } return base32Str.toString(); @@ -150,11 +156,11 @@ private static String binaryToBase32(long binStr, int len) { * @param value (double) The value to be converted to a binary number * @param r (double[]) The range at which the value is to be compared with * @param precision (int) The Precision (number of bits) that the binary number needs - * @return (String) A binary number representation of the value with the given range and precision + * @return (long) A binary number of the value with the given range and precision */ private static long convertToBinary(double value, double[] r, int precision) { - int binVal = 1; + long binVal = 1; for (int i = 0; i < precision; i++) { double mid = (r[0] + r[1]) / 2; if (value >= mid) { @@ -221,29 +227,29 @@ public static String[] coveringGeohash(Envelope2D envelope) { double ymax = envelope.ymax; if (NumberUtils.isNaN(xmax)) { - return new String[] {""}; + return new String[] { "" }; } - String[] geoHash = {containingGeohash(envelope)}; - if (geoHash[0] != ""){ + String[] geoHash = { containingGeohash(envelope) }; + if (geoHash[0] != "") { return geoHash; } int grid = 45; - int gridMaxLon = (int)Math.floor(xmax/grid); - int gridMinLon = (int)Math.floor(xmin/grid); - int gridMaxLat = (int)Math.floor(ymax/grid); - int gridMinLat = (int)Math.floor(ymin/grid); + int gridMaxLon = (int) Math.floor(xmax / grid); + int gridMinLon = (int) Math.floor(xmin / grid); + int gridMaxLat = (int) Math.floor(ymax / grid); + int gridMinLat = (int) Math.floor(ymin / grid); int deltaLon = gridMaxLon - gridMinLon + 1; int deltaLat = gridMaxLat - gridMinLat + 1; String[] geoHashes = new String[deltaLon * deltaLat]; - if (deltaLon * deltaLat > 4){ - return new String[] {""}; + if (deltaLon * deltaLat > 4) { + return new String[] { "" }; } else { - for (int i = 0; i < deltaLon; i++){ - for (int j = 0; j < deltaLat; j++){ + for (int i = 0; i < deltaLon; i++) { + for (int j = 0; j < deltaLat; j++) { Point2D p = new Point2D(xmin + i * grid, ymin + j * grid); - geoHashes[i*deltaLat + j] = toGeohash(p, 1); + geoHashes[i * deltaLat + j] = toGeohash(p, 1); } } } diff --git a/src/test/java/com/esri/core/geometry/TestGeohash.java b/src/test/java/com/esri/core/geometry/TestGeohash.java index ab09d327..5e322254 100644 --- a/src/test/java/com/esri/core/geometry/TestGeohash.java +++ b/src/test/java/com/esri/core/geometry/TestGeohash.java @@ -85,12 +85,12 @@ public void testToGeoHash() { int chrLen = 5; String p0Hash = Geohash.toGeohash(p0, 1); - String p1Hash = Geohash.toGeohash(p1, chrLen); String p2Hash = Geohash.toGeohash(p2, chrLen); String p3Hash = Geohash.toGeohash(p3, chrLen); String p4Hash = Geohash.toGeohash(p4, chrLen); String p5Hash = Geohash.toGeohash(p5, 6); + String p6Hash = Geohash.toGeohash(p5, 25); assertEquals("s", p0Hash); assertEquals("gbsuv", p1Hash); @@ -98,6 +98,7 @@ public void testToGeoHash() { assertEquals("sqdnk", p3Hash); assertEquals("bb9su", p4Hash); assertEquals("bb9sug", p5Hash); + assertEquals("bb9sugymrp0vwb2kzfsq1mzzz", p6Hash); } @Test @@ -129,34 +130,34 @@ public void testContainingGeohash() { @Test public void testContainingGeohash2() { - Envelope2D envelope = new Envelope2D(18.078, 59.3564, 18.1, 59.3344); - assertEquals("u6sce", Geohash.containingGeohash(envelope)); + Envelope2D envelope = new Envelope2D(18.078, 59.3564, 18.1, 59.3344); + assertEquals("u6sce", Geohash.containingGeohash(envelope)); } - + @Test public void testCoveringGeohashEmptyEnvelope() { Envelope2D emptyEnv = new Envelope2D(); - String [] coverage = Geohash.coveringGeohash(emptyEnv); + String[] coverage = Geohash.coveringGeohash(emptyEnv); } @Test public void testCoveringGeohashOneGeohash() { Envelope2D env = new Envelope2D(-180, -90, -149, -49); - String [] coverage = Geohash.coveringGeohash(env); + String[] coverage = Geohash.coveringGeohash(env); assertEquals("0", coverage[0]); } @Test public void testCoveringGeohashPoint() { - Envelope2D env = new Envelope2D(180,90,180,90); - String [] coverage = Geohash.coveringGeohash(env); + Envelope2D env = new Envelope2D(180, 90, 180, 90); + String[] coverage = Geohash.coveringGeohash(env); assertEquals("zzzzzz", coverage[0]); } @Test public void testCoveringGeohashTwoGeohashes() { Envelope2D env = new Envelope2D(-180, -90, -180, -35); - String [] coverage = Geohash.coveringGeohash(env); + String[] coverage = Geohash.coveringGeohash(env); assertEquals("0", coverage[0]); assertEquals("2", coverage[1]); } @@ -164,7 +165,7 @@ public void testCoveringGeohashTwoGeohashes() { @Test public void testCoveringGeohashThreeGeohashes() { Envelope2D env = new Envelope2D(-180, -90, -180, 5); - String [] coverage = Geohash.coveringGeohash(env); + String[] coverage = Geohash.coveringGeohash(env); assertEquals("0", coverage[0]); assertEquals("2", coverage[1]); assertEquals("8", coverage[2]); @@ -173,10 +174,10 @@ public void testCoveringGeohashThreeGeohashes() { @Test public void testCoveringGeohashFourGeohashes() { Envelope2D env = new Envelope2D(-180, -90, -130, -40); - String [] coverage = Geohash.coveringGeohash(env); + String[] coverage = Geohash.coveringGeohash(env); assertEquals("0", coverage[0]); assertEquals("2", coverage[1]); assertEquals("1", coverage[2]); assertEquals("3", coverage[3]); } -} \ No newline at end of file +} From 6bb6b6d224cb3fc72ab8b0a018ebb203fdd7282e Mon Sep 17 00:00:00 2001 From: wenjj2000 Date: Tue, 5 Mar 2024 18:47:35 +0100 Subject: [PATCH 08/13] style : change spaces in files to tabs --- .../java/com/esri/core/geometry/Geohash.java | 449 +++++++++--------- .../com/esri/core/geometry/TestGeohash.java | 358 +++++++------- 2 files changed, 404 insertions(+), 403 deletions(-) diff --git a/src/main/java/com/esri/core/geometry/Geohash.java b/src/main/java/com/esri/core/geometry/Geohash.java index f7abbd9c..09874c8c 100644 --- a/src/main/java/com/esri/core/geometry/Geohash.java +++ b/src/main/java/com/esri/core/geometry/Geohash.java @@ -1,258 +1,259 @@ -package com.esri.core.geometry; + package com.esri.core.geometry; -import java.security.InvalidParameterException; + import java.security.InvalidParameterException; -/** - * Helper class to work with geohash - */ -public class Geohash { + /** + * Helper class to work with geohash + */ + public class Geohash { - private static final String base32 = "0123456789bcdefghjkmnpqrstuvwxyz"; + private static final String base32 = "0123456789bcdefghjkmnpqrstuvwxyz"; - private static final String INVALID_CHARACTER_MESSAGE = - "Invalid character in geohash: "; - private static final String GEOHASH_EXCEED_MAX_PRECISION_MESSAGE = - "Precision to high in geohash (max 24)"; + private static final String INVALID_CHARACTER_MESSAGE = + "Invalid character in geohash: "; + private static final String GEOHASH_EXCEED_MAX_PRECISION_MESSAGE = + "Precision to high in geohash (max 24)"; - /** - * Create an evelope from a given geohash - * @param geoHash - * @return The envelope that corresponds to the geohash - * @throws InvalidParameterException if the precision of geoHash is greater than 24 characters - */ - public static Envelope2D geohashToEnvelope(String geoHash) { - if (geoHash.length() > 24) { - throw new InvalidParameterException(GEOHASH_EXCEED_MAX_PRECISION_MESSAGE); - } - - long latBits = 0; - long lonBits = 0; - for (int i = 0; i < geoHash.length(); i++) { - int pos = base32.indexOf(geoHash.charAt(i)); - if (pos == -1) { - throw new InvalidParameterException( - new StringBuilder(INVALID_CHARACTER_MESSAGE) - .append('\'') - .append(geoHash.charAt(i)) - .append('\'') - .toString() - ); - } + /** + * Create an evelope from a given geohash + * @param geoHash + * @return The envelope that corresponds to the geohash + * @throws InvalidParameterException if the precision of geoHash is greater than 24 characters + */ + public static Envelope2D geohashToEnvelope(String geoHash) { + if (geoHash.length() > 24) { + throw new InvalidParameterException(GEOHASH_EXCEED_MAX_PRECISION_MESSAGE); + } - if (i % 2 == 0) { - lonBits = - (lonBits << 3) | ((pos >> 2) & 4) | ((pos >> 1) & 2) | (pos & 1); - latBits = (latBits << 2) | ((pos >> 2) & 2) | ((pos >> 1) & 1); - } else { - latBits = - (latBits << 3) | ((pos >> 2) & 4) | ((pos >> 1) & 2) | (pos & 1); - lonBits = (lonBits << 2) | ((pos >> 2) & 2) | ((pos >> 1) & 1); - } - } + long latBits = 0; + long lonBits = 0; + for (int i = 0; i < geoHash.length(); i++) { + int pos = base32.indexOf(geoHash.charAt(i)); + if (pos == -1) { + throw new InvalidParameterException( + new StringBuilder(INVALID_CHARACTER_MESSAGE) + .append('\'') + .append(geoHash.charAt(i)) + .append('\'') + .toString() + ); + } - int lonBitsSize = (int) Math.ceil(geoHash.length() * 5 / 2.0); - int latBitsSize = geoHash.length() * 5 - lonBitsSize; + if (i % 2 == 0) { + lonBits = + (lonBits << 3) | ((pos >> 2) & 4) | ((pos >> 1) & 2) | (pos & 1); + latBits = (latBits << 2) | ((pos >> 2) & 2) | ((pos >> 1) & 1); + } else { + latBits = + (latBits << 3) | ((pos >> 2) & 4) | ((pos >> 1) & 2) | (pos & 1); + lonBits = (lonBits << 2) | ((pos >> 2) & 2) | ((pos >> 1) & 1); + } + } - double lat = -90; - double latPrecision = 90; - for (int i = 0; i < latBitsSize; i++) { - if (((1 << (latBitsSize - 1 - i)) & latBits) != 0) { - lat += latPrecision; - } - latPrecision /= 2; - } + int lonBitsSize = (int) Math.ceil(geoHash.length() * 5 / 2.0); + int latBitsSize = geoHash.length() * 5 - lonBitsSize; - double lon = -180; - double lonPrecision = 180; - for (int i = 0; i < lonBitsSize; i++) { - if (((1 << (lonBitsSize - 1 - i)) & lonBits) != 0) { - lon += lonPrecision; - } - lonPrecision /= 2; - } + double lat = -90; + double latPrecision = 90; + for (int i = 0; i < latBitsSize; i++) { + if (((1 << (latBitsSize - 1 - i)) & latBits) != 0) { + lat += latPrecision; + } + latPrecision /= 2; + } - return new Envelope2D( - lon, - lat, - lon + lonPrecision * 2, - lat + latPrecision * 2 - ); - } + double lon = -180; + double lonPrecision = 180; + for (int i = 0; i < lonBitsSize; i++) { + if (((1 << (lonBitsSize - 1 - i)) & lonBits) != 0) { + lon += lonPrecision; + } + lonPrecision /= 2; + } - /** - * Computes the geohash that contains a point at a certain precision - * @param pt A point represented as lat/long pair - * @param characterLength - The precision of the geohash - * @return The geohash of containing pt as a String - */ - public static String toGeohash(Point2D pt, int characterLength) { - if (characterLength < 1) { - throw new InvalidParameterException( - "CharacterLength cannot be less than 1" - ); - } - if (characterLength > 25) { - throw new InvalidParameterException("Max characterLength of 25"); + return new Envelope2D( + lon, + lat, + lon + lonPrecision * 2, + lat + latPrecision * 2 + ); } - int precision = 63; - double lat = pt.y; - double lon = pt.x; - long latBit = Geohash.convertToBinary( - lat, - new double[] { -90, 90 }, - precision - ); - - long lonBit = Geohash.convertToBinary( - lon, - new double[] { -180, 180 }, - precision - ); - return Geohash - .binaryToBase32(lonBit, latBit, precision) - .substring(0, characterLength); - } + /** + * Computes the geohash that contains a point at a certain precision + * @param pt A point represented as lat/long pair + * @param characterLength - The precision of the geohash + * @return The geohash of containing pt as a String + */ + public static String toGeohash(Point2D pt, int characterLength) { + if (characterLength < 1) { + throw new InvalidParameterException( + "CharacterLength cannot be less than 1" + ); + } + if (characterLength > 25) { + throw new InvalidParameterException("Max characterLength of 25"); + } - /** - * Computes the base32 value of the binary string given - * @param lonBits (long) longtitude bits - * @param latBits (long) latitude bits - * @param len (int) number of bits - * @return base32 string of the binStr in chunks of 5 binary digits - */ + int precision = 63; + double lat = pt.y; + double lon = pt.x; + long latBit = Geohash.convertToBinary( + lat, + new double[] { -90, 90 }, + precision + ); - private static String binaryToBase32(long lonBits, long latBits, int len) { - StringBuilder base32Str = new StringBuilder(); - int i = len - 1; - long curr = 1; - int currLen = 0; - while (i >= 0) { - long currLon = (lonBits >>> i) & 1; - long currLat = (latBits >>> i) & 1; - if (currLen >= 5) { - base32Str.append(base32.charAt((int) (curr & 0x1F))); - curr = 1; - currLen = 0; - } - curr = (curr << 1) | currLon; - currLen++; - if (currLen >= 5) { - base32Str.append(base32.charAt((int) (curr & 0x1F))); - curr = 1; - currLen = 0; - } - curr = (curr << 1) | currLat; - currLen++; + long lonBit = Geohash.convertToBinary( + lon, + new double[] { -180, 180 }, + precision + ); - i--; + return Geohash + .binaryToBase32(lonBit, latBit, precision) + .substring(0, characterLength); } - return base32Str.toString(); - } + /** + * Computes the base32 value of the binary string given + * @param lonBits (long) longtitude bits + * @param latBits (long) latitude bits + * @param len (int) number of bits + * @return base32 string of the binStr in chunks of 5 binary digits + */ + + private static String binaryToBase32(long lonBits, long latBits, int len) { + StringBuilder base32Str = new StringBuilder(); + int i = len - 1; + long curr = 1; + int currLen = 0; + while (i >= 0) { + long currLon = (lonBits >>> i) & 1; + long currLat = (latBits >>> i) & 1; + if (currLen >= 5) { + base32Str.append(base32.charAt((int) (curr & 0x1F))); + curr = 1; + currLen = 0; + } + curr = (curr << 1) | currLon; + currLen++; + if (currLen >= 5) { + base32Str.append(base32.charAt((int) (curr & 0x1F))); + curr = 1; + currLen = 0; + } + curr = (curr << 1) | currLat; + currLen++; - /** - * Converts the value given to a binary string with the given precision and range - * @param value (double) The value to be converted to a binary number - * @param r (double[]) The range at which the value is to be compared with - * @param precision (int) The Precision (number of bits) that the binary number needs - * @return (long) A binary number of the value with the given range and precision - */ + i--; + } - private static long convertToBinary(double value, double[] r, int precision) { - long binVal = 1; - for (int i = 0; i < precision; i++) { - double mid = (r[0] + r[1]) / 2; - if (value >= mid) { - binVal = binVal << 1; - binVal = binVal | 1; - r[0] = mid; - } else { - binVal = binVal << 1; - r[1] = mid; - } + return base32Str.toString(); } - return binVal; - } - /** - * Compute the longest geohash that contains the envelope - * @param envelope - * @return the geohash as a string - */ - public static String containingGeohash(Envelope2D envelope) { - double posMinX = envelope.xmin + 180; - double posMaxX = envelope.xmax + 180; - double posMinY = envelope.ymin + 90; - double posMaxY = envelope.ymax + 90; - int chars = 0; - double xmin = 0; - double xmax = 0; - double ymin = 0; - double ymax = 0; - double deltaLon = 360; - double deltaLat = 180; + /** + * Converts the value given to a binary string with the given precision and range + * @param value (double) The value to be converted to a binary number + * @param r (double[]) The range at which the value is to be compared with + * @param precision (int) The Precision (number of bits) that the binary number needs + * @return (long) A binary number of the value with the given range and precision + */ - while (xmin == xmax && ymin == ymax && chars < 7) { - if (chars % 2 == 0) { - deltaLon = deltaLon / 8; - deltaLat = deltaLat / 4; - } else { - deltaLon = deltaLon / 4; - deltaLat = deltaLat / 8; - } + private static long convertToBinary(double value, double[] r, int precision) { + long binVal = 1; + for (int i = 0; i < precision; i++) { + double mid = (r[0] + r[1]) / 2; + if (value >= mid) { + binVal = binVal << 1; + binVal = binVal | 1; + r[0] = mid; + } else { + binVal = binVal << 1; + r[1] = mid; + } + } + return binVal; + } - xmin = Math.floor(posMinX / deltaLon); - xmax = Math.floor(posMaxX / deltaLon); - ymin = Math.floor(posMinY / deltaLat); - ymax = Math.floor(posMaxY / deltaLat); + /** + * Compute the longest geohash that contains the envelope + * @param envelope + * @return the geohash as a string + */ + public static String containingGeohash(Envelope2D envelope) { + double posMinX = envelope.xmin + 180; + double posMaxX = envelope.xmax + 180; + double posMinY = envelope.ymin + 90; + double posMaxY = envelope.ymax + 90; + int chars = 0; + double xmin = 0; + double xmax = 0; + double ymin = 0; + double ymax = 0; + double deltaLon = 360; + double deltaLat = 180; - chars++; - } + while (xmin == xmax && ymin == ymax && chars < 7) { + if (chars % 2 == 0) { + deltaLon = deltaLon / 8; + deltaLat = deltaLat / 4; + } else { + deltaLon = deltaLon / 4; + deltaLat = deltaLat / 8; + } - if (chars == 1) return ""; + xmin = Math.floor(posMinX / deltaLon); + xmax = Math.floor(posMaxX / deltaLon); + ymin = Math.floor(posMinY / deltaLat); + ymax = Math.floor(posMaxY / deltaLat); - return toGeohash(new Point2D(envelope.xmin, envelope.ymin), chars - 1); - } + chars++; + } - /** - * - * @param envelope - * @return up to four geohashes that completely cover given envelope - */ - public static String[] coveringGeohash(Envelope2D envelope) { - double xmin = envelope.xmin; - double ymin = envelope.ymin; - double xmax = envelope.xmax; - double ymax = envelope.ymax; + if (chars == 1) return ""; - if (NumberUtils.isNaN(xmax)) { - return new String[] { "" }; - } - String[] geoHash = { containingGeohash(envelope) }; - if (geoHash[0] != "") { - return geoHash; + return toGeohash(new Point2D(envelope.xmin, envelope.ymin), chars - 1); } - int grid = 45; - int gridMaxLon = (int) Math.floor(xmax / grid); - int gridMinLon = (int) Math.floor(xmin / grid); - int gridMaxLat = (int) Math.floor(ymax / grid); - int gridMinLat = (int) Math.floor(ymin / grid); - int deltaLon = gridMaxLon - gridMinLon + 1; - int deltaLat = gridMaxLat - gridMinLat + 1; - String[] geoHashes = new String[deltaLon * deltaLat]; + /** + * + * @param envelope + * @return up to four geohashes that completely cover given envelope + */ + public static String[] coveringGeohash(Envelope2D envelope) { + double xmin = envelope.xmin; + double ymin = envelope.ymin; + double xmax = envelope.xmax; + double ymax = envelope.ymax; + + if (NumberUtils.isNaN(xmax)) { + return new String[] { "" }; + } + String[] geoHash = { containingGeohash(envelope) }; + if (geoHash[0] != "") { + return geoHash; + } + + int grid = 45; + int gridMaxLon = (int) Math.floor(xmax / grid); + int gridMinLon = (int) Math.floor(xmin / grid); + int gridMaxLat = (int) Math.floor(ymax / grid); + int gridMinLat = (int) Math.floor(ymin / grid); + int deltaLon = gridMaxLon - gridMinLon + 1; + int deltaLat = gridMaxLat - gridMinLat + 1; + String[] geoHashes = new String[deltaLon * deltaLat]; - if (deltaLon * deltaLat > 4) { - return new String[] { "" }; - } else { - for (int i = 0; i < deltaLon; i++) { - for (int j = 0; j < deltaLat; j++) { - Point2D p = new Point2D(xmin + i * grid, ymin + j * grid); - geoHashes[i * deltaLat + j] = toGeohash(p, 1); + if (deltaLon * deltaLat > 4) { + return new String[] { "" }; + } else { + for (int i = 0; i < deltaLon; i++) { + for (int j = 0; j < deltaLat; j++) { + Point2D p = new Point2D(xmin + i * grid, ymin + j * grid); + geoHashes[i * deltaLat + j] = toGeohash(p, 1); + } } - } + } + return geoHashes; + } } - return geoHashes; - } -} diff --git a/src/test/java/com/esri/core/geometry/TestGeohash.java b/src/test/java/com/esri/core/geometry/TestGeohash.java index 5e322254..8b2abc33 100644 --- a/src/test/java/com/esri/core/geometry/TestGeohash.java +++ b/src/test/java/com/esri/core/geometry/TestGeohash.java @@ -1,183 +1,183 @@ -package com.esri.core.geometry; + package com.esri.core.geometry; -import static org.junit.Assert.assertEquals; + import static org.junit.Assert.assertEquals; -import org.junit.Test; + import org.junit.Test; -public class TestGeohash { - - /** - * Check if the center of the new envelope is well placed - */ - @Test - public void testGeohashToEnvelopeWellCentered() { - double delta = 0.00000001; + public class TestGeohash { + + /** + * Check if the center of the new envelope is well placed + */ + @Test + public void testGeohashToEnvelopeWellCentered() { + double delta = 0.00000001; - String geohash1 = "ghgh"; - - double lat1 = 72.50976563; - double lon1 = -40.60546875; - Envelope2D env1 = Geohash.geohashToEnvelope(geohash1); - double centerX1 = (env1.xmax + env1.xmin) * 0.5; - double centerY1 = (env1.ymax + env1.ymin) * 0.5; - - assertEquals(lon1, centerX1, delta); - assertEquals(lat1, centerY1, delta); - - String geohash2 = "p"; - - double lat2 = -67.50000000; - double lon2 = 157.50000000; - Envelope2D env2 = Geohash.geohashToEnvelope(geohash2); - double centerX2 = (env2.xmax + env2.xmin) * 0.5; - double centerY2 = (env2.ymax + env2.ymin) * 0.5; - - assertEquals(lon2, centerX2, delta); - assertEquals(lat2, centerY2, delta); - } - - /** - * Check if the dimension of the new envelope is correct for low precision - */ - @Test - public void testGeohashToEnvelopeGoodDimensions() { - double delta = 0.00000001; - - double latDiff = 180 / 4; - double lonDiff = 360 / 8; - - String geohash = "h"; - - Envelope2D env = Geohash.geohashToEnvelope(geohash); - - assertEquals(lonDiff, env.xmax - env.xmin, delta); - assertEquals(latDiff, env.ymax - env.ymin, delta); - } - - /** - * Check if the dimension of the new envelope is correct for higher precision - */ - @Test - public void testGeohashToEnvelopeGoodDimensions2() { - double delta = 0.00000001; - - double latDiff = 180.0 / 32768; - double lonDiff = 360.0 / 32768; - - String geohash = "hggggg"; - - Envelope2D env = Geohash.geohashToEnvelope(geohash); - - assertEquals(lonDiff, env.xmax - env.xmin, delta); - assertEquals(latDiff, env.ymax - env.ymin, delta); - } - - @Test - public void testToGeoHash() { - Point2D p0 = new Point2D(0, 0); - - Point2D p1 = new Point2D(-4.329, 48.669); - Point2D p2 = new Point2D(-30.382, 70.273); - Point2D p3 = new Point2D(14.276, 37.691); - Point2D p4 = new Point2D(-143.923, 48.669); - Point2D p5 = new Point2D(-143.923, 48.669); - - int chrLen = 5; - - String p0Hash = Geohash.toGeohash(p0, 1); - String p1Hash = Geohash.toGeohash(p1, chrLen); - String p2Hash = Geohash.toGeohash(p2, chrLen); - String p3Hash = Geohash.toGeohash(p3, chrLen); - String p4Hash = Geohash.toGeohash(p4, chrLen); - String p5Hash = Geohash.toGeohash(p5, 6); - String p6Hash = Geohash.toGeohash(p5, 25); - - assertEquals("s", p0Hash); - assertEquals("gbsuv", p1Hash); - assertEquals("gk6ru", p2Hash); - assertEquals("sqdnk", p3Hash); - assertEquals("bb9su", p4Hash); - assertEquals("bb9sug", p5Hash); - assertEquals("bb9sugymrp0vwb2kzfsq1mzzz", p6Hash); - } - - @Test - public void testToGeohashHasGoodPrecision() { - Point2D point = new Point2D(18.068581, 59.329323); - assertEquals(6, Geohash.toGeohash(point, 6).length()); - } - - @Test - public void testToGeohash2() { - String expected = "u6sce"; - Point2D point = new Point2D(18.068581, 59.329323); - String geoHash = Geohash.toGeohash(point, 5); - - assertEquals(expected, geoHash); - } - - @Test - public void testContainingGeohashWithHugeValues() { - Envelope2D envelope = new Envelope2D(-179, -89, 179, 89); - assertEquals("", Geohash.containingGeohash(envelope)); - } - - @Test - public void testContainingGeohash() { - Envelope2D envelope = new Envelope2D(-179, -89, -140, -50); - assertEquals("0", Geohash.containingGeohash(envelope)); - } - - @Test - public void testContainingGeohash2() { - Envelope2D envelope = new Envelope2D(18.078, 59.3564, 18.1, 59.3344); - assertEquals("u6sce", Geohash.containingGeohash(envelope)); - } - - @Test - public void testCoveringGeohashEmptyEnvelope() { - Envelope2D emptyEnv = new Envelope2D(); - String[] coverage = Geohash.coveringGeohash(emptyEnv); - } - - @Test - public void testCoveringGeohashOneGeohash() { - Envelope2D env = new Envelope2D(-180, -90, -149, -49); - String[] coverage = Geohash.coveringGeohash(env); - assertEquals("0", coverage[0]); - } - - @Test - public void testCoveringGeohashPoint() { - Envelope2D env = new Envelope2D(180, 90, 180, 90); - String[] coverage = Geohash.coveringGeohash(env); - assertEquals("zzzzzz", coverage[0]); - } - - @Test - public void testCoveringGeohashTwoGeohashes() { - Envelope2D env = new Envelope2D(-180, -90, -180, -35); - String[] coverage = Geohash.coveringGeohash(env); - assertEquals("0", coverage[0]); - assertEquals("2", coverage[1]); - } - - @Test - public void testCoveringGeohashThreeGeohashes() { - Envelope2D env = new Envelope2D(-180, -90, -180, 5); - String[] coverage = Geohash.coveringGeohash(env); - assertEquals("0", coverage[0]); - assertEquals("2", coverage[1]); - assertEquals("8", coverage[2]); - } - - @Test - public void testCoveringGeohashFourGeohashes() { - Envelope2D env = new Envelope2D(-180, -90, -130, -40); - String[] coverage = Geohash.coveringGeohash(env); - assertEquals("0", coverage[0]); - assertEquals("2", coverage[1]); - assertEquals("1", coverage[2]); - assertEquals("3", coverage[3]); - } -} + String geohash1 = "ghgh"; + + double lat1 = 72.50976563; + double lon1 = -40.60546875; + Envelope2D env1 = Geohash.geohashToEnvelope(geohash1); + double centerX1 = (env1.xmax + env1.xmin) * 0.5; + double centerY1 = (env1.ymax + env1.ymin) * 0.5; + + assertEquals(lon1, centerX1, delta); + assertEquals(lat1, centerY1, delta); + + String geohash2 = "p"; + + double lat2 = -67.50000000; + double lon2 = 157.50000000; + Envelope2D env2 = Geohash.geohashToEnvelope(geohash2); + double centerX2 = (env2.xmax + env2.xmin) * 0.5; + double centerY2 = (env2.ymax + env2.ymin) * 0.5; + + assertEquals(lon2, centerX2, delta); + assertEquals(lat2, centerY2, delta); + } + + /** + * Check if the dimension of the new envelope is correct for low precision + */ + @Test + public void testGeohashToEnvelopeGoodDimensions() { + double delta = 0.00000001; + + double latDiff = 180 / 4; + double lonDiff = 360 / 8; + + String geohash = "h"; + + Envelope2D env = Geohash.geohashToEnvelope(geohash); + + assertEquals(lonDiff, env.xmax - env.xmin, delta); + assertEquals(latDiff, env.ymax - env.ymin, delta); + } + + /** + * Check if the dimension of the new envelope is correct for higher precision + */ + @Test + public void testGeohashToEnvelopeGoodDimensions2() { + double delta = 0.00000001; + + double latDiff = 180.0 / 32768; + double lonDiff = 360.0 / 32768; + + String geohash = "hggggg"; + + Envelope2D env = Geohash.geohashToEnvelope(geohash); + + assertEquals(lonDiff, env.xmax - env.xmin, delta); + assertEquals(latDiff, env.ymax - env.ymin, delta); + } + + @Test + public void testToGeoHash() { + Point2D p0 = new Point2D(0, 0); + + Point2D p1 = new Point2D(-4.329, 48.669); + Point2D p2 = new Point2D(-30.382, 70.273); + Point2D p3 = new Point2D(14.276, 37.691); + Point2D p4 = new Point2D(-143.923, 48.669); + Point2D p5 = new Point2D(-143.923, 48.669); + + int chrLen = 5; + + String p0Hash = Geohash.toGeohash(p0, 1); + String p1Hash = Geohash.toGeohash(p1, chrLen); + String p2Hash = Geohash.toGeohash(p2, chrLen); + String p3Hash = Geohash.toGeohash(p3, chrLen); + String p4Hash = Geohash.toGeohash(p4, chrLen); + String p5Hash = Geohash.toGeohash(p5, 6); + String p6Hash = Geohash.toGeohash(p5, 25); + + assertEquals("s", p0Hash); + assertEquals("gbsuv", p1Hash); + assertEquals("gk6ru", p2Hash); + assertEquals("sqdnk", p3Hash); + assertEquals("bb9su", p4Hash); + assertEquals("bb9sug", p5Hash); + assertEquals("bb9sugymrp0vwb2kzfsq1mzzz", p6Hash); + } + + @Test + public void testToGeohashHasGoodPrecision() { + Point2D point = new Point2D(18.068581, 59.329323); + assertEquals(6, Geohash.toGeohash(point, 6).length()); + } + + @Test + public void testToGeohash2() { + String expected = "u6sce"; + Point2D point = new Point2D(18.068581, 59.329323); + String geoHash = Geohash.toGeohash(point, 5); + + assertEquals(expected, geoHash); + } + + @Test + public void testContainingGeohashWithHugeValues() { + Envelope2D envelope = new Envelope2D(-179, -89, 179, 89); + assertEquals("", Geohash.containingGeohash(envelope)); + } + + @Test + public void testContainingGeohash() { + Envelope2D envelope = new Envelope2D(-179, -89, -140, -50); + assertEquals("0", Geohash.containingGeohash(envelope)); + } + + @Test + public void testContainingGeohash2() { + Envelope2D envelope = new Envelope2D(18.078, 59.3564, 18.1, 59.3344); + assertEquals("u6sce", Geohash.containingGeohash(envelope)); + } + + @Test + public void testCoveringGeohashEmptyEnvelope() { + Envelope2D emptyEnv = new Envelope2D(); + String[] coverage = Geohash.coveringGeohash(emptyEnv); + } + + @Test + public void testCoveringGeohashOneGeohash() { + Envelope2D env = new Envelope2D(-180, -90, -149, -49); + String[] coverage = Geohash.coveringGeohash(env); + assertEquals("0", coverage[0]); + } + + @Test + public void testCoveringGeohashPoint() { + Envelope2D env = new Envelope2D(180, 90, 180, 90); + String[] coverage = Geohash.coveringGeohash(env); + assertEquals("zzzzzz", coverage[0]); + } + + @Test + public void testCoveringGeohashTwoGeohashes() { + Envelope2D env = new Envelope2D(-180, -90, -180, -35); + String[] coverage = Geohash.coveringGeohash(env); + assertEquals("0", coverage[0]); + assertEquals("2", coverage[1]); + } + + @Test + public void testCoveringGeohashThreeGeohashes() { + Envelope2D env = new Envelope2D(-180, -90, -180, 5); + String[] coverage = Geohash.coveringGeohash(env); + assertEquals("0", coverage[0]); + assertEquals("2", coverage[1]); + assertEquals("8", coverage[2]); + } + + @Test + public void testCoveringGeohashFourGeohashes() { + Envelope2D env = new Envelope2D(-180, -90, -130, -40); + String[] coverage = Geohash.coveringGeohash(env); + assertEquals("0", coverage[0]); + assertEquals("2", coverage[1]); + assertEquals("1", coverage[2]); + assertEquals("3", coverage[3]); + } + } From 39d070330ea5d5f20ef6043cb869ced7f96ab7b4 Mon Sep 17 00:00:00 2001 From: wenjj2000 Date: Wed, 6 Mar 2024 15:28:54 +0100 Subject: [PATCH 09/13] fix : fixed Geohash to envelope function --- .../java/com/esri/core/geometry/Geohash.java | 145 +++---- .../com/esri/core/geometry/TestGeohash.java | 376 +++++++++--------- 2 files changed, 270 insertions(+), 251 deletions(-) diff --git a/src/main/java/com/esri/core/geometry/Geohash.java b/src/main/java/com/esri/core/geometry/Geohash.java index 09874c8c..adafc061 100644 --- a/src/main/java/com/esri/core/geometry/Geohash.java +++ b/src/main/java/com/esri/core/geometry/Geohash.java @@ -55,28 +55,29 @@ public static Envelope2D geohashToEnvelope(String geoHash) { double lat = -90; double latPrecision = 90; + long temp = 1; for (int i = 0; i < latBitsSize; i++) { - if (((1 << (latBitsSize - 1 - i)) & latBits) != 0) { - lat += latPrecision; - } - latPrecision /= 2; + if (((temp << (latBitsSize - 1 - i)) & latBits) != 0) { + lat += latPrecision; + } + latPrecision /= 2; } double lon = -180; double lonPrecision = 180; for (int i = 0; i < lonBitsSize; i++) { - if (((1 << (lonBitsSize - 1 - i)) & lonBits) != 0) { - lon += lonPrecision; - } - lonPrecision /= 2; + if (((1 << (lonBitsSize - 1 - i)) & lonBits) != 0) { + lon += lonPrecision; + } + lonPrecision /= 2; } return new Envelope2D( - lon, - lat, - lon + lonPrecision * 2, - lat + latPrecision * 2 - ); + lon, + lat, + lon + lonPrecision * 2, + lat + latPrecision * 2 + ); } /** @@ -87,32 +88,32 @@ public static Envelope2D geohashToEnvelope(String geoHash) { */ public static String toGeohash(Point2D pt, int characterLength) { if (characterLength < 1) { - throw new InvalidParameterException( - "CharacterLength cannot be less than 1" + throw new InvalidParameterException( + "CharacterLength cannot be less than 1" ); } - if (characterLength > 25) { - throw new InvalidParameterException("Max characterLength of 25"); + if (characterLength > 24) { + throw new InvalidParameterException(GEOHASH_EXCEED_MAX_PRECISION_MESSAGE); } int precision = 63; double lat = pt.y; double lon = pt.x; long latBit = Geohash.convertToBinary( - lat, - new double[] { -90, 90 }, - precision + lat, + new double[] { -90, 90 }, + precision ); long lonBit = Geohash.convertToBinary( - lon, - new double[] { -180, 180 }, - precision + lon, + new double[] { -180, 180 }, + precision ); return Geohash - .binaryToBase32(lonBit, latBit, precision) - .substring(0, characterLength); + .binaryToBase32(lonBit, latBit, precision) + .substring(0, characterLength); } /** @@ -129,24 +130,24 @@ private static String binaryToBase32(long lonBits, long latBits, int len) { long curr = 1; int currLen = 0; while (i >= 0) { - long currLon = (lonBits >>> i) & 1; - long currLat = (latBits >>> i) & 1; - if (currLen >= 5) { - base32Str.append(base32.charAt((int) (curr & 0x1F))); - curr = 1; - currLen = 0; - } - curr = (curr << 1) | currLon; - currLen++; - if (currLen >= 5) { - base32Str.append(base32.charAt((int) (curr & 0x1F))); - curr = 1; - currLen = 0; - } - curr = (curr << 1) | currLat; - currLen++; + long currLon = (lonBits >>> i) & 1; + long currLat = (latBits >>> i) & 1; + if (currLen >= 5) { + base32Str.append(base32.charAt((int) (curr & 0x1F))); + curr = 1; + currLen = 0; + } + curr = (curr << 1) | currLon; + currLen++; + if (currLen >= 5) { + base32Str.append(base32.charAt((int) (curr & 0x1F))); + curr = 1; + currLen = 0; + } + curr = (curr << 1) | currLat; + currLen++; - i--; + i--; } return base32Str.toString(); @@ -163,15 +164,15 @@ private static String binaryToBase32(long lonBits, long latBits, int len) { private static long convertToBinary(double value, double[] r, int precision) { long binVal = 1; for (int i = 0; i < precision; i++) { - double mid = (r[0] + r[1]) / 2; - if (value >= mid) { - binVal = binVal << 1; - binVal = binVal | 1; - r[0] = mid; - } else { - binVal = binVal << 1; - r[1] = mid; - } + double mid = (r[0] + r[1]) / 2; + if (value >= mid) { + binVal = binVal << 1; + binVal = binVal | 1; + r[0] = mid; + } else { + binVal = binVal << 1; + r[1] = mid; + } } return binVal; } @@ -195,20 +196,20 @@ public static String containingGeohash(Envelope2D envelope) { double deltaLat = 180; while (xmin == xmax && ymin == ymax && chars < 7) { - if (chars % 2 == 0) { - deltaLon = deltaLon / 8; - deltaLat = deltaLat / 4; - } else { - deltaLon = deltaLon / 4; - deltaLat = deltaLat / 8; - } + if (chars % 2 == 0) { + deltaLon = deltaLon / 8; + deltaLat = deltaLat / 4; + } else { + deltaLon = deltaLon / 4; + deltaLat = deltaLat / 8; + } - xmin = Math.floor(posMinX / deltaLon); - xmax = Math.floor(posMaxX / deltaLon); - ymin = Math.floor(posMinY / deltaLat); - ymax = Math.floor(posMaxY / deltaLat); + xmin = Math.floor(posMinX / deltaLon); + xmax = Math.floor(posMaxX / deltaLon); + ymin = Math.floor(posMinY / deltaLat); + ymax = Math.floor(posMaxY / deltaLat); - chars++; + chars++; } if (chars == 1) return ""; @@ -228,11 +229,11 @@ public static String[] coveringGeohash(Envelope2D envelope) { double ymax = envelope.ymax; if (NumberUtils.isNaN(xmax)) { - return new String[] { "" }; + return new String[] { "" }; } String[] geoHash = { containingGeohash(envelope) }; if (geoHash[0] != "") { - return geoHash; + return geoHash; } int grid = 45; @@ -245,14 +246,14 @@ public static String[] coveringGeohash(Envelope2D envelope) { String[] geoHashes = new String[deltaLon * deltaLat]; if (deltaLon * deltaLat > 4) { - return new String[] { "" }; + return new String[] { "" }; } else { - for (int i = 0; i < deltaLon; i++) { - for (int j = 0; j < deltaLat; j++) { - Point2D p = new Point2D(xmin + i * grid, ymin + j * grid); - geoHashes[i * deltaLat + j] = toGeohash(p, 1); - } - } + for (int i = 0; i < deltaLon; i++) { + for (int j = 0; j < deltaLat; j++) { + Point2D p = new Point2D(xmin + i * grid, ymin + j * grid); + geoHashes[i * deltaLat + j] = toGeohash(p, 1); + } + } } return geoHashes; } diff --git a/src/test/java/com/esri/core/geometry/TestGeohash.java b/src/test/java/com/esri/core/geometry/TestGeohash.java index 8b2abc33..abcbcef2 100644 --- a/src/test/java/com/esri/core/geometry/TestGeohash.java +++ b/src/test/java/com/esri/core/geometry/TestGeohash.java @@ -1,183 +1,201 @@ - package com.esri.core.geometry; + package com.esri.core.geometry; - import static org.junit.Assert.assertEquals; + import static org.junit.Assert.assertEquals; - import org.junit.Test; + import org.junit.Test; - public class TestGeohash { - - /** - * Check if the center of the new envelope is well placed - */ - @Test - public void testGeohashToEnvelopeWellCentered() { - double delta = 0.00000001; + public class TestGeohash { - String geohash1 = "ghgh"; - - double lat1 = 72.50976563; - double lon1 = -40.60546875; - Envelope2D env1 = Geohash.geohashToEnvelope(geohash1); - double centerX1 = (env1.xmax + env1.xmin) * 0.5; - double centerY1 = (env1.ymax + env1.ymin) * 0.5; - - assertEquals(lon1, centerX1, delta); - assertEquals(lat1, centerY1, delta); - - String geohash2 = "p"; - - double lat2 = -67.50000000; - double lon2 = 157.50000000; - Envelope2D env2 = Geohash.geohashToEnvelope(geohash2); - double centerX2 = (env2.xmax + env2.xmin) * 0.5; - double centerY2 = (env2.ymax + env2.ymin) * 0.5; - - assertEquals(lon2, centerX2, delta); - assertEquals(lat2, centerY2, delta); - } - - /** - * Check if the dimension of the new envelope is correct for low precision - */ - @Test - public void testGeohashToEnvelopeGoodDimensions() { - double delta = 0.00000001; - - double latDiff = 180 / 4; - double lonDiff = 360 / 8; - - String geohash = "h"; - - Envelope2D env = Geohash.geohashToEnvelope(geohash); - - assertEquals(lonDiff, env.xmax - env.xmin, delta); - assertEquals(latDiff, env.ymax - env.ymin, delta); - } - - /** - * Check if the dimension of the new envelope is correct for higher precision - */ - @Test - public void testGeohashToEnvelopeGoodDimensions2() { - double delta = 0.00000001; - - double latDiff = 180.0 / 32768; - double lonDiff = 360.0 / 32768; - - String geohash = "hggggg"; - - Envelope2D env = Geohash.geohashToEnvelope(geohash); - - assertEquals(lonDiff, env.xmax - env.xmin, delta); - assertEquals(latDiff, env.ymax - env.ymin, delta); - } - - @Test - public void testToGeoHash() { - Point2D p0 = new Point2D(0, 0); - - Point2D p1 = new Point2D(-4.329, 48.669); - Point2D p2 = new Point2D(-30.382, 70.273); - Point2D p3 = new Point2D(14.276, 37.691); - Point2D p4 = new Point2D(-143.923, 48.669); - Point2D p5 = new Point2D(-143.923, 48.669); - - int chrLen = 5; - - String p0Hash = Geohash.toGeohash(p0, 1); - String p1Hash = Geohash.toGeohash(p1, chrLen); - String p2Hash = Geohash.toGeohash(p2, chrLen); - String p3Hash = Geohash.toGeohash(p3, chrLen); - String p4Hash = Geohash.toGeohash(p4, chrLen); - String p5Hash = Geohash.toGeohash(p5, 6); - String p6Hash = Geohash.toGeohash(p5, 25); - - assertEquals("s", p0Hash); - assertEquals("gbsuv", p1Hash); - assertEquals("gk6ru", p2Hash); - assertEquals("sqdnk", p3Hash); - assertEquals("bb9su", p4Hash); - assertEquals("bb9sug", p5Hash); - assertEquals("bb9sugymrp0vwb2kzfsq1mzzz", p6Hash); - } - - @Test - public void testToGeohashHasGoodPrecision() { - Point2D point = new Point2D(18.068581, 59.329323); - assertEquals(6, Geohash.toGeohash(point, 6).length()); - } - - @Test - public void testToGeohash2() { - String expected = "u6sce"; - Point2D point = new Point2D(18.068581, 59.329323); - String geoHash = Geohash.toGeohash(point, 5); - - assertEquals(expected, geoHash); - } - - @Test - public void testContainingGeohashWithHugeValues() { - Envelope2D envelope = new Envelope2D(-179, -89, 179, 89); - assertEquals("", Geohash.containingGeohash(envelope)); - } - - @Test - public void testContainingGeohash() { - Envelope2D envelope = new Envelope2D(-179, -89, -140, -50); - assertEquals("0", Geohash.containingGeohash(envelope)); - } - - @Test - public void testContainingGeohash2() { - Envelope2D envelope = new Envelope2D(18.078, 59.3564, 18.1, 59.3344); - assertEquals("u6sce", Geohash.containingGeohash(envelope)); - } - - @Test - public void testCoveringGeohashEmptyEnvelope() { - Envelope2D emptyEnv = new Envelope2D(); - String[] coverage = Geohash.coveringGeohash(emptyEnv); - } - - @Test - public void testCoveringGeohashOneGeohash() { - Envelope2D env = new Envelope2D(-180, -90, -149, -49); - String[] coverage = Geohash.coveringGeohash(env); - assertEquals("0", coverage[0]); - } - - @Test - public void testCoveringGeohashPoint() { - Envelope2D env = new Envelope2D(180, 90, 180, 90); - String[] coverage = Geohash.coveringGeohash(env); - assertEquals("zzzzzz", coverage[0]); - } - - @Test - public void testCoveringGeohashTwoGeohashes() { - Envelope2D env = new Envelope2D(-180, -90, -180, -35); - String[] coverage = Geohash.coveringGeohash(env); - assertEquals("0", coverage[0]); - assertEquals("2", coverage[1]); - } - - @Test - public void testCoveringGeohashThreeGeohashes() { - Envelope2D env = new Envelope2D(-180, -90, -180, 5); - String[] coverage = Geohash.coveringGeohash(env); - assertEquals("0", coverage[0]); - assertEquals("2", coverage[1]); - assertEquals("8", coverage[2]); - } - - @Test - public void testCoveringGeohashFourGeohashes() { - Envelope2D env = new Envelope2D(-180, -90, -130, -40); - String[] coverage = Geohash.coveringGeohash(env); - assertEquals("0", coverage[0]); - assertEquals("2", coverage[1]); - assertEquals("1", coverage[2]); - assertEquals("3", coverage[3]); - } - } + /** + * Check if the center of the new envelope is well placed + */ + @Test + public void testGeohashToEnvelopeWellCentered() { + double delta = 0.00000001; + + String geohash1 = "ghgh"; + + double lat1 = 72.50976563; + double lon1 = -40.60546875; + Envelope2D env1 = Geohash.geohashToEnvelope(geohash1); + double centerX1 = (env1.xmax + env1.xmin) * 0.5; + double centerY1 = (env1.ymax + env1.ymin) * 0.5; + + assertEquals(lon1, centerX1, delta); + assertEquals(lat1, centerY1, delta); + + String geohash2 = "p"; + + double lat2 = -67.50000000; + double lon2 = 157.50000000; + Envelope2D env2 = Geohash.geohashToEnvelope(geohash2); + double centerX2 = (env2.xmax + env2.xmin) * 0.5; + double centerY2 = (env2.ymax + env2.ymin) * 0.5; + + assertEquals(lon2, centerX2, delta); + assertEquals(lat2, centerY2, delta); + } + + /** + * Check if the dimension of the new envelope is correct for low precision + */ + @Test + public void testGeohashToEnvelopeGoodDimensions() { + double delta = 0.00000001; + + double latDiff = 180 / 4; + double lonDiff = 360 / 8; + + String geohash = "h"; + + Envelope2D env = Geohash.geohashToEnvelope(geohash); + + assertEquals(lonDiff, env.xmax - env.xmin, delta); + assertEquals(latDiff, env.ymax - env.ymin, delta); + } + + @Test + public void testGeohashToEnvelopeLongHash() { + double delta = 0.0001; + + + String geohash = "9qh9mzv6sg"; + + double lat1 = 34.01274727; + double lon1 = -117.16176862; + Envelope2D env1 = Geohash.geohashToEnvelope(geohash); + double centerX1 = (env1.xmax + env1.xmin) * 0.5; + double centerY1 = (env1.ymax + env1.ymin) * 0.5; + + assertEquals(lon1, centerX1, delta); + assertEquals(lat1, centerY1, delta); + + } + + /** + * Check if the dimension of the new envelope is correct for higher precision + */ + @Test + public void testGeohashToEnvelopeGoodDimensions2() { + double delta = 0.00000001; + + double latDiff = 180.0 / 32768; + double lonDiff = 360.0 / 32768; + + String geohash = "hggggg"; + + Envelope2D env = Geohash.geohashToEnvelope(geohash); + + assertEquals(lonDiff, env.xmax - env.xmin, delta); + assertEquals(latDiff, env.ymax - env.ymin, delta); + } + + @Test + public void testToGeoHash() { + Point2D p0 = new Point2D(0, 0); + + Point2D p1 = new Point2D(-4.329, 48.669); + Point2D p2 = new Point2D(-30.382, 70.273); + Point2D p3 = new Point2D(14.276, 37.691); + Point2D p4 = new Point2D(-143.923, 48.669); + Point2D p5 = new Point2D(-143.923, 48.669); + + int chrLen = 5; + + String p0Hash = Geohash.toGeohash(p0, 1); + String p1Hash = Geohash.toGeohash(p1, chrLen); + String p2Hash = Geohash.toGeohash(p2, chrLen); + String p3Hash = Geohash.toGeohash(p3, chrLen); + String p4Hash = Geohash.toGeohash(p4, chrLen); + String p5Hash = Geohash.toGeohash(p5, 6); + String p6Hash = Geohash.toGeohash(p5, 24); + + assertEquals("s", p0Hash); + assertEquals("gbsuv", p1Hash); + assertEquals("gk6ru", p2Hash); + assertEquals("sqdnk", p3Hash); + assertEquals("bb9su", p4Hash); + assertEquals("bb9sug", p5Hash); + assertEquals("bb9sugymrp0vwb2kzfsq1mzz", p6Hash); + } + + @Test + public void testToGeohashHasGoodPrecision() { + Point2D point = new Point2D(18.068581, 59.329323); + assertEquals(6, Geohash.toGeohash(point, 6).length()); + } + + @Test + public void testToGeohash2() { + String expected = "u6sce"; + Point2D point = new Point2D(18.068581, 59.329323); + String geoHash = Geohash.toGeohash(point, 5); + + assertEquals(expected, geoHash); + } + + @Test + public void testContainingGeohashWithHugeValues() { + Envelope2D envelope = new Envelope2D(-179, -89, 179, 89); + assertEquals("", Geohash.containingGeohash(envelope)); + } + + @Test + public void testContainingGeohash() { + Envelope2D envelope = new Envelope2D(-179, -89, -140, -50); + assertEquals("0", Geohash.containingGeohash(envelope)); + } + + @Test + public void testContainingGeohash2() { + Envelope2D envelope = new Envelope2D(18.078, 59.3564, 18.1, 59.3344); + assertEquals("u6sce", Geohash.containingGeohash(envelope)); + } + + @Test + public void testCoveringGeohashEmptyEnvelope() { + Envelope2D emptyEnv = new Envelope2D(); + String[] coverage = Geohash.coveringGeohash(emptyEnv); + } + + @Test + public void testCoveringGeohashOneGeohash() { + Envelope2D env = new Envelope2D(-180, -90, -149, -49); + String[] coverage = Geohash.coveringGeohash(env); + assertEquals("0", coverage[0]); + } + + @Test + public void testCoveringGeohashPoint() { + Envelope2D env = new Envelope2D(180, 90, 180, 90); + String[] coverage = Geohash.coveringGeohash(env); + assertEquals("zzzzzz", coverage[0]); + } + + @Test + public void testCoveringGeohashTwoGeohashes() { + Envelope2D env = new Envelope2D(-180, -90, -180, -35); + String[] coverage = Geohash.coveringGeohash(env); + assertEquals("0", coverage[0]); + assertEquals("2", coverage[1]); + } + + @Test + public void testCoveringGeohashThreeGeohashes() { + Envelope2D env = new Envelope2D(-180, -90, -180, 5); + String[] coverage = Geohash.coveringGeohash(env); + assertEquals("0", coverage[0]); + assertEquals("2", coverage[1]); + assertEquals("8", coverage[2]); + } + + @Test + public void testCoveringGeohashFourGeohashes() { + Envelope2D env = new Envelope2D(-180, -90, -130, -40); + String[] coverage = Geohash.coveringGeohash(env); + assertEquals("0", coverage[0]); + assertEquals("2", coverage[1]); + assertEquals("1", coverage[2]); + assertEquals("3", coverage[3]); + } + } From ce04e54d8b911b16a993b40189a92bd39fb5dd4a Mon Sep 17 00:00:00 2001 From: wenjj2000 Date: Wed, 6 Mar 2024 15:38:31 +0100 Subject: [PATCH 10/13] docs : removing patch file --- GeoHashPatch.patch | 447 --------------------------------------------- 1 file changed, 447 deletions(-) delete mode 100644 GeoHashPatch.patch diff --git a/GeoHashPatch.patch b/GeoHashPatch.patch deleted file mode 100644 index 83d41e18..00000000 --- a/GeoHashPatch.patch +++ /dev/null @@ -1,447 +0,0 @@ -diff --git a/src/main/java/com/esri/core/geometry/Geohash.java b/src/main/java/com/esri/core/geometry/Geohash.java -new file mode 100644 -index 0000000..3398720 ---- /dev/null -+++ b/src/main/java/com/esri/core/geometry/Geohash.java -@@ -0,0 +1,252 @@ -+package com.esri.core.geometry; -+ -+import java.security.InvalidParameterException; -+ -+/** -+ * Helper class to work with geohash -+ */ -+public class Geohash { -+ -+ private static final String base32 = "0123456789bcdefghjkmnpqrstuvwxyz"; -+ -+ private static final String INVALID_CHARACTER_MESSAGE = -+ "Invalid character in geohash: "; -+ private static final String GEOHASH_EXCEED_MAX_PRECISION_MESSAGE = -+ "Precision to high in geohash (max 24)"; -+ -+ /** -+ * Create an evelope from a given geohash -+ * @param geoHash -+ * @return The envelope that corresponds to the geohash -+ * @throws InvalidParameterException if the precision of geoHash is greater than 24 characters -+ */ -+ public static Envelope2D geohashToEnvelope(String geoHash) { -+ if (geoHash.length() > 24) { -+ throw new InvalidParameterException(GEOHASH_EXCEED_MAX_PRECISION_MESSAGE); -+ } -+ -+ long latBits = 0; -+ long lonBits = 0; -+ for (int i = 0; i < geoHash.length(); i++) { -+ int pos = base32.indexOf(geoHash.charAt(i)); -+ if (pos == -1) { -+ throw new InvalidParameterException( -+ new StringBuilder(INVALID_CHARACTER_MESSAGE) -+ .append('\'') -+ .append(geoHash.charAt(i)) -+ .append('\'') -+ .toString() -+ ); -+ } -+ -+ if (i % 2 == 0) { -+ lonBits = -+ (lonBits << 3) | ((pos >> 2) & 4) | ((pos >> 1) & 2) | (pos & 1); -+ latBits = (latBits << 2) | ((pos >> 2) & 2) | ((pos >> 1) & 1); -+ } else { -+ latBits = -+ (latBits << 3) | ((pos >> 2) & 4) | ((pos >> 1) & 2) | (pos & 1); -+ lonBits = (lonBits << 2) | ((pos >> 2) & 2) | ((pos >> 1) & 1); -+ } -+ } -+ -+ int lonBitsSize = (int) Math.ceil(geoHash.length() * 5 / 2.0); -+ int latBitsSize = geoHash.length() * 5 - lonBitsSize; -+ -+ double lat = -90; -+ double latPrecision = 90; -+ for (int i = 0; i < latBitsSize; i++) { -+ if (((1 << (latBitsSize - 1 - i)) & latBits) != 0) { -+ lat += latPrecision; -+ } -+ latPrecision /= 2; -+ } -+ -+ double lon = -180; -+ double lonPrecision = 180; -+ for (int i = 0; i < lonBitsSize; i++) { -+ if (((1 << (lonBitsSize - 1 - i)) & lonBits) != 0) { -+ lon += lonPrecision; -+ } -+ lonPrecision /= 2; -+ } -+ -+ return new Envelope2D( -+ lon, -+ lat, -+ lon + lonPrecision * 2, -+ lat + latPrecision * 2 -+ ); -+ } -+ -+ /** -+ * Computes the geohash that contains a point at a certain precision -+ * @param pt A point represented as lat/long pair -+ * @param characterLength - The precision of the geohash -+ * @return The geohash of containing pt as a String -+ */ -+ public static String toGeohash(Point2D pt, int characterLength) { -+ if (characterLength < 1) { -+ throw new InvalidParameterException( -+ "CharacterLength cannot be less than 1" -+ ); -+ } -+ if (characterLength > 6) { -+ throw new InvalidParameterException("Max characterLength of 6"); -+ } -+ int precision = characterLength * 5; -+ double lat = pt.y; -+ double lon = pt.x; -+ long latBit = Geohash.convertToBinary( -+ lat, -+ new double[] { -90, 90 }, -+ precision -+ ); -+ -+ long lonBit = Geohash.convertToBinary( -+ lon, -+ new double[] { -180, 180 }, -+ precision -+ ); -+ -+ long interwovenBin = 1; -+ for (int i = precision - 1; i >= 0; i--) { -+ long currLon = (lonBit >>> i) & 1; -+ long currLat = (latBit >>> i) & 1; -+ interwovenBin <<= 1; -+ interwovenBin |= currLon; -+ interwovenBin <<= 1; -+ interwovenBin |= currLat; -+ } -+ -+ return Geohash -+ .binaryToBase32(interwovenBin, precision * 2) -+ .substring(0, characterLength); -+ } -+ -+ /** -+ * Computes the base32 value of the binary string given -+ * @param binStr (long) Binary number that is to be converted to a base32 string -+ * @param len (int) number of bits -+ * @return base32 string of the binStr in chunks of 5 binary digits -+ */ -+ -+ private static String binaryToBase32(long binStr, int len) { -+ StringBuilder base32Str = new StringBuilder(); -+ -+ for (int i = len - 5; i >= 0; i -= 5) { -+ // Extract a group of 5 bits -+ int group = (int) (binStr >>> i) & 0x1F; -+ -+ // Use the extracted group as an index to fetch the corresponding base32 character -+ base32Str.append(base32.charAt(group)); -+ } -+ -+ return base32Str.toString(); -+ } -+ -+ /** -+ * Converts the value given to a binary string with the given precision and range -+ * @param value (double) The value to be converted to a binary number -+ * @param r (double[]) The range at which the value is to be compared with -+ * @param precision (int) The Precision (number of bits) that the binary number needs -+ * @return (String) A binary number representation of the value with the given range and precision -+ */ -+ -+ private static long convertToBinary(double value, double[] r, int precision) { -+ int binVal = 1; -+ for (int i = 0; i < precision; i++) { -+ double mid = (r[0] + r[1]) / 2; -+ if (value >= mid) { -+ binVal = binVal << 1; -+ binVal = binVal | 1; -+ r[0] = mid; -+ } else { -+ binVal = binVal << 1; -+ r[1] = mid; -+ } -+ } -+ return binVal; -+ } -+ -+ /** -+ * Compute the longest geohash that contains the envelope -+ * @param envelope -+ * @return the geohash as a string -+ */ -+ public static String containingGeohash(Envelope2D envelope) { -+ double posMinX = envelope.xmin + 180; -+ double posMaxX = envelope.xmax + 180; -+ double posMinY = envelope.ymin + 90; -+ double posMaxY = envelope.ymax + 90; -+ int chars = 0; -+ double xmin = 0; -+ double xmax = 0; -+ double ymin = 0; -+ double ymax = 0; -+ double deltaLon = 360; -+ double deltaLat = 180; -+ -+ while (xmin == xmax && ymin == ymax && chars < 7) { -+ if (chars % 2 == 0) { -+ deltaLon = deltaLon / 8; -+ deltaLat = deltaLat / 4; -+ } else { -+ deltaLon = deltaLon / 4; -+ deltaLat = deltaLat / 8; -+ } -+ -+ xmin = Math.floor(posMinX / deltaLon); -+ xmax = Math.floor(posMaxX / deltaLon); -+ ymin = Math.floor(posMinY / deltaLat); -+ ymax = Math.floor(posMaxY / deltaLat); -+ -+ chars++; -+ } -+ -+ if (chars == 1) return ""; -+ -+ return toGeohash(new Point2D(envelope.xmin, envelope.ymin), chars - 1); -+ } -+ -+ /** -+ * -+ * @param envelope -+ * @return up to four geohashes that completely cover given envelope -+ */ -+ public static String[] coveringGeohash(Envelope2D envelope) { -+ double xmin = envelope.xmin; -+ double ymin = envelope.ymin; -+ double xmax = envelope.xmax; -+ double ymax = envelope.ymax; -+ -+ if (NumberUtils.isNaN(xmax)) { -+ return new String[] {""}; -+ } -+ String[] geoHash = {containingGeohash(envelope)}; -+ if (geoHash[0] != ""){ -+ return geoHash; -+ } -+ -+ int grid = 45; -+ int gridMaxLon = (int)Math.floor(xmax/grid); -+ int gridMinLon = (int)Math.floor(xmin/grid); -+ int gridMaxLat = (int)Math.floor(ymax/grid); -+ int gridMinLat = (int)Math.floor(ymin/grid); -+ int deltaLon = gridMaxLon - gridMinLon + 1; -+ int deltaLat = gridMaxLat - gridMinLat + 1; -+ String[] geoHashes = new String[deltaLon * deltaLat]; -+ -+ if (deltaLon * deltaLat > 4){ -+ return new String[] {""}; -+ } else { -+ for (int i = 0; i < deltaLon; i++){ -+ for (int j = 0; j < deltaLat; j++){ -+ Point2D p = new Point2D(xmin + i * grid, ymin + j * grid); -+ geoHashes[i*deltaLat + j] = toGeohash(p, 1); -+ } -+ } -+ } -+ return geoHashes; -+ } -+} -diff --git a/src/test/java/com/esri/core/geometry/TestGeohash.java b/src/test/java/com/esri/core/geometry/TestGeohash.java -new file mode 100644 -index 0000000..ab09d32 ---- /dev/null -+++ b/src/test/java/com/esri/core/geometry/TestGeohash.java -@@ -0,0 +1,182 @@ -+package com.esri.core.geometry; -+ -+import static org.junit.Assert.assertEquals; -+ -+import org.junit.Test; -+ -+public class TestGeohash { -+ -+ /** -+ * Check if the center of the new envelope is well placed -+ */ -+ @Test -+ public void testGeohashToEnvelopeWellCentered() { -+ double delta = 0.00000001; -+ -+ String geohash1 = "ghgh"; -+ -+ double lat1 = 72.50976563; -+ double lon1 = -40.60546875; -+ Envelope2D env1 = Geohash.geohashToEnvelope(geohash1); -+ double centerX1 = (env1.xmax + env1.xmin) * 0.5; -+ double centerY1 = (env1.ymax + env1.ymin) * 0.5; -+ -+ assertEquals(lon1, centerX1, delta); -+ assertEquals(lat1, centerY1, delta); -+ -+ String geohash2 = "p"; -+ -+ double lat2 = -67.50000000; -+ double lon2 = 157.50000000; -+ Envelope2D env2 = Geohash.geohashToEnvelope(geohash2); -+ double centerX2 = (env2.xmax + env2.xmin) * 0.5; -+ double centerY2 = (env2.ymax + env2.ymin) * 0.5; -+ -+ assertEquals(lon2, centerX2, delta); -+ assertEquals(lat2, centerY2, delta); -+ } -+ -+ /** -+ * Check if the dimension of the new envelope is correct for low precision -+ */ -+ @Test -+ public void testGeohashToEnvelopeGoodDimensions() { -+ double delta = 0.00000001; -+ -+ double latDiff = 180 / 4; -+ double lonDiff = 360 / 8; -+ -+ String geohash = "h"; -+ -+ Envelope2D env = Geohash.geohashToEnvelope(geohash); -+ -+ assertEquals(lonDiff, env.xmax - env.xmin, delta); -+ assertEquals(latDiff, env.ymax - env.ymin, delta); -+ } -+ -+ /** -+ * Check if the dimension of the new envelope is correct for higher precision -+ */ -+ @Test -+ public void testGeohashToEnvelopeGoodDimensions2() { -+ double delta = 0.00000001; -+ -+ double latDiff = 180.0 / 32768; -+ double lonDiff = 360.0 / 32768; -+ -+ String geohash = "hggggg"; -+ -+ Envelope2D env = Geohash.geohashToEnvelope(geohash); -+ -+ assertEquals(lonDiff, env.xmax - env.xmin, delta); -+ assertEquals(latDiff, env.ymax - env.ymin, delta); -+ } -+ -+ @Test -+ public void testToGeoHash() { -+ Point2D p0 = new Point2D(0, 0); -+ -+ Point2D p1 = new Point2D(-4.329, 48.669); -+ Point2D p2 = new Point2D(-30.382, 70.273); -+ Point2D p3 = new Point2D(14.276, 37.691); -+ Point2D p4 = new Point2D(-143.923, 48.669); -+ Point2D p5 = new Point2D(-143.923, 48.669); -+ -+ int chrLen = 5; -+ -+ String p0Hash = Geohash.toGeohash(p0, 1); -+ -+ String p1Hash = Geohash.toGeohash(p1, chrLen); -+ String p2Hash = Geohash.toGeohash(p2, chrLen); -+ String p3Hash = Geohash.toGeohash(p3, chrLen); -+ String p4Hash = Geohash.toGeohash(p4, chrLen); -+ String p5Hash = Geohash.toGeohash(p5, 6); -+ -+ assertEquals("s", p0Hash); -+ assertEquals("gbsuv", p1Hash); -+ assertEquals("gk6ru", p2Hash); -+ assertEquals("sqdnk", p3Hash); -+ assertEquals("bb9su", p4Hash); -+ assertEquals("bb9sug", p5Hash); -+ } -+ -+ @Test -+ public void testToGeohashHasGoodPrecision() { -+ Point2D point = new Point2D(18.068581, 59.329323); -+ assertEquals(6, Geohash.toGeohash(point, 6).length()); -+ } -+ -+ @Test -+ public void testToGeohash2() { -+ String expected = "u6sce"; -+ Point2D point = new Point2D(18.068581, 59.329323); -+ String geoHash = Geohash.toGeohash(point, 5); -+ -+ assertEquals(expected, geoHash); -+ } -+ -+ @Test -+ public void testContainingGeohashWithHugeValues() { -+ Envelope2D envelope = new Envelope2D(-179, -89, 179, 89); -+ assertEquals("", Geohash.containingGeohash(envelope)); -+ } -+ -+ @Test -+ public void testContainingGeohash() { -+ Envelope2D envelope = new Envelope2D(-179, -89, -140, -50); -+ assertEquals("0", Geohash.containingGeohash(envelope)); -+ } -+ -+ @Test -+ public void testContainingGeohash2() { -+ Envelope2D envelope = new Envelope2D(18.078, 59.3564, 18.1, 59.3344); -+ assertEquals("u6sce", Geohash.containingGeohash(envelope)); -+ } -+ -+ @Test -+ public void testCoveringGeohashEmptyEnvelope() { -+ Envelope2D emptyEnv = new Envelope2D(); -+ String [] coverage = Geohash.coveringGeohash(emptyEnv); -+ } -+ -+ @Test -+ public void testCoveringGeohashOneGeohash() { -+ Envelope2D env = new Envelope2D(-180, -90, -149, -49); -+ String [] coverage = Geohash.coveringGeohash(env); -+ assertEquals("0", coverage[0]); -+ } -+ -+ @Test -+ public void testCoveringGeohashPoint() { -+ Envelope2D env = new Envelope2D(180,90,180,90); -+ String [] coverage = Geohash.coveringGeohash(env); -+ assertEquals("zzzzzz", coverage[0]); -+ } -+ -+ @Test -+ public void testCoveringGeohashTwoGeohashes() { -+ Envelope2D env = new Envelope2D(-180, -90, -180, -35); -+ String [] coverage = Geohash.coveringGeohash(env); -+ assertEquals("0", coverage[0]); -+ assertEquals("2", coverage[1]); -+ } -+ -+ @Test -+ public void testCoveringGeohashThreeGeohashes() { -+ Envelope2D env = new Envelope2D(-180, -90, -180, 5); -+ String [] coverage = Geohash.coveringGeohash(env); -+ assertEquals("0", coverage[0]); -+ assertEquals("2", coverage[1]); -+ assertEquals("8", coverage[2]); -+ } -+ -+ @Test -+ public void testCoveringGeohashFourGeohashes() { -+ Envelope2D env = new Envelope2D(-180, -90, -130, -40); -+ String [] coverage = Geohash.coveringGeohash(env); -+ assertEquals("0", coverage[0]); -+ assertEquals("2", coverage[1]); -+ assertEquals("1", coverage[2]); -+ assertEquals("3", coverage[3]); -+ } -+} -\ No newline at end of file From 381ce8a8f35603736ada3541ee4d7453498632eb Mon Sep 17 00:00:00 2001 From: wenjj2000 Date: Wed, 6 Mar 2024 15:47:31 +0100 Subject: [PATCH 11/13] merge : merging repo from DD2480/geometry-api-java-213-geometry-to-geohash --- .../java/com/esri/core/geometry/Geohash.java | 492 +++++++++--------- 1 file changed, 250 insertions(+), 242 deletions(-) diff --git a/src/main/java/com/esri/core/geometry/Geohash.java b/src/main/java/com/esri/core/geometry/Geohash.java index 3b942f98..55b3f139 100644 --- a/src/main/java/com/esri/core/geometry/Geohash.java +++ b/src/main/java/com/esri/core/geometry/Geohash.java @@ -7,246 +7,254 @@ */ public class Geohash { - private static final String base32 = "0123456789bcdefghjkmnpqrstuvwxyz"; - - private static final String INVALID_CHARACTER_MESSAGE = - "Invalid character in geohash: "; - private static final String GEOHASH_EXCEED_MAX_PRECISION_MESSAGE = - "Precision to high in geohash (max 24)"; - - /** - * Create an evelope from a given geohash - * @param geoHash - * @return The envelope that corresponds to the geohash - * @throws InvalidParameterException if the precision of geoHash is greater than 24 characters - */ - public static Envelope2D geohashToEnvelope(String geoHash) { - if (geoHash.length() > 24) { - throw new InvalidParameterException(GEOHASH_EXCEED_MAX_PRECISION_MESSAGE); - } - - long latBits = 0; - long lonBits = 0; - for (int i = 0; i < geoHash.length(); i++) { - int pos = base32.indexOf(geoHash.charAt(i)); - if (pos == -1) { - throw new InvalidParameterException( - new StringBuilder(INVALID_CHARACTER_MESSAGE) - .append('\'') - .append(geoHash.charAt(i)) - .append('\'') - .toString() - ); - } - - if (i % 2 == 0) { - lonBits = - (lonBits << 3) | ((pos >> 2) & 4) | ((pos >> 1) & 2) | (pos & 1); - latBits = (latBits << 2) | ((pos >> 2) & 2) | ((pos >> 1) & 1); - } else { - latBits = - (latBits << 3) | ((pos >> 2) & 4) | ((pos >> 1) & 2) | (pos & 1); - lonBits = (lonBits << 2) | ((pos >> 2) & 2) | ((pos >> 1) & 1); - } - } - - int lonBitsSize = (int) Math.ceil(geoHash.length() * 5 / 2.0); - int latBitsSize = geoHash.length() * 5 - lonBitsSize; - - double lat = -90; - double latPrecision = 90; - for (int i = 0; i < latBitsSize; i++) { - if (((1 << (latBitsSize - 1 - i)) & latBits) != 0) { - lat += latPrecision; - } - latPrecision /= 2; - } - - double lon = -180; - double lonPrecision = 180; - for (int i = 0; i < lonBitsSize; i++) { - if (((1 << (lonBitsSize - 1 - i)) & lonBits) != 0) { - lon += lonPrecision; - } - lonPrecision /= 2; - } - - return new Envelope2D( - lon, - lat, - lon + lonPrecision * 2, - lat + latPrecision * 2 - ); - } - - /** - * Computes the geohash that contains a point at a certain precision - * @param pt A point represented as lat/long pair - * @param characterLength - The precision of the geohash - * @return The geohash of containing pt as a String - */ - public static String toGeohash(Point2D pt, int characterLength) { - if (characterLength < 1) { - throw new InvalidParameterException( - "CharacterLength cannot be less than 1" - ); - } - if (characterLength > 6) { - throw new InvalidParameterException("Max characterLength of 6"); - } - int precision = characterLength * 5; - double lat = pt.y; - double lon = pt.x; - long latBit = Geohash.convertToBinary( - lat, - new double[] { -90, 90 }, - precision - ); - - long lonBit = Geohash.convertToBinary( - lon, - new double[] { -180, 180 }, - precision - ); - - long interwovenBin = 1; - for (int i = precision - 1; i >= 0; i--) { - long currLon = (lonBit >>> i) & 1; - long currLat = (latBit >>> i) & 1; - interwovenBin <<= 1; - interwovenBin |= currLon; - interwovenBin <<= 1; - interwovenBin |= currLat; - } - - return Geohash - .binaryToBase32(interwovenBin, precision * 2) - .substring(0, characterLength); - } - - /** - * Computes the base32 value of the binary string given - * @param binStr (long) Binary number that is to be converted to a base32 string - * @param len (int) number of bits - * @return base32 string of the binStr in chunks of 5 binary digits - */ - - private static String binaryToBase32(long binStr, int len) { - StringBuilder base32Str = new StringBuilder(); - - for (int i = len - 5; i >= 0; i -= 5) { - // Extract a group of 5 bits - int group = (int) (binStr >>> i) & 0x1F; - - // Use the extracted group as an index to fetch the corresponding base32 character - base32Str.append(base32.charAt(group)); - } - - return base32Str.toString(); - } - - /** - * Converts the value given to a binary string with the given precision and range - * @param value (double) The value to be converted to a binary number - * @param r (double[]) The range at which the value is to be compared with - * @param precision (int) The Precision (number of bits) that the binary number needs - * @return (String) A binary number representation of the value with the given range and precision - */ - - private static long convertToBinary(double value, double[] r, int precision) { - int binVal = 1; - for (int i = 0; i < precision; i++) { - double mid = (r[0] + r[1]) / 2; - if (value >= mid) { - binVal = binVal << 1; - binVal = binVal | 1; - r[0] = mid; - } else { - binVal = binVal << 1; - r[1] = mid; - } - } - return binVal; - } - - /** - * Compute the longest geohash that contains the envelope - * @param envelope - * @return the geohash as a string - */ - public static String containingGeohash(Envelope2D envelope) { - double posMinX = envelope.xmin + 180; - double posMaxX = envelope.xmax + 180; - double posMinY = envelope.ymin + 90; - double posMaxY = envelope.ymax + 90; - int chars = 0; - double xmin = 0; - double xmax = 0; - double ymin = 0; - double ymax = 0; - double deltaLon = 360; - double deltaLat = 180; - - while (xmin == xmax && ymin == ymax && chars < 7) { - if (chars % 2 == 0) { - deltaLon = deltaLon / 8; - deltaLat = deltaLat / 4; - } else { - deltaLon = deltaLon / 4; - deltaLat = deltaLat / 8; - } - - xmin = Math.floor(posMinX / deltaLon); - xmax = Math.floor(posMaxX / deltaLon); - ymin = Math.floor(posMinY / deltaLat); - ymax = Math.floor(posMaxY / deltaLat); - - chars++; - } - - if (chars == 1) return ""; - - return toGeohash(new Point2D(envelope.xmin, envelope.ymin), chars - 1); - } - - /** - * - * @param envelope - * @return up to four geohashes that completely cover given envelope - */ - public static String[] coveringGeohash(Envelope2D envelope) { - double xmin = envelope.xmin; - double ymin = envelope.ymin; - double xmax = envelope.xmax; - double ymax = envelope.ymax; - - if (NumberUtils.isNaN(xmax)) { - return new String[] { "" }; - } - String[] geoHash = { containingGeohash(envelope) }; - if (geoHash[0] != "") { - return geoHash; - } - - int grid = 45; - int gridMaxLon = (int) Math.floor(xmax / grid); - int gridMinLon = (int) Math.floor(xmin / grid); - int gridMaxLat = (int) Math.floor(ymax / grid); - int gridMinLat = (int) Math.floor(ymin / grid); - int deltaLon = gridMaxLon - gridMinLon + 1; - int deltaLat = gridMaxLat - gridMinLat + 1; - String[] geoHashes = new String[deltaLon * deltaLat]; - - if (deltaLon * deltaLat > 4) { - return new String[] { "" }; - } else { - for (int i = 0; i < deltaLon; i++) { - for (int j = 0; j < deltaLat; j++) { - Point2D p = new Point2D(xmin + i * grid, ymin + j * grid); - geoHashes[i * deltaLat + j] = toGeohash(p, 1); - } - } - } - return geoHashes; - } +private static final String base32 = "0123456789bcdefghjkmnpqrstuvwxyz"; + +private static final String INVALID_CHARACTER_MESSAGE = + "Invalid character in geohash: "; +private static final String GEOHASH_EXCEED_MAX_PRECISION_MESSAGE = + "Precision to high in geohash (max 24)"; + +/** + * Create an evelope from a given geohash + * @param geoHash + * @return The envelope that corresponds to the geohash + * @throws InvalidParameterException if the precision of geoHash is greater than 24 characters + */ +public static Envelope2D geohashToEnvelope(String geoHash) { + if (geoHash.length() > 24) { + throw new InvalidParameterException(GEOHASH_EXCEED_MAX_PRECISION_MESSAGE); + } + + long latBits = 0; + long lonBits = 0; + for (int i = 0; i < geoHash.length(); i++) { + int pos = base32.indexOf(geoHash.charAt(i)); + if (pos == -1) { + throw new InvalidParameterException( + new StringBuilder(INVALID_CHARACTER_MESSAGE) + .append('\'') + .append(geoHash.charAt(i)) + .append('\'') + .toString() + ); + } + + if (i % 2 == 0) { + lonBits = + (lonBits << 3) | ((pos >> 2) & 4) | ((pos >> 1) & 2) | (pos & 1); + latBits = (latBits << 2) | ((pos >> 2) & 2) | ((pos >> 1) & 1); + } else { + latBits = + (latBits << 3) | ((pos >> 2) & 4) | ((pos >> 1) & 2) | (pos & 1); + lonBits = (lonBits << 2) | ((pos >> 2) & 2) | ((pos >> 1) & 1); + } + } + + int lonBitsSize = (int) Math.ceil(geoHash.length() * 5 / 2.0); + int latBitsSize = geoHash.length() * 5 - lonBitsSize; + + double lat = -90; + double latPrecision = 90; + long temp = 1; + for (int i = 0; i < latBitsSize; i++) { + if (((temp << (latBitsSize - 1 - i)) & latBits) != 0) { + lat += latPrecision; + } + latPrecision /= 2; + } + + double lon = -180; + double lonPrecision = 180; + for (int i = 0; i < lonBitsSize; i++) { + if (((1 << (lonBitsSize - 1 - i)) & lonBits) != 0) { + lon += lonPrecision; + } + lonPrecision /= 2; + } + + return new Envelope2D( + lon, + lat, + lon + lonPrecision * 2, + lat + latPrecision * 2 + ); +} + +/** + * Computes the geohash that contains a point at a certain precision + * @param pt A point represented as lat/long pair + * @param characterLength - The precision of the geohash + * @return The geohash of containing pt as a String + */ +public static String toGeohash(Point2D pt, int characterLength) { + if (characterLength < 1) { + throw new InvalidParameterException( + "CharacterLength cannot be less than 1" + ); + } + if (characterLength > 24) { + throw new InvalidParameterException(GEOHASH_EXCEED_MAX_PRECISION_MESSAGE); + } + + int precision = 63; + double lat = pt.y; + double lon = pt.x; + long latBit = Geohash.convertToBinary( + lat, + new double[] { -90, 90 }, + precision + ); + + long lonBit = Geohash.convertToBinary( + lon, + new double[] { -180, 180 }, + precision + ); + + return Geohash + .binaryToBase32(lonBit, latBit, precision) + .substring(0, characterLength); +} + +/** + * Computes the base32 value of the binary string given + * @param lonBits (long) longtitude bits + * @param latBits (long) latitude bits + * @param len (int) number of bits + * @return base32 string of the binStr in chunks of 5 binary digits + */ + +private static String binaryToBase32(long lonBits, long latBits, int len) { + StringBuilder base32Str = new StringBuilder(); + int i = len - 1; + long curr = 1; + int currLen = 0; + while (i >= 0) { + long currLon = (lonBits >>> i) & 1; + long currLat = (latBits >>> i) & 1; + if (currLen >= 5) { + base32Str.append(base32.charAt((int) (curr & 0x1F))); + curr = 1; + currLen = 0; + } + curr = (curr << 1) | currLon; + currLen++; + if (currLen >= 5) { + base32Str.append(base32.charAt((int) (curr & 0x1F))); + curr = 1; + currLen = 0; + } + curr = (curr << 1) | currLat; + currLen++; + + i--; + } + + return base32Str.toString(); +} + +/** + * Converts the value given to a binary string with the given precision and range + * @param value (double) The value to be converted to a binary number + * @param r (double[]) The range at which the value is to be compared with + * @param precision (int) The Precision (number of bits) that the binary number needs + * @return (long) A binary number of the value with the given range and precision + */ + +private static long convertToBinary(double value, double[] r, int precision) { + long binVal = 1; + for (int i = 0; i < precision; i++) { + double mid = (r[0] + r[1]) / 2; + if (value >= mid) { + binVal = binVal << 1; + binVal = binVal | 1; + r[0] = mid; + } else { + binVal = binVal << 1; + r[1] = mid; + } + } + return binVal; +} + +/** + * Compute the longest geohash that contains the envelope + * @param envelope + * @return the geohash as a string + */ +public static String containingGeohash(Envelope2D envelope) { + double posMinX = envelope.xmin + 180; + double posMaxX = envelope.xmax + 180; + double posMinY = envelope.ymin + 90; + double posMaxY = envelope.ymax + 90; + int chars = 0; + double xmin = 0; + double xmax = 0; + double ymin = 0; + double ymax = 0; + double deltaLon = 360; + double deltaLat = 180; + + while (xmin == xmax && ymin == ymax && chars < 7) { + if (chars % 2 == 0) { + deltaLon = deltaLon / 8; + deltaLat = deltaLat / 4; + } else { + deltaLon = deltaLon / 4; + deltaLat = deltaLat / 8; + } + + xmin = Math.floor(posMinX / deltaLon); + xmax = Math.floor(posMaxX / deltaLon); + ymin = Math.floor(posMinY / deltaLat); + ymax = Math.floor(posMaxY / deltaLat); + + chars++; + } + + if (chars == 1) return ""; + + return toGeohash(new Point2D(envelope.xmin, envelope.ymin), chars - 1); +} + +/** + * + * @param envelope + * @return up to four geohashes that completely cover given envelope + */ +public static String[] coveringGeohash(Envelope2D envelope) { + double xmin = envelope.xmin; + double ymin = envelope.ymin; + double xmax = envelope.xmax; + double ymax = envelope.ymax; + + if (NumberUtils.isNaN(xmax)) { + return new String[] { "" }; + } + String[] geoHash = { containingGeohash(envelope) }; + if (geoHash[0] != "") { + return geoHash; + } + + int grid = 45; + int gridMaxLon = (int) Math.floor(xmax / grid); + int gridMinLon = (int) Math.floor(xmin / grid); + int gridMaxLat = (int) Math.floor(ymax / grid); + int gridMinLat = (int) Math.floor(ymin / grid); + int deltaLon = gridMaxLon - gridMinLon + 1; + int deltaLat = gridMaxLat - gridMinLat + 1; + String[] geoHashes = new String[deltaLon * deltaLat]; + + if (deltaLon * deltaLat > 4) { + return new String[] { "" }; + } else { + for (int i = 0; i < deltaLon; i++) { + for (int j = 0; j < deltaLat; j++) { + Point2D p = new Point2D(xmin + i * grid, ymin + j * grid); + geoHashes[i * deltaLat + j] = toGeohash(p, 1); + } + } + } + return geoHashes; +} } From b052449d9bd9ec3fe40c35386936eac98a5f13a7 Mon Sep 17 00:00:00 2001 From: wenjj2000 Date: Wed, 6 Mar 2024 15:51:42 +0100 Subject: [PATCH 12/13] format : changing from spaces to tabs --- .../com/esri/core/geometry/TestGeohash.java | 380 +++++++++--------- 1 file changed, 190 insertions(+), 190 deletions(-) diff --git a/src/test/java/com/esri/core/geometry/TestGeohash.java b/src/test/java/com/esri/core/geometry/TestGeohash.java index 7b44efde..2ef35068 100644 --- a/src/test/java/com/esri/core/geometry/TestGeohash.java +++ b/src/test/java/com/esri/core/geometry/TestGeohash.java @@ -1,199 +1,199 @@ -package com.esri.core.geometry; + package com.esri.core.geometry; -import static org.junit.Assert.assertEquals; + import static org.junit.Assert.assertEquals; -import org.junit.Test; + import org.junit.Test; -public class TestGeohash { + public class TestGeohash { - /** - * Check if the center of the new envelope is well placed - */ - @Test - public void testGeohashToEnvelopeWellCentered() { - double delta = 0.00000001; + /** + * Check if the center of the new envelope is well placed + */ + @Test + public void testGeohashToEnvelopeWellCentered() { + double delta = 0.00000001; - String geohash1 = "ghgh"; + String geohash1 = "ghgh"; - double lat1 = 72.50976563; - double lon1 = -40.60546875; - Envelope2D env1 = Geohash.geohashToEnvelope(geohash1); - double centerX1 = (env1.xmax + env1.xmin) * 0.5; - double centerY1 = (env1.ymax + env1.ymin) * 0.5; + double lat1 = 72.50976563; + double lon1 = -40.60546875; + Envelope2D env1 = Geohash.geohashToEnvelope(geohash1); + double centerX1 = (env1.xmax + env1.xmin) * 0.5; + double centerY1 = (env1.ymax + env1.ymin) * 0.5; - assertEquals(lon1, centerX1, delta); - assertEquals(lat1, centerY1, delta); + assertEquals(lon1, centerX1, delta); + assertEquals(lat1, centerY1, delta); - String geohash2 = "p"; - - double lat2 = -67.50000000; - double lon2 = 157.50000000; - Envelope2D env2 = Geohash.geohashToEnvelope(geohash2); - double centerX2 = (env2.xmax + env2.xmin) * 0.5; - double centerY2 = (env2.ymax + env2.ymin) * 0.5; + String geohash2 = "p"; + + double lat2 = -67.50000000; + double lon2 = 157.50000000; + Envelope2D env2 = Geohash.geohashToEnvelope(geohash2); + double centerX2 = (env2.xmax + env2.xmin) * 0.5; + double centerY2 = (env2.ymax + env2.ymin) * 0.5; - assertEquals(lon2, centerX2, delta); - assertEquals(lat2, centerY2, delta); - } - - /** - * Check if the dimension of the new envelope is correct for low precision - */ - @Test - public void testGeohashToEnvelopeGoodDimensions() { - double delta = 0.00000001; - - double latDiff = 180 / 4; - double lonDiff = 360 / 8; - - String geohash = "h"; - - Envelope2D env = Geohash.geohashToEnvelope(geohash); - - assertEquals(lonDiff, env.xmax - env.xmin, delta); - assertEquals(latDiff, env.ymax - env.ymin, delta); - } - - @Test - public void testGeohashToEnvelopeLongHash() { - double delta = 0.0001; - - String geohash = "9qh9mzv6sg"; - - double lat1 = 34.01274727; - double lon1 = -117.16176862; - Envelope2D env1 = Geohash.geohashToEnvelope(geohash); - double centerX1 = (env1.xmax + env1.xmin) * 0.5; - double centerY1 = (env1.ymax + env1.ymin) * 0.5; - - assertEquals(lon1, centerX1, delta); - assertEquals(lat1, centerY1, delta); - } - - /** - * Check if the dimension of the new envelope is correct for higher precision - */ - @Test - public void testGeohashToEnvelopeGoodDimensions2() { - double delta = 0.00000001; - - double latDiff = 180.0 / 32768; - double lonDiff = 360.0 / 32768; - - String geohash = "hggggg"; - - Envelope2D env = Geohash.geohashToEnvelope(geohash); - - assertEquals(lonDiff, env.xmax - env.xmin, delta); - assertEquals(latDiff, env.ymax - env.ymin, delta); - } - - @Test - public void testToGeoHash() { - Point2D p0 = new Point2D(0, 0); - - Point2D p1 = new Point2D(-4.329, 48.669); - Point2D p2 = new Point2D(-30.382, 70.273); - Point2D p3 = new Point2D(14.276, 37.691); - Point2D p4 = new Point2D(-143.923, 48.669); - Point2D p5 = new Point2D(-143.923, 48.669); - - int chrLen = 5; - - String p0Hash = Geohash.toGeohash(p0, 1); - String p1Hash = Geohash.toGeohash(p1, chrLen); - String p2Hash = Geohash.toGeohash(p2, chrLen); - String p3Hash = Geohash.toGeohash(p3, chrLen); - String p4Hash = Geohash.toGeohash(p4, chrLen); - String p5Hash = Geohash.toGeohash(p5, 6); - String p6Hash = Geohash.toGeohash(p5, 24); - - assertEquals("s", p0Hash); - assertEquals("gbsuv", p1Hash); - assertEquals("gk6ru", p2Hash); - assertEquals("sqdnk", p3Hash); - assertEquals("bb9su", p4Hash); - assertEquals("bb9sug", p5Hash); - assertEquals("bb9sugymrp0vwb2kzfsq1mzz", p6Hash); - } - - @Test - public void testToGeohashHasGoodPrecision() { - Point2D point = new Point2D(18.068581, 59.329323); - assertEquals(6, Geohash.toGeohash(point, 6).length()); - } - - @Test - public void testToGeohash2() { - String expected = "u6sce"; - Point2D point = new Point2D(18.068581, 59.329323); - String geoHash = Geohash.toGeohash(point, 5); - - assertEquals(expected, geoHash); - } - - @Test - public void testContainingGeohashWithHugeValues() { - Envelope2D envelope = new Envelope2D(-179, -89, 179, 89); - assertEquals("", Geohash.containingGeohash(envelope)); - } - - @Test - public void testContainingGeohash() { - Envelope2D envelope = new Envelope2D(-179, -89, -140, -50); - assertEquals("0", Geohash.containingGeohash(envelope)); - } - - @Test - public void testContainingGeohash2() { - Envelope2D envelope = new Envelope2D(18.078, 59.3564, 18.1, 59.3344); - assertEquals("u6sce", Geohash.containingGeohash(envelope)); - } - - @Test - public void testCoveringGeohashEmptyEnvelope() { - Envelope2D emptyEnv = new Envelope2D(); - String[] coverage = Geohash.coveringGeohash(emptyEnv); - } - - @Test - public void testCoveringGeohashOneGeohash() { - Envelope2D env = new Envelope2D(-180, -90, -149, -49); - String[] coverage = Geohash.coveringGeohash(env); - assertEquals("0", coverage[0]); - } - - @Test - public void testCoveringGeohashPoint() { - Envelope2D env = new Envelope2D(180, 90, 180, 90); - String[] coverage = Geohash.coveringGeohash(env); - assertEquals("zzzzzz", coverage[0]); - } - - @Test - public void testCoveringGeohashTwoGeohashes() { - Envelope2D env = new Envelope2D(-180, -90, -180, -35); - String[] coverage = Geohash.coveringGeohash(env); - assertEquals("0", coverage[0]); - assertEquals("2", coverage[1]); - } - - @Test - public void testCoveringGeohashThreeGeohashes() { - Envelope2D env = new Envelope2D(-180, -90, -180, 5); - String[] coverage = Geohash.coveringGeohash(env); - assertEquals("0", coverage[0]); - assertEquals("2", coverage[1]); - assertEquals("8", coverage[2]); - } - - @Test - public void testCoveringGeohashFourGeohashes() { - Envelope2D env = new Envelope2D(-180, -90, -130, -40); - String[] coverage = Geohash.coveringGeohash(env); - assertEquals("0", coverage[0]); - assertEquals("2", coverage[1]); - assertEquals("1", coverage[2]); - assertEquals("3", coverage[3]); - } -} + assertEquals(lon2, centerX2, delta); + assertEquals(lat2, centerY2, delta); + } + + /** + * Check if the dimension of the new envelope is correct for low precision + */ + @Test + public void testGeohashToEnvelopeGoodDimensions() { + double delta = 0.00000001; + + double latDiff = 180 / 4; + double lonDiff = 360 / 8; + + String geohash = "h"; + + Envelope2D env = Geohash.geohashToEnvelope(geohash); + + assertEquals(lonDiff, env.xmax - env.xmin, delta); + assertEquals(latDiff, env.ymax - env.ymin, delta); + } + + @Test + public void testGeohashToEnvelopeLongHash() { + double delta = 0.0001; + + String geohash = "9qh9mzv6sg"; + + double lat1 = 34.01274727; + double lon1 = -117.16176862; + Envelope2D env1 = Geohash.geohashToEnvelope(geohash); + double centerX1 = (env1.xmax + env1.xmin) * 0.5; + double centerY1 = (env1.ymax + env1.ymin) * 0.5; + + assertEquals(lon1, centerX1, delta); + assertEquals(lat1, centerY1, delta); + } + + /** + * Check if the dimension of the new envelope is correct for higher precision + */ + @Test + public void testGeohashToEnvelopeGoodDimensions2() { + double delta = 0.00000001; + + double latDiff = 180.0 / 32768; + double lonDiff = 360.0 / 32768; + + String geohash = "hggggg"; + + Envelope2D env = Geohash.geohashToEnvelope(geohash); + + assertEquals(lonDiff, env.xmax - env.xmin, delta); + assertEquals(latDiff, env.ymax - env.ymin, delta); + } + + @Test + public void testToGeoHash() { + Point2D p0 = new Point2D(0, 0); + + Point2D p1 = new Point2D(-4.329, 48.669); + Point2D p2 = new Point2D(-30.382, 70.273); + Point2D p3 = new Point2D(14.276, 37.691); + Point2D p4 = new Point2D(-143.923, 48.669); + Point2D p5 = new Point2D(-143.923, 48.669); + + int chrLen = 5; + + String p0Hash = Geohash.toGeohash(p0, 1); + String p1Hash = Geohash.toGeohash(p1, chrLen); + String p2Hash = Geohash.toGeohash(p2, chrLen); + String p3Hash = Geohash.toGeohash(p3, chrLen); + String p4Hash = Geohash.toGeohash(p4, chrLen); + String p5Hash = Geohash.toGeohash(p5, 6); + String p6Hash = Geohash.toGeohash(p5, 24); + + assertEquals("s", p0Hash); + assertEquals("gbsuv", p1Hash); + assertEquals("gk6ru", p2Hash); + assertEquals("sqdnk", p3Hash); + assertEquals("bb9su", p4Hash); + assertEquals("bb9sug", p5Hash); + assertEquals("bb9sugymrp0vwb2kzfsq1mzz", p6Hash); + } + + @Test + public void testToGeohashHasGoodPrecision() { + Point2D point = new Point2D(18.068581, 59.329323); + assertEquals(6, Geohash.toGeohash(point, 6).length()); + } + + @Test + public void testToGeohash2() { + String expected = "u6sce"; + Point2D point = new Point2D(18.068581, 59.329323); + String geoHash = Geohash.toGeohash(point, 5); + + assertEquals(expected, geoHash); + } + + @Test + public void testContainingGeohashWithHugeValues() { + Envelope2D envelope = new Envelope2D(-179, -89, 179, 89); + assertEquals("", Geohash.containingGeohash(envelope)); + } + + @Test + public void testContainingGeohash() { + Envelope2D envelope = new Envelope2D(-179, -89, -140, -50); + assertEquals("0", Geohash.containingGeohash(envelope)); + } + + @Test + public void testContainingGeohash2() { + Envelope2D envelope = new Envelope2D(18.078, 59.3564, 18.1, 59.3344); + assertEquals("u6sce", Geohash.containingGeohash(envelope)); + } + + @Test + public void testCoveringGeohashEmptyEnvelope() { + Envelope2D emptyEnv = new Envelope2D(); + String[] coverage = Geohash.coveringGeohash(emptyEnv); + } + + @Test + public void testCoveringGeohashOneGeohash() { + Envelope2D env = new Envelope2D(-180, -90, -149, -49); + String[] coverage = Geohash.coveringGeohash(env); + assertEquals("0", coverage[0]); + } + + @Test + public void testCoveringGeohashPoint() { + Envelope2D env = new Envelope2D(180, 90, 180, 90); + String[] coverage = Geohash.coveringGeohash(env); + assertEquals("zzzzzz", coverage[0]); + } + + @Test + public void testCoveringGeohashTwoGeohashes() { + Envelope2D env = new Envelope2D(-180, -90, -180, -35); + String[] coverage = Geohash.coveringGeohash(env); + assertEquals("0", coverage[0]); + assertEquals("2", coverage[1]); + } + + @Test + public void testCoveringGeohashThreeGeohashes() { + Envelope2D env = new Envelope2D(-180, -90, -180, 5); + String[] coverage = Geohash.coveringGeohash(env); + assertEquals("0", coverage[0]); + assertEquals("2", coverage[1]); + assertEquals("8", coverage[2]); + } + + @Test + public void testCoveringGeohashFourGeohashes() { + Envelope2D env = new Envelope2D(-180, -90, -130, -40); + String[] coverage = Geohash.coveringGeohash(env); + assertEquals("0", coverage[0]); + assertEquals("2", coverage[1]); + assertEquals("1", coverage[2]); + assertEquals("3", coverage[3]); + } + } From 266f026eb3643431741e672988cc5b95fde9c935 Mon Sep 17 00:00:00 2001 From: muchembledMartin <62207004+muchembledMartin@users.noreply.github.com> Date: Wed, 6 Mar 2024 17:04:52 +0100 Subject: [PATCH 13/13] Fix first bug (#4) Fixes the first bug mentioned in the previous PR --- .../java/com/esri/core/geometry/Geohash.java | 433 +++++++++--------- .../com/esri/core/geometry/TestGeohash.java | 33 +- 2 files changed, 244 insertions(+), 222 deletions(-) diff --git a/src/main/java/com/esri/core/geometry/Geohash.java b/src/main/java/com/esri/core/geometry/Geohash.java index 55b3f139..92f66064 100644 --- a/src/main/java/com/esri/core/geometry/Geohash.java +++ b/src/main/java/com/esri/core/geometry/Geohash.java @@ -7,254 +7,255 @@ */ public class Geohash { -private static final String base32 = "0123456789bcdefghjkmnpqrstuvwxyz"; + private static final String base32 = "0123456789bcdefghjkmnpqrstuvwxyz"; -private static final String INVALID_CHARACTER_MESSAGE = - "Invalid character in geohash: "; -private static final String GEOHASH_EXCEED_MAX_PRECISION_MESSAGE = - "Precision to high in geohash (max 24)"; + private static final String INVALID_CHARACTER_MESSAGE = + "Invalid character in geohash: "; + private static final String GEOHASH_EXCEED_MAX_PRECISION_MESSAGE = + "Precision to high in geohash (max 24)"; -/** - * Create an evelope from a given geohash - * @param geoHash - * @return The envelope that corresponds to the geohash - * @throws InvalidParameterException if the precision of geoHash is greater than 24 characters - */ -public static Envelope2D geohashToEnvelope(String geoHash) { - if (geoHash.length() > 24) { - throw new InvalidParameterException(GEOHASH_EXCEED_MAX_PRECISION_MESSAGE); - } + /** + * Create an evelope from a given geohash + * @param geoHash + * @return The envelope that corresponds to the geohash + * @throws InvalidParameterException if the precision of geoHash is greater than 24 characters + */ + public static Envelope2D geohashToEnvelope(String geoHash) { + if (geoHash.length() > 24) { + throw new InvalidParameterException(GEOHASH_EXCEED_MAX_PRECISION_MESSAGE); + } - long latBits = 0; - long lonBits = 0; - for (int i = 0; i < geoHash.length(); i++) { - int pos = base32.indexOf(geoHash.charAt(i)); - if (pos == -1) { - throw new InvalidParameterException( - new StringBuilder(INVALID_CHARACTER_MESSAGE) - .append('\'') - .append(geoHash.charAt(i)) - .append('\'') - .toString() - ); - } + long latBits = 0; + long lonBits = 0; + for (int i = 0; i < geoHash.length(); i++) { + int pos = base32.indexOf(geoHash.charAt(i)); + if (pos == -1) { + throw new InvalidParameterException( + new StringBuilder(INVALID_CHARACTER_MESSAGE) + .append('\'') + .append(geoHash.charAt(i)) + .append('\'') + .toString() + ); + } - if (i % 2 == 0) { - lonBits = - (lonBits << 3) | ((pos >> 2) & 4) | ((pos >> 1) & 2) | (pos & 1); - latBits = (latBits << 2) | ((pos >> 2) & 2) | ((pos >> 1) & 1); - } else { - latBits = - (latBits << 3) | ((pos >> 2) & 4) | ((pos >> 1) & 2) | (pos & 1); - lonBits = (lonBits << 2) | ((pos >> 2) & 2) | ((pos >> 1) & 1); - } - } + if (i % 2 == 0) { + lonBits = + (lonBits << 3) | ((pos >> 2) & 4) | ((pos >> 1) & 2) | (pos & 1); + latBits = (latBits << 2) | ((pos >> 2) & 2) | ((pos >> 1) & 1); + } else { + latBits = + (latBits << 3) | ((pos >> 2) & 4) | ((pos >> 1) & 2) | (pos & 1); + lonBits = (lonBits << 2) | ((pos >> 2) & 2) | ((pos >> 1) & 1); + } + } - int lonBitsSize = (int) Math.ceil(geoHash.length() * 5 / 2.0); - int latBitsSize = geoHash.length() * 5 - lonBitsSize; + int lonBitsSize = (int) Math.ceil(geoHash.length() * 5 / 2.0); + int latBitsSize = geoHash.length() * 5 - lonBitsSize; - double lat = -90; - double latPrecision = 90; - long temp = 1; - for (int i = 0; i < latBitsSize; i++) { - if (((temp << (latBitsSize - 1 - i)) & latBits) != 0) { - lat += latPrecision; - } - latPrecision /= 2; - } + long one = 1; + + double lat = -90; + double latPrecision = 90; + for (int i = 0; i < latBitsSize; i++) { + if (((one << (latBitsSize - 1 - i)) & latBits) != 0) { + lat += latPrecision; + } + latPrecision /= 2; + } - double lon = -180; - double lonPrecision = 180; - for (int i = 0; i < lonBitsSize; i++) { - if (((1 << (lonBitsSize - 1 - i)) & lonBits) != 0) { - lon += lonPrecision; + double lon = -180; + double lonPrecision = 180; + for (int i = 0; i < lonBitsSize; i++) { + if (((one << (lonBitsSize - 1 - i)) & lonBits) != 0) { + lon += lonPrecision; + } + lonPrecision /= 2; + } + + return new Envelope2D( + lon, + lat, + lon + lonPrecision * 2, + lat + latPrecision * 2 + ); + } + + /** + * Computes the geohash that contains a point at a certain precision + * @param pt A point represented as lat/long pair + * @param characterLength - The precision of the geohash + * @return The geohash of containing pt as a String + */ + public static String toGeohash(Point2D pt, int characterLength) { + if (characterLength < 1) { + throw new InvalidParameterException( + "CharacterLength cannot be less than 1" + ); + } + if (characterLength > 24) { + throw new InvalidParameterException(GEOHASH_EXCEED_MAX_PRECISION_MESSAGE); } - lonPrecision /= 2; - } - return new Envelope2D( - lon, - lat, - lon + lonPrecision * 2, - lat + latPrecision * 2 + int precision = 63; + double lat = pt.y; + double lon = pt.x; + long latBit = Geohash.convertToBinary( + lat, + new double[] { -90, 90 }, + precision ); -} -/** - * Computes the geohash that contains a point at a certain precision - * @param pt A point represented as lat/long pair - * @param characterLength - The precision of the geohash - * @return The geohash of containing pt as a String - */ -public static String toGeohash(Point2D pt, int characterLength) { - if (characterLength < 1) { - throw new InvalidParameterException( - "CharacterLength cannot be less than 1" - ); - } - if (characterLength > 24) { - throw new InvalidParameterException(GEOHASH_EXCEED_MAX_PRECISION_MESSAGE); + long lonBit = Geohash.convertToBinary( + lon, + new double[] { -180, 180 }, + precision + ); + + return Geohash + .binaryToBase32(lonBit, latBit, precision) + .substring(0, characterLength); } - int precision = 63; - double lat = pt.y; - double lon = pt.x; - long latBit = Geohash.convertToBinary( - lat, - new double[] { -90, 90 }, - precision - ); - - long lonBit = Geohash.convertToBinary( - lon, - new double[] { -180, 180 }, - precision - ); - - return Geohash - .binaryToBase32(lonBit, latBit, precision) - .substring(0, characterLength); -} + /** + * Computes the base32 value of the binary string given + * @param lonBits (long) longtitude bits + * @param latBits (long) latitude bits + * @param len (int) number of bits + * @return base32 string of the binStr in chunks of 5 binary digits + */ -/** - * Computes the base32 value of the binary string given - * @param lonBits (long) longtitude bits - * @param latBits (long) latitude bits - * @param len (int) number of bits - * @return base32 string of the binStr in chunks of 5 binary digits - */ + private static String binaryToBase32(long lonBits, long latBits, int len) { + StringBuilder base32Str = new StringBuilder(); + int i = len - 1; + long curr = 1; + int currLen = 0; + while (i >= 0) { + long currLon = (lonBits >>> i) & 1; + long currLat = (latBits >>> i) & 1; + if (currLen >= 5) { + base32Str.append(base32.charAt((int) (curr & 0x1F))); + curr = 1; + currLen = 0; + } + curr = (curr << 1) | currLon; + currLen++; + if (currLen >= 5) { + base32Str.append(base32.charAt((int) (curr & 0x1F))); + curr = 1; + currLen = 0; + } + curr = (curr << 1) | currLat; + currLen++; -private static String binaryToBase32(long lonBits, long latBits, int len) { - StringBuilder base32Str = new StringBuilder(); - int i = len - 1; - long curr = 1; - int currLen = 0; - while (i >= 0) { - long currLon = (lonBits >>> i) & 1; - long currLat = (latBits >>> i) & 1; - if (currLen >= 5) { - base32Str.append(base32.charAt((int) (curr & 0x1F))); - curr = 1; - currLen = 0; + i--; } - curr = (curr << 1) | currLon; - currLen++; - if (currLen >= 5) { - base32Str.append(base32.charAt((int) (curr & 0x1F))); - curr = 1; - currLen = 0; - } - curr = (curr << 1) | currLat; - currLen++; - i--; + return base32Str.toString(); } - return base32Str.toString(); -} - -/** - * Converts the value given to a binary string with the given precision and range - * @param value (double) The value to be converted to a binary number - * @param r (double[]) The range at which the value is to be compared with - * @param precision (int) The Precision (number of bits) that the binary number needs - * @return (long) A binary number of the value with the given range and precision - */ + /** + * Converts the value given to a binary string with the given precision and range + * @param value (double) The value to be converted to a binary number + * @param r (double[]) The range at which the value is to be compared with + * @param precision (int) The Precision (number of bits) that the binary number needs + * @return (long) A binary number of the value with the given range and precision + */ -private static long convertToBinary(double value, double[] r, int precision) { - long binVal = 1; - for (int i = 0; i < precision; i++) { - double mid = (r[0] + r[1]) / 2; - if (value >= mid) { - binVal = binVal << 1; - binVal = binVal | 1; - r[0] = mid; - } else { - binVal = binVal << 1; - r[1] = mid; + private static long convertToBinary(double value, double[] r, int precision) { + long binVal = 1; + for (int i = 0; i < precision; i++) { + double mid = (r[0] + r[1]) / 2; + if (value >= mid) { + binVal = binVal << 1; + binVal = binVal | 1; + r[0] = mid; + } else { + binVal = binVal << 1; + r[1] = mid; + } } + return binVal; } - return binVal; -} -/** - * Compute the longest geohash that contains the envelope - * @param envelope - * @return the geohash as a string - */ -public static String containingGeohash(Envelope2D envelope) { - double posMinX = envelope.xmin + 180; - double posMaxX = envelope.xmax + 180; - double posMinY = envelope.ymin + 90; - double posMaxY = envelope.ymax + 90; - int chars = 0; - double xmin = 0; - double xmax = 0; - double ymin = 0; - double ymax = 0; - double deltaLon = 360; - double deltaLat = 180; - - while (xmin == xmax && ymin == ymax && chars < 7) { - if (chars % 2 == 0) { - deltaLon = deltaLon / 8; - deltaLat = deltaLat / 4; - } else { - deltaLon = deltaLon / 4; - deltaLat = deltaLat / 8; + /** + * Compute the longest geohash that contains the envelope + * @param envelope + * @return the geohash as a string + */ + public static String containingGeohash(Envelope2D envelope) { + double posMinX = envelope.xmin + 180; + double posMaxX = envelope.xmax + 180; + double posMinY = envelope.ymin + 90; + double posMaxY = envelope.ymax + 90; + int chars = 0; + double xmin = 0; + double xmax = 0; + double ymin = 0; + double ymax = 0; + double deltaLon = 360; + double deltaLat = 180; + + while (xmin == xmax && ymin == ymax && chars < 25) { + if (chars % 2 == 0) { + deltaLon = deltaLon / 8; + deltaLat = deltaLat / 4; + } else { + deltaLon = deltaLon / 4; + deltaLat = deltaLat / 8; + } + + xmin = Math.floor(posMinX / deltaLon); + xmax = Math.floor(posMaxX / deltaLon); + ymin = Math.floor(posMinY / deltaLat); + ymax = Math.floor(posMaxY / deltaLat); + + chars++; } - xmin = Math.floor(posMinX / deltaLon); - xmax = Math.floor(posMaxX / deltaLon); - ymin = Math.floor(posMinY / deltaLat); - ymax = Math.floor(posMaxY / deltaLat); + if (chars == 1) return ""; - chars++; + return toGeohash(new Point2D(envelope.xmin, envelope.ymin), chars - 1); } - if (chars == 1) return ""; + /** + * + * @param envelope + * @return up to four geohashes that completely cover given envelope + */ + public static String[] coveringGeohash(Envelope2D envelope) { + double xmin = envelope.xmin; + double ymin = envelope.ymin; + double xmax = envelope.xmax; + double ymax = envelope.ymax; - return toGeohash(new Point2D(envelope.xmin, envelope.ymin), chars - 1); -} + if (NumberUtils.isNaN(xmax)) { + return new String[] { "" }; + } + String[] geoHash = { containingGeohash(envelope) }; + if (geoHash[0] != "") { + return geoHash; + } -/** - * - * @param envelope - * @return up to four geohashes that completely cover given envelope - */ -public static String[] coveringGeohash(Envelope2D envelope) { - double xmin = envelope.xmin; - double ymin = envelope.ymin; - double xmax = envelope.xmax; - double ymax = envelope.ymax; - - if (NumberUtils.isNaN(xmax)) { - return new String[] { "" }; - } - String[] geoHash = { containingGeohash(envelope) }; - if (geoHash[0] != "") { - return geoHash; - } + int grid = 45; + int gridMaxLon = (int) Math.floor(xmax / grid); + int gridMinLon = (int) Math.floor(xmin / grid); + int gridMaxLat = (int) Math.floor(ymax / grid); + int gridMinLat = (int) Math.floor(ymin / grid); + int deltaLon = gridMaxLon - gridMinLon + 1; + int deltaLat = gridMaxLat - gridMinLat + 1; + String[] geoHashes = new String[deltaLon * deltaLat]; - int grid = 45; - int gridMaxLon = (int) Math.floor(xmax / grid); - int gridMinLon = (int) Math.floor(xmin / grid); - int gridMaxLat = (int) Math.floor(ymax / grid); - int gridMinLat = (int) Math.floor(ymin / grid); - int deltaLon = gridMaxLon - gridMinLon + 1; - int deltaLat = gridMaxLat - gridMinLat + 1; - String[] geoHashes = new String[deltaLon * deltaLat]; - - if (deltaLon * deltaLat > 4) { - return new String[] { "" }; - } else { - for (int i = 0; i < deltaLon; i++) { - for (int j = 0; j < deltaLat; j++) { - Point2D p = new Point2D(xmin + i * grid, ymin + j * grid); - geoHashes[i * deltaLat + j] = toGeohash(p, 1); + if (deltaLon * deltaLat > 4) { + return new String[] { "" }; + } else { + for (int i = 0; i < deltaLon; i++) { + for (int j = 0; j < deltaLat; j++) { + Point2D p = new Point2D(xmin + i * grid, ymin + j * grid); + geoHashes[i * deltaLat + j] = toGeohash(p, 1); + } } } + return geoHashes; } - return geoHashes; -} } diff --git a/src/test/java/com/esri/core/geometry/TestGeohash.java b/src/test/java/com/esri/core/geometry/TestGeohash.java index 2ef35068..1056d18f 100644 --- a/src/test/java/com/esri/core/geometry/TestGeohash.java +++ b/src/test/java/com/esri/core/geometry/TestGeohash.java @@ -1,10 +1,10 @@ - package com.esri.core.geometry; +package com.esri.core.geometry; - import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertEquals; - import org.junit.Test; +import org.junit.Test; - public class TestGeohash { +public class TestGeohash { /** * Check if the center of the new envelope is well placed @@ -36,6 +36,20 @@ public void testGeohashToEnvelopeWellCentered() { assertEquals(lat2, centerY2, delta); } + @Test + public void testGeohashToEnvelopeWithLongGeohashes(){ + double delta = 0.00000001; + double lon1 = -117.16176850225; + double lat1 = 34.01274730565; + + String geohash1 = "9qh9mzv6sgtkwz34"; + Envelope2D env1 = Geohash.geohashToEnvelope(geohash1); + double centerX1 = (env1.xmax + env1.xmin) * 0.5; + double centerY1 = (env1.ymax + env1.ymin) * 0.5; + assertEquals(lon1, centerX1, delta); + assertEquals(lat1, centerY1, delta); + } + /** * Check if the dimension of the new envelope is correct for low precision */ @@ -150,6 +164,13 @@ public void testContainingGeohash2() { assertEquals("u6sce", Geohash.containingGeohash(envelope)); } + @Test + public void testContainingGeohashPoint() { + Envelope2D env = new Envelope2D(180, 90, 180, 90); + String coverage = Geohash.containingGeohash(env); + assertEquals("zzzzzzzzzzzzzzzzzzzzzzzz", coverage); + } + @Test public void testCoveringGeohashEmptyEnvelope() { Envelope2D emptyEnv = new Envelope2D(); @@ -167,7 +188,7 @@ public void testCoveringGeohashOneGeohash() { public void testCoveringGeohashPoint() { Envelope2D env = new Envelope2D(180, 90, 180, 90); String[] coverage = Geohash.coveringGeohash(env); - assertEquals("zzzzzz", coverage[0]); + assertEquals("zzzzzzzzzzzzzzzzzzzzzzzz", coverage[0]); } @Test @@ -196,4 +217,4 @@ public void testCoveringGeohashFourGeohashes() { assertEquals("1", coverage[2]); assertEquals("3", coverage[3]); } - } +}