# Interop

Other systems send and expect geometries in their own formats: map
libraries take GeoJSON layers, spreadsheets and other systems' APIs hold
WKT text, and trackers send pairs of numbers. Jadpo reads and writes each
at the edge of your app, and only there does the order of latitude and
longitude matter:

| Format | What Jadpo does | Where the axis order lives |
|---|---|---|
| GeoJSON geometries | As [Geospatial types](geospatial-types.md) has them: strict RFC 7946, every ring written by the right-hand rule | `geo.from_geojson` and the wire |
| GeoJSON `Feature`, `FeatureCollection` | Two standard generic types, `Feature<G, P>` (a `geometry` of type `G` and `properties` of type `P`) and `FeatureCollection<G, P>` (its `features`), read and written as RFC 7946 objects | The geometry inside |
| `GeometryCollection` | Still refused | |
| WKT and EWKT | `geo.from_wkt(text)` failing `malformed` or `out_of_range`, and `geo.to_wkt(shape)`, longitude first as PostGIS writes it; `SRID=4326;` accepted, any other SRID `out_of_range` | Those two operations |
| WKB | The PostgreSQL driver's own encoding; no operation | Inside the driver |
| Other coordinate systems | Left out: `EastingNorthing` stays a value with its `Crs`, not converted | |

## GeoJSON features and layers

A map library such as Leaflet or MapLibre takes a layer of features: a
`FeatureCollection`, each `Feature` a geometry with properties such as a
name. Jadpo has the two as standard types, so you don't declare your own.

Here is the bakery map's delivery layer. A feature's identity goes in its
properties, so the bakery a patch belongs to is a property. A layer read
from a mapping library goes back to it byte for byte:

```jadpo
type PatchProperties = Object {
    name: PatchName
    bakery_id: Bakery.id
}

/// The map's delivery layer, as a mapping library reads it.
function patch_layer(patches: List<DeliveryPatch>) -> FeatureCollection<Polygon, PatchProperties> {
    return FeatureCollection {
        features: list.map(
            patches,
            patch -> Feature {
                geometry: patch.outline
                properties: PatchProperties {
                    name: patch.name
                    bakery_id: patch.bakery_id
                }
            }
        )
    }
}

/// A layer as the map is sent it.
function layer_sent(layer: FeatureCollection<Polygon, PatchProperties>) -> LayerJson {
    return LayerJson(json.write(layer))
}

/// A layer the planner drew, as its mapping library sends it.
function drawn_layer(
    sent: LayerJson
) fails NotALayer -> FeatureCollection<Polygon, PatchProperties> {
    return attempt json.read(sent, as: FeatureCollection<Polygon, PatchProperties>) {
        malformed: NotALayer
        mismatch: NotALayer  // an `id`, a missing geometry, altitudes on some corners only
    }
}
```

```jadpo
test "a drawn layer goes back to the map as it came" {
    var sent: LayerJson = LayerJson(
        "{\"type\":\"FeatureCollection\",\"features\":[{\"type\":\"Feature\",\"geometry\":{\"type\":\"Polygon\",\"coordinates\":[[[-0.2,51.49],[-0.05,51.49],[-0.05,51.54],[-0.2,51.54],[-0.2,51.49]]]},\"properties\":{\"name\":\"Soho\",\"bakery_id\":\"7b0c1f8e-3a52-4d7e-9a41-2f1d5c6e8b90\"}}]}"
    )
    var layer: FeatureCollection<Polygon, PatchProperties> = attempt drawn_layer(sent)
    assert layer_sent(layer) == sent
    assert list.count(layer.features) == 1
}

test "a feature's identity is in its properties" {
    assert drawn_layer(
        LayerJson(
            "{\"type\":\"FeatureCollection\",\"features\":[{\"type\":\"Feature\",\"id\":7,\"geometry\":{\"type\":\"Polygon\",\"coordinates\":[[[-0.2,51.49],[-0.05,51.49],[-0.05,51.54],[-0.2,51.54],[-0.2,51.49]]]},\"properties\":{\"name\":\"Soho\",\"bakery_id\":\"7b0c1f8e-3a52-4d7e-9a41-2f1d5c6e8b90\"}}]}"
        )
    ) fails NotALayer
}

test "a patch with one corner at a height is refused" {
    assert drawn_layer(
        LayerJson(
            "{\"type\":\"FeatureCollection\",\"features\":[{\"type\":\"Feature\",\"geometry\":{\"type\":\"Polygon\",\"coordinates\":[[[-0.2,51.49],[-0.05,51.49,35],[-0.05,51.54],[-0.2,51.54],[-0.2,51.49]]]},\"properties\":{\"name\":\"Soho\",\"bakery_id\":\"7b0c1f8e-3a52-4d7e-9a41-2f1d5c6e8b90\"}}]}"
        )
    ) fails NotALayer
}
```

