/**
 * This program parses the U.S. FAA airports data base in its textual format and
 * generates a valid ACM scenery. Starts the program with the --help
 * option for more.
 * 
 * 
 * IMPLEMENTATION NOTES
 * 
 * NOTE 1. LOCATOR misalignment issue workaround.
 * The FAA DB reports the magnetic bearing of the locator as degrees with
 * apparent very high precision (one hundred of degree, ex: "123.45") and the
 * local magnetic deviation at a given date, but this latter with very low
 * precision (1 degree only, ex: "09E" which means a magnetic deviation of
 * +9 DEG).
 * The true bearing can be calculate adding the magnetic deviation to the
 * magnetic bearing so obtaining the nominal true bearing:
 * 
 * nominal_true_bearing = nominal_magnetic_bearing + nominal_mag_dev
 *                      = 123.45 + 9 = 132 +/- 1 DEG
 * 
 * This nominal true bearing has then a very poor precision of only 1 degree,
 * which means 32 m of error over a distance of 1 NM. Such an error is
 * acceptable only for non precision approaches (ILS types LDA, SDF), but we
 * expect at least a precision ten times better than that for precision
 * approaches.
 * To get a better precision, this program calculates the actual true bearing
 * of the locator as the direction of the line joining the served runway end
 * to the locator position. The heuristics is then the following:
 * 
 * 1. For non-precision ILS (LDA, SDF) use the nominal true bearing.
 * 
 * 2. For any other type of ILS, calculate the actual true bearing as stated
 * above; if the result differs less than a degree from the nominal true bearing
 * we assume our value is simply a more accurate value than the nominal one;
 * if the difference is greater, keeps the nominal true bearing assuming the
 * misalignment be a design feature of the installation to preserve but displays
 * a warning.
 * 
 * NOTE 2. Cope with current ILS record limitations.
 * In the real world an ILS consists of up to 3 antennas: locator, GS and DME.
 * Each antenna has its own location (latitude and longitude) and its own
 * elevation. Unfortunately, the current format of the ACM ILS record allows to
 * specify the location of the locator and GS and provides only one elevation
 * field; distance is calculated using locator location + available elevation;
 * landing point is calculated using GS location + available elevation.
 * We remedy as follows depending on the type of ILS:
 * 
 * 1. LOCATOR only ILS. No problem at all; the elevation field is set with the
 *    elevation of the locator.
 * 
 * 2. LOCATOR + GS. Elevation field is set with the GS elevation for precise
 *    touch down point simulation; actual locator altitude is usually very close
 *    and does not care too much anyway.
 * 
 * 3. LOCATOR + DME. ACM assumes the DME antenna coincides with the locator,
 *    which is already a source of error for precise distance measurement;
 *    to mitigate this, the elevation field is set with the actual DME elevation;
 *    actual locator altitude is usually very close and does not care too much
 *    anyway.
 * 
 * 4. LOCATOR + GS + DME. Just like LOCATOR + GS for precise touch down point.
 * 
 * Further complication: the elevation field is blank in many cases, so we
 * take the value available in the following order of preference:
 * GS, DME, LOCATOR. If no value available at all, zero is assumed and a
 * warning is displayed.
 * 
 * @file
 * @author Umberto Salsi <salsi@icosaedro.it>
 * @version $Date: 2018/09/23 13:05:36 $
 */
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <ctype.h>
#include <string.h>

#include "../../src/util/error.h"
#include "../../src/util/units.h"
#include "../../src/util/memory.h"
#include "../../src/util/hashtable.h"
#include "../../src/dis/dis/earth.h"

/**
 * State of the string pattern matcher. Our custom implementation of the regex
 * is simple, fast an portable across Windows an Linux.
 */
typedef struct {
	/**
	 * Next char to parse.  Each match_X() function increments this pointer
	 * whenever the parsing of X succeeded.
	 */
	char *c;
} match_Type;


/**
 * Initializes the string pattern matcher.
 * @param this
 * @param s Subject string to parse.
 */
static void match_init(match_Type *this, char *s)
{
	this->c = s;
}

/**
 * Matches the end of the subject string.
 * @param this
 * @return True if currently at the end of the subject string.
 */
static int match_end(match_Type *this)
{
	return *this->c == 0;
}

/**
 * Matches a char.
 * @param this
 * @param c
 * @return 
 */
static int match_char(match_Type *this, char c)
{
	if( *this->c == c ){
		this->c++;
		return 1;
	} else {
		return 0;
	}
}

/**
 * Matches a set of chars.
 * @param this
 * @param set Set of chars.
 * @return 
 */
static int match_set(match_Type *this, char *set)
{
	while( *set != 0 && *set != *this->c )
		set++;
	if( *set != 0 ){
		this->c++;
		return 1;
	} else {
		return 0;
	}
}

/**
 * Matches a capital letter.
 * @param this
 * @return 
 */
static int match_capital(match_Type *this)
{
	if( 'A' <= *this->c && *this->c <= 'Z' ){
		this->c++;
		return 1;
	} else {
		return 0;
	}
}

/**
 * Matches a digit.
 * @param this
 * @return 
 */
static int match_digit(match_Type *this)
{
	if( '0' <= *this->c && *this->c <= '9' ){
		this->c++;
		return 1;
	} else {
		return 0;
	}
}

/**
 * Matches digits.
 * @param this
 * @param min Minimum number of digits to parse.
 * @param max Maximum number of digits to parse.
 * @return 
 */
static int match_digits(match_Type *this, int min, int max)
{
	char *back = this->c;
	int i = 0;
	while( i < max ){
		if( ! match_digit(this) ){
			if( i < min ){
				this->c = back;
				return 0;
			} else {
				return 1;
			}
		}
		i++;
	}
	return 1;
}


/**
 * Matches one or more digits and a possible decimal part. Note that the number
 * must start with a digit, so ".5" isn't valid.
 */
static int match_decimal(match_Type *this)
{
	char *back = this->c;
	if( match_digits(this, 1, 9)
	&& ((match_char(this, '.') && match_digits(this, 1, 9)) || 1) ){
		return 1;
	} else {
		this->c = back;
		return 0;
	}
}


/**
 * Returns true if the string is a decimal number. Note that the number
 * must start with a digit, so ".5" isn't valid.
 */
static int isDecimal(char *s)
{
	match_Type m;
	match_init(&m, s);
	return match_decimal(&m)
	&& match_end(&m);
}

