/**
 * Navigation chart generator for ACM flight simulator. This program reads a scenery
 * file and generates on standard output the PostScript program that draws
 * a single A4 page based on the specified command line options.
 * External programs (ps2pdf) may be used to convert PostScript to PDF or
 * other formats.
 * Type "chart.exe --help" for the list of the available options.
 * 
 * @file
 * @author Umberto Salsi <salsi@icosaedro.it>
 * @version $Date: 2020/04/16 10:35:24 $
 */

#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <string.h>
#include <math.h>
#include <time.h>

#include "../../src/util/error.h"
#include "../../src/util/units.h"
#include "../../src/util/zulu.h"
#include "../../src/wmm/wmm.h"
#include "../../src/dis/dis/earth.h"
#include "ps.h"
#include "ratnest.h"

// State of the PostScript module helper.
static ps_Type *ps;

#define SECtoRAD(sec) ((sec) / 3600.0 / 180.0 * M_PI)

// Current date as Unix timestamp (s). Used to calculate the magnetic variation.
int curr_date_timestamp;

// Current date as year with fraction. Used to calculate the magnetic variation.
double curr_date_year;

static char *title = "";
static char *scene_file_name;
static FILE *scene_file;

// Current line no. of the scenery file.
static int scene_line_no;

// Current line read from the scenery file.
static char scene_line[999];

static char *wmm_cof_file_name = "../objects/WMM.COF";

// If ILSs must be drawn.
static int draw_ils = 1;

// Each record read from the scenery, split into fields.
#define FIELDS_MAX 20
static char *fields[FIELDS_MAX];
static int fields_no;

// Coords. of the central point and delta (RAD).
static double olat, olon, dlat, dlon;

// Denominator of the scale factor 1:scale. This value is related to dlat.
static int scale;

// Geographic coordinates at the sheet edges (RAD):
static double alat, alon, blat, blon;

// Sheet size (A4 portrait, pt);
static double hmax = 600.0;
static double vmax = 840.0;

// Sheet margin (pt).
static int margin = 35;

// Geographical radiants to pt factor.
static double LON_TO_PT;

// Geographical radiants to pt factor.
static double LAT_TO_PT;

// Map area of the sheet (pt). It occupied about 90% of the sheet space.
static double map_x1, map_y1, map_x2, map_y2;

/**
 * Label placement algorithm state used to place labels on free spaces of the
 * map area. Here we will collect all the occupied parts of the map while
 * drawing runways and NAVAIDs. Remaining free space will be available to
 * draw labels.
 */
static ratnest_Type *place;

// Forward declaration, see below.
typedef struct RunwayEnd RunwayEnd;

/**
 * Runway of an airport, including both ends. Ends are listed in the same order
 * of the scenery.
 */
typedef struct {

	/** Airport code. */
	char airportCode[8];

	/** Both runways IDs, example: "12L/30R". */
	char runwayIDs[8];
	
	double altitude_ft, length_ft, width_ft;

	/* Each end. */
	RunwayEnd *end1;
	RunwayEnd *end2;
} Runway;


/**
 * Specific runway end.
 */
typedef struct RunwayEnd {

	/** Runway this end belongs to. */
	Runway *runway;

	/** Name of this runway end, example: "12L". */
	char runwayEndID[8];

	/* Location of this end (RAD). */
	double lat, lon;

	/** The reciprocal end of the same runway. */
	struct RunwayEnd *reciprocalEnd;

} RunwayEnd;


/**
 * A label consisting of two rows of text inside a rectangular area.
 * Used for NAVAIDs.
 */
typedef struct {
	ratnest_Rect r;
	char line1[99];
	char line2[99];
} Label;

/*
 * Labels for NAVAIDs are collected here and drawn all together once the
 * occupied areas of the map are finally known. The "rat nest" module helps
 * to do this.
 */
static int labels_capacity;
static int labels_number;
static Label **labels;

/*
 * Runways are collected here. This list is used to find a matching runway end
 * for any given ILS. In fact, in the scenery file RWY records and ILS records
 * are not clearly related, so some guess is required.
 */
static int runways_capacity;
static int runways_number;
static Runway **runways;


/**
 * Adds a label entry to the list of labels. The rat nest module will be used
 * later to place these labels on free spaces. Only two short lines of text are
 * allowed.
 * @param r Bottom-left corner is the reference point this label is related to.
 * @param line1 Some text to display.
 * @param line2 Some text to display.
 */
static void addLabel( ratnest_Rect *r, char *line1, char *line2)
{
	Label *lbl = malloc(sizeof(Label));
	snprintf(lbl->line1, sizeof(lbl->line1), "%s", line1);
	snprintf(lbl->line2, sizeof(lbl->line2), "%s", line2);
	lbl->r = *r;

	if( labels_number >= labels_capacity ){
		labels_capacity += 10;
		labels = realloc(labels, sizeof(Label *)*labels_capacity);
	}
	labels[labels_number] = lbl;
	labels_number++;
}


/**
 * Adds a runway to the list of known runways. This list will be used to guess
 * the runway end related to any given ILS.
 * @param airportCode
 * @param runwayIDs Both ends IDS separated by slash, for example "12L/30R".
 * @param end1_lat Latitude of the first end, that is the "12L" runway (RAD).
 * @param end1_lon Longitude etc. (RAD).
 * @param end2_lat Latitude of the second end, that is the "30R" runway (RAD).
 * @param end2_lon Longitude etc. (RAD).
 * @param altitude_ft
 * @param length_ft
 * @param width_ft
 * @return Registered runway.
 */
