Skip to content

Commit ba643eb

Browse files
Merge pull request #4 from flitsmeister/feature/GEOHASH_hashes_for_region
Geohashes for region in C
2 parents 29849bf + 9e0a61b commit ba643eb

File tree

6 files changed

+216
-81
lines changed

6 files changed

+216
-81
lines changed
Lines changed: 121 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#include <stdbool.h>
77

88
#define MAX_HASH_LENGTH 22
9+
#define MAX_ITERATIONS 100000
910

1011
#define SET_BIT(bits, mid, range, val, bit) \
1112
mid = (range->min + range->max) / 2.0; \
@@ -122,7 +123,7 @@
122123
char*
123124
GEOHASH_get_adjacent(const char* hash, GEOHASH_direction dir)
124125
{
125-
int len, idx;
126+
size_t len, idx;
126127
const char *border_table, *neighbor_table;
127128
char *base, *refined_base, *ptr, last;
128129

@@ -161,3 +162,122 @@
161162
base[len] = BASE32_ENCODE_TABLE[idx];
162163
return base;
163164
}
165+
166+
void add_hash(GeohashArray* array, char* hash) {
167+
if (array->count >= array->capacity) {
168+
int new_capacity = array->capacity * 2;
169+
char** new_hashes = realloc(array->hashes, new_capacity * sizeof(char*));
170+
if (!new_hashes) {
171+
// Allocation failed
172+
GEOHASH_free_array(array);
173+
free(hash);
174+
exit(EXIT_FAILURE);
175+
}
176+
array->hashes = new_hashes;
177+
array->capacity = new_capacity;
178+
}
179+
array->hashes[array->count++] = hash;
180+
}
181+
182+
int geohash_equals(const char* a, const char* b) {
183+
return strcmp(a, b) == 0;
184+
}
185+
186+
char* GEOHASH_clone(const char* hash) {
187+
if (!hash) {
188+
exit(EXIT_FAILURE);
189+
return NULL;
190+
}
191+
192+
char* clone = strdup(hash);
193+
if (!clone) {
194+
exit(EXIT_FAILURE);
195+
return NULL;
196+
}
197+
return clone;
198+
}
199+
200+
GeohashArray
201+
GEOHASH_hashes_for_region(double centerLat, double centerLon, double latDelta, double lonDelta, unsigned int len) {
202+
203+
double north = centerLat + latDelta / 2.0;
204+
double south = centerLat - latDelta / 2.0;
205+
double west = centerLon - lonDelta / 2.0;
206+
double east = centerLon + lonDelta / 2.0;
207+
208+
char* nw = GEOHASH_encode(north, west, len);
209+
char* ne = GEOHASH_encode(north, east, len);
210+
char* se = GEOHASH_encode(south, east, len);
211+
212+
char* current = GEOHASH_clone(nw);
213+
char* eastLimit = GEOHASH_clone(ne);
214+
char* westStart = GEOHASH_clone(nw);
215+
216+
GeohashArray result = {
217+
.hashes = malloc(128 * sizeof(char*)),
218+
.count = 0,
219+
.capacity = 128
220+
};
221+
if (!result.hashes) {
222+
// Allocation failed
223+
exit(EXIT_FAILURE);
224+
}
225+
226+
add_hash(&result, GEOHASH_clone(current));
227+
228+
int maxIterations = MAX_ITERATIONS; // Prevent infinite loops
229+
while (!geohash_equals(current, se) && maxIterations-- > 0) {
230+
if (geohash_equals(nw, ne)) {
231+
// Single column case
232+
char* nextSouth = GEOHASH_get_adjacent(westStart, GEOHASH_SOUTH);
233+
free(westStart);
234+
westStart = nextSouth;
235+
free(current);
236+
current = GEOHASH_clone(westStart);
237+
add_hash(&result, GEOHASH_clone(current));
238+
continue;
239+
}
240+
241+
// Move east
242+
char* next = GEOHASH_get_adjacent(current, GEOHASH_EAST);
243+
free(current);
244+
current = next;
245+
add_hash(&result, GEOHASH_clone(current));
246+
247+
if (geohash_equals(current, eastLimit) && !geohash_equals(current, se)) {
248+
char* tmp;
249+
250+
tmp = GEOHASH_get_adjacent(eastLimit, GEOHASH_SOUTH);
251+
free(eastLimit);
252+
eastLimit = tmp;
253+
254+
tmp = GEOHASH_get_adjacent(westStart, GEOHASH_SOUTH);
255+
free(westStart);
256+
westStart = tmp;
257+
258+
free(current);
259+
current = GEOHASH_clone(westStart);
260+
add_hash(&result, GEOHASH_clone(current));
261+
}
262+
}
263+
264+
// Cleanup
265+
free(nw);
266+
free(ne);
267+
free(se);
268+
free(current);
269+
free(eastLimit);
270+
free(westStart);
271+
272+
return result;
273+
}
274+
275+
void GEOHASH_free_array(GeohashArray* array) {
276+
for (int i = 0; i < array->count; i++) {
277+
free(array->hashes[i]);
278+
}
279+
free(array->hashes);
280+
array->hashes = NULL;
281+
array->count = 0;
282+
array->capacity = 0;
283+
}

