
///////////////////////////////////////////////////////////
//                                                       //
//                         SAGA                          //
//                                                       //
//      System for Automated Geoscientific Analyses      //
//                                                       //
//                     Tool Library                      //
//                       image_io                        //
//                                                       //
//-------------------------------------------------------//
//                                                       //
//                  extract_exif_gps.cpp                 //
//                                                       //
//                 Copyright (C) 2025 by                 //
//                  Justus Spitzmueller                  //
//                                                       //
//-------------------------------------------------------//
//                                                       //
// This file is part of 'SAGA - System for Automated     //
// Geoscientific Analyses'. SAGA 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, either version 2 of the     //
// License, or (at your option) any later version.       //
//                                                       //
// SAGA 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.                             //
//                                                       //
// You should have received a copy of the GNU General    //
// Public License along with this program; if not, see   //
// <http://www.gnu.org/licenses/>.                       //
//                                                       //
///////////////////////////////////////////////////////////

//---------------------------------------------------------
#if HAVE_EXIV2

//---------------------------------------------------------
#include "extract_exif_gps.h"

#include "image_import_exif.h"

#include <wx/fs_mem.h>


///////////////////////////////////////////////////////////
//                                                       //
//                                                       //
//                                                       //
///////////////////////////////////////////////////////////

//---------------------------------------------------------
CExtract_EXIF_GPS::CExtract_EXIF_GPS(void)
{
	Set_Name		(_TL("Import Image GPS Tags"));

	Set_Author		(SG_T("J.Spitzm\u00FCller \u00A9 2025"));

	Set_Version 	("2.0");

	CSG_String Description(_TW(
		"<p>"
		"This tool extracts EXIF GPS tags from image files and creates corresponding point features using the Exiv2 library. "
		"One can either select individual files or let the tool recursively search entire directories for supported image formats. "
		"The output features can be assigned a target coordinate system, and the tool will reproject the GPS coordinates accordingly. The default projection is WGS84. "
		"The original GPS values are also preserved for reference."
		"</p><p>"
		"The following GPS informations are currently extracted if available:\n"
		"<ul>"
		"<li>Latitude</li>"
		"<li>Longitude</li>"
		"<li>Altitude</li>"
		"<li>Direction</li>"
		"<li>Compass method (Magnetic or True North)</li>"
		"<li>Dilution of Precision (DOP)</li>"
		"</ul>"
		"</p>"
	));

	if( has_GUI() )
	{
		Description += _TW(
			"<p>"
			"This tool can also add the point features to a new or the currently active map and will display the corresponding images next to each point. "
			"To modify the display settings, go to <i>Settings > Display > Image Field</i>."
			"</p><p>"
			"The image and all extracted information are also displayed in the Information tab of the Properties window (last tab) for the current selection. "
			"The Feature Descriptions can be saved as <code>.html</code>-Files to the file system either next to the images or in a specified directory. The corresponding file paths are "
			"stored in the attribute table. To recreate this behavior, select the field under <i>Settings > General > Feature Description</i>."
			"</p>"
		);
	}

	Description += CSG_String::Format("<hr><p>Exiv2 version is <b>%d.%d.%d</b></p>",
		EXIV2_MAJOR_VERSION,
		EXIV2_MINOR_VERSION,
		EXIV2_PATCH_VERSION
	);

	Set_Description(Description);


	Add_Reference("https://exiv2.org/", SG_T("Exiv2 C++ metadata library"));

	//-----------------------------------------------------
	Parameters.Add_Shapes("", "SHAPES", _TL("Shape"), _TL("Shape"), PARAMETER_OUTPUT );

	m_CRS.Create(Parameters, "SHAPES");

	//-----------------------------------------------------
	Parameters.Add_Choice("", 
		"SOURCE", _TL("Select From"), 
		_TL(""), 
		CSG_String::Format("%s|%s|", 
			_TL("Files"), 
			_TL("Directory")
		),0
	);

	Parameters.Add_FilePath("SOURCE",
		"FILES"     , _TL("Files"),
		_TL(""),
		CSG_String::Format(
			"%s"                        "|*.jpg;*.jif;*.jpeg;*.raw;*.dng;*.JPG;*.JIF;*.JPEG;*.RAW;*.DNG|"
			"%s (*.jpg, *.jif, *.jpeg)" "|*.jpg;*.jif;*.jpeg;*.JPG;*.JIF;*.JPEG|"
			"%s (*.raw, *.dng)" 		"|*.raw;*.dng;*.RAW;*.DNG|"
			"%s"                        "|*.*",
			_TL("Recognized File Types"),
			_TL("JPEG - JFIF Compliant"),
			_TL("RAW Images"           ),
			_TL("All Files"            )
		),
		NULL, false, false, true
	);

	Parameters.Add_FilePath("SOURCE",
		"DIRECTORY"     , _TL("Directory"),
		_TL(""),
		NULL, NULL, false, true, false
	);

	Parameters.Add_Bool("SOURCE",
		"RECURSIVE"     , _TL("Recursive"), 
		_TL(""), false
	);

	//-----------------------------------------------------
	Parameters.Add_Choice("", 
		"HTML_INFO"     , _TL("HTML Description"), 
		_TL("Choose if and where to create additional feature description to be shown for selected features in the 'Information' tab."),
		CSG_String::Format("%s|%s|%s|%s",
			_TL("do not create"),
			_TL("create HTML file in the image's directory"),
			_TL("create HTML file in the selected directory"),
			_TL("create as attribute field"),
			_TL("create as temporary memory object")
		), 3
	);

	Parameters.Add_FilePath("HTML_INFO",
		"HTML_DIRECTORY", _TL("Directory"),
		_TL(""),
		NULL, NULL, false, true, false
	);

	Parameters.Add_Bool("HTML_INFO",
		"HTML_COMPLETE" , _TL("Include All Metadata"), 
		_TL(""),
		false
	);

	//-----------------------------------------------------
	Parameters.Add_Choice("", 
		"ADD"           , _TL("Add to Map"), 
		_TL(""), 
		CSG_String::Format("%s|%s|%s|", 
			_TL("Don't Add"), 
			_TL("Add to New Map"),
			_TL("Add to Active Map")
		),1
	)->Set_UseInCMD( false );

	Parameters.Add_Bool("",
		"IMAGE_FIELD"   , _TL("Set Image Field"), 
		_TL(""),
		false
	)->Set_UseInCMD(false);
}


