// swad_photo.c: Users' photos management

/*
    SWAD (Shared Workspace At a Distance),
    is a web platform developed at the University of Granada (Spain),
    and used to support university teaching.

    This file is part of SWAD core.
    Copyright (C) 1999-2016 Antonio Caņas Vargas

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as
    published by the Free Software Foundation, either version 3 of the
    License, or (at your option) any later version.

    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 Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/
/*****************************************************************************/
/********************************* Headers ***********************************/
/*****************************************************************************/

#include <linux/limits.h>	// For PATH_MAX
#include <linux/stddef.h>	// For NULL
#include <math.h>		// For log10, floor, ceil, modf, sqrt...
#include <stdlib.h>		// For system, getenv, etc.
#include <string.h>		// For string functions
#include <sys/wait.h>		// For the macro WEXITSTATUS
#include <unistd.h>		// For unlink

#include "swad_action.h"
#include "swad_config.h"
#include "swad_database.h"
#include "swad_enrollment.h"
#include "swad_file.h"
#include "swad_file_browser.h"
#include "swad_global.h"
#include "swad_logo.h"
#include "swad_parameter.h"
#include "swad_photo.h"
#include "swad_privacy.h"
#include "swad_theme.h"
#include "swad_user.h"

/*****************************************************************************/
/************** External global variables from others modules ****************/
/*****************************************************************************/

extern struct Globals Gbl;
extern struct Act_Actions Act_Actions[Act_NUM_ACTIONS];

/*****************************************************************************/
/****************************** Public constants *****************************/
/*****************************************************************************/

/*****************************************************************************/
/***************************** Private constants *****************************/
/*****************************************************************************/

const char *Pho_StrAvgPhotoDirs[Pho_NUM_AVERAGE_PHOTO_TYPES] =
  {
   Cfg_FOLDER_DEGREE_PHOTO_MEDIAN,
   Cfg_FOLDER_DEGREE_PHOTO_AVERAGE,
  };
const char *Pho_StrAvgPhotoPrograms[Pho_NUM_AVERAGE_PHOTO_TYPES] =
  {
   Cfg_COMMAND_DEGREE_PHOTO_MEDIAN,
   Cfg_COMMAND_DEGREE_PHOTO_AVERAGE,
  };

/*****************************************************************************/
/******************************* Private types *******************************/
/*****************************************************************************/

/*****************************************************************************/
/***************************** Private variables *****************************/
/*****************************************************************************/

/*****************************************************************************/
/***************************** Private prototypes ****************************/
/*****************************************************************************/

static void Pho_PutLinkToRemoveMyPhoto (void);
static void Pho_PutLinkToRemoveOtherUsrPhoto (const struct UsrData *UsrDat);

static void Pho_UpdatePhoto1 (struct UsrData *UsrDat);
static void Pho_UpdatePhoto2 (void);
static void Pho_ClearPhotoName (long UsrCod);

static long Pho_GetDegWithAvgPhotoLeastRecentlyUpdated (void);
static long Pho_GetTimeAvgPhotoWasComputed (long DegCod);
static long Pho_GetTimeToComputeAvgPhoto (long DegCod);
static void Pho_ComputeAveragePhoto (long DegCod,Usr_Sex_t Sex,struct ListUsers *LstUsrs,
                                     Pho_AvgPhotoTypeOfAverage_t TypeOfAverage,const char *DirAvgPhotosRelPath,
                                     unsigned *NumStds,unsigned *NumStdsWithPhoto,long *TimeToComputeAvgPhotoInMicroseconds);
static void Pho_PutSelectorForTypeOfAvg (void);
static Pho_AvgPhotoTypeOfAverage_t Pho_GetPhotoAvgTypeFromForm (void);
static void Pho_PutSelectorForHowComputePhotoSize (void);
static Pho_HowComputePhotoSize_t Pho_GetHowComputePhotoSizeFromForm (void);
static void Pho_PutSelectorForHowOrderDegrees (void);
static Pho_HowOrderDegrees_t Pho_GetHowOrderDegreesFromForm (void);

static void Pho_PutLinkToPrintViewOfDegreeStats (void);
static void Pho_PutLinkToPrintViewOfDegreeStatsParams (void);

static void Pho_PutLinkToCalculateDegreeStats (void);
static void Pho_GetMaxStdsPerDegree (void);
static void Pho_ShowOrPrintClassPhotoDegrees (Pho_AvgPhotoSeeOrPrint_t SeeOrPrint);
static void Pho_ShowOrPrintListDegrees (Pho_AvgPhotoSeeOrPrint_t SeeOrPrint);
static void Pho_BuildQueryOfDegrees (char *Query);
static void Pho_GetNumStdsInDegree (long DegCod,Usr_Sex_t Sex,int *NumStds,int *NumStdsWithPhoto);
static void Pho_UpdateDegStats (long DegCod,Usr_Sex_t Sex,unsigned NumStds,unsigned NumStdsWithPhoto,long TimeToComputeAvgPhoto);
static void Pho_ShowDegreeStat (int NumStds,int NumStdsWithPhoto);
static void Pho_ShowDegreeAvgPhotoAndStat (struct Degree *Deg,Pho_AvgPhotoSeeOrPrint_t SeeOrPrint,Usr_Sex_t Sex,int NumStds,int NumStdsWithPhoto);
static void Pho_ComputePhotoSize (int NumStds,int NumStdsWithPhoto,unsigned *PhotoWidth,unsigned *PhotoHeight);

/*****************************************************************************/
/************** Check if I can change the photo of another user **************/
/*****************************************************************************/

bool Pho_CheckIfICanChangeOtherUsrPhoto (const struct UsrData *UsrDat)
  {
   if (UsrDat->UsrCod == Gbl.Usrs.Me.UsrDat.UsrCod)	// It's me
      return true;

   /* Check if I have permission to change user's photo
      Only users who have accepted registration in courses are counted */
   switch (Gbl.Usrs.Me.LoggedRole)
     {
      case Rol_TEACHER:
	 /* If I am a teacher in current course,
	    I only can change the photo of users from current course */
	 return Usr_CheckIfUsrBelongsToCrs (UsrDat->UsrCod,
	                                    Gbl.CurrentCrs.Crs.CrsCod,
	                                    true);
      case Rol_DEG_ADM:
	 /* If I am an administrator of current degree,
	    I only can change the photo of users from current degree */
	 return Usr_CheckIfUsrBelongsToDeg (UsrDat->UsrCod,
	                                    Gbl.CurrentDeg.Deg.DegCod,
	                                    true);
      case Rol_CTR_ADM:
	 /* If I am an administrator of current centre,
	    I only can change the photo of users from current centre */
	 return Usr_CheckIfUsrBelongsToCtr (UsrDat->UsrCod,
	                                    Gbl.CurrentCtr.Ctr.CtrCod,
	                                    true);
      case Rol_INS_ADM:
	 /* If I am an administrator of current institution,
	    I only can change the photo of users from current institution */
	 return Usr_CheckIfUsrBelongsToIns (UsrDat->UsrCod,
	                                    Gbl.CurrentIns.Ins.InsCod,
	                                    true);
      case Rol_SYS_ADM:
	 return true;
      default:
	 return false;
     }
  }

/*****************************************************************************/
/********** Put a link to the action used to request user's photo ************/
/*****************************************************************************/

void Pho_PutLinkToChangeMyPhoto (void)
  {
   extern const char *Txt_Change_photo;
   extern const char *Txt_Upload_photo;

   /***** Link for changing / uploading the photo *****/
   Lay_PutContextualLink (ActReqMyPho,NULL,"photo64x64.gif",
                          Gbl.Usrs.Me.MyPhotoExists ? Txt_Change_photo :
		                                      Txt_Upload_photo,
                          Gbl.Usrs.Me.MyPhotoExists ? Txt_Change_photo :
		                                      Txt_Upload_photo);
  }

/*****************************************************************************/
/********** Put a link to the action used to request user's photo ************/
/*****************************************************************************/

void Pho_PutLinkToChangeOtherUsrPhoto (void)
  {
   extern const char *Txt_Change_photo;
   extern const char *Txt_Upload_photo;
   bool PhotoExists;
   char PhotoURL[PATH_MAX+1];

   /***** Link for changing / uploading the photo *****/
   if (Gbl.Usrs.Other.UsrDat.UsrCod == Gbl.Usrs.Me.UsrDat.UsrCod)	// It's me
      Pho_PutLinkToChangeMyPhoto ();
   else									// Not me
      if (Pho_CheckIfICanChangeOtherUsrPhoto (&Gbl.Usrs.Other.UsrDat))
	{
	 PhotoExists = Pho_BuildLinkToPhoto (&Gbl.Usrs.Other.UsrDat,PhotoURL,true);
	 Lay_PutContextualLink ( Gbl.Usrs.Other.UsrDat.RoleInCurrentCrsDB == Rol_STUDENT ? ActReqStdPho :
	                        (Gbl.Usrs.Other.UsrDat.RoleInCurrentCrsDB == Rol_TEACHER ? ActReqTchPho :
	                                                                                   ActReqOthPho),	// Guest, visitor or admin
	                        Usr_PutParamOtherUsrCodEncrypted,
	                        "photo64x64.gif",
				PhotoExists ? Txt_Change_photo :
					      Txt_Upload_photo,
				PhotoExists ? Txt_Change_photo :
					      Txt_Upload_photo);
	}
  }

/*****************************************************************************/
/********************* Put a link to remove user's photo *********************/
/*****************************************************************************/

static void Pho_PutLinkToRemoveMyPhoto (void)
  {
   extern const char *Txt_Remove_photo;

   /***** Link for removing the photo *****/
   Lay_PutContextualLink (ActRemMyPho,NULL,"remove-on64x64.png",
                          Txt_Remove_photo,Txt_Remove_photo);
  }

/*****************************************************************************/
/********************* Put a link to remove user's photo *********************/
/*****************************************************************************/

static void Pho_PutLinkToRemoveOtherUsrPhoto (const struct UsrData *UsrDat)
  {
   extern const char *Txt_Remove_photo;

   /***** Link for removing the photo *****/
   Lay_PutContextualLink ( UsrDat->RoleInCurrentCrsDB == Rol_STUDENT ? ActRemStdPho :
	                  (UsrDat->RoleInCurrentCrsDB == Rol_TEACHER ? ActRemTchPho :
	                                                               ActRemOthPho),	// Guest, visitor or admin
                          Usr_PutParamOtherUsrCodEncrypted,
                          "remove-on64x64.png",
                          Txt_Remove_photo,Txt_Remove_photo);
  }

/*****************************************************************************/
/************************ Form for sending my photo **************************/
/*****************************************************************************/

void Pho_ReqMyPhoto (void)
  {
   /* Show the form for sending the photo */
   Pho_ReqPhoto (&Gbl.Usrs.Me.UsrDat,Gbl.Usrs.Me.MyPhotoExists,Gbl.Usrs.Me.PhotoURL);
  }

/*****************************************************************************/
/******************* Form for sending other user's photo *********************/
/*****************************************************************************/

void Pho_ReqUsrPhoto (struct UsrData *UsrDat)
  {
   bool PhotoExists;
   char PhotoURL[PATH_MAX+1];

   /* Check if user's photo exists and create a link to it */
   PhotoExists = Pho_BuildLinkToPhoto (UsrDat,PhotoURL,true);

   /* Show the form for sending the photo */
   Pho_ReqPhoto (UsrDat,PhotoExists,PhotoURL);
  }

/*****************************************************************************/
/****************** Show a form for sending an user's photo ******************/
/*****************************************************************************/

void Pho_ReqPhoto (const struct UsrData *UsrDat,bool PhotoExists,const char *PhotoURL)
  {
   extern const char *The_ClassForm[The_NUM_THEMES];
   extern const char *Txt_Photo;
   extern const char *Txt_You_can_send_a_file_with_an_image_in_jpg_format_;
   extern const char *Txt_File_with_the_photo;
   extern const char *Txt_Upload_photo;
   bool ItsMe = (UsrDat->UsrCod == Gbl.Usrs.Me.UsrDat.UsrCod);

   /***** Write message about photo presence or ausence *****/
   if (PhotoExists)	// User has photo
     {
      /***** Forms to remove photo and change privacy *****/
      fprintf (Gbl.F.Out,"<div class=\"CONTEXT_MENU\">");
      if (ItsMe)
	{
         Pho_PutLinkToRemoveMyPhoto ();
         Pri_PutLinkToChangeMyPrivacy ();	// Put link (form) to change my privacy
	}
      else
         Pho_PutLinkToRemoveOtherUsrPhoto (UsrDat);
      fprintf (Gbl.F.Out,"</div>");
     }

   /***** Start form *****/
   if (ItsMe)
      Act_FormStart (ActDetMyPho);
   else
     {
      Act_FormStart ( UsrDat->RoleInCurrentCrsDB == Rol_STUDENT ? ActDetStdPho :
	             (UsrDat->RoleInCurrentCrsDB == Rol_TEACHER ? ActDetTchPho :
	                                                          ActDetOthPho));	// Guest, visitor or admin
      Usr_PutParamUsrCodEncrypted (UsrDat->EncryptedUsrCod);
     }

   /***** Start frame *****/
   Lay_StartRoundFrameTable (NULL,2,Txt_Photo);

   /***** Show current photo and help message *****/
   fprintf (Gbl.F.Out,"<tr>"
                      "<td colspan=\"2\">");
   // if (PhotoExists)
   Pho_ShowUsrPhoto (UsrDat,PhotoURL,
                     "PHOTO186x248",Pho_NO_ZOOM,false);
   Lay_ShowAlert (Lay_INFO,Txt_You_can_send_a_file_with_an_image_in_jpg_format_);
   fprintf (Gbl.F.Out,"</td>"
                      "</tr>");

   /***** Form to upload photo *****/
   fprintf (Gbl.F.Out,"<tr>"
                      "<td class=\"%s RIGHT_MIDDLE\">"
                      "%s:"
                      "</td>"
                      "<td class=\"LEFT_MIDDLE\">"
                      "<input type=\"file\" name=\"%s\""
                      " size=\"40\" maxlength=\"100\" value=\"%ld.jpg\" />"
                      "</td>"
                      "</tr>",
            The_ClassForm[Gbl.Prefs.Theme],
            Txt_File_with_the_photo,
            Fil_NAME_OF_PARAM_FILENAME_ORG,
            UsrDat->UsrCod);

   /***** End frame *****/
   Lay_EndRoundFrameTableWithButton (Lay_CREATE_BUTTON,Txt_Upload_photo);

   /***** End form *****/
   Act_FormEnd ();
  }