Sources/FlitsGeohashC/include/flitsgeohash.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,17 @@ typedef struct {
2222
char* south_west;
2323
} GEOHASH_neighbors;
2424

25+
typedef struct {
26+
char** hashes;
27+
int count;
28+
int capacity;
29+
} GeohashArray;
30+
2531
char* GEOHASH_encode(double latitude, double longitude, unsigned int hash_length);
2632
GEOHASH_neighbors* GEOHASH_get_neighbors(const char *hash);
2733
void GEOHASH_free_neighbors(GEOHASH_neighbors *neighbors);
2834

2935
char* GEOHASH_get_adjacent(const char* hash, GEOHASH_direction dir);
36+
37+
GeohashArray GEOHASH_hashes_for_region(double centerLatitude, double centerLongitude, double latitudeDelta, double longitudeDelta, unsigned int len);
38+
void GEOHASH_free_array(GeohashArray* array);

Sources/FlitsGeohashSwift/GeoHash.swift

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,6 @@ public enum Geohash {
2020
}
2121
}
2222

23-
public struct Region: Sendable {
24-
public let center: CLLocationCoordinate2D
25-
public let latitudeDelta: CLLocationDegrees
26-
public let longitudeDelta: CLLocationDegrees
27-
28-
public init(center: CLLocationCoordinate2D, latitudeDelta: CLLocationDegrees, longitudeDelta: CLLocationDegrees) {
29-
self.center = center
30-
self.latitudeDelta = latitudeDelta
31-
self.longitudeDelta = longitudeDelta
32-
}
33-
}
34-
3523
public struct Neighbors: Hashable, Sendable {
3624
public let north: String
3725
public let south: String
@@ -96,6 +84,21 @@ public enum Geohash {
9684
GEOHASH_free_neighbors(pointer)
9785
return neighbors
9886
}
87+
88+
public static func hashesForRegion(
89+
centerCoordinate: CLLocationCoordinate2D,
90+
latitudeDelta: CLLocationDegrees,
91+
longitudeDelta: CLLocationDegrees,
92+
length: UInt32
93+
) -> [String] {
94+
var cArray = GEOHASH_hashes_for_region(centerCoordinate.latitude, centerCoordinate.longitude, latitudeDelta, longitudeDelta, length)
95+
let buffer = UnsafeBufferPointer(start: cArray.hashes, count: Int(cArray.count))
96+
let result = buffer.compactMap {
97+
string(from: $0!)
98+
}
99+
GEOHASH_free_array(&cArray)
100+
return result
101+
}
99102
}
100103

101104
extension Geohash {

Sources/FlitsGeohashSwift/LengthedGeohash.swift

Lines changed: 10 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -83,58 +83,6 @@ public struct LengthedGeohash<Length: GeohashLengthed>: Hashable, Sendable {
8383
)
8484
}
8585

