full frontend refactor

This commit is contained in:
2026-01-25 10:26:23 +01:00
parent 317ee96ba3
commit 559a4c3e9f
18 changed files with 2803 additions and 1224 deletions

View File

@@ -1,7 +1,7 @@
defmodule MoundHunters.TileProcessor do
@moduledoc """
GenServer that processes tile requests: lookup tile IDs and download/convert tiles.
Processing pipeline:
1. :looking_up - Query ArcGIS for tile metadata
2. :downloading - Download ZIP from OGRIP
@@ -76,10 +76,12 @@ defmodule MoundHunters.TileProcessor do
# Ensure temp directory exists
temp_dir = Application.get_env(:mound_hunters, :tile_temp_dir)
File.mkdir_p!(temp_dir)
root_output_dir = Application.get_env(:mound_hunters, :tile_output_dir)
mound_dir = Path.join(root_output_dir, "MOUND")
state = %{
las2mound_script: Application.get_env(:mound_hunters, :las2mound_script_path),
tile_output_dir: Application.get_env(:mound_hunters, :tile_output_dir),
tile_output_dir: mound_dir,
tile_temp_dir: temp_dir,
processing_queue: :queue.new(),
current_job: nil
@@ -113,35 +115,38 @@ defmodule MoundHunters.TileProcessor do
def handle_info({:lookup_tile, lookup_id, lat, lng}, state) do
Logger.info("Looking up tile for coordinates (#{lat}, #{lng})")
case OhioLidar.query_tile_info(lng, lat) do
{:ok, tile_info} ->
tile_name = tile_info.tile_name
Logger.info("Found tile: #{tile_name} in #{tile_info.county} county")
# First, check Mnesia to see if we already have a tile at these coordinates
case MoundHunters.Repo.get_tile_at_coords(lat, lng) do
{:ok, tile} ->
# Found in Mnesia - use cached tile_id
tile_name = tile.id
Logger.info("Found tile #{tile_name} in Mnesia (cached), skipping ArcGIS query")
# Update lookup table with success
:ets.insert(:tile_lookups, {lookup_id, {:ok, tile_name}})
# Check if tile already exists
# Check if tile file still exists
output_file = Path.join(state.tile_output_dir, "#{tile_name}.mound")
if File.exists?(output_file) do
Logger.info("Tile #{tile_name} already processed, marking as done")
:ets.insert(:tile_processing, {tile_name, :done, %{tile_info: tile_info}})
else
# Queue for processing
state = queue_tile_for_processing(state, tile_name, tile_info)
:ets.insert(:tile_processing, {tile_name, :done, %{tile_info: tile}})
{:noreply, state}
else
# Tile in DB but file missing - this shouldn't happen, but handle it
Logger.warning("Tile #{tile_name} in Mnesia but file missing, re-querying ArcGIS")
query_arcgis_and_process(lookup_id, lat, lng, state)
end
{:error, :no_tile_found} ->
Logger.warning("No tile found for coordinates (#{lat}, #{lng})")
:ets.insert(:tile_lookups, {lookup_id, {:error, "No tile found at coordinates"}})
{:noreply, state}
{:error, :not_found} ->
# Not in Mnesia - query ArcGIS
Logger.debug("Tile not in Mnesia, querying ArcGIS")
query_arcgis_and_process(lookup_id, lat, lng, state)
{:error, reason} ->
Logger.error("Failed to lookup tile: #{inspect(reason)}")
:ets.insert(:tile_lookups, {lookup_id, {:error, "Lookup failed: #{inspect(reason)}"}})
{:noreply, state}
Logger.error("Failed to query Mnesia: #{inspect(reason)}")
# Fall back to ArcGIS query
query_arcgis_and_process(lookup_id, lat, lng, state)
end
end
@@ -162,11 +167,15 @@ defmodule MoundHunters.TileProcessor do
{:error, reason} ->
Logger.error("Failed to download tile #{tile_name}: #{inspect(reason)}")
:ets.insert(:tile_processing, {tile_name, {:error, "Download failed: #{inspect(reason)}"}, %{}})
:ets.insert(
:tile_processing,
{tile_name, {:error, "Download failed: #{inspect(reason)}"}, %{}}
)
# Clean up
File.rm(temp_zip)
# Process next in queue
state = process_next_in_queue(state)
{:noreply, state}
@@ -192,23 +201,27 @@ defmodule MoundHunters.TileProcessor do
{:error, reason} ->
Logger.error("Failed to find LAS file in #{extract_dir}: #{reason}")
:ets.insert(:tile_processing, {tile_name, {:error, "No LAS file found"}, %{}})
# Clean up
File.rm_rf!(extract_dir)
File.rm(zip_path)
state = process_next_in_queue(state)
{:noreply, state}
end
{:error, reason} ->
Logger.error("Failed to extract #{zip_path}: #{inspect(reason)}")
:ets.insert(:tile_processing, {tile_name, {:error, "Extraction failed: #{inspect(reason)}"}, %{}})
:ets.insert(
:tile_processing,
{tile_name, {:error, "Extraction failed: #{inspect(reason)}"}, %{}}
)
# Clean up
File.rm_rf!(extract_dir)
File.rm(zip_path)
state = process_next_in_queue(state)
{:noreply, state}
end
@@ -225,28 +238,69 @@ defmodule MoundHunters.TileProcessor do
case run_las2mound(state.las2mound_script, las_file, output_file) do
:ok ->
Logger.info("Successfully converted #{tile_name}")
:ets.insert(:tile_processing, {tile_name, :done, %{tile_info: tile_info}})
# Update Mnesia
MoundHunters.Repo.upsert_tile(%{
id: tile_name,
status: :ready,
min_lat: nil, # TODO: Extract from tile_info or LAS bounds
max_lat: nil,
min_lng: nil,
max_lng: nil
})
# Parse header to extract bounds
case MoundHunters.MoundParser.parse_header(output_file) do
{:ok, header} ->
# Convert Web Mercator coordinates to lat/lng
bounds =
MoundHunters.MoundParser.bounds_web_mercator_to_latlon(%{
min_x: header.min_x,
max_x: header.max_x,
min_y: header.min_y,
max_y: header.max_y
})
# Clean up temp files
File.rm_rf!(temp_dir)
File.rm(Path.join(state.tile_temp_dir, "#{tile_name}.zip"))
Logger.debug(
"Tile #{tile_name} bounds: lat [#{bounds.min_lat}, #{bounds.max_lat}], " <>
"lng [#{bounds.min_lng}, #{bounds.max_lng}]"
)
state = process_next_in_queue(state)
{:noreply, state}
:ets.insert(
:tile_processing,
{tile_name, :done, %{tile_info: tile_info, header: header}}
)
# Update Mnesia with extracted bounds
MoundHunters.Repo.upsert_tile(%{
id: tile_name,
status: :ready,
min_lat: bounds.min_lat,
max_lat: bounds.max_lat,
min_lng: bounds.min_lng,
max_lng: bounds.max_lng
})
# Clean up temp files
File.rm_rf!(temp_dir)
File.rm(Path.join(state.tile_temp_dir, "#{tile_name}.zip"))
state = process_next_in_queue(state)
{:noreply, state}
{:error, reason} ->
Logger.error("Failed to parse header for #{tile_name}: #{inspect(reason)}")
:ets.insert(
:tile_processing,
{tile_name, {:error, "Header parsing failed: #{inspect(reason)}"}, %{}}
)
# Keep the .mound file for debugging, but clean up temp files
File.rm_rf!(temp_dir)
File.rm(Path.join(state.tile_temp_dir, "#{tile_name}.zip"))
state = process_next_in_queue(state)
{:noreply, state}
end
{:error, reason} ->
Logger.error("Failed to convert #{tile_name}: #{inspect(reason)}")
:ets.insert(:tile_processing, {tile_name, {:error, "Conversion failed: #{inspect(reason)}"}, %{}})
:ets.insert(
:tile_processing,
{tile_name, {:error, "Conversion failed: #{inspect(reason)}"}, %{}}
)
# Clean up
File.rm_rf!(temp_dir)
@@ -259,6 +313,40 @@ defmodule MoundHunters.TileProcessor do
# Helper functions
defp query_arcgis_and_process(lookup_id, lat, lng, state) do
case OhioLidar.query_tile_info(lng, lat) do
{:ok, tile_info} ->
tile_name = tile_info.tile_name
Logger.info("Found tile: #{tile_name} in #{tile_info.county} county")
# Update lookup table with success
:ets.insert(:tile_lookups, {lookup_id, {:ok, tile_name}})
# Check if tile already exists
output_file = Path.join(state.tile_output_dir, "#{tile_name}.mound")
if File.exists?(output_file) do
Logger.info("Tile #{tile_name} already processed, marking as done")
:ets.insert(:tile_processing, {tile_name, :done, %{tile_info: tile_info}})
{:noreply, state}
else
# Queue for processing
state = queue_tile_for_processing(state, tile_name, tile_info)
{:noreply, state}
end
{:error, :no_tile_found} ->
Logger.warning("No tile found for coordinates (#{lat}, #{lng})")
:ets.insert(:tile_lookups, {lookup_id, {:error, "No tile found at coordinates"}})
{:noreply, state}
{:error, reason} ->
Logger.error("Failed to lookup tile: #{inspect(reason)}")
:ets.insert(:tile_lookups, {lookup_id, {:error, "Lookup failed: #{inspect(reason)}"}})
{:noreply, state}
end
end
defp queue_tile_for_processing(state, tile_name, tile_info) do
# Check if already in queue or processing
case :ets.lookup(:tile_processing, tile_name) do