Skip to content

Commit

Permalink
Add adjacent and neighbors functions
Browse files Browse the repository at this point in the history
  • Loading branch information
JacobHearst committed Oct 31, 2021
1 parent b2fe649 commit afa917a
Show file tree
Hide file tree
Showing 2 changed files with 112 additions and 22 deletions.
25 changes: 25 additions & 0 deletions Sources/Geohash/Extensions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//
// File.swift
//
//
// Created by Jacob Hearst on 10/31/21.
//

import Foundation

extension StringProtocol {
func distance(of element: Element) -> Int? { firstIndex(of: element)?.distance(in: self) }
func distance<S: StringProtocol>(of string: S) -> Int? { range(of: string)?.lowerBound.distance(in: self) }

subscript(offset: Int) -> Character {
self[index(startIndex, offsetBy: offset)]
}
}

extension Collection {
func distance(to index: Index) -> Int { distance(from: startIndex, to: index) }
}

extension String.Index {
func distance<S: StringProtocol>(in string: S) -> Int { string.distance(to: self) }
}
109 changes: 87 additions & 22 deletions Sources/Geohash/Geohash.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@ import Foundation
public struct Geohash {
public static func decode(hash: String) -> (latitude: (min: Double, max: Double), longitude: (min: Double, max: Double))? {
// For example: hash = u4pruydqqvj

let bits = hash.map { bitmap[$0] ?? "?" }.joined(separator: "")
guard bits.count % 5 == 0 else { return nil }
// bits = 1101000100101011011111010111100110010110101101101110001

let (lat, lon) = bits.enumerated().reduce(into: ([Character](), [Character]())) {
if $1.0 % 2 == 0 {
$0.1.append($1.1)
Expand All @@ -39,24 +39,24 @@ public struct Geohash {
}
// lat = [1,1,0,1,0,0,0,1,1,1,1,1,1,1,0,1,0,1,1,0,0,1,1,0,1,0,0]
// lon = [1,0,0,0,0,1,1,1,0,1,1,0,0,1,1,0,1,0,0,1,1,1,0,1,1,1,0,1]

func combiner(array a: (min: Double, max: Double), value: Character) -> (Double, Double) {
let mean = (a.min + a.max) / 2
return value == "1" ? (mean, a.max) : (a.min, mean)
}

let latRange = lat.reduce((-90.0, 90.0), combiner)
// latRange = (57.649109959602356, 57.649111300706863)

let lonRange = lon.reduce((-180.0, 180.0), combiner)
// lonRange = (10.407439023256302, 10.407440364360809)

return (latRange, lonRange)
}

public static func encode(latitude: Double, longitude: Double, length: Int) -> String {
// For example: (latitude, longitude) = (57.6491106301546, 10.4074396938086)

func combiner(array a: (min: Double, max: Double, array: [String]), value: Double) -> (Double, Double, [String]) {
let mean = (a.min + a.max) / 2
if value < mean {
Expand All @@ -65,39 +65,39 @@ public struct Geohash {
return (mean, a.max, a.array + "1")
}
}

let lat = Array(repeating: latitude, count: length*5).reduce((-90.0, 90.0, [String]()), combiner)
// lat = (57.64911063015461, 57.649110630154766, [1,1,0,1,0,0,0,1,1,1,1,1,1,1,0,1,0,1,1,0,0,1,1,0,1,0,0,1,0,0,...])

let lon = Array(repeating: longitude, count: length*5).reduce((-180.0, 180.0, [String]()), combiner)
// lon = (10.407439693808236, 10.407439693808556, [1,0,0,0,0,1,1,1,0,1,1,0,0,1,1,0,1,0,0,1,1,1,0,1,1,1,0,1,0,1,..])

let latlon = lon.2.enumerated().flatMap { [$1, lat.2[$0]] }
// latlon - [1,1,0,1,0,0,0,1,0,0,1,0,1,0,1,1,0,1,1,1,1,1,0,1,0,1,1,1,1,...]

let bits = latlon.enumerated().reduce([String]()) { $1.0 % 5 > 0 ? $0 << $1.1 : $0 + $1.1 }
// bits: [11010,00100,10101,10111,11010,11110,01100,10110,10110,11011,10001,10010,10101,...]

let arr = bits.compactMap { charmap[$0] }
// arr: [u,4,p,r,u,y,d,q,q,v,j,k,p,b,...]

return String(arr.prefix(length))
}

// MARK: Private

private static let bitmap = "0123456789bcdefghjkmnpqrstuvwxyz".enumerated()
.map {
($1, String(integer: $0, radix: 2, padding: 5))
}
.reduce(into: [Character: String]()) {
$0[$1.0] = $1.1
}

}
private static let charmap = bitmap
.reduce(into: [String: Character]()) {
$0[$1.1] = $1.0
}
}
}

public extension Geohash {
Expand All @@ -114,7 +114,7 @@ public extension Geohash {
case sixtyCentimeters // ±0.00060 km
case seventyFourMillimeters // ±0.000074 km
}

static func encode(latitude: Double, longitude: Double, precision: Precision) -> String {
return encode(latitude: latitude, longitude: longitude, length: precision.rawValue)
}
Expand Down Expand Up @@ -156,14 +156,79 @@ public extension CLLocationCoordinate2D {
self = kCLLocationCoordinate2DInvalid
}
}

func geohash(length: Int) -> String {
return Geohash.encode(latitude: latitude, longitude: longitude, length: length)
}

func geohash(precision: Geohash.Precision) -> String {
return geohash(length: precision.rawValue)
}
}