/**
 * Returns true if the string is a decimal number with possible leading sign.
 * Note that the number must have at least one digit in its integral part, so
 * "+.5" isn't valid (rationale: the standard atof() function does not support
 * this abbreviated format).
 */
static int isSignedDecimal(char *s)
{
	match_Type m;
	match_init(&m, s);
	return (match_set(&m, "+-") || 1)
	&& match_decimal(&m)
	&& match_end(&m);
}

/**
 * Returns true if the string is a valid latitude. Ex.: "12-34-56.78N".
 * FIXME: does not check range.
 */
static int isLatitude(char *s)
{
	match_Type m;
	match_init(&m, s);
	return match_digits(&m, 1, 2)
	&& match_char(&m, '-')
	&& match_digits(&m, 1, 2)
	&& match_char(&m, '-')
	&& match_digits(&m, 1, 3)
	&& ( ! match_char(&m, '.') || match_digits(&m, 1, 4) )
	&& match_set(&m, "NS")
	&& match_end(&m);
}

/**
 * Returns true if the string is a valid longitude. Ex.: "12-34-56.78E".
 * FIXME: does not check range.
 */
static int isLongitude(char *s)
{
	match_Type m;
	match_init(&m, s);
	return match_digits(&m, 1, 3)
	&& match_char(&m, '-')
	&& match_digits(&m, 1, 2)
	&& match_char(&m, '-')
	&& match_digits(&m, 1, 3)
	&& ( ! match_char(&m, '.') || match_digits(&m, 1, 4) )
	&& match_set(&m, "WE")
	&& match_end(&m);
}

/**
 * Matches a runway end identifier. Valid identifiers must contain 1 or 2 digits
 * and a possible letter among "LRCX"; the "X" letter seems used for LDA ILS only.
 * @param this
 * @return 
 */
static int match_runwayEndID(match_Type *this)
{
	return match_digit(this)
	&& (match_digit(this) || 1)
	&& (match_set(this, "LRCX") || 1);
}


/**
 * Returns true if the string is a valid runway end identifier.
 * @param s
 * @return 
 */
static int isRunwayEndID(char *s)
{
	match_Type m;
	match_init(&m, s);
	return match_runwayEndID(&m)
	&& match_end(&m);
}

/**
 * Returns true if the string is a valid runway identifier. Valid
 * identifiers must contain two valid runway end IDs separated by "/".
 * @param s
 * @return 
 */
static int isRunwayID(char *s)
{
	match_Type m;
	match_init(&m, s);
	return match_runwayEndID(&m)
	&& match_char(&m, '/')
	&& match_runwayEndID(&m)
	&& match_end(&m);
}

/**
 * Returns true if the string contains a valid ILS identifier.
 */
static int isILSID(char *s)
{
	match_Type m;
	match_init(&m, s);
	return match_char(&m, 'I')
	&& match_char(&m, '-')
	&& match_capital(&m) && match_capital(&m) && match_capital(&m)
	&& match_end(&m);
}


/*
 * Range of latitude and longitude of the items we are interested to extract.
 * The value must be greater or equal to the minimum, and strictly less than the
 * maximum.
 * For runways, the base end is compared.
 * For ILS, the position of the localized antenna is compared.
 * For NDB and VOR with associated DME, the location of the NDB or VOR antenna
 * is compared.
 */
static double lat_min = units_DEGtoRAD(-90);
static double lat_max = units_DEGtoRAD(91);
static double lon_min = units_DEGtoRAD(-180);
static double lon_max = units_DEGtoRAD(181);

/*
 * State of the input file we are parsing. Since we parse one file at a time,
 * one only set of global variables make this program simpler. Moreover,
 * the program abort at any error, and this further simplifies things.
 */
static char line[9999]; // current line to parse
static int line_len; // length of the current line
static int line_no; // no. of the current line, first is no. 1
static char *in_filename; // filename of the current file
static FILE *in; // current file

/**
 * Opens the file to parse. Aborts on error.
 * @param filename
 */
static void openFile(char *filename)
{
	in_filename = filename;
	in = fopen(in_filename, "r");
	if( in == NULL )
		error_system("opening %s", in_filename);
	line_no = 0;
	line[0] = 0;
}


/**
 * Reads the next line and makes it available in global variables.
 * Aborts on error.
 * @return True if a new line is available, false at end of the file.
 */
static int readLine()
{
	if( fgets(line, sizeof(line), in) == NULL ){
		if( feof(in) ){
			line[0] = 0;
			return 0;
		} else {
			error_external("%s:%d: file read error", in_filename, line_no);
			return 0; // prevents gcc -Wall complain
		}
	} else {
		line_no++;
		line_len = strlen(line);
		// Removes trailing "\r\n" or "\n":
		if( line_len > 0 && line[line_len-1] == '\n' ){
			line[line_len-1] = 0;
			line_len--;
			if( line_len > 0 && line[line_len-1] == '\r' ){
				line[line_len-1] = 0;
				line_len--;
			}
		}
		return 1;
	}
}


/**
 * Extracts a field from the current line. Aborts if the current line is too
 * short. Aborts if the destination field cannot contain the source field and
 * the trailing NUL byte even after leading and trailing white space have been
 * removed.
 * @param start Byte offset of the beginning.
 * @param len Length of the field.
 * @param field Destination of the field; leading and trailing white spaces are
 * removed and a trailing NUL byte is added.
 * @param field_capacity Capacity of the destination field, including terminating
 * NUL byte.
 */
static void getField(int start, int len, char *field, int field_capacity)
{
	assert( len + 1 <= field_capacity );
	if( start + len > line_len )
		error_external("%s:%d: cannot parse field at offset %d length %d because line input length too short: %d", in_filename, line_no, start, len, line_len);
	int a = start;
	int b = start + len;
	while( a < b && isspace(line[a]) )
		a++;
	while( b > a && isspace(line[b-1]) )
		b--;
	int field_len = b - a;
	if( field_len > 0 )
		memcpy(field, line + a, field_len);
	field[field_len] = 0;
}


static int getLatitudeField(int start, int len, char *field, int field_capacity)
{
	getField(start, len, field, field_capacity);
	if( isLatitude(field) ){
		return 1;
	} else {
		fprintf(stderr, "%s:%d: invalid latitude: %s\n",
			in_filename, line_no, field);
		return 0;
	}
}


static int getLongitudeField(int start, int len, char *field, int field_capacity)
{
	getField(start, len, field, field_capacity);
	if( isLongitude(field) ){
		return 1;
	} else {
		fprintf(stderr, "%s:%d: invalid longitude: %s\n",
			in_filename, line_no, field);
		return 0;
	}
}