/*****************************************************************************/
/*********************** Send another user's photograph **********************/
/*****************************************************************************/

void Pho_SendPhotoUsr (void)
  {
   extern const char *Txt_User_not_found_or_you_do_not_have_permission_;

   /***** Get user whose photo must be sent or removed *****/
   if (Usr_GetParamOtherUsrCodEncryptedAndGetUsrData ())
     {
      if (Pho_CheckIfICanChangeOtherUsrPhoto (&Gbl.Usrs.Other.UsrDat))	// If I have permission to change user's photo...
	{
	 Gbl.Usrs.Other.UsrDat.Accepted = Usr_CheckIfUsrBelongsToCrs (Gbl.Usrs.Other.UsrDat.UsrCod,
	                                                              Gbl.CurrentCrs.Crs.CrsCod,
	                                                              true);
	 Pho_ReqUsrPhoto (&Gbl.Usrs.Other.UsrDat);        // Request user's photograph
	}
      else
         Lay_ShowAlert (Lay_WARNING,Txt_User_not_found_or_you_do_not_have_permission_);
     }
   else		// User not found
      Lay_ShowAlert (Lay_WARNING,Txt_User_not_found_or_you_do_not_have_permission_);
  }

/*****************************************************************************/
/**************** Receive my photo and detect faces on it ********************/
/*****************************************************************************/

void Pho_RecMyPhotoDetFaces (void)
  {
   /***** Receive my photo and detect faces on it *****/
   Pho_ReceivePhotoAndDetectFaces (true,&Gbl.Usrs.Me.UsrDat);
  }

/*****************************************************************************/
/********** Receive another user's photo and detect faces on it **************/
/*****************************************************************************/

void Pho_RecUsrPhotoDetFaces (void)
  {
   extern const char *Txt_User_not_found_or_you_do_not_have_permission_;

   /***** Get user's code from form *****/
   Usr_GetParamOtherUsrCodEncryptedAndGetListIDs ();

   /***** Get password, user type and user's data from database *****/
   if (Usr_ChkUsrCodAndGetAllUsrDataFromUsrCod (&Gbl.Usrs.Other.UsrDat))
      /***** Receive photo *****/
      Pho_ReceivePhotoAndDetectFaces (false,&Gbl.Usrs.Other.UsrDat);
   else
      Lay_ShowAlert (Lay_WARNING,Txt_User_not_found_or_you_do_not_have_permission_);
  }

/*****************************************************************************/
/****************************** Remove my photo ******************************/
/*****************************************************************************/

void Pho_RemoveMyPhoto1 (void)
  {
   /***** Remove photo *****/
   Gbl.Error = !Pho_RemovePhoto (&Gbl.Usrs.Me.UsrDat);

   /***** The link to my photo is not valid now, so build it again before writing the web page *****/
   Gbl.Usrs.Me.MyPhotoExists = Pho_BuildLinkToPhoto (&Gbl.Usrs.Me.UsrDat,Gbl.Usrs.Me.PhotoURL,true);
  }

void Pho_RemoveMyPhoto2 (void)
  {
   extern const char *Txt_Photo_removed;

   if (!Gbl.Error)
      Lay_ShowAlert (Lay_SUCCESS,Txt_Photo_removed);
  }

/*****************************************************************************/
/************************ Remove another user's photo ************************/
/*****************************************************************************/

void Pho_RemoveUsrPhoto (void)
  {
   extern const char *Txt_Photo_removed;
   extern const char *Txt_User_not_found_or_you_do_not_have_permission_;

   /***** Get user's code from form *****/
   Usr_GetParamOtherUsrCodEncryptedAndGetListIDs ();

   /***** Get password, user type and user's data from database *****/
   if (Usr_ChkUsrCodAndGetAllUsrDataFromUsrCod (&Gbl.Usrs.Other.UsrDat))
     {
      /***** Remove photo *****/
      if (Pho_RemovePhoto (&Gbl.Usrs.Other.UsrDat))
         Lay_ShowAlert (Lay_SUCCESS,Txt_Photo_removed);
     }
   else
      Lay_ShowAlert (Lay_WARNING,Txt_User_not_found_or_you_do_not_have_permission_);
  }

/*****************************************************************************/
/***************** Receive a photo and detect faces on it ********************/
/*****************************************************************************/

void Pho_ReceivePhotoAndDetectFaces (bool ItsMe,const struct UsrData *UsrDat)
  {
   extern const char *Txt_The_file_is_not_X;
   extern const char *Txt_Could_not_detect_any_face_in_front_position_;
   extern const char *Txt_A_face_marked_in_green_has_been_detected_;
   extern const char *Txt_A_face_marked_in_red_has_been_detected_;
   extern const char *Txt_Try_sending_another_image_where_the_face_appears_in_front_position_;
   extern const char *Txt_X_faces_marked_in_green_have_been_detected_;
   extern const char *Txt_X_faces_marked_in_red_have_been_detected_;
   extern const char *Txt_X_faces_have_been_detected_in_front_position_1_Z_;
   extern const char *Txt_X_faces_have_been_detected_in_front_position_Y_Z_;
   extern const char *Txt_Faces_detected;
   char PathPhotosPriv[PATH_MAX+1];
   char PathPhotosPubl[PATH_MAX+1];
   char PathPhotosTmpPubl[PATH_MAX+1];
   char FileNamePhotoSrc[PATH_MAX+1];
   char FileNamePhotoTmp[PATH_MAX+1];	// Full name (including path and .jpg) of the destination temporary file
   char FileNamePhotoMap[PATH_MAX+1];	// Full name (including path) of the temporary file with the original image with faces
   char FileNameTxtMap[PATH_MAX+1];	// Full name (including path) of the temporary file with the text neccesary to make the image map
   char PathRelPhoto[PATH_MAX+1];
   FILE *FileTxtMap = NULL;	// Temporary file with the text neccesary to make the image map. Initialized to avoid warning
   char MIMEType[Brw_MAX_BYTES_MIME_TYPE+1];
   bool WrongType = false;
   char Command[256+PATH_MAX];	// Command to call the program of preprocessing of photos
   int ReturnCode;
   int NumLastForm = 0;	// Initialized to avoid warning
   char FormId[32];
   unsigned NumFacesTotal = 0;
   unsigned NumFacesGreen = 0;
   unsigned NumFacesRed = 0;
   unsigned NumFace;
   unsigned X;
   unsigned Y;
   unsigned Radius;
   unsigned BackgroundCode;
   char StrFileName[NAME_MAX+1];

   /***** Creates directories if not exist *****/
   sprintf (PathPhotosPriv,"%s/%s",
	    Cfg_PATH_SWAD_PRIVATE,Cfg_FOLDER_PHOTO);
   Fil_CreateDirIfNotExists (PathPhotosPriv);
   sprintf (PathPhotosPriv,"%s/%s/%02u",
	    Cfg_PATH_SWAD_PRIVATE,Cfg_FOLDER_PHOTO,
	    (unsigned) (UsrDat->UsrCod % 100));
   Fil_CreateDirIfNotExists (PathPhotosPriv);

   sprintf (PathPhotosPubl,"%s/%s",
	    Cfg_PATH_SWAD_PUBLIC,Cfg_FOLDER_PHOTO);
   Fil_CreateDirIfNotExists (PathPhotosPubl);
   sprintf (PathPhotosTmpPubl,"%s/%s/%s",
	    Cfg_PATH_SWAD_PUBLIC,Cfg_FOLDER_PHOTO,Cfg_FOLDER_PHOTO_TMP);
   Fil_CreateDirIfNotExists (PathPhotosTmpPubl);

   /***** Remove old files *****/
   Fil_RemoveOldTmpFiles (PathPhotosTmpPubl,Cfg_TIME_TO_DELETE_PHOTOS_TMP_FILES,false);

   /***** First of all, copy in disk the file received from stdin (really from Gbl.F.Tmp) *****/
   Fil_StartReceptionOfFile (FileNamePhotoSrc,MIMEType);

   /* Check if the file type is image/jpeg or image/pjpeg or application/octet-stream */
   if (strcmp (MIMEType,"image/jpeg"))
      if (strcmp (MIMEType,"image/pjpeg"))
         if (strcmp (MIMEType,"application/octet-stream"))
            if (strcmp (MIMEType,"application/octetstream"))
               if (strcmp (MIMEType,"application/octet"))
                  WrongType = true;
   if (WrongType)
     {
      sprintf (Gbl.Message,Txt_The_file_is_not_X,"jpg");
      Lay_ShowAlert (Lay_WARNING,Gbl.Message);
      return;
     }

   /* End the reception of photo in a temporary file */
   sprintf (FileNamePhotoTmp,"%s/%s/%s/%s.jpg",
            Cfg_PATH_SWAD_PUBLIC,Cfg_FOLDER_PHOTO,
            Cfg_FOLDER_PHOTO_TMP,Gbl.UniqueNameEncrypted);
   if (!Fil_EndReceptionOfFile (FileNamePhotoTmp))
     {
      Lay_ShowAlert (Lay_WARNING,"Error uploading file.");
      return;
     }

   /* Copy the original photo received to private directory.
      The purpose of this copy is only to have a backup used for researching better methods to detect faces in images */
   sprintf (PathRelPhoto,"%s/%s/%02u/%ld_original.jpg",
            Cfg_PATH_SWAD_PRIVATE,Cfg_FOLDER_PHOTO,
            (unsigned) (UsrDat->UsrCod % 100),UsrDat->UsrCod);
   Fil_FastCopyOfFiles (FileNamePhotoTmp,PathRelPhoto);

   /***** Call to program that makes photo processing / face detection *****/
   sprintf (Command,Cfg_COMMAND_FACE_DETECTION,FileNamePhotoTmp);
   ReturnCode = system (Command);
   if (ReturnCode == -1)
      Lay_ShowErrorAndExit ("Error when running command to process photo and detect faces.");

   /***** Write message depending on return code *****/
   ReturnCode = WEXITSTATUS(ReturnCode);
   switch (ReturnCode)
     {
      case 0:        // Faces detected
         /***** Open text file with text for image map *****/
         sprintf (FileNameTxtMap,"%s/%s/%s/%s_map.txt",
                  Cfg_PATH_SWAD_PUBLIC,Cfg_FOLDER_PHOTO,
                  Cfg_FOLDER_PHOTO_TMP,Gbl.UniqueNameEncrypted);
         if ((FileTxtMap = fopen (FileNameTxtMap,"rb")) == NULL)
            Lay_ShowErrorAndExit ("Can not read text file with coordinates of detected faces.");

         /***** Read file with coordinates for image map and compute the number of faces *****/
         NumLastForm = Gbl.Form.Num;
         while (!feof (FileTxtMap))
           {
            if (fscanf (FileTxtMap,"%u %u %u %u %s\n",&X,&Y,&Radius,&BackgroundCode,StrFileName) != 5)        // Example of StrFileName = "4924a838630e_016"

               break;
            if (BackgroundCode == 1)
              {
               NumFacesGreen++;
               Act_FormStart (ItsMe ? ActUpdMyPho :
        	                      (UsrDat->RoleInCurrentCrsDB == Rol_STUDENT ? ActUpdStdPho :
        	                      (UsrDat->RoleInCurrentCrsDB == Rol_TEACHER ? ActUpdTchPho :
        	                	                                           ActUpdOthPho)));	// Guest, visitor or admin
               if (!ItsMe)
                  Usr_PutParamUsrCodEncrypted (UsrDat->EncryptedUsrCod);
               Par_PutHiddenParamString ("FileName",StrFileName);
               Act_FormEnd ();
              }
            else
               NumFacesRed++;
           }
         NumFacesTotal = NumFacesGreen + NumFacesRed;
         break;
      case 1:        // No faces detected
         NumFacesTotal = NumFacesGreen = NumFacesRed = 0;
         break;
      default:        // Error
         sprintf (Gbl.Message,"Photo could not be processed successfully.<br />"
                              "Error code returned by the program of processing: %d getuid() = %u geteuid() = %u",
                  ReturnCode,(unsigned) getuid(), (unsigned) geteuid());
         Lay_ShowErrorAndExit (Gbl.Message);
         break;
     }

   /***** Message to the user about the number of faces detected in the image*****/
   if (NumFacesTotal == 0)
     {
      Lay_ShowAlert (Lay_WARNING,Txt_Could_not_detect_any_face_in_front_position_);
      Lay_ShowAlert (Lay_INFO,Txt_Try_sending_another_image_where_the_face_appears_in_front_position_);
     }
   else if (NumFacesTotal == 1)
     {
      if (NumFacesGreen == 1)
         Lay_ShowAlert (Lay_SUCCESS,Txt_A_face_marked_in_green_has_been_detected_);
      else
        {
         Lay_ShowAlert (Lay_WARNING,Txt_A_face_marked_in_red_has_been_detected_);
         Lay_ShowAlert (Lay_INFO,Txt_Try_sending_another_image_where_the_face_appears_in_front_position_);
        }
     }
   else        // NumFacesTotal > 1
     {
      if (NumFacesRed == 0)
        {
         sprintf (Gbl.Message,Txt_X_faces_marked_in_green_have_been_detected_,
                  NumFacesGreen);
         Lay_ShowAlert (Lay_SUCCESS,Gbl.Message);
        }
      else if (NumFacesGreen == 0)
        {
         sprintf (Gbl.Message,Txt_X_faces_marked_in_red_have_been_detected_,
                  NumFacesRed);
         Lay_ShowAlert (Lay_WARNING,Gbl.Message);
         Lay_ShowAlert (Lay_INFO,Txt_Try_sending_another_image_where_the_face_appears_in_front_position_);
        }
      else        // NumFacesGreen > 0
        {
         if (NumFacesGreen == 1)
            sprintf (Gbl.Message,Txt_X_faces_have_been_detected_in_front_position_1_Z_,
                     NumFacesTotal,NumFacesRed);
         else
            sprintf (Gbl.Message,Txt_X_faces_have_been_detected_in_front_position_Y_Z_,
                     NumFacesTotal,NumFacesGreen,NumFacesRed);
         Lay_ShowAlert (Lay_SUCCESS,Gbl.Message);
        }
     }

   /***** Button to send another photo *****/
   if (NumFacesGreen == 0)
     {
      fprintf (Gbl.F.Out,"<div class=\"CONTEXT_MENU\">");
      if (UsrDat->UsrCod == Gbl.Usrs.Me.UsrDat.UsrCod)
         Pho_PutLinkToChangeMyPhoto ();
      else if (UsrDat->UsrCod == Gbl.Usrs.Other.UsrDat.UsrCod)
         Pho_PutLinkToChangeOtherUsrPhoto ();
      fprintf (Gbl.F.Out,"</div>");
     }

   /***** Create map *****/
   fprintf (Gbl.F.Out,"<map name=\"faces_map\">\n");
   if (NumFacesTotal)
     {
      /***** Read again the file with coordinates and create area shapes *****/
      rewind (FileTxtMap);
      for (NumFace = 0; !feof (FileTxtMap);)
        {
         if (fscanf (FileTxtMap,"%u %u %u %u %s\n",&X,&Y,&Radius,&BackgroundCode,StrFileName) != 5)
            break;
         if (BackgroundCode == 1)
           {
            NumFace++;
            sprintf (FormId,"form_%d",NumLastForm + NumFace);
            fprintf (Gbl.F.Out,"<area shape=\"circle\""
                               " href=\"javascript:document.getElementById('%s').submit();\""
                               " coords=\"%u,%u,%u\">\n",
                     FormId,X,Y,Radius);
           }
        }
      /***** Close text file with text for image map *****/
      fclose (FileTxtMap);
     }
   fprintf (Gbl.F.Out,"</map>\n");

   /***** Show map photo *****/
   sprintf (FileNamePhotoMap,"%s/%s/%s/%s_map.jpg",
            Cfg_PATH_SWAD_PUBLIC,Cfg_FOLDER_PHOTO,Cfg_FOLDER_PHOTO_TMP,Gbl.UniqueNameEncrypted);
   fprintf (Gbl.F.Out,"<div class=\"TIT CENTER_MIDDLE\">"
                      "<img src=\"%s/%s/%s/%s_map.jpg\""
                      " usemap=\"#faces_map\""
                      " alt=\"%s\" title=\"%s\" />"
                      "</div>",
            Cfg_HTTPS_URL_SWAD_PUBLIC,Cfg_FOLDER_PHOTO,Cfg_FOLDER_PHOTO_TMP,
            Gbl.UniqueNameEncrypted,
            Txt_Faces_detected,Txt_Faces_detected);
  }

