/*--------------------------------------------------------------------
 *	$Id: grdfilter.c,v 1.2.4.2 2002/01/17 17:42:51 pwessel Exp $
 *
 *	Copyright (c) 1991-2002 by P. Wessel and W. H. F. Smith
 *	See COPYING file for copying and redistribution conditions.
 *
 *	This program is free software; you can redistribute it and/or modify
 *	it under the terms of the GNU General Public License as published by
 *	the Free Software Foundation; version 2 of the License.
 *
 *	This program is distributed in the hope that it will be useful,
 *	but WITHOUT ANY WARRANTY; without even the implied warranty of
 *	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *	GNU General Public License for more details.
 *
 *	Contact info: gmt.soest.hawaii.edu
 *--------------------------------------------------------------------*/
/*
 * grdfilter.c  reads a grdfile and creates filtered grd file
 *
 * user selects boxcar, gaussian, cosine arch, median, or mode filter
 * user selects distance scaling as appropriate for map work, etc.
 *
 * Author:  W.H.F. Smith
 * Date: 	16 Aug 88
 *
 * Modified:	27 Sep 88 by W.H.F. Smith, to use new median routine.
 *
 * Updated:	PW: 13-Jun-1991 to v2.0
 *		PW: 13-Jul-1992 to actually do v2 i/o.
 *		PW: 15-Jul-1992 to handle arbitrary new -R -I
 *		PW: 03-Jan-1995 to offer mode-filter (from filter1d)
 *		WS: 21-May-1998 to make warnings silent unless -V on.
 *		PW: 03-Jun-1998 upgrade to GMT 3.1
 *		PW: 02-Jun-1999 upgrade to GMT 3.3 + added SINCOS option
 *		PW: 18-Oct-1999 Use sincos directly
 *		PW: 18-JUN-2000 3.3.5
 * Version:	3.4.1
 */
 
#include "gmt.h"

double	get_wt();
void	set_weight_matrix(int nx_f, int ny_f, double y_0, double north, double south, double dx, double dy, double f_wid, int f_flag, int d_flag, double x_off, double y_off, BOOLEAN fast);

int	*i_origin;
float	*input, *output;
double	*weight, *work_array, *x_shift;
double	deg2km;

char *filter_name[5] = {
	"Boxcar",
	"Cosine Arch",
	"Gaussian",
	"Median",
	"Mode"
};