static Runway * addRunway(char *airportCode, char *runwayIDs,
	double end1_lat, double end1_lon, double end2_lat, double end2_lon,
	double altitude_ft, double length_ft, double width_ft)
{
	Runway *rwy = malloc(sizeof(Runway));
	snprintf(rwy->airportCode, sizeof(rwy->airportCode), "%s", airportCode);
	snprintf(rwy->runwayIDs, sizeof(rwy->runwayIDs), "%s", runwayIDs);
	rwy->altitude_ft = altitude_ft;
	rwy->length_ft = length_ft;
	rwy->width_ft = width_ft;

	RunwayEnd *end1 = malloc(sizeof(RunwayEnd));
	end1->runway = rwy;
	end1->lat = end1_lat;
	end1->lon = end1_lon;
	rwy->end1 = end1;

	RunwayEnd *end2 = malloc(sizeof(RunwayEnd));
	end2->runway = rwy;
	end2->lat = end2_lat;
	end2->lon = end2_lon;
	rwy->end2 = end2;

	end1->reciprocalEnd = end2;
	end2->reciprocalEnd = end1;

	// Determine each end name:
	char *slash_ptr = strchr(runwayIDs, '/');
	if( slash_ptr == NULL ){
		fprintf(stderr, "%s:%d: missing '/' separator in runways identifiers: %s\n",
			scene_file_name, scene_line_no, runwayIDs);
		strcpy(end1->runwayEndID, "?");
		strcpy(end2->runwayEndID, "?");
	} else {
		// FIXME: here we assume IDs and ends are listed in the same order
		// which is reasonable, but we should check anyway.
		*slash_ptr = 0;
		snprintf(end1->runwayEndID, sizeof(end1->runwayEndID), "%s", runwayIDs);
		snprintf(end2->runwayEndID, sizeof(end2->runwayEndID), "%s", slash_ptr+1);
		*slash_ptr = '/'; // restore original string, just in case
	}

	// Add runway to the array:
	if( runways_number >= runways_capacity ){
		runways_capacity += 10;
		runways = realloc(runways, runways_capacity * sizeof(Runway *));
	}
	runways[runways_number] = rwy;
	runways_number++;
	return rwy;
}


static double sq(double x)
{
	return x * x;
}


/**
 * Returns the distance between two points given their latitude and longitude
 * only; ignore altitude and assumes the Earth be a perfect sphere.
 * @param lat1
 * @param lon1
 * @param lat2
 * @param lon2
 * @return Distance (m).
 */
static double bogusDist(double lat1, double lon1, double lat2, double lon2)
{
	double x1 = cos(lat1) * cos(lon1);
	double y1 = cos(lat1) * sin(lat1);
	double z1 = sin(lat1);
	double x2 = cos(lat2) * cos(lon2);
	double y2 = cos(lat2) * sin(lat2);
	double z2 = sin(lat2);
	return earth_MAJOR * sqrt(sq(x1-x2) + sq(y1-y2) + sq(z1-z2));
}


/**
 * Returns the runway end the LOCATOR is serving. Search is based on the
 * LOCATOR position and the served runway ID.
 * Only runway ends with the specified runway ID and not farther than 500 m
 * are considered.
 * It is important to note that the search is performed among the runways
 * registered until now.
 * @param rwyid Runway ID this LOCATOR is serving.
 * @param lat Latitude of the LOCATOR antenna (RAD).
 * @param lon Longitude of the LOCATOR antenna (RAD).
 * @return Served runway end, or NULL if nothing close enough found.
 */
static RunwayEnd *searchILSRunwayEnd(char *rwyid, double lat, double lon)
{
	int i;
	RunwayEnd *served = NULL;
	double served_dist = 0;
	for(i = 0; i < runways_number; i++){

		// Search a runway end whose reciprocal end ID matches rwyid and whose
		// distance from the LOCATOR is not to far.
		RunwayEnd *end = runways[i]->end1;
		if( strcmp(rwyid, end->reciprocalEnd->runwayEndID) == 0 ){
			double dist = bogusDist(lat, lon, end->lat, end->lon);
			if( served == NULL || dist < served_dist ){
				served = end->reciprocalEnd;
				served_dist = dist;
			}
		}

		end = runways[i]->end2;
		if( strcmp(rwyid, end->reciprocalEnd->runwayEndID) == 0 ){
			double dist = bogusDist(lat, lon, end->lat, end->lon);
			if( served == NULL || dist < served_dist ){
				served = end->reciprocalEnd;
				served_dist = dist;
			}
		}

	}

	if( runways_number > 0 && served_dist < 500 ){
		return served;
	} else {
		return NULL;
	}
}


static double parseLatitudeRad(char *s)
{
	double lat;

	if( earth_parseLatitude(s, &lat) ){
		return lat;
	} else {
		fprintf(stderr, "%s:%d: invalid latitude: %s\n",
			scene_file_name, scene_line_no, s);
		return 0;
	}
}


static double parseLongitudeRad(char *s)
{
	double lon;

	if( earth_parseLongitude(s, &lon) ){
		return lon;
	} else {
		fprintf(stderr, "%s:%d: invalid longitude: %s\n",
			scene_file_name, scene_line_no, s);
		return 0;
	}
}