/*****************************************************************************/
/***************************** Update my photo *******************************/
/*****************************************************************************/

void Pho_UpdateMyPhoto1 (void)
  {
   Pho_UpdatePhoto1 (&Gbl.Usrs.Me.UsrDat);

   /***** The link to my photo is not valid now, so build it again before writing the web page *****/
   Gbl.Usrs.Me.MyPhotoExists = Pho_BuildLinkToPhoto (&Gbl.Usrs.Me.UsrDat,Gbl.Usrs.Me.PhotoURL,true);
  }

void Pho_UpdateMyPhoto2 (void)
  {
   Pho_UpdatePhoto2 ();
  }

/*****************************************************************************/
/*********************** Update another user's photo *************************/
/*****************************************************************************/

void Pho_UpdateUsrPhoto1 (void)
  {
   extern const char *Txt_User_not_found_or_you_do_not_have_permission_;

   /***** Get user's code from form and user's data *****/
   if (Usr_GetParamOtherUsrCodEncryptedAndGetUsrData ())
      Pho_UpdatePhoto1 (&Gbl.Usrs.Other.UsrDat);
   else
      Lay_ShowAlert (Lay_WARNING,Txt_User_not_found_or_you_do_not_have_permission_);
  }

void Pho_UpdateUsrPhoto2 (void)
  {
   Pho_UpdatePhoto2 ();
  }

/*****************************************************************************/
/*************** Update a user's photo with a selected face ******************/
/*****************************************************************************/

static void Pho_UpdatePhoto1 (struct UsrData *UsrDat)
  {
   char PathPhotoTmp[PATH_MAX+1];        // Full name (including path and .jpg) of the temporary file with the selected face
   char PathRelPhoto[PATH_MAX+1];

   /***** Get the name of the file with the selected face *****/
   Par_GetParToText ("FileName",Gbl.Usrs.FileNamePhoto,NAME_MAX);        // Example of FileNamePhoto: "4924a838630e_016"

   /***** Convert the temporary photo resulting of the processing to the current photo of the user *****/
   sprintf (PathPhotoTmp,"%s/%s/%s/%s_paso3.jpg",
            Cfg_PATH_SWAD_PUBLIC,Cfg_FOLDER_PHOTO,Cfg_FOLDER_PHOTO_TMP,Gbl.Usrs.FileNamePhoto);
   if (Fil_CheckIfPathExists (PathPhotoTmp))        // The file with the selected photo exists
     {
      /* Copy the temporary file of the third (last) step resulting of the processing to the directory of private photos */
      sprintf (PathRelPhoto,"%s/%s/%02u/%ld.jpg",
               Cfg_PATH_SWAD_PRIVATE,Cfg_FOLDER_PHOTO,
               (unsigned) (UsrDat->UsrCod % 100),UsrDat->UsrCod);
      Fil_FastCopyOfFiles (PathPhotoTmp,PathRelPhoto);

      /* Update public photo name in database */
      Pho_UpdatePhotoName (UsrDat);

      /* Remove the user from the list of users without photo */
      Pho_RemoveUsrFromTableClicksWithoutPhoto (UsrDat->UsrCod);

      Gbl.Error = false;
     }
   else
      Gbl.Error = true;
  }

static void Pho_UpdatePhoto2 (void)
  {
   extern const char *Txt_PHOTO_PROCESSING_CAPTIONS[3];
   extern const char *Txt_Photo_has_been_updated;
   unsigned NumPhoto;

   /***** Show the three images resulting of the processing *****/
   fprintf (Gbl.F.Out,"<table style=\"width:100%%;\">"
                      "<tr>");
   for (NumPhoto = 0;
        NumPhoto < 3;
        NumPhoto++)
      fprintf (Gbl.F.Out,"<td class=\"DAT CENTER_TOP\" style=\"width:33%%;\">"
                         "<img src=\"%s/%s/%s/%s_paso%u.jpg\""
                         " alt=\"%s\" title=\"%s\""
                         " style=\"width:%upx; height:%upx;\" />"
                         "<br />%s"
                         "</td>",
               Cfg_HTTPS_URL_SWAD_PUBLIC,Cfg_FOLDER_PHOTO,Cfg_FOLDER_PHOTO_TMP,
               Gbl.Usrs.FileNamePhoto,NumPhoto + 1,
               Txt_PHOTO_PROCESSING_CAPTIONS[NumPhoto],
               Txt_PHOTO_PROCESSING_CAPTIONS[NumPhoto],
               Pho_PHOTO_REAL_WIDTH,Pho_PHOTO_REAL_HEIGHT,
               Txt_PHOTO_PROCESSING_CAPTIONS[NumPhoto]);
   fprintf (Gbl.F.Out,"</tr>"
	              "</table>");

   /***** Show message *****/
   if (Gbl.Error)        // The file with the selected photo does not exist!
      Lay_ShowErrorAndExit ("Selected photo does not exist.");

   Lay_ShowAlert (Lay_SUCCESS,Txt_Photo_has_been_updated);
  }

/*****************************************************************************/
/******************* Update number of clicks without photo *******************/
/*****************************************************************************/

unsigned Pho_UpdateMyClicksWithoutPhoto (void)
  {
   char Query[512];
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;
   unsigned long NumRows;
   unsigned NumClicks;

   /***** Get number of clicks without photo from database *****/
   sprintf (Query,"SELECT NumClicks FROM clicks_without_photo"
                  " WHERE UsrCod='%ld'",
            Gbl.Usrs.Me.UsrDat.UsrCod);
   NumRows = DB_QuerySELECT (Query,&mysql_res,"can not get number of clicks without photo");

   /***** Update the list of clicks without photo *****/
   if (NumRows)        // The user exists ==> update number of clicks without photo
     {
      /* Get current number of clicks */
      row = mysql_fetch_row (mysql_res);
      sscanf (row[0],"%u",&NumClicks);

      /* Update number of clicks */
      if (NumClicks <= Pho_MAX_CLICKS_WITHOUT_PHOTO)
        {
         sprintf (Query,"UPDATE clicks_without_photo SET NumClicks=NumClicks+1 WHERE UsrCod='%ld'",
                  Gbl.Usrs.Me.UsrDat.UsrCod);
         DB_QueryUPDATE (Query,"can not update number of clicks without photo");
         NumClicks++;
        }
     }
   else                                        // The user does not exist ==> add him/her
     {
      /* Add the user, with one access */
      sprintf (Query,"INSERT INTO clicks_without_photo (UsrCod,NumClicks) VALUES ('%ld',1)",
               Gbl.Usrs.Me.UsrDat.UsrCod);
      DB_QueryINSERT (Query,"can not create number of clicks without photo");
      NumClicks = 1;
     }

   /***** Free structure that stores the query result *****/
   DB_FreeMySQLResult (&mysql_res);

   /***** Return the number of rows of the result *****/
   return NumClicks;
  }

/*****************************************************************************/
/******** Remove user from table with number of clicks without photo *********/
/*****************************************************************************/

void Pho_RemoveUsrFromTableClicksWithoutPhoto (long UsrCod)
  {
   char Query[512];

   sprintf (Query,"DELETE FROM clicks_without_photo WHERE UsrCod='%ld'",UsrCod);
   DB_QueryDELETE (Query,"can not remove a user from the list of users without photo");
  }

/*****************************************************************************/
/********************* Check if user's photo can be shown ********************/
/*****************************************************************************/
// Returns true if the photo can be shown and false if not.
// Public photo means two different things depending on the user's type

bool Pho_ShowUsrPhotoIsAllowed (const struct UsrData *UsrDat,char *PhotoURL)
  {
   bool ICanSeePhoto;

   /***** Check if I can see the other's photo *****/
   ICanSeePhoto = Pri_ShowIsAllowed (UsrDat->PhotoVisibility,UsrDat->UsrCod);

   /***** Photo is shown if I can see it, and it exists *****/
   return ICanSeePhoto ? Pho_BuildLinkToPhoto (UsrDat,PhotoURL,true) :
	                 false;
  }

/*****************************************************************************/
/***************** Create a public link to a private photo *******************/
/*****************************************************************************/
// Returns false if photo does not exist
// Returns true if link is created successfully

