Skip to content

Commit 9d816e0

Browse files
committed
feat: add geohash functionality
1 parent b4e5317 commit 9d816e0

File tree

3 files changed

+157
-18
lines changed

3 files changed

+157
-18
lines changed

README.md

-13
Original file line numberDiff line numberDiff line change
@@ -9,27 +9,14 @@ If this is a brand new project, make sure to create a `package.json` first with
99
Installation is done using the [`npm install` command](https://docs.npmjs.com/getting-started/installing-npm-packages-locally):
1010

1111
### Using npm:
12-
1312
npm install valleyed
1413

1514
### Using yarn:
16-
1715
yarn add valleyed
1816

1917
### Using CDN:
20-
2118
[Valleyed jsDelivr CDN](https://www.jsdelivr.com/package/npm/valleyed)
2219

23-
### Using Skypack:
24-
25-
[Valleyed Skypack CDN](https://www.skypack.dev/view/valleyed)
26-
27-
## Contributing
28-
29-
[Contributing Guide](Contributing.md)
30-
31-
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/kevinand11/valleyed) 
32-
3320

3421
## Basic Usage
3522

src/index.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
export * from './api'
2-
3-
export { Validator } from './validators'
4-
export * from './utils/rules'
5-
export * from './utils/differ'
62
export * from './rules'
7-
export * from './utils/functions'
3+
export * from './utils/differ'
4+
export * from './utils/functions'
5+
export * from './utils/geohash'
6+
export * from './utils/rules'
7+
export { Validator } from './validators'

src/utils/geohash.ts

+152
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import { v } from '../api'
2+
3+
const Base32Chars = '0123456789bcdefghjkmnpqrstuvwxyz'
4+
const Base2Chars = '01'
5+
6+
const base10ToBaseX = (num: number, base: number, chars: string) => {
7+
const charsObj = Object.fromEntries(
8+
chars.toLowerCase().split('')
9+
.map((value, index) => [index, value])
10+
)
11+
const bits: string[] = []
12+
if (num === 0) return '0'
13+
while (num > 0) {
14+
bits.push(charsObj[num % base] ?? '')
15+
num = Math.floor(num / base)
16+
}
17+
return bits.reverse().join('')
18+
}
19+
20+
const baseXToBase10 = (num: string, base: number, chars: string) => {
21+
const charsObj = Object.fromEntries(
22+
chars.toLowerCase().split('')
23+
.map((value, index) => [value, index])
24+
)
25+
26+
return num
27+
.toLowerCase()
28+
.split('')
29+
.reduce((acc, cur, index, arr) => {
30+
const pos = charsObj[cur] ?? 0
31+
return acc + pos * Math.pow(base, arr.length - index - 1)
32+
}, 0)
33+
}
34+
35+
const dichotomy = (min: number, max: number, bits: string) => {
36+
const res = bits
37+
.split('')
38+
.concat('')
39+
.reduce((acc, cur) => {
40+
const mid = (min + max) / 2
41+
const error = (max - min) / 2
42+
acc.mid = mid
43+
acc.error = error
44+
if (cur === '1') min = mid
45+
else max = mid
46+
return acc
47+
}, { mid: 0, error: 0 })
48+
const value = res.mid - res.error
49+
return { value, interval: res.error * 2 }
50+
}
51+
52+
export class Geohash {
53+
static #coords (hash: string) {
54+
const base10 = baseXToBase10(hash, 32, Base32Chars)
55+
const base2 = base10ToBaseX(base10, 2, Base2Chars)
56+
.padStart(hash.length * 5, '0')
57+
58+
const coords = base2
59+
.split('')
60+
.reduce((acc, cur, index) => {
61+
if (index % 2 === 0) {
62+
acc.long += cur
63+
} else {
64+
acc.lat += cur
65+
}
66+
return acc
67+
}, { lat: '', long: '' })
68+
69+
return coords
70+
}
71+
72+
static decode (hash: string): [number, number] {
73+
const valid = v.string().min(1).parse(hash)
74+
if (!valid.valid) throw new Error(valid.errors[0])
75+
76+
const coords = this.#coords(hash)
77+
const lat = dichotomy(-90, 90, coords.lat).value
78+
const long = dichotomy(-180, 180, coords.long).value
79+
return [lat, long]
80+
}
81+
82+
static encode (coords: [number, number]): string {
83+
const valid = v.tuple([v.number(), v.number()]).parse(coords)
84+
if (!valid.valid) throw new Error(valid.errors[0])
85+
86+
let idx = 0, bit = 0,
87+
evenBit = true, geohash = ''
88+
const mins = [-90, -180], maxs = [90, 180]
89+
90+
while (geohash.length < 18) {
91+
const key = evenBit ? 1 : 0
92+
const mid = (mins[key] + maxs[key]) / 2
93+
if (coords[key] >= mid) {
94+
idx = idx * 2 + 1
95+
mins[key] = mid
96+
} else {
97+
idx = idx * 2
98+
maxs[key] = mid
99+
}
100+
evenBit = !evenBit
101+
102+
bit += 1
103+
if (bit === 5) {
104+
geohash += base10ToBaseX(idx, 32, Base32Chars)
105+
bit = idx = 0
106+
}
107+
}
108+
return geohash.replace(/0+$/, '')
109+
}
110+
111+
static neighbors (hash: string) {
112+
const valid = v.string().min(1).parse(hash)
113+
if (!valid.valid) throw new Error(valid.errors[0])
114+
115+
const coords = this.#coords(hash)
116+
const lat = dichotomy(-90, 90, coords.lat)
117+
const long = dichotomy(-180, 180, coords.long)
118+
const neighbors = [
119+
[lat.value - lat.interval, long.value - long.interval],
120+
[lat.value - lat.interval, long.value],
121+
[lat.value - lat.interval, long.value + long.interval],
122+
123+
[lat.value, long.value - long.interval],
124+
[lat.value, long.value + long.interval],
125+
126+
[lat.value + lat.interval, long.value - long.interval],
127+
[lat.value + lat.interval, long.value],
128+
[lat.value + lat.interval, long.value + long.interval],
129+
].map(([lat, long]) => {
130+
return Geohash.encode([
131+
this.#wrap(lat, 90),
132+
this.#wrap(long, 180)
133+
])
134+
})
135+
return {
136+
bl: neighbors[0],
137+
bc: neighbors[1],
138+
br: neighbors[2],
139+
cl: neighbors[3],
140+
cr: neighbors[4],
141+
tl: neighbors[5],
142+
tc: neighbors[6],
143+
tr: neighbors[7],
144+
}
145+
}
146+
147+
static #wrap (num: number, base: number) {
148+
if (num < -base) num = num + base * 2
149+
if (num > +base) num = num - base * 2
150+
return num
151+
}
152+
}

0 commit comments

Comments
 (0)