/**
 * Normalize angle to the canonical range [0,2*M_PI[.
 */
static double normalizeAngle(double a)
{
	if( ! isfinite(a) )
		error_internal("cannot normalize non-finite angle: %g", a);
	if(a <= -2*M_PI)
		a = a + 2*M_PI*floor(-a / (2*M_PI));
	else if(a >= 2*M_PI)
		a = a - 2*M_PI*floor(a / (2*M_PI));
	if( a < 0 )
		a = a + 2*M_PI;
	else if( a >= 2*M_PI )
		a = a - 2*M_PI;
	return a;
}


// ------------------------ RWY ----------------------------


typedef struct {
	char pk[12]; // airport primary key of the data base
	char id[29];
	char type[14];
	char elev[8];
} Airport;


/**
 * Maps airport's pk to the allocated Airport.
 */
static hashtable_Type *airports;

/**
 * Runway end. The airport and end ID are the key of the entry.
 * Used to calculate the actual true bearing of the locator (see implementation
 * note 1).
 */
typedef struct {
	char pk[12]; // airport primary key of the data base
	char id[4]; // runway end ID
	double lat;
	double lon;
} RunwayEnd;

/** Maps airport pk + runway end ID to runway end. */
static hashtable_Type *runwayends;

/** Get hash of runway end callback. */
static int getHashOfRunwayEnd(void *rwyend_)
{
	RunwayEnd *rwyend = (RunwayEnd *) rwyend_;
	return hashtable_getHashOfString(rwyend->pk)*3
		+ hashtable_getHashOfString(rwyend->id);
}

/** Compare runway ends callback. */
static int equalRunwayEnd(void *a_, void *b_)
{
	RunwayEnd *a = (RunwayEnd *) a_;
	RunwayEnd *b = (RunwayEnd *) b_;
	return strcmp(a->pk, b->pk) == 0 && strcmp(a->id, b->id) == 0;
}

/** Add runway end to hash table. */
static void addRunwayEnd(char *airport_pk, char *id, double lat, double lon)
{
	RunwayEnd *rwyend = memory_allocate(sizeof(RunwayEnd), NULL);
	memory_strcpy(rwyend->pk, sizeof(rwyend->pk), airport_pk);
	memory_strcpy(rwyend->id, sizeof(rwyend->id), id);
	rwyend->lat = lat;
	rwyend->lon = lon;
	if( hashtable_get(runwayends, rwyend) != NULL )
		fprintf(stderr, "%s:%d: ERROR: duplicated runway end\n",
			in_filename, line_no);
	hashtable_put(runwayends, rwyend, rwyend);
}


/**
 * Parses the airports data base file "APT.txt" and send to standard output
 * the filtered  list of the RWY records. As a side effect, the global airports
 * array remains available, although currently not used elsewhere here.
 */
static void parseAPT(char *apt_filename)
{
	airports = hashtable_new(hashtable_getHashOfString, hashtable_equalStrings);
	
	runwayends = hashtable_new(getHashOfRunwayEnd, equalRunwayEnd);
	
	openFile(apt_filename);
	
	// Step 1: parse all airports. Generate airports hash table.
	while( readLine() ){
		if( ! memory_startsWith(line, "APT") )
			continue;
		Airport *airport = memory_allocate(sizeof(Airport), NULL);
		getField(3, 11, airport->pk, sizeof(airport->pk));
		getField(27, 4, airport->id, sizeof(airport->id));
		getField(14, 13, airport->type, sizeof(airport->type));
		getField(578, 7, airport->elev, sizeof(airport->elev));
		hashtable_put(airports, airport->pk, airport);
	}
	
	// Step 2: parse runways and runway ends. Generate runway ends hash table
	// and RWY output records.
	rewind(in);
	line_no = 0;
	while( readLine() ){
		if( ! memory_startsWith(line, "RWY") )
			continue;
		
		// RWY id:
		char id[8];
		getField(16, 7, id, sizeof(id));
		if( ! isRunwayID(id) ){
			// Silently discards helicopter, sea, balloon, ice runways.
			continue;
		}
		
		// RWY latitude 1:
		char lat1_text[16];
		getField(88, 15, lat1_text, sizeof(lat1_text));
		if( lat1_text[0] == 0 )
			continue;
		double lat1;
		if( ! earth_parseLatitude(lat1_text, &lat1) ){
			fprintf(stderr, "%s:%d: failed to parse RWY %s latitude: %s - ignore\n", in_filename, line_no, id, lat1_text);
			continue;
		}
		
		// RWY longitude 1:
		char lon1_text[16];
		getField(115, 15, lon1_text, sizeof(lon1_text));
		double lon1;
		if( ! earth_parseLongitude(lon1_text, &lon1) ){
			fprintf(stderr, "%s:%d: failed to parse RWY longitude: %s - ignore\n", in_filename, line_no, lon1_text);
			continue;
		}
		
		char pk[12];
		getField(3, 11, pk, sizeof(pk));
		Airport *airport = hashtable_get(airports, pk);
		if( airport == NULL ){
			fprintf(stderr, "%s:%d: RWY record refers to unknown airport primary key: %s - ignore\n", in_filename, line_no, pk);
			continue;
		}
		if( !(strcmp(airport->type, "AIRPORT") == 0 || strcmp(airport->type, "ULTRALIGHT") == 0) )
			continue;
		
		char len[6];
		getField(23, 5, len, sizeof(len));
		char width[5];
		getField(28, 4, width, sizeof(width));
		
		// RWY latitude 2:
		char lat2_text[16];
		if( ! getLatitudeField(310, 15, lat2_text, sizeof(lat2_text)) )
			continue;
		double lat2;
		if( ! earth_parseLatitude(lat2_text, &lat2) ){
			fprintf(stderr, "%s:%d: failed to parse RWY %s latitude: %s - ignore\n", in_filename, line_no, id, lat2_text);
			continue;
		}
		
		// RWY longitude 2:
		char lon2_text[16];
		getField(337, 15, lon2_text, sizeof(lon2_text));
		double lon2;
		if( ! earth_parseLongitude(lon2_text, &lon2) ){
			fprintf(stderr, "%s:%d: failed to parse RWY longitude: %s - ignore\n", in_filename, line_no, lon2_text);
			continue;
		}
		
		// Ignore runway if middle point lies outside the range:
		double center_lat = (lat1 + lat2)/2;
		double center_lon = (lon1 + lon2)/2;
		if( !(lat_min <= center_lat && center_lat < lat_max
		&& lon_min <= center_lon && center_lon < lon_max) )
			continue;
		
		char id1[4];
		getField(65, 3, id1, sizeof(id1));
		char id2[4];
		getField(287, 3, id2, sizeof(id2));
		
		addRunwayEnd(pk, id1, lat1, lon1);
		addRunwayEnd(pk, id2, lat2, lon2);
		
		printf("RWY %s %s %.0f %s %s %s %s %s %s\n",
			airport->id,
			id,
			atof(airport->elev), // rounding to integral foot
			len, width, lat1_text, lon1_text, lat2_text, lon2_text);
	}

	fclose(in);
}


