//##########################################################################
//#                                                                        #
//#                              CLOUDCOMPARE                              #
//#                                                                        #
//#  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 or later 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.                          #
//#                                                                        #
//#          COPYRIGHT: EDF R&D / TELECOM ParisTech (ENST-TSI)             #
//#                                                                        #
//##########################################################################

#include "ccRasterizeTool.h"
#include "ui_rasterizeDlg.h"

//Local
#include "ccCommon.h"
#include "ccPersistentSettings.h"
#include "ccContourLinesGenerator.h"
#include "mainwindow.h"
#include "ccKrigingParamsDialog.h"

//qCC_db
#include <ccColorScalesManager.h>
#include <ccFileUtils.h>
#include <ccGenericPointCloud.h>
#include <ccMesh.h>
#include <ccPointCloud.h>
#include <ccPolyline.h>
#include <ccProgressDialog.h>
#include <ccScalarField.h>

//qCC_gl
#include <ccGLWindowInterface.h>

//qCC_io
#include <ImageFileFilter.h>

//Qt
#include <QFileDialog>
#include <QMap>
#include <QMessageBox>
#include <QPushButton>
#include <QSettings>
#include <QStandardItemModel>
#include <QInputDialog>

#ifdef CC_GDAL_SUPPORT
//GDAL
#include <cpl_string.h>
#include <gdal.h>
#include <gdal_priv.h>
#include <ogr_api.h>
//local
#include "ui_rasterExportOptionsDlg.h"
#endif

//System
#include <cassert>

constexpr char HILLSHADE_FIELD_NAME[] = "Hillshade";

static void MakeComboBoxOptionInaccessible(QComboBox* comboBox, int index)
{
	if (!comboBox)
	{
		assert(false);
		return;
	}
	
	const QStandardItemModel* model = qobject_cast<const QStandardItemModel*>(comboBox->model());
	QStandardItem* item = model ? model->item(index) : 0;
	if (item)
	{
		item->setFlags(item->flags() & ~(Qt::ItemIsSelectable | Qt::ItemIsEnabled));
		// visually disable by greying out - works only if combobox has been painted already and palette returns the wanted color
		item->setData(comboBox->palette().color(QPalette::Disabled, QPalette::Text), Qt::TextColorRole); // clear item data in order to use default color
	}
}

ccRasterizeTool::ccRasterizeTool(ccGenericPointCloud* cloud, QWidget* parent)
	: QDialog(parent, Qt::WindowMaximizeButtonHint | Qt::WindowCloseButtonHint)
	, cc2Point5DimEditor()
	, m_UI( new Ui::RasterizeToolDialog )
	, m_cloud(cloud)
	, m_cloudHasScalarFields(false)
{
	m_UI->setupUi(this);

#ifdef CC_GDAL_SUPPORT
	m_UI->ignoreContourBordersCheckBox->setVisible(false);
#else
	m_UI->generateRasterPushButton->setDisabled(true);
	m_UI->generateRasterPushButton->setChecked(false);
#endif

	//custom bbox editor (needs to be setup first)
	ccBBox gridBBox = m_cloud ? m_cloud->getOwnBB() : ccBBox();
	if (gridBBox.isValid())
	{
		createBoundingBoxEditor(gridBBox, this);
		connect(m_UI->editGridToolButton, &QAbstractButton::clicked, this, &ccRasterizeTool::showGridBoxEditor);
	}
	else
	{
		m_UI->editGridToolButton->setEnabled(false);
	}

	//force update
	resampleOptionToggled(m_UI->resampleCloudCheckBox->isChecked());
	fillEmptyCellStrategyChanged(0);

	connect(m_UI->buttonBox,					&QDialogButtonBox::accepted,						this,	&ccRasterizeTool::testAndAccept);
	connect(m_UI->buttonBox,					&QDialogButtonBox::rejected,						this,	&ccRasterizeTool::testAndReject);
	
	connect(m_UI->gridStepDoubleSpinBox,		qOverload<double>(&QDoubleSpinBox::valueChanged),	this,	&ccRasterizeTool::updateGridInfo);
	connect(m_UI->gridStepDoubleSpinBox,		qOverload<double>(&QDoubleSpinBox::valueChanged),	this,	&ccRasterizeTool::gridOptionChanged);
	connect(m_UI->emptyValueDoubleSpinBox,		qOverload<double>(&QDoubleSpinBox::valueChanged),	this,	&ccRasterizeTool::gridOptionChanged);

	connect(m_UI->dimensionComboBox,			qOverload<int>(&QComboBox::currentIndexChanged),	this,	&ccRasterizeTool::projectionDirChanged);
	connect(m_UI->heightProjectionComboBox,		qOverload<int>(&QComboBox::currentIndexChanged),	this,	&ccRasterizeTool::projectionTypeChanged);
	connect(m_UI->scalarFieldProjection,		qOverload<int>(&QComboBox::currentIndexChanged),	this,	&ccRasterizeTool::sfProjectionTypeChanged);
	connect(m_UI->fillEmptyCellsComboBox,		qOverload<int>(&QComboBox::currentIndexChanged),	this,	&ccRasterizeTool::fillEmptyCellStrategyChanged);
	connect(m_UI->stdDevLayerComboBox,			qOverload<int>(&QComboBox::currentIndexChanged),	this,	&ccRasterizeTool::stdDevLayerChanged);
	connect(m_UI->activeLayerComboBox,			qOverload<int>(&QComboBox::currentIndexChanged),	this,	[this] (int index) { activeLayerChanged( index ); } );

	connect(m_UI->resampleCloudCheckBox,		&QAbstractButton::toggled,							this,	&ccRasterizeTool::resampleOptionToggled);
	connect(m_UI->updateGridPushButton,			&QAbstractButton::clicked,							this,	&ccRasterizeTool::updateGridAndDisplay);
	connect(m_UI->generateCloudPushButton,		&QAbstractButton::clicked,							this,	[this] () { generateCloud(true); } );
	connect(m_UI->generateImagePushButton,		&QAbstractButton::clicked,							this,	&ccRasterizeTool::generateImage);
	connect(m_UI->generateRasterPushButton,		&QAbstractButton::clicked,							this,	&ccRasterizeTool::generateRaster);
	connect(m_UI->generateASCIIPushButton,		&QAbstractButton::clicked,							this,	&ccRasterizeTool::generateASCIIMatrix);
	connect(m_UI->generateMeshPushButton,		&QAbstractButton::clicked,							this,	&ccRasterizeTool::generateMesh);
	connect(m_UI->generateContoursPushButton,	&QAbstractButton::clicked,							this,	&ccRasterizeTool::generateContours);
	connect(m_UI->exportContoursPushButton,		&QAbstractButton::clicked,							this,	&ccRasterizeTool::exportContourLines);
	connect(m_UI->clearContoursPushButton,		&QAbstractButton::clicked,							this,	&ccRasterizeTool::removeContourLines);
	connect(m_UI->generateHillshadePushButton,	&QAbstractButton::clicked,							this,	&ccRasterizeTool::generateHillshade);
	connect(m_UI->interpParamsToolButton,		&QAbstractButton::clicked,							this,	&ccRasterizeTool::showInterpolationParamsDialog);

	connect(m_UI->exportHeightStatsCheckBox,	&QCheckBox::toggled,								this,	&ccRasterizeTool::onStatExportTargetChanged);
	connect(m_UI->exportSFStatsCheckBox,		&QCheckBox::toggled,								this,	&ccRasterizeTool::onStatExportTargetChanged);

	if (m_cloud)
	{
		//populate layer box
		m_UI->activeLayerComboBox->addItem(ccRasterGrid::GetDefaultFieldName(ccRasterGrid::PER_CELL_VALUE), QVariant(LAYER_HEIGHT));
		if (m_cloud->hasColors())
		{
			m_UI->activeLayerComboBox->addItem("RGB", QVariant(LAYER_RGB));
		}

		if (cloud->isA(CC_TYPES::POINT_CLOUD) && cloud->hasScalarFields())
		{
			ccPointCloud* pc = static_cast<ccPointCloud*>(cloud);
			for (unsigned i = 0; i < pc->getNumberOfScalarFields(); ++i)
			{
				m_UI->activeLayerComboBox->addItem(pc->getScalarField(i)->getName(), QVariant(LAYER_SF));
				//populate std. dev. layer box as well
				m_UI->stdDevLayerComboBox->addItem(pc->getScalarField(i)->getName());
			}
			m_cloudHasScalarFields = true;
		}
		
		//add window
		create2DView(m_UI->mapFrame);
	}

	if (!m_cloudHasScalarFields)
	{
		m_UI->projectSFCheckBox->setChecked(false);
		MakeComboBoxOptionInaccessible(m_UI->heightProjectionComboBox, ccRasterGrid::PROJ_INVERSE_VAR_VALUE);
		MakeComboBoxOptionInaccessible(m_UI->scalarFieldProjection, ccRasterGrid::PROJ_INVERSE_VAR_VALUE);
	}

	m_UI->projectSFCheckBox->setEnabled(m_cloudHasScalarFields);
	m_UI->scalarFieldProjection->setEnabled(m_cloudHasScalarFields);
	m_UI->stdDevLayerComboBox->setEnabled(m_cloudHasScalarFields); // real state will be set later (--> updateStdDevLayerComboBox)
	m_UI->exportSFStatsCheckBox->setEnabled(m_cloudHasScalarFields);

	loadSettings();

	updateStdDevLayerComboBox();

	gridIsUpToDate(false); // will call updateGridInfo

	resize(minimumSize());
}

ccRasterizeTool::~ccRasterizeTool()
{
	removeContourLines();
	
	delete m_UI;
	m_UI = nullptr;
}

void ccRasterizeTool::removeContourLines()
{
	while (!m_contourLines.empty())
	{
		ccPolyline* poly = m_contourLines.back();
		if (m_glWindow)
			m_glWindow->removeFromOwnDB(poly);
		delete poly;
		m_contourLines.pop_back();
	}

	m_UI->exportContoursPushButton->setEnabled(false);
	m_UI->clearContoursPushButton->setEnabled(false);

	if (m_glWindow)
		m_glWindow->redraw();
}

bool ccRasterizeTool::showGridBoxEditor()
{
	return cc2Point5DimEditor::showGridBoxEditor();
}

void ccRasterizeTool::updateCloudName(bool withNonEmptyCells)
{
	QString str;
	if (m_cloud)
	{
		str = QString("<b>%1</b> (%2 points").arg(m_cloud->getName(), QLocale::system().toString(m_cloud->size()));
		if (withNonEmptyCells)
			str += QString(" - %1 non-empty cells)").arg(QLocale::system().toString(m_grid.validCellCount));
		else
			str += ')';
	}
	else
	{
		str = "No cloud loaded";
	}

	m_UI->cloudNameLabel->setText(str);
}

void ccRasterizeTool::updateGridInfo(bool withNonEmptyCells/*=false*/)
{
	m_UI->gridWidthLabel->setText(getGridSizeAsString());
	
	updateCloudName(withNonEmptyCells);
}

double ccRasterizeTool::getGridStep() const
{
	return m_UI->gridStepDoubleSpinBox->value();
}

