#!/usr/bin/env python import math, csv, os, time, traceback from contextlib import closing WGS84_A = 6378137.0 WGS84_F = 1.0/298.257223563; WGS84_B = WGS84_A * (1 - WGS84_F) WGS84_ECC_SQ = 1 - WGS84_B * WGS84_B / (WGS84_A * WGS84_A) WGS84_ECC = math.sqrt(WGS84_ECC_SQ) ABSOLUTE_MAXIMUM_RANGE = 500000.0 def dtor(d): return d * math.pi / 180.0 def rtod(r): return r * 180.0 / math.pi def ft_to_m(ft): return ft * 0.3048 def latlngup_to_ecef(l): "Converts L from WGS84 lat/long/up to ECEF" lat = dtor(l[0]) lng = dtor(l[1]) alt = l[2] slat = math.sin(lat) slng = math.sin(lng) clat = math.cos(lat) clng = math.cos(lng) d = math.sqrt(1 - (slat * slat * WGS84_ECC_SQ)) rn = WGS84_A / d x = (rn + alt) * clat * clng y = (rn + alt) * clat * slng z = (rn * (1 - WGS84_ECC_SQ) + alt) * slat return (x,y,z) def latlngup_to_relxyz(c,l): # this converts WGS84 (lat,lng,alt) to a rotated ECEF frame # where the earth center is still at the origin # but (clat,clng,calt) has been rotated to lie on the positive X axis clat,clng,calt = c llat,llng,lalt = l # rotate by -clng around Z to put C on the X/Z plane # (this is easy to do while still in WGS84 coordinates) llng = llng - clng # find angle between XY plane and C cx,cy,cz = latlngup_to_ecef((clat,0,calt)) a = math.atan2(cz,cx) # convert L to (rotated around Z) ECEF lx,ly,lz = latlngup_to_ecef((llat,llng,lalt)) # rotate by -a around Y to put C on the X axis asin = math.sin(-a) acos = math.cos(-a) rx = lx * acos - lz * asin rz = lx * asin + lz * acos return (rx, ly, rz) # calculate range, bearing, elevation from C to L def range_bearing_elevation(c,l): # rotate C onto X axis crx, cry, crz = latlngup_to_relxyz(c,c) # rotate L in the same way lrx, lry, lrz = latlngup_to_relxyz(c,l) # Now we have cartesian coordinates with C on # the X axis with ground plane YZ and north along +Z. dx, dy, dz = lrx-crx, lry-cry, lrz-crz rng = math.sqrt(dx*dx + dy*dy + dz*dz) bearing = (360 + 90 - rtod(math.atan2(dz,dy))) % 360 elev = rtod(math.asin(dx / rng)) return (rng, bearing, elev) class BinHisto: def __init__(self, n_bins, min_bin_value, max_bin_value): self.min_bin = min_bin_value self.bins = [0] * n_bins self.bins_unique = [0] * n_bins self.icao_seen = [set() for i in xrange(n_bins)] self.bin_size = float(max_bin_value - min_bin_value) / n_bins self.n = 0 self.min_value = None self.max_value = None def bin_start(self, n): return self.min_bin + n * self.bin_size def bin_end(self, n): return self.bin_start(n+1) def bin_for(self, v): return int((v-self.min_bin) / self.bin_size) def bin_for_upper(self, v): return int(math.ceil((v-self.min_bin) / self.bin_size)) def add(self,icao,v): i = self.bin_for(v) if i < 0 or i >= len(self.bins): return if icao not in self.icao_seen[i]: self.icao_seen[i].add(icao) self.bins_unique[i] += 1 self.bins[i] += 1 self.n += 1 self.max_value = v if self.max_value is None else max(self.max_value, v) self.min_value = v if self.min_value is None else min(self.min_value, v) def reset_icao_history(self): for s in self.icao_seen: s.clear() def values(self): return ( (self.bin_start(i), self.bin_end(i), self.bins[i], self.bins_unique[i]) for i in xrange(len(self.bins)) ) def write(self, filename): with closing(open(filename + '.new', 'w')) as w: c = csv.writer(w) c.writerow(['bin_start','bin_end','samples','unique']) for low, high, count, unique in self.values(): if unique: c.writerow(['%f' % low, '%f' % high, '%d' % count, '%d' % unique]) os.rename(filename + '.new', filename) def import_bin(self, low, high, count, unique): if count > 0: self.n += count self.min_value = min(self.min_value, low) self.max_value = max(self.max_value, high) firstbin = max(0, self.bin_for(low)) lastbin = min(len(self.bins), self.bin_for_upper(high)) for i in xrange(firstbin, lastbin): if low == high: break low_val = max(self.bin_start(i), low) high_val = min(self.bin_end(i), high) fraction = (high_val - low_val) / (high - low) frac_count = min(count, int(fraction * count + 0.5)) frac_unique = min(unique, int(fraction * unique + 0.5)) self.bins[i] += frac_count self.bins_unique[i] += frac_unique count -= frac_count unique -= frac_unique low = high_val def read(self, filename): with closing(open(filename, 'r')) as r: csvfile = csv.reader(r) csvfile.next() # skip header for row in csvfile: low = float(row[0]) high = float(row[1]) count = int(row[2]) unique = int(row[3]) self.import_bin(low, high, count, unique) class PolarHisto: def __init__(self, n_sectors, n_bins, min_value, max_value): self.sector_size = 360.0 / n_sectors self.sectors = [BinHisto(n_bins, min_value, max_value) for i in xrange(n_sectors)] self.n = 0 def sector_start(self,i): return self.sector_size * i def sector_end(self,i): return self.sector_size * (i+1) def sector_for(self,v): return int( (v % 360) / self.sector_size ) def sector_for_upper(self,v): return int(math.ceil((v % 360) / self.sector_size)) def reset_icao_history(self): for histo in self.sectors: histo.reset_icao_history() def add(self, icao, b, v): sector = self.sector_for(b) self.sectors[sector].add(icao, v) self.n += 1 def values(self): return ( (self.sector_start(i), self.sector_end(i), self.sectors[i]) for i in xrange(len(self.sectors)) ) def write(self, filename): with closing(open(filename + '.new', 'w')) as w: c = csv.writer(w) c.writerow(['bearing_start','bearing_end','bin_start','bin_end','samples','unique']) for b_low,b_high,histo in self.values(): # make sure we write at least one value per sector, # it makes things a little easier when plotting first = True for h_low,h_high,count,unique in histo.values(): if unique or first: c.writerow(['%f' % b_low, '%f' % b_high, '%f' % h_low, '%f' % h_high, '%d' % count, '%d' % unique]) first = False os.rename(filename + '.new', filename) def import_sector(self, b_low, b_high, h_low, h_high, count, unique): if count > 0: self.n += count firstsect = max(0, self.sector_for(b_low)) lastsect = min(len(self.sectors), self.sector_for_upper(b_high)) for i in xrange(firstsect, lastsect): if b_low == b_high: break low_val = max(self.sector_start(i), b_low) high_val = min(self.sector_end(i), b_high) fraction = (high_val - low_val) / (b_high - b_low) frac_count = min(count, int(fraction * count + 0.5)) frac_unique = min(unique, int(fraction * unique + 0.5)) self.sectors[i].import_bin(h_low, h_high, frac_count, frac_unique) count -= frac_count unique -= frac_unique b_low = high_val def read(self, filename): with closing(open(filename, 'r')) as r: csvfile = csv.reader(r) csvfile.next() # skip header for row in csvfile: b_low = float(row[0]) b_high = float(row[1]) h_low = float(row[2]) h_high = float(row[3]) if len(row) > 5: count = int(row[4]) unique = int(row[5]) else: # older format with only count/unique stored count = int( float(row[4]) * 10.0 ) unique = 10 self.import_sector(b_low, b_high, h_low, h_high, count, unique) def process_basestation_messages(home, f): count = 0 range_histo = BinHisto(110, 0, 440000) polar_range_histo = PolarHisto(120, 100, 0, 400000) polar_elev_histo = PolarHisto(120, 400, -15.0, 90.0) try: range_histo.read('range.csv') except: traceback.print_exc() try: polar_range_histo.read('polar_range.csv') except: traceback.print_exc() try: polar_elev_histo.read('polar_elev.csv') except: traceback.print_exc() last_save = last_reset = time.time() c = csv.reader(f, delimiter=',') for row in c: if row[0] != 'MSG': continue if row[1] != '3': continue try: icao = row[4] ts = row[8] + ' ' + row[9] alt = ft_to_m(float(row[11])) lat = float(row[14]) lng = float(row[15]) except: continue r,b,e = range_bearing_elevation(home, (lat,lng,alt)) if r > ABSOLUTE_MAXIMUM_RANGE: # bad data continue range_histo.add(icao, r) polar_range_histo.add(icao, b, r) polar_elev_histo.add(icao, b, e) now = time.time() if (now - last_save) > 30.0: range_histo.write('range.csv') polar_range_histo.write('polar_range.csv') polar_elev_histo.write('polar_elev.csv') last_save = now if (now - last_reset) > 5.0: range_histo.reset_icao_history() polar_range_histo.reset_icao_history() polar_elev_histo.reset_icao_history() last_reset = now if __name__ == '__main__': import sys home = (52.2, 0.1, 20) process_basestation_messages(home, sys.stdin)