// ------------------- ILS ---------------------------


/**
 * Key that univocally identifies a ILS. Informations about a single ILS are
 * split among several records: this key allows to rejoin these records
 * together.
 */
typedef struct {
	char airport_pk[12];
	char runwayEndId[4];
	char type[11];
} ILSKey;

/** Get ILS hash callback. */
static int getILSKeyHash(void *ilskey_)
{
	ILSKey *ilskey = (ILSKey *) ilskey_;
	return (hashtable_getHashOfString(ilskey->airport_pk) * 3
		+ hashtable_getHashOfString(ilskey->runwayEndId)) * 3
		+ hashtable_getHashOfString(ilskey->type);
}

/** Compare ILS key callback. */
static int equalILSKeys(void *a_, void *b_)
{
	ILSKey *a = (ILSKey *) a_;
	ILSKey *b = (ILSKey *) b_;
	return strcmp(a->airport_pk, b->airport_pk) == 0
	&& strcmp(a->runwayEndId, b->runwayEndId) == 0
	&& strcmp(a->type, b->type) == 0;
}


/**
 * Collected data about an ILS. Note that in the ILS record of out scenery
 * format there is only one elevation field available for all the antennas and
 * there are no fields for the location of the DME, then ACM shows the distance
 * from the LOCALIZER instead. Then, we collect all the elevations of all the
 * antennas and use the first one available among these (in order of preference):
 * 1. GS elevation, so the landing spot is at the planned distance from the
 *    runway threshold.
 * 2. DME elevation, so the displayed distance closely matches the expected value.
 * 3. LOCALIZER elevation as last resort.
 */
typedef struct {
	ILSKey key; // key shared by records referring to a specific ILS
	char airport_pk[12];
	char id[7]; // name of the ILS, typically "I-XXX"
	double nominal_true_bearing; // nominal true bearing
	
	// LOCALIZER data:
	int loc_line_no; // line no. in the file for error messages
	char loc_status[23];
	char loc_lat[15];
	char loc_lon[15];
	char loc_elev[8];
	char loc_freq[8];
	char loc_width[6];
	
	// GS data:
	int gs_line_no; // line no. in the file for error messages
	char gs_status[23];
	char gs_lat[15];
	char gs_lon[15];
	char gs_elev[8];
	char gs_angle[6];
	
	// DME data:
	char dme_elev[8];
} ILS;

/**
 * Dictionary that maps our ILS key into the allocated ILS.
 */
hashtable_Type *ilss;


/**
 * Parse the ILS key fields from the current line, searches the dictionary
 * for a matching ILS or allocates a new one if not found.
 * @return Allocated ILS data structure that matches the key, or NULL if
 * parsing failed.
 */
static ILS * parseILSKey()
{
	ILSKey key;
		
	getField(4, 11, key.airport_pk, sizeof(key.airport_pk));
	if( key.airport_pk[0] == 0 ){
		fprintf(stderr, "%s:%d: empty airport primary key\n", in_filename, line_no);
		return NULL;
	}
	
	getField(15, 3, key.runwayEndId, sizeof(key.runwayEndId));
	if( ! isRunwayEndID(key.runwayEndId) ){
		fprintf(stderr, "%s:%d: invalid runway end ID: %s\n", in_filename, line_no, key.runwayEndId);
		return NULL;
	}
	
	getField(18, 10, key.type, sizeof(key.type));
	if( key.type[0] == 0 ){
		fprintf(stderr, "%s:%d: empty airport type\n", in_filename, line_no);
		return NULL;
	}
	
	ILS *ils = hashtable_get(ilss, &key);
	if( ils == NULL ){
		ils = memory_allocate(sizeof(ILS), NULL);
		memset(ils, 0, sizeof(*ils));
		ils->key = key;
		hashtable_put(ilss, &ils->key, ils);
	}
	return ils;
}


/**
 * Parse magnetic bearing and magnetic variation and returns the true bearing.
 * @param bearing_mag_deg Ex. "012".
 * @param mag_var Ex. "09E".
 * @return True bearing in [0,2*M_PI[, or -1 if parsing failed.
 */
static double computeTrueBearing(char *bearing_mag_deg, char *mag_var)
{
	// Parse bearing:
	match_Type m;
	match_init(&m, bearing_mag_deg);
	if( !( match_decimal(&m) && match_end(&m) ) )
		return -1;
	double bearing_true_deg = atof(bearing_mag_deg);
	
	// Parse magnetic variation:
	match_init(&m, mag_var);
	if( !(match_decimal(&m) && match_set(&m, "EW") && match_end(&m) ) )
		return -1;
	int mag_var_len = strlen(mag_var);
	char sign = mag_var[mag_var_len-1];
	
	// Calculate true bearing:
	mag_var[mag_var_len-1] = 0;
	if( sign == 'E' )
		bearing_true_deg += atof(mag_var);
	else
		bearing_true_deg -= atof(mag_var);
	mag_var[mag_var_len-1] = sign; // restore original
	
	return normalizeAngle( units_DEGtoRAD(bearing_true_deg) );
}


/**
 * Returns the true bearing going from (lat1,lon1) to (lat2,lon2).
 * Used to calculate the actual bearing from runway end to locator.
 */
static double trueBearing(double lat1, double lon1, double lat2, double lon2)
{
	double dlat = lat2 - lat1;
	double dlon = lon2 - lon1;
	return normalizeAngle( atan2(dlon * cos(lat1), dlat) );
}


static int isILSFreq(char *s)
{
	match_Type m;
	match_init(&m, s);
	if( !(match_digits(&m, 1, 3) && (!match_char(&m, '.') || match_digits(&m, 1, 2)) && match_end(&m)) )
		return 0;
	int freq = floor(atof(s) * 100 + 0.5); // tenth of KHz
	return 10800 <= freq && freq <= 11795;
}