86-
public static func hashes(for region: Geohash.Region) -> Set<Self> {
87-
let northWest = CLLocationCoordinate2D(
88-
latitude: region.center.latitude + region.latitudeDelta / 2,
89-
longitude: region.center.longitude - region.longitudeDelta / 2
90-
)
91-
let northEast = CLLocationCoordinate2D(
92-
latitude: region.center.latitude + region.latitudeDelta / 2,
93-
longitude: region.center.longitude + region.longitudeDelta / 2
94-
)
95-
let southEast = CLLocationCoordinate2D(
96-
latitude: region.center.latitude - region.latitudeDelta / 2,
97-
longitude: region.center.longitude + region.longitudeDelta / 2
98-
)
99-
100-
let hashNorthWest = Self.init(northWest)
101-
let hashNorthEast = Self.init(northEast)
102-
let hashSouthEast = Self.init(southEast)
103-
104-
var currentHash = hashNorthWest
105-
var mostEastHash = hashNorthEast
106-
var mostWestHash = hashNorthWest
107-
108-
var hashes: Set<Self> = [currentHash]
109-
while currentHash != hashSouthEast {
110-
111-
guard hashNorthEast != hashNorthWest else {
112-
// Our region fits inside a single geohash (width)
113-
// This will produce 1 or more rows of a single column
114-
// Only look in the southern direction, if you start looking east you will immediately go past the mostEastHash
115-
// Immediately move to the next row until we find the hashSouthEast
116-
mostEastHash = mostEastHash.adjacent(direction: .south)
117-
mostWestHash = mostWestHash.adjacent(direction: .south)
118-
currentHash = mostWestHash
119-
hashes.insert(currentHash)
120-
continue
121-
}
122-
123-
// Look for the next column by moving east
124-
currentHash = currentHash.adjacent(direction: .east)
125-
hashes.insert(currentHash)
126-
127-
if currentHash == mostEastHash && currentHash != hashSouthEast {
128-
// We are now at the most east row. Start again with a new row.
129-
mostEastHash = mostEastHash.adjacent(direction: .south)
130-
mostWestHash = mostWestHash.adjacent(direction: .south)
131-
currentHash = mostWestHash
132-
hashes.insert(currentHash)
133-
}
134-
}
135-
return hashes
136-
}
137-
13886
public func adjacent(direction: Geohash.Direction) -> LengthedGeohash {
13987
.init(string: Geohash.adjacent(hash: string, direction: direction))
14088
}
@@ -149,6 +97,16 @@ public struct LengthedGeohash<Length: GeohashLengthed>: Hashable, Sendable {
14997
}
15098
return .init(string: String(string.prefix(Int(otherLength))))
15199
}
100+
101+
public static func hashesForRegion(centerCoordinate: CLLocationCoordinate2D, latitudeDelta: CLLocationDegrees, longitudeDelta: CLLocationDegrees) -> [LengthedGeohash] {
102+
Geohash.hashesForRegion(
103+
centerCoordinate: centerCoordinate,
104+
latitudeDelta: latitudeDelta,
105+
longitudeDelta: longitudeDelta,
106+
length: Length.length
107+
)
108+
.map { .init(string: $0) }
109+
}
152110
}
153111