main (int argc, char **argv)
{

	int	nx_out, ny_out, nx_fil, ny_fil, n_in_median, n_nan = 0;
	int	x_half_width, y_half_width, j_origin, i_out, j_out;
	int	i_in, j_in, ii, jj, i, ij_in, ij_out, ij_wt, effort_level;
	int	distance_flag, filter_type, one_or_zero = 1;
	
	BOOLEAN	error, new_range, new_increment, fast_way, shift, slow, toggle = FALSE;
	
	double	west_new, east_new, south_new, north_new, dx_new, dy_new, offset;
	double	filter_width, x_scale, y_scale, x_width, y_width;
	double	x_out, y_out, wt_sum, value, last_median, this_median, xincnew2, yincnew2;
	double	xincold2, yincold2, y_shift, x_fix = 0.0, y_fix = 0.0;
	
	char	*fin = CNULL, *fout = CNULL;
	
	struct	GRD_HEADER h, test_h;

	argc = GMT_begin (argc, argv);
	
	deg2km = 0.001 * 2.0 * M_PI * gmtdefs.ellipse[gmtdefs.ellipsoid].eq_radius / 360.0;
	error = new_range = new_increment = FALSE;
	fin = fout = NULL;
	filter_width = dx_new = dy_new = west_new = east_new = 0.0;
	filter_type = distance_flag = -1;
	
	for (i = 1; i < argc; i++) {
		if (argv[i][0] == '-') {
			switch (argv[i][1]) {
				/* Common parameters */
			
				case 'R':
				case 'V':
				case '\0':
					error += GMT_get_common_args (argv[i], &west_new, &east_new, &south_new, &north_new);
					break;
				
				case 'F':
					switch (argv[i][2]) {
						case 'b':
							filter_type = 0;
							break;
						case 'c':
							filter_type = 1;
							break;
						case 'g':
							filter_type = 2;
							break;
						case 'm':
							filter_type = 3;
							break;
						case 'p':
							filter_type = 4;
							break;
						default:
							error = TRUE;
							break;
					}
					filter_width = atof(&argv[i][3]);
					break;
				case 'G':
					fout = &argv[i][2];
					break;
				case 'D':
					distance_flag = atoi(&argv[i][2]);
					break;
				case 'I':
					GMT_getinc (&argv[i][2], &dx_new, &dy_new);
					new_increment = TRUE;
					break;
				case 'N':	/* Pixel registration OBSOLTE but BACKWARD COMPATIBLE */
					one_or_zero = 0;
					break;
				case 'T':	/* Toggle registration */
					toggle = TRUE;
					break;
				default:
					error = TRUE;
					GMT_default_error (argv[i][1]);
					break;
			}
		}
		else
			fin = argv[i];
	}
	
	if (argc == 1 || GMT_quick) {
		fprintf (stderr, "grdfilter %s - Filter a 2-D netCDF grdfile in the Time domain\n\n", GMT_VERSION);
		fprintf(stderr,"usage: grdfilter input_file -D<distance_flag> -F<type><filter_width>\n");
		fprintf(stderr,"\t-G<output_file> [-I<xinc>[m|c][/<yinc>[m|c]] ] [-R<west/east/south/north>] [-T] [-V]\n");
		
		if (GMT_quick) exit (EXIT_FAILURE);
		
		fprintf(stderr,"\tDistance flag determines how grid (x,y) maps into distance units of filter width as follows:\n");
		fprintf(stderr,"\t   -D0 grid x,y same units as <filter_width>, cartesian Distances.\n");
		fprintf(stderr,"\t   -D1 grid x,y in degrees, <filter_width> in km, cartesian Distances.\n");
		fprintf(stderr,"\t   -D2 grid x,y in degrees, <filter_width> in km, x_scaled by cos(middle y), cartesian Distances.\n");
		fprintf(stderr,"\t   These first three options are faster; they allow weight matrix to be computed only once.\n");
		fprintf(stderr,"\t   Next two options are slower; weights must be recomputed for each scan line.\n");
		fprintf(stderr,"\t   -D3 grid x,y in degrees, <filter_width> in km, x_scale varies as cos(y), cartesian Distances.\n");
		fprintf(stderr,"\t   -D4 grid x,y in degrees, <filter_width> in km, spherical Distances.\n");
		fprintf(stderr,"\t-F sets the filter type and full (6 sigma) filter-width.  Choose between\n");
		fprintf(stderr,"\t   (b)oxcar, (c)osine arch, (g)aussian, (m)edian filters\n");
		fprintf(stderr,"\t   or p(maximum liklihood Probability estimator -- a mode estimator)\n");
		fprintf(stderr,"\t-G sets output name for filtered grdfile\n");
		fprintf(stderr, "\n\tOPTIONS:\n");
		fprintf(stderr,"\t-I for new Increment of output grid; enter xinc, optionally xinc/yinc.\n");
		fprintf(stderr,"\t   Default is yinc = xinc.  Append an m [or c] to xinc or yinc to indicate minutes [or seconds];\n");
		fprintf(stderr,"\t   The new xinc and yinc should be divisible by the old ones (new lattice is subset of old).\n");
		fprintf(stderr, "\t-T Toggles between grid and pixel registration for output grid [Default is same as input registration]\n");
		fprintf(stderr, "\t-R for new Range of output grid; enter <WESN> (xmin, xmax, ymin, ymax) separated by slashes.\n");
		GMT_explain_option ('V');
		exit (EXIT_FAILURE);
	}

	if (!fout) {
		fprintf (stderr, "%s: GMT SYNTAX ERROR -G option:  Must specify output file\n", GMT_program);
		error++;
	}
	if (!fin) {
		fprintf (stderr, "%s: GMT SYNTAX ERROR:  Must specify input file\n", GMT_program);
		error++;
	}
	if (distance_flag < 0 || distance_flag > 4) {
		fprintf (stderr, "%s: GMT SYNTAX ERROR -D option:  Choose among 1, 2, 3, or 4\n", GMT_program);
		error++;
	}
	if (filter_type < 0 || filter_width <= 0.0) {
		fprintf (stderr, "%s: GMT SYNTAX ERROR -F option:  Correct syntax:\n", GMT_program);
		fprintf (stderr, "\t-FX<width>, with X one of bcgmp, width is filter fullwidth\n");
		error++;
	}
	if (toggle && one_or_zero == 0) {	/* Both -N and -T set, not good */
		fprintf (stderr, "%s: GMT SYNTAX ERROR -T option:  Not allowed with obsolete -N option\n", GMT_program);
		error++;
	}
	if (error) exit (EXIT_FAILURE);

	GMT_put_history (argc, argv);	/* Update .gmtcommands */

	if (project_info.region_supplied) new_range = TRUE;

	if (GMT_read_grd_info (fin, &h)) {
		fprintf (stderr, "%s: Error opening file %s\n", GMT_program, fin);
		exit (EXIT_FAILURE);
	}
	GMT_grd_init (&h, argc, argv, TRUE);	/* Update command history only */

	if (toggle) {	/* Make output grid of the opposite registration */
		one_or_zero = (h.node_offset) ? 1 : 0;
	}
	else
		one_or_zero = (h.node_offset) ? 0 : 1;
	
	/* Read the input grd file and close it  */
	
	input = (float *) GMT_memory (VNULL, (size_t)(h.nx * h.ny), sizeof(float), GMT_program);

	if (GMT_read_grd (fin, &h, input, 0.0, 0.0, 0.0, 0.0, GMT_pad, FALSE)) {
		fprintf (stderr, "%s: Error reading file %s\n", GMT_program, fin);
		exit (EXIT_FAILURE);
	}
	
	last_median = 0.5 * (h.z_min + h.z_max);

	/* Check range of output area and set i,j offsets, etc.  */

	if (!new_range) {
		west_new = h.x_min;
		east_new = h.x_max;
		south_new = h.y_min;
		north_new = h.y_max;
	}
	if (!new_increment) {
		dx_new = h.x_inc;
		dy_new = h.y_inc;
	}

	if (west_new < h.x_min) error = TRUE;
	if (east_new > h.x_max) error = TRUE;
	if (south_new < h.y_min) error = TRUE;
	if (north_new > h.y_max) error = TRUE;
	if (dx_new <= 0.0) error = TRUE;
	if (dy_new <= 0.0) error = TRUE;
	
	if (error) {
		fprintf(stderr,"%s: New WESN incompatible with old.\n", GMT_program);
		exit (EXIT_FAILURE);
	}

	/* Make sure output grid is kosher */

	test_h.x_min = west_new;	test_h.x_max = east_new;	test_h.x_inc = dx_new;
	test_h.y_min = south_new;	test_h.y_max = north_new;	test_h.y_inc = dy_new;
	GMT_grd_RI_verify (&test_h, 1);

	/* We can save time by computing a weight matrix once [or once pr scanline] only
	   if new grid spacing is multiple of old spacing */
	   
	fast_way = ((irint (dx_new / h.x_inc) == (dx_new / h.x_inc)) && (irint (dy_new / h.y_inc) == (dy_new / h.y_inc)));
	
	if (!fast_way && gmtdefs.verbose) {
		fprintf (stderr, "%s: Warning - Your output grid spacing is such that filter-weights must\n", GMT_program);
		fprintf (stderr, "be recomputed for every output node, so expect this run to be slow.  Calculations\n");
		fprintf (stderr, "can be speeded up significantly if output grid spacing is chosen to be a multiple\n");
		fprintf (stderr, "of the input grid spacing.  If the odd output grid is necessary, consider using\n");
		fprintf (stderr, "a \'fast\' grid for filtering and then resample onto your desired grid with grdsample.\n");
	}

	nx_out = one_or_zero + irint ( (east_new - west_new) / dx_new);
	ny_out = one_or_zero + irint ( (north_new - south_new) / dy_new);
	
	output = (float *) GMT_memory (VNULL, (size_t)(nx_out*ny_out), sizeof(float), GMT_program);
	i_origin = (int *) GMT_memory (VNULL, (size_t)nx_out, sizeof(int), GMT_program);
	if (!fast_way) x_shift = (double *) GMT_memory (VNULL, (size_t)nx_out, sizeof(double), GMT_program);

	xincnew2 = (one_or_zero) ? 0.0 : 0.5 * dx_new;
	yincnew2 = (one_or_zero) ? 0.0 : 0.5 * dy_new;
	/* xincold2 = (h.node_offset) ? 0.0 : 0.5 * h.x_inc;
	yincold2 = (h.node_offset) ? 0.0 : 0.5 * h.y_inc; */
	xincold2 = (h.node_offset) ? 0.5 * h.x_inc : 0.0;
	yincold2 = (h.node_offset) ? 0.5 * h.y_inc : 0.0;
	offset = (h.node_offset) ? 0.0 : 0.5;
	
	if (fast_way && h.node_offset == one_or_zero) {	/* multiple grid but one is pix, other is grid */
		x_fix = 0.5 * h.x_inc;
		y_fix = 0.5 * h.y_inc;
		shift = (x_fix != 0.0 || y_fix != 0.0);
	}
	
	/* Set up weight matrix and i,j range to test  */

	x_scale = y_scale = 1.0;
	if (distance_flag > 0) {
		x_scale *= deg2km;
		y_scale *= deg2km;
	}
	if (distance_flag == 2)
		x_scale *= cos (D2R * 0.5 * (north_new + south_new) );
	else if (distance_flag > 2) {
		if ( fabs(south_new) > north_new )
			x_scale *= cos (D2R * south_new);
		else
			x_scale *= cos (D2R * north_new);
	}
	x_width = filter_width / (h.x_inc * x_scale);
	y_width = filter_width / (h.y_inc * y_scale);
	y_half_width = (int) (ceil(y_width) / 2.0);
	x_half_width = (int) (ceil(x_width) / 2.0);

	nx_fil = 2 * x_half_width + 1;
	ny_fil = 2 * y_half_width + 1;
	weight = (double *) GMT_memory (VNULL, (size_t)(nx_fil*ny_fil), sizeof(double), GMT_program);

	slow = (filter_type == 3 || filter_type == 4);	/* Will require sorting etc */
	
	if (slow) work_array = (double *) GMT_memory (VNULL, (size_t)(nx_fil*ny_fil), sizeof(double), GMT_program);

	if (gmtdefs.verbose) {
		fprintf(stderr,"%s: Input nx,ny = (%d %d), output nx,ny = (%d %d), filter nx,ny = (%d %d)\n", GMT_program,
			h.nx, h.ny, nx_out, ny_out, nx_fil, ny_fil);
		fprintf(stderr,"%s: Filter type is %s.\n", GMT_program, filter_name[filter_type]);
	}
	
	/* Compute nearest xoutput i-indices and shifts once */
	
	for (i_out = 0; i_out < nx_out; i_out++) {
		x_out = west_new + i_out * dx_new + xincnew2;
		i_origin[i_out] = (int)floor(((x_out - h.x_min) / h.x_inc) + offset);
		if (!fast_way) x_shift[i_out] = x_out - (h.x_min + i_origin[i_out] * h.x_inc + xincold2);
	}
	
	/* Now we can do the filtering  */

	/* Determine how much effort to compute weights:
		1 = Compute weights once for entire grid
		2 = Compute weights once per scanline
		3 = Compute weights for every output point [slow]
	*/
	
	if (fast_way && distance_flag <= 2)
		effort_level = 1;
	else if (fast_way && distance_flag > 2)
		effort_level = 2;
	else
		effort_level = 3;
		
	if (effort_level == 1) {	/* Only need this once */
		y_out = north_new - yincnew2;
		set_weight_matrix (nx_fil, ny_fil, y_out, north_new, south_new, h.x_inc, h.y_inc, filter_width, filter_type, distance_flag, x_fix, y_fix, shift);
	}
	
	for (j_out = 0; j_out < ny_out; j_out++) {
	
		if (gmtdefs.verbose) fprintf (stderr, "%s: Processing output line %d\r", GMT_program, j_out);
		y_out = north_new - j_out * dy_new - yincnew2;
		j_origin = (int)floor(((h.y_max - y_out) / h.y_inc) + offset);
		if (effort_level == 2)
			set_weight_matrix (nx_fil, ny_fil, y_out, north_new, south_new, h.x_inc, h.y_inc, filter_width, filter_type, distance_flag, x_fix, y_fix, shift);
		if (!fast_way) y_shift = y_out - (h.y_max - j_origin * h.y_inc - yincold2);
		
		for (i_out = 0; i_out < nx_out; i_out++) {
		
			if (effort_level == 3)
				set_weight_matrix (nx_fil, ny_fil, y_out, north_new, south_new, h.x_inc, h.y_inc, filter_width, filter_type, distance_flag, x_shift[i_out], y_shift, fast_way);
			wt_sum = value = 0.0;
			n_in_median = 0;
			ij_out = j_out * nx_out + i_out;
			
			for (ii = -x_half_width; ii <= x_half_width; ii++) {
				i_in = i_origin[i_out] + ii;
				if ( (i_in < 0) || (i_in >= h.nx) ) continue;

				for (jj = -y_half_width; jj <= y_half_width; jj++) {
					j_in = j_origin + jj;
					if ( (j_in < 0) || (j_in >= h.ny) ) continue;
										
					ij_wt = (jj + y_half_width) * nx_fil + ii + x_half_width;
					if (weight[ij_wt] < 0.0) continue;
					
					ij_in = j_in*h.nx + i_in;
					if (GMT_is_fnan (input[ij_in])) continue;

					/* Get here when point is usable  */
					if (slow) {
						work_array[n_in_median] = input[ij_in];
						n_in_median++;
					}
					else {
						value += input[ij_in] * weight[ij_wt];
						wt_sum += weight[ij_wt];
					}
				}
			}
			
			/* Now we have done the convolution and we can get the value  */
			
			if (slow) {
				if (n_in_median) {
					if (filter_type == 3)
						GMT_median (work_array, n_in_median, h.z_min, h.z_max, last_median, &this_median);
					else
						GMT_mode (work_array, n_in_median, n_in_median/2, TRUE, &this_median);
					output[ij_out] = (float)this_median;
					last_median = this_median;
				}
				else {
					output[ij_out] = GMT_f_NaN;
					n_nan++;
				}
			}
			else {
				if (wt_sum == 0.0) {	/* Assign value = GMT_f_NaN */
					n_nan++;
					output[ij_out] = GMT_f_NaN;
				}
				else
					output[ij_out] = (float)(value / wt_sum);
			}
		}
	}
	if (gmtdefs.verbose) fprintf (stderr, "\n");
	
	/* At last, that's it!  Output: */

	if (n_nan && gmtdefs.verbose) fprintf (stderr, "%s: Unable to estimate value at %d nodes, set to NaN\n", GMT_program, n_nan);
	
	h.nx = nx_out;
	h.ny = ny_out;
	h.x_min = west_new;
	h.x_max = east_new;
	h.y_min = south_new;
	h.y_max = north_new;
	h.x_inc = dx_new;
	h.y_inc = dy_new;
	h.node_offset = !one_or_zero;
	
	if (GMT_write_grd (fout, &h, output, 0.0, 0.0, 0.0, 0.0, GMT_pad, FALSE)) {
		fprintf (stderr, "%s: Error writing file %s\n", GMT_program, fout);
		exit (EXIT_FAILURE);
	}
	
	GMT_free ((void *) input);
	GMT_free ((void *) output);
	GMT_free ((void *) weight);
	GMT_free ((void *) i_origin);
	if (slow) GMT_free ((void *) work_array);
	if (!fast_way) GMT_free ((void *) x_shift);
	
	 GMT_end (argc, argv);
}

