The compute pipeline now actually runs: per-pixel path loss over LIDAR
terrain, antenna pattern lookup, real GeoTIFF + colored PNG output,
Leaflet imageOverlay rendering with opacity slider and dBm legend.
Propagation:
- Towerops.Coverages.Propagation: Friis FSPL + Bullington single-knife-edge
diffraction (ITU-R P.526). Pure Elixir, closed-form, no NIF dependency.
Clean signature so a future ITM/Longley-Rice backend can drop in.
- Towerops.Coverages.Profile: AAIGrid sampler with elevation_at, sample
along great circle, haversine distance.
- Towerops.Coverages.Raster: Float32 GeoTIFF via gdal_translate (with VRT
wrapper) and colored PNG via gdaldem color-relief. Outputs to
priv/static/coverage/<org>/<id>/, served by existing Plug.Static.
Worker:
- Towerops.Workers.CoverageWorker now does the full pipeline: bbox compute
-> Towerops.Lidar.get_elevation_grid -> per-pixel Task.async_stream
(parallel = schedulers) running antenna pattern lookup + propagation +
RSSI threshold check, write rasters. Configurable terrain source via
:coverage_terrain_module application env so tests can stub. Friendly
failure messages for :no_tile, :grid_too_large, :nodata,
:unknown_antenna, :missing_location.
Bundled antenna catalog (~95 entries):
- AntennaCatalog with compact specs for every antenna in the request list:
Cambium, ALPHA, Antel, ITELITE, KP Performance, L-com, MARS, MikroTik,
Mimosa, MTI, RADWIN, RF Elements, Ruckus, Tarana, Simulate, Ubiquiti.
- Antenna.from_spec/1 synthesizes 360+360 attenuation patterns from
gain+beamwidth (cosine-squared main lobe, smooth transition,
front-to-back floor with mild ripple). Real .ant files in priv/antennas/
override the catalog by slug. Test fixtures moved to test/support.
Schema (migration add_radio_fields_to_coverages):
- tx_power_dbm replaces eirp_dbm; EIRP is now derived as
tx_power + antenna.gain - cable_loss in Coverage.eirp_dbm/2 and shown
live in the form header.
- cable_loss_db, sm_gain_dbi, latitude_override, longitude_override,
height_above_rooftop_m, tx_clearance_m, foliage_tuning (0-100 slider).
- Coverage.location/1 returns {lat, lon} from override or parent site.
UI:
- form.html.heex: cnHeat-style grouped form (Identity / Location /
Mounting / RF / Coverage extent), foliage tuning range slider,
computed EIRP badge in the header.
- show.html.heex: Leaflet map area with L.imageOverlay heatmap,
opacity slider, dBm color legend, antenna marker with directional
wedge for azimuth.
- assets/js/app.ts: CoverageMap hook registered.
Tests: 24 new (propagation reference values, profile sampling,
worker end-to-end with stubbed terrain that writes real GeoTIFF + PNG,
org-mismatch job-cancel guard). Full suite: 10815 tests.
29 lines
1 KiB
Elixir
29 lines
1 KiB
Elixir
defmodule Towerops.Test.StubTerrain do
|
|
@moduledoc """
|
|
In-memory stand-in for `Towerops.Lidar` used by coverage worker tests.
|
|
|
|
Tests configure `:towerops, :coverage_terrain_module` to this module
|
|
and put a precomputed grid into the process dictionary; the worker's
|
|
`get_elevation_grid/2` call returns that grid verbatim.
|
|
|
|
Returning the grid via the process dictionary means each test owns
|
|
its own terrain without setting up a global agent.
|
|
"""
|
|
|
|
@doc "Sets the grid that subsequent calls will return."
|
|
@spec put_grid(map() | {:error, term()}) :: :ok
|
|
def put_grid(result) do
|
|
Process.put(__MODULE__, result)
|
|
:ok
|
|
end
|
|
|
|
@doc "Mirrors the relevant subset of `Towerops.Lidar.get_elevation_grid/2`."
|
|
@spec get_elevation_grid(tuple(), number()) :: {:ok, map()} | {:error, term()}
|
|
def get_elevation_grid(_bbox, _cell_size_deg) do
|
|
case Process.get(__MODULE__) do
|
|
nil -> {:error, :no_stub_grid_configured}
|
|
{:error, _} = err -> err
|
|
grid when is_map(grid) -> {:ok, grid}
|
|
end
|
|
end
|
|
end
|