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.
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
Functions
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
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}
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
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)
%{}
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
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}
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}