
///////////////////////////////////////////////////////////
//                                                       //
//                         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/>.                       //
//                                                       //
//-------------------------------------------------------//
//                                                       //
//                                                       //
///////////////////////////////////////////////////////////

//---------------------------------------------------------
#include "extract_exif_gps.h"
#include <wx/fs_mem.h>

#if HAVE_EXIV2

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

//---------------------------------------------------------
CExtract_EXIF_GPS::CExtract_EXIF_GPS(void)
{
	Set_Name		(_TL("Extract 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("Set Image Files"),
		_TL(""),
		CSG_String::Format(
			"%s"                        "|*.jpg;*.jif;*.jpeg;*.raw;*.dng|"
			"%s (*.jpg, *.jif, *.jpeg)" "|*.jpg;*.jif;*.jpeg|"
			"%s (*.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("Set Image 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_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::Get_Coordinate(double &Coordinate, const Exiv2::Value &Value, const std::string &Ref)
{
	if( Value.count() != 3 )
	{
		return false;
	}

    double Deg = Convert_Rational(Value, 0);
    double Min = Convert_Rational(Value, 1);
    double Sec = Convert_Rational(Value, 2);

	Coordinate = SG_Degree_To_Decimal(Deg, Min, Sec);
    
	if( Ref == "S" || Ref == "W" ) 
	{
		Coordinate *= -1;
	}

    return( true );

}

//---------------------------------------------------------
double CExtract_EXIF_GPS::Convert_Rational( const Exiv2::Value& Value, const size_t Position )
{
    return( Value.toRational(Position).first / static_cast<double>(Value.toRational(Position).second) );
}


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

//---------------------------------------------------------
bool CExtract_EXIF_GPS::On_Execute(void)
{
	CSG_Strings Files; CSG_String Name = "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;
		}
	}
		
	//-----------------------------------------------------
	CSG_Shapes *pShapes = Parameters("SHAPES")->asShapes();

	pShapes->Create(SHAPE_TYPE_Point, Name, NULL, SG_VERTEX_TYPE_XYZ);

	m_CRS.Get_CRS(pShapes->Get_Projection(), true);

	if( pShapes->Get_Projection().Get_Type() == ESG_CRS_Type::Undefined )
	{
		pShapes->Get_Projection().Set_GCS_WGS84();
	}

	pShapes->Add_Field("File"       , SG_DATATYPE_String); // 0
	pShapes->Add_Field("Path"       , SG_DATATYPE_String); // 1
	pShapes->Add_Field("GPS X"      , SG_DATATYPE_Double); // 2
	pShapes->Add_Field("GPS Y"      , SG_DATATYPE_Double); // 3
	pShapes->Add_Field("Projected X", SG_DATATYPE_Double); // 4
	pShapes->Add_Field("Projected Y", SG_DATATYPE_Double); // 5
	pShapes->Add_Field("Altitude"   , SG_DATATYPE_Double); // 6
	pShapes->Add_Field("Direction"  , SG_DATATYPE_Double); // 7
	pShapes->Add_Field("Compass"    , SG_DATATYPE_String); // 8
	pShapes->Add_Field("DOP"        , SG_DATATYPE_Double); // 9

	if( Parameters("HTML_INFO")->asInt() )
	{
		pShapes->Add_Field("Information", SG_DATATYPE_String); // 10
	}

	//-----------------------------------------------------
	for(int i=0; i<Files.Get_Count() && Set_Progress(i, Files.Get_Count()); i++)
	{
		CSG_String File = Files.Get_String(i);
		std::unique_ptr<Exiv2::Image> image = Exiv2::ImageFactory::open(File.to_StdString());
		image->readMetadata();
		Exiv2::ExifData &exifData = image->exifData();

        auto latIt 		= exifData.findKey(Exiv2::ExifKey("Exif.GPSInfo.GPSLatitude"));
        auto latRefIt 	= exifData.findKey(Exiv2::ExifKey("Exif.GPSInfo.GPSLatitudeRef"));
        auto lonIt 		= exifData.findKey(Exiv2::ExifKey("Exif.GPSInfo.GPSLongitude"));
        auto lonRefIt 	= exifData.findKey(Exiv2::ExifKey("Exif.GPSInfo.GPSLongitudeRef"));
		auto altIt 		= exifData.findKey(Exiv2::ExifKey("Exif.GPSInfo.GPSAltitude"));
		auto altRefIt 	= exifData.findKey(Exiv2::ExifKey("Exif.GPSInfo.GPSAltitudeRef"));
	//	auto timeIt 	= exifData.findKey(Exiv2::ExifKey("Exif.GPSInfo.GPSTimeStamp"));
		auto dopIt 		= exifData.findKey(Exiv2::ExifKey("Exif.GPSInfo.GPSDOP"));
		auto dirIt 		= exifData.findKey(Exiv2::ExifKey("Exif.GPSInfo.GPSImgDirection"));
		auto dirRefIt 	= exifData.findKey(Exiv2::ExifKey("Exif.GPSInfo.GPSImgDirectionRef"));

		if( latIt != exifData.end() && latRefIt != exifData.end() && lonIt != exifData.end() && lonRefIt != exifData.end() )
		{
			CSG_String Coord_Message(_TL("No coordinates found in exif")), Alt_Message(_TL("No altitude")), Dir_Message(_TL("No direction")), Dop_Message(_TL("No dop"));

			CSG_Shape *pShape = pShapes->Add_Shape();

			pShape->Set_Value("File", SG_File_Get_Name(File, true) );
			pShape->Set_Value("Path", File);
		
			TSG_Point Point;

			if( Get_Coordinate(Point.y, latIt->value(), latRefIt->value().toString())
			&&	Get_Coordinate(Point.x, lonIt->value(), lonRefIt->value().toString()) )
			{
				Coord_Message = "Found coordinates in exif";

				CSG_String 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());

				pShape->Set_Value("GPS X", Point.x); HTML += CSG_String::Format("<tr><td>GPS X</td><td>%.8f</td></tr>\n", Point.x);
				pShape->Set_Value("GPS Y", Point.y); HTML += CSG_String::Format("<tr><td>GPS Y</td><td>%.8f</td></tr>\n", Point.y);

				if( SG_Get_Projected(CSG_Projection::Get_GCS_WGS84(), pShapes->Get_Projection(), Point) )
				{
					pShape->Set_Value("Projected X", Point.x); HTML += CSG_String::Format("\t<tr><td>%s X</td><td>%.8f</td></tr>\n", _TL("Projected"), Point.x);
					pShape->Set_Value("Projected Y", Point.y); HTML += CSG_String::Format("\t<tr><td>%s Y</td><td>%.8f</td></tr>\n", _TL("Projected"), Point.y);
				}
				else
				{
					pShape->Set_NoData("Projected X");
					pShape->Set_NoData("Projected Y");
				}

				pShape->Add_Point(Point);

				if( altIt != exifData.end() && altRefIt != exifData.end() && altIt->value().count() == 1 )
				{
    				double Altitude = Convert_Rational( altIt->value(), 0 );
					#if EXIV2_TEST_VERSION(0, 28, 0)
					if( altRefIt->value().toUint32() == 1 ) 
					#else
					if( altRefIt->value().toLong() == 1 ) 
					#endif
					{
						Altitude *= -1;
					}
					pShape->Set_Value("Altitude", Altitude);
					pShape->Set_Z( Altitude );
					Alt_Message = "Found altitude";
				
					HTML += CSG_String::Format("\t<tr><td>%s</td><td>%.2f</td></tr>\n", _TL("Altitude"), Altitude);
				}
				
				if( dirIt != exifData.end() && dirIt->value().count() == 1 && dirRefIt != exifData.end() )
				{
    				double Direction = Convert_Rational( dirIt->value(), 0 );
					pShape->Set_Value("Direction", Direction );
					pShape->Set_Value("Compass"  , dirRefIt->value().toString().c_str());
					Dir_Message = "Found direction";
					
					HTML += CSG_String::Format("\t<tr><td>%s</td><td>%.2f</td></tr>\n", _TL("Direction"), Direction);
					HTML += CSG_String::Format("\t<tr><td>%s</td><td>%s</td></tr>\n"  , _TL("Compass"  ), dirRefIt->value().toString().c_str());
				}

				if( dopIt != exifData.end() && dopIt->value().count() == 1 )
				{
    				double DOP = Convert_Rational( dopIt->value(), 0 );
					pShape->Set_Value("DOP", DOP);
					Dop_Message = "Found dop";
					
					HTML += CSG_String::Format("\t<tr><td>DOP</td><td>%.2f</td></tr>\n", DOP);
				}

				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); }

					pShape->Set_Value("Information", HTML_File);
				}
				break;

				case 2: // Select Directory
					if( !SG_Dir_Exists(Parameters("HTML_DIRECTORY")->asString()) )
					{
						Message_Fmt("\n%s!\n%s\n%s\n", _TL("Warning"), _TL("Storing feature descriptions will be skipped!"), _TL("Directory does not exist!"));
					}
					else
					{
						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); }

						pShape->Set_Value("Information", HTML_File);
					}
					break;

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

						pShape->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());

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

			Message_Fmt("File %s: %s, %s, %s, %s",
			   File.c_str(), Coord_Message.c_str(), Alt_Message.c_str(), 
			   Dir_Message.c_str(), Dop_Message.c_str()
			);
		}
	}

	//-----------------------------------------------------
	if( has_GUI() )
	{
		if( Parameters("IMAGE_FIELD")->asBool() )
		{
			DataObject_Set_Parameter(pShapes, "IMAGE_FIELD",  1 );
			DataObject_Set_Parameter(pShapes, "IMAGE_SCALE", 30.);
		}

		if( Parameters("HTML_INFO")->asInt() )
		{
			DataObject_Set_Parameter(pShapes, "INFO_FIELD", 10 );
		}

		switch( Parameters("ADD")->asInt() )
		{
		case 1: DataObject_Update(pShapes, SG_UI_DATAOBJECT_SHOW_MAP_NEW   ); break;
		case 2: DataObject_Update(pShapes, SG_UI_DATAOBJECT_SHOW_MAP_ACTIVE); break;
		}
	}

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

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

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