///////////////////////////////////////////////////////////
//                                                       //
///////////////////////////////////////////////////////////

//---------------------------------------------------------
bool CExtract_EXIF_GPS::On_Before_Execution(void)
{
	m_CRS.Activate_GUI();

	return( CSG_Tool::On_Before_Execution() );
}

//---------------------------------------------------------
bool CExtract_EXIF_GPS::On_After_Execution(void)
{
	m_CRS.Deactivate_GUI();

	return( CSG_Tool::On_After_Execution() );
}

//---------------------------------------------------------
int CExtract_EXIF_GPS::On_Parameter_Changed(CSG_Parameters *pParameters, CSG_Parameter *pParameter)
{
	m_CRS.On_Parameter_Changed(pParameters, pParameter);

	return( CSG_Tool::On_Parameter_Changed(pParameters, pParameter) );
}

//---------------------------------------------------------
int CExtract_EXIF_GPS::On_Parameters_Enable(CSG_Parameters *pParameters, CSG_Parameter *pParameter)
{
	m_CRS.On_Parameters_Enable(pParameters, pParameter);

	if( pParameter->Cmp_Identifier("SOURCE") )
	{
		pParameters->Set_Enabled("FILES"    , pParameter->asInt() == 0);
		pParameters->Set_Enabled("DIRECTORY", pParameter->asInt() == 1);
		pParameters->Set_Enabled("RECURSIVE", pParameter->asInt() == 1);
	}
		
	if( pParameter->Cmp_Identifier("HTML_INFO") )
	{
		pParameters->Set_Enabled("HTML_DIRECTORY", pParameter->asInt() == 2);
	}

	return( CSG_Tool::On_Parameters_Enable(pParameters, pParameter) );
}


///////////////////////////////////////////////////////////
//                                                       //
///////////////////////////////////////////////////////////

