Type spec fixes: - duct_usable_* return boolean→float (delegate to duct_usable_for_band) - sanitize/1 spec includes :unicode error tuples - match_delete/1 broadened from :ets.match_spec() - telemetry_event local type replaces :telemetry.event/0 - wgrib2 parse_lon_val_segment corrected to tuple spec - preloaded Ecto assoc types in get_mission/get_contact! specs Unmatched returns: - _ = prefix on Task.start, Oban.insert, Repo.query!, PubSub.subscribe, :ets.new, and if-expression returns across 18 files Pattern match fixes: - markdown: restructure acc!=[] guard as direct pattern match - path_compute/pskr/skewt_location_resolver: remove dead clauses - calibrate.aprs_144: remove unreachable format_float catch-all - unused.ex: suppress MapSet.union no_opaque Also: remove unused unicode_util_compat from mix.lock
79 lines
2.5 KiB
Elixir
79 lines
2.5 KiB
Elixir
defmodule Microwaveprop.Buildings.Parser do
|
|
@moduledoc """
|
|
Streaming parser for Microsoft Building Footprint csv.gz tiles.
|
|
|
|
Each line is a GeoJSON `Feature` whose `geometry.coordinates` is a
|
|
Polygon (or MultiPolygon) and whose `properties.height` is meters
|
|
above ground. We collapse each polygon to a centroid + bounding
|
|
radius — that's all the rover path-clearance scorer needs, and it
|
|
keeps the in-memory footprint to ~32 bytes per building.
|
|
|
|
Polygons with `height = -1.0` (no estimate from the ML model) are
|
|
dropped — including them would just add noise.
|
|
"""
|
|
|
|
@type building_record :: %{
|
|
centroid_lat: float(),
|
|
centroid_lon: float(),
|
|
max_radius_m: float(),
|
|
height_m: float()
|
|
}
|
|
|
|
@doc """
|
|
Returns a `Stream` of `t:building_record/0` for every polygon in `path` whose
|
|
height is known. Lazy; suitable for tiles with millions of rows.
|
|
"""
|
|
@dialyzer {:no_return, parse_tile: 1}
|
|
def parse_tile(path) do
|
|
path
|
|
|> File.stream!([:read, :compressed])
|
|
|> Stream.flat_map(&parse_line/1)
|
|
end
|
|
|
|
defp parse_line(line) do
|
|
case Jason.decode(String.trim(line)) do
|
|
{:ok, %{"properties" => %{"height" => h}, "geometry" => geom}} when h > 0.0 ->
|
|
case ring_from_geometry(geom) do
|
|
[] -> []
|
|
ring -> [build_record(ring, h)]
|
|
end
|
|
|
|
_ ->
|
|
[]
|
|
end
|
|
end
|
|
|
|
defp ring_from_geometry(%{"type" => "Polygon", "coordinates" => [outer | _]}), do: outer
|
|
defp ring_from_geometry(%{"type" => "MultiPolygon", "coordinates" => [[outer | _] | _]}), do: outer
|
|
defp ring_from_geometry(_), do: []
|
|
|
|
defp build_record(ring, height) do
|
|
{sum_lat, sum_lon, n, min_lat, min_lon, max_lat, max_lon} =
|
|
Enum.reduce(ring, {0.0, 0.0, 0, 90.0, 180.0, -90.0, -180.0}, fn
|
|
[lon, lat], {sl, slon, n, mnla, mnlo, mxla, mxlo} ->
|
|
{
|
|
sl + lat,
|
|
slon + lon,
|
|
n + 1,
|
|
min(mnla, lat),
|
|
min(mnlo, lon),
|
|
max(mxla, lat),
|
|
max(mxlo, lon)
|
|
}
|
|
end)
|
|
|
|
centroid_lat = sum_lat / n
|
|
centroid_lon = sum_lon / n
|
|
|
|
# Approximate max radius: half-diagonal of the bbox in meters.
|
|
dlat_m = (max_lat - min_lat) * 111_000.0 / 2.0
|
|
dlon_m = (max_lon - min_lon) * 111_000.0 * :math.cos(centroid_lat * :math.pi() / 180.0) / 2.0
|
|
|
|
%{
|
|
centroid_lat: centroid_lat,
|
|
centroid_lon: centroid_lon,
|
|
max_radius_m: :math.sqrt(dlat_m * dlat_m + dlon_m * dlon_m),
|
|
height_m: height * 1.0
|
|
}
|
|
end
|
|
end
|