void ccRasterizeTool::getExportedStats(std::vector<ccRasterGrid::ExportableFields>& stats) const
{
	stats.clear();

	stats.push_back(ccRasterGrid::PER_CELL_VALUE);
	if (m_UI->generateStatisticsPopulationCheckBox->isChecked())
		stats.push_back(ccRasterGrid::PER_CELL_COUNT);
	if (m_UI->generateStatisticsMinCheckBox->isChecked())
		stats.push_back(ccRasterGrid::PER_CELL_MIN_VALUE);
	if (m_UI->generateStatisticsMaxCheckBox->isChecked())
		stats.push_back(ccRasterGrid::PER_CELL_MAX_VALUE);
	if (m_UI->generateStatisticsAverageCheckBox->isChecked())
		stats.push_back(ccRasterGrid::PER_CELL_AVG_VALUE);
	if (m_UI->generateStatisticsStdDevCheckBox->isChecked())
		stats.push_back(ccRasterGrid::PER_CELL_VALUE_STD_DEV);
	if (m_UI->generateStatisticsRangeCheckBox->isChecked())
		stats.push_back(ccRasterGrid::PER_CELL_VALUE_RANGE);
	if (m_UI->generateStatisticsMedianCheckBox->isChecked())
		stats.push_back(ccRasterGrid::PER_CELL_MEDIAN_VALUE);
	if (m_UI->generateStatisticsUniqueCheckBox->isChecked())
		stats.push_back(ccRasterGrid::PER_CELL_UNIQUE_COUNT_VALUE);
	if (m_UI->generateStatisticsPercentileCheckBox->isChecked())
		stats.push_back(ccRasterGrid::PER_CELL_PERCENTILE_VALUE);
}

bool ccRasterizeTool::resampleOriginalCloud() const
{
	return m_UI->resampleCloudCheckBox->isEnabled() && m_UI->resampleCloudCheckBox->isChecked();
}

unsigned char ccRasterizeTool::getProjectionDimension() const
{
	int dim = m_UI->dimensionComboBox->currentIndex();
	assert(dim >= 0 && dim < 3);

	return static_cast<unsigned char>(dim);
}

int ccRasterizeTool::getStdDevLayerIndex() const
{
	return  m_UI->stdDevLayerComboBox->currentIndex();
}

void ccRasterizeTool::resampleOptionToggled(bool state)
{
	m_UI->warningResampleWithAverageLabel->setVisible(m_UI->resampleCloudCheckBox->isChecked()
		&& (	getTypeOfProjection() == ccRasterGrid::PROJ_AVERAGE_VALUE
			||	getTypeOfProjection() == ccRasterGrid::PROJ_INVERSE_VAR_VALUE)
	);
	gridOptionChanged();
}

void ccRasterizeTool::updateStdDevLayerComboBox()
{
	m_UI->stdDevLayerComboBox->setEnabled(	m_UI->stdDevLayerComboBox->count() != 0
										&& (m_UI->heightProjectionComboBox->currentIndex() == ccRasterGrid::PROJ_INVERSE_VAR_VALUE
										||	m_UI->scalarFieldProjection->currentIndex() == ccRasterGrid::PROJ_INVERSE_VAR_VALUE) );
}

void ccRasterizeTool::projectionTypeChanged(int index)
{
	//we can't use the 'resample origin cloud' option with 'average height' projection
	//resampleCloudCheckBox->setEnabled(index != PROJ_AVERAGE_VALUE && index != PROJ_INVERSE_VAR_VALUE);
	//DGM: now we can! We simply display a warning message
	m_UI->warningResampleWithAverageLabel->setVisible
	(
		m_UI->resampleCloudCheckBox->isChecked()
		&& (	index == ccRasterGrid::PROJ_AVERAGE_VALUE
			||	index == ccRasterGrid::PROJ_INVERSE_VAR_VALUE)
	);

	updateStdDevLayerComboBox();

	gridIsUpToDate(false);
}

void ccRasterizeTool::sfProjectionTypeChanged(int index)
{
	updateStdDevLayerComboBox();

	gridIsUpToDate(false);
}

void ccRasterizeTool::projectionDirChanged(int dir)
{
	gridIsUpToDate(false); // will call updateGridInfo
}

void ccRasterizeTool::stdDevLayerChanged(int index)
{
	gridIsUpToDate(false);
}

void ccRasterizeTool::activeLayerChanged(int layerIndex, bool autoRedraw/*=true*/)
{
	if (m_UI->activeLayerComboBox->itemData(layerIndex).toInt() == LAYER_SF)
	{
		if (m_UI->activeLayerComboBox->itemText(layerIndex) != HILLSHADE_FIELD_NAME)
		{
			m_UI->projectSFCheckBox->setChecked(true); //force the choice of a SF projection strategy
			m_UI->projectSFCheckBox->setEnabled(false);
			m_UI->generateImagePushButton->setEnabled(true);
			m_UI->generateASCIIPushButton->setEnabled(false);
			m_UI->projectContoursOnAltCheckBox->setEnabled(true);
		}
		else
		{
			//m_UI->projectSFCheckBox->setChecked(false); //we shouldn't change that as it only impacts the other fields
			//m_UI->projectSFCheckBox->setEnabled(false);
			m_UI->generateImagePushButton->setEnabled(false);
			m_UI->generateASCIIPushButton->setEnabled(false);
			m_UI->projectContoursOnAltCheckBox->setEnabled(false);
		}
	}
	else
	{
		//m_UI->projectSFCheckBox->setChecked(false); //DGM: we can't force that, just let the user decide
		m_UI->projectSFCheckBox->setEnabled(m_cloudHasScalarFields); //we need SF fields!
		m_UI->generateImagePushButton->setEnabled(true);
		m_UI->generateASCIIPushButton->setEnabled(true);
		m_UI->projectContoursOnAltCheckBox->setEnabled(false);
	}

	if (m_rasterCloud)
	{
		//active layer = RGB colors
		if (m_UI->activeLayerComboBox->currentData().toInt() == LAYER_RGB)
		{
			if (!m_rasterCloud->hasColors())
			{
				gridIsUpToDate(false);
			}
			m_UI->gridLayerRangeLabel->setText("[0 ; 255]");

			m_rasterCloud->showColors(true);
			m_rasterCloud->showSF(false);
		}
		else
		{
			//does the selected 'layer' exist?
			int sfIndex = m_rasterCloud->getScalarFieldIndexByName(qPrintable(m_UI->activeLayerComboBox->itemText(layerIndex)));
			m_rasterCloud->setCurrentDisplayedScalarField(sfIndex);
			m_rasterCloud->showSF(true);
			m_rasterCloud->showColors(false);

			if (sfIndex >= 0)
			{
				ccScalarField* activeLayer = m_rasterCloud->getCurrentDisplayedScalarField();
				if (activeLayer)
				{
					const ccScalarField::Range& layerValues = activeLayer->displayRange();
					m_UI->gridLayerRangeLabel->setText(QString("%1 [%2 ; %3]").arg(layerValues.range()).arg(layerValues.min()).arg(layerValues.max()));
					m_UI->contourStartDoubleSpinBox->setValue(layerValues.min());
					m_UI->contourStepDoubleSpinBox->setValue(layerValues.range() / 10.0);
				}
				else
				{
					assert(false);
					m_UI->gridLayerRangeLabel->setText("no active layer?!");
				}
			}
			else
			{
				m_UI->gridLayerRangeLabel->setText("Layer not computed");
				gridIsUpToDate(false);
			}
		}

		if (m_glWindow && autoRedraw)
		{
			m_glWindow->redraw();
		}
	}
}

void ccRasterizeTool::fillEmptyCellStrategyChanged(int)
{
	ccRasterGrid::EmptyCellFillOption fillEmptyCellsStrategy = getFillEmptyCellsStrategy(m_UI->fillEmptyCellsComboBox);

	// empty cell value
	{
		bool active = (fillEmptyCellsStrategy == ccRasterGrid::FILL_CUSTOM_HEIGHT) || (fillEmptyCellsStrategy == ccRasterGrid::INTERPOLATE_DELAUNAY);
		m_UI->emptyValueDoubleSpinBox->setEnabled(active);
		m_UI->emptyValueDoubleSpinBox->setVisible(active);
	}

	// interpolation parameters button
	{
		m_UI->interpParamsToolButton->setEnabled(fillEmptyCellsStrategy == ccRasterGrid::INTERPOLATE_DELAUNAY || fillEmptyCellsStrategy == ccRasterGrid::KRIGING);
	}

	gridIsUpToDate(false);
}

void ccRasterizeTool::gridOptionChanged()
{
	gridIsUpToDate(false);
}

double ccRasterizeTool::getCustomHeightForEmptyCells() const
{
	return m_UI->emptyValueDoubleSpinBox->value();
}

double ccRasterizeTool::getStatisticsPercentileValue() const
{
	return m_UI->generateStatisticsPercentileDoubleSpinBox->value();
}

ccRasterGrid::ProjectionType ccRasterizeTool::getTypeOfProjection() const
{
	switch (m_UI->heightProjectionComboBox->currentIndex())
	{
	case 0:
		return ccRasterGrid::PROJ_MINIMUM_VALUE;
	case 1:
		return ccRasterGrid::PROJ_AVERAGE_VALUE;
	case 2:
		return ccRasterGrid::PROJ_MAXIMUM_VALUE;
	case 3:
		return ccRasterGrid::PROJ_MEDIAN_VALUE;
	case 4:
		return ccRasterGrid::PROJ_INVERSE_VAR_VALUE;
	default:
		//shouldn't be possible for this option!
		assert(false);
	}

	return ccRasterGrid::INVALID_PROJECTION_TYPE;
}

ccRasterGrid::ProjectionType ccRasterizeTool::getTypeOfSFProjection() const
{
	if (/*!m_UI->projectSFCheckBox->isEnabled() || */!m_UI->projectSFCheckBox->isChecked()) //DGM: the check-box might be disabled to actually 'force' the user to choose a projection type
	{
		return ccRasterGrid::INVALID_PROJECTION_TYPE; //means that we don't want to project SF values
	}

	switch (m_UI->scalarFieldProjection->currentIndex())
	{
	case 0:
		return ccRasterGrid::PROJ_MINIMUM_VALUE;
	case 1:
		return ccRasterGrid::PROJ_AVERAGE_VALUE;
	case 2:
		return ccRasterGrid::PROJ_MAXIMUM_VALUE;
	case 3:
		return ccRasterGrid::PROJ_MEDIAN_VALUE;
	case 4:
		return ccRasterGrid::PROJ_INVERSE_VAR_VALUE;
	default:
		//shouldn't be possible for this option!
		assert(false);
	}

	return ccRasterGrid::INVALID_PROJECTION_TYPE;
}

