whitebox.ml 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  1. (*
  2. * geohash.ml
  3. *
  4. * Created by Marcus Rohrmoser on 11.03.21.
  5. * Copyright © 2021-2021 Marcus Rohrmoser mobile Software http://mro.name/~me. All rights reserved.
  6. *
  7. * This program is free software: you can redistribute it and/or modify
  8. * it under the terms of the GNU General Public License as published by
  9. * the Free Software Foundation, either version 3 of the License, or
  10. * (at your option) any later version.
  11. *
  12. * This program is distributed in the hope that it will be useful,
  13. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. * GNU General Public License for more details.
  16. *
  17. * You should have received a copy of the GNU General Public License
  18. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  19. *)
  20. (* Inspired by https://mmcloughlin.com/posts/geohash-assembly
  21. * and https://github.com/mmcloughlin/geohash/blob/master/geohash.go
  22. * and https://github.com/mmcloughlin/deconstructedgeohash/blob/master/geohash.go *)
  23. open Optint.Int63
  24. type wgs84_lat_lon = (float * float)
  25. type delta = (float * float)
  26. (*
  27. * 60 bits are fine, because
  28. * - 12 geohash characters equal 60 bit
  29. * - more chars would overflow 64 bit
  30. * - ocaml has efficient int63, enough for 60 bits
  31. *)
  32. (* wgs84 -> 30 bit geohash. https://mmcloughlin.com/posts/geohash-assembly#implementation *)
  33. let quantize30 (lat, lng) =
  34. let f r x = Float.ldexp ((x /. r) +. 0.5) 30 |> of_float in
  35. (f 180. lat, f 360. lng)
  36. (* 30 bit geohash -> wgs84. https://mmcloughlin.com/posts/geohash-assembly#implementation *)
  37. let dequantize30 (lat, lon) =
  38. let f r x = r *. (Float.ldexp (x |> to_float) (-30) -. 0.5) in
  39. (f 180. lat, f 360. lon)
  40. let x00000000ffffffff = of_int64 0x00000000ffffffffL
  41. let x0000ffff0000ffff = of_int64 0x0000ffff0000ffffL
  42. let x00ff00ff00ff00ff = of_int64 0x00ff00ff00ff00ffL
  43. let x0f0f0f0f0f0f0f0f = of_int64 0x0f0f0f0f0f0f0f0fL
  44. let x3333333333333333 = of_int64 0x3333333333333333L
  45. let x5555555555555555 = of_int64 0x3555555555555555L
  46. let interleave (x, y) =
  47. let spread x =
  48. let f s m x' = m |> logand (x' |> logor (shift_left x' s)) in
  49. x
  50. |> f 16 x0000ffff0000ffff
  51. |> f 8 x00ff00ff00ff00ff
  52. |> f 4 x0f0f0f0f0f0f0f0f
  53. |> f 2 x3333333333333333
  54. |> f 1 x5555555555555555
  55. in
  56. spread x |> logor (shift_left (spread y) 1)
  57. let deinterleave x =
  58. let squash x =
  59. let f s m x' = m |> logand (x' |> logor (shift_right x' s)) in
  60. x
  61. |> logand x5555555555555555
  62. |> f 1 x3333333333333333
  63. |> f 2 x0f0f0f0f0f0f0f0f
  64. |> f 4 x00ff00ff00ff00ff
  65. |> f 8 x0000ffff0000ffff
  66. |> f 16 x00000000ffffffff
  67. in
  68. (squash x, squash (shift_right x 1))
  69. let alphabet = "0123456789bcdefghjkmnpqrstuvwxyz" |> Bytes.of_string
  70. (* encode the chars * 5 low bits of x *)
  71. let base32_encode chars x =
  72. let b32_int_to_char i = i |> Bytes.get alphabet and x1f = of_int 0x1f in
  73. let rec f i x' b =
  74. match i with
  75. | -1 -> b
  76. | _ ->
  77. x' |> logand x1f |> to_int |> b32_int_to_char |> Bytes.set b i;
  78. f (i - 1) (shift_right x' 5) b
  79. in
  80. chars |> Bytes.create |> f (chars - 1) x |> Bytes.to_string
  81. let base32_decode hash =
  82. let b32_int_of_char c =
  83. (* if we want it fast, either do binary search or construct a sparse LUT from chars 0-z -> int *)
  84. match c |> Bytes.index_opt alphabet with None -> Error c | Some i -> Ok i
  85. and len = hash |> String.length in
  86. match len <= 12 with
  87. | false -> Error '_'
  88. | true ->
  89. let rec f idx x =
  90. match len - idx with
  91. | 0 -> Ok x
  92. | _ ->
  93. Result.bind
  94. (hash.[idx] |> b32_int_of_char)
  95. (fun v -> v |> of_int |> logor (shift_left x 5) |> f (idx + 1))
  96. in
  97. f 0 zero
  98. let encode chars wgs84 =
  99. (* is the empty string a geohash? representing all earth? *)
  100. match 1 <= chars && chars <= 12 with
  101. | false -> Error chars
  102. | true ->
  103. let h60 = wgs84 |> quantize30 |> interleave in
  104. Ok (shift_right h60 (60 - (5 * chars)) |> base32_encode chars)
  105. let error_with_precision bits factor =
  106. let latBits = bits / 2 in
  107. let lonBits = bits - latBits in
  108. let latErr = factor *. Float.ldexp 180. (-latBits)
  109. and lonErr = factor *. Float.ldexp 360. (-lonBits) in
  110. (latErr, lonErr)
  111. let decode hash =
  112. Result.bind (base32_decode hash) (fun h60 ->
  113. let bits = 5 * String.length hash in
  114. let lat, lon = shift_left h60 (60 - bits) |> deinterleave |> dequantize30
  115. and latE2, lonE2 = error_with_precision bits 0.5 in
  116. Ok ((lat +. latE2, lon +. lonE2), (latE2, lonE2)))