mirror of
https://github.com/craigerl/aprsd.git
synced 2024-11-18 06:11:49 -05:00
Added Location info on webchat interface
This patch adds a new popover in the webchat tab to show the location information for a callsign. webchat will try to hit aprs.fi to fetch the location from the callsign's last beacon. If there is no internet, this will fail and webchat will send a request to REPEAT callsign for the location information.
This commit is contained in:
parent
03c58f83cd
commit
d6f0f05315
@ -2,6 +2,7 @@ import datetime
|
||||
import json
|
||||
import logging
|
||||
from logging.handlers import RotatingFileHandler
|
||||
import math
|
||||
import signal
|
||||
import sys
|
||||
import threading
|
||||
@ -14,15 +15,17 @@ from flask import request
|
||||
from flask.logging import default_handler
|
||||
from flask_httpauth import HTTPBasicAuth
|
||||
from flask_socketio import Namespace, SocketIO
|
||||
from geopy.distance import geodesic
|
||||
from oslo_config import cfg
|
||||
from werkzeug.security import check_password_hash, generate_password_hash
|
||||
import wrapt
|
||||
|
||||
import aprsd
|
||||
from aprsd import cli_helper, client, conf, packets, stats, threads, utils
|
||||
from aprsd import cli_helper, client, conf, packets, stats, threads, utils, plugin_utils
|
||||
from aprsd.log import rich as aprsd_logging
|
||||
from aprsd.main import cli
|
||||
from aprsd.threads import rx, tx
|
||||
from aprsd.threads import aprsd as aprsd_threads
|
||||
from aprsd.utils import trace
|
||||
|
||||
|
||||
@ -32,6 +35,16 @@ auth = HTTPBasicAuth()
|
||||
users = {}
|
||||
socketio = None
|
||||
|
||||
# List of callsigns that we don't want to track/fetch their location
|
||||
callsign_no_track = [
|
||||
"REPEAT", "WB4BOR-11", "APDW16", "WXNOW", "WXBOT", "BLN0", "BLN1", "BLN2",
|
||||
"BLN3", "BLN4", "BLN5", "BLN6", "BLN7", "BLN8", "BLN9",
|
||||
]
|
||||
|
||||
# Callsign location information
|
||||
# callsign: {lat: 0.0, long: 0.0, last_update: datetime}
|
||||
callsign_locations = {}
|
||||
|
||||
flask_app = flask.Flask(
|
||||
"aprsd",
|
||||
static_url_path="/static",
|
||||
@ -121,8 +134,186 @@ def verify_password(username, password):
|
||||
return username
|
||||
|
||||
|
||||
def calculate_initial_compass_bearing(point_a, point_b):
|
||||
"""
|
||||
Calculates the bearing between two points.
|
||||
The formulae used is the following:
|
||||
θ = atan2(sin(Δlong).cos(lat2),
|
||||
cos(lat1).sin(lat2) − sin(lat1).cos(lat2).cos(Δlong))
|
||||
:Parameters:
|
||||
- `pointA: The tuple representing the latitude/longitude for the
|
||||
first point. Latitude and longitude must be in decimal degrees
|
||||
- `pointB: The tuple representing the latitude/longitude for the
|
||||
second point. Latitude and longitude must be in decimal degrees
|
||||
:Returns:
|
||||
The bearing in degrees
|
||||
:Returns Type:
|
||||
float
|
||||
"""
|
||||
if (type(point_a) is not tuple) or (type(point_b) is not tuple):
|
||||
raise TypeError("Only tuples are supported as arguments")
|
||||
|
||||
lat1 = math.radians(point_a[0])
|
||||
lat2 = math.radians(point_b[0])
|
||||
|
||||
diff_long = math.radians(point_b[1] - point_a[1])
|
||||
|
||||
x = math.sin(diff_long) * math.cos(lat2)
|
||||
y = math.cos(lat1) * math.sin(lat2) - (
|
||||
math.sin(lat1)
|
||||
* math.cos(lat2) * math.cos(diff_long)
|
||||
)
|
||||
|
||||
initial_bearing = math.atan2(x, y)
|
||||
|
||||
# Now we have the initial bearing but math.atan2 return values
|
||||
# from -180° to + 180° which is not what we want for a compass bearing
|
||||
# The solution is to normalize the initial bearing as shown below
|
||||
initial_bearing = math.degrees(initial_bearing)
|
||||
compass_bearing = (initial_bearing + 360) % 360
|
||||
|
||||
return compass_bearing
|
||||
|
||||
|
||||
def _build_location_from_repeat(message):
|
||||
# This is a location message Format is
|
||||
# ^ld^callsign:latitude,longitude,altitude,course,speed,timestamp
|
||||
a = message.split(":")
|
||||
LOG.warning(a)
|
||||
if len(a) == 2:
|
||||
callsign = a[0].replace('^ld^', '')
|
||||
b = a[1].split(",")
|
||||
LOG.warning(b)
|
||||
if len(b) == 6:
|
||||
lat = float(b[0])
|
||||
lon = float(b[1])
|
||||
alt = float(b[2])
|
||||
course = float(b[3])
|
||||
speed = float(b[4])
|
||||
time = int(b[5])
|
||||
data = {
|
||||
"callsign": callsign,
|
||||
"lat": lat,
|
||||
"lon": lon,
|
||||
"altitude": alt,
|
||||
"course": course,
|
||||
"speed": speed,
|
||||
"lasttime": time,
|
||||
}
|
||||
LOG.warning(f"Location data from REPEAT {data}")
|
||||
return data
|
||||
|
||||
|
||||
def _calculate_location_data(location_data):
|
||||
"""Calculate all of the location data from data from aprs.fi or REPEAT."""
|
||||
lat = location_data["lat"]
|
||||
lon = location_data["lon"]
|
||||
alt = location_data["altitude"]
|
||||
speed = location_data["speed"]
|
||||
lasttime = location_data["lasttime"]
|
||||
# now calculate distance from our own location
|
||||
distance = 0
|
||||
if CONF.webchat.latitude and CONF.webchat.longitude:
|
||||
our_lat = float(CONF.webchat.latitude)
|
||||
our_lon = float(CONF.webchat.longitude)
|
||||
distance = geodesic((our_lat, our_lon), (lat, lon)).kilometers
|
||||
bearing = calculate_initial_compass_bearing(
|
||||
(our_lat, our_lon),
|
||||
(lat, lon),
|
||||
)
|
||||
return {
|
||||
"callsign": location_data["callsign"],
|
||||
"lat": lat,
|
||||
"lon": lon,
|
||||
"altitude": alt,
|
||||
"course": f"{bearing:0.1f}",
|
||||
"speed": speed,
|
||||
"lasttime": lasttime,
|
||||
"distance": f"{distance:0.3f}",
|
||||
}
|
||||
|
||||
|
||||
def send_location_data_to_browser(location_data):
|
||||
global socketio
|
||||
callsign = location_data["callsign"]
|
||||
LOG.info(f"Got location for {callsign} {callsign_locations[callsign]}")
|
||||
socketio.emit(
|
||||
"callsign_location", callsign_locations[callsign],
|
||||
namespace="/sendmsg",
|
||||
)
|
||||
|
||||
|
||||
def populate_callsign_location(callsign, data=None):
|
||||
"""Populate the location for the callsign.
|
||||
|
||||
if data is passed in, then we have the location already from
|
||||
an APRS packet. If data is None, then we need to fetch the
|
||||
location from aprs.fi or REPEAT.
|
||||
"""
|
||||
global socketio
|
||||
"""Fetch the location for the callsign."""
|
||||
LOG.debug(f"populate_callsign_location {callsign}")
|
||||
if data:
|
||||
location_data = _calculate_location_data(data)
|
||||
callsign_locations[callsign] = location_data
|
||||
send_location_data_to_browser(location_data)
|
||||
return
|
||||
|
||||
# First we are going to try to get the location from aprs.fi
|
||||
# if there is no internets, then this will fail and we will
|
||||
# fallback to calling REPEAT for the location for the callsign.
|
||||
fallback = False
|
||||
if not CONF.aprs_fi.apiKey:
|
||||
LOG.warning(
|
||||
"Config aprs_fi.apiKey is not set. Can't get location from aprs.fi "
|
||||
" falling back to sending REPEAT to get location.",
|
||||
)
|
||||
fallback = True
|
||||
else:
|
||||
try:
|
||||
aprs_data = plugin_utils.get_aprs_fi(CONF.aprs_fi.apiKey, callsign)
|
||||
if not len(aprs_data["entries"]):
|
||||
LOG.error("Didn't get any entries from aprs.fi")
|
||||
return
|
||||
lat = float(aprs_data["entries"][0]["lat"])
|
||||
lon = float(aprs_data["entries"][0]["lng"])
|
||||
try: # altitude not always provided
|
||||
alt = float(aprs_data["entries"][0]["altitude"])
|
||||
except Exception:
|
||||
alt = 0
|
||||
location_data = {
|
||||
'lat': lat,
|
||||
'long': lon,
|
||||
'alt': alt,
|
||||
'lasttime': int(aprs_data["entries"][0]["lasttime"]),
|
||||
'course': float(aprs_data["entries"][0].get("course", 0)),
|
||||
'speed': float(aprs_data["entries"][0].get("speed", 0)),
|
||||
}
|
||||
location_data = _calculate_location_data(location_data)
|
||||
callsign_locations[callsign] = location_data
|
||||
send_location_data_to_browser(location_data)
|
||||
return
|
||||
except Exception as ex:
|
||||
LOG.error(f"Failed to fetch aprs.fi '{ex}'")
|
||||
fallback = True
|
||||
|
||||
if fallback:
|
||||
# We don't have the location data
|
||||
# and we can't get it from aprs.fi
|
||||
# Send a special message to REPEAT to get the location data
|
||||
LOG.info(f"Sending REPEAT to get location for callsign {callsign}.")
|
||||
tx.send(
|
||||
packets.MessagePacket(
|
||||
from_call=CONF.callsign,
|
||||
to_call="REPEAT",
|
||||
message_text=f"ld {callsign}",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class WebChatProcessPacketThread(rx.APRSDProcessPacketThread):
|
||||
"""Class that handles packets being sent to us."""
|
||||
|
||||
def __init__(self, packet_queue, socketio):
|
||||
self.socketio = socketio
|
||||
self.connected = False
|
||||
@ -132,20 +323,51 @@ class WebChatProcessPacketThread(rx.APRSDProcessPacketThread):
|
||||
super().process_ack_packet(packet)
|
||||
ack_num = packet.get("msgNo")
|
||||
SentMessages().ack(ack_num)
|
||||
msg = SentMessages().get(ack_num)
|
||||
if msg:
|
||||
self.socketio.emit(
|
||||
"ack", SentMessages().get(ack_num),
|
||||
"ack", msg,
|
||||
namespace="/sendmsg",
|
||||
)
|
||||
self.got_ack = True
|
||||
|
||||
def process_our_message_packet(self, packet: packets.MessagePacket):
|
||||
global callsign_locations
|
||||
LOG.info(f"process MessagePacket {repr(packet)}")
|
||||
# ok lets see if we have the location for the
|
||||
# person we just sent a message to.
|
||||
from_call = packet.get("from_call").upper()
|
||||
if from_call == 'REPEAT':
|
||||
# We got a message from REPEAT. Is this a location message?
|
||||
message = packet.get("message_text")
|
||||
if message.startswith("^ld^"):
|
||||
location_data = _build_location_from_repeat(message)
|
||||
callsign = location_data["callsign"]
|
||||
location_data = _calculate_location_data(location_data)
|
||||
callsign_locations[callsign] = location_data
|
||||
send_location_data_to_browser(location_data)
|
||||
return
|
||||
elif (from_call not in callsign_locations
|
||||
and from_call not in callsign_no_track):
|
||||
# We have to ask aprs for the location for the callsign
|
||||
# We send a message packet to wb4bor-11 asking for location.
|
||||
populate_callsign_location(from_call)
|
||||
# Send the packet to the browser.
|
||||
self.socketio.emit(
|
||||
"new", packet.__dict__,
|
||||
namespace="/sendmsg",
|
||||
)
|
||||
|
||||
|
||||
class LocationProcessingThread(aprsd_threads.APRSDThread):
|
||||
"""Class to handle the location processing."""
|
||||
def __init__(self):
|
||||
super().__init__("LocationProcessingThread")
|
||||
|
||||
def loop(self):
|
||||
pass
|
||||
|
||||
|
||||
def set_config():
|
||||
global users
|
||||
|
||||
|
3
aprsd/web/chat/static/images/globe.svg
Normal file
3
aprsd/web/chat/static/images/globe.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-globe" viewBox="0 0 16 16">
|
||||
<path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8m7.5-6.923c-.67.204-1.335.82-1.887 1.855A8 8 0 0 0 5.145 4H7.5zM4.09 4a9.3 9.3 0 0 1 .64-1.539 7 7 0 0 1 .597-.933A7.03 7.03 0 0 0 2.255 4zm-.582 3.5c.03-.877.138-1.718.312-2.5H1.674a7 7 0 0 0-.656 2.5zM4.847 5a12.5 12.5 0 0 0-.338 2.5H7.5V5zM8.5 5v2.5h2.99a12.5 12.5 0 0 0-.337-2.5zM4.51 8.5a12.5 12.5 0 0 0 .337 2.5H7.5V8.5zm3.99 0V11h2.653c.187-.765.306-1.608.338-2.5zM5.145 12q.208.58.468 1.068c.552 1.035 1.218 1.65 1.887 1.855V12zm.182 2.472a7 7 0 0 1-.597-.933A9.3 9.3 0 0 1 4.09 12H2.255a7 7 0 0 0 3.072 2.472M3.82 11a13.7 13.7 0 0 1-.312-2.5h-2.49c.062.89.291 1.733.656 2.5zm6.853 3.472A7 7 0 0 0 13.745 12H11.91a9.3 9.3 0 0 1-.64 1.539 7 7 0 0 1-.597.933M8.5 12v2.923c.67-.204 1.335-.82 1.887-1.855q.26-.487.468-1.068zm3.68-1h2.146c.365-.767.594-1.61.656-2.5h-2.49a13.7 13.7 0 0 1-.312 2.5m2.802-3.5a7 7 0 0 0-.656-2.5H12.18c.174.782.282 1.623.312 2.5zM11.27 2.461c.247.464.462.98.64 1.539h1.835a7 7 0 0 0-3.072-2.472c.218.284.418.598.597.933M10.855 4a8 8 0 0 0-.468-1.068C9.835 1.897 9.17 1.282 8.5 1.077V4z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
@ -1,5 +1,6 @@
|
||||
var cleared = false;
|
||||
var callsign_list = {};
|
||||
var callsign_location = {};
|
||||
var message_list = {};
|
||||
var from_msg_list = {};
|
||||
var selected_tab_callsign = null;
|
||||
@ -9,6 +10,25 @@ MSG_TYPE_TX = "tx";
|
||||
MSG_TYPE_RX = "rx";
|
||||
MSG_TYPE_ACK = "ack";
|
||||
|
||||
function reload_popovers() {
|
||||
$('[data-bs-toggle="popover"]').popover(
|
||||
{html: true, animation: true}
|
||||
);
|
||||
}
|
||||
|
||||
function build_location_string(msg) {
|
||||
console.log("Building location string");
|
||||
console.log(msg);
|
||||
dt = new Date(parseInt(msg['lasttime']) * 1000);
|
||||
loc = "Last Location Update: " + dt.toLocaleString();
|
||||
loc += "<br>Latitude: " + msg['lat'] + "<br>Longitude: " + msg['lon'];
|
||||
loc += "<br>" + "Altitude: " + msg['altitude'] + " m";
|
||||
loc += "<br>" + "Speed: " + msg['speed'] + " kph";
|
||||
loc += "<br>" + "Bearing: " + msg['course'] + "°";
|
||||
loc += "<br>" + "distance: " + msg['distance'] + " km";
|
||||
return loc;
|
||||
}
|
||||
|
||||
function size_dict(d){c=0; for (i in d) ++c; return c}
|
||||
|
||||
function raise_error(msg) {
|
||||
@ -43,6 +63,8 @@ function init_chat() {
|
||||
});
|
||||
|
||||
socket.on("ack", function(msg) {
|
||||
console.log("ACK");
|
||||
console.log(msg);
|
||||
msg["type"] = MSG_TYPE_ACK;
|
||||
ack_msg(msg);
|
||||
});
|
||||
@ -57,6 +79,20 @@ function init_chat() {
|
||||
from_msg(msg);
|
||||
});
|
||||
|
||||
socket.on("callsign_location", function(msg) {
|
||||
console.log("CALLSIGN Location!");
|
||||
console.log(msg);
|
||||
callsign_location[msg['callsign']] = msg;
|
||||
|
||||
popover_id = callsign_location_popover(msg['callsign'], true);
|
||||
location_string = build_location_string(msg);
|
||||
console.log(location_string);
|
||||
$(popover_id).attr('data-bs-content', location_string);
|
||||
$(popover_id).removeClass('visually-hidden');
|
||||
reload_popovers();
|
||||
save_data();
|
||||
});
|
||||
|
||||
$("#sendform").submit(function(event) {
|
||||
event.preventDefault();
|
||||
to_call = $('#to_call').val();
|
||||
@ -71,7 +107,7 @@ function init_chat() {
|
||||
return false;
|
||||
}
|
||||
msg = {'to': to_call, 'message': message, 'path': path};
|
||||
console.log(msg);
|
||||
//console.log(msg);
|
||||
socket.emit("send", msg);
|
||||
$('#message').val('');
|
||||
}
|
||||
@ -82,6 +118,7 @@ function init_chat() {
|
||||
init_messages();
|
||||
}
|
||||
|
||||
|
||||
function tab_string(callsign, id=false) {
|
||||
name = "msgs"+callsign;
|
||||
if (id) {
|
||||
@ -121,6 +158,10 @@ function callsign_tab(callsign) {
|
||||
return "#"+tab_string(callsign);
|
||||
}
|
||||
|
||||
function callsign_location_popover(callsign, id=false) {
|
||||
return tab_string(callsign, id)+"Location";
|
||||
}
|
||||
|
||||
function bubble_msg_id(msg, id=false) {
|
||||
// The id of the div that contains a specific message
|
||||
name = msg["from_call"] + "_" + msg["msgNo"];
|
||||
@ -155,20 +196,26 @@ function save_data() {
|
||||
// Save the relevant data to local storage
|
||||
localStorage.setItem('callsign_list', JSON.stringify(callsign_list));
|
||||
localStorage.setItem('message_list', JSON.stringify(message_list));
|
||||
localStorage.setItem('callsign_location', JSON.stringify(callsign_location));
|
||||
}
|
||||
|
||||
function init_messages() {
|
||||
// This tries to load any previous conversations from local storage
|
||||
callsign_list = JSON.parse(localStorage.getItem('callsign_list'));
|
||||
message_list = JSON.parse(localStorage.getItem('message_list'));
|
||||
callsign_location = JSON.parse(localStorage.getItem('callsign_location'));
|
||||
if (callsign_list == null) {
|
||||
callsign_list = {};
|
||||
}
|
||||
if (message_list == null) {
|
||||
message_list = {};
|
||||
}
|
||||
//console.log(callsign_list);
|
||||
//console.log(message_list);
|
||||
if (callsign_location == null) {
|
||||
callsign_location = {};
|
||||
}
|
||||
console.log(callsign_list);
|
||||
console.log(message_list);
|
||||
console.log(callsign_location);
|
||||
|
||||
// Now loop through each callsign and add the tabs
|
||||
first_callsign = null;
|
||||
@ -245,19 +292,34 @@ function create_callsign_tab(callsign, active=false) {
|
||||
tab_id_li = tab_li_string(callsign);
|
||||
tab_notify_id = tab_notification_id(callsign);
|
||||
tab_content = tab_content_name(callsign);
|
||||
popover_id = callsign_location_popover(callsign);
|
||||
if (active) {
|
||||
active_str = "active";
|
||||
} else {
|
||||
active_str = "";
|
||||
}
|
||||
|
||||
location_str = 'No Location Information';
|
||||
location_class = 'visually-hidden';
|
||||
if (callsign in callsign_location) {
|
||||
location_str = build_location_string(callsign_location[callsign]);
|
||||
location_class = '';
|
||||
}
|
||||
|
||||
item_html = '<li class="nav-item" role="presentation" callsign="'+callsign+'" id="'+tab_id_li+'">';
|
||||
//item_html += '<button onClick="callsign_select(\''+callsign+'\');" callsign="'+callsign+'" class="nav-link '+active_str+'" id="'+tab_id+'" data-bs-toggle="tab" data-bs-target="#'+tab_content+'" type="button" role="tab" aria-controls="'+callsign+'" aria-selected="true">';
|
||||
item_html += '<button onClick="callsign_select(\''+callsign+'\');" callsign="'+callsign+'" class="nav-link position-relative '+active_str+'" id="'+tab_id+'" data-bs-toggle="tab" data-bs-target="#'+tab_content+'" type="button" role="tab" aria-controls="'+callsign+'" aria-selected="true">';
|
||||
item_html += callsign+' ';
|
||||
|
||||
item_html += '<img id="'+popover_id+'" src="/static/images/globe.svg" ';
|
||||
item_html += 'alt="View location information" class="'+location_class+'" ';
|
||||
item_html += 'data-bs-original-title="APRS Location" data-bs-toggle="popover" data-bs-placement="top" '
|
||||
item_html += 'data-bs-trigger="hover" data-bs-content="'+location_str+'"> ';
|
||||
|
||||
item_html += '<span id="'+tab_notify_id+'" class="position-absolute top-0 start-80 translate-middle badge bg-danger border border-light rounded-pill visually-hidden">0</span>';
|
||||
item_html += '<span onclick="delete_tab(\''+callsign+'\');">×</span>';
|
||||
item_html += '</button></li>'
|
||||
|
||||
callsignTabs.append(item_html);
|
||||
create_callsign_tab_content(callsign, active);
|
||||
}
|
||||
@ -288,6 +350,7 @@ function delete_tab(callsign) {
|
||||
$(tab_content).remove();
|
||||
delete callsign_list[callsign];
|
||||
delete message_list[callsign];
|
||||
delete callsign_location[callsign];
|
||||
|
||||
// Now select the first tab
|
||||
first_tab = $("#msgsTabList").children().first().children().first();
|
||||
@ -382,11 +445,23 @@ function create_message_html(date, time, from, to, message, ack_id, msg, acked=f
|
||||
date_str = date + " " + time;
|
||||
sane_date_str = date_str.replace(/ /g,"").replaceAll("/","").replaceAll(":","");
|
||||
|
||||
bubble_msg_class = "bubble-message";
|
||||
if (ack_id) {
|
||||
bubble_arrow_class = "bubble-arrow alt";
|
||||
popover_placement = "left";
|
||||
} else {
|
||||
bubble_arrow_class = "bubble-arrow";
|
||||
popover_placement = "right";
|
||||
}
|
||||
|
||||
msg_html = '<div class="bubble-row'+alt+'">';
|
||||
msg_html += '<div id="'+bubble_msgid+'" class="'+ bubble_class + '" data-bs-toggle="popover" data-bs-content="'+msg['raw']+'">';
|
||||
msg_html += '<div id="'+bubble_msgid+'" class="'+ bubble_class + '" ';
|
||||
msg_html += 'title="APRS Raw Packet" data-bs-placement="'+popover_placement+'" data-bs-toggle="popover" ';
|
||||
msg_html += 'data-bs-trigger="hover" data-bs-content="'+msg['raw']+'">';
|
||||
msg_html += '<div class="bubble-text">';
|
||||
msg_html += '<p class="'+ bubble_name_class +'">'+from+' ';
|
||||
msg_html += '<span class="bubble-timestamp">'+date_str+'</span>';
|
||||
|
||||
if (ack_id) {
|
||||
if (acked) {
|
||||
msg_html += '<span class="material-symbols-rounded md-10" id="' + ack_id + '">thumb_up</span>';
|
||||
@ -395,24 +470,11 @@ function create_message_html(date, time, from, to, message, ack_id, msg, acked=f
|
||||
}
|
||||
}
|
||||
msg_html += "</p>";
|
||||
bubble_msg_class = "bubble-message"
|
||||
if (ack_id) {
|
||||
bubble_arrow_class = "bubble-arrow alt"
|
||||
popover_placement = "left"
|
||||
} else {
|
||||
bubble_arrow_class = "bubble-arrow"
|
||||
popover_placement = "right"
|
||||
}
|
||||
|
||||
msg_html += '<p class="' +bubble_msg_class+ '">'+message+'</p>';
|
||||
msg_html += '<div class="'+ bubble_arrow_class + '"></div>';
|
||||
msg_html += "</div></div></div>";
|
||||
|
||||
popover_html = '\n<script>$(function () {$(\'[data-bs-toggle="popover"]\').popover('
|
||||
popover_html += '{title: "APRS Raw Packet", html: false, trigger: \'hover\', placement: \''+popover_placement+'\'});})';
|
||||
popover_html += '</script>'
|
||||
|
||||
return msg_html+popover_html
|
||||
return msg_html
|
||||
}
|
||||
|
||||
function flash_message(msg) {
|
||||
|
@ -64,8 +64,12 @@
|
||||
to_call.val(callsign);
|
||||
selected_tab_callsign = callsign;
|
||||
});
|
||||
});
|
||||
|
||||
/*$('[data-bs-toggle="popover"]').popover(
|
||||
{html: true, animation: true}
|
||||
);*/
|
||||
reload_popovers();
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user