void ccRasterizeTool::loadSettings()
{
	QSettings settings;
	settings.beginGroup(ccPS::HeightGridGeneration());
	int projType							= settings.value("ProjectionType",        m_UI->heightProjectionComboBox->currentIndex()).toInt();
	int projDim								= settings.value("ProjectionDim",         m_UI->dimensionComboBox->currentIndex()).toInt();
	bool sfProj								= settings.value("SfProjEnabled",         m_UI->projectSFCheckBox->isChecked()).toBool();
	int sfProjStrategy						= settings.value("SfProjStrategy",        m_UI->scalarFieldProjection->currentIndex()).toInt();
	int fillStrategy						= settings.value("FillStrategy",          m_UI->fillEmptyCellsComboBox->currentIndex()).toInt();
	m_delaunayInterpParams.maxEdgeLength	= settings.value("MaxEdgeLength",         m_delaunayInterpParams.maxEdgeLength).toDouble();
	m_krigingParams.kNN						= settings.value("KrigingKNN",            m_krigingParams.kNN).toDouble();
	double step								= settings.value("GridStep",              m_UI->gridStepDoubleSpinBox->value()).toDouble();
	double emptyHeight						= settings.value("EmptyCellsHeight",      m_UI->emptyValueDoubleSpinBox->value()).toDouble();
	bool resampleCloud						= settings.value("ResampleOrigCloud",     m_UI->resampleCloudCheckBox->isChecked()).toBool();
	int minVertexCount						= settings.value("MinVertexCount",        m_UI->minVertexCountSpinBox->value()).toInt();
	bool ignoreBorders						= settings.value("IgnoreBorders",         m_UI->ignoreContourBordersCheckBox->isChecked()).toBool();
	bool projectContoursOnAlt				= settings.value("projectContoursOnAlt",  m_UI->projectContoursOnAltCheckBox->isChecked()).toBool();
	
	//Statistics checkboxes
	bool generateHeightStatistics				= settings.value("GenerateHeightStatistics",			m_UI->exportHeightStatsCheckBox->isChecked()).toBool();
	bool generateSFStatistics					= settings.value("GenerateSFStatistics",				m_UI->exportSFStatsCheckBox->isChecked()).toBool();
	bool generateStatisticsPopulation			= settings.value("GenerateStatisticsPopulation",		m_UI->generateStatisticsPopulationCheckBox->isChecked()).toBool();
	bool generateStatisticsMin					= settings.value("GenerateStatisticsMin",	   			m_UI->generateStatisticsMinCheckBox->isChecked()).toBool();
	bool generateStatisticsMax					= settings.value("GenerateStatisticsMax",	   			m_UI->generateStatisticsMaxCheckBox->isChecked()).toBool();
	bool generateStatisticsAverage				= settings.value("GenerateStatisticsAverage",	   		m_UI->generateStatisticsAverageCheckBox->isChecked()).toBool();
	bool generateStatisticsStdDev				= settings.value("GenerateStatisticsStdDev",	   		m_UI->generateStatisticsStdDevCheckBox->isChecked()).toBool();
	bool generateStatisticsRange				= settings.value("GenerateStatisticsRange",	   			m_UI->generateStatisticsRangeCheckBox->isChecked()).toBool();
	bool generateStatisticsMedian				= settings.value("GenerateStatisticsMedian",	   		m_UI->generateStatisticsMedianCheckBox->isChecked()).toBool();
	bool generateStatisticsUnique				= settings.value("GenerateStatisticsUnique",	   		m_UI->generateStatisticsUniqueCheckBox->isChecked()).toBool();
	bool generateStatisticsPercentile			= settings.value("GenerateStatisticsPercentile",		m_UI->generateStatisticsPercentileCheckBox->isChecked()).toBool();
	double generateStatisticsPercentileValue	= settings.value("GenerateStatisticsPercentileValue",	m_UI->generateStatisticsPercentileDoubleSpinBox->value()).toDouble();
	
	settings.endGroup();

	m_UI->gridStepDoubleSpinBox->setValue(step);
	m_UI->heightProjectionComboBox->setCurrentIndex(m_cloudHasScalarFields || projType != ccRasterGrid::PROJ_INVERSE_VAR_VALUE ? projType : 0);
	m_UI->fillEmptyCellsComboBox->setCurrentIndex(fillStrategy);
	m_UI->emptyValueDoubleSpinBox->setValue(emptyHeight);
	m_UI->dimensionComboBox->setCurrentIndex(projDim);
	m_UI->projectSFCheckBox->setChecked(m_cloudHasScalarFields && sfProj);
	m_UI->scalarFieldProjection->setCurrentIndex(m_cloudHasScalarFields || sfProjStrategy != ccRasterGrid::PROJ_INVERSE_VAR_VALUE ? sfProjStrategy : 0);
	m_UI->resampleCloudCheckBox->setChecked(resampleCloud);
	m_UI->minVertexCountSpinBox->setValue(minVertexCount);
	m_UI->ignoreContourBordersCheckBox->setChecked(ignoreBorders);
	m_UI->projectContoursOnAltCheckBox->setChecked(projectContoursOnAlt);

	//SF Statistics checkboxes
	m_UI->exportHeightStatsCheckBox->setChecked(				generateHeightStatistics						);
	m_UI->exportSFStatsCheckBox->setChecked(					generateSFStatistics && m_cloudHasScalarFields	);
	m_UI->generateStatisticsPopulationCheckBox->setChecked(		generateStatisticsPopulation					);
    m_UI->generateStatisticsMinCheckBox->setChecked(			generateStatisticsMin							);
    m_UI->generateStatisticsMaxCheckBox->setChecked(			generateStatisticsMax							);
    m_UI->generateStatisticsAverageCheckBox->setChecked(		generateStatisticsAverage						);
    m_UI->generateStatisticsStdDevCheckBox->setChecked(			generateStatisticsStdDev						);
    m_UI->generateStatisticsRangeCheckBox->setChecked(			generateStatisticsRange							);
    m_UI->generateStatisticsMedianCheckBox->setChecked(			generateStatisticsMedian						);
    m_UI->generateStatisticsUniqueCheckBox->setChecked(			generateStatisticsUnique						);
    m_UI->generateStatisticsPercentileCheckBox->setChecked(		generateStatisticsPercentile					);
	m_UI->generateStatisticsPercentileDoubleSpinBox->setValue(	generateStatisticsPercentileValue				);

}

bool ccRasterizeTool::canClose()
{
	if (!m_contourLines.empty())
	{
		//ask the user to confirm before it's tool late!
		if (QMessageBox::question(	this,
									"Unsaved contour lines",
									"Contour lines have not been exported! Do you really want to close the tool?",
									QMessageBox::Yes,
									QMessageBox::No) == QMessageBox::No)
			return false;
	}
	return true;
}

void ccRasterizeTool::testAndAccept()
{
	if (!canClose())
		return;
	
	saveSettings();
	accept();
}

void ccRasterizeTool::testAndReject()
{
	if (!canClose())
		return;
	
	reject();
}

void ccRasterizeTool::saveSettings()
{
	QSettings settings;
	settings.beginGroup(ccPS::HeightGridGeneration());
	settings.setValue("ProjectionType", m_UI->heightProjectionComboBox->currentIndex());
	settings.setValue("ProjectionDim", m_UI->dimensionComboBox->currentIndex());
	settings.setValue("SfProjEnabled", m_UI->projectSFCheckBox->isChecked());
	settings.setValue("SfProjStrategy", m_UI->scalarFieldProjection->currentIndex());
	settings.setValue("FillStrategy", m_UI->fillEmptyCellsComboBox->currentIndex());
	settings.setValue("MaxEdgeLength", m_delaunayInterpParams.maxEdgeLength);
	settings.setValue("KrigingKNN", m_krigingParams.kNN);
	settings.setValue("GridStep", m_UI->gridStepDoubleSpinBox->value());
	settings.setValue("EmptyCellsHeight", m_UI->emptyValueDoubleSpinBox->value());
	settings.setValue("ResampleOrigCloud", m_UI->resampleCloudCheckBox->isChecked());
	settings.setValue("MinVertexCount", m_UI->minVertexCountSpinBox->value());
	settings.setValue("IgnoreBorders", m_UI->ignoreContourBordersCheckBox->isChecked());
	settings.setValue("projectContoursOnAlt", m_UI->projectContoursOnAltCheckBox->isChecked());

	//SF Statistics checkboxes
	settings.setValue("generateStatisticsPopulation",	   	m_UI->generateStatisticsPopulationCheckBox->isChecked());
	settings.setValue("generateStatisticsMin",	   			m_UI->generateStatisticsMinCheckBox->isChecked());
	settings.setValue("generateStatisticsMax",	   			m_UI->generateStatisticsMaxCheckBox->isChecked());
	settings.setValue("generateStatisticsAverage",	   		m_UI->generateStatisticsAverageCheckBox->isChecked());
	settings.setValue("generateStatisticsStdDev",	   		m_UI->generateStatisticsStdDevCheckBox->isChecked());
	settings.setValue("generateStatisticsRange",	   		m_UI->generateStatisticsRangeCheckBox->isChecked());
	settings.setValue("generateStatisticsMedian",	   		m_UI->generateStatisticsMedianCheckBox->isChecked());
	settings.setValue("generateStatisticsUnique",	   		m_UI->generateStatisticsUniqueCheckBox->isChecked());
	settings.setValue("generateStatisticsPercentile",	   	m_UI->generateStatisticsPercentileCheckBox->isChecked());
	settings.setValue("generateStatisticsPercentileValue",	m_UI->generateStatisticsPercentileDoubleSpinBox->value());

	settings.endGroup();
}

void ccRasterizeTool::gridIsUpToDate(bool state)
{
	if (state)
	{
		//standard button
		m_UI->updateGridPushButton->setStyleSheet(QString());
	}
	else
	{
		//red button
		m_UI->updateGridPushButton->setStyleSheet("color: white; background-color:red;");
	}
	
	m_UI->updateGridPushButton->setDisabled(state);

	m_UI->tabWidget->setEnabled(state);

	updateGridInfo(state);
}

ccPointCloud* ccRasterizeTool::convertGridToCloud(	bool exportHeightStats,
													bool exportSFStats,
													const std::vector<ccRasterGrid::ExportableFields>& exportedStatistics,
													bool projectSFs,
													bool projectColors,
													bool copyHillshadeSF,
													const QString& activeSFName,
													double percentileValue,
													bool exportToOriginalCS,
													bool appendGridSizeToSFNames,
													ccProgressDialog* progressDialog/*=nullptr*/ ) const
{
	if (!m_cloud || !m_grid.isValid())
		return nullptr;

	//call parent method
	ccPointCloud* cloudGrid = cc2Point5DimEditor::convertGridToCloud(	exportHeightStats,
																		exportSFStats,
																		exportedStatistics,
																		projectSFs,
																		projectColors,
																		/*resampleInputCloudXY=*/resampleOriginalCloud(),
																		/*resampleInputCloudZ=*/getTypeOfProjection() != ccRasterGrid::PROJ_AVERAGE_VALUE,
																		/*inputCloud=*/m_cloud,
                                            							percentileValue,
																		exportToOriginalCS,
																		appendGridSizeToSFNames,
																		progressDialog );

	//success?
	if (cloudGrid)
	{
		//add the hillshade SF
		if (copyHillshadeSF)
		{
			int hillshadeSFIdx = m_rasterCloud->getScalarFieldIndexByName(HILLSHADE_FIELD_NAME);
			if (hillshadeSFIdx >= 0)
			{
				CCCoreLib::ScalarField* hillshadeField = m_rasterCloud->getScalarField(hillshadeSFIdx);
				if (hillshadeField->currentSize() == cloudGrid->size())
				{
					try
					{
						ccScalarField* hillshadeClone = new ccScalarField(*static_cast<ccScalarField*>(hillshadeField));
						cloudGrid->addScalarField(hillshadeClone);
					}
					catch (const std::bad_alloc&)
					{
						ccLog::Warning("[Rasterize] Not enough memory to export the hillshade field");
					}
				}
			}
		}

		//currently displayed SF
		int activeSFIndex = cloudGrid->getScalarFieldIndexByName(qPrintable(activeSFName));
		if (activeSFIndex < 0 && cloudGrid->getNumberOfScalarFields() != 0)
		{
			//if no SF is displayed, we should at least set a valid one (for later)
			activeSFIndex = 0;
		}
		cloudGrid->setCurrentDisplayedScalarField(activeSFIndex);

		cloudGrid->showColors(projectColors && cloudGrid->hasColors());
		cloudGrid->showSF(activeSFIndex >= 0);

		//don't forget the original shift
		cloudGrid->copyGlobalShiftAndScale(*m_cloud);
	}

	return cloudGrid;
}

