# `Rover.Shape`
[🔗](https://github.com/nseaSeb/rover/blob/v0.4.0/lib/rover/shape.ex#L1)

A geometry on the map: an outline, a route, a zone.

Where `Rover.Marker` takes a coordinate, a shape takes **GeoJSON**:

    %{
      id: "parcel-42",
      geometry: %{"type" => "Polygon", "coordinates" => [[[4.83, 45.76], ...]]},
      color: "#16a34a"
    }

> #### Shapes are the one place Rover is not latitude-first {: .info}
>
> Everywhere else — markers, `center`, event payloads — Rover speaks
> `{latitude, longitude}`, because that is the order people say out loud.
> GeoJSON is defined the other way round, `[longitude, latitude]`
> ([RFC 7946 §3.1.1](https://www.rfc-editor.org/rfc/rfc7946#section-3.1.1)),
> and here the standard wins.
>
> The reason is that shape data is almost never typed by hand. It arrives from
> `ST_AsGeoJSON`, from a cadastral API, from a routing service — already
> conformant. Inventing a latitude-first geometry format would mean converting
> on the way in and on the way out, and would cut Rover off from every tool
> that already speaks GeoJSON.

## What `:geometry` accepts

Anything `ol/format/GeoJSON` can read, which is to say any of:

* a bare geometry — `Point`, `LineString`, `Polygon`, `MultiPolygon`, …
* a `Feature`
* a `FeatureCollection`

With atom or string keys, or as an undecoded JSON string — so the output of
`Ecto.Adapters.SQL.query(repo, "select ST_AsGeoJSON(geom) …")` goes straight in.

## Fields

| Field | Type | Meaning |
|---|---|---|
| `:id` | term | **Required.** Stable identity used to diff the map. |
| `:geometry` | map / string | **Required.** GeoJSON, as above. |
| `:color` | string | Stroke colour. |
| `:width` | number | Stroke width in pixels. |
| `:fill_color` | string | Fill colour. Defaults to `:color`. |
| `:fill_opacity` | float | `0.0`–`1.0`. Applied to the fill only. |
| `:label` | string | Text drawn at the centre of the geometry. |
| `:tooltip` | string | Shown on hover, at the pointer. Defaults to `:label`. |
| `:rev` | term | Revision. See below. |
| `:data` | map | Echoed back verbatim in shape events. |
| `:editable` | boolean | Lets the user drag its vertices — see `on_shape_edit_end`. Only for a shape backed by a single feature; a `FeatureCollection` of several is not editable. |

## Why there is a `:rev`

Markers are diffed by hashing their coordinate — two numbers, free. A route can
be thousands of points, and re-hashing it on the client on every update is
exactly the cost the reconciler exists to avoid.

So the revision is computed **once per render, on the server**:
`:erlang.phash2(geometry)` by default. If you already have something cheaper
and more meaningful — a `updated_at`, a database revision, a version column —
pass it as `:rev` and Rover will trust it instead:

    %{id: p.id, geometry: p.geom, rev: p.updated_at}

Two shapes with the same id and the same `:rev` are assumed to have the same
geometry, and the client leaves the feature alone.

# `id`

```elixir
@type id() :: String.t() | integer() | atom()
```

# `t`

```elixir
@type t() :: %Rover.Shape{
  color: String.t() | nil,
  data: map() | nil,
  editable: boolean(),
  fill_color: String.t() | nil,
  fill_opacity: float() | nil,
  geometry: map(),
  id: id(),
  label: String.t() | nil,
  rev: term(),
  tooltip: String.t() | nil,
  width: number() | nil
}
```

# `coordinates`

```elixir
@spec coordinates(t() | map() | String.t()) :: [{float(), float()}]
```

Every coordinate in a geometry, as `{lat, lon}` pairs.

This is what lets a map with shapes and no markers still find its centre. It
walks any nesting depth, so a `MultiPolygon` with holes and a `Point` are the
same call.

## Examples

    iex> Rover.Shape.coordinates(%{"type" => "LineString", "coordinates" => [[4.85, 45.75], [2.35, 48.85]]})
    [{45.75, 4.85}, {48.85, 2.35}]

    iex> Rover.Shape.coordinates(%{"type" => "Point", "coordinates" => [4.85, 45.75]})
    [{45.75, 4.85}]

# `dump`

```elixir
@spec dump(t()) :: map()
```

Renders a shape as the compact map handed to the JavaScript runtime.

`nil` fields are dropped, so a shape that only sets a colour does not ship
seven nulls alongside it.

## Examples

    iex> shape = Rover.Shape.new!(%{id: 1, geometry: %{"type" => "Point", "coordinates" => [4.85, 45.75]}, rev: 7})
    iex> Rover.Shape.dump(shape) |> Map.keys() |> Enum.sort()
    [:geometry, :id, :rev]

# `new!`

```elixir
@spec new!(
  t() | map(),
  keyword()
) :: t()
```

Normalises `source` into a `Rover.Shape`.

`opts` maps Rover fields onto keys of `source`, exactly as
`Rover.Marker.new!/2` does. Every option takes a key (atom or string) or a
1-arity function.

## Examples

    iex> shape = Rover.Shape.new!(%{id: 1, geometry: %{"type" => "Point", "coordinates" => [4.85, 45.75]}})
    iex> shape.geometry["type"]
    "Point"

    iex> Rover.Shape.new!(%{ref: "a", geom: ~s({"type":"Point","coordinates":[4.85,45.75]})}, id: :ref).id
    "a"

# `new_all!`

```elixir
@spec new_all!(
  Enumerable.t(),
  keyword()
) :: [t()]
```

Normalises a list of shapes. Nil entries are dropped.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
