# `StaticMaps.Viewport`
[🔗](https://gitlab.com/tristanperalta/staticmaps/blob/v0.1.0/lib/static_maps/viewport.ex#L1)

A resolved view: the centre, zoom, size and scale of one rendered image.

The viewport converts world pixels to output pixels. Everything that places something on
the image goes through `to_output/2`, so tiles and overlays share one rounding rule and
cannot disagree about where a coordinate lands.

`fit/1` works out the centre and zoom when the caller gives bounds or markers instead.

# `fit_request`

```elixir
@type fit_request() :: %{
  optional(:reserve) =&gt; non_neg_integer(),
  center: StaticMaps.Projection.lat_lng() | nil,
  zoom: non_neg_integer() | nil,
  bounds:
    {StaticMaps.Projection.lat_lng(), StaticMaps.Projection.lat_lng()} | nil,
  markers: [StaticMaps.Marker.t()],
  padding: non_neg_integer(),
  size: {pos_integer(), pos_integer()},
  zooms: Range.t()
}
```

What `fit/1` fits the view to. `:center` and `:zoom` are kept when given; `:zooms` is
the range to choose a zoom from, highest first. `:reserve` is a band of that many
logical pixels along the bottom, such as the attribution strip, that nothing is fitted
into.

# `t`

```elixir
@type t() :: %StaticMaps.Viewport{
  center: StaticMaps.Projection.lat_lng(),
  origin: StaticMaps.Projection.point(),
  scale: pos_integer(),
  size: {pos_integer(), pos_integer()},
  zoom: non_neg_integer()
}
```

* `:center` — `{lat, lng}` of the middle of the image
* `:zoom` — integer zoom, in the 256-pixel convention
* `:size` — `{width, height}` in logical pixels, before `:scale`
* `:scale` — output pixels per logical pixel
* `:origin` — the top-left corner of the view, in world pixels

# `fit`

```elixir
@spec fit(fit_request()) :: {StaticMaps.Projection.lat_lng(), non_neg_integer()}
```

Resolves the centre and zoom of a view.

  * With `:center` and `:zoom`, returns them as given.
  * With neither, picks the highest zoom at which the bounds and every marker, pin
    included, fit inside `:size` less `:padding` on every side, and centres on them.
  * With `:center` only, keeps it and picks the highest zoom at which everything fits
    around it.
  * With `:zoom` only, centres everything at that zoom.

The centre is the middle of what is shown in world pixels, not in degrees: Mercator
stretches latitudes unevenly, so the midpoint in degrees is off-centre on the map.

Longitudes are fitted by the shortest arc that covers them all, so markers either side
of the antimeridian frame the antimeridian rather than the whole world. A `:bounds`
with `west > east` crosses the antimeridian. Everything at a single point gets zoom
15, within `:zooms`, or lower if a given centre is too far from it
for the point to show. When nothing fits even at the lowest zoom, the lowest zoom is
used.

    iex> {{lat, lng}, zoom} =
    ...>   StaticMaps.Viewport.fit(%{center: nil, zoom: nil, bounds: {{0, 0}, {10, 10}},
    ...>     markers: [], padding: 0, size: {256, 256}, zooms: 0..19})
    iex> {Float.round(lat, 4), Float.round(lng, 4), zoom}
    {5.0191, 5.0, 5}

# `new`

```elixir
@spec new(
  StaticMaps.Projection.lat_lng(),
  non_neg_integer(),
  {pos_integer(), pos_integer()},
  pos_integer()
) :: t()
```

Builds a viewport centred on `center`.

    iex> vp = StaticMaps.Viewport.new({0, 0}, 0, {256, 256}, 1)
    iex> vp.origin
    {0.0, 0.0}

# `output_size`

```elixir
@spec output_size(t()) :: {pos_integer(), pos_integer()}
```

The size of the output image in pixels, `size * scale`.

# `to_output`

```elixir
@spec to_output(t(), {number(), number()}) :: {integer(), integer()}
```

Converts a point in world pixels to output pixels, rounding halves up.

    iex> vp = StaticMaps.Viewport.new({0, 0}, 1, {100, 100}, 2)
    iex> StaticMaps.Viewport.to_output(vp, {256.0, 256.0})
    {100, 100}

---

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