void ccRasterizeTool::updateGridAndDisplay()
{
	//special case: remove the (temporary) hillshade field entry
	int hillshadeIndex = m_UI->activeLayerComboBox->findText(HILLSHADE_FIELD_NAME);
	if (hillshadeIndex >= 0)
	{
		if (m_UI->activeLayerComboBox->currentIndex() == hillshadeIndex && m_UI->activeLayerComboBox->count() > 1)
		{
			m_UI->activeLayerComboBox->setCurrentIndex(0);
		}
		m_UI->activeLayerComboBox->removeItem(hillshadeIndex);
	}

	//remove the previous cloud
	if (m_rasterCloud)
	{
		if (m_glWindow)
		{
			m_glWindow->removeFromOwnDB(m_rasterCloud);
			m_glWindow->redraw();
		}
		
		delete m_rasterCloud;
		m_rasterCloud = nullptr;
	}

	bool activeLayerIsSF = (m_UI->activeLayerComboBox->currentData().toInt() == LAYER_SF);
	bool projectSFs = (getTypeOfSFProjection() != ccRasterGrid::INVALID_PROJECTION_TYPE) || activeLayerIsSF;
	bool projectColors = m_cloud && m_cloud->hasColors();
	bool success = updateGrid(projectSFs);

	if (success && m_glWindow)
	{
		//convert grid to point cloud
		try
		{
			//we always compute the default 'height' layer
			std::vector<ccRasterGrid::ExportableFields> exportedStatistics(1);
			exportedStatistics.front() = ccRasterGrid::PER_CELL_VALUE;
			//but we may also have to compute the 'original SF(s)' layer(s)
			QString activeLayerName = m_UI->activeLayerComboBox->currentText();
			m_rasterCloud = convertGridToCloud(	true,
												false,
												exportedStatistics,
												projectSFs,
												projectColors,
												/*copyHillshadeSF=*/false,
												activeLayerName,
												getStatisticsPercentileValue(),
												false,
												false,
												nullptr );

			// Special case: the 'LAYER_HEIGHT' field has now a dynamic name
			if (m_UI->activeLayerComboBox->currentIndex() == 0
				&& m_rasterCloud
				&& m_rasterCloud->getNumberOfScalarFields() != 0 )
			{
				assert(m_UI->activeLayerComboBox->itemData(0).toInt() == LAYER_HEIGHT);
				m_UI->activeLayerComboBox->setItemText(0, QString(m_rasterCloud->getScalarField(0)->getName()));
			}
		}
		catch (const std::bad_alloc&)
		{
			//see below
		}

		if (m_rasterCloud)
		{
			//just in case
			m_rasterCloud->setVisible(true);
			m_rasterCloud->setEnabled(true);

			m_glWindow->addToOwnDB(m_rasterCloud);
			ccBBox box = m_rasterCloud->getDisplayBB_recursive(false, m_glWindow);
			update2DDisplayZoom(box);

			//update 
			activeLayerChanged(m_UI->activeLayerComboBox->currentIndex(), false);
		}
		else
		{
			ccLog::Error("Not enough memory!");
			m_glWindow->redraw();
		}
	}

	gridIsUpToDate(success);
}

bool ccRasterizeTool::updateGrid(bool projectSFs/*=false*/)
{
	if (!m_cloud)
	{
		assert(false);
		return false;
	}

	//main parameters
	ccRasterGrid::ProjectionType projectionType = getTypeOfProjection();
	ccRasterGrid::ProjectionType sfProjectionType = projectSFs ? getTypeOfSFProjection() : ccRasterGrid::INVALID_PROJECTION_TYPE;

	ccRasterGrid::InterpolationType interpolationType = ccRasterGrid::InterpolationTypeFromEmptyCellFillOption(getFillEmptyCellsStrategy(m_UI->fillEmptyCellsComboBox));
	void* interpolationParams = nullptr;
	switch (interpolationType)
	{
	case ccRasterGrid::InterpolationType::DELAUNAY:
		interpolationParams = (void*)&m_delaunayInterpParams;
		break;
	case ccRasterGrid::InterpolationType::KRIGING:
		interpolationParams = (void*)&m_krigingParams;
		break;
	default:
		// do nothing
		break;
	}

	//cloud bounding-box --> grid size
	ccBBox box = getCustomBBox();
	if (!box.isValid())
	{
		return false;
	}

	//clear volume info
	{
		m_UI->volumeLabel->setText("0");
		m_UI->filledCellsPercentageLabel->setText("0 %");
	}

	unsigned gridWidth = 0;
	unsigned gridHeight = 0;
	if (!getGridSize(gridWidth, gridHeight))
	{
		return false;
	}

	//grid size
	unsigned gridTotalSize = gridWidth * gridHeight;
	if (gridTotalSize == 1)
	{
		if (QMessageBox::question(	this,
									"Unexpected grid size",
									"The generated grid will only have 1 cell! Do you want to proceed anyway?",
									QMessageBox::Yes,
									QMessageBox::No) == QMessageBox::No)
			return false;
	}
	else if (gridTotalSize > 10000000)
	{
		if (QMessageBox::question(	this,
									"Big grid size",
									"The generated grid will have more than 10.000.000 cells! Do you want to proceed anyway?",
									QMessageBox::Yes,
									QMessageBox::No) == QMessageBox::No)
			return false;
	}

	removeContourLines();

	//grid step
	double gridStep = getGridStep();
	assert(gridStep != 0);

	//memory allocation
	CCVector3d minCorner = box.minCorner();
	if (!m_grid.init(gridWidth, gridHeight, gridStep, minCorner))
	{
		//not enough memory
		ccLog::Error("Not enough memory");
		return false;
	}
	
	//vertical dimension
	const unsigned char Z = getProjectionDimension();
	assert(Z <= 2);

    int zStdDevSfIndex = getStdDevLayerIndex();

	ccProgressDialog pDlg(true, this);
	if (!m_grid.fillWith(	m_cloud,
							Z,
							projectionType,
							interpolationType,
							interpolationParams,
							sfProjectionType,
							&pDlg,
                            zStdDevSfIndex))
	{
		return false;
	}

	//fill empty cells (if necessary)
	ccRasterGrid::EmptyCellFillOption fillEmptyCellsStrategy = getFillEmptyCellsStrategy(m_UI->fillEmptyCellsComboBox);
	double customEmptyCellsHeight = getCustomHeightForEmptyCells();
	m_grid.fillEmptyCells(fillEmptyCellsStrategy, customEmptyCellsHeight);

	//update volume estimate
	{
		double hSum = 0;
		unsigned filledCellCount = 0;
		for (unsigned j = 0; j < m_grid.height; ++j)
		{
			const ccRasterGrid::Row& row = m_grid.rows[j];
			for (unsigned i = 0; i < m_grid.width; ++i)
			{
				if (std::isfinite(row[i].h))
				{
					hSum += row[i].h;
					++filledCellCount;
				}
			}
		}

		if (filledCellCount)
		{
			double cellArea = m_grid.gridStep * m_grid.gridStep;
			m_UI->volumeLabel->setText(QString::number(hSum * cellArea));
			m_UI->filledCellsPercentageLabel->setText(QString::number(static_cast<double>(100 * filledCellCount) / (m_grid.width * m_grid.height), 'f', 2) + " %");
		}
	}

	ccLog::Print(QString("[Rasterize] Current raster grid:\n\tSize: %1 x %2\n\tHeight values: [%3 ; %4]").arg(m_grid.width).arg(m_grid.height).arg(m_grid.minHeight).arg(m_grid.maxHeight));
	
	return true;
}

ccPointCloud* ccRasterizeTool::generateCloud(bool autoExport/*=true*/)
{
	if (!m_cloud || !m_grid.isValid())
	{
		return nullptr;
	}

	//look for statistics fields (min,max,median,etc) fields to be exported
	std::vector<ccRasterGrid::ExportableFields> exportedStatistics;
	bool exportHeightStats = m_UI->exportHeightStatsCheckBox->isChecked();
	bool exportSFStats = m_UI->exportSFStatsCheckBox->isEnabled() && m_UI->exportSFStatsCheckBox->isChecked();
	if (exportHeightStats || exportSFStats)
	{
		try
		{
			getExportedStats(exportedStatistics);
		}
		catch (const std::bad_alloc&)
		{
			ccLog::Error("Not enough memory!");
			return nullptr;
		}
	}


	QString activeLayerName = m_UI->activeLayerComboBox->currentText();
	bool activeLayerIsSF = (m_UI->activeLayerComboBox->currentData().toInt() == LAYER_SF);
	bool projectSFs = (getTypeOfSFProjection() != ccRasterGrid::INVALID_PROJECTION_TYPE) || activeLayerIsSF;
	//bool activeLayerIsRGB = (activeLayerComboBox->currentData().toInt() == LAYER_RGB);
	bool projectColors = m_cloud->hasColors();

	ccProgressDialog pDlg(true, this);
	ccPointCloud* rasterCloud = convertGridToCloud(	exportHeightStats,
													exportSFStats,
													exportedStatistics,
													projectSFs,
													projectColors,
													/*copyHillshadeSF=*/true,
													activeLayerName,
													getStatisticsPercentileValue(),
													true,
													true, // we want nicer SF names
                                                    &pDlg );

	if (rasterCloud && autoExport)
	{
		if (m_cloud->getParent())
		{
			m_cloud->getParent()->addChild(rasterCloud);
		}
		rasterCloud->setDisplay(m_cloud->getDisplay());
		
		if (m_cloud->isEnabled())
		{
			m_cloud->setEnabled(false);
			ccLog::Warning("[Rasterize] Previously selected entity (source cloud) has been hidden!");
		}

		MainWindow* mainWindow = MainWindow::TheInstance();
		if (mainWindow)
		{
			mainWindow->addToDB(rasterCloud);
			ccLog::Print(QString("[Rasterize] Cloud '%1' successfully exported").arg(rasterCloud->getName()));
		}
		else
		{
			assert(false);
			delete rasterCloud;
		}
	}

	return rasterCloud;
}