bool Pho_BuildLinkToPhoto (const struct UsrData *UsrDat,char *PhotoURL,bool HTTPS)
  {
   char PathPublPhoto[PATH_MAX+1];
   char PathPrivPhoto[PATH_MAX+1];

   if (UsrDat->Photo[0])
     {
      /***** Make path to public photo *****/
      sprintf (PathPublPhoto,"%s/%s/%s.jpg",
               Cfg_PATH_SWAD_PUBLIC,Cfg_FOLDER_PHOTO,UsrDat->Photo);

      /***** Make path to private photo from public directory *****/
      sprintf (PathPrivPhoto,"%s/%s/%02u/%ld.jpg",
               Cfg_PATH_SWAD_PRIVATE,Cfg_FOLDER_PHOTO,
               (unsigned) (UsrDat->UsrCod % 100),UsrDat->UsrCod);

      /***** Create a symbolic link to the private photo, if not exists *****/
      if (!Fil_CheckIfPathExists (PathPublPhoto))
         if (symlink (PathPrivPhoto,PathPublPhoto) != 0)
           {
            sprintf (Gbl.Message,"symlink (%s,%s)",
                     PathPrivPhoto,PathPublPhoto);
            Lay_ShowAlert (Lay_WARNING,Gbl.Message);
            Lay_ShowErrorAndExit ("Can not create public link"
                                 " to access to user's private photo");
           }

      /***** Create the public URL of the photo *****/
      sprintf (PhotoURL,"%s/%s/%s.jpg",
               HTTPS ? Cfg_HTTPS_URL_SWAD_PUBLIC :
                       Cfg_HTTP_URL_SWAD_PUBLIC,
               Cfg_FOLDER_PHOTO,UsrDat->Photo);

      return true;
     }
   else
     {
      PhotoURL[0] = '\0';
      return false;
     }
  }

/*****************************************************************************/
/*************** Check if photo exists and return path to it *****************/
/*****************************************************************************/
// Returns false if photo does not exist
// Returns true if photo exists

bool Pho_CheckIfPrivPhotoExists (long UsrCod,char *PathPrivRelPhoto)
  {
   /***** Make path to private photo *****/
   sprintf (PathPrivRelPhoto,"%s/%s/%02u/%ld.jpg",
            Cfg_PATH_SWAD_PRIVATE,Cfg_FOLDER_PHOTO,
            (unsigned) (UsrCod % 100),UsrCod);

   return Fil_CheckIfPathExists (PathPrivRelPhoto);
  }

/*****************************************************************************/
/************************** Remove a user's photo ****************************/
/*****************************************************************************/
// Returns true on success, false on error

bool Pho_RemovePhoto (struct UsrData *UsrDat)
  {
   char PathPrivRelPhoto[PATH_MAX+1];
   char PathPublPhoto[PATH_MAX+1];
   unsigned NumErrors = 0;

   if (UsrDat->Photo[0])
     {
      /***** Clear photo name in database *****/
      Pho_ClearPhotoName (UsrDat->UsrCod);

      /***** Remove public link *****/
      sprintf (PathPublPhoto,"%s/%s/%s.jpg",
               Cfg_PATH_SWAD_PUBLIC,Cfg_FOLDER_PHOTO,UsrDat->Photo);
      if (Fil_CheckIfPathExists (PathPublPhoto))	// Public link exists
         if (unlink (PathPublPhoto))			// Remove public link
           {
            Lay_ShowAlert (Lay_ERROR,"Error removing public link to photo.");
            NumErrors++;
           }

      /***** Remove photo *****/
      sprintf (PathPrivRelPhoto,"%s/%s/%02u/%ld.jpg",
               Cfg_PATH_SWAD_PRIVATE,Cfg_FOLDER_PHOTO,
               (unsigned) (UsrDat->UsrCod % 100),UsrDat->UsrCod);
      if (Fil_CheckIfPathExists (PathPrivRelPhoto))        // Photo exists
        {
         if (unlink (PathPrivRelPhoto))                        // Remove photo
           {
            Lay_ShowAlert (Lay_ERROR,"Error removing photo.");
            NumErrors++;
           }
        }

      /***** Remove original photo *****/
      sprintf (PathPrivRelPhoto,"%s/%s/%02u/%ld_original.jpg",
               Cfg_PATH_SWAD_PRIVATE,Cfg_FOLDER_PHOTO,
               (unsigned) (UsrDat->UsrCod % 100),UsrDat->UsrCod);
      if (Fil_CheckIfPathExists (PathPrivRelPhoto))		// Original photo exists
         if (unlink (PathPrivRelPhoto))				// Remove original photo
           {
            Lay_ShowAlert (Lay_ERROR,"Error removing the original photo.");
            NumErrors++;
           }

      /***** Clear photo name in user's data *****/
      UsrDat->Photo[0] = '\0';
     }

   return (NumErrors == 0);
  }

/*****************************************************************************/
/****************** Clear photo name of an user in database ******************/
/*****************************************************************************/

static void Pho_ClearPhotoName (long UsrCod)
  {
   char Query[128];

   /***** Clear photo name in user's data *****/
   sprintf (Query,"UPDATE usr_data SET Photo=''"
	          " WHERE UsrCod='%ld'",
	    UsrCod);
   DB_QueryUPDATE (Query,"can not clear the name of a user's photo");
  }

/*****************************************************************************/
/***************** Update photo name of an user in database ******************/
/*****************************************************************************/

void Pho_UpdatePhotoName (struct UsrData *UsrDat)
  {
   char Query[512];
   char PathPublPhoto[PATH_MAX+1];

   /***** Update photo name in database *****/
   sprintf (Query,"UPDATE usr_data SET Photo='%s'"
	          " WHERE UsrCod='%ld'",
            Gbl.UniqueNameEncrypted,UsrDat->UsrCod);
   DB_QueryUPDATE (Query,"can not update the name of a user's photo");

   /***** Remove the old symbolic link to photo *****/
   sprintf (PathPublPhoto,"%s/%s/%s.jpg",
            Cfg_PATH_SWAD_PUBLIC,Cfg_FOLDER_PHOTO,UsrDat->Photo);
   unlink (PathPublPhoto);                // Remove public link

   /***** Update photo name in user's data *****/
   strcpy (UsrDat->Photo,Gbl.UniqueNameEncrypted);
  }

/*****************************************************************************/
/****************** Write code to show the photo of a user *******************/
/*****************************************************************************/

void Pho_ShowUsrPhoto (const struct UsrData *UsrDat,const char *PhotoURL,
                       const char *ClassPhoto,Pho_Zoom_t Zoom,
                       bool FormUnique)
  {
   char SpecialFullName [3*(Usr_MAX_BYTES_NAME_SPEC_CHAR+1)+1];
   char SpecialShortName[3*(Usr_MAX_BYTES_NAME_SPEC_CHAR+1)+6];
   char SpecialSurnames [2*(Usr_MAX_BYTES_NAME_SPEC_CHAR+1)+1];
   bool PhotoExists;
   bool PutLinkToPublicProfile = !Gbl.Form.Inside &&						// Only if not inside another form
                                 Act_Actions[Gbl.Action.Act].BrowserWindow == Act_MAIN_WINDOW;	// Only in main window
   bool PutZoomCode = PhotoURL &&							// Photo exists
                      Zoom == Pho_ZOOM &&						// Make zoom
                      Act_Actions[Gbl.Action.Act].BrowserWindow == Act_MAIN_WINDOW;	// Only in main window

   /***** Replace tildes, ņ, etc. in full name by codes,
          because some browsers (i.e., IE5)
          don't show correctly tildes with AJAX *****/
   strcpy (SpecialFullName,UsrDat->FullName);
   Str_ReplaceSpecialCharByCodes (SpecialFullName,sizeof (SpecialFullName)-1);

   /***** Start form to go to public profile *****/
   if (PutLinkToPublicProfile)
     {
      if (FormUnique)
         Act_FormStartUnique (ActSeePubPrf);
      else
         Act_FormStart (ActSeePubPrf);
      Usr_PutParamUsrCodEncrypted (UsrDat->EncryptedUsrCod);
      if (FormUnique)
         Act_LinkFormSubmitUnique (NULL,NULL);
      else
         Act_LinkFormSubmit (NULL,NULL);
     }

   /***** Start image *****/
   fprintf (Gbl.F.Out,"<img src=\"");
   PhotoExists = false;
   if (PhotoURL)
      if (PhotoURL[0])
	 PhotoExists = true;
   if (PhotoExists)
      fprintf (Gbl.F.Out,"%s",PhotoURL);
   else
      fprintf (Gbl.F.Out,"%s/usr_bl.jpg",Gbl.Prefs.IconsURL);
   fprintf (Gbl.F.Out,"\" alt=\"%s\" title=\"%s\""
	              " class=\"%s\"",
            SpecialFullName,SpecialFullName,
	    ClassPhoto);

   /***** Image zoom *****/
   if (PutZoomCode)
     {
      strcpy (SpecialShortName,UsrDat->FirstName);
      Str_LimitLengthHTMLStr (SpecialShortName,23);
      Str_ReplaceSpecialCharByCodes (SpecialShortName,Usr_MAX_BYTES_NAME_SPEC_CHAR);
      SpecialSurnames[0] = '\0';
      if (UsrDat->Surname1[0])
         strcpy (SpecialSurnames,UsrDat->Surname1);
      if (UsrDat->Surname2[0])
        {
         strcat (SpecialSurnames," ");
         strcat (SpecialSurnames,UsrDat->Surname2);
        }
      Str_LimitLengthHTMLStr (SpecialSurnames,23);
      Str_ReplaceSpecialCharByCodes (SpecialSurnames,2*Usr_MAX_BYTES_NAME_SPEC_CHAR+1);
      strcat (SpecialShortName,"<br />");
      strcat (SpecialShortName,SpecialSurnames);
      fprintf (Gbl.F.Out," onmouseover=\"zoom(this,'%s','%s')\" onmouseout=\"noZoom();\"",
               PhotoURL,SpecialShortName);
     }

   /***** End image *****/
   fprintf (Gbl.F.Out," />");

   /***** End form to go to public profile *****/
   if (PutLinkToPublicProfile)
     {
      fprintf (Gbl.F.Out,"</a>");
      Act_FormEnd ();
     }
  }

/*****************************************************************************/
/************************** Change photo visibility **************************/
/*****************************************************************************/

void Pho_ChangePhotoVisibility (void)
  {
   extern const char *Pri_VisibilityDB[Pri_NUM_OPTIONS_PRIVACY];
   extern const char *Txt_The_visibility_of_your_photo_has_changed;
   char Query[128];

   /***** Get param with public/private photo *****/
   Gbl.Usrs.Me.UsrDat.PhotoVisibility = Pri_GetParamVisibility ("VisPho");

   /***** Store public/private photo in database *****/
   sprintf (Query,"UPDATE usr_data SET PhotoVisibility='%s'"
	          " WHERE UsrCod='%ld'",
            Pri_VisibilityDB[Gbl.Usrs.Me.UsrDat.PhotoVisibility],
            Gbl.Usrs.Me.UsrDat.UsrCod);
   DB_QueryUPDATE (Query,"can not update your preference about photo visibility");

   /***** Show alert *****/
   Lay_ShowAlert (Lay_SUCCESS,Txt_The_visibility_of_your_photo_has_changed);

   /***** Show form again *****/
   Pri_EditMyPrivacy ();
  }

/*****************************************************************************/
/******** Calculate average photos of all students from each degree **********/
/*****************************************************************************/

void Pho_CalcPhotoDegree (void)
  {
   char PathPhotosPublic[PATH_MAX+1];
   char PathPhotosTmpPriv[PATH_MAX+1];
   Pho_AvgPhotoTypeOfAverage_t TypeOfAverage;
   long DegCod = -1L;
   char DirAvgPhotosRelPath[Pho_NUM_AVERAGE_PHOTO_TYPES][PATH_MAX+1];
   unsigned NumStds,NumStdsWithPhoto;
   Usr_Sex_t Sex;
   long TotalTimeToComputeAvgPhotoInMicroseconds,PartialTimeToComputeAvgPhotoInMicroseconds;

   /***** Get type of average *****/
   Gbl.Stat.DegPhotos.TypeOfAverage = Pho_GetPhotoAvgTypeFromForm ();

   /***** Create public directories for average photos if not exist *****/
   sprintf (PathPhotosPublic,"%s/%s",
            Cfg_PATH_SWAD_PUBLIC,Cfg_FOLDER_PHOTO);
   Fil_CreateDirIfNotExists (PathPhotosPublic);
   for (TypeOfAverage = (Pho_AvgPhotoTypeOfAverage_t) 0;
	TypeOfAverage < Pho_NUM_AVERAGE_PHOTO_TYPES;
	TypeOfAverage++)
     {
      sprintf (DirAvgPhotosRelPath[TypeOfAverage],"%s/%s/%s",
               Cfg_PATH_SWAD_PUBLIC,Cfg_FOLDER_PHOTO,Pho_StrAvgPhotoDirs[TypeOfAverage]);
      Fil_CreateDirIfNotExists (DirAvgPhotosRelPath[TypeOfAverage]);
     }

   /***** Creates private directory for lists of users' photos if not exists *****/
   sprintf (PathPhotosTmpPriv,"%s/%s/%s",
            Cfg_PATH_SWAD_PRIVATE,Cfg_FOLDER_PHOTO,Cfg_FOLDER_PHOTO_TMP);
   Fil_CreateDirIfNotExists (PathPhotosTmpPriv);

   /***** Remove old private files used for lists *****/
   Fil_RemoveOldTmpFiles (PathPhotosTmpPriv,Cfg_TIME_TO_DELETE_PHOTOS_TMP_FILES,false);

   /***** Get the degree which photo will be computed *****/
   if ((DegCod = Deg_GetParamOtherDegCod ()) == -1L)
      Lay_ShowErrorAndExit ("Code of degree is missing.");

   /***** Prevent the computing of an average photo too recently updated *****/
   if (Pho_GetTimeAvgPhotoWasComputed (DegCod) >=
       Gbl.StartExecutionTimeUTC - Cfg_MIN_TIME_TO_RECOMPUTE_AVG_PHOTO)
      Lay_ShowErrorAndExit ("Average photo has been computed recently.");

   /***** Get list of students in this degree *****/
   Usr_GetUnorderedStdsCodesInDeg (DegCod);

   for (Sex = (Usr_Sex_t) 0;
	Sex < Usr_NUM_SEXS;
	Sex++)
     {
      TotalTimeToComputeAvgPhotoInMicroseconds = 0;
      for (TypeOfAverage = (Pho_AvgPhotoTypeOfAverage_t) 0;
	   TypeOfAverage < Pho_NUM_AVERAGE_PHOTO_TYPES;
	   TypeOfAverage++)
        {
         /***** Compute average photos of students belonging this degree *****/
         Pho_ComputeAveragePhoto (DegCod,Sex,&Gbl.Usrs.LstStds,
                                  TypeOfAverage,DirAvgPhotosRelPath[TypeOfAverage],
                                  &NumStds,&NumStdsWithPhoto,&PartialTimeToComputeAvgPhotoInMicroseconds);
         TotalTimeToComputeAvgPhotoInMicroseconds += PartialTimeToComputeAvgPhotoInMicroseconds;
        }

      /***** Store stats in database *****/
      Pho_UpdateDegStats (DegCod,Sex,NumStds,NumStdsWithPhoto,TotalTimeToComputeAvgPhotoInMicroseconds);
     }

   /***** Free memory for students list *****/
   Usr_FreeUsrsList (&Gbl.Usrs.LstStds);

   /***** Show photos *****/
   Pho_ShowOrPrintPhotoDegree (Pho_DEGREES_SEE);
  }

