diff --git a/assets/js/app.js b/assets/js/app.js index 2e0c6a7..a318235 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -29,10 +29,34 @@ let Hooks = {}; Hooks.APRSMap = { mounted() { console.log("APRSMap hook mounted"); - // Initialize the map centered on the United States - const map = L.map(this.el).setView([39.8283, -98.5795], 5); + // Get initial center and zoom from server-provided data attributes + const initialCenter = JSON.parse(this.el.dataset.center); + const initialZoom = parseInt(this.el.dataset.zoom); + + // Initialize the map with the server-provided location + const map = L.map(this.el).setView([initialCenter.lat, initialCenter.lng], initialZoom); console.log("Map initialized:", map); + // Handle geolocation requests from server + this.handleEvent("request_geolocation", () => { + if ("geolocation" in navigator) { + navigator.geolocation.getCurrentPosition( + (position) => { + const { latitude, longitude } = position.coords; + console.log("User location:", latitude, longitude); + map.setView([latitude, longitude], 12); + // Notify server of new location + this.pushEvent("set_location", { lat: latitude, lng: longitude }); + }, + (error) => { + console.warn("Geolocation error:", error.message); + }, + ); + } else { + console.warn("Geolocation not available in this browser"); + } + }); + // Add OpenStreetMap tile layer L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { attribution: @@ -107,6 +131,24 @@ Hooks.APRSMap = { this.clearAllMarkers(); }); + // Handle geolocation button clicks + this.handleEvent("request_geolocation", () => { + if ("geolocation" in navigator) { + navigator.geolocation.getCurrentPosition( + (position) => { + const { latitude, longitude } = position.coords; + // Send location back to server + this.pushEvent("set_location", { lat: latitude, lng: longitude }); + }, + (error) => { + console.warn("Geolocation error:", error.message); + }, + ); + } else { + console.warn("Geolocation not available in this browser"); + } + }); + // Update bounds when map moves or zooms map.on("moveend", () => { this.sendBoundsToServer(); diff --git a/lib/aprs_web/live/map_live/index.ex b/lib/aprs_web/live/map_live/index.ex index 8abb8c2..6dc0112 100644 --- a/lib/aprs_web/live/map_live/index.ex +++ b/lib/aprs_web/live/map_live/index.ex @@ -8,27 +8,45 @@ defmodule AprsWeb.MapLive.Index do alias AprsWeb.Endpoint alias Parser.Types.MicE + @default_center %{lat: 39.8283, lng: -98.5795} + @default_zoom 5 + @ip_api_url "http://ip-api.com/json/" + @finch_name Aprs.Finch + @impl true def mount(_params, _session, socket) do + socket = + assign(socket, + packets: [], + packet_count: 0, + page_title: "APRS Map", + # Track visible packets by callsign + visible_packets: %{}, + # Default bounds for USA + map_bounds: %{ + north: 49.0, + south: 24.0, + east: -66.0, + west: -125.0 + }, + map_center: @default_center, + map_zoom: @default_zoom + ) + if connected?(socket) do Endpoint.subscribe("aprs_messages") + # Get IP-based location on initial load + ip = + case socket.private[:connect_info][:peer_data][:address] do + {a, b, c, d} -> "#{a}.#{b}.#{c}.#{d}" + {a, b, c, d, e, f, g, h} -> "#{a}:#{b}:#{c}:#{d}:#{e}:#{f}:#{g}:#{h}" + _ -> nil + end + + if ip, do: Task.async(fn -> get_ip_location(ip) end) end - {:ok, - assign(socket, - packets: [], - packet_count: 0, - page_title: "APRS Map", - # Track visible packets by callsign - visible_packets: %{}, - # Default bounds for USA - map_bounds: %{ - north: 49.0, - south: 24.0, - east: -66.0, - west: -125.0 - } - )} + {:ok, socket} end @impl true @@ -62,50 +80,68 @@ defmodule AprsWeb.MapLive.Index do end @impl true - def handle_info(%{event: "packet", payload: payload}, socket) do - # Sanitize the packet to prevent encoding errors - sanitized_packet = EncodingUtils.sanitize_packet(payload) + def handle_event("locate_me", _params, socket) do + # Send JavaScript command to request browser geolocation + {:noreply, push_event(socket, "request_geolocation", %{})} + end - # Log packet type for debugging - IO.inspect(sanitized_packet.data_type, label: "Packet type") + @impl true + def handle_event("set_location", %{"lat" => lat, "lng" => lng}, socket) do + # Update map center and zoom when location is received + {:noreply, assign(socket, map_center: %{lat: lat, lng: lng}, map_zoom: 12)} + end - if sanitized_packet.data_extended do - # Check if data_extended is a struct before accessing __struct__ - case sanitized_packet.data_extended do - %{__struct__: module} -> IO.inspect(module, label: "Data extended type") - _ -> IO.inspect("Plain map", label: "Data extended type") - end - end + @impl true + def handle_info(msg, socket) do + case msg do + {:ip_location, %{lat: lat, lng: lng}} -> + {:noreply, assign(socket, map_center: %{lat: lat, lng: lng}, map_zoom: 12)} - # Only process packets with position data that are within current map bounds - if has_position_data?(sanitized_packet) && within_bounds?(sanitized_packet, socket.assigns.map_bounds) do - # Convert to a simple map structure for JSON encoding - packet_data = build_packet_data(sanitized_packet) + %{event: "packet", payload: payload} -> + # Sanitize the packet to prevent encoding errors + sanitized_packet = EncodingUtils.sanitize_packet(payload) - # Only push if we have valid packet data - if packet_data do - # Generate a unique key for this packet - callsign_key = - "#{sanitized_packet.base_callsign}#{if sanitized_packet.ssid, do: "-#{sanitized_packet.ssid}", else: ""}" + # Log packet type for debugging + IO.inspect(sanitized_packet.data_type, label: "Packet type") - # Update visible packets tracking - visible_packets = Map.put(socket.assigns.visible_packets, callsign_key, sanitized_packet) - packet_count = map_size(visible_packets) + if sanitized_packet.data_extended do + # Check if data_extended is a struct before accessing __struct__ + case sanitized_packet.data_extended do + %{__struct__: module} -> IO.inspect(module, label: "Data extended type") + _ -> IO.inspect("Plain map", label: "Data extended type") + end + end - # Push the packet to the client-side JavaScript - socket = - socket - |> push_event("new_packet", packet_data) - |> assign(visible_packets: visible_packets, packet_count: packet_count) + # Only process packets with position data that are within current map bounds + if has_position_data?(sanitized_packet) && within_bounds?(sanitized_packet, socket.assigns.map_bounds) do + # Convert to a simple map structure for JSON encoding + packet_data = build_packet_data(sanitized_packet) - {:noreply, socket} - else - # Invalid packet data, skip it - {:noreply, socket} - end - else - # Ignore packets without position data or outside bounds - {:noreply, socket} + # Only push if we have valid packet data + if packet_data do + # Generate a unique key for this packet + callsign_key = + "#{sanitized_packet.base_callsign}#{if sanitized_packet.ssid, do: "-#{sanitized_packet.ssid}", else: ""}" + + # Update visible packets tracking + visible_packets = Map.put(socket.assigns.visible_packets, callsign_key, sanitized_packet) + packet_count = map_size(visible_packets) + + # Push the packet to the client-side JavaScript + socket = + socket + |> push_event("new_packet", packet_data) + |> assign(visible_packets: visible_packets, packet_count: packet_count) + + {:noreply, socket} + else + # Invalid packet data, skip it + {:noreply, socket} + end + else + # Ignore packets without position data or outside bounds + {:noreply, socket} + end end end @@ -158,6 +194,22 @@ defmodule AprsWeb.MapLive.Index do z-index: 1000; } + .locate-button { + position: absolute; + left: 10px; + top: 80px; + z-index: 1000; + background: white; + border: 2px solid rgba(0,0,0,0.2); + border-radius: 4px; + padding: 5px; + cursor: pointer; + } + + .locate-button:hover { + background: #f4f4f4; + } + .packet-counter { font-size: 14px; font-weight: 600; @@ -170,13 +222,40 @@ defmodule AprsWeb.MapLive.Index do } -
+