/**
 * Returns the magnetic variation at the given location, at current set date.
 * @param lat Latitude (RAD).
 * @param lon Longitude (RAD).
 * @param alt Altitude over the WGS84 ellipsoid (m). It exact value does not
 * makes much difference, so setting zero here is generally fine.
 * @return Magnetic variation at the given location current time,
 * positive east (RAD).
 */
static double getMagneticVariationAt(double lat, double lon, double alt)
{
	wmm_MagneticField mf;
	wmm_getMagneticField(curr_date_year, lat, lon, alt, &mf);
	return mf.Decl;
}


/**
 * Split line of the scenery into fields at each space. Fill global fields[] and
 * set fields_no.
 * Lines beginning with '#' are comments and produces 0 fields.
 * @param line
 */
static void splitLineIntoFields(char *line)
{
	fields_no = 0;
	do {
		// find beginning next field:
		while( isspace(*line) )
			line++;
		if( *line == 0 )
			return;
		if( *line == '#' && fields_no == 0 )
			return;
		if( fields_no >= FIELDS_MAX ){
			fprintf(stderr, "%s:%d: too many fields, ignoring entire line\n",
				scene_file_name, scene_line_no);
			fields_no = 0;
			return;
		}
		fields[fields_no++] = line;
		// skip to end of the field:
		do {
			line++;
			if( *line == 0 )
				return;
			if( isspace(*line) ){
				*line = 0;
				line++;
				break;
			}
		}while(1);
	}while(1);
}


/**
 * Draws a generic mark, with circle and cross.
 * @param lat_rad
 * @param lon_rad
 * @param descr
 */
static void drawMark(double lat_rad, double lon_rad, char *descr)
{
	if ( lat_rad < alat || lat_rad > blat || lon_rad < alon || lon_rad > blon )
		return;

	double h = map_x1 + LON_TO_PT * (lon_rad - alon);
	double v = map_y1 + LAT_TO_PT * (lat_rad - alat);

	double r = 5;
	ps_setLineWidth(ps, 1);
	ps_setGray(ps, 0);
	ps_drawLine(ps, h - 2*r, v, h + 2*r, v);
	ps_drawLine(ps, h, v - 2*r, h, v + 2*r);
	ps_drawCircumference(ps, h, v, r);
	addLabel(&(ratnest_Rect){h, v, h + 75, v + 13}, "", descr);
}


/**
 * Draws a grid of parallels and meridians in the map area of the sheet.
 */
static void drawGrid()
{
	int degrees, primes;
	double v, h;
	int lato;
	char s[99];

	// Compute a suitable latitude step interval.
	int suitable_steps_sec[] = { 60, 2*60, 5*60, 10*60, 20*60, 60*60, 120*60, 0 };
	int coord_step_sec;
	int i = 0;
	do {
		coord_step_sec = suitable_steps_sec[i];
		if( LAT_TO_PT * SECtoRAD(coord_step_sec) > 72 )
			break;
		// Lines too close, try next interval.
		i++;
		if( suitable_steps_sec[i] == 0 )
			break;
	} while(1);

	ps_setLineWidth(ps, 0.3);

	int lat_sec = (int) (alat / M_PI * 180 * 3600) / coord_step_sec * coord_step_sec;
	double lat_rad = SECtoRAD(lat_sec);
	while (lat_rad < blat) {
		if( alat < lat_rad && lat_rad < blat ){
			v = map_y1 + LAT_TO_PT * (lat_rad - alat);
			ps_drawLine(ps, map_x1, v, map_x2, v);
			if (lat_sec >= 0) {
				lato = 'N';
				degrees = lat_sec / 3600;
				primes = (lat_sec / 60) % 60;
			} else {
				lato = 'S';
				degrees = -lat_sec / 3600;
				primes = (-lat_sec / 60) % 60;
			}
			ps_moveTo(ps, map_x1 + 10, v + 2);
			sprintf(s, "%d&degree; %02d' %c", degrees, primes, lato);
			ps_drawString(ps, s);
		}
		lat_sec = lat_sec + coord_step_sec;
		lat_rad = SECtoRAD(lat_sec);
	}

	int lon_sec = (int) (alon / M_PI * 180 * 3600) / coord_step_sec * coord_step_sec;
	double lon_rad = SECtoRAD(lon_sec);
	while (lon_rad < blon) {
		if( alon < lon_rad && lon_rad < blon ){
			h = map_x1 + LON_TO_PT * (lon_rad - alon);
			ps_drawLine(ps, h, map_y1, h, map_y2);
			if (lon_sec >= 0) {
				lato = 'E';
				degrees = lon_sec / 3600;
				primes = (lon_sec / 60) % 60;
			} else {
				lato = 'W';
				degrees = -lon_sec / 3600;
				primes = (-lon_sec / 60) % 60;
			}
			ps_moveTo(ps, h - 2, map_y1 + 10);
			printf("gsave 90 rotate\n");
			sprintf(s, "%d&degree; %02d' %c", degrees, primes, lato);
			ps_drawString(ps, s);
			printf("grestore  stroke\n");
		}
		lon_sec = lon_sec + coord_step_sec;
		lon_rad = SECtoRAD(lon_sec);
	}
}


// Size of the magnetic variation box.
#define BOX_W 58
#define BOX_H 60