void ccRasterizeTool::generateMesh()
{
	if (!m_cloud)
	{
		assert(false);
		return;
	}

	ccPointCloud* rasterCloud = generateCloud(false);
	if (rasterCloud)
	{
		std::string errorStr;
		CCCoreLib::GenericIndexedMesh* baseMesh = CCCoreLib::PointProjectionTools::computeTriangulation(rasterCloud,
																								CCCoreLib::DELAUNAY_2D_AXIS_ALIGNED,
																								CCCoreLib::PointProjectionTools::IGNORE_MAX_EDGE_LENGTH,
																								getProjectionDimension(),
																								errorStr);
		ccMesh* rasterMesh = nullptr;
		if (baseMesh)
		{
			rasterMesh = new ccMesh(baseMesh, rasterCloud);
			delete baseMesh;
			baseMesh = nullptr;
		}
		if (rasterMesh)
		{
			if (m_cloud->getParent())
				m_cloud->getParent()->addChild(rasterMesh);
			rasterCloud->setEnabled(false);
			rasterCloud->setVisible(true);
			rasterCloud->setName("vertices");
			rasterMesh->addChild(rasterCloud);
			rasterMesh->setDisplay_recursive(m_cloud->getDisplay());
			rasterMesh->setName(m_cloud->getName() + ".mesh");
			rasterMesh->showSF(rasterCloud->sfShown());
			rasterMesh->showColors(rasterCloud->colorsShown());

			MainWindow* mainWindow = MainWindow::TheInstance();
			if (mainWindow)
				MainWindow::TheInstance()->addToDB(rasterMesh);
			ccLog::Print(QString("[Rasterize] Mesh '%1' successfully exported").arg(rasterMesh->getName()));
		}
		else
		{
			ccLog::Error( QStringLiteral("Failed to create mesh ('%1')")
						  .arg( QString::fromStdString( errorStr ) ) );
		}
	}
}

#ifdef CC_GDAL_SUPPORT
class RasterExportOptionsDlg
	: public QDialog
	, public Ui::RasterExportOptionsDialog
{
public:

	explicit RasterExportOptionsDlg(QWidget* parent = nullptr)
		: QDialog(parent, Qt::Tool)
		, Ui::RasterExportOptionsDialog()
	{
		setupUi(this);
	}
};
#endif

void ccRasterizeTool::generateRaster() const
{
#ifdef CC_GDAL_SUPPORT

	if (!m_cloud || !m_grid.isValid())
	{
		return;
	}

	bool hasScalarFields = !m_grid.scalarFields.empty();
	int visibleSfIndex = -1;
	if (m_UI->activeLayerComboBox->currentData().toInt() == LAYER_SF && m_cloud->isA(CC_TYPES::POINT_CLOUD))
	{
		//the indexes in the 'm_grid.scalarFields' are the same as in the cloud
		visibleSfIndex = static_cast<ccPointCloud*>(m_cloud)->getScalarFieldIndexByName(qPrintable(m_UI->activeLayerComboBox->currentText()));
	}

	//which (and how many) bands shall we create?
	ExportBands exportBands;
	exportBands.height = true; //height by default
	exportBands.rgb = false; //not a good idea to mix RGB and height values!

	RasterExportOptionsDlg reoDlg;
	reoDlg.dimensionsLabel->setText(QString("%1 x %2").arg(m_grid.width).arg(m_grid.height));
	reoDlg.exportRGBCheckBox->setEnabled(m_grid.hasColors);
	reoDlg.exportRGBCheckBox->setChecked(exportBands.rgb);
	reoDlg.exportHeightsCheckBox->setChecked(exportBands.height);
	reoDlg.exportDensityCheckBox->setChecked(exportBands.density);
	reoDlg.exportActiveLayerCheckBox->setChecked(exportBands.visibleSF);
	reoDlg.exportActiveLayerCheckBox->setEnabled(visibleSfIndex >= 0);
	reoDlg.exportAllSFCheckBox->setEnabled(hasScalarFields);
	reoDlg.exportAllSFCheckBox->setChecked(exportBands.allSFs);

	while (true)
	{
		if (!reoDlg.exec())
		{
			//cancelled by user
			return;
		}

		//check the selection
		if (	reoDlg.exportRGBCheckBox->isEnabled()
			&&	reoDlg.exportRGBCheckBox->isChecked()
			&& (	reoDlg.exportHeightsCheckBox->isChecked()
				||	reoDlg.exportDensityCheckBox->isChecked()
				||	reoDlg.exportActiveLayerCheckBox->isChecked()
				||	reoDlg.exportAllSFCheckBox->isChecked())
			)
		{
			if (QMessageBox::warning(	nullptr,
										"Mixed raster",
										"Mixing colors and other layers will result in\na strange raster file with 64 bits color bands\n(some applications won't handle them properly)",
										QMessageBox::Ignore,
										QMessageBox::Retry) == QMessageBox::Ignore)
			{
				//the user ignored the warning
				break;
			}
		}
		else
		{
			//nothing to worry about :D
			break;
		}
	}

	//we ask the output filename AFTER displaying the export parameters ;)
	QString outputFilename;
	{
		QSettings settings;
		settings.beginGroup(ccPS::HeightGridGeneration());
		QString imageSavePath = settings.value("savePathImage", ccFileUtils::defaultDocPath()).toString();
		outputFilename = QFileDialog::getSaveFileName(	const_cast<ccRasterizeTool*>(this),
														"Save height grid raster",
														imageSavePath + QString("/raster.tif"),
														"geotiff (*.tif)");

		if (outputFilename.isNull())
		{
			return;
		}

		//save current export path to persistent settings
		settings.setValue("savePathImage", QFileInfo(outputFilename).absolutePath());
	}

	exportBands.height    = reoDlg.exportHeightsCheckBox->isChecked();
	exportBands.rgb       = reoDlg.exportRGBCheckBox->isChecked();
	exportBands.density   = reoDlg.exportDensityCheckBox->isChecked();
	exportBands.allSFs    = reoDlg.exportAllSFCheckBox->isChecked();
	exportBands.visibleSF = reoDlg.exportActiveLayerCheckBox->isChecked();

	ExportGeoTiff
	(
		outputFilename,
		exportBands,
		getFillEmptyCellsStrategy(m_UI->fillEmptyCellsComboBox),
		m_grid,
		getCustomBBox(),
		getProjectionDimension(),
		getCustomHeightForEmptyCells(),
		m_cloud,
		visibleSfIndex
	);

#else

	assert(false);
	ccLog::Error("[Rasterize] GDAL not supported by this version! Can't generate a raster...");

#endif
}
		