//---------------------------------------------------------
bool CExtract_EXIF_GPS::On_Execute(void)
{
	CSG_Strings Files; CSG_String Name = _TL("Images");

	if( Parameters["SOURCE"].asInt() == 0 )
	{
		if( !Parameters["FILES"].asFilePath()->Get_FilePaths(Files) )
		{
			Error_Set(_TL("No files found"));

			return( false );
		}
	}
	else 
	{
		CSG_String Directory = Parameters["DIRECTORY"].asString();

		if( !SG_Dir_Exists(Directory) )
		{
			Error_Set(_TL("No directory found"));

			return( false );
		}

		Name = SG_File_Get_Name(Directory, false);

		bool Recursive = Parameters["RECURSIVE"].asBool();

		CSG_Strings List, Extensions(SG_String_Tokenize("jpg jif jpeg raw dng"));

		for(int i=0; i<Extensions.Get_Count(); i++)
		{
			SG_Dir_List_Files(List, Directory, Extensions[i], Recursive); Files += List;
		}
	}

	//-----------------------------------------------------
	if( Parameters["HTML_INFO"].asInt() == 2 && !SG_Dir_Exists(Parameters["HTML_DIRECTORY"].asString()) ) // Select Directory
	{
		Message_Fmt("\n%s!\n%s\n%s\n\"%s\"\n", _TL("Warning"), _TL("Storing feature descriptions will be skipped!"),
			_TL("Target directory does not exist!"), Parameters["HTML_DIRECTORY"].asString()
		);
	}

	//-----------------------------------------------------
	CSG_Shapes &Points = *Parameters["SHAPES"].asShapes();

	Points.Create(SHAPE_TYPE_Point, Name, NULL, SG_VERTEX_TYPE_XYZ);

	m_CRS.Get_CRS(Points.Get_Projection(), true);

	if( Points.Get_Projection().Get_Type() == ESG_CRS_Type::Undefined )
	{
		Points.Get_Projection().Set_GCS_WGS84();
	}

	Points.Add_Field("File"       , SG_DATATYPE_String); // 0
	Points.Add_Field("Path"       , SG_DATATYPE_String); // 1
	Points.Add_Field("GPS.lon"    , SG_DATATYPE_Double); // 2
	Points.Add_Field("GPS.lat"    , SG_DATATYPE_Double); // 3
	Points.Add_Field("Projected.x", SG_DATATYPE_Double); // 4
	Points.Add_Field("Projected.y", SG_DATATYPE_Double); // 5
	Points.Add_Field("Altitude"   , SG_DATATYPE_Double); // 6
	Points.Add_Field("Direction"  , SG_DATATYPE_Double); // 7
	Points.Add_Field("Compass"    , SG_DATATYPE_String); // 8
	Points.Add_Field("DOP"        , SG_DATATYPE_Double); // 9

	if( Parameters["HTML_INFO"].asInt() )
	{
		Points.Add_Field("Information", SG_DATATYPE_String); // 10
	}

	//-----------------------------------------------------
	for(int i=0; i<Files.Get_Count() && Set_Progress(i, Files.Get_Count()); i++)
	{
		Add_File(Files[i], Points);
	}

	//-----------------------------------------------------
	if( has_GUI() )
	{
		if( Parameters["IMAGE_FIELD"].asBool() )
		{
			DataObject_Set_Parameter(&Points, "IMAGE_FIELD",  1 );
			DataObject_Set_Parameter(&Points, "IMAGE_SCALE", 30.);
		}

		if( Parameters["HTML_INFO"].asInt() )
		{
			DataObject_Set_Parameter(&Points, "INFO_FIELD" , 10 );
		}

		switch( Parameters["ADD"].asInt() )
		{
		case 1: DataObject_Update(&Points, SG_UI_DATAOBJECT_SHOW_MAP_NEW   ); break;
		case 2: DataObject_Update(&Points, SG_UI_DATAOBJECT_SHOW_MAP_ACTIVE); break;
		}
	}

	//-----------------------------------------------------
	return( true );
}


///////////////////////////////////////////////////////////
//                                                       //
///////////////////////////////////////////////////////////