/**
 * Draws a box with the magnetic variation calculated at the center of this chart,
 * assumed to be the average of the depicted area.
 * @param line
 */
static void drawMagneticVariationBox()
{
	ps_setFontHeight(ps, 7);
	double magnetic_variation = getMagneticVariationAt(olat, olon, 0);
	int x0 = hmax - margin - BOX_W;
	int y0 = vmax - margin - BOX_H;
	double r = BOX_H * 40.0 / 100;

	ps_drawFramedRect(ps, x0, y0, BOX_W, BOX_H);

	ps_setLineWidth(ps, 0.5);
	ps_moveTo(ps, x0 + BOX_W / 2, y0 + BOX_H - BOX_H * 10.0 / 100);
	ps_relativeLineTo(ps, 0, -r);
	double dx = r*sin(magnetic_variation);
	double dy = r*cos(magnetic_variation);
	ps_relativeLineTo(ps, dx, dy);
	if( magnetic_variation >= 0 ){
		dx = 0.3*r*sin(magnetic_variation + units_DEGtoRAD(160));
		dy = 0.3*r*cos(magnetic_variation + units_DEGtoRAD(160));
	} else {
		dx = 0.3*r*sin(magnetic_variation - units_DEGtoRAD(160));
		dy = 0.3*r*cos(magnetic_variation - units_DEGtoRAD(160));
	}
	ps_relativeLineTo(ps, dx, dy);
	ps_stroke(ps);

	char s[99];
	snprintf(s, sizeof(s), "TH = MH %c %.1f&degree;",
		magnetic_variation >= 0? '-' : '+',
		units_RADtoDEG(fabs(magnetic_variation))
	);
	ps_moveTo(ps, x0 + 3, y0 + 15);
	ps_drawString(ps, s);
	
	zulu_Date d;
	zulu_timestampToDate(curr_date_timestamp, &d);
	snprintf(s, sizeof(s), "at  %d-%02d-%02d", d.year, d.month, d.day);
	ps_moveTo(ps, x0 + 3, y0 + 5);
	ps_drawString(ps, s);
	
	
}


/**
 * Draws a runway.
 * @param rwy
 */
static void drawRunway(Runway *rwy)
{
	double y1 = rwy->end1->lat;
	double x1 = rwy->end1->lon;
	double y2 = rwy->end2->lat;
	double x2 = rwy->end2->lon;

	// Draw the runway if both ends are visible.
	if ( y1 < alat || y1 > blat || x1 < alon || x1 > blon )
		return;
	if ( y2 < alat || y2 > blat || x2 < alon || x2 > blon )
		return;

	double h1 = map_x1 + LON_TO_PT * (x1-alon);
	double v1 = map_y1 + LAT_TO_PT * (y1-alat);

	double h2 = map_x1 + LON_TO_PT * (x2-alon);
	double v2 = map_y1 + LAT_TO_PT * (y2-alat);

	double width_pt = LAT_TO_PT * rwy->width_ft / units_NmToFeetFactor / 60 / 180 * M_PI;
	if( width_pt < 3 )
		width_pt = 3;
	ps_setLineWidth(ps, width_pt);
	ps_setGray(ps, 0.5);
	ps_drawLine(ps, h1, v1, h2, v2);
	ps_setGray(ps, 0);
	ratnest_markSegment(place, h1, v1, h2, v2);
	
	// Place runway name and basic data.
	double h = (h1+h2)/2;
	double v = (v1+v2)/2;
	char line1[99];
	snprintf(line1, sizeof(line1), "%s  %s", rwy->airportCode, rwy->runwayIDs);
	char line2[99];
	snprintf(line2, sizeof(line2), "%.0f&multiply;%.0f ft   %.0f ft",
		rwy->length_ft, rwy->width_ft, rwy->altitude_ft);
	addLabel(&(ratnest_Rect){h, v, h + 75, v + 22}, line1, line2);
}


/**
 * Parse and draw RWY record.
 */
static void drawRWY()
{
	if( fields_no != 10 ){
		fprintf(stderr, "%s:%d: expected 10 fields in RWY but found %d\n",
			scene_file_name, scene_line_no, fields_no);
		return;
	}

	// Add all the known runways to the array, just in case we draw an ILS:
	double altitude_ft = atof(fields[3]);
	double length_ft = atof(fields[4]);
	double width_ft = atof(fields[5]);
	double y1 = parseLatitudeRad(fields[6]);
	double x1 = parseLongitudeRad(fields[7]);
	double y2 = parseLatitudeRad(fields[8]);
	double x2 = parseLongitudeRad(fields[9]);
	Runway *rwy = addRunway(fields[1], fields[2], y1, x1, y2, x2,
		altitude_ft, length_ft, width_ft);

	// Finally, draw:
	drawRunway(rwy);
}


/**
 * Parse and draw RWY2 record.
 */