void	set_weight_matrix (int nx_f, int ny_f, double y_0, double north, double south, double dx, double dy, double f_wid, int f_flag, int d_flag, double x_off, double y_off, BOOLEAN fast)

      	                                               	/* Last two gives offset between output node and 'origin' input node for this window (0,0 for integral grids) */
   	           
             	/* TRUE when input/output grids are offset by integer values in dx/dy */
   	               
{

	int	i, j, ij, i_half, j_half;
	double	x_scl, y_scl, f_half, r_f_half, sigma, sig_2;
	double	y1, y2, theta, x, y, r, s_y1, c_y1, s_y2, c_y2;

	/* Set Scales  */

	y_scl = (d_flag) ? deg2km : 1.0;
	if (d_flag < 2)
		x_scl = y_scl;
	else if (d_flag == 2)
		x_scl = deg2km * cos (0.5 * D2R * (north + south));
	else
		x_scl = deg2km * cos (D2R * y_0);

	/* Get radius, weight, etc.  */

	i_half = nx_f / 2;
	j_half = ny_f / 2;
	f_half = 0.5 * f_wid;
	r_f_half = 1.0 / f_half;
	sigma = f_wid / 6.0;
	sig_2 = -0.5 / (sigma * sigma);
	for (i = -i_half; i <= i_half; i++) {
		for (j = -j_half; j <= j_half; j++) {
			ij = (j + j_half) * nx_f + i + i_half;
			if (fast && i == 0)
				r = (j == 0) ? 0.0 : j * y_scl * dy;
			else if (fast && j == 0)
				r = i * x_scl * dx;
			else if (d_flag < 4) {
				x = x_scl * (i * dx - x_off);
				y = y_scl * (j * dy - y_off);
				r = hypot (x, y);
			}
			else {
				theta = D2R * (i * dx - x_off);
				y1 = D2R * (90.0 - y_0);
				y2 = D2R * (90.0 - (y_0 + (j * dy - y_off)) );
				sincos (y1, &s_y1, &c_y1);
				sincos (y2, &s_y2, &c_y2);
				r = d_acos (c_y1 * c_y2 + s_y1 * s_y2 * cos(theta)) * deg2km * R2D;
			}
			/* Now we know r in f_wid units  */
			
			if (r > f_half) {
				weight[ij] = -1.0;
				continue;
			}
			else if (f_flag == 3) {
				weight[ij] = 1.0;
				continue;
			}
			else {
				if (f_flag == 0)
					weight[ij] = 1.0;
				else if (f_flag == 1)
					weight[ij] = 1.0 + cos (M_PI * r * r_f_half);
				else
					weight[ij] = exp (r * r * sig_2);
			}
		}
	}
}
