map improvements
This commit is contained in:
parent
4a6665ac76
commit
c5b5d0762a
18 changed files with 1072 additions and 269 deletions
|
|
@ -5,5 +5,34 @@
|
|||
/* This file is for your main application CSS */
|
||||
|
||||
#map {
|
||||
height: 500px;
|
||||
height: calc(100vh - 60px); /* Adjust based on header height */
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Full page map for APRS home page */
|
||||
#aprs-map {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Hide header on home page */
|
||||
body.home-page header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Adjust main content area for full page map */
|
||||
body.home-page main {
|
||||
padding: 0;
|
||||
max-width: none;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
body.home-page main > div {
|
||||
max-width: none;
|
||||
height: 100%;
|
||||
}
|
||||
|
|
|
|||
202
assets/js/app.js
202
assets/js/app.js
|
|
@ -22,18 +22,208 @@ import { Socket } from "phoenix";
|
|||
import { LiveSocket } from "phoenix_live_view";
|
||||
import topbar from "../vendor/topbar";
|
||||
|
||||
let csrfToken = document
|
||||
.querySelector("meta[name='csrf-token']")
|
||||
.getAttribute("content");
|
||||
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content");
|
||||
|
||||
// APRS Map Hook
|
||||
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);
|
||||
console.log("Map initialized:", map);
|
||||
|
||||
// Add OpenStreetMap tile layer
|
||||
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
||||
attribution:
|
||||
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors | APRS.me',
|
||||
maxZoom: 19,
|
||||
}).addTo(map);
|
||||
|
||||
// Store markers to avoid duplicates
|
||||
this.markers = new Map();
|
||||
this.packetCount = 0;
|
||||
|
||||
// Store map instance
|
||||
this.map = map;
|
||||
|
||||
// Send initial bounds to server
|
||||
this.sendBoundsToServer();
|
||||
|
||||
// Listen for new packets from the server
|
||||
this.handleEvent("new_packet", (packet) => {
|
||||
console.log("Received new packet:", packet);
|
||||
this.addPacketMarker(packet);
|
||||
});
|
||||
|
||||
// Listen for clear markers event
|
||||
this.handleEvent("clear_markers", () => {
|
||||
this.clearAllMarkers();
|
||||
});
|
||||
|
||||
// Update bounds when map moves or zooms
|
||||
map.on("moveend", () => {
|
||||
this.sendBoundsToServer();
|
||||
});
|
||||
|
||||
// Handle map resize when window is resized
|
||||
window.addEventListener("resize", () => {
|
||||
map.invalidateSize();
|
||||
});
|
||||
},
|
||||
|
||||
sendBoundsToServer() {
|
||||
const bounds = this.map.getBounds();
|
||||
this.pushEvent("update_bounds", {
|
||||
bounds: {
|
||||
north: bounds.getNorth(),
|
||||
south: bounds.getSouth(),
|
||||
east: bounds.getEast(),
|
||||
west: bounds.getWest(),
|
||||
},
|
||||
});
|
||||
|
||||
// Remove markers that are now outside the visible bounds
|
||||
this.removeMarkersOutsideBounds(bounds);
|
||||
},
|
||||
|
||||
clearAllMarkers() {
|
||||
// Remove all markers from the map
|
||||
this.markers.forEach((marker) => {
|
||||
this.map.removeLayer(marker);
|
||||
});
|
||||
this.markers.clear();
|
||||
this.packetCount = 0;
|
||||
const counterElement = document.getElementById("packet-count");
|
||||
if (counterElement) {
|
||||
counterElement.textContent = this.packetCount;
|
||||
}
|
||||
},
|
||||
|
||||
removeMarkersOutsideBounds(bounds) {
|
||||
// Remove markers that are outside the current bounds
|
||||
const markersToRemove = [];
|
||||
|
||||
this.markers.forEach((marker, callsign) => {
|
||||
const latLng = marker.getLatLng();
|
||||
if (!bounds.contains(latLng)) {
|
||||
this.map.removeLayer(marker);
|
||||
markersToRemove.push(callsign);
|
||||
}
|
||||
});
|
||||
|
||||
// Remove from our tracking map
|
||||
markersToRemove.forEach((callsign) => {
|
||||
this.markers.delete(callsign);
|
||||
this.packetCount--;
|
||||
});
|
||||
|
||||
// Update counter
|
||||
const counterElement = document.getElementById("packet-count");
|
||||
if (counterElement) {
|
||||
counterElement.textContent = this.packetCount;
|
||||
}
|
||||
|
||||
// Notify server of the updated packet count
|
||||
this.pushEvent("update_packet_count", { count: this.packetCount });
|
||||
},
|
||||
|
||||
addPacketMarker(packet) {
|
||||
console.log("addPacketMarker called with:", packet);
|
||||
if (
|
||||
!packet["data_extended"] ||
|
||||
!packet["data_extended"]["latitude"] ||
|
||||
!packet["data_extended"]["longitude"]
|
||||
) {
|
||||
console.warn("Packet missing required location data:", packet);
|
||||
return;
|
||||
}
|
||||
|
||||
const lat = parseFloat(packet["data_extended"]["latitude"]);
|
||||
const lng = parseFloat(packet["data_extended"]["longitude"]);
|
||||
|
||||
// Validate coordinates are within valid ranges
|
||||
if (isNaN(lat) || isNaN(lng) || lat < -90 || lat > 90 || lng < -180 || lng > 180) {
|
||||
console.error("Invalid coordinates:", { lat, lng, packet });
|
||||
return;
|
||||
}
|
||||
console.log("Valid coordinates:", { lat, lng, callsign: packet["base_callsign"] });
|
||||
const callsign = packet["base_callsign"] + (packet["ssid"] ? "-" + packet["ssid"] : "");
|
||||
|
||||
// Create popup content
|
||||
const popupContent = `
|
||||
<div style="min-width: 200px;">
|
||||
<h4 style="margin: 0 0 5px 0; font-weight: bold;">${callsign}</h4>
|
||||
<p style="margin: 2px 0; font-size: 12px;">
|
||||
<strong>Position:</strong> ${lat.toFixed(4)}°, ${lng.toFixed(4)}°<br>
|
||||
<strong>Type:</strong> ${packet["data_type"]}<br>
|
||||
${packet["data_extended"]["comment"] ? `<strong>Comment:</strong> ${packet["data_extended"]["comment"]}<br>` : ""}
|
||||
<strong>Path:</strong> ${packet["path"]}<br>
|
||||
<strong>Time:</strong> ${new Date().toLocaleTimeString()}
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Check if marker already exists
|
||||
if (this.markers.has(callsign)) {
|
||||
// Update existing marker
|
||||
const existingMarker = this.markers.get(callsign);
|
||||
existingMarker.setLatLng([lat, lng]);
|
||||
existingMarker.setPopupContent(popupContent);
|
||||
} else {
|
||||
// Create new marker
|
||||
const icon = this.createAPRSIcon(
|
||||
packet["data_extended"]["symbol_table_id"] || "/",
|
||||
packet["data_extended"]["symbol_code"] || ">",
|
||||
);
|
||||
|
||||
const marker = L.marker([lat, lng], { icon: icon }).addTo(this.map).bindPopup(popupContent);
|
||||
|
||||
this.markers.set(callsign, marker);
|
||||
console.log("New marker added for:", callsign, "Total markers:", this.markers.size);
|
||||
|
||||
// Update packet counter
|
||||
this.packetCount++;
|
||||
const counterElement = document.getElementById("packet-count");
|
||||
if (counterElement) {
|
||||
counterElement.textContent = this.packetCount;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
createAPRSIcon(symbolTable, symbolCode) {
|
||||
// Default icon color based on symbol table
|
||||
const color = symbolTable === "/" ? "#2563eb" : "#dc2626";
|
||||
|
||||
return L.divIcon({
|
||||
html: `<div style="background-color: ${color}; width: 12px; height: 12px; border-radius: 50%; border: 2px solid white; box-shadow: 0 1px 3px rgba(0,0,0,0.4);"></div>`,
|
||||
className: "aprs-marker",
|
||||
iconSize: [16, 16],
|
||||
iconAnchor: [8, 8],
|
||||
popupAnchor: [0, -8],
|
||||
});
|
||||
},
|
||||
|
||||
destroyed() {
|
||||
console.log("APRSMap hook destroyed");
|
||||
if (this.map) {
|
||||
console.log("Removing map instance");
|
||||
this.map.remove();
|
||||
this.map = null;
|
||||
}
|
||||
this.markers.clear();
|
||||
},
|
||||
};
|
||||
|
||||
let liveSocket = new LiveSocket("/live", Socket, {
|
||||
longPollFallbackMs: 2500,
|
||||
params: { _csrf_token: csrfToken },
|
||||
hooks: Hooks,
|
||||
});
|
||||
|
||||
// Show progress bar on live navigation and form submits
|
||||
topbar.config({ barColors: { 0: "#29d" }, shadowColor: "rgba(0, 0, 0, .3)" });
|
||||
window.addEventListener("phx:page-loading-start", (info) =>
|
||||
topbar.delayedShow(200)
|
||||
);
|
||||
window.addEventListener("phx:page-loading-start", (info) => topbar.delayedShow(200));
|
||||
window.addEventListener("phx:page-loading-stop", (info) => topbar.hide());
|
||||
|
||||
// connect if there are any LiveViews on the page
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ defmodule Aprs.Is do
|
|||
|
||||
require Logger
|
||||
|
||||
@aprs_timeout 30 * 1000
|
||||
@aprs_timeout 60 * 1000
|
||||
@keepalive_interval 20 * 1000
|
||||
|
||||
def start_link(opts \\ []) do
|
||||
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
|
||||
|
|
@ -15,11 +16,14 @@ defmodule Aprs.Is do
|
|||
# Trap exits so we can gracefully shut down
|
||||
Process.flag(:trap_exit, true)
|
||||
|
||||
# Add a small delay to prevent rapid reconnection attempts
|
||||
Process.sleep(2000)
|
||||
|
||||
# Get startup parameters
|
||||
server = Application.get_env(:aprs, :aprs_is_server, ~c"rotate.aprs2.net")
|
||||
port = Application.get_env(:aprs, :aprs_is_port, 14_580)
|
||||
default_filter = Application.get_env(:aprs, :aprs_is_default_filter, "r/33/-96/100")
|
||||
aprs_user_id = Application.get_env(:aprs, :aprs_is_login_id, "w5isp")
|
||||
aprs_user_id = Application.get_env(:aprs, :aprs_is_login_id, "W5ISP")
|
||||
aprs_passcode = Application.get_env(:aprs, :aprs_is_password, "-1")
|
||||
|
||||
# Set up ets tables
|
||||
|
|
@ -44,13 +48,20 @@ defmodule Aprs.Is do
|
|||
with {:ok, socket} <- connect_to_aprs_is(server, port),
|
||||
:ok <- send_login_string(socket, aprs_user_id, aprs_passcode, default_filter) do
|
||||
timer = create_timer(@aprs_timeout)
|
||||
keepalive_timer = create_keepalive_timer(@keepalive_interval)
|
||||
|
||||
{:ok,
|
||||
%{
|
||||
server: server,
|
||||
port: port,
|
||||
socket: socket,
|
||||
timer: timer
|
||||
timer: timer,
|
||||
keepalive_timer: keepalive_timer,
|
||||
login_params: %{
|
||||
user_id: aprs_user_id,
|
||||
passcode: aprs_passcode,
|
||||
filter: default_filter
|
||||
}
|
||||
}}
|
||||
else
|
||||
_ ->
|
||||
|
|
@ -88,9 +99,9 @@ defmodule Aprs.Is do
|
|||
|
||||
defp send_login_string(socket, aprs_user_id, aprs_passcode, filter) do
|
||||
login_string =
|
||||
"user #{aprs_user_id} pass #{aprs_passcode} vers aprs.me 0.1 filter #{filter} \n"
|
||||
"user #{aprs_user_id} pass #{aprs_passcode} vers aprs.me 0.1 filter #{filter}\r\n"
|
||||
|
||||
Logger.debug("Sending login string: #{login_string}")
|
||||
Logger.info("Sending login string: user #{aprs_user_id} pass ***** vers aprs.me 0.1 filter #{filter}")
|
||||
|
||||
:gen_tcp.send(socket, login_string)
|
||||
end
|
||||
|
|
@ -99,6 +110,10 @@ defmodule Aprs.Is do
|
|||
Process.send_after(self(), :aprs_no_message_timeout, timeout)
|
||||
end
|
||||
|
||||
defp create_keepalive_timer(interval) do
|
||||
Process.send_after(self(), :send_keepalive, interval)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:send_message, message}, _from, state) do
|
||||
next_ack_number = :ets.update_counter(:aprs, :message_number, 1)
|
||||
|
|
@ -116,6 +131,20 @@ defmodule Aprs.Is do
|
|||
{:stop, :aprs_timeout, state}
|
||||
end
|
||||
|
||||
def handle_info(:send_keepalive, state) do
|
||||
# Send a comment line as keepalive (APRS-IS standard)
|
||||
case :gen_tcp.send(state.socket, "# keepalive\r\n") do
|
||||
:ok ->
|
||||
Logger.debug("Sent keepalive")
|
||||
keepalive_timer = create_keepalive_timer(@keepalive_interval)
|
||||
{:noreply, %{state | keepalive_timer: keepalive_timer}}
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.error("Failed to send keepalive: #{inspect(reason)}")
|
||||
{:stop, :normal, state}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_info({:tcp, _socket, packet}, state) do
|
||||
# Cancel the previous timer
|
||||
Process.cancel_timer(state.timer)
|
||||
|
|
@ -139,21 +168,29 @@ defmodule Aprs.Is do
|
|||
end
|
||||
|
||||
def handle_info({:tcp_closed, _socket}, state) do
|
||||
Logger.info("Socket has been closed")
|
||||
Logger.warning("Socket has been closed by remote server - will reconnect")
|
||||
{:stop, :normal, state}
|
||||
end
|
||||
|
||||
def handle_info({:tcp_error, _socket, reason}, state) do
|
||||
Logger.error("Connection closed due to #{inspect(reason)}")
|
||||
Logger.error("Connection error: #{inspect(reason)}")
|
||||
{:stop, :normal, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def terminate(reason, state) do
|
||||
# Do Shutdown Stuff
|
||||
Logger.info("Going Down: #{inspect(reason)} - #{inspect(state)}")
|
||||
Logger.info("Closing socket")
|
||||
:gen_tcp.close(state.socket)
|
||||
Logger.info("Terminating APRS-IS connection: #{inspect(reason)}")
|
||||
|
||||
# Cancel timers
|
||||
if Map.has_key?(state, :timer), do: Process.cancel_timer(state.timer)
|
||||
if Map.has_key?(state, :keepalive_timer), do: Process.cancel_timer(state.keepalive_timer)
|
||||
|
||||
# Close socket
|
||||
if Map.has_key?(state, :socket) do
|
||||
Logger.info("Closing socket")
|
||||
:gen_tcp.close(state.socket)
|
||||
end
|
||||
|
||||
:normal
|
||||
end
|
||||
|
|
|
|||
|
|
@ -9,9 +9,11 @@ defmodule Aprs.Is.IsSupervisor do
|
|||
@impl true
|
||||
def init(:ok) do
|
||||
children = [
|
||||
Aprs.Is
|
||||
{Aprs.Is, []}
|
||||
]
|
||||
|
||||
Supervisor.init(children, strategy: :one_for_one)
|
||||
# Supervisor will restart children max 3 times in 5 seconds
|
||||
# This prevents rapid reconnection attempts
|
||||
Supervisor.init(children, strategy: :one_for_one, max_restarts: 3, max_seconds: 5)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3,4 +3,20 @@ defmodule AprsWeb.Layouts do
|
|||
use AprsWeb, :html
|
||||
|
||||
embed_templates "layouts/*"
|
||||
|
||||
def body_class(assigns) do
|
||||
classes = ["bg-white antialiased"]
|
||||
|
||||
# Check if this is the home page (map)
|
||||
is_home =
|
||||
cond do
|
||||
# For regular controller requests
|
||||
assigns[:conn] && assigns.conn.request_path == "/" -> true
|
||||
# For LiveView requests
|
||||
assigns[:socket] && assigns.socket.view == AprsWeb.MapLive.Index -> true
|
||||
true -> false
|
||||
end
|
||||
|
||||
if is_home, do: ["home-page" | classes], else: classes
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
<script defer phx-track-static type="text/javascript" src={~p"/assets/app.js"}>
|
||||
</script>
|
||||
</head>
|
||||
<body class="bg-white antialiased">
|
||||
<body class={body_class(assigns)}>
|
||||
<ul>
|
||||
<%= if @current_user do %>
|
||||
<li>
|
||||
|
|
|
|||
|
|
@ -2,13 +2,11 @@ defmodule AprsWeb.PageController do
|
|||
use AprsWeb, :controller
|
||||
|
||||
def home(conn, _params) do
|
||||
# The home page is often custom made,
|
||||
# so skip the default app layout.
|
||||
render(conn, :home, layout: false)
|
||||
render(conn, :home)
|
||||
end
|
||||
|
||||
def map(conn, _params) do
|
||||
render(conn, :map, layout: false)
|
||||
render(conn, :map)
|
||||
end
|
||||
|
||||
def packets(conn, _params) do
|
||||
|
|
|
|||
|
|
@ -1,236 +0,0 @@
|
|||
<div class="fixed inset-y-0 right-0 left-[40rem] hidden lg:block xl:left-[50rem]">
|
||||
<svg
|
||||
viewBox="0 0 1480 957"
|
||||
fill="none"
|
||||
aria-hidden="true"
|
||||
class="absolute inset-0 h-full w-full"
|
||||
preserveAspectRatio="xMinYMid slice"
|
||||
>
|
||||
<path fill="#EE7868" d="M0 0h1480v957H0z" />
|
||||
<path
|
||||
d="M137.542 466.27c-582.851-48.41-988.806-82.127-1608.412 658.2l67.39 810 3083.15-256.51L1535.94-49.622l-98.36 8.183C1269.29 281.468 734.115 515.799 146.47 467.012l-8.928-.742Z"
|
||||
fill="#FF9F92"
|
||||
/>
|
||||
<path
|
||||
d="M371.028 528.664C-169.369 304.988-545.754 149.198-1361.45 665.565l-182.58 792.025 3014.73 694.98 389.42-1689.25-96.18-22.171C1505.28 697.438 924.153 757.586 379.305 532.09l-8.277-3.426Z"
|
||||
fill="#FA8372"
|
||||
/>
|
||||
<path
|
||||
d="M359.326 571.714C-104.765 215.795-428.003-32.102-1349.55 255.554l-282.3 1224.596 3047.04 722.01 312.24-1354.467C1411.25 1028.3 834.355 935.995 366.435 577.166l-7.109-5.452Z"
|
||||
fill="#E96856"
|
||||
fill-opacity=".6"
|
||||
/>
|
||||
<path
|
||||
d="M1593.87 1236.88c-352.15 92.63-885.498-145.85-1244.602-613.557l-5.455-7.105C-12.347 152.31-260.41-170.8-1225-131.458l-368.63 1599.048 3057.19 704.76 130.31-935.47Z"
|
||||
fill="#C42652"
|
||||
fill-opacity=".2"
|
||||
/>
|
||||
<path
|
||||
d="M1411.91 1526.93c-363.79 15.71-834.312-330.6-1085.883-863.909l-3.822-8.102C72.704 125.95-101.074-242.476-1052.01-408.907l-699.85 1484.267 2837.75 1338.01 326.02-886.44Z"
|
||||
fill="#A41C42"
|
||||
fill-opacity=".2"
|
||||
/>
|
||||
<path
|
||||
d="M1116.26 1863.69c-355.457-78.98-720.318-535.27-825.287-1115.521l-1.594-8.816C185.286 163.833 112.786-237.016-762.678-643.898L-1822.83 608.665 571.922 2635.55l544.338-771.86Z"
|
||||
fill="#A41C42"
|
||||
fill-opacity=".2"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="px-4 py-10 sm:py-28 sm:px-6 lg:px-8 xl:py-32 xl:px-28">
|
||||
<div class="mx-auto max-w-xl lg:mx-0">
|
||||
<svg viewBox="0 0 71 48" class="h-12" aria-hidden="true">
|
||||
<path
|
||||
d="m26.371 33.477-.552-.1c-3.92-.729-6.397-3.1-7.57-6.829-.733-2.324.597-4.035 3.035-4.148 1.995-.092 3.362 1.055 4.57 2.39 1.557 1.72 2.984 3.558 4.514 5.305 2.202 2.515 4.797 4.134 8.347 3.634 3.183-.448 5.958-1.725 8.371-3.828.363-.316.761-.592 1.144-.886l-.241-.284c-2.027.63-4.093.841-6.205.735-3.195-.16-6.24-.828-8.964-2.582-2.486-1.601-4.319-3.746-5.19-6.611-.704-2.315.736-3.934 3.135-3.6.948.133 1.746.56 2.463 1.165.583.493 1.143 1.015 1.738 1.493 2.8 2.25 6.712 2.375 10.265-.068-5.842-.026-9.817-3.24-13.308-7.313-1.366-1.594-2.7-3.216-4.095-4.785-2.698-3.036-5.692-5.71-9.79-6.623C12.8-.623 7.745.14 2.893 2.361 1.926 2.804.997 3.319 0 4.149c.494 0 .763.006 1.032 0 2.446-.064 4.28 1.023 5.602 3.024.962 1.457 1.415 3.104 1.761 4.798.513 2.515.247 5.078.544 7.605.761 6.494 4.08 11.026 10.26 13.346 2.267.852 4.591 1.135 7.172.555ZM10.751 3.852c-.976.246-1.756-.148-2.56-.962 1.377-.343 2.592-.476 3.897-.528-.107.848-.607 1.306-1.336 1.49Zm32.002 37.924c-.085-.626-.62-.901-1.04-1.228-1.857-1.446-4.03-1.958-6.333-2-1.375-.026-2.735-.128-4.031-.61-.595-.22-1.26-.505-1.244-1.272.015-.78.693-1 1.31-1.184.505-.15 1.026-.247 1.6-.382-1.46-.936-2.886-1.065-4.787-.3-2.993 1.202-5.943 1.06-8.926-.017-1.684-.608-3.179-1.563-4.735-2.408l-.043.03a2.96 2.96 0 0 0 .04-.029c-.038-.117-.107-.12-.197-.054l.122.107c1.29 2.115 3.034 3.817 5.004 5.271 3.793 2.8 7.936 4.471 12.784 3.73A66.714 66.714 0 0 1 37 40.877c1.98-.16 3.866.398 5.753.899Zm-9.14-30.345c-.105-.076-.206-.266-.42-.069 1.745 2.36 3.985 4.098 6.683 5.193 4.354 1.767 8.773 2.07 13.293.51 3.51-1.21 6.033-.028 7.343 3.38.19-3.955-2.137-6.837-5.843-7.401-2.084-.318-4.01.373-5.962.94-5.434 1.575-10.485.798-15.094-2.553Zm27.085 15.425c.708.059 1.416.123 2.124.185-1.6-1.405-3.55-1.517-5.523-1.404-3.003.17-5.167 1.903-7.14 3.972-1.739 1.824-3.31 3.87-5.903 4.604.043.078.054.117.066.117.35.005.699.021 1.047.005 3.768-.17 7.317-.965 10.14-3.7.89-.86 1.685-1.817 2.544-2.71.716-.746 1.584-1.159 2.645-1.07Zm-8.753-4.67c-2.812.246-5.254 1.409-7.548 2.943-1.766 1.18-3.654 1.738-5.776 1.37-.374-.066-.75-.114-1.124-.17l-.013.156c.135.07.265.151.405.207.354.14.702.308 1.07.395 4.083.971 7.992.474 11.516-1.803 2.221-1.435 4.521-1.707 7.013-1.336.252.038.503.083.756.107.234.022.479.255.795.003-2.179-1.574-4.526-2.096-7.094-1.872Zm-10.049-9.544c1.475.051 2.943-.142 4.486-1.059-.452.04-.643.04-.827.076-2.126.424-4.033-.04-5.733-1.383-.623-.493-1.257-.974-1.889-1.457-2.503-1.914-5.374-2.555-8.514-2.5.05.154.054.26.108.315 3.417 3.455 7.371 5.836 12.369 6.008Zm24.727 17.731c-2.114-2.097-4.952-2.367-7.578-.537 1.738.078 3.043.632 4.101 1.728.374.388.763.768 1.182 1.106 1.6 1.29 4.311 1.352 5.896.155-1.861-.726-1.861-.726-3.601-2.452Zm-21.058 16.06c-1.858-3.46-4.981-4.24-8.59-4.008a9.667 9.667 0 0 1 2.977 1.39c.84.586 1.547 1.311 2.243 2.055 1.38 1.473 3.534 2.376 4.962 2.07-.656-.412-1.238-.848-1.592-1.507Zm17.29-19.32c0-.023.001-.045.003-.068l-.006.006.006-.006-.036-.004.021.018.012.053Zm-20 14.744a7.61 7.61 0 0 0-.072-.041.127.127 0 0 0 .015.043c.005.008.038 0 .058-.002Zm-.072-.041-.008-.034-.008.01.008-.01-.022-.006.005.026.024.014Z"
|
||||
fill="#FD4F00"
|
||||
/>
|
||||
</svg>
|
||||
<h1 class="mt-10 flex items-center text-sm font-semibold leading-6 text-brand">
|
||||
Phoenix Framework
|
||||
<small class="ml-3 rounded-full bg-brand/5 px-2 text-[0.8125rem] font-medium leading-6">
|
||||
v1.7
|
||||
</small>
|
||||
</h1>
|
||||
<p class="mt-4 text-[2rem] font-semibold leading-10 tracking-tighter text-zinc-900">
|
||||
Peace of mind from prototype to production.
|
||||
</p>
|
||||
<p class="mt-4 text-base leading-7 text-zinc-600">
|
||||
Build rich, interactive web applications quickly, with less code and fewer moving parts. Join our growing community of developers using Phoenix to craft APIs, HTML5 apps and more, for fun or at scale.
|
||||
</p>
|
||||
<div class="flex">
|
||||
<div class="w-full sm:w-auto">
|
||||
<div class="mt-10 grid grid-cols-1 gap-x-6 gap-y-4 sm:grid-cols-3">
|
||||
<a
|
||||
href="https://hexdocs.pm/phoenix/overview.html"
|
||||
class="group relative rounded-2xl px-6 py-4 text-sm font-semibold leading-6 text-zinc-900 sm:py-6"
|
||||
>
|
||||
<span class="absolute inset-0 rounded-2xl bg-zinc-50 transition group-hover:bg-zinc-100 sm:group-hover:scale-105">
|
||||
</span>
|
||||
<span class="relative flex items-center gap-4 sm:flex-col">
|
||||
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" class="h-6 w-6">
|
||||
<path d="m12 4 10-2v18l-10 2V4Z" fill="#18181B" fill-opacity=".15" />
|
||||
<path
|
||||
d="M12 4 2 2v18l10 2m0-18v18m0-18 10-2v18l-10 2"
|
||||
stroke="#18181B"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
Guides & Docs
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/phoenixframework/phoenix"
|
||||
class="group relative rounded-2xl px-6 py-4 text-sm font-semibold leading-6 text-zinc-900 sm:py-6"
|
||||
>
|
||||
<span class="absolute inset-0 rounded-2xl bg-zinc-50 transition group-hover:bg-zinc-100 sm:group-hover:scale-105">
|
||||
</span>
|
||||
<span class="relative flex items-center gap-4 sm:flex-col">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true" class="h-6 w-6">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M12 0C5.37 0 0 5.506 0 12.303c0 5.445 3.435 10.043 8.205 11.674.6.107.825-.262.825-.585 0-.292-.015-1.261-.015-2.291C6 21.67 5.22 20.346 4.98 19.654c-.135-.354-.72-1.446-1.23-1.738-.42-.23-1.02-.8-.015-.815.945-.015 1.62.892 1.845 1.261 1.08 1.86 2.805 1.338 3.495 1.015.105-.8.42-1.338.765-1.645-2.67-.308-5.46-1.37-5.46-6.075 0-1.338.465-2.446 1.23-3.307-.12-.308-.54-1.569.12-3.26 0 0 1.005-.323 3.3 1.26.96-.276 1.98-.415 3-.415s2.04.139 3 .416c2.295-1.6 3.3-1.261 3.3-1.261.66 1.691.24 2.952.12 3.26.765.861 1.23 1.953 1.23 3.307 0 4.721-2.805 5.767-5.475 6.075.435.384.81 1.122.81 2.276 0 1.645-.015 2.968-.015 3.383 0 .323.225.707.825.585a12.047 12.047 0 0 0 5.919-4.489A12.536 12.536 0 0 0 24 12.304C24 5.505 18.63 0 12 0Z"
|
||||
fill="#18181B"
|
||||
/>
|
||||
</svg>
|
||||
Source Code
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/phoenixframework/phoenix/blob/v1.7/CHANGELOG.md"
|
||||
class="group relative rounded-2xl px-6 py-4 text-sm font-semibold leading-6 text-zinc-900 sm:py-6"
|
||||
>
|
||||
<span class="absolute inset-0 rounded-2xl bg-zinc-50 transition group-hover:bg-zinc-100 sm:group-hover:scale-105">
|
||||
</span>
|
||||
<span class="relative flex items-center gap-4 sm:flex-col">
|
||||
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" class="h-6 w-6">
|
||||
<path
|
||||
d="M12 1v6M12 17v6"
|
||||
stroke="#18181B"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<circle
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="4"
|
||||
fill="#18181B"
|
||||
fill-opacity=".15"
|
||||
stroke="#18181B"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
Changelog
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="mt-10 grid grid-cols-1 gap-y-4 text-sm leading-6 text-zinc-700 sm:grid-cols-2">
|
||||
<div>
|
||||
<a
|
||||
href="https://twitter.com/elixirphoenix"
|
||||
class="group -mx-2 -my-0.5 inline-flex items-center gap-3 rounded-lg px-2 py-0.5 hover:bg-zinc-50 hover:text-zinc-900"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 16 16"
|
||||
aria-hidden="true"
|
||||
class="h-4 w-4 fill-zinc-400 group-hover:fill-zinc-600"
|
||||
>
|
||||
<path d="M5.403 14c5.283 0 8.172-4.617 8.172-8.62 0-.131 0-.262-.008-.391A6.033 6.033 0 0 0 15 3.419a5.503 5.503 0 0 1-1.65.477 3.018 3.018 0 0 0 1.263-1.676 5.579 5.579 0 0 1-1.824.736 2.832 2.832 0 0 0-1.63-.916 2.746 2.746 0 0 0-1.821.319A2.973 2.973 0 0 0 8.076 3.78a3.185 3.185 0 0 0-.182 1.938 7.826 7.826 0 0 1-3.279-.918 8.253 8.253 0 0 1-2.64-2.247 3.176 3.176 0 0 0-.315 2.208 3.037 3.037 0 0 0 1.203 1.836A2.739 2.739 0 0 1 1.56 6.22v.038c0 .7.23 1.377.65 1.919.42.54 1.004.912 1.654 1.05-.423.122-.866.14-1.297.052.184.602.541 1.129 1.022 1.506a2.78 2.78 0 0 0 1.662.598 5.656 5.656 0 0 1-2.007 1.074A5.475 5.475 0 0 1 1 12.64a7.827 7.827 0 0 0 4.403 1.358" />
|
||||
</svg>
|
||||
Follow on Twitter
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
href="https://elixirforum.com"
|
||||
class="group -mx-2 -my-0.5 inline-flex items-center gap-3 rounded-lg px-2 py-0.5 hover:bg-zinc-50 hover:text-zinc-900"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 16 16"
|
||||
aria-hidden="true"
|
||||
class="h-4 w-4 fill-zinc-400 group-hover:fill-zinc-600"
|
||||
>
|
||||
<path d="M8 13.833c3.866 0 7-2.873 7-6.416C15 3.873 11.866 1 8 1S1 3.873 1 7.417c0 1.081.292 2.1.808 2.995.606 1.05.806 2.399.086 3.375l-.208.283c-.285.386-.01.905.465.85.852-.098 2.048-.318 3.137-.81a3.717 3.717 0 0 1 1.91-.318c.263.027.53.041.802.041Z" />
|
||||
</svg>
|
||||
Discuss on the Elixir forum
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
href="https://elixir-slackin.herokuapp.com"
|
||||
class="group -mx-2 -my-0.5 inline-flex items-center gap-3 rounded-lg px-2 py-0.5 hover:bg-zinc-50 hover:text-zinc-900"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 16 16"
|
||||
aria-hidden="true"
|
||||
class="h-4 w-4 fill-zinc-400 group-hover:fill-zinc-600"
|
||||
>
|
||||
<path d="M3.95 9.85a1.47 1.47 0 1 1-2.94 0 1.47 1.47 0 0 1 1.47-1.472h1.47v1.471Zm.735 0a1.47 1.47 0 1 1 2.94 0v3.678a1.47 1.47 0 1 1-2.94 0V9.85ZM6.156 3.942a1.47 1.47 0 0 1-1.47-1.472 1.47 1.47 0 1 1 2.94 0v1.472h-1.47Zm0 .747c.813 0 1.47.658 1.47 1.471a1.47 1.47 0 0 1-1.47 1.472H2.47A1.47 1.47 0 0 1 1 6.16 1.47 1.47 0 0 1 2.47 4.69h3.686ZM12.048 6.16a1.47 1.47 0 1 1 2.94 0 1.47 1.47 0 0 1-1.47 1.472h-1.47V6.16Zm-.735 0a1.47 1.47 0 1 1-2.94 0V2.47a1.47 1.47 0 1 1 2.94 0v3.69ZM9.843 12.057c.813 0 1.47.657 1.47 1.471a1.47 1.47 0 1 1-2.94 0v-1.471h1.47Zm0-.736a1.47 1.47 0 0 1-1.47-1.472 1.47 1.47 0 0 1 1.47-1.471h3.686c.813 0 1.47.658 1.47 1.471a1.47 1.47 0 0 1-1.47 1.472H9.843Z" />
|
||||
</svg>
|
||||
Join our Slack channel
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
href="https://web.libera.chat/#elixir"
|
||||
class="group -mx-2 -my-0.5 inline-flex items-center gap-3 rounded-lg px-2 py-0.5 hover:bg-zinc-50 hover:text-zinc-900"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 16 16"
|
||||
aria-hidden="true"
|
||||
class="h-4 w-4 fill-zinc-400 group-hover:fill-zinc-600"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M6.356 2.007a.75.75 0 0 1 .637.849l-1.5 10.5a.75.75 0 1 1-1.485-.212l1.5-10.5a.75.75 0 0 1 .848-.637ZM11.356 2.008a.75.75 0 0 1 .637.848l-1.5 10.5a.75.75 0 0 1-1.485-.212l1.5-10.5a.75.75 0 0 1 .848-.636Z"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M14 5.25a.75.75 0 0 1-.75.75h-9.5a.75.75 0 0 1 0-1.5h9.5a.75.75 0 0 1 .75.75ZM13 10.75a.75.75 0 0 1-.75.75h-9.5a.75.75 0 0 1 0-1.5h9.5a.75.75 0 0 1 .75.75Z"
|
||||
/>
|
||||
</svg>
|
||||
Chat on Libera IRC
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
href="https://discord.gg/elixir"
|
||||
class="group -mx-2 -my-0.5 inline-flex items-center gap-3 rounded-lg px-2 py-0.5 hover:bg-zinc-50 hover:text-zinc-900"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 16 16"
|
||||
aria-hidden="true"
|
||||
class="h-4 w-4 fill-zinc-400 group-hover:fill-zinc-600"
|
||||
>
|
||||
<path d="M13.545 2.995c-1.02-.46-2.114-.8-3.257-.994a.05.05 0 0 0-.052.024c-.141.246-.297.567-.406.82a12.377 12.377 0 0 0-3.658 0 8.238 8.238 0 0 0-.412-.82.052.052 0 0 0-.052-.024 13.315 13.315 0 0 0-3.257.994.046.046 0 0 0-.021.018C.356 6.063-.213 9.036.066 11.973c.001.015.01.029.02.038a13.353 13.353 0 0 0 3.996 1.987.052.052 0 0 0 .056-.018c.308-.414.582-.85.818-1.309a.05.05 0 0 0-.028-.069 8.808 8.808 0 0 1-1.248-.585.05.05 0 0 1-.005-.084c.084-.062.168-.126.248-.191a.05.05 0 0 1 .051-.007c2.619 1.176 5.454 1.176 8.041 0a.05.05 0 0 1 .053.006c.08.065.164.13.248.192a.05.05 0 0 1-.004.084c-.399.23-.813.423-1.249.585a.05.05 0 0 0-.027.07c.24.457.514.893.817 1.307a.051.051 0 0 0 .056.019 13.31 13.31 0 0 0 4.001-1.987.05.05 0 0 0 .021-.037c.334-3.396-.559-6.345-2.365-8.96a.04.04 0 0 0-.021-.02Zm-8.198 7.19c-.789 0-1.438-.712-1.438-1.587 0-.874.637-1.586 1.438-1.586.807 0 1.45.718 1.438 1.586 0 .875-.637 1.587-1.438 1.587Zm5.316 0c-.788 0-1.438-.712-1.438-1.587 0-.874.637-1.586 1.438-1.586.807 0 1.45.718 1.438 1.586 0 .875-.63 1.587-1.438 1.587Z" />
|
||||
</svg>
|
||||
Join our Discord server
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
href="https://fly.io/docs/getting-started/elixir/"
|
||||
class="group -mx-2 -my-0.5 inline-flex items-center gap-3 rounded-lg px-2 py-0.5 hover:bg-zinc-50 hover:text-zinc-900"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 20 20"
|
||||
aria-hidden="true"
|
||||
class="h-4 w-4 fill-zinc-400 group-hover:fill-zinc-600"
|
||||
>
|
||||
<path d="M1 12.5A4.5 4.5 0 005.5 17H15a4 4 0 001.866-7.539 3.504 3.504 0 00-4.504-4.272A4.5 4.5 0 004.06 8.235 4.502 4.502 0 001 12.5z" />
|
||||
</svg>
|
||||
Deploy your application
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
274
lib/aprs_web/live/map_live/index.ex
Normal file
274
lib/aprs_web/live/map_live/index.ex
Normal file
|
|
@ -0,0 +1,274 @@
|
|||
defmodule AprsWeb.MapLive.Index do
|
||||
@moduledoc """
|
||||
LiveView for displaying real-time APRS packets on a map
|
||||
"""
|
||||
use AprsWeb, :live_view
|
||||
|
||||
alias Aprs.EncodingUtils
|
||||
alias AprsWeb.Endpoint
|
||||
alias Parser.Types.MicE
|
||||
|
||||
@impl true
|
||||
def mount(_params, _session, socket) do
|
||||
if connected?(socket) do
|
||||
Endpoint.subscribe("aprs_messages")
|
||||
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
|
||||
}
|
||||
)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("update_bounds", %{"bounds" => bounds}, socket) do
|
||||
# Update the map bounds from the client
|
||||
map_bounds = %{
|
||||
north: bounds["north"],
|
||||
south: bounds["south"],
|
||||
east: bounds["east"],
|
||||
west: bounds["west"]
|
||||
}
|
||||
|
||||
# Don't clear markers - let the client preserve those still in view
|
||||
# Recalculate packet count based on new bounds
|
||||
visible_packets =
|
||||
socket.assigns.visible_packets
|
||||
|> Enum.filter(fn {_callsign, packet} ->
|
||||
within_bounds?(packet, map_bounds)
|
||||
end)
|
||||
|> Map.new()
|
||||
|
||||
packet_count = map_size(visible_packets)
|
||||
|
||||
{:noreply, assign(socket, map_bounds: map_bounds, visible_packets: visible_packets, packet_count: packet_count)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("update_packet_count", %{"count" => count}, socket) do
|
||||
# Update packet count from client after markers are removed
|
||||
{:noreply, assign(socket, packet_count: count)}
|
||||
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)
|
||||
|
||||
# Log packet type for debugging
|
||||
IO.inspect(sanitized_packet.data_type, label: "Packet type")
|
||||
|
||||
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
|
||||
|
||||
# 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)
|
||||
|
||||
# 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
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
||||
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
|
||||
crossorigin=""
|
||||
/>
|
||||
<script
|
||||
src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
||||
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
|
||||
crossorigin=""
|
||||
>
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#aprs-map {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.map-overlay {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
padding: 10px 15px;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.packet-counter {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.aprs-marker {
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="aprs-map" phx-hook="APRSMap" phx-update="ignore"></div>
|
||||
|
||||
<div class="map-overlay">
|
||||
<div class="packet-counter">
|
||||
<span id="packet-count">{@packet_count}</span> packets in view
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
# Helper functions
|
||||
|
||||
defp has_position_data?(packet) do
|
||||
case packet.data_extended do
|
||||
%MicE{} = mic_e ->
|
||||
# MicE packets have lat/lon in separate components
|
||||
is_number(mic_e.lat_degrees) && is_number(mic_e.lat_minutes) &&
|
||||
is_number(mic_e.lon_degrees) && is_number(mic_e.lon_minutes)
|
||||
|
||||
%{latitude: lat, longitude: lon} ->
|
||||
# Regular position packets have decimal lat/lon
|
||||
is_number(lat) && is_number(lon)
|
||||
|
||||
_ ->
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
defp within_bounds?(packet, bounds) do
|
||||
{lat, lng} = get_coordinates(packet)
|
||||
|
||||
lat && lng &&
|
||||
lat >= bounds.south && lat <= bounds.north &&
|
||||
lng >= bounds.west && lng <= bounds.east
|
||||
end
|
||||
|
||||
defp get_coordinates(packet) do
|
||||
case packet.data_extended do
|
||||
%MicE{} = mic_e ->
|
||||
# Convert MicE components to decimal degrees
|
||||
lat = mic_e.lat_degrees + mic_e.lat_minutes / 60.0 + mic_e.lat_fractional / 6000.0
|
||||
lat = if mic_e.lat_direction == :south, do: -lat, else: lat
|
||||
|
||||
lng = mic_e.lon_degrees + mic_e.lon_minutes / 60.0 + mic_e.lon_fractional / 6000.0
|
||||
lng = if mic_e.lon_direction == :west, do: -lng, else: lng
|
||||
|
||||
# Validate coordinates are within valid ranges
|
||||
if lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180 do
|
||||
{lat, lng}
|
||||
else
|
||||
IO.puts("Invalid MicE coordinates: lat=#{lat}, lng=#{lng}")
|
||||
{nil, nil}
|
||||
end
|
||||
|
||||
%{latitude: lat, longitude: lon} ->
|
||||
{lat, lon}
|
||||
|
||||
_ ->
|
||||
{nil, nil}
|
||||
end
|
||||
end
|
||||
|
||||
defp build_packet_data(packet) do
|
||||
data_extended = build_data_extended(packet.data_extended)
|
||||
|
||||
if data_extended do
|
||||
%{
|
||||
"base_callsign" => packet.base_callsign || "",
|
||||
"ssid" => packet.ssid || "",
|
||||
"data_type" => to_string(packet.data_type || "unknown"),
|
||||
"path" => packet.path || "",
|
||||
"data_extended" => data_extended
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
defp build_data_extended(nil), do: nil
|
||||
|
||||
defp build_data_extended(data_extended) do
|
||||
case data_extended do
|
||||
%MicE{} = mic_e ->
|
||||
# Convert MicE components to decimal degrees
|
||||
lat = mic_e.lat_degrees + mic_e.lat_minutes / 60.0 + mic_e.lat_fractional / 6000.0
|
||||
lat = if mic_e.lat_direction == :south, do: -lat, else: lat
|
||||
|
||||
lng = mic_e.lon_degrees + mic_e.lon_minutes / 60.0 + mic_e.lon_fractional / 6000.0
|
||||
lng = if mic_e.lon_direction == :west, do: -lng, else: lng
|
||||
|
||||
# Validate coordinates are within valid ranges
|
||||
if lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180 do
|
||||
%{
|
||||
"latitude" => lat,
|
||||
"longitude" => lng,
|
||||
"comment" => mic_e.message || "",
|
||||
"symbol_table_id" => "/",
|
||||
"symbol_code" => ">",
|
||||
"aprs_messaging" => false
|
||||
}
|
||||
end
|
||||
|
||||
_ ->
|
||||
%{
|
||||
"latitude" => data_extended[:latitude],
|
||||
"longitude" => data_extended[:longitude],
|
||||
"comment" => data_extended[:comment] || "",
|
||||
"symbol_table_id" => data_extended[:symbol_table_id] || "/",
|
||||
"symbol_code" => data_extended[:symbol_code] || ">",
|
||||
"aprs_messaging" => data_extended[:aprs_messaging] || false
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
248
lib/aprs_web/live/map_live/index.old
Normal file
248
lib/aprs_web/live/map_live/index.old
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
defmodule AprsWeb.MapLive.Index do
|
||||
@moduledoc """
|
||||
LiveView for displaying real-time APRS packets on a map
|
||||
"""
|
||||
use AprsWeb, :live_view
|
||||
|
||||
alias Aprs.EncodingUtils
|
||||
alias AprsWeb.Endpoint
|
||||
alias Parser.Types.MicE
|
||||
|
||||
@impl true
|
||||
def mount(_params, _session, socket) do
|
||||
if connected?(socket) do
|
||||
Endpoint.subscribe("aprs_messages")
|
||||
end
|
||||
|
||||
{:ok,
|
||||
assign(socket,
|
||||
packets: [],
|
||||
packet_count: 0,
|
||||
page_title: "APRS Map",
|
||||
# Default bounds for USA
|
||||
map_bounds: %{
|
||||
north: 49.0,
|
||||
south: 24.0,
|
||||
east: -66.0,
|
||||
west: -125.0
|
||||
}
|
||||
)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("update_bounds", %{"bounds" => bounds}, socket) do
|
||||
# Update the map bounds from the client
|
||||
map_bounds = %{
|
||||
north: bounds["north"],
|
||||
south: bounds["south"],
|
||||
east: bounds["east"],
|
||||
west: bounds["west"]
|
||||
}
|
||||
|
||||
# Clear existing markers when bounds change
|
||||
socket = push_event(socket, "clear_markers", %{})
|
||||
|
||||
{:noreply, assign(socket, map_bounds: map_bounds, packet_count: 0)}
|
||||
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)
|
||||
|
||||
# Log packet type for debugging
|
||||
IO.inspect(sanitized_packet.data_type, label: "Packet type")
|
||||
|
||||
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
|
||||
|
||||
# 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)
|
||||
|
||||
# Only push if we have valid packet data
|
||||
if packet_data do
|
||||
# Push the packet to the client-side JavaScript and increment count
|
||||
socket =
|
||||
socket
|
||||
|> push_event("new_packet", packet_data)
|
||||
|> update(:packet_count, &(&1 + 1))
|
||||
|
||||
{:noreply, socket}
|
||||
else
|
||||
# Invalid packet data, skip it
|
||||
{:noreply, socket}
|
||||
end
|
||||
else
|
||||
# Ignore packets without position data or outside bounds
|
||||
{:noreply, socket}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
||||
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
|
||||
crossorigin=""
|
||||
/>
|
||||
<script
|
||||
src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
||||
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
|
||||
crossorigin=""
|
||||
>
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#aprs-map {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.map-overlay {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
padding: 10px 15px;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.packet-counter {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.aprs-marker {
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="aprs-map" phx-hook="APRSMap"></div>
|
||||
|
||||
<div class="map-overlay">
|
||||
<div class="packet-counter">
|
||||
<span id="packet-count">{@packet_count}</span> packets in view
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
# Helper functions
|
||||
|
||||
defp has_position_data?(packet) do
|
||||
case packet.data_extended do
|
||||
%MicE{} = mic_e ->
|
||||
# MicE packets have lat/lon in separate components
|
||||
is_number(mic_e.lat_degrees) && is_number(mic_e.lat_minutes) &&
|
||||
is_number(mic_e.lon_degrees) && is_number(mic_e.lon_minutes)
|
||||
|
||||
%{latitude: lat, longitude: lon} ->
|
||||
# Regular position packets have decimal lat/lon
|
||||
is_number(lat) && is_number(lon)
|
||||
|
||||
_ ->
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
defp within_bounds?(packet, bounds) do
|
||||
{lat, lng} = get_coordinates(packet)
|
||||
|
||||
lat && lng &&
|
||||
lat >= bounds.south && lat <= bounds.north &&
|
||||
lng >= bounds.west && lng <= bounds.east
|
||||
end
|
||||
|
||||
defp get_coordinates(packet) do
|
||||
case packet.data_extended do
|
||||
%MicE{} = mic_e ->
|
||||
# Convert MicE components to decimal degrees
|
||||
lat = mic_e.lat_degrees + mic_e.lat_minutes / 60.0 + mic_e.lat_fractional / 6000.0
|
||||
lat = if mic_e.lat_direction == :south, do: -lat, else: lat
|
||||
|
||||
lng = mic_e.lon_degrees + mic_e.lon_minutes / 60.0 + mic_e.lon_fractional / 6000.0
|
||||
lng = if mic_e.lon_direction == :west, do: -lng, else: lng
|
||||
|
||||
# Validate coordinates are within valid ranges
|
||||
if lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180 do
|
||||
{lat, lng}
|
||||
else
|
||||
IO.puts("Invalid MicE coordinates: lat=#{lat}, lng=#{lng}")
|
||||
{nil, nil}
|
||||
end
|
||||
|
||||
%{latitude: lat, longitude: lon} ->
|
||||
{lat, lon}
|
||||
|
||||
_ ->
|
||||
{nil, nil}
|
||||
end
|
||||
end
|
||||
|
||||
defp build_packet_data(packet) do
|
||||
data_extended = build_data_extended(packet.data_extended)
|
||||
|
||||
if data_extended do
|
||||
%{
|
||||
"base_callsign" => packet.base_callsign || "",
|
||||
"ssid" => packet.ssid || "",
|
||||
"data_type" => to_string(packet.data_type || "unknown"),
|
||||
"path" => packet.path || "",
|
||||
"data_extended" => data_extended
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
defp build_data_extended(nil), do: nil
|
||||
|
||||
defp build_data_extended(data_extended) do
|
||||
case data_extended do
|
||||
%MicE{} = mic_e ->
|
||||
# Convert MicE components to decimal degrees
|
||||
lat = mic_e.lat_degrees + mic_e.lat_minutes / 60.0 + mic_e.lat_fractional / 6000.0
|
||||
lat = if mic_e.lat_direction == :south, do: -lat, else: lat
|
||||
|
||||
lng = mic_e.lon_degrees + mic_e.lon_minutes / 60.0 + mic_e.lon_fractional / 6000.0
|
||||
lng = if mic_e.lon_direction == :west, do: -lng, else: lng
|
||||
|
||||
# Validate coordinates are within valid ranges
|
||||
if lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180 do
|
||||
%{
|
||||
"latitude" => lat,
|
||||
"longitude" => lng,
|
||||
"comment" => mic_e.message || "",
|
||||
"symbol_table_id" => "/",
|
||||
"symbol_code" => ">",
|
||||
"aprs_messaging" => false
|
||||
}
|
||||
end
|
||||
|
||||
_ ->
|
||||
%{
|
||||
"latitude" => data_extended[:latitude],
|
||||
"longitude" => data_extended[:longitude],
|
||||
"comment" => data_extended[:comment] || "",
|
||||
"symbol_table_id" => data_extended[:symbol_table_id] || "/",
|
||||
"symbol_code" => data_extended[:symbol_code] || ">",
|
||||
"aprs_messaging" => data_extended[:aprs_messaging] || false
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -20,7 +20,7 @@ defmodule AprsWeb.Router do
|
|||
scope "/", AprsWeb do
|
||||
pipe_through :browser
|
||||
|
||||
get "/", PageController, :home
|
||||
live "/", MapLive.Index, :index
|
||||
get "/map", PageController, :map
|
||||
|
||||
live "/packets", PacketsLive.Index, :index
|
||||
|
|
|
|||
BIN
priv/static/favicon-a8ca4e3a2bb8fea46a9ee9e102e7d3eb.ico
Normal file
BIN
priv/static/favicon-a8ca4e3a2bb8fea46a9ee9e102e7d3eb.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
BIN
priv/static/images/phoenix-5bd99a0d17dd41bc9d9bf6840abcc089.png
Normal file
BIN
priv/static/images/phoenix-5bd99a0d17dd41bc9d9bf6840abcc089.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
5
priv/static/robots-9e2c81b0855bbff2baa8371bc4a78186.txt
Normal file
5
priv/static/robots-9e2c81b0855bbff2baa8371bc4a78186.txt
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
|
||||
#
|
||||
# To ban all spiders from the entire site uncomment the next two lines:
|
||||
# User-agent: *
|
||||
# Disallow: /
|
||||
BIN
priv/static/robots-9e2c81b0855bbff2baa8371bc4a78186.txt.gz
Normal file
BIN
priv/static/robots-9e2c81b0855bbff2baa8371bc4a78186.txt.gz
Normal file
Binary file not shown.
BIN
priv/static/robots.txt.gz
Normal file
BIN
priv/static/robots.txt.gz
Normal file
Binary file not shown.
|
|
@ -1,8 +0,0 @@
|
|||
defmodule AprsWeb.PageControllerTest do
|
||||
use AprsWeb.ConnCase
|
||||
|
||||
test "GET /", %{conn: conn} do
|
||||
conn = get(conn, ~p"/")
|
||||
assert html_response(conn, 200) =~ "Peace of mind from prototype to production"
|
||||
end
|
||||
end
|
||||
248
tmp/index.ex.backup
Normal file
248
tmp/index.ex.backup
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
defmodule AprsWeb.MapLive.Index do
|
||||
@moduledoc """
|
||||
LiveView for displaying real-time APRS packets on a map
|
||||
"""
|
||||
use AprsWeb, :live_view
|
||||
|
||||
alias Aprs.EncodingUtils
|
||||
alias AprsWeb.Endpoint
|
||||
alias Parser.Types.MicE
|
||||
|
||||
@impl true
|
||||
def mount(_params, _session, socket) do
|
||||
if connected?(socket) do
|
||||
Endpoint.subscribe("aprs_messages")
|
||||
end
|
||||
|
||||
{:ok,
|
||||
assign(socket,
|
||||
packets: [],
|
||||
packet_count: 0,
|
||||
page_title: "APRS Map",
|
||||
# Default bounds for USA
|
||||
map_bounds: %{
|
||||
north: 49.0,
|
||||
south: 24.0,
|
||||
east: -66.0,
|
||||
west: -125.0
|
||||
}
|
||||
)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("update_bounds", %{"bounds" => bounds}, socket) do
|
||||
# Update the map bounds from the client
|
||||
map_bounds = %{
|
||||
north: bounds["north"],
|
||||
south: bounds["south"],
|
||||
east: bounds["east"],
|
||||
west: bounds["west"]
|
||||
}
|
||||
|
||||
# Clear existing markers when bounds change
|
||||
socket = push_event(socket, "clear_markers", %{})
|
||||
|
||||
{:noreply, assign(socket, map_bounds: map_bounds, packet_count: 0)}
|
||||
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)
|
||||
|
||||
# Log packet type for debugging
|
||||
IO.inspect(sanitized_packet.data_type, label: "Packet type")
|
||||
|
||||
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
|
||||
|
||||
# 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)
|
||||
|
||||
# Only push if we have valid packet data
|
||||
if packet_data do
|
||||
# Push the packet to the client-side JavaScript and increment count
|
||||
socket =
|
||||
socket
|
||||
|> push_event("new_packet", packet_data)
|
||||
|> update(:packet_count, &(&1 + 1))
|
||||
|
||||
{:noreply, socket}
|
||||
else
|
||||
# Invalid packet data, skip it
|
||||
{:noreply, socket}
|
||||
end
|
||||
else
|
||||
# Ignore packets without position data or outside bounds
|
||||
{:noreply, socket}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
||||
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
|
||||
crossorigin=""
|
||||
/>
|
||||
<script
|
||||
src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
||||
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
|
||||
crossorigin=""
|
||||
>
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#aprs-map {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.map-overlay {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
padding: 10px 15px;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.packet-counter {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.aprs-marker {
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="aprs-map" phx-hook="APRSMap"></div>
|
||||
|
||||
<div class="map-overlay">
|
||||
<div class="packet-counter">
|
||||
<span id="packet-count">{@packet_count}</span> packets in view
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
# Helper functions
|
||||
|
||||
defp has_position_data?(packet) do
|
||||
case packet.data_extended do
|
||||
%MicE{} = mic_e ->
|
||||
# MicE packets have lat/lon in separate components
|
||||
is_number(mic_e.lat_degrees) && is_number(mic_e.lat_minutes) &&
|
||||
is_number(mic_e.lon_degrees) && is_number(mic_e.lon_minutes)
|
||||
|
||||
%{latitude: lat, longitude: lon} ->
|
||||
# Regular position packets have decimal lat/lon
|
||||
is_number(lat) && is_number(lon)
|
||||
|
||||
_ ->
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
defp within_bounds?(packet, bounds) do
|
||||
{lat, lng} = get_coordinates(packet)
|
||||
|
||||
lat && lng &&
|
||||
lat >= bounds.south && lat <= bounds.north &&
|
||||
lng >= bounds.west && lng <= bounds.east
|
||||
end
|
||||
|
||||
defp get_coordinates(packet) do
|
||||
case packet.data_extended do
|
||||
%MicE{} = mic_e ->
|
||||
# Convert MicE components to decimal degrees
|
||||
lat = mic_e.lat_degrees + mic_e.lat_minutes / 60.0 + mic_e.lat_fractional / 6000.0
|
||||
lat = if mic_e.lat_direction == :south, do: -lat, else: lat
|
||||
|
||||
lng = mic_e.lon_degrees + mic_e.lon_minutes / 60.0 + mic_e.lon_fractional / 6000.0
|
||||
lng = if mic_e.lon_direction == :west, do: -lng, else: lng
|
||||
|
||||
# Validate coordinates are within valid ranges
|
||||
if lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180 do
|
||||
{lat, lng}
|
||||
else
|
||||
IO.puts("Invalid MicE coordinates: lat=#{lat}, lng=#{lng}")
|
||||
{nil, nil}
|
||||
end
|
||||
|
||||
%{latitude: lat, longitude: lon} ->
|
||||
{lat, lon}
|
||||
|
||||
_ ->
|
||||
{nil, nil}
|
||||
end
|
||||
end
|
||||
|
||||
defp build_packet_data(packet) do
|
||||
data_extended = build_data_extended(packet.data_extended)
|
||||
|
||||
if data_extended do
|
||||
%{
|
||||
"base_callsign" => packet.base_callsign || "",
|
||||
"ssid" => packet.ssid || "",
|
||||
"data_type" => to_string(packet.data_type || "unknown"),
|
||||
"path" => packet.path || "",
|
||||
"data_extended" => data_extended
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
defp build_data_extended(nil), do: nil
|
||||
|
||||
defp build_data_extended(data_extended) do
|
||||
case data_extended do
|
||||
%MicE{} = mic_e ->
|
||||
# Convert MicE components to decimal degrees
|
||||
lat = mic_e.lat_degrees + mic_e.lat_minutes / 60.0 + mic_e.lat_fractional / 6000.0
|
||||
lat = if mic_e.lat_direction == :south, do: -lat, else: lat
|
||||
|
||||
lng = mic_e.lon_degrees + mic_e.lon_minutes / 60.0 + mic_e.lon_fractional / 6000.0
|
||||
lng = if mic_e.lon_direction == :west, do: -lng, else: lng
|
||||
|
||||
# Validate coordinates are within valid ranges
|
||||
if lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180 do
|
||||
%{
|
||||
"latitude" => lat,
|
||||
"longitude" => lng,
|
||||
"comment" => mic_e.message || "",
|
||||
"symbol_table_id" => "/",
|
||||
"symbol_code" => ">",
|
||||
"aprs_messaging" => false
|
||||
}
|
||||
end
|
||||
|
||||
_ ->
|
||||
%{
|
||||
"latitude" => data_extended[:latitude],
|
||||
"longitude" => data_extended[:longitude],
|
||||
"comment" => data_extended[:comment] || "",
|
||||
"symbol_table_id" => data_extended[:symbol_table_id] || "/",
|
||||
"symbol_code" => data_extended[:symbol_code] || ">",
|
||||
"aprs_messaging" => data_extended[:aprs_messaging] || false
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Reference in a new issue