- **A feature's arguments.** `G` is one of the six geometries or
  `Geometry`. `P` is an `Object` type of your application's, whose fields
  are the properties; an `Input`, an `Output` or a standard object isn't
  allowed. Either kind of wrong argument is refused as `type.mismatch`,
  where the type is written or where a literal's values give it. A
  literal takes its arguments from the type its place expects, or else
  from its values: a feature's from its geometry and its properties, a
  collection's from its features.
- **Read strictly.** A feature's members are `type`, which must be
  `Feature` (another is `variant`), `geometry`, `properties`, and the
  `bbox` that RFC 7946 §5 lets any object carry. A `bbox` is checked as
  four or six numbers and ignored, as a geometry's is. An `id` or any
  other member is `unknown_field`: your application's identity for a
  feature goes in `properties`. A `geometry` of `null`, which RFC 7946
  allows, is `required`, since a feature with no geometry is left out;
  so are `properties` of `null` or none. A collection's members are
  `type`, `features` and `bbox`. A collection of no features is read, as
  an empty layer, though a multi-geometry of no parts isn't. Each problem
  is reported at its place, such as `features.0.properties.vans`. A
  feature or collection is written in that order, `type` first, with no
  `bbox`.
- **No text form, and no form for people.** `text.from`, `text.parse`
  and `text.format` on a feature are `type.mismatch`; its geometry and
  its properties have theirs. A feature field in an entity is stored as
  its GeoJSON in `jsonb`, as
  [structured values](structured-values.md) are.

A route sends a layer bare, as it sends a `List` or `Page` of an `Output`
([Output and status](output-and-status.md)), so a mapping library
fetches it as the whole response, plain RFC 7946 GeoJSON. Its properties
hold only what an `Output` may, so only fields declared for clients leave
the server. A layer may also be a field of an `Output`. OpenAPI describes
it as RFC 7946's object with each feature's geometry and properties, and
the API record names it, as, say, `FeatureCollection<Point, PinProperties>`:

```jadpo
type PinLabel = Text {
    max_length: 80
}

type LayerJson = Text {}

type PinProperties = Object {
    label: PinLabel
}

/// The bakeries on the map, each pin labelled.
action bakery_pins() -> FeatureCollection<Point, PinProperties> {
    return FeatureCollection {
        features: [
            Feature {
                geometry: Point {
                    latitude: 51.5136
                    longitude: -0.1365
                    altitude: none
                }
                properties: PinProperties {
                    label: PinLabel("Soho")
                }
            }
        ]
    }
}

routes Pins {
    prefix: /pins

    POST / {
        authentication: RouteAuthentication.none
        output: FeatureCollection<Point, PinProperties>
        run: bakery_pins()
    }
}

policy {
    actions: {
        bakery_pins: [Access.public]
    }
}

test "the map fetches its pins as GeoJSON" {
    var response: Response = request {
        method: HttpMethod.post
        path: "/pins"
    }
    assert response.status == 200
    assert response.output == bakery_pins()
    assert LayerJson(json.write(bakery_pins())) == LayerJson(
        "{\"type\":\"FeatureCollection\",\"features\":[{\"type\":\"Feature\",\"geometry\":{\"type\":\"Point\",\"coordinates\":[-0.1365,51.5136]},\"properties\":{\"label\":\"Soho\"}}]}"
    )
}
```

This route answers `POST`, since an action builds its layer. A layer of
stored records comes from a query, which a `GET` route runs, so a mapping
library fetches it from the route's address
([A query giving a map layer](a-query-giving-a-map-layer.md)).

## WKT and EWKT

WKT is the text form of a geometry that spreadsheets, CSV exports and
other systems' APIs use, such as `POINT(-0.1276 51.5072)`, longitude
first. EWKT is PostGIS's form of it, which starts with the coordinate
system, as in `SRID=4326;POINT(...)`. WKB, the binary form, only passes
between programs and databases, so it stays inside the PostgreSQL driver.

The planner's spreadsheet holds each outline as WKT:

