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