From 428e8d5e142984e3fc834ce6ebf2d5e27b1167b2 Mon Sep 17 00:00:00 2001 From: fummicc1 Date: Mon, 9 Dec 2024 00:23:34 +0900 Subject: [PATCH 1/2] Fix the bound generation bug in being over 180, 90 degree. Unify the method converting from binary into coord. Introduce new api that consider about boundary. --- Sources/GeoHashFramework/GeoHash.swift | 149 ++++++++++++------ .../GeoHashFrameworkTests/GeoHashTests.swift | 16 +- 2 files changed, 108 insertions(+), 57 deletions(-) diff --git a/Sources/GeoHashFramework/GeoHash.swift b/Sources/GeoHashFramework/GeoHash.swift index 427dc25..dc993b3 100644 --- a/Sources/GeoHashFramework/GeoHash.swift +++ b/Sources/GeoHashFramework/GeoHash.swift @@ -210,40 +210,61 @@ extension GeoHash { } public func getNeighbors() -> [GeoHash] { - let latitudeBits = self.latitudeBits - let longitudeBits = self.longitudeBits + var maxLatitude = 90.0 + var minLatitude = -90.0 + var maxLongitude = 180.0 + var minLongitude = -180.0 + + // Calculate the step size based on precision + let latStep = 180.0 / pow(2.0, Double(latitudeBits.count)) + let lngStep = 360.0 / pow(2.0, Double(longitudeBits.count)) + + // Decode current position + for (i, bit) in binary.enumerated() { + if i % 2 == 0 { + let mid = (minLongitude + maxLongitude) / 2 + if bit == "1" { + minLongitude = mid + } else { + maxLongitude = mid + } + } else { + let mid = (minLatitude + maxLatitude) / 2 + if bit == "1" { + minLatitude = mid + } else { + maxLatitude = mid + } + } + } - let north = add(bits: latitudeBits, by: 1) - let south = add(bits: latitudeBits, by: -1) - let east = add(bits: longitudeBits, by: 1) - let west = add(bits: longitudeBits, by: -1) + // Calculate center coordinates + let centerLat = (minLatitude + maxLatitude) / 2 + let centerLng = (minLongitude + maxLongitude) / 2 - let northEast = combineBits(latitude: north, longitude: east) - let northWest = combineBits(latitude: north, longitude: west) - let southEast = combineBits(latitude: south, longitude: east) - let southWest = combineBits(latitude: south, longitude: west) + func makeNeighbor(latOffset: Double, lngOffset: Double) -> GeoHash { + var newLat = centerLat + (latOffset * latStep) + var newLng = centerLng + (lngOffset * lngStep) - return [ - GeoHash( - binary: combineBits(latitude: north, longitude: longitudeBits), - precision: precision - ), - GeoHash(binary: northEast, precision: precision), - GeoHash( - binary: combineBits(latitude: latitudeBits, longitude: east), - precision: precision - ), - GeoHash(binary: southEast, precision: precision), - GeoHash( - binary: combineBits(latitude: south, longitude: longitudeBits), - precision: precision - ), - GeoHash(binary: southWest, precision: precision), - GeoHash( - binary: combineBits(latitude: latitudeBits, longitude: west), + newLat = clampLatitude(newLat) + newLng = normalizeLongitude(newLng) + + return GeoHash( + latitude: newLat, + longitude: newLng, precision: precision - ), - GeoHash(binary: northWest, precision: precision), + ) + } + + return [ + makeNeighbor(latOffset: 1, lngOffset: 0), // north + makeNeighbor(latOffset: 1, lngOffset: 1), // northeast + makeNeighbor(latOffset: 0, lngOffset: 1), // east + makeNeighbor(latOffset: -1, lngOffset: 1), // southeast + makeNeighbor(latOffset: -1, lngOffset: 0), // south + makeNeighbor(latOffset: -1, lngOffset: -1), // southwest + makeNeighbor(latOffset: 0, lngOffset: -1), // west + makeNeighbor(latOffset: 1, lngOffset: -1), // northwest ] } @@ -277,37 +298,67 @@ extension GeoHash { return result } - public func getBound() -> [GeoHashCoordinate2D] { - let baseGeoCoordinate = coordinate - let precision = precision + private func normalizeLongitude(_ lng: Double) -> Double { + var normalized = lng + while normalized > 180.0 { + normalized -= 360.0 + } + while normalized < -180.0 { + normalized += 360.0 + } + return normalized + } - let latitudeBits = precision.rawValue / 2 - let longitudeBits = (precision.rawValue + 1) / 2 + private func clampLatitude(_ lat: Double) -> Double { + return min(90.0, max(-90.0, lat)) + } - let latitudeRange = 180.0 // 90 - (-90) - let latitudeDelta = latitudeRange / pow(2.0, Double(latitudeBits)) + public func getBound() -> [GeoHashCoordinate2D] { + var maxLatitude = 90.0 + var minLatitude = -90.0 + var maxLongitude = 180.0 + var minLongitude = -180.0 - let longitudeRange = 360.0 // 180 - (-180) - let longitudeDelta = longitudeRange / pow(2.0, Double(longitudeBits)) + for (index, bit) in binary.enumerated() { + if index % 2 == 0 { + // longitude bits + let mid = (minLongitude + maxLongitude) / 2 + if bit == "1" { + minLongitude = mid + } else { + maxLongitude = mid + } + } else { + // latitude bits + let mid = (minLatitude + maxLatitude) / 2 + if bit == "1" { + minLatitude = mid + } else { + maxLatitude = mid + } + } + } - let longitude = baseGeoCoordinate.longitude - let latitude = baseGeoCoordinate.latitude + maxLatitude = clampLatitude(maxLatitude) + minLatitude = clampLatitude(minLatitude) + maxLongitude = normalizeLongitude(maxLongitude) + minLongitude = normalizeLongitude(minLongitude) let topLeft = GeoHashCoordinate2D( - latitude: latitude + latitudeDelta, - longitude: longitude + latitude: maxLatitude, + longitude: minLongitude ) let topRight = GeoHashCoordinate2D( - latitude: latitude + latitudeDelta, - longitude: longitude + longitudeDelta + latitude: maxLatitude, + longitude: maxLongitude ) let bottomRight = GeoHashCoordinate2D( - latitude: latitude, - longitude: longitude + longitudeDelta + latitude: minLatitude, + longitude: maxLongitude ) let bottomLeft = GeoHashCoordinate2D( - latitude: latitude, - longitude: longitude + latitude: minLatitude, + longitude: minLongitude ) return [topLeft, topRight, bottomRight, bottomLeft] diff --git a/Tests/GeoHashFrameworkTests/GeoHashTests.swift b/Tests/GeoHashFrameworkTests/GeoHashTests.swift index cd950bb..3848895 100644 --- a/Tests/GeoHashFrameworkTests/GeoHashTests.swift +++ b/Tests/GeoHashFrameworkTests/GeoHashTests.swift @@ -56,20 +56,20 @@ struct GeoHashTests { let actual = geoHash.getBound() let expected = [ GeoHashCoordinate2D( - latitude: 35.68634033203124, - longitude: 139.76257324218756 + latitude: 35.68359375, + longitude: 139.757080078125 ), GeoHashCoordinate2D( - latitude: 35.68634033203124, - longitude: 139.77355957031256 + latitude: 35.68359375, + longitude: 139.76806640625 ), GeoHashCoordinate2D( - latitude: 35.68084716796874, - longitude: 139.77355957031256 + latitude: 35.6781005859375, + longitude: 139.76806640625 ), GeoHashCoordinate2D( - latitude: 35.68084716796874, - longitude: 139.76257324218756 + latitude: 35.6781005859375, + longitude: 139.757080078125 ), ] #expect(actual == expected) From 4af8301adfd1a793a7d58e2a0067cd2d80fa97e6 Mon Sep 17 00:00:00 2001 From: fummicc1 Date: Mon, 9 Dec 2024 00:38:33 +0900 Subject: [PATCH 2/2] Unify getBound logic --- Sources/GeoHashFramework/GeoHash.swift | 81 +++++++++++--------------- 1 file changed, 35 insertions(+), 46 deletions(-) diff --git a/Sources/GeoHashFramework/GeoHash.swift b/Sources/GeoHashFramework/GeoHash.swift index dc993b3..dc95b5d 100644 --- a/Sources/GeoHashFramework/GeoHash.swift +++ b/Sources/GeoHashFramework/GeoHash.swift @@ -210,33 +210,12 @@ extension GeoHash { } public func getNeighbors() -> [GeoHash] { - var maxLatitude = 90.0 - var minLatitude = -90.0 - var maxLongitude = 180.0 - var minLongitude = -180.0 - // Calculate the step size based on precision let latStep = 180.0 / pow(2.0, Double(latitudeBits.count)) let lngStep = 360.0 / pow(2.0, Double(longitudeBits.count)) // Decode current position - for (i, bit) in binary.enumerated() { - if i % 2 == 0 { - let mid = (minLongitude + maxLongitude) / 2 - if bit == "1" { - minLongitude = mid - } else { - maxLongitude = mid - } - } else { - let mid = (minLatitude + maxLatitude) / 2 - if bit == "1" { - minLatitude = mid - } else { - maxLatitude = mid - } - } - } + let (minLatitude, maxLatitude, minLongitude, maxLongitude) = getBound(binary: binary) // Calculate center coordinates let centerLat = (minLatitude + maxLatitude) / 2 @@ -267,6 +246,38 @@ extension GeoHash { makeNeighbor(latOffset: 1, lngOffset: -1), // northwest ] } + + private func getBound(binary: String) -> ( + minLatitude: Double, + maxLatitude: Double, + minLongitude: Double, + maxLongitude: Double + ) { + var maxLatitude = 90.0 + var minLatitude = -90.0 + var maxLongitude = 180.0 + var minLongitude = -180.0 + for (index, bit) in binary.enumerated() { + if index % 2 == 0 { + // longitude bits + let mid = (minLongitude + maxLongitude) / 2 + if bit == "1" { + minLongitude = mid + } else { + maxLongitude = mid + } + } else { + // latitude bits + let mid = (minLatitude + maxLatitude) / 2 + if bit == "1" { + minLatitude = mid + } else { + maxLatitude = mid + } + } + } + return (minLatitude, maxLatitude, minLongitude, maxLongitude) + } /// Add `delta` to `bits` private func add(bits: String, by delta: Int) -> String { @@ -314,30 +325,8 @@ extension GeoHash { } public func getBound() -> [GeoHashCoordinate2D] { - var maxLatitude = 90.0 - var minLatitude = -90.0 - var maxLongitude = 180.0 - var minLongitude = -180.0 - - for (index, bit) in binary.enumerated() { - if index % 2 == 0 { - // longitude bits - let mid = (minLongitude + maxLongitude) / 2 - if bit == "1" { - minLongitude = mid - } else { - maxLongitude = mid - } - } else { - // latitude bits - let mid = (minLatitude + maxLatitude) / 2 - if bit == "1" { - minLatitude = mid - } else { - maxLatitude = mid - } - } - } + + var (minLatitude, maxLatitude, minLongitude, maxLongitude) = getBound(binary: binary) maxLatitude = clampLatitude(maxLatitude) minLatitude = clampLatitude(minLatitude)