mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 06:05:20 +00:00
* Location notes: fix performance and UI issues Performance fixes: - Add Set-based duplicate detection (O(1) vs O(n) lookup) - Eliminates lag when receiving 200+ notes during EOSE Correctness fixes: - Fix optimistic echo timestamp to match signed event timestamp - Add echo IDs to noteIDs set for consistency UI improvements: - Remove duplicate "loading recent notes" text in header - Simplify toolbar icon color logic for immediate green indication - Icon now reliably turns green when notes exist in geohash Tests: All 3 LocationNotes tests passing * Location notes: add remaining robustness fixes Additional improvements: - Align counter/manager limits to 200 (prevents showing count higher than displayable) - Set loading state before clearing notes to eliminate UI flicker on geohash change - Add geohash validation for building-level precision (8 valid base32 chars) - Add defensive 500-note memory cap with automatic trimming - Clear stale notesGeohash state on sheet dismiss Tests: - Fix test geohashes to use valid base32 characters - All 3 LocationNotes tests passing --------- Co-authored-by: jack <jackjackbits@users.noreply.github.com>
123 lines
4.7 KiB
Swift
123 lines
4.7 KiB
Swift
import Foundation
|
|
|
|
/// Lightweight Geohash encoder used for Location Channels.
|
|
/// Encodes latitude/longitude to base32 geohash with a fixed precision.
|
|
enum Geohash {
|
|
private static let base32Chars = Array("0123456789bcdefghjkmnpqrstuvwxyz")
|
|
private static let base32Map: [Character: Int] = {
|
|
var map: [Character: Int] = [:]
|
|
for (i, c) in base32Chars.enumerated() { map[c] = i }
|
|
return map
|
|
}()
|
|
|
|
/// Validates a geohash string for building-level precision (8 characters).
|
|
/// - Parameter geohash: The geohash string to validate
|
|
/// - Returns: true if valid 8-character base32 geohash, false otherwise
|
|
static func isValidBuildingGeohash(_ geohash: String) -> Bool {
|
|
guard geohash.count == 8 else { return false }
|
|
return geohash.lowercased().allSatisfy { base32Map[$0] != nil }
|
|
}
|
|
|
|
/// Encodes the provided coordinates into a geohash string.
|
|
/// - Parameters:
|
|
/// - latitude: Latitude in degrees (-90...90)
|
|
/// - longitude: Longitude in degrees (-180...180)
|
|
/// - precision: Number of geohash characters (2-12 typical). Values <= 0 return an empty string.
|
|
/// - Returns: Base32 geohash string of length `precision`.
|
|
static func encode(latitude: Double, longitude: Double, precision: Int) -> String {
|
|
guard precision > 0 else { return "" }
|
|
|
|
var latInterval: (Double, Double) = (-90.0, 90.0)
|
|
var lonInterval: (Double, Double) = (-180.0, 180.0)
|
|
|
|
var isEven = true
|
|
var bit = 0
|
|
var ch = 0
|
|
var geohash: [Character] = []
|
|
|
|
let lat = max(-90.0, min(90.0, latitude))
|
|
let lon = max(-180.0, min(180.0, longitude))
|
|
|
|
while geohash.count < precision {
|
|
if isEven {
|
|
let mid = (lonInterval.0 + lonInterval.1) / 2
|
|
if lon >= mid {
|
|
ch |= (1 << (4 - bit))
|
|
lonInterval.0 = mid
|
|
} else {
|
|
lonInterval.1 = mid
|
|
}
|
|
} else {
|
|
let mid = (latInterval.0 + latInterval.1) / 2
|
|
if lat >= mid {
|
|
ch |= (1 << (4 - bit))
|
|
latInterval.0 = mid
|
|
} else {
|
|
latInterval.1 = mid
|
|
}
|
|
}
|
|
|
|
isEven.toggle()
|
|
if bit < 4 {
|
|
bit += 1
|
|
} else {
|
|
geohash.append(base32Chars[ch])
|
|
bit = 0
|
|
ch = 0
|
|
}
|
|
}
|
|
|
|
return String(geohash)
|
|
}
|
|
|
|
/// Decodes a geohash into the center latitude/longitude of its bounding box.
|
|
/// - Parameter geohash: Base32 geohash string.
|
|
/// - Returns: (lat, lon) center coordinate.
|
|
static func decodeCenter(_ geohash: String) -> (lat: Double, lon: Double) {
|
|
var latInterval: (Double, Double) = (-90.0, 90.0)
|
|
var lonInterval: (Double, Double) = (-180.0, 180.0)
|
|
|
|
var isEven = true
|
|
for ch in geohash.lowercased() {
|
|
guard let cd = base32Map[ch] else { continue }
|
|
for mask in [16, 8, 4, 2, 1] {
|
|
if isEven {
|
|
let mid = (lonInterval.0 + lonInterval.1) / 2
|
|
if (cd & mask) != 0 { lonInterval.0 = mid } else { lonInterval.1 = mid }
|
|
} else {
|
|
let mid = (latInterval.0 + latInterval.1) / 2
|
|
if (cd & mask) != 0 { latInterval.0 = mid } else { latInterval.1 = mid }
|
|
}
|
|
isEven.toggle()
|
|
}
|
|
}
|
|
let lat = (latInterval.0 + latInterval.1) / 2
|
|
let lon = (lonInterval.0 + lonInterval.1) / 2
|
|
return (lat, lon)
|
|
}
|
|
|
|
/// Decodes a geohash into its latitude and longitude bounds.
|
|
/// - Parameter geohash: Base32 geohash string.
|
|
/// - Returns: (latMin, latMax, lonMin, lonMax)
|
|
static func decodeBounds(_ geohash: String) -> (latMin: Double, latMax: Double, lonMin: Double, lonMax: Double) {
|
|
var latInterval: (Double, Double) = (-90.0, 90.0)
|
|
var lonInterval: (Double, Double) = (-180.0, 180.0)
|
|
|
|
var isEven = true
|
|
for ch in geohash.lowercased() {
|
|
guard let cd = base32Map[ch] else { continue }
|
|
for mask in [16, 8, 4, 2, 1] {
|
|
if isEven {
|
|
let mid = (lonInterval.0 + lonInterval.1) / 2
|
|
if (cd & mask) != 0 { lonInterval.0 = mid } else { lonInterval.1 = mid }
|
|
} else {
|
|
let mid = (latInterval.0 + latInterval.1) / 2
|
|
if (cd & mask) != 0 { latInterval.0 = mid } else { latInterval.1 = mid }
|
|
}
|
|
isEven.toggle()
|
|
}
|
|
}
|
|
return (latInterval.0, latInterval.1, lonInterval.0, lonInterval.1)
|
|
}
|
|
}
|