- 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
97 lines
3.1 KiB
Elixir
97 lines
3.1 KiB
Elixir
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 (0–255). 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
|