static void drawRWY2()
{
	if( fields_no != 9 ){
		fprintf(stderr, "%s:%d: expected 9 fields in RWY2 but found %d\n",
			scene_file_name, scene_line_no, fields_no);
		return;
	}

	char *len_ft = fields[4];
	double altitude_ft = atof(fields[3]);
	double length_ft = atof(fields[4]);
	double width_ft = atof(fields[5]);
	double center_lat_rad = parseLatitudeRad( fields[6] );
	double center_lon_rad = parseLongitudeRad( fields[7] );
	double heading_rad = units_DEGtoRAD( atof( fields[8] ) );
	
	// Normalize heading range to [0,180[ so runway ends can be listed in
	// the correct order.
	if( !(0 <= heading_rad && heading_rad <= 2*M_PI) ){
		printf("%s:%d: runway heading out of the range: %s\n",
			scene_file_name, scene_line_no, fields[8]);
		return;
	}
	if( heading_rad >= M_PI )
		heading_rad -= M_PI;

	// FIXME: valid only for small lengths
	double half_len_m = 0.5 * units_FEETtoMETERS( atof( len_ft ) );
	double delta_lat_rad = half_len_m * cos(heading_rad) / earth_MAJOR;
	double delta_lon_rad = half_len_m * sin(heading_rad) / (earth_MAJOR * cos(center_lat_rad));

	// Add all the known runways to the array, just in case:
	Runway *rwy = addRunway(fields[1], fields[2],
		center_lat_rad - delta_lat_rad, center_lon_rad - delta_lon_rad,
		center_lat_rad + delta_lat_rad, center_lon_rad + delta_lon_rad,
		altitude_ft, length_ft, width_ft);

	// Finally, draw:
	drawRunway(rwy);
}


static void drawNDB(double h, double v)
{
	double r = 5;
	ps_setLineWidth(ps, 1);
	ps_drawCircumference(ps, h, v, r);
	ps_drawCircumference(ps, h, v, r/2);
	ratnest_markRect(place, &(ratnest_Rect){h-r, v-r, h+r, v+r});
}


static void drawDME(double h, double v)
{
	double r = 7;
	ps_setLineWidth(ps, 1);
	ps_drawRect(ps, h - r, v - r, 2*r, 2*r);
	ratnest_markRect(place, &(ratnest_Rect){h-r, v-r, h+r, v+r});
}


static void drawVOR(double h, double v)
{
	int r = 7;
	ps_setLineWidth(ps, 1);
	ps_moveTo(ps, h, v-r);
	ps_lineTo(ps, h+r, v-r/2);
	ps_lineTo(ps, h+r, v+r/2);
	ps_lineTo(ps, h, v+r);
	ps_lineTo(ps, h-r, v+r/2);
	ps_lineTo(ps, h-r, v-r/2);
	ps_lineTo(ps, h, v-r);
	ps_stroke(ps);
	ratnest_markRect(place, &(ratnest_Rect){h-r, v-r, h+r, v+r});
}


/**
 * Parse and draw NAV record.
 */
static void drawNAV()
{
	if( fields_no != 8 ){
		fprintf(stderr, "%s:%d: expected 8 fields in NAV record but found %d\n",
			scene_file_name, scene_line_no, fields_no);
		return;
	}

	char *id = fields[1];
	char *type = fields[2];
	char *vhf = fields[6];
	//char *alt = fields[5];

	double y = parseLatitudeRad(fields[3]);
	double x = parseLongitudeRad(fields[4]);
	if ( y < alat || y > blat || x < alon || x > blon )
		return;

	double h = map_x1 + LON_TO_PT * (x-alon);
	double v = map_y1 + LAT_TO_PT * (y-alat);
	
	char *freq_unit = NULL;

	if( strcmp(type, "VOR") == 0 ){
		drawVOR(h, v);
		freq_unit = "MHz";

	} else if( strcmp(type, "DME") == 0 ){
		drawDME(h, v);
		freq_unit = "MHz";

	} else if( strcmp(type, "VOR/DME") == 0 ){
		drawVOR(h, v);
		drawDME(h, v);
		freq_unit = "MHz";

	} else if( strcmp(type, "VORTAC") == 0 ){
		drawVOR(h, v);
		drawDME(h, v);
		freq_unit = "MHz";

	} else if( strcmp(type, "TACAN") == 0 ){
		drawVOR(h, v);
		drawDME(h, v);
		freq_unit = "MHz";

	} else if( strcmp(type, "NDB") == 0 ){
		drawNDB(h, v);
		freq_unit = "KHz";

	} else if( strcmp(type, "OMARKER") == 0 ){
		drawNDB(h, v);
		freq_unit = "KHz";

	} else if( strcmp(type, "OMARKER/COMLO") == 0 ){
		drawNDB(h, v);
		freq_unit = "KHz";

	} else if( strcmp(type, "MMARKER") == 0 ){
		drawNDB(h, v);
		freq_unit = "KHz";

	} else if( strcmp(type, "IMARKER") == 0 ){
		drawNDB(h, v);
		freq_unit = "KHz";

	} else {
		fprintf(stderr, "%s:%d: unexpected/unsupported NAVAID type %s\n",
			scene_file_name, scene_line_no, type);
		return;
	}
	
	char line1[99];
	snprintf(line1, sizeof(line1), "%s  %s", id, type);
	
	char line2[99];
	snprintf(line2, sizeof(line2), "%s %s", vhf, freq_unit);
	
	addLabel(&(ratnest_Rect){h, v, h+85, v+22}, line1, line2);
}


static void drawFeature()
{
	if( fields_no != 6 ){
		fprintf(stderr, "%s:%d: expected 6 fields in FEATURE record but found %d\n",
			scene_file_name, scene_line_no, fields_no);
		return;
	}

	drawMark(parseLatitudeRad(fields[2]), parseLongitudeRad(fields[3]), fields[1]);
}


