package geojsone

  1. Overview
  2. Docs
Streaming GeoJSON library

Install

Dune Dependency

Authors

Maintainers

Sources

geojson-0.2.0.tbz
sha256=3e44559e8eb8587a5cbcdcb508dd1464204b4e5f7a4c9819d4f91dd4f110cd9b
sha512=821d423219ccb27d446bdfa76404bbf4e03c4e68e78d66bff3a70bbc6dff9aebcf8c4844c89b2cc922b459767844c2195202790d0c9ca4eefb310e35ca315901

Description

A library for manipulating GeoJSON using a streaming parser. This is useful because GeoJSON can be gigabytes in size.

Tags

geojson geospatial geocaml

Published: 24 Nov 2023

README

ocaml-geojson

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.

Geojson

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 GeoJSON Values

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

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")]

Accessing Deeply Nested Objects with Optics

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|]|]

Building GeoJSON values

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

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.

Constructing Decoders and Encoders

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.

Eio sources and sinks

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>

Mapping

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

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

Dependencies (5)

  1. sexplib0
  2. hex
  3. cstruct >= "6.0.1"
  4. geojson = version
  5. dune >= "3.2"

Dev Dependencies (4)

  1. odoc with-doc
  2. eio_main >= "0.6" & with-test
  3. ezjsonm with-test
  4. mdx with-test

Used by (1)

  1. topojsone

Conflicts

None