static void parseILS(char *ils_filename)
{
	// Step 1: collect data for each specific ILS, spread among several records.
	ilss = hashtable_new(getILSKeyHash, equalILSKeys);
	openFile(ils_filename);
	while( readLine() ){
		
		if( memory_startsWith(line, "ILS1") ){
			ILS *ils = parseILSKey();
			if( ils == NULL )
				continue;
			
			getField(4, 11, ils->airport_pk, sizeof(ils->airport_pk));
			
			getField(28, 6, ils->id, sizeof(ils->id));
			if( ! isILSID(ils->id) ){
				fprintf(stderr, "%s:%d: invalid ILS ID '%s', check!\n",
					in_filename, line_no, ils->id);
			}
			
			char bearing_mag_deg[7];
			getField(281, 6, bearing_mag_deg, sizeof(bearing_mag_deg));
			char mag_var[7];
			getField(287, 3, mag_var, sizeof(mag_var));
			ils->nominal_true_bearing = computeTrueBearing(bearing_mag_deg, mag_var);
			if( ils->nominal_true_bearing < 0 ){
				fprintf(stderr, "%s:%d: invalid magnetic bearing ('%s') or magnetic variation ('%s') for ILS %s, check!\n",
					in_filename, line_no, bearing_mag_deg, mag_var, ils->id);
				ils->nominal_true_bearing = 0;
			}
			
		} else if( memory_startsWith(line, "ILS2") ){ // LOCALIZER
			ILS *ils = parseILSKey();
			if( ils == NULL )
				continue;
			ils->loc_line_no = line_no;
			getField(28, 22, ils->loc_status, sizeof(ils->loc_status));
			if( ! getLatitudeField(60, 14, ils->loc_lat, sizeof(ils->loc_lat)) )
				continue;
			if( ! getLongitudeField(85, 14, ils->loc_lon, sizeof(ils->loc_lon)) )
				continue;
			
			getField(126, 7, ils->loc_elev, sizeof(ils->loc_elev));
			if( ! isSignedDecimal(ils->loc_elev) ){
				fprintf(stderr, "%s:%d: invalid localizer elevation '%s' for ILS %s -- assuming zero\n",
					in_filename, line_no, ils->loc_elev, ils->id);
				strcpy(ils->loc_elev, "0");
			}
			
			getField(133, 7, ils->loc_freq, sizeof(ils->loc_freq));
			if( ! isDecimal(ils->loc_freq) ){
				fprintf(stderr, "%s:%d: invalid frequency '%s' for ILS %s -- assuming zero\n",
					in_filename, line_no, ils->loc_freq, ils->id);
				strcpy(ils->loc_freq, "0");
			}
			
			getField(155, 5, ils->loc_width, sizeof(ils->loc_width));
			if( ! isDecimal(ils->loc_width) ){
				if( *(ils->loc_width) != 0 )
					fprintf(stderr, "%s:%d: invalid beam width '%s' for ILS %s -- assuming 5.0\n",
						in_filename, line_no, ils->loc_width, ils->id);
				strcpy(ils->loc_width, "5.0");
			}
			
		} else if( memory_startsWith(line, "ILS3") ){ // GS
			ILS *ils = parseILSKey();
			if( ils == NULL )
				continue;
			ils->gs_line_no = line_no;
			getField(28, 22, ils->gs_status, sizeof(ils->gs_status));
			if( ! getLatitudeField(60, 14, ils->gs_lat, sizeof(ils->gs_lat)) )
				continue;
			if( ! getLongitudeField(85, 14, ils->gs_lon, sizeof(ils->gs_lon)) )
				continue;
			getField(126, 7, ils->gs_elev, sizeof(ils->gs_elev));
			if( ! isSignedDecimal(ils->gs_elev) ){
				fprintf(stderr, "%s:%d: invalid GS elevation '%s' for ILS %s -- assuming zero\n",
					in_filename, line_no, ils->gs_elev, ils->id);
				strcpy(ils->gs_elev, "0");
			}
			getField(148, 5, ils->gs_angle, sizeof(ils->gs_angle));
			
		} else if( memory_startsWith(line, "ILS4") ){ // DME
			ILS *ils = parseILSKey();
			if( ils == NULL )
				continue;
			getField(126, 7, ils->dme_elev, sizeof(ils->dme_elev));
			if( ! isSignedDecimal(ils->dme_elev) ){
				fprintf(stderr, "%s:%d: invalid DME elevation '%s' for ILS %s -- assuming zero\n",
					in_filename, line_no, ils->dme_elev, ils->id);
				strcpy(ils->dme_elev, "0");
			}
			
		} else if( memory_startsWith(line, "ILS5") ){
		} else if( memory_startsWith(line, "ILS6") ){
		} else {
			fprintf(stderr, "%s:%d: not a valid record ILS1...6 line -- ignore\n",
				in_filename, line_no);
		}
	}
	fclose(in);
	
	// Step 2: generate the output by filtering the wanted ILS.
	ILSKey *key;
	for(key = hashtable_firstKey(ilss); key != NULL; key = hashtable_nextKey(ilss)){
		
		ILS *ils = hashtable_get(ilss, key);
		
		if( ! memory_startsWith(ils->loc_status, "OPERATIONAL") )
			continue;
		
		double lat;
		if( ! earth_parseLatitude(ils->loc_lat, &lat) ){
			fprintf(stderr, "%s:%d: invalid latitude %s -- ignore\n",
				in_filename, ils->loc_line_no, ils->loc_lat);
			continue;
		}
		if( !(lat_min <= lat && lat < lat_max) )
			continue;
		
		double lon;
		if( ! earth_parseLongitude(ils->loc_lon, &lon) ){
			fprintf(stderr, "%s:%d: invalid longitude %s -- ignore\n",
				in_filename, ils->loc_line_no, ils->loc_lon);
			continue;
		}
		if( !(lon_min <= lon && lon < lon_max) )
			continue;

		if( ! isILSFreq(ils->loc_freq) ){
			fprintf(stderr, "%s:%d: %s %s: invalid frequency: %s -- ignore\n",
				in_filename, line_no, ils->key.type, ils->id, ils->loc_freq);
			continue;
		}
		
		/*
		 * If this ILS claims to have an operational GS, its type must match
		 * this fact, otherwise ACM would complain. Fix as needed.
		 */
		int has_gs = memory_startsWith(ils->gs_status, "OPERATIONAL");
		if( ! has_gs && (memory_startsWith(ils->key.type, "ILS") || strcmp(ils->key.type, "LOC/GS") == 0) ){
			fprintf(stderr, "%s:%d: GS not operational for ILS %s -- ignore\n",
				in_filename, ils->loc_line_no, ils->id);
			continue;
		}
		
		// Remove leading "I-" from ID:
		char id[7];
		memory_strcpy(id, sizeof(id), ils->id);
		if( memory_startsWith(id, "I-") ){
			memmove(id + 1, id + 2, strlen(id) -1);
		}
		
		// Chose elevation (see implementation note 2):
		char *elev_ptr;
		if( ils->gs_elev[0] != 0 )
			elev_ptr = ils->gs_elev;
		else if( ils->dme_elev[0] != 0 )
			elev_ptr = ils->dme_elev;
		else
			elev_ptr = ils->loc_elev;
		if( *elev_ptr == 0 ){
			fprintf(stderr, "%s: no elevation available for ILS %s -- using zero\n", in_filename, ils->id);
			elev_ptr = "0";
		}
		char elev[9];
		snprintf(elev, sizeof(elev), "%.0f", atof(elev_ptr));
		
		// Compute the actual true bearing and compare with the nominal true
		// bearing and decide which one to report (see implementation note 1).
		double true_bearing = ils->nominal_true_bearing;
		RunwayEnd rwyendkey;
		memory_strcpy(rwyendkey.pk, sizeof(rwyendkey.pk), ils->airport_pk);
		memory_strcpy(rwyendkey.id, sizeof(rwyendkey.id), ils->key.runwayEndId);
		RunwayEnd *rwyend = hashtable_get(runwayends, &rwyendkey);
		if( rwyend == NULL ){
			fprintf(stderr, "%s:%d: Warning: ILS %s, no runway end found for airport pk %s and runway end ID %s. Cannot calculate actual true bearing of the locator, using nominal.",
				in_filename, ils->loc_line_no, ils->id, ils->airport_pk, ils->key.runwayEndId);
		} else {
			
			int precision_approach = !( memory_startsWith(ils->key.type, "LDA")
					|| memory_startsWith(ils->key.type, "SDF") );
			
			// Compute actual geographic bearing:
			double actual_true_bearing = trueBearing(rwyend->lat, rwyend->lon,
				lat, lon);
			double delta = fabs( actual_true_bearing - true_bearing );
			if( delta > M_PI )
				delta = 2*M_PI - delta;
			
			// Which bearing to take?
			if( delta < units_DEGtoRAD(1) ){
				// Very small misalignment.
				if( precision_approach )
					true_bearing = actual_true_bearing;
				
			} else {
				// Quite large misalignment.
				if( precision_approach )
					fprintf(stderr, "%s:%d: Warning: ILS %s of type %s:\n"
						"\t- nominal true bearing is %03.2f DEG,\n"
						"\t- locator to runway end %s true bearing is %03.2f DEG,\n"
						"\t- delta %.2f DEG.\n",
						in_filename, ils->loc_line_no, ils->id, ils->key.type,
						units_RADtoDEG(ils->nominal_true_bearing),
						rwyend->id,
						units_RADtoDEG(actual_true_bearing),
						units_RADtoDEG(delta));
			}
		}
		
		printf("ILS %s %s %s %s %s %s %s %s %s %s %03.2f %s\n",
			ils->key.runwayEndId,
			ils->key.type,
			id,
			ils->loc_freq,
			ils->loc_lat,
			ils->loc_lon,
			has_gs? ils->gs_lat : "-",
			has_gs? ils->gs_lon : "-",
			elev,
			ils->loc_width,
			units_RADtoDEG(true_bearing),
			has_gs? ils->gs_angle : "-"
		);
	}
}



