A collection of libraries for reading and writing GeoJSON. This repository contains two libraries, a non-blocking streaming GeoJSON library called geojsone
(this requires OCaml 5 with effects) and a normal GeoJSON library.
The geojson
library allows you to parse GeoJSON objects. The implementation requires you to provide a JSON parser of your choosing. Take a look in the prelude file for an implementation using ezjsonm. The prelude also contains encoded GeoJSON objects as OCaml strings for use in the examples.
The first thing to do is create a GeoJSON parser from your JSON parser.
module G = Geojson.Make (Ezjsonm_parser)
Reading values relies on your JSON parser's methods for creating a JSON value. With ezjsonm
we can read strings.
# let feature = G.of_json (Ezjsonm.value_from_string feature_example);;
val feature : (G.t, [ `Msg of string ]) result = Ok <abstr>
This returns a result, so either Ok g
where g
is a GeoJSON object or an Error
.
# let feature = Result.get_ok feature;;
val feature : G.t = <abstr>
A GeoJSON object can either be a feature, a geometry or a feature collection. To know which one you have, you will need to pattern match on the value for the geojson
value. With this example we know we should have a feature so we will just assert false
.
# let f = match G.geojson feature with
| G.Feature f -> f
| _ -> assert false;;
val f : G.Feature.t = <abstr>
Now we can access feature specific values from our OCaml value.
# let props = G.Feature.properties f;;
val props : G.json option = Some (`O [("name", `String "Dinagat Islands")])
Foreign members are those JSON key-value pairs that are not a part of the specification. Sometimes your GeoJSON data might include extra information and this is a way to gain access to it after you have parsed the value.
# G.Feature.foreign_members f;;
- : (string * G.json) list = [("title", `String "Some Islands")]
There is an experimental module in the library called Geojson.Accessor
. This uses optics to allow you to more easily access values that are deeply nested. An important note is that they will always tend to be less efficient than manually pattern-matching.
However, using our feature as an example, if we wanted to access the multipoint without matching all the way down, we can construct an optic to help us.
# let g_to_mp = G.Accessor.(geojson >& feature &> Feature.geometry_exn &> Geometry.geometry $> Geometry.multipoint);;
val g_to_mp : (G.t, G.Geometry.MultiPoint.t) G.Accessor.Optics.Optional.t =
Geojson__Optics.Lens.V (<fun>, <fun>)
This is a lens that lets use focus all the way down from the GeoJSON object containing a feature, containing a geometry that is a multipoint.
# G.Accessor.get g_to_mp feature |> Option.get |> G.Geometry.MultiPoint.coordinates;;
- : G.Geometry.Position.t array = [|[|125.1; 40.|]; [|155.9; 22.5|]|]
You can also construct GeoJSON objects using OCaml values.
# let geometry =
G.Geometry.(v
~foreign_members:["hello", `String "World"]
(Point (Point.v (Position.v ~lat:1.123 ~lng:2.321 ()))));;
val geometry : G.Geometry.t = <abstr>
# let g = G.(v (Geometry geometry));;
val g : G.t = <abstr>
# G.to_json g |> Ezjsonm.value_to_string;;
- : string =
"{\"type\":\"Point\",\"coordinates\":[2.321,1.123],\"hello\":\"World\"}"
Geojsone is a non-blocking, streaming parser for GeoJSON objects. Currently, it uses a modified version of jsonm, called jsone. It uses effects to provide non-blocking reading and writing functions rather than passing continuations the whole way through the parser. It is still experimental.
In order to build decoders and encoder, you must provide a source and destination.
# #show_type Geojsone.Jsone.src;;
type nonrec src = unit -> Cstruct.t
# #show_type Geojsone.Jsone.dst;;
type nonrec dst = Cstruct.t -> unit
These are functions for filling a buffer (a Cstruct.t
) and reading a buffer. Note they appear as normal OCaml functions (no IO monad like Lwt.t
). You will have to use a library that uses effects for non-blocking IO in order to make the decoders and encoders non-blocking. If you don't mind blocking (for example, in js_of_ocaml) you can provide blocking versions of these functions.
The geojsone.eio
sublibrary has some useful Eio-related functions for working with the geojsone
libary.
# let src_of_flow = Geojsone_eio.src_of_flow;;
val src_of_flow :
?buff:Cstruct.t -> [> Eio.Flow.source_ty ] Eio.Std.r -> Geojsone.Jsone.src =
<fun>
# let buffer_to_dst buf = Geojsone_eio.dst_of_flow (Eio.Flow.buffer_sink buf);;
val buffer_to_dst : Buffer.t -> Geojsone.Jsone.dst = <fun>
Note that your source function should raise End_of_file
when there are no more bytes to be read. Eio.Flow.single_read
does this.
With both of these we can now construct an encoder and decoder.
# let decoder s = Geojsone.Jsone.decoder (src_of_flow @@ Eio.Flow.string_source s);;
val decoder : string -> Geojsone.Jsone.decoder = <fun>
# let encoder buf = Geojsone.Jsone.encoder (buffer_to_dst buf);;
val encoder : Buffer.t -> Geojsone.Jsone.encoder = <fun>
There are various mapping functions for iterating over a GeoJSON object. For example, you may wish to visit all of the properties
in your object.
# Geojsone.map_props;;
- : (Geojsone.G.json -> Geojsone.G.json) ->
Geojsone.Jsone.src -> Geojsone.Jsone.dst -> (unit, Geojsone.Err.t) result
= <fun>
We can see if any properties are objects with a field called name
, and capitalise its value.
let capitalise_name = function
| `O [ "name", `String s ] -> `O [ "name", `String (String.uppercase_ascii s) ]
| v -> v
let buf = Buffer.create 256
We can then use this function for our example.
# let feature_source () = src_of_flow @@ Eio.Flow.string_source feature_example;;
val feature_source : unit -> Geojsone.Jsone.src = <fun>
# Geojsone.(map_props capitalise_name (feature_source ()) (buffer_to_dst buf));;
- : (unit, Geojsone.Err.t) result = Ok ()
# Buffer.contents buf;;
- : string =
"{\"type\":\"Feature\",\"geometry\":{\"type\":\"MultiPoint\",\"coordinates\":[[125.1,40],[155.9,22.5]]},\"properties\":{\"name\":\"DINAGAT ISLANDS\"},\"title\":\"Some Islands\"}"
Folding is similar to mapping except you can accumulate a value as you iterate over the document.
# Geojsone.fold_geometry;;
- : ('a -> Geojsone.G.Geometry.t -> 'a) ->
'a -> Geojsone.Jsone.src -> ('a, Geojsone.Err.t) result
= <fun>
So we could simply count the number of geometry objects for example.
let count_geometries acc _ = acc + 1
And we can apply it to our running example.
# Geojsone.fold_geometry count_geometries 0 (feature_source ()) ;;
- : (int, Geojsone.Err.t) result = Ok 1