/*****************************************************************************/
/******** Get degree code with average photo least recently updated **********/
/*****************************************************************************/
// Returns -1 when error or when all the degrees have been updated too recently

static long Pho_GetDegWithAvgPhotoLeastRecentlyUpdated (void)
  {
   char Query[1024];
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;
   unsigned long NumRows = 0;
   long DegCod = -1L;

   /***** Delete all the degrees in sta_degrees table not present in degrees table *****/
   Pho_RemoveObsoleteStatDegrees ();

   /***** 1. If any degree is not in table, choose it as least recently updated *****/
   /* Get degrees from database */
   sprintf (Query,"SELECT DegCod FROM degrees"
	          " WHERE DegCod NOT IN (SELECT DISTINCT DegCod FROM sta_degrees)"
	          " LIMIT 1");
   NumRows = DB_QuerySELECT (Query,&mysql_res,"can not get degrees");

   /* If number of rows is 1, then get the degree code */
   if (NumRows == 1)
     {
      /* Get row */
      row = mysql_fetch_row (mysql_res);

      /* Get degree code (row[0]) */
      if ((DegCod = Str_ConvertStrCodToLongCod (row[0])) < 0)
         Lay_ShowErrorAndExit ("Wrong code of degree.");
     }
   else   // NumRows == 0
     {
      /* Free structure that stores the query result */
      DB_FreeMySQLResult (&mysql_res);

      /***** 2. If all the degrees are in table, choose the least recently updated *****/
      /* Get degrees from database */
      sprintf (Query,"SELECT DegCod FROM sta_degrees"
                     " WHERE TimeAvgPhoto<FROM_UNIXTIME(UNIX_TIMESTAMP()-'%lu')"
	             " ORDER BY TimeAvgPhoto LIMIT 1",
               Cfg_MIN_TIME_TO_RECOMPUTE_AVG_PHOTO);
      NumRows = DB_QuerySELECT (Query,&mysql_res,"can not get degrees");

      /* If number of rows is 1, then get the degree code */
      if (NumRows == 1)
        {
         /* Get row */
        row = mysql_fetch_row (mysql_res);

         /* Get degree code (row[0]) */
         if ((DegCod = Str_ConvertStrCodToLongCod (row[0])) < 0)
            Lay_ShowErrorAndExit ("Wrong code of degree.");
        }
     }

   /***** Free structure that stores the query result *****/
   DB_FreeMySQLResult (&mysql_res);

   return DegCod;
  }

/*****************************************************************************/
/* Delete all the degrees in sta_degrees table not present in degrees table **/
/*****************************************************************************/

void Pho_RemoveObsoleteStatDegrees (void)
  {
   char Query[512];

   sprintf (Query,"DELETE FROM sta_degrees"
                  " WHERE DegCod NOT IN (SELECT DegCod FROM degrees)");
   DB_QueryDELETE (Query,"can not remove old degrees from stats");
  }

/*****************************************************************************/
/***************** Get time when average photo was computed ******************/
/*****************************************************************************/

static long Pho_GetTimeAvgPhotoWasComputed (long DegCod)
  {
   char Query[256];
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;
   unsigned long NumRows;
   long TimeAvgPhotoWasComputed = 0L;

   /***** Get last time an average photo was computed from database *****/
   sprintf (Query,"SELECT MIN(UNIX_TIMESTAMP(TimeAvgPhoto))"
                  " FROM sta_degrees WHERE DegCod='%ld'",
            DegCod);
   NumRows = DB_QuerySELECT (Query,&mysql_res,"can not get last time an average photo was computed");

   if (NumRows == 1)
     {
      /***** Get row *****/
      row = mysql_fetch_row (mysql_res);

      /***** Get time when average photo was computed (row[0]) *****/
      if (row[0])
         sscanf (row[0],"%ld",&TimeAvgPhotoWasComputed);
     }

   /***** Free structure that stores the query result *****/
   DB_FreeMySQLResult (&mysql_res);

   return TimeAvgPhotoWasComputed;
  }

/*****************************************************************************/
/********************* Get time to compute average photo *********************/
/*****************************************************************************/

static long Pho_GetTimeToComputeAvgPhoto (long DegCod)
  {
   char Query[256];
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;
   unsigned long NumRows;
   Usr_Sex_t Sex;
   long PartialTimeToComputeAvgPhoto;
   long TotalTimeToComputeAvgPhoto = -1L;

   /***** Get time to compute average photo from database *****/
   sprintf (Query,"SELECT TimeToComputeAvgPhoto FROM sta_degrees"
                  " WHERE DegCod='%ld'",DegCod);
   NumRows = DB_QuerySELECT (Query,&mysql_res,"can not get time to compute average photo");

   /***** Count number of rows in result *****/
   if (NumRows == Usr_NUM_SEXS)
     {
      TotalTimeToComputeAvgPhoto = 0;
      for (Sex = (Usr_Sex_t) 0;
	   Sex < Usr_NUM_SEXS;
	   Sex++)
        {
         /***** Get row *****/
         row = mysql_fetch_row (mysql_res);

         /***** Get time to compute average photo (row[0]) *****/
         PartialTimeToComputeAvgPhoto = -1L;
         if (row[0][0])
            if (sscanf (row[0],"%ld",&PartialTimeToComputeAvgPhoto) != 1)
               PartialTimeToComputeAvgPhoto = -1L;
         if (PartialTimeToComputeAvgPhoto < 0)
           {
            TotalTimeToComputeAvgPhoto = -1L;
            break;
           }
         TotalTimeToComputeAvgPhoto += PartialTimeToComputeAvgPhoto;
        }
     }

   /***** Free structure that stores the query result *****/
   DB_FreeMySQLResult (&mysql_res);

   return TotalTimeToComputeAvgPhoto;
  }

/*****************************************************************************/
/********************** Compute the average photo of a list ******************/
/*****************************************************************************/
// Returns number of users in list with photo

static void Pho_ComputeAveragePhoto (long DegCod,Usr_Sex_t Sex,struct ListUsers *LstUsrs,
                                     Pho_AvgPhotoTypeOfAverage_t TypeOfAverage,const char *DirAvgPhotosRelPath,
                                     unsigned *NumStds,unsigned *NumStdsWithPhoto,long *TimeToComputeAvgPhotoInMicroseconds)
  {
   extern const char *Usr_StringsSexDB[Usr_NUM_SEXS];
   unsigned NumUsr;
   char PathPrivRelPhoto[PATH_MAX+1];	// Relative path to private photo, to calculate average face
   char PathRelAvgPhoto[PATH_MAX+1];
   char FileNamePhotoNames[PATH_MAX+1];
   FILE *FilePhotoNames = NULL;	// Initialized to avoid warning
   char StrCallToProgram[256];	// Call to photo processing program
   int ReturnCode;
   /* To compute execution time of this function */
   struct timeval tvStartComputingStat;
   struct timeval tvEndComputingStat;
   struct timezone tz;

   /***** Set start time to compute the stats of this degree *****/
   gettimeofday (&tvStartComputingStat,&tz);

   /***** Reset number of students in this degree *****/
   *NumStds = *NumStdsWithPhoto = 0;

   /***** Build name for file with average photo *****/
   sprintf (PathRelAvgPhoto,"%s/%ld_%s.jpg",
            DirAvgPhotosRelPath,DegCod,Usr_StringsSexDB[Sex]);

   /***** Remove old file if exists *****/
   if (Fil_CheckIfPathExists (PathRelAvgPhoto))  // If file exists
      unlink (PathRelAvgPhoto);

   if (LstUsrs->NumUsrs >= Cfg_MIN_PHOTOS_TO_COMPUTE_AVERAGE)
     {
      /***** Build names for text file with photo paths *****/
      sprintf (FileNamePhotoNames,"%s/%s/%s/%ld.txt",
               Cfg_PATH_SWAD_PRIVATE,Cfg_FOLDER_PHOTO,Cfg_FOLDER_PHOTO_TMP,DegCod);
      if ((FilePhotoNames = fopen (FileNamePhotoNames,"wb")) == NULL)
         Lay_ShowErrorAndExit ("Can not open file to compute average photo.");

      /***** Loop writing file names in text file *****/
      for (NumUsr = 0;
	   NumUsr < LstUsrs->NumUsrs;
	   NumUsr++)
        {
         Gbl.Usrs.Other.UsrDat.Sex = LstUsrs->Lst[NumUsr].Sex;
         if (Sex == Usr_SEX_ALL || Sex == Gbl.Usrs.Other.UsrDat.Sex)
           {
            (*NumStds)++;

            /***** Add photo to file for average face calculation *****/
            Gbl.Usrs.Other.UsrDat.UsrCod = LstUsrs->Lst[NumUsr].UsrCod;
            if (Pho_CheckIfPrivPhotoExists (Gbl.Usrs.Other.UsrDat.UsrCod,PathPrivRelPhoto))
              {
               (*NumStdsWithPhoto)++;
               fprintf (FilePhotoNames,"%s\n",PathPrivRelPhoto);
              }
           }
        }
      fclose (FilePhotoNames);

      /***** Call to program to calculate average photo *****/
      if (*NumStdsWithPhoto >= Cfg_MIN_PHOTOS_TO_COMPUTE_AVERAGE)
        {
         sprintf (StrCallToProgram,"%s %s %s",Pho_StrAvgPhotoPrograms[TypeOfAverage],FileNamePhotoNames,PathRelAvgPhoto);
         ReturnCode = system (StrCallToProgram);
         if (ReturnCode == -1)
            Lay_ShowErrorAndExit ("Error when running program that computes the average photo.");

         /* Write message depending on the return code */
         if (WEXITSTATUS(ReturnCode))
            Lay_ShowErrorAndExit ("The average photo has not been computed successfully.");
        }
     }

   /***** Time used to compute the stats of this degree *****/
   if (gettimeofday (&tvEndComputingStat, &tz))
      // Error in gettimeofday
      *TimeToComputeAvgPhotoInMicroseconds = -1L;
   else
     {
      if (tvEndComputingStat.tv_usec < tvStartComputingStat.tv_usec)
	{
	 tvEndComputingStat.tv_sec--;
	 tvEndComputingStat.tv_usec += 1000000L;
	}
      *TimeToComputeAvgPhotoInMicroseconds = (tvEndComputingStat.tv_sec - tvStartComputingStat.tv_sec) * 1000000L + tvEndComputingStat.tv_usec - tvStartComputingStat.tv_usec;
     }
  }

/*****************************************************************************/
/*** Show class photo with average photos of all students from each degree ***/
/*****************************************************************************/

void Pho_ShowPhotoDegree (void)
  {
   /***** Get type of average *****/
   Gbl.Stat.DegPhotos.TypeOfAverage = Pho_GetPhotoAvgTypeFromForm ();

   Pho_ShowOrPrintPhotoDegree (Pho_DEGREES_SEE);
  }

/*****************************************************************************/
/** Print class photo with average photos of all students from each degree ***/
/*****************************************************************************/

void Pho_PrintPhotoDegree (void)
  {
   /***** Get type of average *****/
   Gbl.Stat.DegPhotos.TypeOfAverage = Pho_GetPhotoAvgTypeFromForm ();

   Pho_ShowOrPrintPhotoDegree (Pho_DEGREES_PRINT);
  }

/*****************************************************************************/
/*** Show class photo with average photos of all students from each degree ***/
/*****************************************************************************/