154112
extension LengthedGeohash {

Tests/FlitsGeohashSwiftTests/LengthTests.swift

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11

22
import XCTest
33
import CoreLocation
4-
@testable import FlitsGeohash
4+
import FlitsGeohash
55

66
final class GeohashTests: XCTestCase {
77

@@ -48,6 +48,16 @@ final class GeohashTests: XCTestCase {
4848
)
4949
XCTAssertEqual(neighbors, expectedNeighbors)
5050
}
51+
52+
func testRegion() {
53+
let hashes = Geohash3.hashesForRegion(
54+
centerCoordinate: .init(latitude: 57.64911063015461, longitude: 10.40743969380855),
55+
latitudeDelta: 2,
56+
longitudeDelta: 2
57+
)
58+
let hashesString = hashes.map(\.string).sorted().joined(separator: ",")
59+
XCTAssertEqual(hashesString, "u4n,u4p,u4q,u4r,u60,u62")
60+
}
5161

5262
func testLowerLength() {
5363
let (lat, lon) = (57.64911063015461, 10.40743969380855)
@@ -69,13 +79,4 @@ final class GeohashTests: XCTestCase {
6979
let geohashFromSame: Geohash11? = geohash11.toLowerLength()
7080
XCTAssertEqual(geohashFromSame, geohash11)
7181
}
72-
73-
func testRegion() {
74-
let expectedHashes: Set<String> = ["u14zr","u14yd","u16bj","u14yg","u16b4","u14zd","u16bc","u14yf","u16bd","u14zb","u14xn","u14zh","u14zp","u16b6","u16bz","u14xq","u14wy","u14xy","u14wx","u14zq","u16bh","u14zc","u16b3","u16bv","u14xp","u14zv","u14zf","u14wz","u14xz","u14ww","u16b2","u168w","u16bq","u16bb","u16bk","u14zs","u14yx","u16b1","u14ye","u14zm","u14zy","u14yv","u16bg","u16bn","u16bt","u14zu","u16b7","u14yc","u14xx","u16bx","u14z3","u14y9","u14z9","u14zn","u16br","u16be","u16bf","u14yu","u16b9","u14xr","u16b8","u16b5","u14z8","u14yy","u14zg","u168n","u14zx","u14z5","u14y8","u14zt","u168x","u14zz","u14z0","u16bm","u14zj","u14yt","u168q","u168z","u14xw","u16b0","u14z2","u14yz","u14ze","u16bs","u14z6","u14z7","u14ys","u14zw","u14z1","u14yb","u16bw","u16bp","u14zk","u14z4","u16by","u168r","u14yw","u168p","u168y","u16bu"]
75-
measure {
76-
let region = Geohash.Region(center: .init(latitude: 52, longitude: 4), latitudeDelta: 0.4, longitudeDelta: 0.4)
77-
let hashesInRegion = Geohash5.hashes(for: region).reduce(into: Set(), { $0.insert($1.string) })
78-
XCTAssertEqual(hashesInRegion, expectedHashes)
79-
}
80-
}
8182
}

Tests/FlitsGeohashSwiftTests/PerformanceTests.swift

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
import XCTest
99
import CoreLocation
10-
@testable import FlitsGeohash
10+
import FlitsGeohash
1111

1212
final class PerformanceTests: XCTestCase {
1313

@@ -24,13 +24,21 @@ final class PerformanceTests: XCTestCase {
2424
}
2525

2626
func testMakeGeohashes() {
27+
var geohashes: [String] = []
28+
geohashes.reserveCapacity(Self.size)
29+
for coordinate in Self.array {
30+
geohashes.append(Geohash.hash(coordinate, length: 5))
31+
}
32+
XCTAssertEqual(geohashes.count, Self.size)
33+
XCTAssertEqual(geohashes.first, "u15d1")
34+
XCTAssertEqual(geohashes.last, "u1hrb")
35+
}
36+
37+
func testMakeGeohashesPerformance() {
2738
measure {
28-
let geohashes = Self.array.map {
29-
Geohash.hash($0, length: 5)
39+
for coordinate in Self.array {
40+
_ = Geohash.hash(coordinate, length: 5)
3041
}
31-
XCTAssertEqual(geohashes.count, Self.size)
32-
XCTAssertEqual(geohashes.first, "u15d1")
33-
XCTAssertEqual(geohashes.last, "u1hrb")
3442
}
3543
}
3644

@@ -55,6 +63,42 @@ final class PerformanceTests: XCTestCase {
5563
XCTAssertEqual(adjacentHashes.last, "u1k20")
5664
}
5765
}
66+
67+
func testRegion() {
68+
let expectedHashes: [String] = ["u14zr","u14yd","u16bj","u14yg","u16b4","u14zd","u16bc","u14yf","u16bd","u14zb","u14xn","u14zh","u14zp","u16b6","u16bz","u14xq","u14wy","u14xy","u14wx","u14zq","u16bh","u14zc","u16b3","u16bv","u14xp","u14zv","u14zf","u14wz","u14xz","u14ww","u16b2","u168w","u16bq","u16bb","u16bk","u14zs","u14yx","u16b1","u14ye","u14zm","u14zy","u14yv","u16bg","u16bn","u16bt","u14zu","u16b7","u14yc","u14xx","u16bx","u14z3","u14y9","u14z9","u14zn","u16br","u16be","u16bf","u14yu","u16b9","u14xr","u16b8","u16b5","u14z8","u14yy","u14zg","u168n","u14zx","u14z5","u14y8","u14zt","u168x","u14zz","u14z0","u16bm","u14zj","u14yt","u168q","u168z","u14xw","u16b0","u14z2","u14yz","u14ze","u16bs","u14z6","u14z7","u14ys","u14zw","u14z1","u14yb","u16bw","u16bp","u14zk","u14z4","u16by","u168r","u14yw","u168p","u168y","u16bu"].sorted()
69+
let center = CLLocationCoordinate2D(latitude: 52, longitude: 4)
70+
var hashes: [String] = []
71+
measure {
72+
hashes = Geohash.hashesForRegion(centerCoordinate: center, latitudeDelta: 0.4, longitudeDelta: 0.4, length: 5)
73+
}
74+
XCTAssertEqual(hashes.sorted(), expectedHashes)
75+
}
76+
77+
func testSmallRegionReturnsNonEmptyArray() {
78+
let hashes = Geohash.hashesForRegion(centerCoordinate: .init(latitude: 52, longitude: 5), latitudeDelta: 0.001, longitudeDelta: 0.001, length: 6)
79+
XCTAssertGreaterThan(hashes.count, 0)
80+
}
81+
82+
func testLargeRegionReturnsManyHashes() {
83+
let hashes = Geohash.hashesForRegion(centerCoordinate: .init(latitude: 52, longitude: 5), latitudeDelta: 0.5, longitudeDelta: 0.5, length: 6)
84+
XCTAssertGreaterThan(hashes.count, 1000)
85+
}
86+
87+
func testFreeingHashArrayWorks() {
88+
var array = GEOHASH_hashes_for_region(52, 5, 0.02, 0.02, 6)
89+
GEOHASH_free_array(&array)
90+
91+
XCTAssertEqual(array.count, 0, "After free, count should be zero")
92+
XCTAssertNil(array.hashes, "After free, hashes should be nil")
93+
XCTAssertEqual(array.capacity, 0, "After free, capacity should be zero")
94+
}
95+
96+
func testEdgeAlignedRegion() {
97+
let hashes = Geohash.hashesForRegion(centerCoordinate: .init(latitude: 0, longitude: 0), latitudeDelta: 0.01, longitudeDelta: 0.01, length: 6)
98+
.sorted()
99+
.joined(separator: ",")
100+
XCTAssertEqual(hashes, "7zzzzz,ebpbpb,kpbpbp,s00000")
101+
}
58102
}
59103

60104
private extension CLLocationCoordinate2D {

0 commit comments

Comments
 (0)