static void drawTeamLoc()
{
	if( fields_no != 5 ){
		fprintf(stderr, "%s:%d: expected 5 fields in TEAM1_LOC record but found %d\n",
			scene_file_name, scene_line_no, fields_no);
		return;
	}

	drawMark(parseLatitudeRad(fields[1]), parseLongitudeRad(fields[2]), fields[0]);
}


/**
 * Returns the angle x (RAD) as readable, rounded DEG angle in the range [0,359].
 * @param x
 * @return
 */
static int normalizeAngle(double rad)
{
	int deg = (int)(units_RADtoDEG(rad) + 0.5);
	while(deg > 359)
		deg -= 360;
	while(deg < 0)
		deg += 360;
	return deg;
}


/**
 * Parse and drw ILS record.
 */
static void drawILS()
{
	if( ! draw_ils )
		return;
	
	if( fields_no != 13 ){
		fprintf(stderr, "%s:%d: expected 13 fields in ILS record but found %d\n",
			scene_file_name, scene_line_no, fields_no);
		return;
	}

	char *rwyid = fields[1];
	char *type = fields[2];
	char *id = fields[3];
	char *vhf = fields[4];
	char *alt = fields[9];

	double lat = parseLatitudeRad(fields[5]);
	double lon = parseLongitudeRad(fields[6]);
	if ( lat < alat || lat > blat || lon < alon || lon > blon )
		return;

	// Draw glide slope antenna location.
/*
	if( strcmp(fields[7], "-") != 0 && strcmp(fields[8], "-") != 0 ){
		double gs_lat_rad = parseLatitudeRad(fields[7]);
		double gs_lon_rad = parseLongitudeRad(fields[8]);
		drawMark(gs_lat_rad, gs_lon_rad, "GP");
	}
*/

	double h, v;
	double bearing_deg = atof(fields[11]);

	// Determines the reference point the big ILS arrows points to.
	if( strcmp(type, "LDA") == 0
		|| strcmp(type, "LDA/DME") == 0
		|| strcmp(type, "SDF") == 0
		|| strcmp(type, "SDF/DME") == 0
	){
		// Use locator antenna as reference point.
		h = map_x1 + LON_TO_PT * (lon-alon);
		v = map_y1 + LAT_TO_PT * (lat-alat);
		
	} else {
		// Search the runway end point (h,v) where this locator is located.
		RunwayEnd *landingEnd = searchILSRunwayEnd(rwyid, lat, lon);
		if( landingEnd != NULL ){
			// Use reciprocal end as reference point:
			h = map_x1 + LON_TO_PT * (landingEnd->lon - alon);
			v = map_y1 + LAT_TO_PT * (landingEnd->lat - alat);

		} else {
			fprintf(stderr, "%s:%d: Warning: no runway end found for ILS %s in the scene file as scanned so far. Hint: the runway must be defined before the ILS record; the runway ID of the ILS must match one of the runway ends; the distance of the locator from the runway end be less than 500 m.\n",
				scene_file_name, scene_line_no, fields[3]);
			// Use locator antenna as reference point.
			h = map_x1 + LON_TO_PT * (lon-alon);
			v = map_y1 + LAT_TO_PT * (lat-alat);
		}
	}

	// Draw big arrow pointing to the landing end of the runway.
	// Locator outbound bearing in the coordinate system of the sheet
	// (attention! x right, y top):
	double sheet_bearing = units_DEGtoRAD(270 - bearing_deg);
	double r1 = 70.0;
	double r2 = 0.75 * r1;
	double da = units_DEGtoRAD(5);
	double ah = h + r1 * cos(sheet_bearing - da);
	double av = v + r1 * sin(sheet_bearing - da);
	double bh = h + r1 * cos(sheet_bearing + da);
	double bv = v + r1 * sin(sheet_bearing + da);
	double ch = h + r2 * cos(sheet_bearing);
	double cv = v + r2 * sin(sheet_bearing);
	ps_setGray(ps, 0);
	ps_setLineWidth(ps, 1);
	ps_moveTo(ps, ch, cv);
	ps_lineTo(ps, ah, av);
	ps_lineTo(ps, h, v);
	ps_lineTo(ps, bh, bv);
	ps_lineTo(ps, ch, cv);
	ps_lineTo(ps, h, v);
	ps_stroke(ps);

	ratnest_markSegment(place, h, v, ah, av);
	ratnest_markSegment(place, h, v, bh, bv);

	double magnetic_variation = getMagneticVariationAt(lat, lon,
		units_FEETtoMETERS(atoi(alt)));

	char line1[99];
	snprintf(line1, sizeof(line1), "%s  %s  %s", id, type, rwyid);

	char line2[99];
	snprintf(line2, sizeof(line1), "%d&degree;  %s MHz",
		normalizeAngle(units_DEGtoRAD(bearing_deg) - magnetic_variation), vhf);

	double dh = h + 0.7 * r2 * cos(sheet_bearing);
	double dv = v + 0.7 * r2 * sin(sheet_bearing);
	addLabel(&(ratnest_Rect){dh, dv, dh + 75, dv + 22}, line1, line2);
}


