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

The `<.map>` component.

    import Rover.Components

    <.map id="clients" center={{45.75, 4.85}} zoom={12} markers={@clients} />

That is the whole API for the common case. Everything below is about the less
common ones.

## How updates reach the map

Rover renders your markers into a `data-rover-markers` attribute. When you
`assign/3` a new list, LiveView diffs the attribute and sends only that
attribute down the wire; the JavaScript runtime then diffs the list *by marker
id* and touches only the OpenLayers features that actually changed. Adding one
marker to a list of five hundred adds one feature — it does not rebuild the
layer, and it does not interrupt a pan, a zoom or an open popup.

This is why `Rover.Marker` insists on a stable `:id`.

## Events

Each `on_*` attribute takes the name of an event your LiveView handles:

    <.map id="clients" markers={@clients} on_marker_click="select_client" />

    def handle_event("select_client", %{"id" => id}, socket) do
      {:noreply, assign(socket, selected: id)}
    end

| Attribute | Payload |
|---|---|
| `on_marker_click` | `%{"id" => id, "lat" => lat, "lon" => lon, "data" => data}` |
| `on_cluster_click` | `%{"count" => n, "ids" => [id, …], "lat" => lat, "lon" => lon}` |
| `on_shape_click` | `%{"id" => id, "lat" => lat, "lon" => lon, "data" => data}` |
| `on_map_click` | `%{"lat" => lat, "lon" => lon}` |
| `on_move_end` | `%{"center" => [lat, lon], "zoom" => zoom, "bbox" => %{"south" =>, "west" =>, "north" =>, "east" =>}}` |
| `on_marker_drag_end` | `%{"id" => id, "lat" => lat, "lon" => lon}` |
| `on_shape_edit_end` | `%{"id" => id, "geometry" => geojson_geometry, "properties" => geojson_properties, "data" => data}` |

Inside a `Phoenix.LiveComponent`, route the events to yourself with
`target={@myself}`.

> #### Viewports can straddle the antimeridian {: .warning}
>
> Longitudes are wrapped into `-180..180`, so a user looking at Fiji or New
> Zealand gets a `bbox` where `west` is greater than `east`. When that happens
> the map adds `"crosses_antimeridian" => true`, because the obvious query —
> `where: m.lon >= ^west and m.lon <= ^east` — matches nothing for those
> users. Split the range in two when you see the flag.

## Controlled view, uncontrolled panning

`center` and `zoom` are applied when they *change on the server*. A user
panning the map does not push new values back unless you ask for them with
`on_move_end`, and a re-render triggered by something unrelated will not yank
the view back to where it started. Assign a new `center` and the map animates
to it.

When you give no `center` at all, Rover derives a starting frame from the
markers. That derived value is explicitly *not* treated as an instruction —
otherwise moving one marker would shift the centroid and drag the view along
with it on every update.

## Framing versus refitting

Two separate things:

* **The first frame.** With no `center`, "put my markers on screen" is the
  whole instruction, so the map always fits the markers once when it appears.
  The client does it, because only the client knows the viewport size.
* **Refitting.** `fit` governs what happens *afterwards*. `false` leaves the
  view alone, `:once` does nothing more, `true` refits on every change.

# `map`

```elixir
@spec map(map()) :: Phoenix.LiveView.Rendered.t()
```

Renders an interactive map.

## Examples

Three markers around Lyon, clickable:

    <.map
      id="clients"
      center={{45.75, 4.85}}
      zoom={12}
      markers={@clients}
      on_marker_click="select_client"
    />

Fit the view to whatever is on the map instead of choosing a center:

    <.map id="fleet" markers={@vehicles} fit={true} tiles={:carto_dark} height="60vh" />

Read markers out of an Ecto schema that names its fields differently:

    <.map
      id="stores"
      markers={@stores}
      marker_fields={[lat: :latitude, lon: :longitude, label: :trade_name]}
    />

## Attributes

* `id` (`:string`) (required) - DOM id. Required — the map is a stateful hook and LiveView needs to track it.
* `center` (`:any`) - The `{lat, lon}` the view is centred on. Defaults to the centre of `markers`
  when they are given, and to `{0.0, 0.0}` otherwise.

  Defaults to `nil`.
