~mro/geohash

73f0fda85f7de77c47149177e7a212e89d87aff1 — Marcus Rohrmoser 1 year, 7 months ago a462f80
- opam++, dune++, Makefile--
- indentation
M Makefile => Makefile +1 -7
@@ 6,11 6,6 @@ dst := _build/geohash-$(os)-$(cpu)-$(ver).cgi

final: build $(dst)

lib/res.ml:	res res/doap.rdf res/doap2html.xslt
	find res -name .DS_Store -delete
	# opam install ocp-ocamlres
	ocp-ocamlres -format ocaml $< -o $@

$(dst): _build/default/bin/main.exe
	cp $< $@
	chmod u+w $@


@@ 26,8 21,7 @@ test/assert.ml:
#
.PHONY: all build clean test install uninstall doc examples

build: lib/res.ml
	@echo "let git_sha = \""`git rev-parse --short HEAD`"\"" > bin/version.ml
build:
	dune build bin/main.exe

all: build

M bin/cgi.ml => bin/cgi.ml +32 -31
@@ 29,42 29,43 @@ let handle_hash oc req =
      match Geohash.decode hash with
      | Error _ -> error oc 406 "Cannot decode hash."
      | Ok ((lat, lon), (dlat, dlon)) ->
          let mime = "text/xml"
          and xslt = "gpx2html.xslt"
          and uri = req |> request_uri
          and base = "http://purl.mro.name/geohash" in
          Printf.fprintf oc "%s: %s\n" "Content-Type" mime;
          Printf.fprintf oc "\n";
          Printf.fprintf oc
            "<?xml version='1.0'?><!-- \
             https://www.topografix.com/GPX/1/1/gpx.xsd -->\n\
             <?xml-stylesheet type='text/xsl' href='%s'?>\n\
             <gpx xmlns='http://www.topografix.com/GPX/1/1' version='1.1' \
             creator='%s'>\n\
            \  <metadata>\n\
            \    <link href='%s://%s:%s%s'/>\n\
            \    <bounds minlat='%f' minlon='%f' maxlat='%f' maxlon='%f'/>\n\
            \  </metadata>\n\
            \  <wpt lat='%f' lon='%f'>\n\
            \    <name>#%s</name>\n\
            \    <link href='%s://%s:%s%s'/>\n\
            \  </wpt>\n\
             </gpx>"
            xslt base req.scheme req.host req.server_port uri (lat -. dlat)
            (lon -. dlon) (lat +. dlat) (lon +. dlon) lat lon hash req.scheme
            req.host req.server_port uri;
          0)
        let mime = "text/xml"
        and xslt = "gpx2html.xslt"
        and uri = req |> request_uri
        and base = "http://purl.mro.name/geohash" in
        Printf.fprintf oc "%s: %s\n" "Content-Type" mime;
        Printf.fprintf oc "\n";
        Printf.fprintf oc
          "<?xml version='1.0'?><!-- \
           https://www.topografix.com/GPX/1/1/gpx.xsd -->\n\
           <?xml-stylesheet type='text/xsl' href='%s'?>\n\
           <gpx xmlns='http://www.topografix.com/GPX/1/1' version='1.1' \
           creator='%s'>\n\
          \  <metadata>\n\
          \    <link href='%s://%s:%s%s'/>\n\
          \    <bounds minlat='%f' minlon='%f' maxlat='%f' maxlon='%f'/>\n\
          \  </metadata>\n\
          \  <wpt lat='%f' lon='%f'>\n\
          \    <name>#%s</name>\n\
          \    <link href='%s://%s:%s%s'/>\n\
          \  </wpt>\n\
           </gpx>"
          xslt base req.scheme req.host req.server_port uri (lat -. dlat)
          (lon -. dlon) (lat +. dlat) (lon +. dlon) lat lon hash req.scheme
          req.host req.server_port uri;
        0)
  | _ -> error oc 404 "Not found"

