Add simple map of stations #57
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -2,3 +2,5 @@
 | 
				
			|||||||
config.ini
 | 
					config.ini
 | 
				
			||||||
*.db
 | 
					*.db
 | 
				
			||||||
*.log
 | 
					*.log
 | 
				
			||||||
 | 
					/temp/*
 | 
				
			||||||
 | 
					*.pyc
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										87
									
								
								api_app.py
									
									
									
									
									
								
							
							
						
						
									
										87
									
								
								api_app.py
									
									
									
									
									
								
							@@ -85,6 +85,19 @@ def select_frames(conn, n, url_params):
 | 
				
			|||||||
    cur.execute(sql)
 | 
					    cur.execute(sql)
 | 
				
			||||||
    rows = cur.fetchall()
 | 
					    rows = cur.fetchall()
 | 
				
			||||||
    return rows
 | 
					    return rows
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					def select_stations(conn, n):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Query rows in the stations table
 | 
				
			||||||
 | 
					    :param conn: the Connection object
 | 
				
			||||||
 | 
					    :return:
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    cur = conn.cursor()
 | 
				
			||||||
 | 
					    sql = 'SELECT * FROM stations ORDER BY last_heard_unix DESC LIMIT {n}'.format(n=n)
 | 
				
			||||||
 | 
					    print(sql)
 | 
				
			||||||
 | 
					    cur.execute(sql)
 | 
				
			||||||
 | 
					    rows = cur.fetchall()
 | 
				
			||||||
 | 
					    return rows
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@api_app.route('/')
 | 
					@api_app.route('/')
 | 
				
			||||||
def index():
 | 
					def index():
 | 
				
			||||||
@@ -113,12 +126,82 @@ def index():
 | 
				
			|||||||
            station['time_ago'] = timeago.format(station['last_heard_unix'], datetime.datetime.now())
 | 
					            station['time_ago'] = timeago.format(station['last_heard_unix'], datetime.datetime.now())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Map stuff
 | 
				
			||||||
 | 
					    frames_locs = list(filter(lambda x: x['latitude'] != None, frames))
 | 
				
			||||||
 | 
					    # Make a GeoJSON
 | 
				
			||||||
 | 
					    geojs = json.dumps({
 | 
				
			||||||
 | 
					     "type": "FeatureCollection",
 | 
				
			||||||
 | 
					     "features":[
 | 
				
			||||||
 | 
					           {
 | 
				
			||||||
 | 
					                "type":"Feature",
 | 
				
			||||||
 | 
					                "geometry": {
 | 
				
			||||||
 | 
					                "type":"Point",
 | 
				
			||||||
 | 
					                "coordinates":[frame['longitude'], frame['latitude']],
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					                "properties":frame,
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					         } for frame in frames_locs
 | 
				
			||||||
 | 
					    ]  
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return render_template('index.html',
 | 
					    return render_template('index.html',
 | 
				
			||||||
                            station_call = config['Settings']['station_call'],
 | 
					                            station_call = config['Settings']['station_call'],
 | 
				
			||||||
                            station_lat = config['Settings']['station_lat'],
 | 
					                            station_lat = config['Settings']['station_lat'],
 | 
				
			||||||
                            station_lon = config['Settings']['station_lon'],
 | 
					                            station_lon = config['Settings']['station_lon'],
 | 
				
			||||||
                            frames = frames,
 | 
					                            frames = frames,
 | 
				
			||||||
                            stations = stations)
 | 
					                            stations = stations,
 | 
				
			||||||
 | 
					                            geojs = geojs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@api_app.route('/map')
 | 
				
			||||||
 | 
					def map():
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Get the default list of frames from the API
 | 
				
			||||||
 | 
					    frames = json.loads(requests.get(config['Settings']['base_url']+"/packets").text)['data']
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    frames_locs = list(filter(lambda x: x['latitude'] != None, frames))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Make a GeoJSON
 | 
				
			||||||
 | 
					    geojs = json.dumps({
 | 
				
			||||||
 | 
					     "type": "FeatureCollection",
 | 
				
			||||||
 | 
					     "features":[
 | 
				
			||||||
 | 
					           {
 | 
				
			||||||
 | 
					                "type":"Feature",
 | 
				
			||||||
 | 
					                "geometry": {
 | 
				
			||||||
 | 
					                "type":"Point",
 | 
				
			||||||
 | 
					                "coordinates":[frame['longitude'], frame['latitude']],
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					                "properties":frame,
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					         } for frame in frames_locs
 | 
				
			||||||
 | 
					    ]  
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Make markers for all the frames
 | 
				
			||||||
 | 
					    # id_counter = 0
 | 
				
			||||||
 | 
					    # markers = ''
 | 
				
			||||||
 | 
					    # marker_ids = []
 | 
				
			||||||
 | 
					    # for frame in frames:
 | 
				
			||||||
 | 
					    #     if frame['latitude'] != None:
 | 
				
			||||||
 | 
					    #         # Create unique ID for each marker
 | 
				
			||||||
 | 
					    #         idd = 'frame' + str(id_counter)
 | 
				
			||||||
 | 
					    #         id_counter += 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #         # Create each marker
 | 
				
			||||||
 | 
					    #         markers += "var {idd} = L.marker([{latitude}, {longitude}]);\
 | 
				
			||||||
 | 
					    #                     {idd}.addTo(map).bindTooltip('{from_ssid}', permanent=true).openTooltip();".format(idd=idd, latitude=frame['latitude'],\
 | 
				
			||||||
 | 
					    #                                                                                 longitude=frame['longitude'],
 | 
				
			||||||
 | 
					    #                                                                                 from_ssid=frame['from'],
 | 
				
			||||||
 | 
					    #                                                                                 created=frame['created'])
 | 
				
			||||||
 | 
					    #         # Try to make a list of markers for Leaflet, but not working
 | 
				
			||||||
 | 
					    #         marker_ids.append(idd)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return render_template('map.html',
 | 
				
			||||||
 | 
					                           station_lat = config['Settings']['station_lat'],
 | 
				
			||||||
 | 
					                           station_lon = config['Settings']['station_lon'],
 | 
				
			||||||
 | 
					                           station_call = config['Settings']['station_call'],
 | 
				
			||||||
 | 
					                           #markers = markers,
 | 
				
			||||||
 | 
					                           geojs = geojs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Packets(Resource):
 | 
					class Packets(Resource):
 | 
				
			||||||
    def get(self):
 | 
					    def get(self):
 | 
				
			||||||
@@ -145,7 +228,7 @@ class Stations(Resource):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        conn = get_db_connection()
 | 
					        conn = get_db_connection()
 | 
				
			||||||
        # Limit to number of records requested
 | 
					        # Limit to number of records requested
 | 
				
			||||||
        data = select_all_stations(conn)
 | 
					        data = select_stations(conn, n = n)
 | 
				
			||||||
        # Sort by created date, descending (https://stackoverflow.com/a/45266808)
 | 
					        # Sort by created date, descending (https://stackoverflow.com/a/45266808)
 | 
				
			||||||
        #data.sort(key=operator.itemgetter('created'), reverse=True)
 | 
					        #data.sort(key=operator.itemgetter('created'), reverse=True)
 | 
				
			||||||
        return {'data':data}, 200  # return data and 200 OK code
 | 
					        return {'data':data}, 200  # return data and 200 OK code
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										7
									
								
								aprs_tool.code-workspace
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								aprs_tool.code-workspace
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
						"folders": [
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								"path": "."
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -4,18 +4,79 @@
 | 
				
			|||||||
    <meta charset="UTF-8">
 | 
					    <meta charset="UTF-8">
 | 
				
			||||||
    <title>{{station_call}} Status</title>
 | 
					    <title>{{station_call}} Status</title>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <!-- Leaflet's CSS -->
 | 
				
			||||||
 | 
					  <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
 | 
				
			||||||
 | 
					  integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
 | 
				
			||||||
 | 
					  crossorigin=""/>
 | 
				
			||||||
 | 
					  <!-- Make sure you put this AFTER Leaflet's CSS -->
 | 
				
			||||||
 | 
					  <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
 | 
				
			||||||
 | 
					  integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
 | 
				
			||||||
 | 
					  crossorigin=""></script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <style>
 | 
					    <style>
 | 
				
			||||||
      table, th, td {
 | 
					      table, th, td {
 | 
				
			||||||
        border: 1px solid black;
 | 
					        border: 1px solid black;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					      #map { height: 250px; }
 | 
				
			||||||
 | 
					      .leaflet-tooltip.my-labels {
 | 
				
			||||||
 | 
					        background-color: transparent;
 | 
				
			||||||
 | 
					        border: transparent;
 | 
				
			||||||
 | 
					        box-shadow: none;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
    </style>
 | 
					    </style>
 | 
				
			||||||
</head>
 | 
					</head>
 | 
				
			||||||
<body>
 | 
					<body>
 | 
				
			||||||
<h1>{{station_call}} Status</h1>
 | 
					<div style="width: 100%; overflow: hidden;">
 | 
				
			||||||
Station location: {{station_lat}}, {{station_lon}}
 | 
					    <div style="width: 50%; float: left;">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<h2> About </h2>
 | 
					      <h1>{{station_call}} Status</h1>
 | 
				
			||||||
This is a work in progress. See <a href="https://amiok.net/gitea/W1CDN/aprs_tool">https://amiok.net/gitea/W1CDN/aprs_tool</a> for usage.
 | 
					      Station location: {{station_lat}}, {{station_lon}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <h2> About </h2>
 | 
				
			||||||
 | 
					      This is a work in progress. See <a href="https://amiok.net/gitea/W1CDN/aprs_tool">https://amiok.net/gitea/W1CDN/aprs_tool</a> for usage.
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					    <div style="margin-left: 50%;">
 | 
				
			||||||
 | 
					      <div id="map"></div>
 | 
				
			||||||
 | 
					        <script>
 | 
				
			||||||
 | 
					            var map = L.map('map').setView([{{station_lat}}, {{station_lon}}], 10);
 | 
				
			||||||
 | 
					            L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {attribution: '© <a href=\"https://www.openstreetmap.org/copyright\">OpenStreetMap</a> contributors'}).addTo(map);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            //{{markers|safe}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Show station location
 | 
				
			||||||
 | 
					            var station = L.marker([{{station_lat}}, {{station_lon}}]).addTo(map).bindTooltip('{{station_call}}', {permanent: true}).openTooltip();
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // Show GeoJSON of markers
 | 
				
			||||||
 | 
					            var group = L.geoJSON({{geojs|safe}},
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        style: function (feature) {
 | 
				
			||||||
 | 
					                            return {color: feature.properties.color};
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // group.bindTooltip(function (layer) {
 | 
				
			||||||
 | 
					            //                           return 'Object '+layer.feature.properties.object_name+' from '+layer.feature.properties.from;
 | 
				
			||||||
 | 
					            //                           }, {permanent: false}).openTooltip().addTo(map);
 | 
				
			||||||
 | 
					            // Hacked together from https://gis.stackexchange.com/a/246919
 | 
				
			||||||
 | 
					            var pointLayer = L.geoJSON(null, {
 | 
				
			||||||
 | 
					                                pointToLayer: function(feature,latlng){
 | 
				
			||||||
 | 
					                                  //(true condition) ? "true" : "false"
 | 
				
			||||||
 | 
					                                  label = (feature.properties.object_name === null) ? String(feature.properties.from) : String(feature.properties.object_name)
 | 
				
			||||||
 | 
					                                  //label = String('Object '+feature.properties.object_name+' from '+feature.properties.from) // Must convert to string, .bindTooltip can't use straight 'feature.properties.attribute'
 | 
				
			||||||
 | 
					                                  return new L.CircleMarker(latlng, {
 | 
				
			||||||
 | 
					                                    radius: 1,
 | 
				
			||||||
 | 
					                                  }).bindTooltip(label, {permanent: true, opacity: 0.7, className: "my-labels"}).openTooltip();
 | 
				
			||||||
 | 
					                                  }
 | 
				
			||||||
 | 
					                                });
 | 
				
			||||||
 | 
					            pointLayer.addData({{geojs|safe}});
 | 
				
			||||||
 | 
					            map.addLayer(pointLayer);
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Zoom to show all
 | 
				
			||||||
 | 
					            map.fitBounds(group.getBounds().pad(0.3));
 | 
				
			||||||
 | 
					        </script>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<h2> Recent RF Packets </h2>
 | 
					<h2> Recent RF Packets </h2>
 | 
				
			||||||
   <table>
 | 
					   <table>
 | 
				
			||||||
@@ -40,6 +101,8 @@ This is a work in progress. See <a href="https://amiok.net/gitea/W1CDN/aprs_tool
 | 
				
			|||||||
      {% endfor %}
 | 
					      {% endfor %}
 | 
				
			||||||
  </table>
 | 
					  </table>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<h2> Recent Stations </h2>
 | 
					<h2> Recent Stations </h2>
 | 
				
			||||||
<table>
 | 
					<table>
 | 
				
			||||||
  <tr>
 | 
					  <tr>
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										42
									
								
								templates/map.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								templates/map.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,42 @@
 | 
				
			|||||||
 | 
					<html>
 | 
				
			||||||
 | 
					<head>
 | 
				
			||||||
 | 
					    <!-- Leaflet's CSS -->
 | 
				
			||||||
 | 
					    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
 | 
				
			||||||
 | 
					     integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
 | 
				
			||||||
 | 
					     crossorigin=""/>
 | 
				
			||||||
 | 
					     <!-- Make sure you put this AFTER Leaflet's CSS -->
 | 
				
			||||||
 | 
					    <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
 | 
				
			||||||
 | 
					    integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
 | 
				
			||||||
 | 
					    crossorigin=""></script>
 | 
				
			||||||
 | 
					    <style>
 | 
				
			||||||
 | 
					        #map { height: 100%; }
 | 
				
			||||||
 | 
					    </style>
 | 
				
			||||||
 | 
					</head>
 | 
				
			||||||
 | 
					<body>
 | 
				
			||||||
 | 
					    <div id="map"></div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <script>
 | 
				
			||||||
 | 
					        var map = L.map('map').setView([{{station_lat}}, {{station_lon}}], 10);
 | 
				
			||||||
 | 
					        L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {attribution: '© <a href=\"https://www.openstreetmap.org/copyright\">OpenStreetMap</a> contributors'}).addTo(map);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        //{{markers|safe}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Show station location
 | 
				
			||||||
 | 
					        var station = L.marker([{{station_lat}}, {{station_lon}}]).addTo(map).bindTooltip('{{station_call}}', permanent=true).openTooltip();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Show GeoJSON of markers
 | 
				
			||||||
 | 
					        var group = L.geoJSON({{geojs|safe}},
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    style: function (feature) {
 | 
				
			||||||
 | 
					                        return {color: feature.properties.color};
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }).bindTooltip(function (layer) {
 | 
				
			||||||
 | 
					            return 'Object '+layer.feature.properties.object_name+' from '+layer.feature.properties.from;
 | 
				
			||||||
 | 
					            }, permanent=true).addTo(map);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Zoom to show all
 | 
				
			||||||
 | 
					        map.fitBounds(group.getBounds().pad(0.2));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    </script>
 | 
				
			||||||
 | 
					</body>
 | 
				
			||||||
 | 
					</html>
 | 
				
			||||||
							
								
								
									
										36
									
								
								test_db.py
									
									
									
									
									
								
							
							
						
						
									
										36
									
								
								test_db.py
									
									
									
									
									
								
							@@ -1,36 +0,0 @@
 | 
				
			|||||||
 | 
					 | 
				
			||||||
# Learn how to update database
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import sqlite3
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def get_db_connection():
 | 
					 | 
				
			||||||
    conn = sqlite3.connect('database.db')
 | 
					 | 
				
			||||||
    conn.row_factory = sqlite3.Row
 | 
					 | 
				
			||||||
    return conn
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
conn = get_db_connection()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Grab a random row from frames table and pretend it is new
 | 
					 | 
				
			||||||
cur = conn.cursor()
 | 
					 | 
				
			||||||
cur.execute("SELECT [from], id, created_unix FROM frames ORDER BY RANDOM() LIMIT 1;")
 | 
					 | 
				
			||||||
rows = cur.fetchall()
 | 
					 | 
				
			||||||
results = dict(rows[0])
 | 
					 | 
				
			||||||
values = ', '.join('"%s"' % w for w in results.values())
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Build query
 | 
					 | 
				
			||||||
# "from" is wrappedin [] because it is a reserved word and using '' doesn't work.
 | 
					 | 
				
			||||||
query3 = "INSERT INTO stations ([from], frames_id, last_heard_unix, count) \
 | 
					 | 
				
			||||||
VALUES("+values+", 1) \
 | 
					 | 
				
			||||||
ON CONFLICT([from]) \
 | 
					 | 
				
			||||||
DO UPDATE SET count = count + 1;"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# example https://stackoverflow.com/a/50718957/2152245
 | 
					 | 
				
			||||||
# query2 = "INSERT INTO stations ([from], frames_id, last_heard_unix, count) \
 | 
					 | 
				
			||||||
# VALUES('KC9TZN-8', 4068, 1687623864, 1) \
 | 
					 | 
				
			||||||
# ON CONFLICT([from]) \
 | 
					 | 
				
			||||||
# DO UPDATE SET count = count + 1;"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
conn.execute(query3)
 | 
					 | 
				
			||||||
conn.commit()
 | 
					 | 
				
			||||||
conn.close()
 | 
					 | 
				
			||||||
		Reference in New Issue
	
	Block a user