Visualizing historical ADS-B coverage: Overlaying past daily outline files on Leaflet

Hi everyone,

As you know, tar1090 normally displays the latest maximum coverage overwritten for the past 24 hours. To intuitively track how my reception coverage changes and accumulates over time, I put together a small, lightweight HTML viewer using Leaflet.

It automatically fetches and overlays past daily outline files simultaneously with semi-transparent cyan strokes. Here is how it looks in action (loaded with 4 daily files):

Just thought I’d share in case anyone else finds it useful for visualizing their historical coverage trends!

Best regards, Yukio

2 Likes

Update: I’ve updated the UI text to English so it’s easier to follow for everyone. Here is how it looks with more daily files loaded!

If anyone is interested in the HTML script, I’d be happy to share it here.

I am interested in this.
I am using SkyAware for the map, so I guess I need to install tar1090 ? Do I need readsb rather than dump1090-fa also? Thanks.

1 Like

Hi jimMerk2,

Thanks for your interest!

To answer your question, this script specifically relies on tar1090 because it utilizes the daily outline JSON files automatically generated and archived by tar1090_archive.

Regarding readsb vs dump1090-fa, tar1090 itself works great with readsb (and is usually packaged with it in popular setups like wiedehopf’s installation scripts), which also provides the historical outline archiving feature out of the box.

I’ll share the HTML script in my next reply so you can check out how it reads those files!

Best regards, Yukio

1 Like

This connects to readsb and will convert positions to degree and distance.

import asyncio, math
from haversine import haversine

local_position = (21.443234, 17.73423423)

def calculate_initial_compass_bearing(pointA, pointB):
    """
        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(pointA) != tuple) or (type(pointB) != tuple):
        raise TypeError("Only tuples are supported as arguments")

    lat1 = math.radians(pointA[0])
    lat2 = math.radians(pointB[0])

    diffLong = math.radians(pointB[1] - pointA[1])

    x = math.sin(diffLong) * math.cos(lat2)
    y = math.cos(lat1) * math.sin(lat2) - (math.sin(lat1)
                                       * math.cos(lat2) * math.cos(diffLong))

    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 handle_position(icoa, alt, lat, lon):
    dist = round(haversine(local_position, (lat,lon)),5)
    dir = round(calculate_initial_compass_bearing(local_position, (lat,lon)),5)
    print(f'{icoa},{alt},{lat},{lon},{dist},{dir}')

def handle_message(msg):
    if len(msg) != 22:
        return
    msg_id = int(msg[1])
    icoa = msg[4]
    match msg_id:
        case 3:
            try:
            	alt = int(msg[11])
            	lat = float(msg[14])
            	lon = float(msg[15])
            	handle_position(icoa, alt, lat, lon)
            except:
                return

async def tcp_echo_client():
    reader, writer = await asyncio.open_connection('localhost', 30003)
    while True:
        line = await reader.readline()
        line = line.decode('latin1').rstrip()
        msg = line.split(',')
        handle_message(msg)

asyncio.run(tcp_echo_client())

This will then convert above stuff to geojson that can you can plot in leaflet.

import csv,sys
from geojson import MultiPoint
from geojson import Polygon
import numpy as np

local_position = (52.3231232, 7.1345345)
black_list = ['406F7B', '48C121', '485E2F', '4CAE68', '7815FA', '78100A', '781093', '06A2E8', 'A31A76', '78158E', '406F77', '70C0F9', '485F82', '78100a', '80166C', '781003', '8966C3', '40731C', '70209E', '70C125', '4CADF4', '485785', '48C229', '4073CE', '406D70', '51408C', '78140C']

def print_points(rows):
    points = []
    for row in rows:
        points.append((float(row[3]), float(row[2])))
        print(MultiPoint(points))

def print_range(rows):
    resolution = 360
    max_range = np.zeros(shape=resolution)
    max_range_points = np.empty(shape=(resolution,2))

    for i in range(resolution):
        max_range_points[i] = local_position
    
    for row in rows:
        dist = float(row[4])
        dir = float(row[5])
        i = int(round((dir / 360.0) * resolution)) % resolution
        if ((dist < 500) and (max_range[i] < dist) and (row[0] not in black_list)):
            max_range[i] = dist
            max_range_points[i] = (float(row[2]), float(row[3]))
            #if (dist > 250):
            #    print(row[0])

    result = []
    for i in range(resolution):
        result.append((max_range_points[i][0], max_range_points[i][1]))
    print(Polygon(result))

with open(sys.argv[1], newline='') as csvfile:
    rows = csv.reader(csvfile, delimiter=',')
    if (sys.argv[2]=='-points'):
        print_points(rows)
    if (sys.argv[2]=='-range'):
        print_range(rows)

Something like this then:

// fetch the current 50 range polygon from the server
$.getJSON('./curr.json', function (data) {
          // and add it to the map
          var polygon = L.polygon(data.coordinates, {
                                  color: '#000000',
                                  weight: 1,
                                  fillOpacity: 0.1,
                                  opacity: 0.4
                                  }).addTo(map);
          });

Edit: Blacklist is there for planes that i do not want in the range plot mostly because they flew over a GPS-jammed area.

1 Like

Some script will do the rest.

#!/bin/bash
while true
do
../../code/python/bin/python parse_loc.py >> curr.txt
../../code/python/bin/python convert_to_geojson.py curr.txt -range > ../../code/web_server/public/curr.json
sleep 60
done

Outline depends on your style. Looks then something like this:

1 Like

Hi mgrone,

Thank you for sharing your Python scripts and the awesome visualization!

Calculating bearings and converting them into custom GeoJSON polygons to display on Leaflet is a really neat approach. Also, adding a blacklist for GPS-jammed flights is such a smart idea to keep the coverage outline clean and accurate.

It’s great to see different ways of approaching coverage tracking in the community. Thanks again for sharing!

Best regards, Yukio

You are welcome. I started about a little more than a year ago small.I just pasted that stuff from back then which i am still using when it comes to a range plot.

1 Like

I had no idea you could do it that way—what a neat approach! It’s really cool that you’re still using a script you built over a year ago.

Once you get the hang of it: Check out GitHub - mgrone/stream1090: Mode-S demodulation with CRC-based framing · GitHub works very well in high noise envoirements.

1 Like

Thanks for sharing the link to stream1090! High-noise environment handling sounds really fascinating. I’ll definitely check out your repository. Thanks again for the great insights!

How do you know planes that flew over a GPS-jammed area?
Also, wouldn’t that list need to be continually updated?

1 Like

GPS-jammed areas are not just jamming the planes GPS for a bit when it is going through there. You can have some British Airways flight that cut short by flying too much north of Istanbul, Turkey (Black sea) and carries this all the way up to the approach to Heathrow, UK. The plane is basically is overhead, while the GPS reports it to be 400 nm away. The script is for range plots, therefore you wanna get rid of those. Yes manual work.

1 Like

Interesting, I would think that once a plane is out of the area being jammed, GPS would start to work again. What would keep it in error after the jamming is stopped?

1 Like

I do not know how this works. However, i had plenty of “outliers” and all that they had common was that they were flying over previously jammed areas.

1 Like