```jadpo
type SheetText = Text {}

type NotAShape = Failure {
    kind: FailureKind.invalid_value
}

/// A patch's outline from the planner's spreadsheet, as WKT.
function outline_from(sheet_text: SheetText) fails NotAShape -> Geometry {
    return attempt geo.from_wkt(sheet_text) {
        malformed: NotAShape  // not WKT, rings that cross, or a height on some corners only
        out_of_range: NotAShape  // past the poles, or another system
    }
}

/// An outline for the planner's spreadsheet, longitude first.
function sheet_text_for(outline: Geometry) -> SheetText {
    return SheetText(geo.to_wkt(outline))
}
```

```jadpo
test "a patch comes back from the spreadsheet as it went" {
    var sheet_text: SheetText = SheetText(
        "POLYGON((-0.2 51.49,-0.05 51.49,-0.05 51.54,-0.2 51.54,-0.2 51.49))"
    )
    var outline: Geometry = attempt outline_from(sheet_text)
    assert sheet_text_for(outline) == sheet_text
    var respaced: Geometry = attempt outline_from(
        SheetText(
            "SRID=4326;polygon ((-0.2 51.49, -0.05 51.49, -0.05 51.54, -0.2 51.54, -0.2 51.49))"
        )
    )
    assert respaced == outline
}

test "what isn't an outline is refused" {
    assert outline_from(SheetText("POINT(-0.1276 91)")) fails NotAShape
    assert outline_from(SheetText("SRID=4258;POINT(-0.1276 51.5072)")) fails NotAShape
    assert outline_from(SheetText("POLYGON((0 0,10 10,0 10,10 0,0 0))")) fails NotAShape
    assert outline_from(SheetText("LINESTRING(0 0,1 1 35)")) fails NotAShape
}
```

- **`geo.from_wkt` gives a `Geometry`,** the variant of the kind the
  text names, which your code matches, as it matches a `Geometry` read
  from GeoJSON. There is no `as:` to pick a kind, so you never have to
  say what a polygon read as a line fails with.
- **What is read.** The six geometries, keywords in any case, `Z`
  written apart or joined (`POINT Z (...)`, `POINTZ(...)`), a
  multi-point's points with or without brackets, any spacing, numbers
  with an exponent, and `SRID=4326;` before it. A position is two or
  three numbers, three for `Z`. Each is read as the double it is and held
  as that double's shortest decimal
  ([Precision and determinism](precision-and-determinism.md)). A third
  number without `Z` is an altitude, as PostGIS reads it.