// --------------------- NAV ----------------------------------


/** Channel to frequency map. */
static char *channels[][2] = {
{"17X", "108.00"}, {"17Y", "108.05"}, {"18X", "108.10"}, {"18Y", "108.15"},
{"19X", "108.20"}, {"19Y", "108.25"}, {"20X", "108.30"}, {"20Y", "108.35"},
{"21X", "108.40"}, {"21Y", "108.45"}, {"22X", "108.50"}, {"22Y", "108.55"},
{"23X", "108.60"}, {"23Y", "108.65"}, {"24X", "108.70"}, {"24Y", "108.75"},
{"25X", "108.80"}, {"25Y", "108.85"}, {"26X", "108.90"}, {"26Y", "108.95"},
{"27X", "109.00"}, {"27Y", "109.05"}, {"28X", "109.10"}, {"28Y", "109.15"},
{"29X", "109.20"}, {"29Y", "109.25"}, {"30X", "109.30"}, {"30Y", "109.35"},
{"31X", "109.40"}, {"31Y", "109.45"}, {"32X", "109.50"}, {"32Y", "109.55"},
{"33X", "109.60"}, {"33Y", "109.65"}, {"34X", "109.70"}, {"34Y", "109.75"},
{"35X", "109.80"}, {"35Y", "109.85"}, {"36X", "109.90"}, {"36Y", "109.95"},
{"37X", "110.00"}, {"37Y", "110.05"}, {"38X", "110.10"}, {"38Y", "110.15"},
{"39X", "110.20"}, {"39Y", "110.25"}, {"40X", "110.30"}, {"40Y", "110.35"},
{"41X", "110.40"}, {"41Y", "110.45"}, {"42X", "110.50"}, {"42Y", "110.55"},
{"43X", "110.60"}, {"43Y", "110.65"}, {"44X", "110.70"}, {"44Y", "110.75"},
{"45X", "110.80"}, {"45Y", "110.85"}, {"46X", "110.90"}, {"46Y", "110.95"},
{"47X", "111.00"}, {"47Y", "111.05"}, {"48X", "111.10"}, {"48Y", "111.15"},
{"49X", "111.20"}, {"49Y", "111.25"}, {"50X", "111.30"}, {"50Y", "111.35"},
{"51X", "111.40"}, {"51Y", "111.45"}, {"52X", "111.50"}, {"52Y", "111.55"},
{"53X", "111.60"}, {"53Y", "111.65"}, {"54X", "111.70"}, {"54Y", "111.75"},
{"55X", "111.80"}, {"55Y", "111.85"}, {"56X", "111.90"}, {"56Y", "111.95"},
{"57X", "112.00"}, {"57Y", "112.05"}, {"58X", "112.10"}, {"58Y", "112.15"},
{"59X", "112.20"}, {"59Y", "112.25"}, {"70X", "112.30"}, {"70Y", "112.35"},
{"71X", "112.40"}, {"71Y", "112.45"}, {"72X", "112.50"}, {"72Y", "112.55"},
{"73X", "112.60"}, {"73Y", "112.65"}, {"74X", "112.70"}, {"74Y", "112.75"},
{"75X", "112.80"}, {"75Y", "112.85"}, {"76X", "112.90"}, {"76Y", "112.95"},
{"77X", "113.00"}, {"77Y", "113.05"}, {"78X", "113.10"}, {"78Y", "113.15"},
{"79X", "113.20"}, {"79Y", "113.25"}, {"80X", "113.30"}, {"80Y", "113.35"},
{"81X", "113.40"}, {"81Y", "113.45"}, {"82X", "113.50"}, {"82Y", "113.55"},
{"83X", "113.60"}, {"83Y", "113.65"}, {"84X", "113.70"}, {"84Y", "113.75"},
{"85X", "113.80"}, {"85Y", "113.85"}, {"86X", "113.90"}, {"86Y", "113.95"},
{"87X", "114.00"}, {"87Y", "114.05"}, {"88X", "114.10"}, {"88Y", "114.15"},
{"89X", "114.20"}, {"89Y", "114.25"}, {"90X", "114.30"}, {"90Y", "114.35"},
{"91X", "114.40"}, {"91Y", "114.45"}, {"92X", "114.50"}, {"92Y", "114.55"},
{"93X", "114.60"}, {"93Y", "114.65"}, {"94X", "114.70"}, {"94Y", "114.75"},
{"95X", "114.80"}, {"95Y", "114.85"}, {"96X", "114.90"}, {"96Y", "114.95"},
{"97X", "115.00"}, {"97Y", "115.05"}, {"98X", "115.10"}, {"98Y", "115.15"},
{"99X", "115.20"}, {"99Y", "115.25"}, {"100X", "115.30"}, {"100Y", "115.35"},
{"101X", "115.40"}, {"101Y", "115.45"}, {"102X", "115.50"}, {"102Y", "115.55"},
{"103X", "115.60"}, {"103Y", "115.65"}, {"104X", "115.70"}, {"104Y", "115.75"},
{"105X", "115.80"}, {"105Y", "115.85"}, {"106X", "115.90"}, {"106Y", "115.95"},
{"107X", "116.00"}, {"107Y", "116.05"}, {"108X", "116.10"}, {"108Y", "116.15"},
{"109X", "116.20"}, {"109Y", "116.25"}, {"110X", "116.30"}, {"110Y", "116.35"},
{"111X", "116.40"}, {"111Y", "116.45"}, {"112X", "116.50"}, {"112Y", "116.55"},
{"113X", "116.60"}, {"113Y", "116.65"}, {"114X", "116.70"}, {"114Y", "116.75"},
{"115X", "116.80"}, {"115Y", "116.85"}, {"116X", "116.90"}, {"116Y", "116.95"},
{"117X", "117.00"}, {"117Y", "117.05"}, {"118X", "117.10"}, {"118Y", "117.15"},
{"119X", "117.20"}, {"119Y", "117.25"}, {"120X", "117.30"}, {"120Y", "117.35"},
{"121X", "117.40"}, {"121Y", "117.45"}, {"122X", "117.50"}, {"122Y", "117.55"},
{"123X", "117.60"}, {"123Y", "117.65"}, {"124X", "117.70"}, {"124Y", "117.75"},
{"125X", "117.80"}, {"125Y", "117.85"}, {"126X", "117.90"}, {"126Y", "117.95"},
{NULL, NULL}
};