let handle oc req =
  let mercator_birth = "u154c" and uri = req |> request_uri in
  match req.request_method with
  | "GET" -> (
      let r n = n |> Res.read |> Option.value ~default:"" in
      match req.path_info with
      | "/about" -> dump_clob oc "text/xml" Res.doap_rdf
      | "/LICENSE" -> dump_clob oc "text/plain" Res._LICENSE
      | "/doap2html.xslt" -> dump_clob oc "text/xml" Res.doap2html_xslt
      | "/gpx2html.xslt" -> dump_clob oc "text/xml" Res.gpx2html_xslt
      | "/about" -> dump_clob oc "text/xml" (r "doap.rdf")
      | "/LICENSE" -> dump_clob oc "text/plain" (r "LICENSE")
      | "/doap2html.xslt" -> dump_clob oc "text/xml" (r "doap2html.xslt")
      | "/gpx2html.xslt" -> dump_clob oc "text/xml" (r "gpx2html.xslt")
      | "" -> uri ^ "/" |> redirect oc
      | "/" -> (
          match req.query_string with


@@ 72,9 73,9 @@ let handle oc req =
          | qs -> (
              match qs |> Route.coord_from_qs with
              | Error (`NoMatch (_, s')) ->
                  error oc 406 ("Cannot encode coords: '" ^ s' ^ "'")
                error oc 406 ("Cannot encode coords: '" ^ s' ^ "'")
              | Error (`ConverterFailure _) ->
                  error oc 406 "Cannot encode coords."
                error oc 406 "Cannot encode coords."
              | Ok co -> (
                  (* actually logic :-( *)
                  let prec =

M bin/dune => bin/dune +8 -2
@@ 1,9 1,15 @@
;; https://discuss.ocaml.org/t/dune-how-to-link-statically-on-linux-not-on-others/8537/4?u=mro
(rule (with-stdout-to link_flags.sexp (run sh %{dep:gen_link_flags.sh})))
(rule (with-stdout-to version.ml (run echo "let git_sha = \""`git rev-parse --short HEAD`"\"")))
(rule
 (target res.ml)
 (deps (source_tree ../res))
 (action (with-stdout-to %{target}
  (run ocaml-crunch --mode=plain ../res))))

; https://stackoverflow.com/a/53325230/349514
(executable
  (name main)
  (libraries lib geohash)
  ; (link_flags (:include link_flags.sexp))
  (libraries lib geohash tyre)
  (link_flags (:include link_flags.sexp))
)

M bin/gen_link_flags.sh => bin/gen_link_flags.sh +1 -1
@@ 7,7 7,7 @@ case "$(uname -s)" in
    echo '()'
    ;;
  *)
    echo '(-ccopt "-static")'
    echo '(-ccopt -static)'
    ;;
esac


M bin/shell.ml => bin/shell.ml +2 -1
@@ 53,7 53,8 @@ let exec args =
  | [ "-h" ] | [ "--help" ] -> print_help oc
  | [ "-V" ] | [ "--version" ] -> print_version oc
  | [ "--doap" ] ->
    Printf.fprintf oc "%s" Lib.Res.doap_rdf;
    let r n = n |> Res.read |> Option.value ~default:"" in
    Printf.fprintf oc "%s" (r "doap.rdf");
    0
  | [ i ] ->
    (i |> to_hash |> function

M dune-project => dune-project +5 -5
@@ 6,7 6,7 @@
(implicit_transitive_deps true)
(generate_opam_files true)

(license GPLv3)
(license GPL-3.0-or-later)
(maintainers "Marcus Rohrmoser <work@mro.name>")
(authors "The GeoHash# programmers")



@@ 16,7 16,7 @@

(package
 (name geohash)
 (synopsis "#🌐 geohash converter 🐫 library.")
 (synopsis "#🌐 geohash converter 🐫 library")
 (description "Convert WGS84 lat/lon pairs to [Gustavo Niemeyer](http://niemeyer.net/)s
[Geohash](http://en.wikipedia.org/wiki/Geohash) and back.")
 (tags (Social Web GeoHash CGI RFC3875))


@@ 24,16 24,16 @@
   (optint (>= 0.3))
   (tyre (>= 0.5))
  (odoc :with-doc)
  (ocaml (and (>= 4.05) (< 4.12)))
  (ocaml (>= 4.05))
))

(package
 (name geohash_bin)
 (synopsis "#🌐 geohash converter 🐫. Commandline and CGI. Zero-config, single-file.")
 (synopsis "#🌐 geohash converter 🐫. Commandline and CGI. Zero-config, single-file")
 (description "Convert WGS84 lat/lon pairs to [Gustavo Niemeyer](http://niemeyer.net/)s
[Geohash](http://en.wikipedia.org/wiki/Geohash) and back. Web and commandline, 🐪,
statically linked, single-file, zero-config.")
 (tags (Social Web GeoHash CGI RFC3875))
 (depends
   ocp-ocamlres
   crunch
))

M geohash.opam => geohash.opam +3 -3
@@ 1,12 1,12 @@
# This file is generated by dune, edit dune-project instead
opam-version: "2.0"
synopsis: "#🌐 geohash converter 🐫 library."
synopsis: "#🌐 geohash converter 🐫 library"
description: """
Convert WGS84 lat/lon pairs to [Gustavo Niemeyer](http://niemeyer.net/)s
[Geohash](http://en.wikipedia.org/wiki/Geohash) and back."""
maintainer: ["Marcus Rohrmoser <work@mro.name>"]
authors: ["The GeoHash# programmers"]
license: "GPLv3"
license: "GPL-3.0-or-later"
tags: ["Social" "Web" "GeoHash" "CGI" "RFC3875"]
homepage: "https://demo.mro.name/geohash.cgi"
bug-reports: "https://codeberg.org/mro/geohash/issues"


@@ 15,7 15,7 @@ depends: [
  "optint" {>= "0.3"}
  "tyre" {>= "0.5"}
  "odoc" {with-doc}
  "ocaml" {>= "4.05" & < "4.12"}
  "ocaml" {>= "4.05"}
]
build: [
  ["dune" "subst"] {dev}

M geohash_bin.opam => geohash_bin.opam +3 -3
@@ 1,20 1,20 @@
# This file is generated by dune, edit dune-project instead
opam-version: "2.0"
synopsis:
  "#🌐 geohash converter 🐫. Commandline and CGI. Zero-config, single-file."
  "#🌐 geohash converter 🐫. Commandline and CGI. Zero-config, single-file"
description: """
Convert WGS84 lat/lon pairs to [Gustavo Niemeyer](http://niemeyer.net/)s
[Geohash](http://en.wikipedia.org/wiki/Geohash) and back. Web and commandline, 🐪,
statically linked, single-file, zero-config."""
maintainer: ["Marcus Rohrmoser <work@mro.name>"]
authors: ["The GeoHash# programmers"]
license: "GPLv3"
license: "GPL-3.0-or-later"
tags: ["Social" "Web" "GeoHash" "CGI" "RFC3875"]
homepage: "https://demo.mro.name/geohash.cgi"
bug-reports: "https://codeberg.org/mro/geohash/issues"
depends: [
  "dune" {>= "2.8"}
  "ocp-ocamlres"
  "crunch"
  "odoc" {with-doc}
]
build: [

M lib/dune => lib/dune +1 -3
@@ 1,7 1,5 @@
(library
  (name lib)
  (libraries
    tyre
   )
  (public_name geohash_bin.lib)
  (libraries tyre)
)

M public/dune => public/dune +1 -4
@@ 1,8 1,5 @@
(library
  (name geohash)
  (libraries
    tyre
    optint
  )
  (public_name geohash)
  (libraries optint)
)

M public/geohash.mli => public/geohash.mli +9 -5
@@ 1,12 1,16 @@

(** Hello, world! *)
(** Geohash math to encode and decode base32 character hashes. *)

(** a WGS84 coordinate pair *)
type wgs84_lat_lon = (float * float)
type delta = (float * float)

  (** encode a WGS84 (lat,lon) coordinate pair into Geohash of given length. *)
  val encode : int -> (float * float) -> (string, int) result
(** encode a WGS84 (lat,lon) coordinate pair into Geohash of given
    hash character count. *)
val encode : int -> wgs84_lat_lon -> (string, int) result

  (** decode a geohash base32 string to a WGS84 coordinate region center,delta. *)
  val decode : string -> (((float * float) * (float * float)), char) result
(** decode a geohash base32 string to a WGS84 coordinate region center,delta. *)
val decode : string -> ((wgs84_lat_lon * delta), char) result

(**/**) (* hide Whitebox module *)
module Whitebox : module type of Whitebox

M public/whitebox.ml => public/whitebox.ml +29 -18
@@ 19,11 19,14 @@
 *)

(* Inspired by https://mmcloughlin.com/posts/geohash-assembly
 * and https://github.com/mmcloughlin/geohash/blob/master/geohash.go 
 * and https://github.com/mmcloughlin/geohash/blob/master/geohash.go
 * and https://github.com/mmcloughlin/deconstructedgeohash/blob/master/geohash.go *)

open Optint.Int63

type wgs84_lat_lon = (float * float)
type delta         = (float * float)

(*
 * 60 bits are fine, because
 * - 12 geohash characters equal 60 bit


@@ 56,16 59,24 @@ let x5555555555555555 = of_int64 0x3555555555555555L
let interleave (x, y) =
  let spread x =
    let f s m x' = m |> logand (x' |> logor (shift_left x' s)) in
    x |> f 16 x0000ffff0000ffff |> f 8 x00ff00ff00ff00ff
    |> f 4 x0f0f0f0f0f0f0f0f |> f 2 x3333333333333333 |> f 1 x5555555555555555
    x
    |> f 16 x0000ffff0000ffff
    |> f  8 x00ff00ff00ff00ff
    |> f  4 x0f0f0f0f0f0f0f0f
    |> f  2 x3333333333333333
    |> f  1 x5555555555555555
  in
  spread x |> logor (shift_left (spread y) 1)

let deinterleave x =
  let squash x =
    let f s m x' = m |> logand (x' |> logor (shift_right x' s)) in
    x |> logand x5555555555555555 |> f 1 x3333333333333333
    |> f 2 x0f0f0f0f0f0f0f0f |> f 4 x00ff00ff00ff00ff |> f 8 x0000ffff0000ffff
    x
    |> logand x5555555555555555
    |> f  1 x3333333333333333
    |> f  2 x0f0f0f0f0f0f0f0f
    |> f  4 x00ff00ff00ff00ff
    |> f  8 x0000ffff0000ffff
    |> f 16 x00000000ffffffff
  in
  (squash x, squash (shift_right x 1))


@@ 79,8 90,8 @@ let base32_encode chars x =
    match i with
    | -1 -> b
    | _ ->
        x' |> logand x1f |> to_int |> b32_int_to_char |> Bytes.set b i;
        f (i - 1) (shift_right x' 5) b
      x' |> logand x1f |> to_int |> b32_int_to_char |> Bytes.set b i;
      f (i - 1) (shift_right x' 5) b
  in
  chars |> Bytes.create |> f (chars - 1) x |> Bytes.to_string



@@ 92,23 103,23 @@ let base32_decode hash =
  match len <= 12 with
  | false -> Error '_'
  | true ->
      let rec f idx x =
        match len - idx with
        | 0 -> Ok x
        | _ ->
            Result.bind
              (hash.[idx] |> b32_int_of_char)
              (fun v -> v |> of_int |> logor (shift_left x 5) |> f (idx + 1))
      in
      f 0 zero
    let rec f idx x =
      match len - idx with
      | 0 -> Ok x
      | _ ->
        Result.bind
          (hash.[idx] |> b32_int_of_char)
          (fun v -> v |> of_int |> logor (shift_left x 5) |> f (idx + 1))
    in
    f 0 zero

let encode chars wgs84 =
  (* is the empty string a geohash? representing all earth? *)
  match 1 <= chars && chars <= 12 with
  | false -> Error chars
  | true ->
      let h60 = wgs84 |> quantize30 |> interleave in
      Ok (shift_right h60 (60 - (5 * chars)) |> base32_encode chars)
    let h60 = wgs84 |> quantize30 |> interleave in
    Ok (shift_right h60 (60 - (5 * chars)) |> base32_encode chars)

let error_with_precision bits factor =
  let latBits = bits / 2 in