bool ccRasterizeTool::ExportGeoTiff(const QString& outputFilename,
									const ExportBands& exportBands,
									ccRasterGrid::EmptyCellFillOption fillEmptyCellsStrategy,
									const ccRasterGrid& grid,
									const ccBBox& gridBBox,
									unsigned char Z,
									double customHeightForEmptyCells/*=std::numeric_limits<double>::quiet_NaN()*/,
									ccGenericPointCloud* originCloud/*=nullptr*/,
									int visibleSfIndex/*=-1*/)
{
#ifdef CC_GDAL_SUPPORT

	if (exportBands.visibleSF && visibleSfIndex < 0)
	{
		assert(false);
		return false;
	}

	//vertical dimension
	assert(Z <= 2);
	const unsigned char X = (Z == 2 ? 0 : Z + 1);
	const unsigned char Y = (X == 2 ? 0 : X + 1);

	double stepX = grid.gridStep;
	double stepY = grid.gridStep;

	//global shift
	assert(gridBBox.isValid());
	double shiftX = gridBBox.minCorner().u[X] - stepX / 2; //we will declare the raster grid as 'Pixel-is-area'!
	double shiftY = gridBBox.maxCorner().u[Y] + stepY / 2; //we will declare the raster grid as 'Pixel-is-area'!
	double shiftZ = 0.0;

	if (originCloud)
	{
		const CCVector3d& shift = originCloud->getGlobalShift();
		shiftX -= shift.u[X];
		shiftY -= shift.u[Y];
		shiftZ -= shift.u[Z];

		double scale = originCloud->getGlobalScale();
		assert(scale != 0);
		stepX /= scale;
		stepY /= scale;
	}

	int totalBands = 0;
	bool onlyRGBA = true;

	if (exportBands.height)
	{
		++totalBands;
		onlyRGBA = false;
	}
	
	bool rgbaMode = false;
	if (exportBands.rgb)
	{
		totalBands += 3; //one per component
		if (fillEmptyCellsStrategy == ccRasterGrid::LEAVE_EMPTY && grid.validCellCount < grid.height * grid.width)
		{
			rgbaMode = true;
			++totalBands; //alpha
		}
	}
	else
	{
		onlyRGBA = false;
	}
	
	if (exportBands.density)
	{
		++totalBands;
		onlyRGBA = false;
	}
	
	if (exportBands.allSFs)
	{
		for (const auto & scalarField : grid.scalarFields)
		{
			if (!scalarField.empty())
			{
				++totalBands;
				onlyRGBA = false;
			}
		}
	}
	else if (exportBands.visibleSF && visibleSfIndex >= 0)
	{
		++totalBands;
		onlyRGBA = false;
	}
	
	if (totalBands == 0)
	{
		ccLog::Error("Can't output a raster with no band! (check export parameters)");
		return false;
	}

	GDALAllRegister();
	ccLog::PrintDebug("(GDAL drivers: %i)", GetGDALDriverManager()->GetDriverCount());

	const char pszFormat[] = "GTiff";
	GDALDriver* poDriver = GetGDALDriverManager()->GetDriverByName(pszFormat);
	if (!poDriver)
	{
		ccLog::Error("[GDAL] Driver %s is not supported", pszFormat);
		return false;
	}

	char** papszMetadata = poDriver->GetMetadata();
	if (!CSLFetchBoolean(papszMetadata, GDAL_DCAP_CREATE, FALSE))
	{
		ccLog::Error("[GDAL] Driver %s doesn't support Create() method", pszFormat);
		return false;
	}

	char **papszOptions = nullptr;
	GDALDataset* poDstDS = poDriver->Create(qUtf8Printable(outputFilename),
											static_cast<int>(grid.width),
											static_cast<int>(grid.height),
											totalBands,
											onlyRGBA ? GDT_Byte : GDT_Float64,
											papszOptions);

	if (!poDstDS)
	{
		ccLog::Error("[GDAL] Failed to create output raster (not enough memory?)");
		return false;
	}

	poDstDS->SetMetadataItem("AREA_OR_POINT", "AREA");

	double adfGeoTransform[6] {	shiftX,		//top left x
								stepX,		//w-e pixel resolution (can be negative)
								0,			//0
								shiftY,		//top left y
								0,			//0
								-stepY		//n-s pixel resolution (can be negative)
	};

	poDstDS->SetGeoTransform( adfGeoTransform );

	//OGRSpatialReference oSRS;
	//oSRS.SetUTM( 11, TRUE );
	//oSRS.SetWellKnownGeogCS( "NAD27" );
	//char *pszSRS_WKT = nullptr;
	//oSRS.exportToWkt( &pszSRS_WKT );
	//poDstDS->SetProjection( pszSRS_WKT );
	//CPLFree( pszSRS_WKT );

	int currentBand = 0;

	//exort RGB band?
	if (exportBands.rgb)
	{
		GDALRasterBand* rgbBands[3] {	poDstDS->GetRasterBand(++currentBand),
										poDstDS->GetRasterBand(++currentBand),
										poDstDS->GetRasterBand(++currentBand) };
		rgbBands[0]->SetColorInterpretation(GCI_RedBand);
		rgbBands[1]->SetColorInterpretation(GCI_GreenBand);
		rgbBands[2]->SetColorInterpretation(GCI_BlueBand);

		unsigned char* cLine = static_cast<unsigned char*>(CPLMalloc(sizeof(unsigned char)*grid.width));
		if (!cLine)
		{
			ccLog::Error("[GDAL] Not enough memory");
			GDALClose(poDstDS);
			return false;
		}

		bool error = false;
		
		//export the R, G and B components
		for (unsigned k = 0; k < 3; ++k)
		{
			rgbBands[k]->SetStatistics(0, 255, 128, 0); //warning: arbitrary average and std. dev. values

			for (unsigned j = 0; j < grid.height; ++j)
			{
				const ccRasterGrid::Row& row = grid.rows[grid.height - 1 - j]; //the first row is the northest one (i.e. Ymax)
				for (unsigned i = 0; i < grid.width; ++i)
				{
					cLine[i] = (std::isfinite(row[i].h) ? static_cast<unsigned char>(std::max(0.0, std::min(255.0, row[i].color.u[k]))) : 0);
				}

				if (rgbBands[k]->RasterIO(GF_Write, 0, static_cast<int>(j), static_cast<int>(grid.width), 1, cLine, static_cast<int>(grid.width), 1, GDT_Byte, 0, 0) != CE_None)
				{
					error = true;
					k = 3; //early stop
					break;
				}
			}
		}

		//export the alpha band (if necessary)
		if (!error && rgbaMode)
		{
			GDALRasterBand* aBand = poDstDS->GetRasterBand(++currentBand);
			aBand->SetColorInterpretation(GCI_AlphaBand);
			aBand->SetStatistics(0, 255, 255, 0); //warning: arbitrary average and std. dev. values

			for (unsigned j = 0; j < grid.height; ++j)
			{
				const ccRasterGrid::Row& row = grid.rows[grid.height - 1 - j];
				for (unsigned i = 0; i < grid.width; ++i)
				{
					cLine[i] = (std::isfinite(row[i].h) ? 255 : 0);
				}

				if (aBand->RasterIO(GF_Write, 0, static_cast<int>(j), static_cast<int>(grid.width), 1, cLine, static_cast<int>(grid.width), 1, GDT_Byte, 0, 0) != CE_None)
				{
					error = true;
					break;
				}
			}
		}

		CPLFree(cLine);

		if (error)
		{
			ccLog::Error("[GDAL] An error occurred while writing the color bands!");
			GDALClose(poDstDS);
			return false;
		}
	}

	double* scanline = static_cast<double*>(CPLMalloc(sizeof(double)*grid.width));
	if (!scanline)
	{
		ccLog::Error("[GDAL] Not enough memory");
		GDALClose(poDstDS);
		return false;
	}

	//exort height band?
	if (exportBands.height)
	{
		GDALRasterBand* poBand = poDstDS->GetRasterBand(++currentBand);
		assert(poBand);
		poBand->SetColorInterpretation(GCI_Undefined);

		double emptyCellHeight = 0.0;
		switch (fillEmptyCellsStrategy)
		{
		case ccRasterGrid::LEAVE_EMPTY:
		{
			emptyCellHeight = std::numeric_limits<double>::quiet_NaN();
			if (CE_None != poBand->SetNoDataValue(emptyCellHeight))
			{
				ccLog::Warning("[GDAL] Failed to set the No Data value");
			}
			break;
		}
		case ccRasterGrid::FILL_MINIMUM_HEIGHT:
			emptyCellHeight = grid.minHeight;
			break;
		case ccRasterGrid::FILL_MAXIMUM_HEIGHT:
			emptyCellHeight = grid.maxHeight;
			break;
		case ccRasterGrid::FILL_CUSTOM_HEIGHT:
		case ccRasterGrid::INTERPOLATE_DELAUNAY:
			emptyCellHeight = customHeightForEmptyCells;
			break;
		case ccRasterGrid::FILL_AVERAGE_HEIGHT:
			emptyCellHeight = grid.meanHeight;
			break;
		default:
			assert(false);
		}

		if (fillEmptyCellsStrategy != ccRasterGrid::LEAVE_EMPTY)
		{
			emptyCellHeight += shiftZ;
		}

		for (unsigned j = 0; j < grid.height; ++j)
		{
			const ccRasterGrid::Row& row = grid.rows[grid.height - 1 - j];
			for (unsigned i = 0; i < grid.width; ++i)
			{
				scanline[i] = std::isfinite(row[i].h) ? row[i].h + shiftZ : emptyCellHeight;
			}

			if (poBand->RasterIO(	GF_Write,
									0,
									static_cast<int>(j),
									static_cast<int>(grid.width),
									1,
									scanline,
									static_cast<int>(grid.width),
									1,
									GDT_Float64,
									0,
									0) != CE_None)
			{
				ccLog::Error("[GDAL] An error occurred while writing the height band!");
				if (scanline)
				{
					CPLFree(scanline);
				}
				GDALClose(poDstDS);
				return false;
			}
		}
	}

	//export density band
	if (exportBands.density)
	{
		GDALRasterBand* poBand = poDstDS->GetRasterBand(++currentBand);
		assert(poBand);
		poBand->SetColorInterpretation(GCI_Undefined);
		for (unsigned j = 0; j < grid.height; ++j)
		{
			const ccRasterGrid::Row& row = grid.rows[grid.height - 1 - j];
			for (unsigned i = 0; i < grid.width; ++i)
			{
				scanline[i] = row[i].nbPoints;
			}

			if (poBand->RasterIO(	GF_Write,
									0,
									static_cast<int>(j),
									static_cast<int>(grid.width),
									1,
									scanline,
									static_cast<int>(grid.width),
									1,
									GDT_Float64,
									0,
									0) != CE_None)
			{
				ccLog::Error("[GDAL] An error occurred while writing the density band!");
				if (scanline)
				{
					CPLFree(scanline);
				}
				GDALClose(poDstDS);
				return false;
			}
		}
	}

	//export SF bands
	if (exportBands.allSFs || (exportBands.visibleSF && visibleSfIndex >= 0))
	{
		for (size_t k = 0; k < grid.scalarFields.size(); ++k)
		{
			assert(!grid.scalarFields[k].empty());
			if (exportBands.allSFs || (exportBands.visibleSF && visibleSfIndex == static_cast<int>(k)))
			{
				const double* sfGrid = grid.scalarFields[k].data();
				GDALRasterBand* poBand = poDstDS->GetRasterBand(++currentBand);

				double sfNanValue = std::numeric_limits<ccRasterGrid::SF::value_type>::quiet_NaN();
				poBand->SetNoDataValue(sfNanValue); //should be transparent!
				assert(poBand);
				poBand->SetColorInterpretation(GCI_Undefined);

				for (unsigned j = 0; j < grid.height; ++j)
				{
					const ccRasterGrid::Row& row = grid.rows[grid.height - 1 - j];
					const double* sfRow = sfGrid + (grid.height - 1 - j) * grid.width;
					for (unsigned i = 0; i < grid.width; ++i)
					{
						scanline[i] = std::isfinite(sfRow[i]) ? sfRow[i] : sfNanValue;
					}

					if (poBand->RasterIO(	GF_Write,
											0,
											static_cast<int>(j),
											static_cast<int>(grid.width),
											1,
											scanline,
											static_cast<int>(grid.width),
											1,
											GDT_Float64,
											0,
											0 ) != CE_None)
					{
						//the corresponding SF should exist on the input cloud
						ccLog::Error(QString("[GDAL] An error occurred while writing a scalar field band!"));
						k = grid.scalarFields.size(); //quick stop
						break;
					}
				}
			}
		}
	}

	if (scanline)
	{
		CPLFree(scanline);
	}
	scanline = nullptr;

	/* Once we're done, close properly the dataset */
	GDALClose(poDstDS);

	ccLog::Print(QString("[Rasterize] Raster '%1' successfully saved").arg(outputFilename));
	return true;

#else
	assert(false);
	ccLog::Error("[Rasterize] GDAL not supported by this version! Can't generate a raster...");
	return false;
#endif
}