/**
 * Draw fancy line connecting a rectangle (containing labels) to the point it
 * is associated to. Among the 4 edges of the rectangular area, choose the
 * middle point nearest to the target (x,y); from this point starts a Bézier
 * smooth curve that ends on the target.
 * @param r Label area.
 * @param x Target point, x coordinate (pt).
 * @param y Target point, y coordinate (pt).
 */
static void drawRectToPointReferenceLine(ratnest_Rect *r, double x, double y)
{
	// First, the center of the rectangle (x0,y0):
	double x0 = (r->x1 + r->x2)/2;
	double y0 = (r->y1 + r->y2)/2;
	
	// Choose one middle point from one of the sides and set (x1,y1) so the
	// curve is perpendicular to the edge:
	double x1, y1;
	double radial = units_RADtoDEG( atan2(y - y0, x - x0) );
	if( 45 <= radial && radial <= 135 ){
		y0 = r->y2;
		x1 = x0;  y1 = y0 + 10;
	} else if( -45 <= radial && radial <= 45 ){
		x0 = r->x2;
		x1 = x0 + 10;  y1 = y0;
	} else if( -135 <= radial && radial <= -45 ){
		y0 = r->y1;
		x1 = x0;  y1 = y0 - 10;
	} else {
		x0 = r->x1;
		x1 = x0 - 10;  y1 = y0;
	}
	
	// The second control point is simply the middle point between 1 and target:
	double x2 = (x1 + x)/2;
	double y2 = (y1 + y)/2;
	
	ps_drawCircle(ps, x0, y0, 2);
	ps_drawCircle(ps, x, y, 1);
	printf("%.1f %.1f moveto %.1f %.1f %.1f %.1f %.1f %.1f curveto stroke\n",
		x0, y0, x1, y1, x2, y2, x, y);
}


/**
 * Place all the labels on the remaining free space of the map.
 */
static void drawLabels()
{
	// Place each label box and draw reference line:
	ps_setLineWidth(ps, 0.5);
	int i;
	for(i = 0; i < labels_number; i++){
		Label *lbl = labels[i];
		// Save reference point:
		double x0 = lbl->r.x1;
		double y0 = lbl->r.y1;
		ratnest_Rect r;
		// Add a margin to the requested rectangle:
		lbl->r.x1 -= 5;
		lbl->r.y1 -= 5;
		lbl->r.x2 += 5;
		lbl->r.y2 += 5;
		if( ! ratnest_place(place, &lbl->r, &r) ){
			fprintf(stderr, "Warning: no space left in map to place the rectangle %s\n",
				ratnest_RectToString(&lbl->r));
			continue;
		}
		// Restore original size:
		r.x1 += 5;
		r.y1 += 5;
		r.x2 -= 5;
		r.y2 -= 5;
		ratnest_markRect(place, &r);
		drawRectToPointReferenceLine(&r, x0, y0);
		lbl->r = r;
	}
	
	// Actually draw labels' boxes:
	for(i = 0; i < labels_number; i++){
		Label *lbl = labels[i];
		ps_drawFramedRect(ps, lbl->r.x1, lbl->r.y1, lbl->r.x2 - lbl->r.x1, lbl->r.y2 - lbl->r.y1);
		ps_moveTo(ps, lbl->r.x1 + 5, lbl->r.y1 + 12);
		ps_drawString(ps, lbl->line1);
		ps_moveTo(ps, lbl->r.x1 + 5, lbl->r.y1 + 4);
		ps_drawString(ps, lbl->line2);
	}
}


/**
 * Draws title etc.
 */
static void drawHeader()
{
	double v = vmax - margin - 14;
	double h = margin;
	char s[999];
	
	ps_setFontHeight(ps, 14);
	ps_moveTo(ps, h, v);
	ps_drawString(ps, title);
	
	ps_setFontHeight(ps, 10);
	double lineHeight = 14;
	
	v -= lineHeight;
	snprintf(s, sizeof(s), "Scenery file: %s     Scale: 1:%d", scene_file_name, scale);
	ps_moveTo(ps, h, v);
	ps_drawString(ps, s);
	
	zulu_Date d;
	zulu_timestampToDate(time(NULL), &d);
	v -= lineHeight;
	snprintf(s, sizeof(s), "Date created: %d-%02d-%02d", d.year, d.month, d.day);
	ps_moveTo(ps, h, v);
	ps_drawString(ps, s);
	
	v -= lineHeight;
	snprintf(s, sizeof(s), "Applicability: ACM flight simulator, see http://www.icosaedro.it/acm/download.html for more.");
	ps_moveTo(ps, h, v);
	ps_drawString(ps, s);
}


static void help()
{
	printf(
"char.exe - Navigation map generator for ACM-6.\n"
"\n"
"Generates a navigation chart from ACM-6 scenery file.\n"
"The PostScript code is sent to standard output on a single A4 format page.\n"
"Non-zero exit status indicates error.\n"
"\n"
"Syntax:\n"
"    chart.exe [OPTIONS]\n"
"\n"
"Options:\n"
"    --scene FILE    Scenery file. MANDATORY.\n"
"    --wmm-cof FILE  World Magnetic Model Coefficients file.\n"
"                    Default: \"%s\".\n"
"    --title TITLE   Title of the chart. Default: \"%s\".\n"
"    --olat LAT      Latitude of the center of the chart.\n"
"                    Example: \"32-30-00.000N\". Default: \"0N\".\n"
"    --olon LON      Longitude of the center of the chart.\n"
"                    Example: \"096-40-00.000W\". Default: \"0E\".\n"
"    --scale N       Denominator of the scale factor 1:N. Default: %d.\n"
"    --dlat SEC      Half-latitude range as number of arc-seconds.\n"
"                    Default: %d.\n"
"    --date-year Y   Magnetic variation calculated for the specified fractional\n"
"                    year, example: 2017.5 stands for 2017-07-02.\n"
"                    Default: today date.\n"
"    --date-timestamp T   Magnetic variation calculated for the specified date\n"
"                    given as Unix timestamp. Default: today date.\n"
"    --no-ils        Does not draw ILS. Handy for wide area maps.\n"
"    --help | -h     This help.\n\n",
		wmm_cof_file_name,
		title,
		scale,
		(int) (dlat/M_PI*180*3600+0.5)
	);
}


