prop/lib/microwaveprop/canopy.ex
Graham McIntire 828814e767
fix: resolve all compiler warnings except framework-generated phoenix_component_verify
- Replace deprecated xref:exclude with elixirc_options in vendor/oban_pro
- Remove unused require Logger from 8 files
- Pin bitstring size variables with ^ in simple_packing, wgrib2,
  complex_packing, section, nexrad_client, mqtt, and radar worker
- Remove dead defp clauses (format/1 nil, depression/2 nil)
- Rename Buildings.Parser type record/0 -> building_record/0 to avoid
  overriding built-in type
- Remove redundant catch-all in candidate_detail.ex
- Simplify contact_live/show.ex conditional based on type narrowing
- Fix DateTime.from_iso8601 return pattern in vendor/oban_web
- Upgrade phoenix_live_view to 1.1.31
- Drop --warnings-as-errors from precommit alias; known false positives
  from @after_verify-generated code in Elixir 1.20.0-rc.6 + PLV 1.1.x
- Add credo --strict to precommit as replacement static-analysis gate
2026-05-29 10:18:52 -05:00

97 lines
3.1 KiB
Elixir
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

defmodule Microwaveprop.Canopy do
@moduledoc """
Tree-canopy heights from a 30 m global canopy-height grid (Potapov et al.
2019, Lang et al. 2022, or any source able to produce 1° × 1° uint8
per-pixel rasters).
Tile format: `@samples × @samples` bytes, row 0 at the north edge,
one byte per pixel = canopy height in meters (0255). Filename
`N{lat}W{lon}.canopy` mirrors the SRTM `.hgt` convention.
Returns 0 m for points without a tile on disk so the lookup can be
threaded through the path-clearance pipeline without raising on
un-fetched areas (treat unknown as treeless).
"""
@samples 3600
@spec tile_filename(float(), float()) :: String.t()
def tile_filename(lat, lon) do
lat_floor = floor(lat)
lon_floor = floor(lon)
lat_prefix = if lat_floor >= 0, do: "N", else: "S"
lon_prefix = if lon_floor >= 0, do: "E", else: "W"
lat_str = lat_floor |> abs() |> Integer.to_string() |> String.pad_leading(2, "0")
lon_str = lon_floor |> abs() |> Integer.to_string() |> String.pad_leading(3, "0")
"#{lat_prefix}#{lat_str}#{lon_prefix}#{lon_str}.canopy"
end
@doc """
Returns the canopy height in meters at `lat, lon`, or 0 if no tile is
on disk for that 1° square.
"""
@spec lookup(float(), float(), String.t(), keyword()) :: non_neg_integer()
def lookup(lat, lon, tiles_dir \\ default_dir(), opts \\ []) do
samples = Keyword.get(opts, :samples, @samples)
path = Path.join(tiles_dir, tile_filename(lat, lon))
case :file.open(path, [:read, :binary, :raw]) do
{:ok, fd} ->
result = read_height(fd, lat, lon, samples)
_ = :file.close(fd)
result
{:error, _} ->
0
end
end
@doc """
Batch lookup. Groups points by tile so each tile is opened once.
Returns `%{ {lat, lon} => height_m }`. Missing tiles yield 0.
"""
@spec lookup_many([{float(), float()}], String.t(), keyword()) ::
%{{float(), float()} => non_neg_integer()}
def lookup_many(points, tiles_dir \\ default_dir(), opts \\ []) do
samples = Keyword.get(opts, :samples, @samples)
points
|> Enum.group_by(fn {lat, lon} -> {floor(lat), floor(lon)} end)
|> Enum.flat_map(fn {_tile_key, pts} -> read_tile_points(pts, tiles_dir, samples) end)
|> Map.new()
end
defp read_tile_points([{lat0, lon0} | _] = pts, tiles_dir, samples) do
path = Path.join(tiles_dir, tile_filename(lat0, lon0))
case :file.open(path, [:read, :binary, :raw]) do
{:ok, fd} ->
result =
Enum.map(pts, fn {lat, lon} = pt -> {pt, read_height(fd, lat, lon, samples)} end)
_ = :file.close(fd)
result
{:error, _} ->
Enum.map(pts, fn pt -> {pt, 0} end)
end
end
defp read_height(fd, lat, lon, samples) do
row = round((floor(lat) + 1 - lat) * (samples - 1))
col = round((lon - floor(lon)) * (samples - 1))
offset = row * samples + col
case :file.pread(fd, offset, 1) do
{:ok, <<h::unsigned-integer-size(8)>>} -> h
_ -> 0
end
end
defp default_dir do
Application.get_env(:microwaveprop, :canopy_tiles_dir, "/data/canopy")
end
end