//See http://edndoc.esri.com/arcobjects/9.2/net/shared/geoprocessing/spatial_analyst_tools/how_hillshade_works.htm
void ccRasterizeTool::generateHillshade()
{
	if (!m_grid.isValid() || !m_rasterCloud)
	{
		ccLog::Error("Need a valid raster/cloud to generate hillshade!");
		return;
	}
	if (m_grid.height < 3 || m_grid.width < 3)
	{
		ccLog::Error("Grid is too small");
		return;
	}

	//get/create layer
	ccScalarField* hillshadeLayer = nullptr;
	int sfIdx = m_rasterCloud->getScalarFieldIndexByName(HILLSHADE_FIELD_NAME);
	if (sfIdx >= 0)
	{
		hillshadeLayer = static_cast<ccScalarField*>(m_rasterCloud->getScalarField(sfIdx));
	}
	else
	{
		hillshadeLayer = new ccScalarField(HILLSHADE_FIELD_NAME);
		if (!hillshadeLayer->reserveSafe(m_rasterCloud->size()))
		{
			ccLog::Error("Not enough memory!");
			hillshadeLayer->release();
			hillshadeLayer = nullptr;
			return;
		}

		sfIdx = m_rasterCloud->addScalarField(hillshadeLayer);
		m_UI->activeLayerComboBox->addItem(HILLSHADE_FIELD_NAME, QVariant(LAYER_SF));
		m_UI->activeLayerComboBox->setEnabled(true);
	}
	assert(hillshadeLayer && hillshadeLayer->currentSize() == m_rasterCloud->size());
	hillshadeLayer->fill(CCCoreLib::NAN_VALUE);

	bool sparseSF = (hillshadeLayer->currentSize() != m_grid.height * m_grid.width);
	bool resampleInputCloudXY = resampleOriginalCloud();

	//now we can compute the hillshade
	int zenith_deg = m_UI->sunZenithSpinBox->value();
	double zenith_rad = CCCoreLib::DegreesToRadians(static_cast<double>(zenith_deg));

	double cos_zenith_rad = cos(zenith_rad);
	double sin_zenith_rad = sin(zenith_rad);

	int azimuth_deg = m_UI->sunAzimuthSpinBox->value();
	int azimuth_math = 360 - azimuth_deg + 90;
	double azimuth_rad = CCCoreLib::DegreesToRadians(static_cast<double>(azimuth_math));

	//for all cells
	unsigned nonEmptyCellIndex = 0;
	unsigned validButEmptyCellIndex = 0;
	for (unsigned j = 0; j < m_grid.height - 1; ++j)
	{
		const ccRasterGrid::Row& row = m_grid.rows[j];

		for (unsigned i = 0; i < m_grid.width; ++i)
		{
			//valid height value
			const ccRasterCell& cell = row[i];
			if (std::isfinite(cell.h))
			{
				if (j != 0 && i != 0 && i + 1 != m_grid.width)
				{
					double dz_dx = 0.0;
					int dz_dx_count = 0;
					double dz_dy = 0.0;
					int dz_dy_count = 0;

					for (int di = -1; di <= 1; ++di)
					{
						for (int dj = -1; dj <= 1; ++dj)
						{
							const ccRasterCell& n = m_grid.rows[j - dj][i + di]; //-dj (instead of + dj) because we scan the grid in the reverse orientation! (from bottom to top)
							if (std::isfinite(n.h))
							{
								if (di != 0)
								{
									int dx_weight = (dj == 0 ? 2 : 1);
									dz_dx += (di < 0 ? -1.0 : 1.0) * dx_weight * n.h;
									dz_dx_count += dx_weight;
								}

								if (dj != 0)
								{
									int dy_weight = (di == 0 ? 2 : 1);
									dz_dy += (dj < 0 ? -1.0 : 1.0) * dy_weight * n.h;
									dz_dy_count += dy_weight;
								}
							}
						}
					}

					//for now we only handle the cell that have 8 valid neighbors!
					if (dz_dx_count == 8 && dz_dy_count == 8)
					{
						dz_dx /= (8 * m_grid.gridStep);
						dz_dy /= (8 * m_grid.gridStep);

						double slope_rad = atan( /*z_factor **/sqrt(dz_dx*dz_dx + dz_dy * dz_dy));

						double aspect_rad = 0;
						static const double s_Zero = 1.0e-8;
						if (std::abs(dz_dx) > s_Zero)
						{
							aspect_rad = atan2(dz_dy, -dz_dx);
							if (aspect_rad < 0)
							{
								aspect_rad += 2.0 * M_PI;
							}
						}
						else // dz_dx == 0
						{
							if (dz_dy > s_Zero)
							{
								aspect_rad = 0.5 * M_PI;
							}
							else if (dz_dy < s_Zero)
							{
								aspect_rad = 1.5 * M_PI;
							}
						}

						ScalarType hillshade = static_cast<ScalarType>(std::max(0.0, cos_zenith_rad * cos(slope_rad) + sin_zenith_rad * sin(slope_rad) * cos(azimuth_rad - aspect_rad)));
						if (!resampleInputCloudXY)
						{
							hillshadeLayer->setValue(sparseSF ? nonEmptyCellIndex : i + j * m_grid.width, hillshade);
						}
						else // resampling mode
						{
							if (cell.nbPoints != 0)
							{
								// non-empty cells are at the beginning
								hillshadeLayer->setValue(nonEmptyCellIndex, hillshade);
							}
							else
							{
								// filled or interpolated cells are appended at the end
								hillshadeLayer->setValue(m_grid.nonEmptyCellCount + validButEmptyCellIndex, hillshade);
							}
						}
					}
				}

				if (!resampleInputCloudXY || cell.nbPoints != 0)
				{
					++nonEmptyCellIndex;
				}
				else
				{
					++validButEmptyCellIndex;
				}
			}
			else
			{
				if (cell.nbPoints)
				{
					// with inv. var. projection mode, it's possible to have a non empty cell with NaN height!
					++nonEmptyCellIndex;
				}
			}
		}
	}

	hillshadeLayer->computeMinAndMax();
	hillshadeLayer->setColorScale(ccColorScalesManager::GetDefaultScale(ccColorScalesManager::GREY));
	m_rasterCloud->setCurrentDisplayedScalarField(sfIdx);
	m_rasterCloud->showSF(true);
	m_UI->activeLayerComboBox->setCurrentIndex(m_UI->activeLayerComboBox->findText(HILLSHADE_FIELD_NAME));

	if (m_glWindow)
	{
		m_glWindow->redraw();
	}
}

void ccRasterizeTool::addNewContour(ccPolyline* poly, double height, unsigned subIndex)
{
	if (!m_cloud || !poly)
	{
		assert(false);
		return;
	}

	if (poly->size() > 1)
	{
		poly->setName(QString("Contour line value = %1 (#%2)").arg(height).arg(subIndex));
		poly->setGlobalScale(m_cloud->getGlobalScale());
		poly->setGlobalShift(m_cloud->getGlobalShift());
		poly->setWidth(m_UI->contourWidthSpinBox->value() < 2 ? 0 : m_UI->contourWidthSpinBox->value()); //size 1 is equivalent to the default size
		poly->setColor(ccColor::darkGrey);
		//poly->setClosed(isClosed);
		if (m_UI->colorizeContoursCheckBox->isChecked())
		{
			ccScalarField* activeLayer = m_rasterCloud->getCurrentDisplayedScalarField();
			if (activeLayer)
			{
				const ccColor::Rgb* col = activeLayer->getColor(height);
				if (col)
				{
					poly->setColor(*col);
				}
			}
		}
		poly->showColors(true);
		//vertices->setEnabled(false);

		if (m_glWindow)
			m_glWindow->addToOwnDB(poly);

		m_contourLines.push_back(poly);
	}
}

void ccRasterizeTool::generateContours()
{
	if (!m_grid.isValid() || !m_rasterCloud)
	{
		ccLog::Error("Need a valid raster/cloud to compute contours!");
		return;
	}

	//initialize parameters
	ccContourLinesGenerator::Parameters params;
	{
		params.projectContourOnAltitudes = false;
		{
			switch (m_UI->activeLayerComboBox->currentData().toInt())
			{
			case LAYER_HEIGHT:
				//nothing to do
				break;
			case LAYER_RGB:
				ccLog::Error("Can't generate contours from RGB colors");
				return;
			default:
				params.projectContourOnAltitudes = m_UI->projectContoursOnAltCheckBox->isChecked();
				break;
			}
		}

		//use current layer for 'altitudes'
		params.altitudes = m_rasterCloud->getCurrentDisplayedScalarField();
		if (!params.altitudes)
		{
			ccLog::Error("No valid/active layer!");
			return;
		}
		params.emptyCellsValue = params.altitudes->getMin() - 1.0;

		//min and max 'altitudes'
		params.startAltitude = m_UI->contourStartDoubleSpinBox->value();
		params.maxAltitude = params.altitudes->getMax();
		assert(params.startAltitude <= params.maxAltitude);

		//gap between levels
		params.step = m_UI->contourStepDoubleSpinBox->value();
		assert(params.step > 0);

		//minimum number of vertices per contour line
		params.minVertexCount = m_UI->minVertexCountSpinBox->value();
		assert(params.minVertexCount >= 3);

		//the parameters below are only required if GDAL is not supported (but we can set them anyway)
		params.ignoreBorders = m_UI->ignoreContourBordersCheckBox->isChecked();
		params.parentWidget = this;
	}

	removeContourLines();

	//compute the grid min corner (2D)
	CCVector2d gridMinCorner;
	{
		const unsigned char Z = getProjectionDimension();
		assert(Z <= 2);
		const unsigned char X = Z == 2 ? 0 : Z + 1;
		const unsigned char Y = X == 2 ? 0 : X + 1;
		gridMinCorner = CCVector2d(m_grid.minCorner.u[X], m_grid.minCorner.u[Y]);
	}

	//generate the contour lines
	std::vector<ccPolyline*> contourLines;
	if (!ccContourLinesGenerator::GenerateContourLines(	&m_grid,
														gridMinCorner,
														params,
														contourLines))
	{
		ccLog::Error("Process failed (see console)");
		return;
	}

	for (ccPolyline* poly : contourLines)
	{
		addNewContour(	poly,
						poly->getMetaData(ccPolyline::MetaKeyConstAltitude()).toUInt(),
						poly->getMetaData(ccContourLinesGenerator::MetaKeySubIndex()).toUInt() );
	}

	if (!m_contourLines.empty())
	{
		m_UI->exportContoursPushButton->setEnabled(true);
		m_UI->clearContoursPushButton->setEnabled(true);
	}

	if (m_glWindow)
	{
		m_glWindow->redraw();
	}
}

void ccRasterizeTool::exportContourLines()
{
	MainWindow* mainWindow = MainWindow::TheInstance();
	if (!mainWindow || !m_cloud || m_contourLines.empty())
	{
		assert(false);
		return;
	}

	bool colorize = m_UI->colorizeContoursCheckBox->isChecked();

	//vertical dimension
	const unsigned char Z = getProjectionDimension();
	assert(Z <= 2);
	const unsigned char X = (Z == 2 ? 0 : Z + 1);
	const unsigned char Y = (X == 2 ? 0 : X + 1);

	ccHObject* group = new ccHObject(QString("Contour plot(%1) [step=%2]").arg(m_cloud->getName()).arg(m_UI->contourStepDoubleSpinBox->value()));
	for (auto poly : m_contourLines)
	{
		//now is the time to map the polyline coordinates to the right dimensions!
		ccPointCloud* vertices = dynamic_cast<ccPointCloud*>(poly->getAssociatedCloud());
		assert(vertices);
		if (vertices && Z != 2)
		{
			for (unsigned j = 0; j < vertices->size(); ++j)
			{
				CCVector3* P = const_cast<CCVector3*>(vertices->getPoint(j));
				CCVector3 Q = *P;
				P->u[X] = Q.x;
				P->u[Y] = Q.y;
				P->u[Z] = Q.z;
			}
			vertices->invalidateBoundingBox();
			poly->invalidateBoundingBox();
		}

		if (!colorize)
			poly->showColors(false);
		group->addChild(poly);
		if (m_glWindow)
			m_glWindow->removeFromOwnDB(poly);
	}
	m_contourLines.resize(0);
	m_UI->exportContoursPushButton->setEnabled(false);

	group->setDisplay_recursive(m_cloud->getDisplay());
	mainWindow->addToDB(group);

	ccLog::Print(QString("Contour lines have been successfully exported to DB (group name: %1)").arg(group->getName()));
}

ccRasterGrid::EmptyCellFillOption ccRasterizeTool::getFillEmptyCellsStrategyExt(double& emptyCellsHeight,
																				double& minHeight,
																				double& maxHeight) const
{
	ccRasterGrid::EmptyCellFillOption fillEmptyCellsStrategy = getFillEmptyCellsStrategy(m_UI->fillEmptyCellsComboBox);

	emptyCellsHeight = 0.0;
	minHeight = m_grid.minHeight;
	maxHeight = m_grid.maxHeight;
	
	switch (fillEmptyCellsStrategy)
	{
	case ccRasterGrid::LEAVE_EMPTY:
		//nothing to do
		break;
	case ccRasterGrid::FILL_MINIMUM_HEIGHT:
		emptyCellsHeight = m_grid.minHeight;
		break;
	case ccRasterGrid::FILL_MAXIMUM_HEIGHT:
		emptyCellsHeight = m_grid.maxHeight;
		break;
	case ccRasterGrid::FILL_CUSTOM_HEIGHT:
	case ccRasterGrid::INTERPOLATE_DELAUNAY:
		{
			double customEmptyCellsHeight = getCustomHeightForEmptyCells();
			//update min and max height by the way (only if there are invalid cells ;)
			if (m_grid.validCellCount != m_grid.width * m_grid.height)
			{
				if (customEmptyCellsHeight <= m_grid.minHeight)
					minHeight = customEmptyCellsHeight;
				else if (customEmptyCellsHeight >= m_grid.maxHeight)
					maxHeight = customEmptyCellsHeight;
				emptyCellsHeight = customEmptyCellsHeight;
			}
		}
		break;
	case ccRasterGrid::FILL_AVERAGE_HEIGHT:
		//'average height' is a kind of 'custom height' so we can fall back to this mode!
		fillEmptyCellsStrategy = ccRasterGrid::FILL_CUSTOM_HEIGHT;
		emptyCellsHeight = m_grid.meanHeight;
		break;
	default:
		assert(false);
	}

	return fillEmptyCellsStrategy;
}

