From a56f6ae18ceae1f9fdf9ee24931677953dfee590 Mon Sep 17 00:00:00 2001 From: wenjj2000 Date: Mon, 4 Mar 2024 10:20:21 +0100 Subject: [PATCH] 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