- Fix leader election guard test for :undefined pid - Fix cpu_sup.util() pattern matching to handle numeric return - Fix encoding_utils to always return explicit nil when needed - Remove unused finite_float? function - Fix signal handler to match :ok return from :os.set_signal/2 - Remove redundant catch-all pattern in database metrics - Fix Gridsquare pattern matching in info_live template - Remove unnecessary nil check for calculate_course result - Fix get_packet_received_at to not check for nil (always returns DateTime) - Remove redundant catch-all pattern in weather format_weather_value - Fix query builder to use from(p in Packet) instead of bare Packet atom Reduced dialyzer errors from 49 to 6. Remaining warnings are mostly false positives from template compilation and overloaded function specs. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
594 lines
20 KiB
Elixir
594 lines
20 KiB
Elixir
defmodule AprsmeWeb.MapLive.DataBuilder do
|
|
@moduledoc """
|
|
Centralizes all packet data building logic for map display.
|
|
|
|
This module consolidates data building functions that were previously
|
|
scattered across multiple modules (index.ex, display_manager.ex,
|
|
packet_utils.ex, and historical_loader.ex).
|
|
|
|
The refactoring eliminates code duplication and provides a single
|
|
source of truth for:
|
|
- Building packet data for real-time display
|
|
- Building minimal packet data for historical display
|
|
- Creating popup content for both weather and standard packets
|
|
- Converting packet data structures for frontend consumption
|
|
|
|
All functions handle both real-time and historical packet data with
|
|
proper coordinate validation, symbol rendering, and weather data processing.
|
|
"""
|
|
|
|
alias AprsmeWeb.Live.Shared.CoordinateUtils
|
|
alias AprsmeWeb.Live.Shared.PacketUtils, as: SharedPacketUtils
|
|
alias AprsmeWeb.Live.Shared.ParamUtils
|
|
alias AprsmeWeb.MapLive.MapHelpers
|
|
alias AprsmeWeb.MapLive.PopupComponent
|
|
alias AprsmeWeb.TimeHelpers
|
|
alias Phoenix.HTML.Safe
|
|
alias Phoenix.LiveView.Socket
|
|
|
|
@doc """
|
|
Build packet data list from a map of packets.
|
|
Replaces the duplicated function in both index.ex and display_manager.ex.
|
|
"""
|
|
@spec build_packet_data_list_from_map(map(), boolean(), Socket.t()) :: list()
|
|
def build_packet_data_list_from_map(packets_map, is_most_recent, socket) do
|
|
locale = get_locale(socket)
|
|
|
|
packets_map
|
|
|> Enum.map(fn {_callsign, packet} ->
|
|
build_packet_data(packet, is_most_recent, locale)
|
|
end)
|
|
|> Enum.filter(& &1)
|
|
end
|
|
|
|
@doc """
|
|
Build packet data for display on the map.
|
|
Main function moved from packet_utils.ex.
|
|
"""
|
|
@spec build_packet_data(map(), boolean(), String.t()) :: map() | nil
|
|
def build_packet_data(packet, is_most_recent_for_callsign, locale) when is_boolean(is_most_recent_for_callsign) do
|
|
{lat, lon, data_extended} = MapHelpers.get_coordinates(packet)
|
|
callsign = generate_callsign(packet)
|
|
|
|
if lat && lon && callsign != "" && callsign != nil do
|
|
packet_data = build_packet_map(packet, lat, lon, data_extended, locale)
|
|
Map.put(packet_data, "is_most_recent_for_callsign", is_most_recent_for_callsign)
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Build packet data with default locale.
|
|
"""
|
|
@spec build_packet_data(map(), boolean()) :: map() | nil
|
|
def build_packet_data(packet, is_most_recent_for_callsign) do
|
|
build_packet_data(packet, is_most_recent_for_callsign, "en")
|
|
end
|
|
|
|
@doc """
|
|
Build packet data with default parameters.
|
|
"""
|
|
@spec build_packet_data(map()) :: map() | nil
|
|
def build_packet_data(packet) do
|
|
build_packet_data(packet, false, "en")
|
|
end
|
|
|
|
@doc """
|
|
Build minimal packet data for historical display.
|
|
Moved from historical_loader.ex.
|
|
"""
|
|
@spec build_minimal_packet_data(map(), boolean(), boolean()) :: map() | nil
|
|
def build_minimal_packet_data(packet, is_most_recent, has_weather) do
|
|
# Build minimal packet data without calling expensive build_packet_data
|
|
{lat, lon, _} = get_coordinates(packet)
|
|
|
|
if lat && lon do
|
|
# Use get_packet_field to get symbol information properly (includes data_extended fallback)
|
|
symbol_table_id = get_packet_field(packet, :symbol_table_id, "/")
|
|
symbol_code = get_packet_field(packet, :symbol_code, ">")
|
|
|
|
# Generate symbol HTML using the SymbolRenderer
|
|
symbol_html =
|
|
AprsmeWeb.SymbolRenderer.render_marker_symbol(
|
|
symbol_table_id,
|
|
symbol_code,
|
|
get_packet_field(packet, :sender, ""),
|
|
32
|
|
)
|
|
|
|
%{
|
|
"id" => if(is_most_recent, do: "current_#{get_packet_id(packet)}", else: "hist_#{get_packet_id(packet)}"),
|
|
"lat" => lat,
|
|
"lng" => lon,
|
|
"callsign" => get_packet_field(packet, :sender, ""),
|
|
"symbol_table_id" => symbol_table_id,
|
|
"symbol_code" => symbol_code,
|
|
"symbol_html" => symbol_html,
|
|
"comment" => get_packet_field(packet, :comment, ""),
|
|
"timestamp" => get_packet_timestamp_unix(packet),
|
|
"historical" => !is_most_recent,
|
|
"is_most_recent_for_callsign" => is_most_recent,
|
|
"path" => get_packet_field(packet, :path, ""),
|
|
"popup" => build_simple_popup(packet, has_weather)
|
|
}
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Build packet data list for historical display.
|
|
Moved from historical_loader.ex.
|
|
"""
|
|
@spec build_packet_data_list(list()) :: list()
|
|
def build_packet_data_list(historical_packets) do
|
|
# Include weather data in initial grouping to avoid separate query
|
|
grouped_packets =
|
|
Enum.group_by(historical_packets, fn packet ->
|
|
get_packet_field(packet, :sender, "unknown")
|
|
end)
|
|
|
|
# Build weather callsign set from packets themselves (no DB query needed)
|
|
weather_callsigns = build_weather_callsign_set(historical_packets)
|
|
|
|
# For each callsign group, find the most recent packet and mark it appropriately
|
|
grouped_packets
|
|
|> Enum.flat_map(fn {callsign, packets} ->
|
|
# Sort by received_at to find most recent
|
|
sorted_packets = Enum.sort_by(packets, &get_packet_received_at(&1), {:desc, DateTime})
|
|
|
|
case sorted_packets do
|
|
[] ->
|
|
[]
|
|
|
|
packets_list ->
|
|
# Find the best packet to display as "current" - prioritize position over weather
|
|
selected_packet = select_best_packet_for_display(packets_list)
|
|
historical = Enum.reject(packets_list, &(get_packet_id(&1) == get_packet_id(selected_packet)))
|
|
|
|
# Always include the selected packet
|
|
has_weather = MapSet.member?(weather_callsigns, String.upcase(callsign))
|
|
most_recent_data = build_minimal_packet_data(selected_packet, true, has_weather)
|
|
|
|
# Get coordinates of selected packet for distance filtering
|
|
{most_recent_lat, most_recent_lon, _} = get_coordinates(selected_packet)
|
|
|
|
# Filter historical packets that are too close to most recent position
|
|
filtered_historical =
|
|
if most_recent_lat && most_recent_lon do
|
|
Enum.filter(historical, fn packet ->
|
|
{lat, lon, _} = get_coordinates(packet)
|
|
|
|
if lat && lon do
|
|
distance_meters =
|
|
CoordinateUtils.calculate_distance_meters(
|
|
most_recent_lat,
|
|
most_recent_lon,
|
|
lat,
|
|
lon
|
|
)
|
|
|
|
# Only show if 10+ meters away
|
|
distance_meters >= 10.0
|
|
else
|
|
# Skip packets without coordinates
|
|
false
|
|
end
|
|
end)
|
|
else
|
|
# If most recent has no coordinates, include all historical
|
|
historical
|
|
end
|
|
|
|
# Build data for remaining historical packets
|
|
historical_data =
|
|
filtered_historical
|
|
|> Enum.map(fn packet -> build_minimal_packet_data(packet, false, has_weather) end)
|
|
|> Enum.filter(& &1)
|
|
|
|
# Combine most recent and filtered historical
|
|
Enum.filter([most_recent_data | historical_data], & &1)
|
|
end
|
|
end)
|
|
|> Enum.filter(& &1)
|
|
end
|
|
|
|
@doc """
|
|
Build simple popup content for historical display.
|
|
Moved from historical_loader.ex.
|
|
"""
|
|
@spec build_simple_popup(map(), boolean()) :: String.t()
|
|
def build_simple_popup(packet, has_weather) do
|
|
# Build popup HTML directly without database queries
|
|
callsign = get_packet_field(packet, :sender, "Unknown")
|
|
timestamp_dt = get_packet_received_at(packet)
|
|
cache_buster = System.system_time(:millisecond)
|
|
|
|
# Check if this packet itself is a weather packet
|
|
is_weather = weather_packet?(packet)
|
|
|
|
if is_weather do
|
|
# Build weather popup
|
|
%{
|
|
callsign: callsign,
|
|
comment: nil,
|
|
timestamp_dt: timestamp_dt,
|
|
cache_buster: cache_buster,
|
|
weather: true,
|
|
weather_link: true,
|
|
temperature: get_weather_field(packet, :temperature),
|
|
temp_unit: "°F",
|
|
humidity: get_weather_field(packet, :humidity),
|
|
wind_direction: get_weather_field(packet, :wind_direction),
|
|
wind_speed: get_weather_field(packet, :wind_speed),
|
|
wind_unit: "mph",
|
|
wind_gust: get_weather_field(packet, :wind_gust),
|
|
gust_unit: "mph",
|
|
pressure: get_weather_field(packet, :pressure),
|
|
rain_1h: get_weather_field(packet, :rain_1h),
|
|
rain_24h: get_weather_field(packet, :rain_24h),
|
|
rain_since_midnight: get_weather_field(packet, :rain_since_midnight),
|
|
rain_1h_unit: "in",
|
|
rain_24h_unit: "in",
|
|
rain_since_midnight_unit: "in"
|
|
}
|
|
|> PopupComponent.popup()
|
|
|> Safe.to_iodata()
|
|
|> IO.iodata_to_binary()
|
|
else
|
|
# Build standard popup
|
|
%{
|
|
callsign: callsign,
|
|
comment: get_packet_field(packet, :comment, ""),
|
|
timestamp_dt: timestamp_dt,
|
|
cache_buster: cache_buster,
|
|
weather: false,
|
|
# Use pre-fetched weather info
|
|
weather_link: has_weather
|
|
}
|
|
|> PopupComponent.popup()
|
|
|> Safe.to_iodata()
|
|
|> IO.iodata_to_binary()
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Select the best packet to display for a callsign.
|
|
Moved from historical_loader.ex.
|
|
"""
|
|
@spec select_best_packet_for_display(list()) :: map()
|
|
def select_best_packet_for_display(packets) do
|
|
# Separate position and weather packets using the same logic as weather_packet?
|
|
{position_packets, weather_packets} =
|
|
Enum.split_with(packets, fn packet ->
|
|
# A packet is a position packet if it's NOT a weather packet
|
|
not weather_packet?(packet)
|
|
end)
|
|
|
|
# Prefer the most recent position packet, fall back to most recent weather packet
|
|
case position_packets do
|
|
[] ->
|
|
# No position packets, use most recent weather packet
|
|
hd(weather_packets)
|
|
|
|
[single_position] ->
|
|
# Only one position packet, use it
|
|
single_position
|
|
|
|
position_list ->
|
|
# Multiple position packets, use most recent one
|
|
Enum.max_by(position_list, &get_packet_received_at(&1), DateTime)
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Build weather callsign set from packets.
|
|
Moved from historical_loader.ex.
|
|
"""
|
|
@spec build_weather_callsign_set(list()) :: MapSet.t()
|
|
def build_weather_callsign_set(packets) do
|
|
packets
|
|
|> Enum.filter(&weather_packet?/1)
|
|
|> MapSet.new(fn packet -> String.upcase(get_packet_field(packet, :sender, "")) end)
|
|
end
|
|
|
|
# Private functions moved from packet_utils.ex
|
|
|
|
@spec build_packet_map(map(), float(), float(), map(), String.t()) :: map()
|
|
defp build_packet_map(packet, lat, lon, data_extended, locale) do
|
|
data_extended = data_extended || %{}
|
|
packet_info = extract_packet_info(packet, data_extended)
|
|
popup = build_popup_content(packet, packet_info, lat, lon, locale)
|
|
|
|
build_packet_result(packet, packet_info, lat, lon, popup)
|
|
end
|
|
|
|
@spec extract_packet_info(map(), map()) :: map()
|
|
defp extract_packet_info(packet, data_extended) do
|
|
%{
|
|
callsign: get_packet_field(packet, :sender, ""),
|
|
symbol_table_id: get_packet_field(packet, :symbol_table_id, "/"),
|
|
symbol_code: get_packet_field(packet, :symbol_code, ">"),
|
|
timestamp: get_timestamp(packet),
|
|
comment: get_packet_field(packet, :comment, ""),
|
|
safe_data_extended: convert_tuples_to_strings(data_extended),
|
|
is_weather_packet: weather_packet?(packet)
|
|
}
|
|
end
|
|
|
|
@spec build_popup_content(map(), map(), float(), float(), String.t()) :: String.t()
|
|
defp build_popup_content(packet, packet_info, lat, lon, locale) do
|
|
if packet_info.is_weather_packet do
|
|
build_weather_popup_html(packet, packet_info.callsign, locale)
|
|
else
|
|
build_standard_popup_html(packet_info, lat, lon)
|
|
end
|
|
end
|
|
|
|
@spec build_standard_popup_html(map(), float(), float()) :: String.t()
|
|
defp build_standard_popup_html(packet_info, _lat, _lon) do
|
|
timestamp_dt = TimeHelpers.to_datetime(packet_info.timestamp)
|
|
cache_buster = System.system_time(:millisecond)
|
|
weather_link = has_weather_packets?(packet_info.callsign)
|
|
|
|
popup_data = %{
|
|
callsign: packet_info.callsign,
|
|
comment: packet_info.comment,
|
|
timestamp_dt: timestamp_dt,
|
|
cache_buster: cache_buster,
|
|
weather: false,
|
|
weather_link: weather_link
|
|
}
|
|
|
|
# Optimize string conversion - avoid intermediate iodata step
|
|
popup_data
|
|
|> PopupComponent.popup()
|
|
|> Safe.to_iodata()
|
|
|> IO.iodata_to_binary()
|
|
end
|
|
|
|
@spec build_weather_popup_html(map(), String.t(), String.t()) :: String.t()
|
|
defp build_weather_popup_html(packet, sender, locale) do
|
|
received_at = get_received_at(packet)
|
|
timestamp_dt = TimeHelpers.to_datetime(received_at)
|
|
cache_buster = System.system_time(:millisecond)
|
|
|
|
# Get weather data and convert units based on locale
|
|
temperature_raw = get_weather_field(packet, :temperature)
|
|
wind_speed_raw = get_weather_field(packet, :wind_speed)
|
|
wind_gust_raw = get_weather_field(packet, :wind_gust)
|
|
rain_1h_raw = get_weather_field(packet, :rain_1h)
|
|
rain_24h_raw = get_weather_field(packet, :rain_24h)
|
|
rain_since_midnight_raw = get_weather_field(packet, :rain_since_midnight)
|
|
|
|
# Convert units if the values are numbers
|
|
{temperature, temp_unit} = convert_temperature(temperature_raw, locale)
|
|
{wind_speed, wind_unit} = convert_wind_speed(wind_speed_raw, locale)
|
|
{wind_gust, gust_unit} = convert_wind_speed(wind_gust_raw, locale)
|
|
{rain_1h, rain_1h_unit} = convert_rain(rain_1h_raw, locale)
|
|
{rain_24h, rain_24h_unit} = convert_rain(rain_24h_raw, locale)
|
|
{rain_since_midnight, rain_since_midnight_unit} = convert_rain(rain_since_midnight_raw, locale)
|
|
|
|
%{
|
|
callsign: sender,
|
|
comment: nil,
|
|
timestamp_dt: timestamp_dt,
|
|
cache_buster: cache_buster,
|
|
weather: true,
|
|
weather_link: true,
|
|
temperature: temperature,
|
|
temp_unit: temp_unit,
|
|
humidity: get_weather_field(packet, :humidity),
|
|
wind_direction: get_weather_field(packet, :wind_direction),
|
|
wind_speed: wind_speed,
|
|
wind_unit: wind_unit,
|
|
wind_gust: wind_gust,
|
|
gust_unit: gust_unit,
|
|
pressure: get_weather_field(packet, :pressure),
|
|
rain_1h: rain_1h,
|
|
rain_24h: rain_24h,
|
|
rain_since_midnight: rain_since_midnight,
|
|
rain_1h_unit: rain_1h_unit,
|
|
rain_24h_unit: rain_24h_unit,
|
|
rain_since_midnight_unit: rain_since_midnight_unit
|
|
}
|
|
|> PopupComponent.popup()
|
|
|> Safe.to_iodata()
|
|
|> IO.iodata_to_binary()
|
|
end
|
|
|
|
@spec build_packet_result(map(), map(), float(), float(), String.t()) :: map()
|
|
defp build_packet_result(packet, packet_info, lat, lon, popup) do
|
|
# Generate unique ID for live packets to prevent conflicts
|
|
packet_id =
|
|
if Map.has_key?(packet, :id) do
|
|
"live_#{packet.id}_#{System.unique_integer([:positive])}"
|
|
else
|
|
"live_#{packet_info.callsign}_#{System.unique_integer([:positive])}"
|
|
end
|
|
|
|
# Generate symbol HTML using the server-side renderer
|
|
symbol_html =
|
|
AprsmeWeb.SymbolRenderer.render_marker_symbol(
|
|
packet_info.symbol_table_id,
|
|
packet_info.symbol_code,
|
|
packet_info.callsign,
|
|
32
|
|
)
|
|
|
|
path_value = get_packet_field(packet, :path, "")
|
|
|
|
%{
|
|
"id" => packet_id,
|
|
"callsign" => packet_info.callsign,
|
|
"base_callsign" => get_packet_field(packet, :base_callsign, ""),
|
|
"ssid" => get_packet_field(packet, :ssid, nil),
|
|
"lat" => to_float(lat),
|
|
"lng" => to_float(lon),
|
|
"data_type" => to_string(get_packet_field(packet, :data_type, "unknown")),
|
|
"path" => path_value,
|
|
"comment" => packet_info.comment,
|
|
"data_extended" => packet_info.safe_data_extended || %{},
|
|
"symbol_table_id" => packet_info.symbol_table_id,
|
|
"symbol_code" => packet_info.symbol_code,
|
|
"symbol_description" => "Symbol: #{packet_info.symbol_table_id}#{packet_info.symbol_code}",
|
|
"timestamp" => packet_info.timestamp,
|
|
"popup" => popup,
|
|
"symbol_html" => symbol_html
|
|
}
|
|
end
|
|
|
|
# Utility functions for packet field access
|
|
|
|
@spec get_packet_field(map(), atom() | String.t(), any()) :: any()
|
|
defp get_packet_field(packet, field, default) do
|
|
SharedPacketUtils.get_packet_field(packet, field, default)
|
|
end
|
|
|
|
@spec get_timestamp(map()) :: String.t()
|
|
defp get_timestamp(packet) do
|
|
SharedPacketUtils.get_timestamp(packet)
|
|
end
|
|
|
|
@spec to_float(any()) :: float()
|
|
defp to_float(value) do
|
|
ParamUtils.to_float(value)
|
|
end
|
|
|
|
@spec generate_callsign(map()) :: String.t()
|
|
defp generate_callsign(packet) do
|
|
SharedPacketUtils.generate_callsign(packet)
|
|
end
|
|
|
|
@spec weather_packet?(map()) :: boolean()
|
|
defp weather_packet?(packet) do
|
|
SharedPacketUtils.weather_packet?(packet)
|
|
end
|
|
|
|
@spec has_weather_packets?(String.t()) :: boolean()
|
|
defp has_weather_packets?(callsign) when is_binary(callsign) do
|
|
Aprsme.Packets.has_weather_packets?(callsign)
|
|
rescue
|
|
_ -> false
|
|
end
|
|
|
|
defp has_weather_packets?(_), do: false
|
|
|
|
@spec convert_tuples_to_strings(any()) :: any()
|
|
defp convert_tuples_to_strings(map) when is_map(map) do
|
|
# Avoid processing structs entirely
|
|
case map do
|
|
%{__struct__: _} -> map
|
|
_ -> Map.new(map, &convert_tuple_entry/1)
|
|
end
|
|
end
|
|
|
|
defp convert_tuples_to_strings(list) when is_list(list) do
|
|
# Use Stream for memory efficiency on large lists
|
|
list
|
|
|> Stream.map(&convert_tuples_to_strings/1)
|
|
|> Enum.to_list()
|
|
end
|
|
|
|
defp convert_tuples_to_strings(tuple) when is_tuple(tuple) do
|
|
# Optimize tuple string conversion - avoid inspect for common cases
|
|
case tuple_size(tuple) do
|
|
2 ->
|
|
{a, b} = tuple
|
|
"#{a}, #{b}"
|
|
|
|
3 ->
|
|
{a, b, c} = tuple
|
|
"#{a}, #{b}, #{c}"
|
|
|
|
_ ->
|
|
inspect(tuple)
|
|
end
|
|
end
|
|
|
|
defp convert_tuples_to_strings(other), do: other
|
|
|
|
defp convert_tuple_entry({k, v}), do: {k, convert_tuples_to_strings(v)}
|
|
|
|
@spec get_weather_field(map(), atom()) :: String.t()
|
|
defp get_weather_field(packet, key) do
|
|
case SharedPacketUtils.get_weather_field(packet, key) do
|
|
nil -> "N/A"
|
|
value -> value
|
|
end
|
|
end
|
|
|
|
@spec get_received_at(map()) :: DateTime.t() | nil
|
|
defp get_received_at(packet) do
|
|
cond do
|
|
Map.has_key?(packet, :received_at) -> packet.received_at
|
|
Map.has_key?(packet, "received_at") -> packet["received_at"]
|
|
true -> nil
|
|
end
|
|
end
|
|
|
|
# Helper to get locale from socket
|
|
@spec get_locale(Socket.t()) :: String.t()
|
|
defp get_locale(socket) do
|
|
Map.get(socket.assigns, :locale, "en")
|
|
end
|
|
|
|
# Helper to get coordinates from packet
|
|
@spec get_coordinates(map()) :: {float() | nil, float() | nil, map()}
|
|
defp get_coordinates(packet) do
|
|
CoordinateUtils.get_coordinates(packet)
|
|
end
|
|
|
|
# Helper to get packet ID
|
|
@spec get_packet_id(map()) :: any()
|
|
defp get_packet_id(packet) do
|
|
Map.get(packet, :id) || Map.get(packet, "id")
|
|
end
|
|
|
|
# Helper to get packet received_at datetime
|
|
@spec get_packet_received_at(map()) :: DateTime.t()
|
|
defp get_packet_received_at(packet) do
|
|
get_received_at(packet) || DateTime.utc_now()
|
|
end
|
|
|
|
# Helper to get packet timestamp as unix milliseconds
|
|
@spec get_packet_timestamp_unix(map()) :: integer()
|
|
defp get_packet_timestamp_unix(packet) do
|
|
received_at = get_packet_received_at(packet)
|
|
DateTime.to_unix(received_at, :millisecond)
|
|
end
|
|
|
|
# Weather unit conversion helpers
|
|
defp convert_temperature(value, locale) when is_binary(value) and value != "N/A" do
|
|
case Float.parse(value) do
|
|
{num_value, _} ->
|
|
AprsmeWeb.WeatherUnits.format_temperature(num_value, locale)
|
|
|
|
:error ->
|
|
{value, "°F"}
|
|
end
|
|
end
|
|
|
|
defp convert_temperature(value, _locale), do: {value, "°F"}
|
|
|
|
defp convert_wind_speed(value, locale) when is_binary(value) and value != "N/A" do
|
|
case Float.parse(value) do
|
|
{num_value, _} ->
|
|
AprsmeWeb.WeatherUnits.format_wind_speed(num_value, locale)
|
|
|
|
:error ->
|
|
{value, "mph"}
|
|
end
|
|
end
|
|
|
|
defp convert_wind_speed(value, _locale), do: {value, "mph"}
|
|
|
|
defp convert_rain(value, locale) when is_binary(value) and value != "N/A" do
|
|
case Float.parse(value) do
|
|
{num_value, _} ->
|
|
AprsmeWeb.WeatherUnits.format_rain(num_value, locale)
|
|
|
|
:error ->
|
|
{value, "in"}
|
|
end
|
|
end
|
|
|
|
defp convert_rain(value, _locale), do: {value, "in"}
|
|
end
|