void Pho_ShowOrPrintPhotoDegree (Pho_AvgPhotoSeeOrPrint_t SeeOrPrint)
  {
   /***** Get photo size from form *****/
   Gbl.Stat.DegPhotos.HowComputePhotoSize = Pho_GetHowComputePhotoSizeFromForm ();

   /***** Get how to order degrees from form *****/
   Gbl.Stat.DegPhotos.HowOrderDegrees = Pho_GetHowOrderDegreesFromForm ();

   /***** Get and update type of list,
          number of columns in class photo
          and preference about view photos *****/
   Usr_GetAndUpdatePrefsAboutUsrList ();

   if (SeeOrPrint == Pho_DEGREES_SEE)
     {
      fprintf (Gbl.F.Out,"<table class=\"CELLS_PAD_2\""
	                 " style=\"margin:0 auto;\">");

      /***** Put a selector for the type of average *****/
      Pho_PutSelectorForTypeOfAvg ();

      /***** Put a selector for the size of photos *****/
      Pho_PutSelectorForHowComputePhotoSize ();

      /***** Put a selector for the order of degrees *****/
      Pho_PutSelectorForHowOrderDegrees ();

      fprintf (Gbl.F.Out,"</table>");

      /***** Link to print view *****/
      Pho_PutLinkToPrintViewOfDegreeStats ();

      /***** Link to computation of average photos *****/
      Pho_PutLinkToCalculateDegreeStats ();
     }

   /***** Get maximum number of students and maximum number of students with photo in all degrees *****/
   Pho_GetMaxStdsPerDegree ();

   /***** Draw the classphoto/list *****/
   switch (Gbl.Usrs.Me.ListType)
     {
      case Usr_CLASS_PHOTO:
         Pho_ShowOrPrintClassPhotoDegrees (SeeOrPrint);
         break;
      case Usr_LIST:
         Pho_ShowOrPrintListDegrees (SeeOrPrint);
         break;
     }
  }

/*****************************************************************************/
/******************* Put a selector for the type of average ******************/
/*****************************************************************************/

static void Pho_PutSelectorForTypeOfAvg (void)
  {
   extern const char *The_ClassForm[The_NUM_THEMES];
   extern const char *Txt_Average_type;
   extern const char *Txt_AVERAGE_PHOTO_TYPES[Pho_NUM_AVERAGE_PHOTO_TYPES];
   Pho_AvgPhotoTypeOfAverage_t TypeOfAvg;

   fprintf (Gbl.F.Out,"<tr>"
	              "<td class=\"%s RIGHT_MIDDLE\">"
	              "%s:"
	              "</td>"
	              "<td class=\"LEFT_MIDDLE\">",
	    The_ClassForm[Gbl.Prefs.Theme],Txt_Average_type);
   Act_FormStart (ActSeePhoDeg);
   Pho_PutHiddenParamPhotoSize ();
   Pho_PutHiddenParamOrderDegrees ();
   Usr_PutParamColsClassPhoto ();
   fprintf (Gbl.F.Out,"<select name=\"AvgType\""
                      " onchange=\"document.getElementById('%s').submit();\">",
            Gbl.Form.Id);
   for (TypeOfAvg = (Pho_AvgPhotoTypeOfAverage_t) 0;
	TypeOfAvg < Pho_NUM_AVERAGE_PHOTO_TYPES;
	TypeOfAvg++)
     {
      fprintf (Gbl.F.Out,"<option value=\"%u\"",(unsigned) TypeOfAvg);
      if (TypeOfAvg == Gbl.Stat.DegPhotos.TypeOfAverage)
         fprintf (Gbl.F.Out," selected=\"selected\"");
      fprintf (Gbl.F.Out,">%s</option>",Txt_AVERAGE_PHOTO_TYPES[TypeOfAvg]);
     }
   fprintf (Gbl.F.Out,"</select>");
   Act_FormEnd ();
   fprintf (Gbl.F.Out,"</td>"
	              "</tr>");
  }

/*****************************************************************************/
/**************** Put hidden parameter for the type of average ***************/
/*****************************************************************************/

void Pho_PutHiddenParamTypeOfAvg (void)
  {
   Par_PutHiddenParamUnsigned ("AvgType",(unsigned) Gbl.Stat.DegPhotos.TypeOfAverage);
  }

/*****************************************************************************/
/*********************** Get type of average for photos **********************/
/*****************************************************************************/

static Pho_AvgPhotoTypeOfAverage_t Pho_GetPhotoAvgTypeFromForm (void)
  {
   char UnsignedStr[10+1];
   unsigned UnsignedNum;

   Par_GetParToText ("AvgType",UnsignedStr,10);
   if (UnsignedStr[0])
     {
      if (sscanf (UnsignedStr,"%u",&UnsignedNum) != 1)
         Lay_ShowErrorAndExit ("Type of average is missing.");
      if (UnsignedNum >= Pho_NUM_AVERAGE_PHOTO_TYPES)
         Lay_ShowErrorAndExit ("Type of average is missing.");
      return (Pho_AvgPhotoTypeOfAverage_t) UnsignedNum;
     }
   return (Pho_AvgPhotoTypeOfAverage_t) 0;
  }

/*****************************************************************************/
/****************** Put a selector for the size of photos ********************/
/*****************************************************************************/

static void Pho_PutSelectorForHowComputePhotoSize (void)
  {
   extern const char *The_ClassForm[The_NUM_THEMES];
   extern const char *Txt_Size_of_photos;
   extern const char *Txt_STAT_DEGREE_PHOTO_SIZE[Pho_NUM_HOW_COMPUTE_PHOTO_SIZES];
   Pho_HowComputePhotoSize_t PhoSi;

   fprintf (Gbl.F.Out,"<tr>"
	              "<td class=\"%s RIGHT_MIDDLE\">"
	              "%s:"
	              "</td>"
	              "<td class=\"LEFT_MIDDLE\">",
	    The_ClassForm[Gbl.Prefs.Theme],Txt_Size_of_photos);
   Act_FormStart (ActSeePhoDeg);
   Pho_PutHiddenParamTypeOfAvg ();
   Pho_PutHiddenParamOrderDegrees ();
   Usr_PutParamColsClassPhoto ();
   fprintf (Gbl.F.Out,"<select name=\"PhotoSize\""
                      " onchange=\"document.getElementById('%s').submit();\">",
            Gbl.Form.Id);
   for (PhoSi = (Pho_HowComputePhotoSize_t) 0;
	PhoSi < Pho_NUM_HOW_COMPUTE_PHOTO_SIZES;
	PhoSi++)
     {
      fprintf (Gbl.F.Out,"<option value=\"%u\"",(unsigned) PhoSi);
      if (PhoSi == Gbl.Stat.DegPhotos.HowComputePhotoSize)
         fprintf (Gbl.F.Out," selected=\"selected\"");
      fprintf (Gbl.F.Out,">%s</option>",Txt_STAT_DEGREE_PHOTO_SIZE[PhoSi]);
     }
   fprintf (Gbl.F.Out,"</select>");
   Act_FormEnd ();
   fprintf (Gbl.F.Out,"</td>"
	              "</tr>");
  }

/*****************************************************************************/
/**************** Put hidden parameter for the size of photos ****************/
/*****************************************************************************/

void Pho_PutHiddenParamPhotoSize (void)
  {
   Par_PutHiddenParamUnsigned ("PhotoSize",(unsigned) Gbl.Stat.DegPhotos.HowComputePhotoSize);
  }

/*****************************************************************************/
/********************* Get how to compute size of photos *********************/
/*****************************************************************************/

static Pho_HowComputePhotoSize_t Pho_GetHowComputePhotoSizeFromForm (void)
  {
   char UnsignedStr[10+1];
   unsigned UnsignedNum;

   Par_GetParToText ("PhotoSize",UnsignedStr,10);
   if (UnsignedStr[0])
     {
      if (sscanf (UnsignedStr,"%u",&UnsignedNum) != 1)
         Lay_ShowErrorAndExit ("Size of photo is missing.");
      if (UnsignedNum >= Pho_NUM_HOW_COMPUTE_PHOTO_SIZES)
         Lay_ShowErrorAndExit ("Size of photo is missing.");
      return (Pho_HowComputePhotoSize_t) UnsignedNum;
     }
   return (Pho_HowComputePhotoSize_t) 0;
  }

/*****************************************************************************/
/****************** Put a selector for the order of degrees ******************/
/*****************************************************************************/

static void Pho_PutSelectorForHowOrderDegrees (void)
  {
   extern const char *The_ClassForm[The_NUM_THEMES];
   extern const char *Txt_Sort_degrees_by;
   extern const char *Txt_STAT_DEGREE_PHOTO_ORDER[Pho_NUM_HOW_ORDER_DEGREES];
   Pho_HowOrderDegrees_t Order;

   fprintf (Gbl.F.Out,"<tr>"
	              "<td class=\"%s RIGHT_MIDDLE\">"
	              "%s:"
	              "</td>"
	              "<td class=\"LEFT_MIDDLE\">",
	    The_ClassForm[Gbl.Prefs.Theme],Txt_Sort_degrees_by);
   Act_FormStart (ActSeePhoDeg);
   Pho_PutHiddenParamTypeOfAvg ();
   Pho_PutHiddenParamPhotoSize ();
   Usr_PutParamColsClassPhoto ();
   fprintf (Gbl.F.Out,"<select name=\"OrdDeg\""
                      " onchange=\"document.getElementById('%s').submit();\">",
            Gbl.Form.Id);
   for (Order = (Pho_HowOrderDegrees_t) 0;
	Order < Pho_NUM_HOW_ORDER_DEGREES;
	Order++)
     {
      fprintf (Gbl.F.Out,"<option value=\"%u\"",(unsigned) Order);
      if (Order == Gbl.Stat.DegPhotos.HowOrderDegrees)
         fprintf (Gbl.F.Out," selected=\"selected\"");
      fprintf (Gbl.F.Out,">%s</option>",Txt_STAT_DEGREE_PHOTO_ORDER[Order]);
     }
   fprintf (Gbl.F.Out,"</select>");
   Act_FormEnd ();
   fprintf (Gbl.F.Out,"</td>"
	              "</tr>");
  }

/*****************************************************************************/
/**************** Put hidden parameter for the order of degrees **************/
/*****************************************************************************/

void Pho_PutHiddenParamOrderDegrees (void)
  {
   Par_PutHiddenParamUnsigned ("OrdDeg",(unsigned) Gbl.Stat.DegPhotos.HowOrderDegrees);
  }

/*****************************************************************************/
/*************************** Get how to order degrees ************************/
/*****************************************************************************/

static Pho_HowOrderDegrees_t Pho_GetHowOrderDegreesFromForm (void)
  {
   char UnsignedStr[10+1];
   unsigned UnsignedNum;

   Par_GetParToText ("OrdDeg",UnsignedStr,10);
   if (UnsignedStr[0])
     {
      if (sscanf (UnsignedStr,"%u",&UnsignedNum) != 1)
         Lay_ShowErrorAndExit ("Sort order is missing.");
      if (UnsignedNum >= Pho_NUM_HOW_ORDER_DEGREES)
         Lay_ShowErrorAndExit ("Sort order is missing.");
      return (Pho_HowOrderDegrees_t) UnsignedNum;
     }
   return (Pho_HowOrderDegrees_t) 0;
  }

/*****************************************************************************/
/*************** Put a link to print view the stats of degrees ***************/
/*****************************************************************************/

static void Pho_PutLinkToPrintViewOfDegreeStats (void)
  {
   extern const char *Txt_Print;

   fprintf (Gbl.F.Out,"<div class=\"CONTEXT_MENU\">");
   Lay_PutContextualLink (ActPrnPhoDeg,Pho_PutLinkToPrintViewOfDegreeStatsParams,
                          "print64x64.png",
                          Txt_Print,Txt_Print);
   fprintf (Gbl.F.Out,"</div>");
  }

static void Pho_PutLinkToPrintViewOfDegreeStatsParams (void)
  {
   Pho_PutHiddenParamTypeOfAvg ();
   Pho_PutHiddenParamPhotoSize ();
   Pho_PutHiddenParamOrderDegrees ();
   Usr_PutParamUsrListType (Gbl.Usrs.Me.ListType);
   Usr_PutParamColsClassPhoto ();
   Usr_PutParamListWithPhotos ();
  }

/*****************************************************************************/
/*************** Put a link to calculate the stats of degrees ****************/
/*****************************************************************************/