#endif


public extension Geohash {
private static var base32 = "0123456789bcdefghjkmnpqrstuvwxyz"
enum Direction: String {
case n, e, s, w

var neighbor: [String] {
switch self {
case .n:
return ["p0r21436x8zb9dcf5h7kjnmqesgutwvy", "bc01fg45238967deuvhjyznpkmstqrwx"]
case .e:
return ["bc01fg45238967deuvhjyznpkmstqrwx", "p0r21436x8zb9dcf5h7kjnmqesgutwvy"]
case .s:
return ["14365h7k9dcfesgujnmqp0r2twvyx8zb", "238967debc01fg45kmstqrwxuvhjyznp"]
case .w:
return ["238967debc01fg45kmstqrwxuvhjyznp", "14365h7k9dcfesgujnmqp0r2twvyx8zb"]
}
}

var border: [String] {
switch self {
case .n:
return ["prxz", "bcfguvyz"]
case .e:
return ["bcfguvyz", "prxz"]
case .s:
return ["028b", "0145hjnp"]
case .w:
return ["0145hjnp", "028b"]
}
}
}

static func adjacent(geohash: String, direction: Direction) throws -> String {
let lastChar = geohash.last!
var parent = String(geohash.dropLast())
let type = geohash.count % 2;

// Check for edge-cases which don"t share common prefix
if direction.border[type].contains(lastChar) && !parent.isEmpty {
parent = try Geohash.adjacent(geohash: parent, direction: direction)
}

// Append letter for direction to parent
let charIndex = direction.neighbor[type].distance(of: lastChar)!

return parent + String(base32[charIndex])
}

static func neighbors(geohash: String) throws -> [String] {
let n = try adjacent(geohash: geohash, direction: .n)
let e = try adjacent(geohash: geohash, direction: .e)
let s = try adjacent(geohash: geohash, direction: .s)
let w = try adjacent(geohash: geohash, direction: .w)

return [
n, e, s, w,
try adjacent(geohash: n, direction: .e), // ne
try adjacent(geohash: s, direction: .e), // se
try adjacent(geohash: n, direction: .w), // nw
try adjacent(geohash: s, direction: .w) // sw
]
}
}

0 comments on commit afa917a

Please sign in to comment.