#define RADtoPTFactor (180*60/M_PI*1853000/25.4*72)


int main(int argc, char** argv)
{
	int i;
	error_init(argv[0]);

	curr_date_timestamp = time(NULL);
	curr_date_year = zulu_timestampToYear(curr_date_timestamp);
	
	map_x1 = margin;
	map_x2 = hmax - margin;
	map_y1 = margin;
	map_y2 = margin + 0.9*(vmax - 2*margin);

	double map_w = map_x2 - map_x1;
	double map_h = map_y2 - map_y1;

	scale = 500000;
	dlat = scale * map_h / (2*RADtoPTFactor);
	
	for(i = 1; i < argc; i++){
		char *a = argv[i];
		char *v = (i+1) < argc? argv[i+1] : "?";
		if( strcmp(a, "--help") == 0 || strcmp(a, "-h") == 0 ){
			help();
		} else if( strcmp(a, "--wmm-cof") == 0 ){
			wmm_cof_file_name = v;
			i++;
		} else if( strcmp(a, "--scene") == 0 ){
			scene_file_name = v;
			i++;
		} else if( strcmp(a, "--title") == 0 ){
			title = v;
			i++;
		} else if( strcmp(a, "--olat") == 0 ){
			olat = parseLatitudeRad(v);
			i++;
		} else if( strcmp(a, "--olon") == 0 ){
			olon = parseLongitudeRad(v);
			i++;
		} else if( strcmp(a, "--dlat") == 0 ){
			dlat = SECtoRAD(atof(v));
			scale = 2*dlat/map_h*RADtoPTFactor;
			i++;
		} else if( strcmp(a, "--scale") == 0 ){
			scale = atoi(v);
			dlat = scale * map_h / (2*RADtoPTFactor);
			i++;
		} else if( strcmp(a, "--date-year") == 0 ){
			curr_date_year = atof(v);
			curr_date_timestamp = zulu_yearToTimestamp(curr_date_year);
			i++;
		} else if( strcmp(a, "--date-timestamp") == 0 ){
			curr_date_timestamp = atoi(v);
			curr_date_year = zulu_timestampToYear(curr_date_timestamp);
			i++;
		} else if( strcmp(a, "--no-ils") == 0 ){
			draw_ils = 0;
		} else {
			error_external("unknown option: %s", a);
		}
	}

	LAT_TO_PT = map_h / (2 * dlat);
	dlon = map_w / (2 * LAT_TO_PT * cos(olat));
	LON_TO_PT = map_w / (2 * dlon);

	alat = olat - dlat;
	alon = olon - dlon;
	blat = olat + dlat;
	blon = olon + dlon;

	scene_file = fopen(scene_file_name, "r");
	if( scene_file == NULL )
		error_system("opening scenery file %s", scene_file_name);

	wmm_init(wmm_cof_file_name);

	ps = ps_new();

	place = ratnest_new(&(ratnest_Rect){map_x1, map_y1, map_x2, map_y2});
	
	drawHeader();
	drawMagneticVariationBox();
	
	ps_setFontHeight(ps, 7);

	ps_setClipRect(ps, map_x1, map_y1, map_w, map_h);

	drawGrid();

	do {
		if( fgets(scene_line, sizeof(scene_line), scene_file) == NULL )
			break;
		scene_line_no++;
		splitLineIntoFields(scene_line);
		if( fields_no == 0 )
			continue;
		if( strcmp(fields[0], "RWY") == 0 ){
			drawRWY();

		} else if( strcmp(fields[0], "RWY2") == 0 ){
			drawRWY2();

		} else if( strcmp(fields[0], "ILS") == 0 ){
			drawILS();

		} else if( strcmp(fields[0], "NAV") == 0 ){
			drawNAV();

		} else if( strcmp(fields[0], "FEATURE") == 0 ){
			drawFeature();

		} else if( strcmp(fields[0], "TEAM1_LOC") == 0 ){
			drawTeamLoc();

		} else if( strcmp(fields[0], "TEAM2_LOC") == 0 ){
			drawTeamLoc();

		} else if( strcmp(fields[0], "GROUND_COLOR") == 0 ){

		} else {
			fprintf(stderr, "%s:%d: unknown/unsupported record %s\n",
				scene_file_name, scene_line_no, fields[0]);
		}
	}while(1);
	if( ferror(scene_file) )
		error_system("reading %s", scene_file_name);
	fclose(scene_file);
	
	drawLabels();
	
	//ratnest_debug(place, ps);

	ps_close(ps);

	return 0;
}