static void Pho_PutLinkToCalculateDegreeStats (void)
  {
   extern const char *The_ClassFormBold[The_NUM_THEMES];
   extern const char *Txt_Calculate_average_photo_of_a_degree;
   extern const char *Txt_Calculate_average_photo_of_THE_DEGREE_X;
   extern const char *Txt_unknown_TIME;
   extern const char *Txt_time;
   unsigned NumDeg;
   struct Degree Deg;
   long EstimatedTimeToComputeAvgPhotoInMicroseconds;
   char StrEstimatedTimeToComputeAvgPhoto[64];

   if ((Deg.DegCod = Pho_GetDegWithAvgPhotoLeastRecentlyUpdated ()) > 0)
     {
      /***** Get list of all the degrees *****/
      Deg_GetListAllDegs ();

      /***** Get data of the degree from database *****/
      Deg_GetDataOfDegreeByCod (&Deg);

      /***** Start div *****/
      fprintf (Gbl.F.Out,"<div class=\"CONTEXT_MENU\">");

      /***** Start form *****/
      Act_FormStart (ActCalPhoDeg);
      Pho_PutHiddenParamTypeOfAvg ();
      Pho_PutHiddenParamPhotoSize ();
      Pho_PutHiddenParamOrderDegrees ();
      Usr_PutParamUsrListType (Gbl.Usrs.Me.ListType);
      Usr_PutParamColsClassPhoto ();
      Usr_PutParamListWithPhotos ();
      Act_LinkFormSubmitAnimated (Txt_Calculate_average_photo_of_a_degree,The_ClassFormBold[Gbl.Prefs.Theme]);
      Lay_PutCalculateIconWithText (Txt_Calculate_average_photo_of_a_degree,Txt_Calculate_average_photo_of_THE_DEGREE_X);

      /***** Put selector with all the degrees *****/
      fprintf (Gbl.F.Out,"<select name=\"OthDegCod\">");
      for (NumDeg = 0;
	   NumDeg < Gbl.Degs.AllDegs.Num;
	   NumDeg++)
        {
         /* Get time to compute average photo of this degree */
         EstimatedTimeToComputeAvgPhotoInMicroseconds = Pho_GetTimeToComputeAvgPhoto (Gbl.Degs.AllDegs.Lst[NumDeg].DegCod);
         if (EstimatedTimeToComputeAvgPhotoInMicroseconds == -1L)
            strcpy (StrEstimatedTimeToComputeAvgPhoto,Txt_unknown_TIME);
         else
            Sta_WriteTime (StrEstimatedTimeToComputeAvgPhoto,EstimatedTimeToComputeAvgPhotoInMicroseconds);

         fprintf (Gbl.F.Out,"<option value=\"%ld\"%s>%s (%s: %s)</option>",
                  Gbl.Degs.AllDegs.Lst[NumDeg].DegCod,
                  Gbl.Degs.AllDegs.Lst[NumDeg].DegCod == Deg.DegCod ? " selected=\"selected\"" :
                                                                      ((Pho_GetTimeAvgPhotoWasComputed (Gbl.Degs.AllDegs.Lst[NumDeg].DegCod) >=
                                                                	Gbl.StartExecutionTimeUTC - Cfg_MIN_TIME_TO_RECOMPUTE_AVG_PHOTO) ? " disabled=\"disabled\"" :
                                                                	                                                                ""),
                  Gbl.Degs.AllDegs.Lst[NumDeg].ShortName,
                  Txt_time,
                  StrEstimatedTimeToComputeAvgPhoto);
        }

      /***** End selector, form and div *****/
      fprintf (Gbl.F.Out,"</select>");
      Act_FormEnd ();
      fprintf (Gbl.F.Out,"</div>");

      /***** Free list of all the degrees *****/
      Deg_FreeListAllDegs ();
     }
  }

/*****************************************************************************/
/*** Get number of students and number of students with photo in a degree ****/
/*****************************************************************************/

static void Pho_GetMaxStdsPerDegree (void)
  {
   char Query[512];
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;
   unsigned long NumRows;

   /***** Get maximum number of students in a degree from database *****/
   sprintf (Query,"SELECT MAX(NumStds),MAX(NumStdsWithPhoto),MAX(NumStdsWithPhoto/NumStds)"
                  " FROM sta_degrees WHERE Sex='all' AND NumStds>'0'");
   NumRows = DB_QuerySELECT (Query,&mysql_res,"can not get maximum number of students in a degree");

   /***** Count number of rows in result *****/
   if (NumRows == 1)
     {
      row = mysql_fetch_row (mysql_res);

      if (row[0] == NULL)
	 Gbl.Stat.DegPhotos.MaxStds = -1;
      else if (sscanf (row[0],"%d",&Gbl.Stat.DegPhotos.MaxStds) != 1)
	 Gbl.Stat.DegPhotos.MaxStds = -1;

      if (row[1] == NULL)
	 Gbl.Stat.DegPhotos.MaxStdsWithPhoto = -1;
      else if (sscanf (row[1],"%d",&Gbl.Stat.DegPhotos.MaxStdsWithPhoto) != 1)
	 Gbl.Stat.DegPhotos.MaxStdsWithPhoto = -1;

      if (row[2] == NULL)
	 Gbl.Stat.DegPhotos.MaxPercent = -1.0;
      else if (sscanf (row[2],"%f",&Gbl.Stat.DegPhotos.MaxPercent) != 1)
	 Gbl.Stat.DegPhotos.MaxPercent = -1.0;

      /***** Free structure that stores the query result *****/
      DB_FreeMySQLResult (&mysql_res);
     }
   else
     {
      Gbl.Stat.DegPhotos.MaxStds = Gbl.Stat.DegPhotos.MaxStdsWithPhoto = -1;
      Gbl.Stat.DegPhotos.MaxPercent = -1.0;
     }
  }

/*****************************************************************************/
/************ Show or print the stats of degrees as class photo **************/
/*****************************************************************************/

static void Pho_ShowOrPrintClassPhotoDegrees (Pho_AvgPhotoSeeOrPrint_t SeeOrPrint)
  {
   extern const char *Txt_Degrees;
   extern const char *Txt_No_users_found[Rol_NUM_ROLES];
   char Query[512];
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;
   unsigned long NumRow;
   unsigned long NumRows;
   struct Degree Deg;
   unsigned NumDegsNotEmpty;
   int NumStds;
   int NumStdsWithPhoto;
   bool TRIsOpen = false;

   /***** Get degrees from database *****/
   Pho_BuildQueryOfDegrees (Query);
   NumRows = DB_QuerySELECT (Query,&mysql_res,"can not get degrees");

   if (NumRows)	// Degrees with students found
     {
      /***** Start frame *****/
      Lay_StartRoundFrame (NULL,Txt_Degrees,NULL);

      /***** Form to select type of list used to display degree photos *****/
      if (SeeOrPrint == Pho_DEGREES_SEE)
	 Usr_ShowFormsToSelectUsrListType (ActSeePhoDeg);
      fprintf (Gbl.F.Out,"<table style=\"margin:0 auto;\">");

      /***** Get and print degrees *****/
      for (NumRow = 0, NumDegsNotEmpty = 0;
	   NumRow < NumRows;
	   NumRow++)
	{
	 /***** Get next degree *****/
	 row = mysql_fetch_row (mysql_res);

	 /* Get degree code (row[0]) */
	 if ((Deg.DegCod = Str_ConvertStrCodToLongCod (row[0])) < 0)
	    Lay_ShowErrorAndExit ("Wrong code of degree.");

	 /* Get data of degree */
	 Deg_GetDataOfDegreeByCod (&Deg);

	 /* Get number of students and number of students with photo in this degree */
	 Pho_GetNumStdsInDegree (Deg.DegCod,Usr_SEX_ALL,&NumStds,&NumStdsWithPhoto);

	 if (NumStds > 0)
	   {
	    if ((NumDegsNotEmpty % Gbl.Usrs.ClassPhoto.Cols) == 0)
	      {
	       fprintf (Gbl.F.Out,"<tr>");
	       TRIsOpen = true;
	      }

	    /***** Show average photo of students belonging to this degree *****/
	    fprintf (Gbl.F.Out,"<td class=\"CLASSPHOTO CENTER_MIDDLE\">");
	    Pho_ShowDegreeAvgPhotoAndStat (&Deg,SeeOrPrint,Usr_SEX_ALL,NumStds,NumStdsWithPhoto);
	    fprintf (Gbl.F.Out,"</td>");

	    if ((++NumDegsNotEmpty % Gbl.Usrs.ClassPhoto.Cols) == 0)
	      {
	       fprintf (Gbl.F.Out,"</tr>");
	       TRIsOpen = false;
	      }
	   }
	}
      if (TRIsOpen)
	 fprintf (Gbl.F.Out,"</tr>");

      fprintf (Gbl.F.Out,"</table>");

      /***** End frame *****/
      Lay_EndRoundFrame ();
     }
   else	// No degrees with students found
      Lay_ShowAlert (Lay_INFO,Txt_No_users_found[Rol_STUDENT]);

   /***** Free structure that stores the query result *****/
   DB_FreeMySQLResult (&mysql_res);

  }

/*****************************************************************************/
/**************** Show or print the stats of degrees as list *****************/
/*****************************************************************************/

static void Pho_ShowOrPrintListDegrees (Pho_AvgPhotoSeeOrPrint_t SeeOrPrint)
  {
   extern const char *Txt_Degrees;
   extern const char *Txt_No_INDEX;
   extern const char *Txt_Degree;
   extern const char *Txt_SEX_PLURAL_Abc[Usr_NUM_SEXS];
   extern const char *Txt_No_users_found[Rol_NUM_ROLES];
   char Query[512];
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;
   unsigned long NumRow;
   unsigned long NumRows;
   unsigned NumDegsNotEmpty;
   int NumStds;
   int NumStdsWithPhoto;
   struct Degree Deg;
   Usr_Sex_t Sex;

   /***** Get degrees from database *****/
   Pho_BuildQueryOfDegrees (Query);
   NumRows = DB_QuerySELECT (Query,&mysql_res,"can not get degrees");

   if (NumRows)	// Degrees with students found
     {
      /***** Class photo start *****/
      if (SeeOrPrint == Pho_DEGREES_SEE)
	{
	 Lay_StartRoundFrame (NULL,Txt_Degrees,NULL);

	 /***** Form to select type of list used to display degree photos *****/
	 Usr_ShowFormsToSelectUsrListType (ActSeePhoDeg);
	}

      /***** Write heading *****/
      fprintf (Gbl.F.Out,"<table class=\"CELLS_PAD_2\""
	                 " style=\"margin:0 auto;\">"
			 "<tr>"
			 "<th class=\"RIGHT_TOP\">"
			 "%s"
			 "</th>"
			 "<th class=\"CENTER_TOP\">"
			 "%s"
			 "</th>",
	       Txt_No_INDEX,
	       Txt_Degree);
      for (Sex = (Usr_Sex_t) 0;
	   Sex < Usr_NUM_SEXS;
	   Sex++)
	 fprintf (Gbl.F.Out,"<th class=\"CENTER_TOP\">"
			    "%s"
			    "</th>",
		  Txt_SEX_PLURAL_Abc[Sex]);
      fprintf (Gbl.F.Out,"</tr>");

      /***** Get degrees *****/
      for (NumRow = 0, Gbl.RowEvenOdd = 0, NumDegsNotEmpty = 0;
	   NumRow < NumRows;
	   NumRow++, Gbl.RowEvenOdd = 1 - Gbl.RowEvenOdd)
	{
	 /***** Get next degree *****/
	 row = mysql_fetch_row (mysql_res);

	 /* Get degree code (row[0]) */
	 if ((Deg.DegCod = Str_ConvertStrCodToLongCod (row[0])) < 0)
	    Lay_ShowErrorAndExit ("Wrong code of degree.");

	 /* Get data of degree */
	 Deg_GetDataOfDegreeByCod (&Deg);

	 /***** Show logo and name of this degree *****/
	 fprintf (Gbl.F.Out,"<tr>"
			    "<td class=\"DAT RIGHT_MIDDLE COLOR%u\">"
			    "%u"
			    "</td>",
		  Gbl.RowEvenOdd,++NumDegsNotEmpty);

	 /***** Show logo and name of this degree *****/
	 fprintf (Gbl.F.Out,"<td class=\"DAT LEFT_MIDDLE COLOR%u\">",
		  Gbl.RowEvenOdd);
	 if (SeeOrPrint == Pho_DEGREES_SEE)
	    Deg_DrawDegreeLogoAndNameWithLink (&Deg,ActSeeDegInf,
					       "DAT","CENTER_TOP");
	 else	// Pho_DEGREES_PRINT
	   {
	    Log_DrawLogo (Sco_SCOPE_DEG,Deg.DegCod,Deg.ShortName,20,"CENTER_TOP",true);
	    fprintf (Gbl.F.Out,"&nbsp;%s</a>",Deg.FullName);
	   }
	 fprintf (Gbl.F.Out,"</td>");

	 for (Sex = (Usr_Sex_t) 0;
	      Sex < Usr_NUM_SEXS;
	      Sex++)
	   {
	    /***** Show average photo of students belonging to this degree *****/
	    Pho_GetNumStdsInDegree (Deg.DegCod,Sex,&NumStds,&NumStdsWithPhoto);
	    fprintf (Gbl.F.Out,"<td class=\"CENTER_MIDDLE COLOR%u\">",
		     Gbl.RowEvenOdd);
	    if (Gbl.Usrs.Listing.WithPhotos)
	       Pho_ShowDegreeAvgPhotoAndStat (&Deg,SeeOrPrint,Sex,NumStds,NumStdsWithPhoto);
	    else
	       Pho_ShowDegreeStat (NumStds,NumStdsWithPhoto);
	    fprintf (Gbl.F.Out,"</td>");
	   }
	 fprintf (Gbl.F.Out,"</tr>");
	}

      /***** Photos end *****/
      fprintf (Gbl.F.Out,"</table>");
      if (SeeOrPrint == Pho_DEGREES_SEE)
	 Lay_EndRoundFrame ();
     }
   else	// No degrees with students found!
      Lay_ShowAlert (Lay_INFO,Txt_No_users_found[Rol_STUDENT]);

   /***** Free structure that stores the query result *****/
   DB_FreeMySQLResult (&mysql_res);
  }

/*****************************************************************************/
/****** Build a query to get the degrees ordered by different criteria *******/
/*****************************************************************************/

