Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

geohash.gno

9.49 Kb · 323 lines
  1// Package geohash encodes a latitude/longitude pair into the short base32
  2// string known as a geohash, decodes one back into the cell it names, and
  3// walks from a cell to the eight around it.
  4//
  5// A geohash is a prefix code: every character narrows the box, so two places
  6// that share a long prefix are close together, and truncating a hash zooms
  7// out. That is the whole reason to use one on chain, where an avl tree keyed
  8// by geohash gives a cheap "everything near here" range scan without storing
  9// any geometry: Get the prefix, iterate, done.
 10//
 11//	hash, _ := geohash.Encode(42.6, -5.6, 5)   // "ezs42"
 12//	box, _ := geohash.DecodeBox("ezs42")       // the cell it names
 13//	north, _ := geohash.Neighbor("ezs42", geohash.North)
 14//
 15// The alphabet is the standard one and deliberately drops a, i, l and o, so a
 16// hash survives being read aloud or written down. Anything outside it is
 17// refused rather than quietly skipped.
 18//
 19// Everything here is pure arithmetic on exact binary fractions: both world
 20// ranges are powers of two divided by powers of two, so every box bound is
 21// representable in a float64 and two identical calls can never disagree. No
 22// map is iterated and no value is rounded, which is what makes a Render built
 23// on this deterministic.
 24//
 25// A live demo of this package is at
 26// [r/moul/x/daily/geohashdemo](/r/moul/x/daily/geohashdemo/v0).
 27package geohash
 28
 29import "errors"
 30
 31// Alphabet is the standard geohash base32 alphabet. It omits a, i, l and o so
 32// that a hash read aloud or copied by hand stays unambiguous.
 33const Alphabet = "0123456789bcdefghjkmnpqrstuvwxyz"
 34
 35// MaxPrecision is the longest hash this package will produce or accept: 12
 36// characters, 60 bits, a cell well under a millimetre across. Past that the
 37// extra characters say nothing a float64 can still distinguish.
 38const MaxPrecision = 12
 39
 40var (
 41	// ErrPrecision is returned by Encode for a length outside 1..MaxPrecision.
 42	ErrPrecision = errors.New("geohash: precision out of range")
 43	// ErrLatitude is returned by Encode for a latitude outside -90..90.
 44	ErrLatitude = errors.New("geohash: latitude out of range")
 45	// ErrLongitude is returned by Encode for a longitude outside -180..180.
 46	ErrLongitude = errors.New("geohash: longitude out of range")
 47	// ErrEmpty is returned when a hash is required and none was given.
 48	ErrEmpty = errors.New("geohash: empty hash")
 49	// ErrTooLong is returned for a hash longer than MaxPrecision.
 50	ErrTooLong = errors.New("geohash: hash longer than the maximum precision")
 51	// ErrInvalidChar is returned for a character outside Alphabet, which
 52	// includes the four letters the alphabet deliberately leaves out.
 53	ErrInvalidChar = errors.New("geohash: character is not in the geohash alphabet")
 54	// ErrDirection is returned by Neighbor for a Direction it does not know.
 55	ErrDirection = errors.New("geohash: unknown direction")
 56	// ErrNoNeighbor is returned by Neighbor when the step leaves the world:
 57	// north of the top row, or south of the bottom one. Longitude wraps and
 58	// never produces this.
 59	ErrNoNeighbor = errors.New("geohash: no neighbour in that direction")
 60)
 61
 62// Box is the rectangle of latitudes and longitudes a geohash names. A geohash
 63// is a box, never a point, and the shorter the hash the larger the box.
 64type Box struct {
 65	MinLat, MaxLat float64
 66	MinLon, MaxLon float64
 67}
 68
 69// Center returns the midpoint of the box, which is what Decode reports.
 70func (b Box) Center() (lat, lon float64) {
 71	return (b.MinLat + b.MaxLat) / 2, (b.MinLon + b.MaxLon) / 2
 72}
 73
 74// Contains reports whether a point falls inside the box. The lower and left
 75// edges belong to the box and the upper and right ones do not, matching how
 76// Encode assigns a point sitting exactly on a midpoint.
 77func (b Box) Contains(lat, lon float64) bool {
 78	return lat >= b.MinLat && lat < b.MaxLat && lon >= b.MinLon && lon < b.MaxLon
 79}
 80
 81// HeightDeg is the box's north-south extent in degrees.
 82func (b Box) HeightDeg() float64 { return b.MaxLat - b.MinLat }
 83
 84// WidthDeg is the box's east-west extent in degrees.
 85func (b Box) WidthDeg() float64 { return b.MaxLon - b.MinLon }
 86
 87// Direction names one of the eight cells around a geohash. The values are
 88// ordered clockwise from north, so adding four modulo eight is the reverse.
 89type Direction int
 90
 91// The eight directions, clockwise from north.
 92const (
 93	North Direction = iota
 94	NorthEast
 95	East
 96	SouthEast
 97	South
 98	SouthWest
 99	West
100	NorthWest
101)
102
103// String returns the lowercase hyphenated name, or "unknown" for a value that
104// is not one of the eight.
105func (d Direction) String() string {
106	switch d {
107	case North:
108		return "north"
109	case NorthEast:
110		return "north-east"
111	case East:
112		return "east"
113	case SouthEast:
114		return "south-east"
115	case South:
116		return "south"
117	case SouthWest:
118		return "south-west"
119	case West:
120		return "west"
121	case NorthWest:
122		return "north-west"
123	}
124	return "unknown"
125}
126
127// step returns the latitude and longitude offsets, in whole cells, for a
128// direction. The second result is false for a direction that is not one of
129// the eight.
130func (d Direction) step() (dLat, dLon float64, ok bool) {
131	switch d {
132	case North:
133		return 1, 0, true
134	case NorthEast:
135		return 1, 1, true
136	case East:
137		return 0, 1, true
138	case SouthEast:
139		return -1, 1, true
140	case South:
141		return -1, 0, true
142	case SouthWest:
143		return -1, -1, true
144	case West:
145		return 0, -1, true
146	case NorthWest:
147		return 1, -1, true
148	}
149	return 0, 0, false
150}
151
152// Encode returns the geohash of a point, precision characters long.
153//
154// Longitude takes the first bit and the two alternate, so an odd number of
155// characters gives longitude one bit more than latitude and the cell comes out
156// roughly square in degrees. A point sitting exactly on a dividing line goes
157// to the upper, or eastern, half.
158func Encode(lat, lon float64, precision int) (string, error) {
159	if precision < 1 || precision > MaxPrecision {
160		return "", ErrPrecision
161	}
162	if lat < -90 || lat > 90 {
163		return "", ErrLatitude
164	}
165	if lon < -180 || lon > 180 {
166		return "", ErrLongitude
167	}
168
169	latMin, latMax := -90.0, 90.0
170	lonMin, lonMax := -180.0, 180.0
171
172	out := make([]byte, 0, precision)
173	ch, bit, even := 0, 0, true
174	for len(out) < precision {
175		if even {
176			mid := (lonMin + lonMax) / 2
177			if lon >= mid {
178				ch |= 1 << uint(4-bit)
179				lonMin = mid
180			} else {
181				lonMax = mid
182			}
183		} else {
184			mid := (latMin + latMax) / 2
185			if lat >= mid {
186				ch |= 1 << uint(4-bit)
187				latMin = mid
188			} else {
189				latMax = mid
190			}
191		}
192		even = !even
193		if bit < 4 {
194			bit++
195			continue
196		}
197		out = append(out, Alphabet[ch])
198		ch, bit = 0, 0
199	}
200	return string(out), nil
201}
202
203// Decode returns the centre of the cell a geohash names. The centre is the
204// best single point a hash can give: the cell itself is what was encoded, and
205// DecodeBox is how to ask how big it is.
206func Decode(hash string) (lat, lon float64, err error) {
207	b, err := DecodeBox(hash)
208	if err != nil {
209		return 0, 0, err
210	}
211	lat, lon = b.Center()
212	return lat, lon, nil
213}
214
215// DecodeBox returns the cell a geohash names.
216func DecodeBox(hash string) (Box, error) {
217	if hash == "" {
218		return Box{}, ErrEmpty
219	}
220	if len(hash) > MaxPrecision {
221		return Box{}, ErrTooLong
222	}
223
224	b := Box{MinLat: -90, MaxLat: 90, MinLon: -180, MaxLon: 180}
225	even := true
226	for i := 0; i < len(hash); i++ {
227		v, ok := symbol(hash[i])
228		if !ok {
229			return Box{}, ErrInvalidChar
230		}
231		for _, mask := range [5]int{16, 8, 4, 2, 1} {
232			if even {
233				mid := (b.MinLon + b.MaxLon) / 2
234				if v&mask != 0 {
235					b.MinLon = mid
236				} else {
237					b.MaxLon = mid
238				}
239			} else {
240				mid := (b.MinLat + b.MaxLat) / 2
241				if v&mask != 0 {
242					b.MinLat = mid
243				} else {
244					b.MaxLat = mid
245				}
246			}
247			even = !even
248		}
249	}
250	return b, nil
251}
252
253// Neighbor returns the geohash of the cell one step away, at the same
254// precision.
255//
256// It works by walking one whole cell from the centre and re-encoding, which is
257// exact: every cell bound is a binary fraction, so the step lands on another
258// centre rather than near one.
259//
260// Longitude wraps, so the cell east of the last column is the first column
261// again: the antimeridian is a line on a map, not an edge of the world. The
262// classic lookup-table implementations report nothing there, which is a
263// property of the tables and not of the globe. Latitude genuinely stops, and
264// a step past a pole returns ErrNoNeighbor.
265func Neighbor(hash string, d Direction) (string, error) {
266	dLat, dLon, ok := d.step()
267	if !ok {
268		return "", ErrDirection
269	}
270	b, err := DecodeBox(hash)
271	if err != nil {
272		return "", err
273	}
274	lat, lon := b.Center()
275
276	lat += dLat * b.HeightDeg()
277	if lat > 90 || lat < -90 {
278		return "", ErrNoNeighbor
279	}
280	lon += dLon * b.WidthDeg()
281	if lon > 180 {
282		lon -= 360
283	}
284	if lon < -180 {
285		lon += 360
286	}
287	return Encode(lat, lon, len(hash))
288}
289
290// Neighbors returns the cells around a geohash, clockwise from north.
291//
292// The result is short rather than padded when a direction has no cell, which
293// only happens at the poles: a caller iterating the result never has to filter
294// an empty string out of it.
295func Neighbors(hash string) ([]string, error) {
296	if _, err := DecodeBox(hash); err != nil {
297		return nil, err
298	}
299	out := make([]string, 0, 8)
300	for d := North; d <= NorthWest; d++ {
301		n, err := Neighbor(hash, d)
302		if err == ErrNoNeighbor {
303			continue
304		}
305		if err != nil {
306			return nil, err
307		}
308		out = append(out, n)
309	}
310	return out, nil
311}
312
313// symbol maps an alphabet character to its five-bit value. It is a linear scan
314// rather than a map because the alphabet is 32 bytes long and a package-level
315// map would be one more object for every realm that imports this.
316func symbol(c byte) (int, bool) {
317	for i := 0; i < len(Alphabet); i++ {
318		if Alphabet[i] == c {
319			return i, true
320		}
321	}
322	return 0, false
323}