- **What is refused.** `M` and `ZM`, `EMPTY`, `GEOMETRYCOLLECTION`, the
  curves and every other kind are `malformed`, and so are positions
  mixing altitudes ([Altitudes are all or nothing](predicates-and-shapes.md#altitudes-are-all-or-nothing)).
  A coordinate out of its range is `out_of_range`, where PostGIS's
  `ST_GeogFromText` coerces it into range with a notice (verified,
  PostGIS 3.4.2). Any SRID but 4326 is `out_of_range` too, though
  PostGIS's `geography` takes other geographic systems (`SRID=4258;` is
  read, verified).
- **Rings are kept as written,** as storage keeps them, not rewound: only
  GeoJSON winds rings by the right-hand rule. A polygon is checked as one
  read from GeoJSON is, closed rings of four points or more and
  `rings_valid`, and one that isn't valid is `malformed`.
- **The exact place.** The reader reports the character it stopped at,
  counted from 0, with the condition: the start of a word that isn't a
  geometry, the bracket of a ring or line that isn't one, the number out
  of range, or the SRID's code. The operation's failure carries the
  condition only, as `encoding.from_hex`'s does; the place is for the
  tests, and for a later failure that receives it.
- **`geo.to_wkt` writes `ST_AsText`'s form:** the keyword in capitals,
  ` Z ` before the brackets when every position has an altitude, no
  space after a comma, a multi-point's points each in brackets, each
  coordinate the decimal the value holds, and an altitude in metres with
  no trailing zeros. `ST_AsText` keeps at most 15 places after the
  point, so a coordinate with more (below 10⁻¹⁵ of a degree) comes back
  from PostGIS rounded: `0.30000000000000004` is written `0.3`
  (verified). Every other geometry, such as a patch with a hole, a patch
  wound the other way or one at a height, goes into PostGIS as WKT or
  EWKT and comes back exactly as written.

## Pairs of numbers and GeoJSON text

A van's tracker sends its position latitude first, the map's pin
longitude first, and a planner pastes a shape from a drawing tool as
GeoJSON text. Each operation's name says which number comes first, so a
swapped pair is a call that reads wrong, not a value that looks right:

```jadpo
type TrackerReading = Decimal {
    scale: 9
}

type PastedText = Text {}

type NotAPlace = Failure {
    kind: FailureKind.invalid_value
}

/// Where a van's tracker says it is, latitude first.
function van_at(tracker_fix: List<TrackerReading>) fails NotAPlace -> Point {
    return attempt geo.from_lat_lng(tracker_fix) {
        malformed: NotAPlace  // not two numbers, or three with a height
        out_of_range: NotAPlace  // past a pole, or past the antimeridian
    }
}

/// Where a pin dropped on the map is, longitude first, as GeoJSON writes it.
function pin_at(pin_dropped: List<TrackerReading>) fails NotAPlace -> Point {
    return attempt geo.from_lng_lat(pin_dropped) {
        malformed: NotAPlace
        out_of_range: NotAPlace
    }
}

/// A patch pasted from a drawing tool, as GeoJSON.
function pasted_patch(pasted: PastedText) fails NotAPlace -> Geometry {
    return attempt geo.from_geojson(pasted) {
        malformed: NotAPlace  // not GeoJSON, a feature, or rings that cross
        out_of_range: NotAPlace  // past a pole, or past the antimeridian
    }
}
```

```jadpo
test "each pair says which way round it is" {
    var van_spot: Point = attempt van_at([51.5136, -0.1365])
    var map_pin: Point = attempt pin_at([-0.1365, 51.5136])
    assert van_spot == map_pin
    assert geo.to_wkt(map_pin) == "POINT(-0.1365 51.5136)"
    assert van_spot == Point {
        latitude: 51.5136
        longitude: -0.1365
        altitude: none
    }
    assert van_at([91.0, 0.0]) fails NotAPlace
    var far_east: Point = attempt pin_at([91.0, 0.0])
    assert far_east.longitude == 91.0
    assert van_at([51.5136]) fails NotAPlace
}

test "a pasted patch is read longitude first" {
    var patch_text: PastedText = PastedText(
        "{\"type\":\"Polygon\",\"coordinates\":[[[-0.2,51.49],[-0.05,51.49],[-0.05,51.54],[-0.2,51.54],[-0.2,51.49]]]}"
    )
    var patch: Geometry = attempt pasted_patch(patch_text)
    assert PastedText(json.write(patch)) == patch_text
    assert pasted_patch(
        PastedText("{\"type\":\"Point\",\"coordinates\":[-0.1365,91]}")
    ) fails NotAPlace
    assert pasted_patch(PastedText("POINT(-0.1365 51.5136)")) fails NotAPlace
}
```

- **`geo.from_geojson(text)` gives a `Geometry`,** the variant of the
  kind the text names, as `geo.from_wkt` does, read by the one GeoJSON
  reader a geometry on the wire is: strict RFC 7946, each ring closed,
  valid and rewound by the right-hand rule, every position's altitude
  given or none. Only the six geometries are read; a `Feature`, a
  `FeatureCollection` or a `GeometryCollection` is `malformed`, since a
  layer is read with `json.read(text, as: FeatureCollection<G, P>)`.
  Text that isn't one JSON document or isn't one of the six is
  `malformed`, and so is a ring that isn't closed or valid, or positions
  of mixed sizes. A coordinate out of its range, or an altitude past a
  `Length`'s, is `out_of_range`. The reader reports the place
  (`coordinates.0.3`) and the rule; the operation's failure carries the
  condition only, as `geo.from_wkt`'s does. It takes `Text` only, not
  `Bytes`.
- **A pair is two numbers or three.** `geo.from_lng_lat(numbers)` and
  `geo.from_lat_lng(numbers)` take a `List` of values built on `Int` or
  `Decimal` and give a `Point`. A third number is the altitude in
  metres, as GeoJSON's third is, a `Length` rounded half to even to the
  micrometre. Any other count is `malformed`. Each coordinate is first
  held as the shortest decimal of its nearest double, as a `Point` built
  in code is ([Precision and determinism](precision-and-determinism.md)),
  and that value is then held to its range, as GeoJSON's numbers are. So
  `90.00000000000000001` is 90 and in range; past it is `out_of_range`. The order lives only in the name: a
  latitude of 91 is `out_of_range` from `from_lat_lng` and a longitude of
  91 from `from_lng_lat`.
- **Always attempted.** What a list or a text holds is known only when
  the program runs, so each call maps both conditions, or `invalid:`,
  even for a list written out, as `geo.from_wkt` does. No literal is
  decided while compiling.
- **No `geo.to_geojson`.** `json.write` of a geometry already writes its
  GeoJSON, longitude first, so a pair or a text read here goes back out
  through it or `geo.to_wkt`.
