Spots that include subsquare (EM12kl), extended-square (EM12kl37), or extended-extended-subsquare (EM12kl37ab) precision now keep that resolution end-to-end. Previously we truncated everything to 4 chars at parse time, which collapsed distinct paths into the same row whenever two spotters shared a field. The aggregation key uses the full grid as reported, so two spotters in different subsquares of `EM12` produce two rows — correct, since they are physically different paths. Any spot that fails Maidenhead validation (odd length, illegal char at position N) is still dropped via the same error path. Storage normalizes to uppercase so the unique index doesn't split on case. Maidenhead.valid?/1 already accepts both cases on input.
71 lines
2.6 KiB
Elixir
71 lines
2.6 KiB
Elixir
defmodule Microwaveprop.Pskr.SpotHourly do
|
|
@moduledoc """
|
|
Hourly aggregate of PSK Reporter spots between two Maidenhead
|
|
grid locators on a single band. Locator precision is preserved
|
|
from the source spot (4/6/8/10 chars), so two spotters in
|
|
different subsquares of the same field generate distinct rows
|
|
even though they share an `EM12` prefix.
|
|
|
|
A single QSO often produces 50+ reception reports (one per
|
|
monitoring station that decoded it). Storing each report verbatim
|
|
would dwarf the rest of the database — instead `Pskr.Aggregator`
|
|
collapses every spot inside `(hour_utc, band, sender_grid,
|
|
receiver_grid)` into one row, retaining `spot_count`, SNR
|
|
envelope, and the set of modes observed. Downstream weather
|
|
correlation cares about *did propagation occur in this cell during
|
|
this hour*, not about each individual reporter — so the lossy
|
|
aggregation is the desired shape, not just a space optimization.
|
|
|
|
`midpoint_lat/lon` is the great-circle midpoint of the path,
|
|
pre-computed at insert time so HRRR / IEMRE grid lookups are a
|
|
cheap `WHERE midpoint_lat BETWEEN …` rather than a per-row
|
|
recomputation.
|
|
"""
|
|
use Ecto.Schema
|
|
|
|
import Ecto.Changeset
|
|
|
|
@primary_key {:id, :binary_id, autogenerate: true}
|
|
@foreign_key_type :binary_id
|
|
|
|
schema "pskr_spots_hourly" do
|
|
field :hour_utc, :utc_datetime
|
|
field :band, :string
|
|
field :sender_grid, :string
|
|
field :receiver_grid, :string
|
|
field :sender_country, :integer
|
|
field :receiver_country, :integer
|
|
field :sender_lat, :float
|
|
field :sender_lon, :float
|
|
field :receiver_lat, :float
|
|
field :receiver_lon, :float
|
|
field :midpoint_lat, :float
|
|
field :midpoint_lon, :float
|
|
field :distance_km, :float
|
|
field :spot_count, :integer, default: 0
|
|
field :max_snr_db, :integer
|
|
field :min_snr_db, :integer
|
|
field :modes, {:array, :string}, default: []
|
|
field :first_spot_at, :utc_datetime
|
|
field :last_spot_at, :utc_datetime
|
|
|
|
timestamps(type: :utc_datetime)
|
|
end
|
|
|
|
@type t :: %__MODULE__{}
|
|
|
|
@cast_fields ~w(hour_utc band sender_grid receiver_grid sender_country receiver_country
|
|
sender_lat sender_lon receiver_lat receiver_lon midpoint_lat midpoint_lon
|
|
distance_km spot_count max_snr_db min_snr_db modes first_spot_at
|
|
last_spot_at)a
|
|
|
|
@required_fields ~w(hour_utc band sender_grid receiver_grid spot_count)a
|
|
|
|
@spec changeset(t(), map()) :: Ecto.Changeset.t()
|
|
def changeset(record, attrs) do
|
|
record
|
|
|> cast(attrs, @cast_fields)
|
|
|> validate_required(@required_fields)
|
|
|> unique_constraint([:hour_utc, :band, :sender_grid, :receiver_grid])
|
|
end
|
|
end
|