void ccRasterizeTool::generateImage() const
{
	if (!m_cloud || !m_grid.isValid())
	{
		assert(false);
		return;
	}

	bool exportRGB = (m_UI->activeLayerComboBox->currentData().toInt() == LAYER_RGB);
	const ccRasterGrid::SF* gridSF = nullptr;
	const CCCoreLib::ScalarField* cloudSF = nullptr;
	if (!exportRGB && m_UI->activeLayerComboBox->currentData().toInt() == LAYER_SF && m_cloud->isA(CC_TYPES::POINT_CLOUD))
	{
		//the indexes in the 'm_grid.scalarFields' are the same as in the cloud
		ccPointCloud* pc = static_cast<ccPointCloud*>(m_cloud);
		int visibleSfIndex = pc->getScalarFieldIndexByName(qPrintable(m_UI->activeLayerComboBox->currentText()));
		if (visibleSfIndex >= 0 && static_cast<size_t>(visibleSfIndex) < m_grid.scalarFields.size())
		{
			cloudSF = pc->getScalarField(visibleSfIndex);
			gridSF = &(m_grid.scalarFields[visibleSfIndex]);
		}
		else
		{
			ccLog::Error("Internal error: can't find the selected field");
			return;
		}
	}

	ccRasterGrid::EmptyCellFillOption fillEmptyCellsStrategy = ccRasterGrid::LEAVE_EMPTY;
		
	// exported field extreme values
	double emptyCellsValue = std::numeric_limits<double>::quiet_NaN();
	double minValue = 0.0;
	double maxValue = 0.0;
	if (cloudSF)
	{
		// override the default min and max values (based on the height values)
		minValue = cloudSF->getMin();
		maxValue = cloudSF->getMax();
	}
	else if (!exportRGB)
	{
		// retrieve height values
		fillEmptyCellsStrategy = getFillEmptyCellsStrategyExt(	emptyCellsValue,
																minValue,
																maxValue);
	}

	double valueRange = maxValue - minValue;
	if (!exportRGB && CCCoreLib::LessThanEpsilon(valueRange))
	{
		ccLog::Warning("[Rasterize::generateImage] Exported field has a flat range");
		valueRange = 1.0; // to simplify tests below
	}

	QImage outputImage(m_grid.width, m_grid.height, exportRGB ? QImage::Format_ARGB32 : QImage::Format_Indexed8);

	if (!outputImage.isNull())
	{
		unsigned emptyCellColorIndex = 0;
		double maxColorComp = 255.99; //.99 --> to avoid round-off issues later!

		if (!exportRGB)
		{
			bool addTransparentColor = (cloudSF || fillEmptyCellsStrategy == ccRasterGrid::LEAVE_EMPTY);

			//build a custom palette
			QVector<QRgb> palette(256);
			if (	m_rasterCloud
				&&	m_rasterCloud->getCurrentDisplayedScalarField()
				&&	m_rasterCloud->getCurrentDisplayedScalarField()->getColorScale())
			{
				const ccColorScale::Shared& colorScale = m_rasterCloud->getCurrentDisplayedScalarField()->getColorScale();
				unsigned steps = (addTransparentColor ? 255 : 256);
				for (unsigned i = 0; i < steps; i++)
				{
					const ccColor::Rgb* col = colorScale->getColorByRelativePos(i / static_cast<double>(steps - 1), steps, &ccColor::lightGreyRGB);
					palette[i] = qRgba(col->r, col->g, col->b, 255);
				}
			}
			else
			{
				for (unsigned i = 0; i < 256; i++)
				{
					palette[i] = qRgba(i, i, i, 255);
				}
			}

			if (addTransparentColor)
			{
				palette[255] = qRgba(255, 0, 255, 0); //magenta/transparent color for empty cells (in place of pure white)
				maxColorComp = 254.99;
			}

			outputImage.setColorTable(palette);

			if (!cloudSF) //we are using height values
			{
				switch (fillEmptyCellsStrategy)
				{
				case ccRasterGrid::LEAVE_EMPTY:
					emptyCellColorIndex = 255; //should be transparent!
					break;
				case ccRasterGrid::FILL_MINIMUM_HEIGHT:
					emptyCellColorIndex = 0;
					break;
				case ccRasterGrid::FILL_MAXIMUM_HEIGHT:
					emptyCellColorIndex = 255;
					break;
				case ccRasterGrid::FILL_CUSTOM_HEIGHT:
					{
						double normalizedHeight = (emptyCellsValue - minValue) / valueRange;
						assert(normalizedHeight >= 0.0 && normalizedHeight <= 1.0);
						emptyCellColorIndex = static_cast<unsigned>(normalizedHeight*maxColorComp); //static_cast is equivalent to floor if value >= 0
					}
					break;
				case ccRasterGrid::FILL_AVERAGE_HEIGHT:
				default:
					assert(false);
				}
			}
			else
			{
				emptyCellColorIndex = 255;
			}
			//outputImage.fill(emptyCellColorIndex);
		}

		// Filling the image with grid values
		for (unsigned j = 0; j < m_grid.height; ++j)
		{
			const ccRasterGrid::Row& row = m_grid.rows[j];
			const double* sfRow = (gridSF ? gridSF->data() + j * m_grid.width : nullptr);
			for (unsigned i = 0; i < m_grid.width; ++i)
			{
				if (std::isfinite(row[i].h))
				{
					if (exportRGB)
					{
						int r = static_cast<int>(std::max(0.0, std::min(255.0, row[i].color.u[0])));
						int g = static_cast<int>(std::max(0.0, std::min(255.0, row[i].color.u[1])));
						int b = static_cast<int>(std::max(0.0, std::min(255.0, row[i].color.u[2])));
						outputImage.setPixel(i, m_grid.height - 1 - j, qRgba(r, g, b, 255));
					}
					else
					{
						double value = sfRow ? sfRow[i] : row[i].h;
						double normalizedHeight = (value - minValue) / valueRange;
						assert(normalizedHeight >= 0.0 && normalizedHeight <= 1.0);
						unsigned char val = static_cast<unsigned char>(normalizedHeight*maxColorComp); //static_cast is equivalent to floor if value >= 0
						outputImage.setPixel(i, m_grid.height - 1 - j, val);
					}
				}
				else //NaN
				{
					outputImage.setPixel(i, m_grid.height - 1 - j, emptyCellColorIndex); //in RGBA mode, it should be 0
				}
			}
		}

		//open file saving dialog
		{
			QSettings settings;
			settings.beginGroup(ccPS::HeightGridGeneration());
			QString imageSavePath = settings.value("savePathImage", ccFileUtils::defaultDocPath()).toString();

			QString outputFilename = ImageFileFilter::GetSaveFilename(	"Save raster as image",
																		"image",
																		imageSavePath,
																		const_cast<ccRasterizeTool*>(this));

			if (!outputFilename.isNull())
			{
				//save current export path to persistent settings
				settings.setValue("savePathImage", QFileInfo(outputFilename).absolutePath());
				settings.endGroup();

				if (outputImage.save(outputFilename))
				{
					ccLog::Print(QString("[Rasterize] Image '%1' successfully saved").arg(outputFilename));
				}
				else
				{
					ccLog::Error("Failed to save image file!");
				}
			}
		}
	}
	else
	{
		ccLog::Error("Failed to create output image! (not enough memory?)");
	}
}

void ccRasterizeTool::generateASCIIMatrix() const
{
	if (!m_grid.isValid())
		return;

	QSettings settings;
	settings.beginGroup(ccPS::HeightGridGeneration());
	QString asciiGridSavePath = settings.value("savePathASCIIGrid", ccFileUtils::defaultDocPath()).toString();

	//open file saving dialog
	QString filter("ASCII file (*.txt)");
	QString outputFilename = QFileDialog::getSaveFileName(nullptr, "Save grid as ASCII file", asciiGridSavePath + QString("/raster_matrix.txt"), filter);
	if (outputFilename.isNull())
		return;

	QFile fp(outputFilename);
	if (!fp.open(QFile::WriteOnly))
	{
		ccLog::Warning(QString("[ccHeightGridGeneration] Failed to write '%1' file!").arg(outputFilename));
	}

	//default values
	double emptyCellsHeight = 0;
	double minHeight = m_grid.minHeight;
	double maxHeight = m_grid.maxHeight;
	//get real values
	getFillEmptyCellsStrategyExt(emptyCellsHeight, minHeight, maxHeight);
	QTextStream stream(&fp);
	stream.setRealNumberPrecision(8);
	for (unsigned j = 0; j < m_grid.height; ++j)
	{
		const ccRasterGrid::Row& row = m_grid.rows[m_grid.height - 1 - j];
		for (unsigned i = 0; i < m_grid.width; ++i)
		{
			stream << (std::isfinite(row[i].h) ? row[i].h : emptyCellsHeight) << ' ';
		}
		stream << endl;
	}

	//save current export path to persistent settings
	settings.setValue("savePathASCIIGrid", QFileInfo(outputFilename).absolutePath());

	ccLog::Print(QString("[Rasterize] Raster matrix '%1' successfully saved").arg(outputFilename));
}

void ccRasterizeTool::onStatExportTargetChanged(bool)
{
	m_UI->exportStatisticsFrame->setEnabled(	m_UI->exportHeightStatsCheckBox->isChecked()
											||	(m_UI->exportSFStatsCheckBox->isEnabled() && m_UI->exportSFStatsCheckBox->isChecked()));
}

void ccRasterizeTool::showInterpolationParamsDialog()
{
	switch (getFillEmptyCellsStrategy(m_UI->fillEmptyCellsComboBox))
	{
	case ccRasterGrid::EmptyCellFillOption::INTERPOLATE_DELAUNAY:
	{
		bool ok = false;
		double value = QInputDialog::getDouble(this, tr("Delaunay triangulation"), tr("Triangles max edge length (0 = no limit)"), m_delaunayInterpParams.maxEdgeLength , 0, 1.0e6, 6, &ok);
		if (ok)
		{
			m_delaunayInterpParams.maxEdgeLength = value;
			gridIsUpToDate(false);
		}
	}
	break;

	case ccRasterGrid::EmptyCellFillOption::KRIGING:
	{
		ccKrigingParamsDialog dlg(this);

		dlg.setParameters(m_krigingParams);

		if (dlg.exec())
		{
			dlg.getParameters(m_krigingParams);

			gridIsUpToDate(false);
		}
	}
	break;

	default:
		assert(false);
		break;
	}

}
