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
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),
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. |
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.
Summary
Functions
Every coordinate in a geometry, as {lat, lon} pairs.
Renders a shape as the compact map handed to the JavaScript runtime.
Normalises source into a Rover.Shape.
Normalises a list of shapes. Nil entries are dropped.
Types
Functions
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}]
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]
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"
@spec new_all!( Enumerable.t(), keyword() ) :: [t()]
Normalises a list of shapes. Nil entries are dropped.