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.

geohashdemo.gno

7.72 Kb · 199 lines
  1// Package geohashdemo is a small gnoweb demo of the
  2// [p/moul/x/daily/geohash](/p/moul/x/daily/geohash/v0) library: it shows a
  3// point encoded at growing precision, the box a hash actually names, and the
  4// eight cells around one, including the wrap at the antimeridian.
  5//
  6// It contains no geohash logic of its own. Stateless and read-only, so Render
  7// is fully deterministic and takes nothing from a caller.
  8package geohashdemo
  9
 10import (
 11	"strconv"
 12	"strings"
 13
 14	"gno.land/p/g1magld5ptgzaktqcexxq0pgq5lmdf0kz6re8hlv/x/daily/geohash/v0"
 15)
 16
 17// The three fixed points the demo encodes. Coordinates only, so nothing here
 18// depends on the chain, the clock or the caller.
 19const (
 20	jutlandLat, jutlandLon = 57.64911, 10.40744
 21	leonLat, leonLon       = 42.6, -5.6
 22	sampleHash             = "ezs42"
 23)
 24
 25// Render renders the demo for gnoweb.
 26//
 27//	Render("") / Render("/")  -> a point at growing precision
 28//	Render("/cell")           -> the box a hash names
 29//	Render("/neighbors")      -> the eight cells around one, and the wrap
 30func Render(path string) string {
 31	var b strings.Builder
 32	b.WriteString("# Geohash\n\n")
 33	b.WriteString("Turning a latitude and longitude into a short base32 string, ")
 34	b.WriteString("demoing the [`p/moul/x/daily/geohash`](/p/moul/x/daily/geohash/v0) library.\n\n")
 35
 36	switch parseArg(path) {
 37	case "cell":
 38		b.WriteString(renderCell())
 39	case "neighbors":
 40		b.WriteString(renderNeighbors())
 41	default:
 42		b.WriteString(renderPrecision())
 43	}
 44	return b.String()
 45}
 46
 47// renderPrecision shows one point at every length, which is the property that
 48// makes a geohash worth storing: the prefix is the same place, zoomed out.
 49func renderPrecision() string {
 50	var b strings.Builder
 51	b.WriteString("## One point, at growing precision\n\n")
 52	b.WriteString("Jutland, Denmark, at ")
 53	b.WriteString(coord(jutlandLat, jutlandLon))
 54	b.WriteString(":\n\n```\n")
 55	for p := 1; p <= 11; p++ {
 56		h, err := geohash.Encode(jutlandLat, jutlandLon, p)
 57		if err != nil {
 58			return b.String() + "```\n_unexpected error: " + err.Error() + "_\n"
 59		}
 60		b.WriteString(pad(strconv.Itoa(p), 2))
 61		b.WriteString("  ")
 62		b.WriteString(h)
 63		b.WriteString("\n")
 64	}
 65	b.WriteString("```\n\n")
 66	b.WriteString("Every character adds five bits and every prefix is itself a valid ")
 67	b.WriteString("geohash of the same place, so truncating one zooms out. Two hashes ")
 68	b.WriteString("sharing a long prefix are close together, which is what lets an avl ")
 69	b.WriteString("tree keyed this way answer \"everything near here\" with a range scan ")
 70	b.WriteString("and no geometry at all.\n\n")
 71	b.WriteString("Two more places, at the length each one needs:\n\n")
 72	b.WriteString(bullet("Null Island", 0, 0, 5))
 73	b.WriteString(bullet("Leon, Spain", leonLat, leonLon, 5))
 74	b.WriteString(bullet("Jutland, Denmark", jutlandLat, jutlandLon, 6))
 75	b.WriteString("\n> Next: [`/cell`](/r/moul/x/daily/geohashdemo/v0:cell) for the box a ")
 76	b.WriteString("hash names, [`/neighbors`](/r/moul/x/daily/geohashdemo/v0:neighbors) ")
 77	b.WriteString("for the eight cells around one.\n")
 78	return b.String()
 79}
 80
 81// renderCell shows that a geohash is a box and not a point, which is the thing
 82// most callers get wrong about it.
 83func renderCell() string {
 84	var b strings.Builder
 85	box, err := geohash.DecodeBox(sampleHash)
 86	if err != nil {
 87		return "_unexpected error: " + err.Error() + "_\n"
 88	}
 89	b.WriteString("## The box `" + sampleHash + "` names\n\n")
 90	b.WriteString("A geohash is a box, never a point, and the shorter the hash the bigger ")
 91	b.WriteString("the box.\n\n```\n")
 92	b.WriteString("latitude   " + num(box.MinLat) + " .. " + num(box.MaxLat) + "\n")
 93	b.WriteString("longitude  " + num(box.MinLon) + " .. " + num(box.MaxLon) + "\n")
 94	b.WriteString("size       " + num(box.HeightDeg()) + " by " + num(box.WidthDeg()) + " degrees\n")
 95	b.WriteString("```\n\n")
 96	lat, lon := box.Center()
 97	b.WriteString("`Decode` reports the centre, " + coord(lat, lon) + ", because the centre ")
 98	b.WriteString("is the best single point a box can give. The coordinates that produced ")
 99	b.WriteString("the hash, " + coord(leonLat, leonLon) + ", are inside it: ")
100	b.WriteString(yesNo(box.Contains(leonLat, leonLon)) + ".\n\n")
101	b.WriteString("Null Island is not: " + yesNo(box.Contains(0, 0)) + ".\n\n")
102	b.WriteString("> Next: [`/neighbors`](/r/moul/x/daily/geohashdemo/v0:neighbors).\n")
103	return b.String()
104}
105
106// renderNeighbors shows the eight cells around one, and the wrap that the
107// classic lookup-table implementations do not do.
108func renderNeighbors() string {
109	var b strings.Builder
110	b.WriteString("## The eight cells around `" + sampleHash + "`\n\n```\n")
111	// The grid read as a map: north at the top, east on the right. The centre
112	// is the hash itself, so it is not a direction.
113	rows := [3][]geohash.Direction{
114		{geohash.NorthWest, geohash.North, geohash.NorthEast},
115		{geohash.West, geohash.East},
116		{geohash.SouthWest, geohash.South, geohash.SouthEast},
117	}
118	for i, row := range rows {
119		cells := make([]string, 0, 3)
120		for _, d := range row {
121			n, err := geohash.Neighbor(sampleHash, d)
122			if err != nil {
123				return b.String() + "```\n_unexpected error: " + err.Error() + "_\n"
124			}
125			cells = append(cells, n)
126		}
127		if i == 1 {
128			// Put the hash itself back in the middle of the middle row.
129			cells = []string{cells[0], sampleHash, cells[1]}
130		}
131		b.WriteString(strings.Join(cells, " ") + "\n")
132	}
133	b.WriteString("```\n\n")
134	b.WriteString("Each one is computed by walking exactly one cell from the centre and ")
135	b.WriteString("re-encoding. Every bound is an exact binary fraction, so the step lands ")
136	b.WriteString("on another centre rather than near one, and stepping back is the identity.\n\n")
137	b.WriteString("### The antimeridian is not an edge of the world\n\n")
138	east, err := geohash.Neighbor("z", geohash.East)
139	if err != nil {
140		return b.String() + "_unexpected error: " + err.Error() + "_\n"
141	}
142	b.WriteString("`z` is the top right corner cell of the world. The cell east of it is ")
143	b.WriteString("`" + east + "`, the top left one, because longitude wraps. The classic ")
144	b.WriteString("lookup-table implementations report nothing there, which is a property ")
145	b.WriteString("of the tables and not of the globe.\n\n")
146	around, err := geohash.Neighbors("z")
147	if err != nil {
148		return b.String() + "_unexpected error: " + err.Error() + "_\n"
149	}
150	b.WriteString("Latitude genuinely stops, though, so `z` has no northern neighbour and ")
151	b.WriteString("`Neighbors` returns " + strconv.Itoa(len(around)) + " cells rather than eight: ")
152	b.WriteString("`" + strings.Join(around, "`, `") + "`.\n\n")
153	b.WriteString("> Back to [the overview](/r/moul/x/daily/geohashdemo/v0).\n")
154	return b.String()
155}
156
157// bullet renders one named place as a list item.
158func bullet(name string, lat, lon float64, precision int) string {
159	h, err := geohash.Encode(lat, lon, precision)
160	if err != nil {
161		return "- " + name + ": _unexpected error: " + err.Error() + "_\n"
162	}
163	return "- " + name + " (" + coord(lat, lon) + "): `" + h + "`\n"
164}
165
166// coord formats a latitude and longitude pair.
167func coord(lat, lon float64) string { return num(lat) + ", " + num(lon) }
168
169// num formats a float with no trailing zeros. Every value this demo prints is
170// an exact binary fraction, so the shortest round-trip form is also the exact
171// one.
172func num(v float64) string { return strconv.FormatFloat(v, 'f', -1, 64) }
173
174// yesNo renders a containment answer as a sentence fragment.
175func yesNo(ok bool) string {
176	if ok {
177		return "yes"
178	}
179	return "no"
180}
181
182// pad right-aligns a short string to width n, so the precision column lines up
183// inside the code block.
184func pad(s string, n int) string {
185	for len(s) < n {
186		s = " " + s
187	}
188	return s
189}
190
191// parseArg extracts the first path segment.
192func parseArg(path string) string {
193	s := strings.TrimSpace(path)
194	s = strings.TrimPrefix(s, "/")
195	if i := strings.IndexByte(s, '/'); i >= 0 {
196		s = s[:i]
197	}
198	return s
199}