//---------------------------------------------------------
bool CExtract_EXIF_GPS::Add_File(const CSG_String &File, CSG_Shapes &Points)
{
	if( SG_File_Exists(File) == false )
	{
		Error_Fmt("%s\n%s: %s", _TL("File does not exist!"), _TL("File"), File.c_str());

		return( false );
	}

	std::unique_ptr<Exiv2::Image> pImage;

	try
	{
		pImage = Exiv2::ImageFactory::open(File.to_StdString());

		pImage->readMetadata();
	}
	catch( Exiv2::Error &e )
	{
		Error_Fmt("%s\n%s: %s", CSG_String(e.what()).c_str(), _TL("File"), File.c_str());

		return( false );
	}
	catch(...)
	{
		Error_Fmt("%s\n%s: %s", _TL("File open error!"), _TL("File"), File.c_str());

		return( false );
	}

	//-----------------------------------------------------
	Exiv2::ExifData &exifData = pImage->exifData(); CSG_Point Coordinate;

	if( !CEXIV_Helper::Get_Coordinate(exifData, Coordinate) )
	{
		Error_Fmt("%s: %s", SG_File_Get_Name(File, false).c_str(), _TL("File does not provide coordinate information!"));

		return( false );
	}

	Message_Fmt("\n%s: x=%.8f, y=%.8f", SG_File_Get_Name(File, false).c_str(), Coordinate.x, Coordinate.y);

	//-----------------------------------------------------
	CSG_Shape &Point = *Points.Add_Shape(); for(int i=0; i<Points.Get_Field_Count(); i++) { Point.Set_NoData(i); }

	Point.Set_Value("File"   , SG_File_Get_Name(File, true));
	Point.Set_Value("Path"   , File); // full path!
	Point.Set_Value("GPS.lon", Coordinate.x);
	Point.Set_Value("GPS.lat", Coordinate.y);

	if( SG_Get_Projected(CSG_Projection::Get_GCS_WGS84(), Points.Get_Projection(), Coordinate) )
	{
		Point.Set_Value("Projected.x", Coordinate.x);
		Point.Set_Value("Projected.y", Coordinate.y);
	}

	Point.Set_Point(Coordinate);

	//-----------------------------------------------------
	auto altIt    = exifData.findKey(Exiv2::ExifKey("Exif.GPSInfo.GPSAltitude"   ));
	auto altRefIt = exifData.findKey(Exiv2::ExifKey("Exif.GPSInfo.GPSAltitudeRef"));

	if( altIt != exifData.end() && altRefIt != exifData.end() && altIt->value().count() == 1 )
	{
    	double Altitude = CEXIV_Helper::Get_Rational(altIt->value(), 0);

		#if EXIV2_TEST_VERSION(0, 28, 0)
			if( altRefIt->value().toUint32() == 1 ) { Altitude *= -1; }
		#else
			if( altRefIt->value().toLong  () == 1 ) { Altitude *= -1; }
		#endif

		Message_Fmt(", z=%f", Altitude);

		Point.Set_Value("Altitude", Altitude); Point.Set_Z(Altitude);
	}

	//-----------------------------------------------------
	auto dirIt    = exifData.findKey(Exiv2::ExifKey("Exif.GPSInfo.GPSImgDirection"   ));
	auto dirRefIt = exifData.findKey(Exiv2::ExifKey("Exif.GPSInfo.GPSImgDirectionRef"));

	if( dirIt != exifData.end() && dirIt->value().count() == 1 && dirRefIt != exifData.end() )
	{
    	double Direction = CEXIV_Helper::Get_Rational(dirIt->value(), 0);

		Message_Fmt(", dir=%f", Direction);

		Point.Set_Value("Direction", Direction); Point.Set_Value("Compass", dirRefIt->value().toString().c_str());
	}

	//-----------------------------------------------------
	auto dopIt = exifData.findKey(Exiv2::ExifKey("Exif.GPSInfo.GPSDOP"));

	if( dopIt != exifData.end() && dopIt->value().count() == 1 )
	{
    	double DOP = CEXIV_Helper::Get_Rational(dopIt->value(), 0);

		Message_Fmt("DOP=%.2f", DOP);

		Point.Set_Value("DOP", DOP);
	}

	//-----------------------------------------------------
	CSG_String HTML;

	HTML += "<html>\n<head>\n\t<title>" + SG_File_Get_Name(File, false) + "</title>\n</head>\n<body>\n";
	HTML += "<h3>" + SG_File_Get_Name(File, false) + "</h3>\n";
	HTML += "<hr>\n<img width=\"100%\" src=\"" + File + "\">\n<hr>\n";
	HTML += "<table border=\"1\" width=\"100%\">\n";
	HTML += CSG_String::Format("\t<tr><td>%s</td><td>%s</td></tr>\n", _TL("File"    ), SG_File_Get_Name(File, true).c_str());
	HTML += CSG_String::Format("\t<tr><td>%s</td><td>%s</td></tr>\n", _TL("Location"), SG_File_Get_Path(File      ).c_str());
	HTML += CSG_String::Format("<tr><td>GPS Longitude</td><td>%.8f</td></tr>\n", Point.asDouble("GPS.lon"));
	HTML += CSG_String::Format("<tr><td>GPS Latitude</td><td>%.8f</td></tr>\n" , Point.asDouble("GPS.lat"));

	if( !Point.is_NoData("Projected.x") ) { HTML += CSG_String::Format("\t<tr><td>%s.x</td><td>%.8f</td></tr>\n", _TL("Projected"), Point.asDouble("Projected.x")); }
	if( !Point.is_NoData("Projected.y") ) { HTML += CSG_String::Format("\t<tr><td>%s.y</td><td>%.8f</td></tr>\n", _TL("Projected"), Point.asDouble("Projected.y")); }
	if( !Point.is_NoData("Altitude"   ) ) { HTML += CSG_String::Format("\t<tr><td>%s</td><td>%.2f</td></tr>\n"  , _TL("Altitude" ), Point.asDouble("Altitude"   )); }
	if( !Point.is_NoData("Direction"  ) ) { HTML += CSG_String::Format("\t<tr><td>%s</td><td>%.2f</td></tr>\n"  , _TL("Direction"), Point.asDouble("Direction"  )); }
	if( !Point.is_NoData("Compass"    ) ) { HTML += CSG_String::Format("\t<tr><td>%s</td><td>%s</td></tr>\n"    , _TL("Compass"  ), Point.asString("Compass"    )); }
	if( !Point.is_NoData("DOP"        ) ) { HTML += CSG_String::Format("\t<tr><td>DOP</td><td>%.2f</td></tr>\n"                   , Point.asDouble("DOP"        )); }

	if( Parameters["HTML_COMPLETE"].asBool() )
	{
		CSG_MetaData Info;

		if( CEXIV_Helper::Get_MetaData(File, Info, true) )
		{
			for(int i=0; i<Info.Get_Children_Count(); i++)
			{
				HTML += "\t<tr><td>" + Info[i].Get_Name() + "</td><td>" + Info[i].Get_Content() + "</td></tr>\n";
			}
		}
	}

	HTML += "</table>\n</body>\n</html>";

	//-----------------------------------------------------
	switch( Parameters["HTML_INFO"].asInt() )
	{
	case 0: default: break;

	case 1: // Same Directory as the Image
		{
			CSG_String HTML_File = SG_File_Make_Path(SG_File_Get_Path(File), SG_File_Get_Name(File, false), "html");

			CSG_File Stream; if( Stream.Open(HTML_File, SG_FILE_W, false) )
			{
				Stream.Write(HTML);

				Point.Set_Value("Information", HTML_File);
			}
		}
	break;

	case 2: // Select Directory
		if( SG_Dir_Exists(Parameters["HTML_DIRECTORY"].asString()) )
		{
			CSG_String HTML_File = SG_File_Make_Path(Parameters["HTML_DIRECTORY"].asString(), SG_File_Get_Name(File, false), "html");
							
			CSG_File Stream; if( Stream.Open(HTML_File, SG_FILE_W, false) )
			{
				Stream.Write(HTML);

				Point.Set_Value("Information", HTML_File);
			}
		}
		break;

	case 3: // Attribute Field
		{
			HTML.Replace("\n", ""); HTML.Replace("\t", "");

			Point.Set_Value("Information", HTML);
		}
		break;

	case 4: // Don't save on Disk
		{
			CSG_String HTML_File = SG_File_Make_Path("", SG_File_Get_Name(File, false), "html");

			wxFileSystem fs;

			if( fs.OpenFile(CSG_String::Format("memory:%s", HTML_File.c_str()).c_str()) != NULL )
			{
				wxMemoryFSHandler::RemoveFile(HTML_File.c_str());
			}

			wxMemoryFSHandler::AddFile(HTML_File.c_str(), HTML.c_str());

			Point.Set_Value("Information", CSG_String::Format("memory:%s", HTML_File.c_str()).c_str());
		}
		break;
	}

	return( true );
}


///////////////////////////////////////////////////////////
//                                                       //
//                                                       //
//                                                       //
///////////////////////////////////////////////////////////

//---------------------------------------------------------
#endif // HAVE_EXIV2