static void Pho_BuildQueryOfDegrees (char *Query)
  {
   switch (Gbl.Stat.DegPhotos.HowOrderDegrees)
     {
      case Pho_NUMBER_OF_STUDENTS:
         sprintf (Query,"SELECT degrees.DegCod"
                        " FROM degrees,sta_degrees"
                        " WHERE sta_degrees.Sex='all'"
                        " AND sta_degrees.NumStds>'0'"
                        " AND degrees.DegCod=sta_degrees.DegCod"
                        " ORDER BY sta_degrees.NumStds DESC,sta_degrees.NumStdsWithPhoto DESC,degrees.ShortName");
         break;
      case Pho_NUMBER_OF_PHOTOS:
         sprintf (Query,"SELECT degrees.DegCod"
                        " FROM degrees,sta_degrees"
                        " WHERE sta_degrees.Sex='all'"
                        " AND sta_degrees.NumStds>'0'"
                        " AND degrees.DegCod=sta_degrees.DegCod"
                        " ORDER BY sta_degrees.NumStdsWithPhoto DESC,sta_degrees.NumStds DESC,degrees.ShortName");
         break;
      case Pho_PERCENT:
         sprintf (Query,"SELECT degrees.DegCod"
                        " FROM degrees,sta_degrees"
                        " WHERE sta_degrees.Sex='all'"
                        " AND sta_degrees.NumStds>'0'"
                        " AND degrees.DegCod=sta_degrees.DegCod"
                        " ORDER BY sta_degrees.NumStdsWithPhoto/sta_degrees.NumStds DESC,degrees.ShortName");
         break;
      case Pho_DEGREE_NAME:
         sprintf (Query,"SELECT degrees.DegCod"
                        " FROM degrees,sta_degrees"
                        " WHERE sta_degrees.Sex='all'"
                        " AND sta_degrees.NumStds>'0'"
                        " AND degrees.DegCod=sta_degrees.DegCod"
                        " ORDER BY degrees.ShortName");
         break;
     }
  }

/*****************************************************************************/
/*** Get number of students and number of students with photo in a degree ****/
/*****************************************************************************/

static void Pho_GetNumStdsInDegree (long DegCod,Usr_Sex_t Sex,int *NumStds,int *NumStdsWithPhoto)
  {
   extern const char *Usr_StringsSexDB[Usr_NUM_SEXS];
   char Query[512];
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;
   unsigned long NumRows;

   /***** Get the number of students in a degree from database *****/
   sprintf (Query,"SELECT NumStds,NumStdsWithPhoto FROM sta_degrees"
                  " WHERE DegCod='%ld' AND Sex='%s'",
            DegCod,Usr_StringsSexDB[Sex]);
   NumRows = DB_QuerySELECT (Query,&mysql_res,"can not get the number of students in a degree");

   if (NumRows == 0)
      *NumStds = *NumStdsWithPhoto = -1;
   else
     {
      row = mysql_fetch_row (mysql_res);
      if (sscanf (row[0],"%d",NumStds) != 1)
	 *NumStds = -1;
      if (sscanf (row[1],"%d",NumStdsWithPhoto) != 1)
	 *NumStdsWithPhoto = -1;

      /***** Free structure that stores the query result *****/
      DB_FreeMySQLResult (&mysql_res);
     }
  }

/*****************************************************************************/
/*********************** Update statistics of a degree ***********************/
/*****************************************************************************/

static void Pho_UpdateDegStats (long DegCod,Usr_Sex_t Sex,unsigned NumStds,unsigned NumStdsWithPhoto,long TimeToComputeAvgPhotoInMicroseconds)
  {
   extern const char *Usr_StringsSexDB[Usr_NUM_SEXS];
   char Query[1024];

   sprintf (Query,"REPLACE INTO sta_degrees"
                  " (DegCod,Sex,NumStds,NumStdsWithPhoto,TimeAvgPhoto,TimeToComputeAvgPhoto)"
                  " VALUES ('%ld','%s','%u','%u',NOW(),'%ld')",
	    DegCod,Usr_StringsSexDB[Sex],NumStds,NumStdsWithPhoto,TimeToComputeAvgPhotoInMicroseconds);
   DB_QueryREPLACE (Query,"can not save stats of a degree");
  }

/*****************************************************************************/
/******************* Show the average photo of a degree **********************/
/*****************************************************************************/

static void Pho_ShowDegreeStat (int NumStds,int NumStdsWithPhoto)
  {
   extern const char *Txt_photos;

   fprintf (Gbl.F.Out,"<span class=\"DAT\">%d&nbsp;</span>"
                      "<span class=\"DAT_SMALL\">(%d&nbsp;%s,&nbsp;%d%%)</span>",
            NumStds,NumStdsWithPhoto,Txt_photos,
            NumStds > 0 ? (int) (((NumStdsWithPhoto * 100.0) / NumStds) + 0.5) :
        	          0);
  }

/*****************************************************************************/
/******************* Show the average photo of a degree **********************/
/*****************************************************************************/

static void Pho_ShowDegreeAvgPhotoAndStat (struct Degree *Deg,Pho_AvgPhotoSeeOrPrint_t SeeOrPrint,Usr_Sex_t Sex,int NumStds,int NumStdsWithPhoto)
  {
   extern const char *Usr_StringsSexDB[Usr_NUM_SEXS];
   extern const char *Txt_students_ABBREVIATION;
   extern const char *Txt_SEX_PLURAL_abc[Usr_NUM_SEXS];
   extern const char *Txt_photos;
   unsigned PhotoWidth;
   unsigned PhotoHeight;
   char PathRelAvgPhoto[PATH_MAX+1];
   char PhotoURL[PATH_MAX+1];
   char CopyOfDegShortName[Deg_MAX_LENGTH_DEGREE_SHORT_NAME+1];	// Short name of degree
   char PhotoCaption[512];

   /***** Compute photo width and height to be proportional to number of students *****/
   Pho_ComputePhotoSize (NumStds,NumStdsWithPhoto,&PhotoWidth,&PhotoHeight);

   /***** Make a copy of the degree short name *****/
   strncpy (CopyOfDegShortName,Deg->ShortName,Deg_MAX_LENGTH_DEGREE_SHORT_NAME);
   CopyOfDegShortName[Deg_MAX_LENGTH_DEGREE_SHORT_NAME] = '\0';
   Str_LimitLengthHTMLStr (CopyOfDegShortName,
                           SeeOrPrint == Pho_DEGREES_SEE ? 10 :
                        	                           15);

   if (SeeOrPrint == Pho_DEGREES_SEE)
      fprintf (Gbl.F.Out,"<a href=\"%s\" title=\"%s\" class=\"CLASSPHOTO\" target=\"_blank\">",
               Deg->WWW,Deg->FullName);
   fprintf (Gbl.F.Out,"<img src=\"");

   if (NumStds && NumStdsWithPhoto)
     {
      sprintf (PathRelAvgPhoto,"%s/%s/%s/%ld_%s.jpg",
               Cfg_PATH_SWAD_PUBLIC,Cfg_FOLDER_PHOTO,
               Pho_StrAvgPhotoDirs[Gbl.Stat.DegPhotos.TypeOfAverage],
               Deg->DegCod,Usr_StringsSexDB[Sex]);

      if (Fil_CheckIfPathExists (PathRelAvgPhoto))
        {
         sprintf (PhotoURL,"%s/%s/%s/%ld_%s.jpg",
                  Cfg_HTTPS_URL_SWAD_PUBLIC,Cfg_FOLDER_PHOTO,
                  Pho_StrAvgPhotoDirs[Gbl.Stat.DegPhotos.TypeOfAverage],
                  Deg->DegCod,Usr_StringsSexDB[Sex]);
         fprintf (Gbl.F.Out,"%s\" style=\"width:%upx; height:%upx;\"",
                  PhotoURL,PhotoWidth,PhotoHeight);
         if (SeeOrPrint == Pho_DEGREES_SEE)
           {
            sprintf (PhotoCaption,"%s<br />"
        	                  "%d&nbsp;%s&nbsp;(%s)<br />"
        	                  "%d&nbsp;%s&nbsp;(%d%%)",
                     Deg->ShortName,
                     NumStds,Txt_students_ABBREVIATION,Txt_SEX_PLURAL_abc[Sex],
                     NumStdsWithPhoto,Txt_photos,
                     NumStds > 0 ? (int) (((NumStdsWithPhoto * 100.0) / NumStds) + 0.5) :
                	           0);
            fprintf (Gbl.F.Out," onmouseover=\"zoom(this,'%s','%s')\" onmouseout=\"noZoom();\"",
                     PhotoURL,PhotoCaption);
           }
        }
      else
         fprintf (Gbl.F.Out,"%s/usr_bl.jpg\""
                            " style=\"width:%upx; height:%upx;\"",
                  Gbl.Prefs.IconsURL,PhotoWidth,PhotoHeight);
     }
   else
      fprintf (Gbl.F.Out,"%s/usr_bl.jpg\""
	                 " style=\"width:%upx; height:%upx;\"",
	       Gbl.Prefs.IconsURL,PhotoWidth,PhotoHeight);
   fprintf (Gbl.F.Out," alt=\"%s\" title=\"%s\" />",
            Deg->ShortName,
            Deg->FullName);
   if (SeeOrPrint == Pho_DEGREES_PRINT)
      fprintf (Gbl.F.Out,"<span class=\"CLASSPHOTO\">");
   fprintf (Gbl.F.Out,"<br />%s<br />%d&nbsp;%s<br />%d&nbsp;%s<br />(%d%%)",
            CopyOfDegShortName,
            NumStds,Txt_students_ABBREVIATION,
            NumStdsWithPhoto,Txt_photos,
            NumStds > 0 ? (int) (((NumStdsWithPhoto * 100.0) / NumStds) + 0.5) :
        	          0);
   if (SeeOrPrint == Pho_DEGREES_SEE)
      fprintf (Gbl.F.Out,"</a>");
   else
      fprintf (Gbl.F.Out,"</span>");
  }

/*****************************************************************************/
/******** Compute photo width and height for class photo of degrees **********/
/*****************************************************************************/
#define MIN_WIDTH_PHOTO		10
#define MIN_HEIGHT_PHOTO	15
#define MIN_PIXELS_PHOTO	(MIN_WIDTH_PHOTO * MIN_HEIGHT_PHOTO)
#define DEF_WIDTH_PHOTO		45
#define DEF_HEIGHT_PHOTO	60
#define DEF_PIXELS_PHOTO	(DEF_WIDTH_PHOTO * DEF_HEIGHT_PHOTO)
#define MAX_WIDTH_PHOTO		90
#define MAX_HEIGHT_PHOTO	120
#define MAX_PIXELS_PHOTO	(MAX_WIDTH_PHOTO * MAX_HEIGHT_PHOTO)

static void Pho_ComputePhotoSize (int NumStds,int NumStdsWithPhoto,unsigned *PhotoWidth,unsigned *PhotoHeight)
  {
   unsigned PhotoPixels = DEF_PIXELS_PHOTO;

   switch (Gbl.Stat.DegPhotos.HowComputePhotoSize)
     {
      case Pho_PROPORTIONAL_TO_NUMBER_OF_STUDENTS:
         if (Gbl.Stat.DegPhotos.MaxStds > 0)
            PhotoPixels = (unsigned) (((float) (MAX_PIXELS_PHOTO - MIN_PIXELS_PHOTO) /
        	                       Gbl.Stat.DegPhotos.MaxStds) * NumStds +
        	                      MIN_PIXELS_PHOTO + 0.5);
         break;
      case Pho_PROPORTIONAL_TO_NUMBER_OF_PHOTOS:
         if (Gbl.Stat.DegPhotos.MaxStdsWithPhoto > 0)
            PhotoPixels = (unsigned) (((float) (MAX_PIXELS_PHOTO - MIN_PIXELS_PHOTO) /
        	                       Gbl.Stat.DegPhotos.MaxStdsWithPhoto) * NumStdsWithPhoto +
        	                      MIN_PIXELS_PHOTO + 0.5);
         break;
      case Pho_PROPORTIONAL_TO_PERCENT:
         if (Gbl.Stat.DegPhotos.MaxPercent > 0.0)
           {
            if (NumStds)
               PhotoPixels = (unsigned) (((float) (MAX_PIXELS_PHOTO - MIN_PIXELS_PHOTO) /
        	                          Gbl.Stat.DegPhotos.MaxPercent) *
        	                         ((float) NumStdsWithPhoto / NumStds) +
        	                         MIN_PIXELS_PHOTO + 0.5);
            else
              {
               *PhotoWidth  = MIN_WIDTH_PHOTO;
               *PhotoHeight = MIN_HEIGHT_PHOTO;
               return;
              }
           }
         break;
      case Pho_UNIQUE_SIZE:
         *PhotoWidth  = DEF_WIDTH_PHOTO;
         *PhotoHeight = DEF_HEIGHT_PHOTO;
         return;
     }
   *PhotoWidth  = (unsigned) (sqrt (((double) Pho_PHOTO_REAL_WIDTH /
	                             (double) Pho_PHOTO_REAL_HEIGHT) *
	                            (double) PhotoPixels) + 0.5);
   *PhotoHeight = (unsigned) (((double) Pho_PHOTO_REAL_HEIGHT /
	                       (double) Pho_PHOTO_REAL_WIDTH) *
	                      (double) *PhotoWidth + 0.5);
  }
