Rover.Shape (Rover v0.2.0)

Copy Markdown View Source

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

FieldTypeMeaning
:idtermRequired. Stable identity used to diff the map.
:geometrymap / stringRequired. GeoJSON, as above.
:colorstringStroke colour.
:widthnumberStroke width in pixels.
:fill_colorstringFill colour. Defaults to :color.
:fill_opacityfloat0.01.0. Applied to the fill only.
:labelstringText drawn at the centre of the geometry.
:revtermRevision. See below.
:datamapEchoed 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

id()

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

t()

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

Functions

coordinates(geometry)

@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(shape)

@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!(source, opts \\ [])

@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!(shapes, opts \\ [])

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

Normalises a list of shapes. Nil entries are dropped.