Getting GeoJSON for radio station coverage

Radio stations can produce cool looking coverage maps, like below:

Each area is close to circular. The variance is determined by ground topology. Here’s a close-up, from radio-locator.com:

As these are inherently round, they are computed as a series of points around a circle.

The points for all radio stations in the U.S are published by the FCC here:

Index of /Bureaus/MB/Databases/fm_service_contour_data/ (fcc.gov)

These are listed by two forms of application ID, but not call sign. They offer an API that is supposed to provide the radials by call sign, but it seemed to be down when I did this research.

If you want to filter to a smaller set of stations, the FCC site lets you filter by state. If you wanted to get all radio stations in Pennysylvania, you might want to pull neighboring states too, to find stations just over the border that broadcast into PA.

FM Query Broadcast Station Search | Federal Communications Commission (fcc.gov)

The data for radials is a pipe-delimited file. Some of the columns have extraneous trailing whitespace.

There are 360 columns containing the longitude/latitude values for the radials (note: backwards from what you’d expect – latitude/longitude). The entire list of points also needs to be reversed to follow the “right hand rule” in geojson polygons.

There are a few other oddities – you need to point the starting point of a polygon as the ending point, and the are some blank columns. The complete solution is below, and on github:

const papa = require("papaparse");
const fs = require('fs');
const helpers = require('@turf/helpers');
const concave = require('@turf/concave').default;

const readline = require('readline');

const items = [];
papa.parse(fs.createReadStream('../datasets/ny_pa_fm_radio.txt'), {
  header: false,
  complete: function(results, file) {
    const applications = {}
    const lms = {};
    results.data.map(
      (record) => {
        const callsign = record[1].trim();
        const frequency = record[2].trim();
        const city = record[10].trim();
        const state = record[11].trim();
        
        const lms_application_id = record[38].trim();
        const application_id = record[37].trim();
        if (callsign !== '-') {

          applications[application_id] = {callsign, frequency, city, state, lms_application_id, application_id};
          lms[lms_application_id] = {callsign, frequency, city, state, lms_application_id, application_id};
        }
      }
    );
           
    const rl = readline.createInterface({
      input: fs.createReadStream('../datasets/FM_service_contour_current.txt')
    });
       
    rl.on('line', (line) => {
      const fields = line.split('|');
      if (fields.length > 10) {
        const application_id = fields.shift().trim();
        const service = fields.shift().trim();
        const lms_application_id = fields.shift().trim();

        let allPoints = fields.map(
          field => field.split(",").map(x => parseFloat(x.trim())).reverse()
        ).filter(
          field => field.length == 2 && field[0] !== null
        );

        allPoints.shift();

        allPoints = allPoints.reverse();
        allPoints.push(allPoints[0]);
        
        const poly = { 
          "type":"Feature","properties": applications[application_id] || lms[lms_application_id],
          "geometry":{ 
              "type":"Polygon","coordinates":[
                  allPoints
              ]
            }
        };
           
        items.push(poly);
    });
  
    rl.on('close', () => {
      const collection = helpers.featureCollection(items);
      fs.writeFileSync("../public/static/radio.geojson", JSON.stringify(collection, null, 2));
    })
  }
});