static int mapChannelToFrequency(char *channel, char *freq, int freq_capacity)
{
	// Skip leading zeros:
	while( *channel == '0' )
		channel++;
	int i = 0;
	while(channels[i][0] != NULL){
		if( strcmp(channels[i][0], channel) == 0 ){
			memory_strcpy(freq, freq_capacity, channels[i][1]);
			return 1;
		}
		i++;
	}
	return 0;
}


// VOR, VORTAC, DME.
static int isNAVFreq(char *s)
{
	match_Type m;
	match_init(&m, s);
	if( !(match_digits(&m, 1, 3) && (!match_char(&m, '.') || match_digits(&m, 1, 2)) && match_end(&m)) )
		return 0;
	int freq = floor(atof(s) * 100 + 0.5); // tenth of KHz
	return 10800 <= freq && freq <= 11795;
}


static int isNDBFreq(char *s)
{
	match_Type m;
	match_init(&m, s);
	if( !(match_digits(&m, 3, 3) && match_end(&m)) )
		return 0;
	int freq = atoi(s);
	return 200 <= freq && freq <= 529;
}


static void parseNAV(char *nav_filename)
{
	openFile(nav_filename);
	while( readLine() ){
		if( ! memory_startsWith(line, "NAV1") )
			continue;
		char type[21];
		getField(8, 20, type, sizeof(type));
		if( strcmp(type, "FAN MARKER") == 0 )
			// Unsure what they are, but no freq and no channel available;
			// currently only 6 remaining, but it seems are going to be removed.
			continue;
		char id[5];
		getField(28, 4, id, sizeof(id));
		char lat[15];
		if( ! getLatitudeField(371, 14, lat, sizeof(lat)) )
			continue;
		char lon[15];
		if( ! getLongitudeField(396, 14, lon, sizeof(lon)) )
			continue;
		char elev[8];
		getField(472, 7, elev, sizeof(elev));
		char freq[7];
		getField(533, 6, freq, sizeof(freq));
		char channel[5];
		getField(529, 4, channel, sizeof(channel));
	
		// True if have to add a dummy DME for some special type of NDB.
		int add_dme = 0;
		
		if( strcmp(type, "VORTAC") == 0
		|| strcmp(type, "VOR/DME") == 0
		|| strcmp(type, "NDB") == 0
		|| strcmp(type, "TACAN") == 0
		|| strcmp(type, "VOR") == 0
		|| strcmp(type, "DME") == 0
		){
			// ok
			
		} else if( strcmp(type, "FAN MARKER") == 0
		|| strcmp(type, "MARINE NDB") == 0
		|| strcmp(type, "UHF/NDB") == 0){
			strcpy(type, "NDB");
			
		} else if( strcmp(type, "CONSOLAN") == 0 
		|| strcmp(type, "VOT") == 0 ){
			// FAA test facility. Ignore.
			continue;
			
		} else if( strcmp(type, "MARINE NDB/DME") == 0
		|| strcmp(type, "NDB/DME") == 0 ){
			strcpy(type, "NDB");
			add_dme = 1;
			
		} else {
			fprintf(stderr, "%s:%d: unknown type of NAVAID: %s -- ignore\n",
				in_filename, line_no, type);
			continue;
		}
		
		double lat_rad;
		if( ! earth_parseLatitude(lat, &lat_rad) ){
			fprintf(stderr, "%s:%d: invalid latitude -- ignore\n", in_filename, line_no);
			continue;
		}
		if( !(lat_min <= lat_rad && lat_rad < lat_max) )
			continue;
		
		double lon_rad;
		if( ! earth_parseLongitude(lon, &lon_rad) ){
			fprintf(stderr, "%s:%d: invalid longitude -- ignore\n", in_filename, line_no);
			continue;
		}
		if( !(lon_min <= lon_rad && lon_rad < lon_max) )
			continue;
		
		// Elevation is missing on some NDB, but it does not care too much
		// when there is no associated DME.
		// Workaround:
		if( elev[0] == 0 ){
			if( ! add_dme && strcmp(type, "NDB") == 0 ){
				strcpy(elev, "0");
			} else {
				
				//fprintf(stderr, "%s:%d: elevation field is empty -- ignore\n",
				//	in_filename, line_no);
				//continue;
				
				// We currently do not support NDB/DME and report NDB only anyway,
				// so continue:
				strcpy(elev, "0");
			}
		}
		
		if( freq[0] == 0 ){
			if( channel[0] == 0 ){
				fprintf(stderr, "%s:%d: missing both frequency and channel -- ignore\n", in_filename, line_no);
				continue;
			} else {
				if( ! mapChannelToFrequency(channel, freq, sizeof(freq)) ){
					fprintf(stderr, "%s:%d: invalid channel: %s -- ignore\n",
						in_filename, line_no, channel);
					continue;
				}
			}
		}
		
		if( (memory_startsWith(type, "VOR") || memory_startsWith(type, "DME"))
		&& ! isNAVFreq(freq) ){
			fprintf(stderr, "%s:%d: %s %s: invalid frequency: %s -- ignore\n",
				in_filename, line_no, type, id, freq);
			continue;
		}
		
		if( strcmp(type, "NDB") == 0 && ! isNDBFreq(freq) ){
			fprintf(stderr, "%s:%d: %s %s: invalid frequency: %s -- ignore\n",
				in_filename, line_no, type, id, freq);
			continue;
		}
		
		char elev_rounded[9];
		snprintf(elev_rounded, sizeof(elev_rounded), "%.0f", atof(elev));
		
		printf("NAV %s %s %s %s %s %s %s\n",
			id, type, lat, lon, elev_rounded, freq,
			channel[0] == 0? "-" : channel);
		// FIXME: unsure what to do with NDB+DME: they cannot share the same freq!
		if( add_dme )
			fprintf(stderr, "%s:%d: Warning: unsupported DME associated to NDB -- reporting NDB only antenna instead.\n",
				in_filename, line_no);
	}
	
	fclose(in);
}


