full frontend refactor
This commit is contained in:
@@ -266,4 +266,70 @@ defmodule MoundHunters.MoundParser do
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts Web Mercator coordinates (EPSG:3857) to WGS84 lat/lng (EPSG:4326).
|
||||
|
||||
Web Mercator uses meters from origin, this converts back to decimal degrees.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> MoundHunters.MoundParser.web_mercator_to_latlon(0, 0)
|
||||
{0.0, 0.0}
|
||||
|
||||
iex> MoundHunters.MoundParser.web_mercator_to_latlon(1113194.91, 5009377.09)
|
||||
{10.0, 40.0}
|
||||
"""
|
||||
@spec web_mercator_to_latlon(float(), float()) :: {lat :: float(), lon :: float()}
|
||||
def web_mercator_to_latlon(x, y) do
|
||||
# Web Mercator origin offset (meters)
|
||||
origin_shift = :math.pi() * 6_378_137.0
|
||||
|
||||
# Convert x to longitude
|
||||
lon = x / origin_shift * 180.0
|
||||
|
||||
# Convert y to latitude
|
||||
lat = y / origin_shift * 180.0
|
||||
|
||||
lat =
|
||||
180.0 / :math.pi() *
|
||||
(2.0 * :math.atan(:math.exp(lat * :math.pi() / 180.0)) - :math.pi() / 2.0)
|
||||
|
||||
{lat, lon}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts a bounding box from Web Mercator to lat/lng.
|
||||
|
||||
Returns a map with min/max lat/lng values.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> MoundHunters.MoundParser.bounds_web_mercator_to_latlon(
|
||||
...> %{min_x: -9_000_000, max_x: -8_900_000, min_y: 4_900_000, max_y: 5_000_000}
|
||||
...> )
|
||||
%{min_lat: ..., max_lat: ..., min_lng: ..., max_lng: ...}
|
||||
"""
|
||||
@spec bounds_web_mercator_to_latlon(%{
|
||||
min_x: float(),
|
||||
max_x: float(),
|
||||
min_y: float(),
|
||||
max_y: float()
|
||||
}) :: %{
|
||||
min_lat: float(),
|
||||
max_lat: float(),
|
||||
min_lng: float(),
|
||||
max_lng: float()
|
||||
}
|
||||
def bounds_web_mercator_to_latlon(%{min_x: min_x, max_x: max_x, min_y: min_y, max_y: max_y}) do
|
||||
{min_lat, min_lng} = web_mercator_to_latlon(min_x, min_y)
|
||||
{max_lat, max_lng} = web_mercator_to_latlon(max_x, max_y)
|
||||
|
||||
%{
|
||||
min_lat: min_lat,
|
||||
max_lat: max_lat,
|
||||
min_lng: min_lng,
|
||||
max_lng: max_lng
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -185,6 +185,8 @@ defmodule MoundHunters.Repo do
|
||||
def update_tile_availability do
|
||||
tile_dir = Application.get_env(:mound_hunters, :tile_output_dir)
|
||||
mound_dir = Path.join(tile_dir, "MOUND")
|
||||
png_dir = Path.join(tile_dir, "PNG")
|
||||
jpg_dir = Path.join(tile_dir, "JPG")
|
||||
|
||||
Logger.info("Scanning tile availability in: #{mound_dir}")
|
||||
|
||||
@@ -199,8 +201,8 @@ defmodule MoundHunters.Repo do
|
||||
tile = tile_to_map(tile_record)
|
||||
tile_id = tile.id
|
||||
|
||||
jpg_path = Path.join(mound_dir, "#{tile_id}.jpg")
|
||||
png_path = Path.join(mound_dir, "#{tile_id}.png")
|
||||
jpg_path = Path.join(jpg_dir, "#{tile_id}.jpg")
|
||||
png_path = Path.join(png_dir, "#{tile_id}.png")
|
||||
|
||||
jpg_available = File.exists?(jpg_path)
|
||||
png_available = File.exists?(png_path)
|
||||
@@ -263,10 +265,13 @@ defmodule MoundHunters.Repo do
|
||||
case MoundHunters.MoundParser.parse_header(file_path) do
|
||||
{:ok, header} ->
|
||||
# Convert Web Mercator coordinates to lat/lng
|
||||
min_lng = header.min_x
|
||||
max_lng = header.max_x
|
||||
min_lat = header.min_y
|
||||
max_lat = header.max_y
|
||||
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
|
||||
})
|
||||
|
||||
# Check for jpg and png availability
|
||||
tile_dir = Application.get_env(:mound_hunters, :tile_output_dir)
|
||||
@@ -276,10 +281,10 @@ defmodule MoundHunters.Repo do
|
||||
|
||||
attrs = %{
|
||||
id: tile_id,
|
||||
min_lat: min_lat,
|
||||
max_lat: max_lat,
|
||||
min_lng: min_lng,
|
||||
max_lng: max_lng,
|
||||
min_lat: bounds.min_lat,
|
||||
max_lat: bounds.max_lat,
|
||||
min_lng: bounds.min_lng,
|
||||
max_lng: bounds.max_lng,
|
||||
status: :ready,
|
||||
error_message: nil,
|
||||
jpg_available: jpg_available,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -60,10 +60,9 @@ defmodule MoundHuntersWeb.TileController do
|
||||
|
||||
@doc """
|
||||
Serve a tile file.
|
||||
GET /tiles/MOUND/:tile_id.mound
|
||||
GET /tiles/JPG/:tile_id.jpg
|
||||
GET /tiles/PNG/:tile_id.jpng
|
||||
GET /tiles/JSON/:tile_id.json
|
||||
GET /tiles/mound/:tile_id.mound
|
||||
GET /tiles/jpg/:tile_id.jpg
|
||||
GET /tiles/png/:tile_id.jpng
|
||||
"""
|
||||
def serve(conn) do
|
||||
raw_tile_id = conn.path_params["tile_id"]
|
||||
@@ -81,10 +80,9 @@ defmodule MoundHuntersWeb.TileController do
|
||||
|
||||
file_path =
|
||||
case format do
|
||||
"MOUND" -> Path.join(tile_dir, "MOUND/#{tile_id}.mound")
|
||||
"JSON" -> Path.join(tile_dir, "JSON/#{tile_id}.json")
|
||||
"JPG" -> Path.join(tile_dir, "JPG/#{tile_id}.jpg")
|
||||
"PNG" -> Path.join(tile_dir, "PNG/#{tile_id}.png")
|
||||
"mound" -> Path.join(tile_dir, "MOUND/#{tile_id}.mound")
|
||||
"jpg" -> Path.join(tile_dir, "JPG/#{tile_id}.jpg")
|
||||
"png" -> Path.join(tile_dir, "PNG/#{tile_id}.png")
|
||||
end
|
||||
|
||||
Logger.info("Tile request started for #{file_path}")
|
||||
|
||||
Reference in New Issue
Block a user