create backend
This commit is contained in:
196
lib/mound_hunters/ohio_lidar.ex
Normal file
196
lib/mound_hunters/ohio_lidar.ex
Normal file
@@ -0,0 +1,196 @@
|
||||
defmodule MoundHunters.OhioLidar do
|
||||
@moduledoc """
|
||||
Functions for querying Ohio's ArcGIS tile service and coordinate conversions.
|
||||
"""
|
||||
require Logger
|
||||
|
||||
# Ohio ArcGIS tile service
|
||||
@tile_service_url "https://maps.ohio.gov/arcgis/rest/services/OGRIP/3DepTiles/MapServer/0/query"
|
||||
|
||||
# OGRIP download URL template
|
||||
@download_url_template "https://gis1.oit.ohio.gov/ZIPARCHIVES_III/ELEVATION/3DEP/LIDAR/{county}/{tile_name}.zip"
|
||||
|
||||
@doc """
|
||||
Convert lon/lat (WGS84) to Web Mercator (EPSG:3857).
|
||||
|
||||
Formula from: https://en.wikipedia.org/wiki/Web_Mercator_projection
|
||||
"""
|
||||
def lonlat_to_webmercator(lon, lat) do
|
||||
# Earth radius in meters
|
||||
r = 6378137.0
|
||||
|
||||
# Convert to radians
|
||||
lon_rad = lon * :math.pi() / 180.0
|
||||
lat_rad = lat * :math.pi() / 180.0
|
||||
|
||||
# Web Mercator formulas
|
||||
x = r * lon_rad
|
||||
y = r * :math.log(:math.tan(:math.pi() / 4.0 + lat_rad / 2.0))
|
||||
|
||||
{x, y}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Query Ohio ArcGIS service for tile information at given coordinates.
|
||||
|
||||
Returns {:ok, tile_info} or {:error, reason}
|
||||
|
||||
tile_info contains: %{
|
||||
tile_name: "BS19820747",
|
||||
county: "LIC",
|
||||
year: "2020",
|
||||
block: "4",
|
||||
note: "..."
|
||||
}
|
||||
"""
|
||||
def query_tile_info(lon, lat) do
|
||||
{x, y} = lonlat_to_webmercator(lon, lat)
|
||||
|
||||
geometry =
|
||||
Jason.encode!(%{
|
||||
x: x,
|
||||
y: y,
|
||||
spatialReference: %{wkid: 3857}
|
||||
})
|
||||
|
||||
params = %{
|
||||
"f" => "json",
|
||||
"returnGeometry" => "false",
|
||||
"spatialRel" => "esriSpatialRelIntersects",
|
||||
"geometry" => geometry,
|
||||
"geometryType" => "esriGeometryPoint",
|
||||
"inSR" => "3857",
|
||||
"outFields" => "*",
|
||||
"outSR" => "3857"
|
||||
}
|
||||
|
||||
Logger.debug("Querying ArcGIS for tile at (#{lon}, #{lat}) -> WebMercator (#{x}, #{y})")
|
||||
|
||||
case HTTPoison.get(@tile_service_url, [], params: params, timeout: 10_000) do
|
||||
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
|
||||
parse_tile_response(body)
|
||||
|
||||
{:ok, %HTTPoison.Response{status_code: status}} ->
|
||||
Logger.error("ArcGIS returned status #{status}")
|
||||
{:error, "ArcGIS service returned status #{status}"}
|
||||
|
||||
{:error, %HTTPoison.Error{reason: reason}} ->
|
||||
Logger.error("Failed to query ArcGIS: #{inspect(reason)}")
|
||||
{:error, "Network error: #{inspect(reason)}"}
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_tile_response(body) do
|
||||
case Jason.decode(body) do
|
||||
{:ok, %{"features" => []}} ->
|
||||
{:error, :no_tile_found}
|
||||
|
||||
{:ok, %{"features" => [feature | _]}} ->
|
||||
attrs = feature["attributes"]
|
||||
|
||||
tile_info = %{
|
||||
tile_name: attrs["TileName"],
|
||||
county: attrs["County"],
|
||||
year: attrs["Year"],
|
||||
block: attrs["Block"],
|
||||
note: attrs["note"]
|
||||
}
|
||||
|
||||
{:ok, tile_info}
|
||||
|
||||
{:ok, _} ->
|
||||
{:error, :invalid_response}
|
||||
|
||||
{:error, _} ->
|
||||
{:error, :json_parse_error}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get the download URL for a tile.
|
||||
"""
|
||||
def get_download_url(tile_name, county) do
|
||||
@download_url_template
|
||||
|> String.replace("{county}", county)
|
||||
|> String.replace("{tile_name}", tile_name)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Download a tile ZIP file from OGRIP.
|
||||
|
||||
Returns {:ok, file_path} or {:error, reason}
|
||||
"""
|
||||
def download_tile(tile_name, county, output_path) do
|
||||
url = get_download_url(tile_name, county)
|
||||
Logger.info("Downloading #{tile_name} from #{url}")
|
||||
|
||||
# Ensure output directory exists
|
||||
output_path
|
||||
|> Path.dirname()
|
||||
|> File.mkdir_p!()
|
||||
|
||||
case HTTPoison.get(url, [], timeout: 60_000, recv_timeout: 60_000, stream_to: self()) do
|
||||
{:ok, %HTTPoison.AsyncResponse{id: id}} ->
|
||||
receive_download(id, output_path, 0)
|
||||
|
||||
{:error, %HTTPoison.Error{reason: reason}} ->
|
||||
Logger.error("Failed to start download: #{inspect(reason)}")
|
||||
{:error, "Download failed: #{inspect(reason)}"}
|
||||
end
|
||||
end
|
||||
|
||||
defp receive_download(id, output_path, bytes_received) do
|
||||
receive do
|
||||
%HTTPoison.AsyncStatus{id: ^id, code: 200} ->
|
||||
Logger.debug("Download started, status 200")
|
||||
receive_download(id, output_path, bytes_received)
|
||||
|
||||
%HTTPoison.AsyncStatus{id: ^id, code: status} ->
|
||||
Logger.error("Download failed with status #{status}")
|
||||
{:error, "HTTP status #{status}"}
|
||||
|
||||
%HTTPoison.AsyncHeaders{id: ^id} ->
|
||||
# Start writing to file
|
||||
File.open(output_path, [:write, :binary], fn file ->
|
||||
receive_download_chunks(id, file, bytes_received)
|
||||
end)
|
||||
|
||||
%HTTPoison.AsyncEnd{id: ^id} ->
|
||||
Logger.error("Download ended prematurely")
|
||||
{:error, :unexpected_end}
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.error("Download error: #{inspect(reason)}")
|
||||
{:error, reason}
|
||||
after
|
||||
70_000 ->
|
||||
{:error, :timeout}
|
||||
end
|
||||
end
|
||||
|
||||
defp receive_download_chunks(id, file, bytes_received) do
|
||||
receive do
|
||||
%HTTPoison.AsyncChunk{id: ^id, chunk: chunk} ->
|
||||
IO.binwrite(file, chunk)
|
||||
new_bytes = bytes_received + byte_size(chunk)
|
||||
|
||||
# Log progress every 10MB
|
||||
if div(new_bytes, 10_485_760) > div(bytes_received, 10_485_760) do
|
||||
Logger.debug("Downloaded #{div(new_bytes, 1_048_576)} MB")
|
||||
end
|
||||
|
||||
receive_download_chunks(id, file, new_bytes)
|
||||
|
||||
%HTTPoison.AsyncEnd{id: ^id} ->
|
||||
size_mb = bytes_received / 1_048_576
|
||||
Logger.info("Download complete: #{Float.round(size_mb, 2)} MB")
|
||||
{:ok, bytes_received}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
after
|
||||
70_000 ->
|
||||
{:error, :timeout}
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user