static void help()
{
	puts(
	"This program parses the U.S. FAA data base in its textual format and generates\n"
	"an ACM scenery. The FAA data base can be downloaded from\n"
	"https://nfdc.faa.gov/xwiki/bin/view/NFDC\n"
	"Download and decompress the APT.zip, ILS.zip and NAV.zip files in a folder,\n"
	"then start this program with the following command line parameters:\n"
	"\n"
	"   --lat-min DEG   Minimum latitude, default 90S.\n"
	"   --lat-max DEG   Maximum latitude, default 90N.\n"
	"   --lon-min DEG   Minimum longitude, default 180W.\n"
	"   --lon-max DEG   maximum longitude, default 180E.\n"
	"   --src-dir PATH  Path to the directory of the decompressed FAA data base,\n"
	"                   default . (current directory).\n"
	"\n"
	"The program collects all the runways, ILS and NAVAID in the given range of\n"
	"latitude and longitude and writes the scenery on standard output; errors and\n"
	"warnings are sent to standard error. Only the RWY, RWY2, ILS and NAV records\n"
	"are generated. You may want to add the TEAM1_LOC, TEAM2_LOC and GROUND_COLOR\n"
	"records to complete the scenery.\n"
	);
}


int main(int argc, char** argv)
{
	error_init(argv[0]);
	char *src_dir = ".";
	
	int i = 1;
	while( i < argc ){
		char *a = argv[i];
		char *v = i < argc? argv[i+1] : NULL;
		if( strcmp(a, "--help") == 0 || strcmp(a, "-h") == 0 ){
			help();
			return 0;
		} else if( strcmp(a, "--lat-min") == 0 && v != NULL ){
			if( ! earth_parseLatitude(v, &lat_min) )
				error_external("invalid latitude: %s", v);
			i += 2;
		} else if( strcmp(a, "--lat-max") == 0 && v != NULL ){
			if( ! earth_parseLatitude(v, &lat_max) )
				error_external("invalid latitude: %s", v);
			i += 2;
		} else if( strcmp(a, "--lon-min") == 0 && v != NULL ){
			if( ! earth_parseLongitude(v, &lon_min) )
				error_external("invalid longitude: %s", v);
			i += 2;
		} else if( strcmp(a, "--lon-max") == 0 && v != NULL ){
			if( ! earth_parseLongitude(v, &lon_max) )
				error_external("invalid longitude: %s", v);
			i += 2;
		} else if( strcmp(a, "--src-dir") == 0 && v != NULL ){
			src_dir = v;
			i += 2;
		} else {
			error_external("unknown option/value: %s", a);
		}
	}
	
	if( !(lat_min < lat_max && lon_min < lon_max) )
		error_external("invalid latitude [%g,%g[ DEG or longitude [%g,%g[ DEG ranges", units_RADtoDEG(lat_min), units_RADtoDEG(lat_max), units_RADtoDEG(lon_min), units_RADtoDEG(lon_max));
	
	char path[999];
	
	snprintf(path, sizeof(path), "%s/APT.txt", src_dir);
	parseAPT(path);
	
	snprintf(path, sizeof(path), "%s/ILS.txt", src_dir);
	parseILS(path);
	
	snprintf(path, sizeof(path), "%s/NAV.txt", src_dir);
	parseNAV(path);
}