* `zoom` (`:any`) - Zoom level, roughly 0 (world) to 20 (building). Defaults to `nil`.
* `min_zoom` (`:any`) - Lowest zoom the user can reach. Defaults to `nil`.
* `max_zoom` (`:any`) - Highest zoom the user can reach. Defaults to `nil`.
* `markers` (`:list`) - Anything `Rover.Marker.new!/2` accepts: maps, structs, `Rover.Marker`s. Defaults to `[]`.
* `marker_fields` (`:list`) - Field mapping passed to `Rover.Marker.new!/2`, e.g. `[lat: :latitude]`. Defaults to `[]`.
* `shapes` (`:list`) - Anything `Rover.Shape.new!/2` accepts. GeoJSON geometries — see `Rover.Shape`. Defaults to `[]`.
* `shape_fields` (`:list`) - Field mapping passed to `Rover.Shape.new!/2`, e.g. `[geometry: :outline]`. Defaults to `[]`.
* `cluster` (`:any`) - Groups nearby markers into counted circles, which is the answer to hundreds of
  them. `true` for the defaults, or a keyword list:

    * `:distance` — how close, in pixels, two markers must be to group. Default `40`.
    * `:min_distance` — minimum gap between two groups, in pixels. Default `20`.
    * `:zoom_on_click` — zoom into a group when it is clicked. Default `true`.

  Clicking a group also sends `on_cluster_click`. A group of one is drawn as its
  own marker, so nothing looks clustered until it actually is.

  Two consequences worth knowing: a marker that has been grouped has no popup —
  the popup would point at the group's centre rather than at the marker — and
  `:draggable` markers cannot be dragged at all while `:cluster` is set, even
  standing alone. Every marker is wrapped by a cluster feature once clustering
  is on, a lone one included, and dragging that would move the wrapper rather
  than the marker.

  Defaults to `false`.
* `on_cluster_click` (`:string`) - Receives `%{"count" => n, "ids" => [id, …], "lat" => lat, "lon" => lon}` when a
  group is clicked. Note `"ids"` and `"count"` rather than a single `"id"` — a
  group is not a marker.

  Defaults to `nil`.
* `heatmap` (`:list`) - Points for a density field — see `Rover.Heatmap`. No `:id` needed: a heatmap is
  an aggregate, so it is diffed by revision rather than feature by feature.

  Defaults to `[]`.
* `heatmap_fields` (`:list`) - Field mapping for the heatmap, e.g. `[weight: fn r -> r.orders / 40 end]`. Defaults to `[]`.
* `heatmap_style` (`:list`) - Any of `:radius`, `:blur`, `:opacity`, `:gradient`. See `Rover.Heatmap.style!/1`. Defaults to `[]`.
* `tiles` (`:any`) - A `Rover.Tiles` preset, `{:xyz, url}`, or `:none`. Defaults to `:osm`.
* `fit` (`:any`) - Controls *re*fitting as markers change: `true` refits on every change,
  `:once` or `false` do not. Defaults to `:once` when no `center` is given,
  `false` otherwise. Note that a map given no `center` always fits once when it
  first appears, whatever `fit` says — see "Framing versus refitting".

  Defaults to `nil`.
* `fit_padding` (`:integer`) - Pixels kept clear around a fitted view. Defaults to `48`.
* `controls` (`:list`) - Any of `:zoom`, `:attribution`, `:scale_line`, `:full_screen`, `:rotate`. Defaults to `[:zoom, :attribution]`.
* `interactive` (`:boolean`) - When false the map becomes a picture: no panning, zooming, dragging,
  tooltips, cursor changes or click events, and the zoom, fullscreen and rotate
  controls are withheld. The attribution stays — it is a licence obligation,
  not an interaction — and so does the scale line if you asked for one.

  Defaults to `true`.
* `on_marker_click` (`:string`) - Defaults to `nil`.
* `on_shape_click` (`:string`) - Defaults to `nil`.
* `on_map_click` (`:string`) - Defaults to `nil`.
* `on_move_end` (`:string`) - Defaults to `nil`.
* `on_marker_drag_end` (`:string`) - Defaults to `nil`.
* `on_shape_edit_end` (`:string`) - Defaults to `nil`.
* `target` (`:any`) - `@myself` to route events to the enclosing `Phoenix.LiveComponent`. Defaults to `nil`.
* `height` (`:any`) - CSS height, applied as an inline style. Pass `nil` to emit no style at all and
  size the map from your own CSS — a Tailwind class, a flex parent, a container
  query. Note that an inline style beats a class, so `class="h-96"` needs
  `height={nil}` to take effect.

  Defaults to `"24rem"`.
* `class` (`:any`) - Extra classes on the map container. Defaults to `nil`.
* Global attributes are accepted.
## Slots

* `popup` - Rendered once per marker and shown when that marker is clicked, with no server
  round-trip. Receives the `Rover.Marker` via `:let`.

      <.map id="clients" markers={@clients}>
        <:popup :let={marker}>
          <h3>{marker.label}</h3>
          <p>{marker.data && marker.data.address}</p>
          <button data-rover-popup-close>Close</button>
        </:popup>
      </.map>

  `:data` is `nil` unless you set it, hence the guard.

  Any element carrying `data-rover-popup-close` closes it; so do a click on the
  map and the Escape key. Because every marker's popup is rendered up front,
  this costs one DOM node per marker — fine for dozens, which is why clustering
  rather than popups is the answer to hundreds.

* `shape_popup` - The same, for shapes. Receives the `Rover.Shape` via `:let`, and opens where the
  geometry was clicked rather than at its centroid — pointing at the middle of a
  long route or a large parcel would point at nothing the user did.

      <.map id="parcels" shapes={@parcels}>
        <:shape_popup :let={shape}>
          <h3>{shape.label}</h3>
          <p>{shape.data && shape.data.area} ha</p>
        </:shape_popup>
      </.map>

  Works with or without `on_shape_click`: the click is claimed when either the
  server or a popup wants it, and by neither when the shape is scenery.

---

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