HoloMap.Spec (HoloMap v0.1.0)

Copy Markdown View Source

Turns Elixir-flavoured component props into MapLibre style-specification maps.

The MapLibre style specification is not written in one casing. Paint and layout properties are kebab-case (fill-color, text-allow-overlap), some layer keys are kebab-case (source-layer), and several source keys are camelCase (tileSize, promoteId, clusterMaxZoom). A single mechanical rule would get one of those groups wrong, so this module keeps two distinct paths:

  • dasherize/1, for paint and layout maps, where underscore-to-hyphen is genuinely the universal rule.
  • take/2, for everything else, where each prop is mapped to its exact specification key through an explicit table declared by the component.

Both paths drop nil values instead of emitting null. MapLibre treats an explicit null as "reset to the default", which is not what an unset prop means, and an absent key leaves the property alone.

Escape hatch

String keys always pass through untouched. A property this library has not caught up with is still reachable:

paint={%{"fill-color" => "#f00", "some-new-property" => 1}}

Summary

Types

A longitude/latitude pair in any of the accepted input shapes.

t()

A MapLibre style specification fragment, ready for JSON encoding.

Functions

Coerces a boolean prop written as a bare attribute.

Applies boolean/1 to the listed keys of a props map.

Normalises a bounding box into MapLibre's [[west, south], [east, north]].

Converts a paint or layout map into specification keys.

Normalises a longitude/latitude pair into the [lng, lat] list MapLibre uses.

Merges specification fragments left to right, with later keys winning.

Builds a specification map by pulling props through an explicit key table.

Types

lng_lat()

@type lng_lat() :: {number(), number()} | [number()] | %{lng: number(), lat: number()}

A longitude/latitude pair in any of the accepted input shapes.

t()

@type t() :: %{optional(String.t()) => term()}

A MapLibre style specification fragment, ready for JSON encoding.

Functions

boolean(other)

@spec boolean(term()) :: boolean() | nil

Coerces a boolean prop written as a bare attribute.

In a HOLO template a bare attribute is an empty string, not true, so <HoloMap.Source.GeoJSON cluster /> would otherwise hand MapLibre "" and fail style validation with boolean expected, string found. Writing the attribute at all means "on", which is what every component framework's bare attribute means, so that is what it becomes.

iex> HoloMap.Spec.boolean("")
true

iex> HoloMap.Spec.boolean(true)
true

iex> HoloMap.Spec.boolean("false")
false

iex> HoloMap.Spec.boolean(nil)
nil

booleans(props, keys)

@spec booleans(map(), [atom()]) :: map()

Applies boolean/1 to the listed keys of a props map.

Components declare their own boolean props rather than coercing everything, because an empty string is a legitimate value for a text prop and must not silently become true there.

iex> HoloMap.Spec.booleans(%{cluster: "", radius: 50}, [:cluster])
%{cluster: true, radius: 50}

bounds(other)

@spec bounds(term()) :: [[number()]] | nil

Normalises a bounding box into MapLibre's [[west, south], [east, north]].

Accepts a pair of corners in tuple or list form, or the flat [west, south, east, north] shape that GeoJSON bounding boxes use.

iex> HoloMap.Spec.bounds({{-71.0, 19.0}, {-70.0, 20.0}})
[[-71.0, 19.0], [-70.0, 20.0]]

iex> HoloMap.Spec.bounds([-71.0, 19.0, -70.0, 20.0])
[[-71.0, 19.0], [-70.0, 20.0]]

iex> HoloMap.Spec.bounds(nil)
nil

dasherize(map)

@spec dasherize(map() | nil) :: t()

Converts a paint or layout map into specification keys.

Atom keys are dasherized; string keys pass through verbatim; nil values are dropped.

iex> HoloMap.Spec.dasherize(%{fill_color: "#f00", fill_opacity: nil})
%{"fill-color" => "#f00"}

iex> HoloMap.Spec.dasherize(%{"text-allow-overlap" => true})
%{"text-allow-overlap" => true}

iex> HoloMap.Spec.dasherize(nil)
%{}

lng_lat(other)

@spec lng_lat(lng_lat() | nil) :: [number()] | nil

Normalises a longitude/latitude pair into the [lng, lat] list MapLibre uses.

Tuples read well in templates but have no JSON representation, so they are converted here rather than at the encoder.

iex> HoloMap.Spec.lng_lat({-70.66, 19.45})
[-70.66, 19.45]

iex> HoloMap.Spec.lng_lat(%{lng: -70.66, lat: 19.45})
[-70.66, 19.45]

iex> HoloMap.Spec.lng_lat([-70.66, 19.45])
[-70.66, 19.45]

iex> HoloMap.Spec.lng_lat(nil)
nil

merge(fragments)

@spec merge([t() | nil]) :: t()

Merges specification fragments left to right, with later keys winning.

nil fragments are ignored, which lets a component pipe optional sections together without branching.

iex> HoloMap.Spec.merge([%{"a" => 1}, nil, %{"a" => 2, "b" => 3}])
%{"a" => 2, "b" => 3}

take(props, table)

@spec take(
  map(),
  keyword(String.t())
) :: t()

Builds a specification map by pulling props through an explicit key table.

table is a keyword list of {prop_name, specification_key}. Props that are absent or nil are skipped, so a component can declare its full surface once and let the user set only what they need.

iex> props = %{source_layer: "water", min_zoom: 4, max_zoom: nil}
iex> table = [source_layer: "source-layer", min_zoom: "minzoom", max_zoom: "maxzoom"]
iex> HoloMap.Spec.take(props, table)
%{"source-layer" => "water", "minzoom" => 4}