/********************************************************************************

   Fotoxx      edit photos and manage collections

   Copyright 2007-2016 Michael Cornelison
   Source URL: http://kornelix.net
   Contact: kornelix@posteo.de

   This program is free software: you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation, 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 General Public License for more details.

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

*********************************************************************************

   Fotoxx image editor - image metadata functions.

   View and edit metadata
   ----------------------
   m_meta_view_short          metadata short report
   m_meta_view_long           report all metadata
   m_captions                 write captions and comments at top of current image
   m_edit_metadata            primary edit metadata dialog
   manage_tags                maintain list of defined tags
   m_meta_edit_any            dialog to fetch and save any image file metadata by name
   m_meta_delete              dialog to delete any image file metadata by name
   m_batch_tags               batch add and delete tags for selected image files
   m_batch_rename_tags        convert tag names for all image files using a from-to list
   m_batch_change_metadata    add/change or delete metadata for selected image files
   m_batch_report_metadata    batch metadata report to text file
   pdate_metadate             convert yyyy-mm-dd to yyyymmdd
   ptime_metatime             convert hh:mm:ss to hhmmss
   metadate_pdate             convert yyyymmddhhmmss to yyyy-mm-dd and hh:mm:ss
   datetimeOK                 validate yyyymmddhhmmss date/time
   add_tag                    add tag to a tag list
   del_tag                    remove tag from a tag list
   add_recentag               add tag to recent tags list, remove oldest if needed
   load_deftags               load defined tags list from tags file and image index
   save_deftags               save defined tags list to tags file
   find_deftag                check if given tag is in defined tags list
   add_deftag                 add new tag to defined tags list or change category
   del_deftag                 remove tag from defined tags list
   deftags_stuff              stuff defined tags into dialog text widget
   defcats_stuff              stuff defined categories into dialog combobox widget
   tag_orphans                report tags defined and not used in any image file
   load_filemeta              load image file metadata into memory (indexed data only)
   save_filemeta              save metadata to image file EXIF and to image index
   update_image_index         update index data for current image file
   delete_image_index         delete index record for deleted image file

   m_batch_geotags            add given geotag to selected set of images
   geotags_chooseloc          get possible location matches for partial input, choose one
   web_geocode                web service to map location to earth coordinates
   init_geolocs               load geolocations table from image index file
   get_geolocs                get earth coordinates for a city or location
   put_geolocs                put new location data in geolocations table
   validate_latlong           validate earth coordinates data
   earth_distance             compute km distance between two earth coordinates

   Geotag mapping (W view)
   -----------------------
   m_load_geomap              load a geographic map chosen by user
   m_mapsearch_range          set the map search range from a clicked position, km
   geomap_position            convert earth coordinates to map pixel position
   geomap_coordinates         convert map pixel position to earth coordinates
   geomap_paint_dots          paint red dots on map where images are located
   geomap_mousefunc           respond to mouse movement and left clicks on geomap
   find_geomap_images         find images within range of geolocation
   free_geomap                free huge memory for geomap image

   Geotag mapping (M view)
   -----------------------
   m_load_OSM_map             initialize OSM map
   m_OSM_zoomin               zoom OSM map in on image location
   OSM_mousefunc              respond to clicks on OSM map
   find_OSM_images            find images

   Image search utilities
   ----------------------
   m_search_images            find images using any metadata and/or file names
   m_locations                find images by location and date range
   m_timeline                 find images by year and month

   EXIF store and retrieve
   -----------------------
   exif_get                   get image metadata from list of keys
   exif_put                   update image metadata from list of keys and data
   exif_copy                  copy metadata from file to file, with revisions
   exif_server                start exiftool server process, send data requests
   exif_tagdate               yyyy:mm:dd hh:mm:ss to yyyymmddhhmmss
   tag_exifdate               yyyymmddhhmmss to yyyy:mm:dd hh:mm:ss

   Image index functions
   ---------------------
   init_image_index           initialization for image file index read/write
   get_sxrec                  get image index record for image file
   get_sxrec_min              get index data used for gallery view
   put_sxrec                  add or update index record for an image file
   read_sxrec_seq             read all index records sequentially, one per call
   write_sxrec_seq            write all index records sequentially


*********************************************************************************/

#define EX extern                                                                //  enable extern declarations
#include "fotoxx.h"                                                              //  (variables in fotoxx.h are refs)

/********************************************************************************/

char  *pdate_metadate(cchar *pdate);                                             //  "yyyy-mm-dd" to "yyyymmdd"
char  *ptime_metatime(cchar *ptime);                                             //  "hh:mm" to "hhmm"
void  manage_tags();                                                             //  manage tags dialog
int   add_tag(char *tag, char *taglist, int maxcc);                              //  add tag if unique and enough space
int   del_tag(char *tag, char *taglist);                                         //  remove tag from tag list
int   add_recentag(char *tag);                                                   //  add tag to recent tags, keep recent
void  load_deftags();                                                            //  tags_defined file >> tags_deftags[]
void  save_deftags();                                                            //  tags_deftags[] >> defined_tags file
int   find_deftag(char *tag);                                                    //  find tag in tags_deftags[]
int   add_deftag(char *catg, char *tag);                                         //  add tag to tags_deftags[]
int   del_deftag(char *tag);                                                     //  remove tag from tags_deftags[]
void  deftags_stuff(zdialog *zd, cchar *catg);                                   //  tags_deftags[] >> zd widget deftags
void  defcats_stuff(zdialog *zd);                                                //  defined categories >> " widget defcats

int   geotags_chooseloc(char *location[2], char *coord[2]);                      //  choose location from multiple options
cchar * web_geocode(char *location[2], char *coord[2]);                          //  find earth coordinates via web service
int   init_geolocs();                                                            //  load geolocs table from index file
int   get_geolocs(char *location[2], char *coord[2], char *matches[20][2]);      //  get earth coordinates from location
int   put_geolocs(char *location[2], char *coord[2]);                            //  Update geolocations table in memory
int   validate_latlong(char *lati, char *longi, float &flati, float &flongi);    //  convert and validate earth coordinates

namespace meta_names 
{
   char     meta_pdate[16];                                                      //  image (photo) date, yyyymmddhhmmss
   char     meta_rating[4];                                                      //  image rating in stars, "0" to "5"
   char     meta_size[16];                                                       //  image size, "NNNNxNNNN"
   char     meta_tags[tagFcc];                                                   //  tags for current image file
   char     meta_comments[exif_maxcc];                                           //  image comments            expanded
   char     meta_caption[exif_maxcc];                                            //  image caption
   char     meta_location[100], meta_country[100];                               //  geolocs: city/location, country
   char     meta_lati[20], meta_longi[20];                                       //  geolocs: earth coordinates (-123.4567)

   char     p_meta_pdate[16];                                                    //  previous file metadata             16.03
   char     p_meta_rating[4];
   char     p_meta_tags[tagFcc];
   char     p_meta_comments[exif_maxcc];
   char     p_meta_caption[exif_maxcc];
   char     p_meta_location[100], p_meta_country[100];
   char     p_meta_lati[20], p_meta_longi[20];

   char     *tags_deftags[maxtagcats];                                           //  defined tags: catg: tag1, ... tagN,
   char     tags_recentags[tagRcc] = "";                                         //  recently added tags list

   char     keyname[40], keydata[exif_maxcc];

   struct geolocs_t {                                                            //  geolocations table, memory DB
      char     *location, *country;                                              //  maps locations <-> earth coordinates
      char     *lati, *longi;
      float    flati, flongi;
   };

   geolocs_t   *geolocs;
   int         Ngeolocs = 0;                                                     //  size of geolocations table

   zdialog     *zd_mapgeotags = 0;                                               //  zdialog wanting geotags via map click
}

using namespace meta_names;


/********************************************************************************/

//  menu function and popup dialog to show EXIF/IPTC data
//  window is updated when navigating to another image

int   metadata_report_type = 1;


//  called by f_open() if zdexifview is defined

void meta_view(int type)
{
   if (type && metadata_report_type != type) {
      if (zdexifview) zdialog_free(zdexifview);
      zdexifview = 0;
   }

   if (type) metadata_report_type = type;

   if (metadata_report_type == 2)
      m_meta_view_long(0,0);
   else 
      m_meta_view_short(0,0);
   return;
}


//  menu function - short report

void m_meta_view_short(GtkWidget *, cchar *menu)
{
   int   meta_view_dialog_event(zdialog *zd, cchar *event);

   #define     NK 20
   cchar       *keyname[NK] = { "ImageSize", "FileSize", exif_date_key, "FileModifyDate", "Make", "Model",
                              exif_focal_length_key, "ExposureTime", "FNumber", "ISO",
                              exif_city_key, exif_country_key, exif_lati_key, exif_longi_key,
                              iptc_keywords_key, iptc_rating_key, iptc_caption_key, exif_comment_key,
                              exif_usercomment_key, exif_editlog_key };
   char        *keyval[NK];

   char           chsec[12], **text;
   static char    *file = 0, *filen, *chsize;
   float          fsize, fsec;
   int            err, ii, nn;
   cchar          *textdelims = "!-,.:;?/)}]";
   cchar          *editdelims = ":|,";
   GtkWidget      *widget;

   FILE           *fid;
   int            nkx;
   char           *keynamex[NK], *keyvalx[NK];
   char           extras_file[200], buff[100], *pp;

   F1_help_topic = "view_metadata";
   
   if (metadata_report_type != 1) {
      if (zdexifview) zdialog_free(zdexifview);
      zdexifview = 0;
      metadata_report_type = 1;
   }
   
   if (file) zfree(file);
   file = 0;

   if (clicked_file) {                                                           //  use clicked file if present
      file = clicked_file;
      clicked_file = 0;
   }
   else if (curr_file)                                                           //  else current file
      file = zstrdup(curr_file);
   else return;

   if (! zdexifview)                                                             //  popup dialog if not already
   {
      zdexifview = zdialog_new(ZTX("View Metadata"),Mwin,ZTX("Extras"),Bcancel,null);
      zdialog_add_widget(zdexifview,"scrwin","scroll","dialog",0,"expand");
      zdialog_add_widget(zdexifview,"text","exifdata","scroll",0,"expand");
      zdialog_resize(zdexifview,550,350);
      zdialog_run(zdexifview,meta_view_dialog_event);
   }

   widget = zdialog_widget(zdexifview,"exifdata");
   wclear(widget);

   err = exif_get(file,keyname,keyval,NK);
   if (err) {
      zmessageACK(Mwin,"exif failure");
      return;
   }

   filen = strrchr(file,'/');                                                    //  get file name without directory
   if (filen) filen++;
   else filen = file;

   fsize = 0;
   if (keyval[1]) fsize = atof(keyval[1]);                                       //  get file size in B/KB/MB units
   chsize = formatKBMB(fsize,3);

   if (keyval[2] && strlen(keyval[2]) > 16) keyval[2][16] = 0;                   //  truncate dates to yyyy-mm-dd hh:mm
   if (keyval[3] && strlen(keyval[3]) > 16) keyval[3][16] = 0;

   wprintf(widget,"File        %s \n",filen);
   wprintf(widget,"Size        %s  %s \n",keyval[0],chsize);
   wprintf(widget,"Dates       photo: %s  file: %s \n",keyval[2],keyval[3]);

   if (keyval[4] || keyval[5])
      wprintf(widget,"Camera      %s  %s \n",keyval[4],keyval[5]);

   if (keyval[6] || keyval[7] || keyval[8] || keyval[9])                         //  photo exposure data
   {
      strcpy(chsec,"null");
      if (keyval[7]) {
         fsec = atof(keyval[7]);                                                 //  convert 0.008 seconds to 1/125 etc.
         if (fsec > 0 && fsec <= 0.5) {
            fsec = 1/fsec;
            snprintf(chsec,12,"1/%.0f",fsec);
         }
         else if (fsec > 0)
            snprintf(chsec,12,"%.0f",fsec);
      }
      wprintf(widget,"Exposure    %s mm  %s sec  F%s  ISO %s \n",keyval[6],chsec,keyval[8],keyval[9]);
   }

   if (keyval[10] || keyval[11] || keyval[12] || keyval[13])                     //  geotag data
      wprintf(widget,"Location    %s %s  %s %s \n",keyval[10],keyval[11],keyval[12],keyval[13]);

   if (keyval[14])
      wprintf(widget,"Keywords    %s \n",keyval[14]);                            //  tags

   if (keyval[15])                                                               //  rating
      wprintf(widget,"Rating      %s \n",keyval[15]);

   if (keyval[16]) {                                                             //  caption-abstract
      nn = breakup_text(keyval[16],text,textdelims,40,60);
      wprintf(widget,"Caption     %s \n",text[0]);
      for (ii = 1; ii < nn; ii++)
         wprintf(widget,"            %s \n",text[ii]);
      for (ii = 0; ii < nn; ii++)
         zfree(text[ii]);
      zfree(text);
   }

   if (keyval[17]) {                                                             //  comment                            15.10
      nn = breakup_text(keyval[17],text,textdelims,40,60);
      wprintf(widget,"Comment     %s \n",text[0]);
      for (ii = 1; ii < nn; ii++)
         wprintf(widget,"            %s \n",text[ii]);
      for (ii = 0; ii < nn; ii++)
         zfree(text[ii]);
      zfree(text);
   }

   if (keyval[18]) {                                                             //  usercomment                        15.10
      nn = breakup_text(keyval[18],text,textdelims,40,60);
      wprintf(widget,"UserComment %s \n",text[0]);
      for (ii = 1; ii < nn; ii++)
         wprintf(widget,"            %s \n",text[ii]);
      for (ii = 0; ii < nn; ii++)
         zfree(text[ii]);
      zfree(text);
   }

   if (keyval[19]) {                                                             //  edit history log
      nn = breakup_text(keyval[19],text,editdelims,40,60);
      wprintf(widget,"Edits       %s \n",text[0]);
      for (ii = 1; ii < nn; ii++)
         wprintf(widget,"            %s \n",text[ii]);
      for (ii = 0; ii < nn; ii++)
         zfree(text[ii]);
      zfree(text);
   }

   for (ii = 0; ii < NK; ii++)                                                   //  free memory
      if (keyval[ii]) zfree(keyval[ii]);

   //  append extra report items if any      

   snprintf(extras_file,200,"%s/metadata_view_extra",get_zuserdir()); 
   fid = fopen(extras_file,"r"); 
   if (! fid) goto finished;                                                     //  no extras file

   for (nkx = 0; nkx < NK; nkx++) {                                              //  get list of user extras            15.12
      pp = fgets_trim(buff,100,fid,1);
      if (! pp) break;
      strCompress(pp);
      if (*pp <= ' ') { nkx--; continue; }
      keynamex[nkx] = zstrdup(pp);
   }
   fclose(fid);
   
   if (nkx == 0) goto finished;                                                  //  empty file
      
   err = exif_get(file,(cchar **) keynamex,keyvalx,nkx);                         //  get all items at once
   if (err) {
      zmessageACK(Mwin,"exif failure");
      goto finished;
   }

   wprintf(widget,"\n");                                                         //  blank line
   
   for (ii = 0; ii < nkx; ii++)                                                  //  report user extra items
      if (keyvalx[ii]) 
         wprintf(widget,"%-24s : %s \n",keynamex[ii],keyvalx[ii]);
   
   for (ii = 0; ii < nkx; ii++) {                                                //  free memory
      if (keynamex[ii]) zfree(keynamex[ii]);
      if (keyvalx[ii]) zfree(keyvalx[ii]);
   }

finished:
   wscroll(widget,1);                                                            //  back to top of report
   gtk_window_present(MWIN);                                                     //  keep focus on main window          15.03
   return;
}


//  menu function - long report

void m_meta_view_long(GtkWidget *, cchar *menu)
{
   int   meta_view_dialog_event(zdialog *zd, cchar *event);

   char           *buff;
   static char    *file = 0;
   int            contx = 0;
   GtkWidget      *widget;

   F1_help_topic = "view_metadata";

   if (metadata_report_type != 2) {
      if (zdexifview) zdialog_free(zdexifview);
      zdexifview = 0;
      metadata_report_type = 2;
   }

   if (file) zfree(file);
   file = 0;

   if (clicked_file) {                                                           //  use clicked file if present
      file = clicked_file;
      clicked_file = 0;
   }
   else if (curr_file)                                                           //  else current file
      file = zstrdup(curr_file);
   else return;

   if (! zdexifview)                                                             //  popup dialog if not already
   {
      zdexifview = zdialog_new(ZTX("View Metadata"),Mwin,Bcancel,null);
      zdialog_add_widget(zdexifview,"scrwin","scroll","dialog",0,"expand");
      zdialog_add_widget(zdexifview,"text","exifdata","scroll",0,"expand|wrap");
      zdialog_resize(zdexifview,800,800);
      zdialog_run(zdexifview,meta_view_dialog_event);
   }

   widget = zdialog_widget(zdexifview,"exifdata");
   gtk_text_view_set_editable(GTK_TEXT_VIEW(widget),0);                          //  disable widget editing
   gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(widget),GTK_WRAP_NONE);             //  disable text wrap
   wclear(widget);

   snprintf(command,CCC,"exiftool -s -e \"%s\" ", file);

   while ((buff = command_output(contx,command))) {                              //  run command, output into window
      wprintf(widget,"%s\n",buff);
      zfree(buff);
   }

   command_status(contx);

   wscroll(widget,1);                                                            //  back to top of report

   gtk_window_present(MWIN);                                                     //  keep focus on main window          15.03
   return;
}



//  dialog event and completion callback function

int meta_view_dialog_event(zdialog *zd, cchar *event)
{
   char     filex[200];
   STATB    statb;
   int      zstat, err;
   
   zstat = zd->zstat;
   if (strmatch(event,"escape")) zstat = -1;                                     //  escape = cancel                    15.07
   if (! zstat) return 1;                                                        //  wait for completion

   zdialog_free(zd);                                                             //  kill dialog
   zdexifview = 0;

   if (metadata_report_type != 1) return 1;                                      //  not short report
   if (zstat != 1) return 1;                                                     //  not [extras] button
   
   snprintf(filex,200,"%s/metadata_view_extra",get_zuserdir());                  //  edit file with extra keywords
   err = stat(filex,&statb);
   if (err) shell_quiet("echo -n \"extra keywords list\n\" > %s",filex);
   shell_quiet("xdg-open %s",filex);
   
   return 1;
}


/********************************************************************************/

//  Show captions and comments on top of current image in main window.
//  Menu call (menu arg not null): toggle switch on/off.
//  Non-menu call: write caption/comment on image if switch is ON.

void m_captions(GtkWidget *, cchar *menu)
{
   cchar        *keynames[2] = { iptc_caption_key, exif_comment_key };
   char         *keyvals[2];
   char         caption[200], comment[200];
   static char  text[402];

   F1_help_topic = "show_captions";

   if (menu) {                                                                   //  if menu call, flip toggle
      Fcaptions = 1 - Fcaptions;
      if (curr_file) f_open(curr_file);
      return;
   }

   if (! Fcaptions) return;

   if (! curr_file) return;
   *caption = *comment = 0;

   exif_get(curr_file,keynames,keyvals,2);                                       //  get captions and comments metadata

   if (keyvals[0]) {
      strncpy0(caption,keyvals[0],200);
      zfree(keyvals[0]);
   }

   if (keyvals[1]) {
      strncpy0(comment,keyvals[1],200);
      zfree(keyvals[1]);
   }

   *text = 0;

   if (*caption) strcpy(text,caption);
   if (*caption && *comment) strcat(text,"\n");
   if (*comment) strcat(text,comment);

   for (int ii = 0; text[ii]; ii++)                                              //  replace "\n" with newline chars.
      if (text[ii] == '\\' && text[ii+1] == 'n')
         memmove(text+ii,"\n ",2);

   if (*text) add_toptext(1,0,0,text,"Sans 10");

   return;
}


/********************************************************************************/

//  edit metadata menu function

void m_edit_metadata(GtkWidget *, cchar *menu)                                   //  overhauled
{
   void  edit_imagetags_clickfunc(GtkWidget *widget, int line, int pos);
   void  edit_recentags_clickfunc(GtkWidget *widget, int line, int pos);
   void  edit_matchtags_clickfunc(GtkWidget *widget, int line, int pos);
   void  edit_deftags_clickfunc(GtkWidget *widget, int line, int pos);
   int   editmeta_dialog_event(zdialog *zd, cchar *event);

   cchar    *mapquest = ZTX("Geocoding service by MapQuest");

   GtkWidget   *widget;
   zdialog     *zd;
   char        *ppv, pdate[12], ptime[12];
   char        cctext[exif_maxcc+50];
   char        RN[4] = "R0";
   int         ii;

   F1_help_topic = "edit_metadata";

   if (clicked_file) {                                                           //  use clicked file if present
      f_open(clicked_file,0,0,1,0);
      clicked_file = 0;
   }
   
   if (! curr_file) {
      if (zdeditmeta) zdialog_free(zdeditmeta);
      zdeditmeta = 0;
      zd_mapgeotags = 0;
      return;
   }

   if (! init_geolocs()) return;                                                 //  initialize geotags

   if (FGWM == 'G') gallery(curr_file,"paint");                                  //  if gallery view, repaint           16.06

/***
          ________________________________________________________
         |                Edit Metadata                           |
         |                                                        |
         |  Image File: filename.jpg                              |
         |  Image Date: [_________]  Time: [______]  [prev]       |
         |  Rating (stars): ʘ 1  ʘ 2  ʘ 3  ʘ 4  ʘ 5  ʘ 6       |
         |  Caption [___________________________________________] |
         |  Comments [__________________________________________] |
         |  - - - - - - - - - - - - - - - - - - - - - - - - - - - |
         |  location [_____________]  country [____________]      |              //  16.07
         |  latitude [__________]  longitude [__________]         |
         |  [Find] [Web] [Clear]    Geocoding service by MapQuest |
         |  - - - - - - - - - - - - - - - - - - - - - - - - - - - |
         |  Current Tags [______________________________________] |
         |  Recent Tags [_______________________________________] |
         |  Type New Tag [______________________________________] |
         |  Matching Tags [_____________________________________] |
         |  - - - - - - - - - - - - - - - - - - - - - - - - - - - |
         |  Defined Tags Category [___________________________|v] |
         |  |                                                   | |
         |  |                                                   | |
         |  |                                                   | |
         |  |                                                   | |
         |  |                                                   | |
         |  |                                                   | |
         |  |___________________________________________________| |
         |                                                        |
         |                 [Manage Tags] [Prev] [Apply] [Cancel]  |
         |________________________________________________________|

***/

   if (! zdeditmeta)                                                             //  (re) start edit dialog
   {
      zd = zdialog_new(ZTX("Edit Metadata"),Mwin,Bmanagetags,Bprev,Bapply,Bcancel,null);
      zdeditmeta = zd;
      
      zdialog_add_ttip(zd,Bapply,ZTX("save metadata to file"));

      //  Image File: xxxxxxxxx.jpg
      zdialog_add_widget(zd,"hbox","hbf","dialog",0,"space=3");
      zdialog_add_widget(zd,"label","labf","hbf",ZTX("Image File:"),"space=3");
      zdialog_add_widget(zd,"label","file","hbf","filename.jpg","space=5");

      //  Image Date yyyy-mm-dd [__________]  Time hh:mm [_____]  [prev]
      zdialog_add_widget(zd,"hbox","hbdt","dialog",0,"space=1");
      zdialog_add_widget(zd,"label","labdate","hbdt",ZTX("Image Date"),"space=3");
      zdialog_add_widget(zd,"entry","date","hbdt",0,"size=12");
      zdialog_add_widget(zd,"label","space","hbdt",0,"space=5");
      zdialog_add_widget(zd,"label","labtime","hbdt",ZTX("Time"),"space=3");
      zdialog_add_widget(zd,"entry","time","hbdt",0,"size=8");
      zdialog_add_widget(zd,"button","ppdate","hbdt",Bprev,"space=8");
      zdialog_add_ttip(zd,"date","yyyy-mm-dd");
      zdialog_add_ttip(zd,"time","hh:mm");

      //  Rating (stars): ʘ 1  ʘ 2  ʘ 3  ʘ 4  ʘ 5  ʘ 6
      zdialog_add_widget(zd,"hbox","hbrate","dialog");
      zdialog_add_widget(zd,"label","labrate","hbrate",ZTX("Rating (stars):"),"space=3");
      zdialog_add_widget(zd,"radio","R0","hbrate","0","space=6");
      zdialog_add_widget(zd,"radio","R1","hbrate","1","space=6");
      zdialog_add_widget(zd,"radio","R2","hbrate","2","space=6");
      zdialog_add_widget(zd,"radio","R3","hbrate","3","space=6");
      zdialog_add_widget(zd,"radio","R4","hbrate","4","space=6");
      zdialog_add_widget(zd,"radio","R5","hbrate","5","space=6");

      zdialog_add_widget(zd,"hbox","space","dialog",0,"space=3");

      //  Caption [___________________________________________]
      zdialog_add_widget(zd,"hbox","hbcap","dialog",0,"space=1");
      zdialog_add_widget(zd,"label","labcap","hbcap",ZTX("Caption"),"space=3");
      zdialog_add_widget(zd,"frame","frcap","hbcap",0,"space=3|expand");
      zdialog_add_widget(zd,"edit","caption","frcap",0,"wrap");

      //  Comments [__________________________________________]
      zdialog_add_widget(zd,"hbox","hbcom","dialog",0,"space=1");
      zdialog_add_widget(zd,"label","labcom","hbcom",ZTX("Comments"),"space=3");
      zdialog_add_widget(zd,"frame","frcom","hbcom",0,"space=3|expand");
      zdialog_add_widget(zd,"edit","comments","frcom",0,"wrap");

      zdialog_add_widget(zd,"hsep","sep","dialog",0,"space=3");

      //  location [_____________]  country [____________]
      zdialog_add_widget(zd,"hbox","hbcc","dialog",0,"space=3");
      zdialog_add_widget(zd,"label","labloc","hbcc",ZTX("location"),"space=5");
      zdialog_add_widget(zd,"entry","location","hbcc",0,"expand");
      zdialog_add_widget(zd,"label","space","hbcc",0,"space=5");
      zdialog_add_widget(zd,"label","labcountry","hbcc",ZTX("country"),"space=5");
      zdialog_add_widget(zd,"entry","country","hbcc",0,"expand");

      //  latitude [__________]  longitude [__________]     
      zdialog_add_widget(zd,"hbox","hbll","dialog");
      zdialog_add_widget(zd,"label","lablat","hbll","Latitude","space=3");
      zdialog_add_widget(zd,"entry","lati","hbll",0,"size=10");
      zdialog_add_widget(zd,"label","space","hbll",0,"space=5");
      zdialog_add_widget(zd,"label","lablong","hbll","Longitude","space=3");
      zdialog_add_widget(zd,"entry","longi","hbll",0,"size=10");

      //  [Find] [Web] [Clear]    Geocoding service by MapQuest
      zdialog_add_widget(zd,"hbox","hbgeo","dialog");
      zdialog_add_widget(zd,"button","geofind","hbgeo",Bfind,"space=5");
      zdialog_add_widget(zd,"button","geoweb","hbgeo",Bweb,"space=5");
      zdialog_add_widget(zd,"button","geoclear","hbgeo",Bclear,"space=5");
      zdialog_add_widget(zd,"label","labmq","hbgeo",mapquest,"space=3");

      zdialog_add_widget(zd,"hsep","sep","dialog",0,"space=3");

      //  Current Tags [________________________________________] 
      zdialog_add_widget(zd,"hbox","hbit","dialog",0,"space=1");
      zdialog_add_widget(zd,"label","labit","hbit",ZTX("Image Tags"),"space=3");
      zdialog_add_widget(zd,"frame","frit","hbit",0,"space=3|expand");
      zdialog_add_widget(zd,"text","imagetags","frit",0,"wrap");

      //  Recent Tags [_______________________________________]
      zdialog_add_widget(zd,"hbox","hbrt","dialog",0,"space=1");
      zdialog_add_widget(zd,"label","labrt","hbrt",ZTX("Recent Tags"),"space=3");
      zdialog_add_widget(zd,"frame","frrt","hbrt",0,"space=3|expand");
      zdialog_add_widget(zd,"text","recentags","frrt",0,"wrap");
      
      //  Enter New Tag [________________]
      zdialog_add_widget(zd,"hbox","hbnt","dialog",0,"space=1");
      zdialog_add_widget(zd,"label","labnt","hbnt",ZTX("Enter New Tag"),"space=3");
      zdialog_add_widget(zd,"entry","newtag","hbnt");
      //zdialog_add_widget(zd,"frame","frnt","hbnt",0,"space=3|expand");         //  works OK, but GTK warnings galore
      //zdialog_add_widget(zd,"edit","newtag","frnt");

      //  Matching Tags [_____________________________________]
      zdialog_add_widget(zd,"hbox","hbmt","dialog",0,"space=1");
      zdialog_add_widget(zd,"label","labmt","hbmt",ZTX("Matching Tags"),"space=3");
      zdialog_add_widget(zd,"frame","frmt","hbmt",0,"space=3|expand");
      zdialog_add_widget(zd,"text","matchtags","frmt",0,"wrap");

      zdialog_add_widget(zd,"hsep","sep","dialog",0,"space=3");

      //  Defined Tags Category 
      zdialog_add_widget(zd,"hbox","space","dialog");
      zdialog_add_widget(zd,"hbox","hbdt1","dialog");
      zdialog_add_widget(zd,"label","labdt","hbdt1",ZTX("Defined Tags Category"),"space=3");
      zdialog_add_widget(zd,"combo","defcats","hbdt1",0,"expand|space=5");
      zdialog_add_widget(zd,"hbox","hbdt2","dialog",0,"expand");
      zdialog_add_widget(zd,"frame","frdt2","hbdt2",0,"expand|space=3");
      zdialog_add_widget(zd,"scrwin","swdt2","frdt2",0,"expand");
      zdialog_add_widget(zd,"text","deftags","swdt2",0,"wrap");
      
      zdialog_add_ttip(zd,"geofind",ZTX("search known locations"));
      zdialog_add_ttip(zd,"geoweb",ZTX("search using web service"));
      zdialog_add_ttip(zd,Bmanagetags,ZTX("create tag categories and tags"));

      load_deftags();                                                            //  stuff defined tags into dialog
      deftags_stuff(zd,"ALL");
      defcats_stuff(zd);                                                         //  and defined categories             15.08

      widget = zdialog_widget(zd,"imagetags");                                   //  tag widget mouse functions
      textwidget_set_clickfunc(widget,edit_imagetags_clickfunc);

      widget = zdialog_widget(zd,"recentags");
      textwidget_set_clickfunc(widget,edit_recentags_clickfunc);

      widget = zdialog_widget(zd,"matchtags");
      textwidget_set_clickfunc(widget,edit_matchtags_clickfunc);

      widget = zdialog_widget(zd,"deftags");
      textwidget_set_clickfunc(widget,edit_deftags_clickfunc);

      zdialog_resize(zd,400,700);                                                //  run dialog
      zdialog_run(zd,editmeta_dialog_event);
   }

   zd = zdeditmeta;

   ppv = (char *) strrchr(curr_file,'/');
   zdialog_stuff(zd,"file",ppv+1);                                               //  stuff dialog fields from curr. file

   metadate_pdate(meta_pdate,pdate,ptime);
   zdialog_stuff(zd,"date",pdate);
   zdialog_stuff(zd,"time",ptime);

   for (ii = 0; ii <= 5; ii++) {                                                 //  set all rating radio buttons OFF
      RN[1] = '0' + ii;
      zdialog_stuff(zd,RN,0);
   }
   RN[1] = meta_rating[0];                                                       //  set radio button ON for current rating
   zdialog_stuff(zd,RN,1);

   repl_1str(meta_caption,cctext,"\\n","\n");
   zdialog_stuff(zd,"caption",cctext);
   repl_1str(meta_comments,cctext,"\\n","\n");
   zdialog_stuff(zd,"comments",cctext);

   if (strmatch(meta_location,"null")) zdialog_stuff(zd,"location","");          //  geotags data
   else zdialog_stuff(zd,"location",meta_location);                              //  if null, stuff blank               16.08
   if (strmatch(meta_country,"null")) zdialog_stuff(zd,"country","");
   else zdialog_stuff(zd,"country",meta_country);
   if (strmatch(meta_lati,"null")) zdialog_stuff(zd,"lati","");
   else zdialog_stuff(zd,"lati",meta_lati);
   if (strmatch(meta_longi,"null")) zdialog_stuff(zd,"longi","");
   else zdialog_stuff(zd,"longi",meta_longi);

   zdialog_stuff(zd,"imagetags",meta_tags);
   zdialog_stuff(zd,"recentags",tags_recentags);

   zd_mapgeotags = zd;                                                           //  activate geomap clicks

   if (FGWM == 'F') gtk_window_present(MWIN);                                    //  keep focus on main window          16.06
   return;
}


//  mouse click functions for various text widgets for tags

void edit_imagetags_clickfunc(GtkWidget *widget, int line, int pos)              //  existing image tag was clicked
{
   char     *txline, *txtag, end = 0;

   txline = textwidget_get_line(widget,line,0);
   if (! txline) return;

   txtag = textwidget_get_word(txline,pos,",;",end);
   if (! txtag) { zfree(txline); return; }

   del_tag(txtag,meta_tags);                                                     //  remove tag from image
   zdialog_stuff(zdeditmeta,"imagetags",meta_tags);
   Fmetamod++;                                                                   //  note change                        15.03

   zfree(txline);
   zfree(txtag);
   return;
}


void edit_recentags_clickfunc(GtkWidget *widget, int line, int pos)              //  recent tag was clicked
{
   char     *txline, *txtag, end = 0;

   txline = textwidget_get_line(widget,line,0);
   if (! txline) return;

   txtag = textwidget_get_word(txline,pos,",;",end);
   if (! txtag) { zfree(txline); return; }

   add_tag(txtag,meta_tags,tagFcc);                                              //  add recent tag to image
   zdialog_stuff(zdeditmeta,"imagetags",meta_tags);
   Fmetamod++;                                                                   //  note change                        15.03

   zfree(txline);
   zfree(txtag);
   return;
}


void edit_matchtags_clickfunc(GtkWidget *widget, int line, int pos)              //  matching tag was clicked           15.07
{
   char     *txline, *txtag, end = 0;

   txline = textwidget_get_line(widget,line,0);
   if (! txline) return;

   txtag = textwidget_get_word(txline,pos,",;",end);
   if (! txtag) { zfree(txline); return; }

   add_tag(txtag,meta_tags,tagFcc);                                              //  add matching tag to image
   Fmetamod++;                                                                   //  note change                        15.03
   add_recentag(txtag);                                                          //  and add to recent tags

   zdialog_stuff(zdeditmeta,"imagetags",meta_tags);                              //  update dialog widgets
   zdialog_stuff(zdeditmeta,"recentags",tags_recentags);
   zdialog_stuff(zdeditmeta,"newtag","");
   zdialog_stuff(zdeditmeta,"matchtags","");

   zdialog_goto(zdeditmeta,"newtag");                                            //  put focus back on newtag widget

   zfree(txline);
   zfree(txtag);
   return;
}


void edit_deftags_clickfunc(GtkWidget *widget, int line, int pos)                //  defined tag was clicked
{
   char     *txline, *txtag, end = 0;

   txline = textwidget_get_line(widget,line,0);
   if (! txline) return;

   txtag = textwidget_get_word(txline,pos,",;:",end);
   if (! txtag || end == ':') { zfree(txline); return; }                         //  tag category clicked, ignore

   add_tag(txtag,meta_tags,tagFcc);                                              //  add new tag to image
   zdialog_stuff(zdeditmeta,"imagetags",meta_tags);                              //    from defined tags list
   Fmetamod++;                                                                   //  note change                        15.03

   add_recentag(txtag);                                                          //  and add to recent tags
   zdialog_stuff(zdeditmeta,"recentags",tags_recentags);

   zfree(txline);
   zfree(txtag);
   return;
}


//  dialog event and completion callback function

int editmeta_dialog_event(zdialog *zd, cchar *event)                             //  overhauled
{
   char     pdate[12], ptime[12];                                                //  yyyy-mm-dd  and  hh:mm:ss
   char     *metadate, *metatime;                                                //  yyyymmdd  and  hhmmss
   cchar    *errmess;
   int      ii, jj, nt, cc1, cc2, ff;
   char     *pp1, *pp2;
   char     catgname[tagcc];
   char     newtag[tagcc], matchtags[20][tagcc];
   char     matchtagstext[(tagcc+2)*20];
   char     cctext[exif_maxcc+50];
   char     RN[4] = "R0";

   int      Nmatch, err, gstat;
   char     *location[2], *coord[2], *matches[20][2];
   char     locatn[100], country[100];
   char     lati[20], longi[20], *pp;
   float    fcoord[2];

   if (strmatch(event,"escape")) zd->zstat = 4;                                  //  escape = cancel                    15.07
   if (! curr_file) zd->zstat = 4;                                               //  current file gone

   if (strstr("date time caption comments",event))                               //  note change but process later
      Fmetamod++;

   if (strmatch(event,"ppdate")) {                                               //  repeat last date used
      if (*p_meta_pdate) {
         metadate_pdate(p_meta_pdate,pdate,ptime);
         zdialog_stuff(zd,"date",pdate);
         zdialog_stuff(zd,"time",ptime);
         Fmetamod++;
         return 1;
      }
   }

   if (strstr("R0 R1 R2 R3 R4 R5",event)) {                                      //  note if rating changed
      Fmetamod++;
      return 1;
   }

   if (strstr("location country lati longi",event)) {                            //  dialog inputs changed
      Fmetamod++;
      return 1;
   }

   if (strmatch(event,"geomap")) {                                               //  have geotags data from geomap
      Fmetamod++;
      return 1;
   }
   
   if (strmatch(event,"geofind"))                                                //  [find] search location data
   {
      zdialog_fetch(zd,"location",locatn,99);                                    //  get city/location [country] from dialog
      zdialog_fetch(zd,"country",country,99);
      locatn[0] = toupper(locatn[0]);                                            //  capitalize                         16.06
      country[0] = toupper(country[0]);
      zdialog_stuff(zd,"location",locatn);
      zdialog_stuff(zd,"country",country);

      location[0] = locatn;
      location[1] = country;
      Nmatch = get_geolocs(location,coord,matches);                              //  find in geolocs[*] table
      if (Nmatch == 0)                                                           //  no matches
         zmessageACK(Mwin,ZTX("location not found"));

      else if (Nmatch == 1) {                                                    //  one match
         zdialog_stuff(zd,"location",matches[0][0]);                             //  stuff matching location data into dialog
         zdialog_stuff(zd,"country",matches[0][1]);
         zdialog_stuff(zd,"lati",coord[0]);
         zdialog_stuff(zd,"longi",coord[1]);
         Fmetamod++;
      }

      else {                                                                     //  multiple matching cities
         gstat = geotags_chooseloc(location,coord);                              //  ask user to choose one
         if (gstat == 1) {                                                       //  response is available
            zdialog_stuff(zd,"location",location[0]);                            //  stuff matching location data into dialog
            zdialog_stuff(zd,"country",location[1]);
            zdialog_stuff(zd,"lati",coord[0]);
            zdialog_stuff(zd,"longi",coord[1]);
            Fmetamod++;
         }
      }

      return 1;
   }

   if (strmatch(event,"geoweb"))                                                 //  [web] search location data
   {
      zdialog_fetch(zd,"location",locatn,99);                                    //  get location [country] from dialog
      zdialog_fetch(zd,"country",country,99);
      locatn[0] = toupper(locatn[0]);                                            //  capitalize                         16.06
      country[0] = toupper(country[0]);
      zdialog_stuff(zd,"location",locatn);
      zdialog_stuff(zd,"country",country);

      location[0] = locatn;
      location[1] = country;
      coord[0] = lati;
      coord[1] = longi;

      errmess = web_geocode(location,coord);                                     //  look-up in web service
      if (errmess) zmessageACK(Mwin,errmess);                                    //  fail
      else {
         zdialog_stuff(zd,"location",location[0]);                               //  success, return all data
         zdialog_stuff(zd,"country",location[1]);                                //  (location may have been completed)
         zdialog_stuff(zd,"lati",coord[0]);
         zdialog_stuff(zd,"longi",coord[1]);
         Fmetamod++;
      }

      return 1;
   }

   if (strmatch(event,"geoclear"))                                               //  [clear] location data
   {
      zdialog_stuff(zd,"location","");                                           //  erase dialog fields
      zdialog_stuff(zd,"country","");
      zdialog_stuff(zd,"lati","");
      zdialog_stuff(zd,"longi","");
      Fmetamod++;
      return 1;
   }

   if (strmatch(event,"defcats")) {                                              //  new tag category selection         15.08
      zdialog_fetch(zd,"defcats",catgname,tagcc);
      deftags_stuff(zd,catgname);
   }

   if (strmatch(event,"newtag"))                                                 //  new tag is being typed in          15.07
   {
      zdialog_stuff(zd,"matchtags","");                                          //  clear matchtags in dialog

      zdialog_fetch(zd,"newtag",newtag,tagcc);                                   //  get chars. typed so far
      cc1 = strlen(newtag);
      
      for (ii = jj = 0; ii <= cc1; ii++) {                                       //  remove foul characters
         if (strchr(",:;",newtag[ii])) continue;
         newtag[jj++] = newtag[ii];
      }
      
      if (jj < cc1) {                                                            //  something was removed
         newtag[jj] = 0;
         cc1 = jj;
         zdialog_stuff(zd,"newtag",newtag);
      }

      if (cc1 < 2) return 1;                                                     //  wait for at least 2 chars.

      for (ii = nt = 0; ii < maxtagcats; ii++)                                   //  loop all categories
      {
         pp2 = tags_deftags[ii];                                                 //  category: aaaaaa, bbbbb, ... tagN,
         if (! pp2) continue;                                                    //            |     |
         pp2 = strchr(pp2,':');                                                  //            pp1   pp2
         
         while (true)                                                            //  loop all deftags in category
         {
            pp1 = pp2 + 2;
            if (! *pp1) break;
            pp2 = strchr(pp1,',');
            if (! pp2) break;
            if (strmatchcaseN(newtag,pp1,cc1)) {                                 //  deftag matches chars. typed so far
               cc2 = pp2 - pp1;
               strncpy(matchtags[nt],pp1,cc2);                                   //  save deftags that match
               matchtags[nt][cc2] = 0;
               if (++nt == 20) return 1;                                         //  quit if 20 matches or more
            }
         }
      }
      
      if (nt == 0) return 1;                                                     //  no matches

      pp1 = matchtagstext;

      for (ii = 0; ii < nt; ii++)                                                //  make deftag list: aaaaa, bbb, cccc ...
      {
         strcpy(pp1,matchtags[ii]);
         pp1 += strlen(pp1);
         strcpy(pp1,", ");
         pp1 += 2;
      }
      
      zdialog_stuff(zd,"matchtags",matchtagstext);                               //  stuff matchtags in dialog
      return 1;
   }

   if (strmatch(event,"enter"))                                                  //  KB Enter, new tag finished         15.07
   {
      zdialog_fetch(zd,"newtag",newtag,tagcc);                                   //  get finished tag
      cc1 = strlen(newtag);
      if (! cc1) return 1;
      if (newtag[cc1-1] == '\n') {                                               //  remove newline character
         cc1--;
         newtag[cc1] = 0;
      }

      for (ii = ff = 0; ii < maxtagcats; ii++)                                   //  loop all categories
      {
         pp2 = tags_deftags[ii];                                                 //  category: aaaaaa, bbbbb, ... tagN,
         if (! pp2) continue;                                                    //            |     |
         pp2 = strchr(pp2,':');                                                  //            pp1   pp2
         
         while (true)                                                            //  loop all deftags in category
         {
            pp1 = pp2 + 2;
            if (! *pp1) break;
            pp2 = strchr(pp1,',');
            if (! pp2) break;
            cc2 = pp2 - pp1;
            if (cc2 != cc1) continue;
            if (strmatchcaseN(newtag,pp1,cc1)) {                                 //  entered tag matches deftag
               strncpy(newtag,pp1,cc1);                                          //  use deftag upper/lower case
               ff = 1;
               break;
            }
         }

         if (ff) break;
      }

      add_tag(newtag,meta_tags,tagFcc);                                          //  add to image tag list
      Fmetamod++;                                                                //  note change
      add_recentag(newtag);                                                      //  and add to recent tags

      if (! ff) {                                                                //  if new tag, add to defined tags
         add_deftag((char *) "nocatg",newtag);
         deftags_stuff(zd,"ALL");
      }

      zdialog_stuff(zd,"newtag","");                                             //  update dialog widgets
      zdialog_stuff(zd,"imagetags",meta_tags);
      zdialog_stuff(zd,"recentags",tags_recentags);
      zdialog_stuff(zd,"matchtags","");

      zdialog_goto(zd,"newtag");                                                 //  put focus back on newtag widget
      return 1;
   }

   if (! zd->zstat) return 1;                                                    //  wait for completion

   if (zd->zstat == 1) {                                                         //  [manage tags]
      zd->zstat = 0;                                                             //  keep dialog active
      zdialog_show(zd,0);                                                        //  hide parent dialog
      manage_tags();
      return 1;
   }

   if (zd->zstat == 2)                                                           //  [prev] stuff previous file data    16.03
   {
      zd->zstat = 0;                                                             //  keep dialog active

      if (! *meta_pdate && *p_meta_pdate) {                                      //  stuff photo date only if none
         metadate_pdate(p_meta_pdate,pdate,ptime);
         zdialog_stuff(zd,"date",pdate);
         zdialog_stuff(zd,"time",ptime);
      }

      for (ii = 0; ii <= 5; ii++) {                                              //  stuff rating
         RN[1] = '0' + ii;                                                       //  radio buttons, "R0" to "R5"
         if (RN[1] == *p_meta_rating) zdialog_stuff(zd,RN,1);                    //  for ratings "0" to "5"
         else zdialog_stuff(zd,RN,0);
      }

      zdialog_stuff(zd,"location",p_meta_location);                              //  get last-used geotags              16.06
      zdialog_stuff(zd,"country",p_meta_country);
      zdialog_stuff(zd,"lati",p_meta_lati);
      zdialog_stuff(zd,"longi",p_meta_longi);

      zdialog_stuff(zd,"imagetags",p_meta_tags);                                 //  stuff tags
      strncpy0(meta_tags,p_meta_tags,tagFcc);

      repl_1str(p_meta_caption,cctext,"\\n","\n");                               //  stuff caption
      zdialog_stuff(zd,"caption",cctext);

      repl_1str(p_meta_comments,cctext,"\\n","\n");                              //  stuff comments
      zdialog_stuff(zd,"comments",cctext);
      
      Fmetamod++;
      return 1;
   }

   if (zd->zstat != 3) {                                                         //  [cancel] or [x]
      zdialog_free(zd);                                                          //  kill dialog
      zdeditmeta = 0;
      Fmetamod = 0;
      zd_mapgeotags = 0;                                                         //  deactivate geomap clicks
      return 1;
   }

   zd->zstat = 0;                                                                //  [apply] - keep dialog active

   if (! Fmetamod) return 1;                                                     //  nothing changed

   zdialog_fetch(zd,"date",pdate,12);                                            //  get photo date and time
   zdialog_fetch(zd,"time",ptime,12);
   if (*pdate) {                                                                 //  date available
      metadate = pdate_metadate(pdate);                                          //  validate
      if (! metadate) return 1;                                                  //  bad, re-input
      strcpy(meta_pdate,metadate);                                               //  convert to yyyymmdd
      if (*ptime) {                                                              //  time available
         metatime = ptime_metatime(ptime);                                       //  validate
         if (! metatime) return 1;                                               //  bad, re-input
         strcat(meta_pdate,metatime);                                            //  append hhmmss
      }
   }
   else *meta_pdate = 0;                                                         //  leave empty

   strcpy(meta_rating,"0");
   for (ii = 0; ii <= 5; ii++) {                                                 //  get which rating radio button ON
      RN[1] = '0' + ii;
      zdialog_fetch(zd,RN,jj);
      if (jj) meta_rating[0] = '0' + ii;                                         //  set corresponding rating
   }

   zdialog_fetch(zd,"caption",cctext,exif_maxcc);                                //  get new caption
   repl_1str(cctext,meta_caption,"\n","\\n");                                    //  replace newlines with "\n"
   zdialog_fetch(zd,"comments",cctext,exif_maxcc);                               //  get new comments
   repl_1str(cctext,meta_comments,"\n","\\n");                                   //  replace newlines with "\n"

   zdialog_fetch(zd,"location",locatn,99);                                       //  get location [country] from dialog
   zdialog_fetch(zd,"country",country,99);
   zdialog_fetch(zd,"lati",lati,20);                                             //  and latitude, longitude
   zdialog_fetch(zd,"longi",longi,20);
   
   pp = strchr(lati,',');                                                        //  replace comma decimal point        16.06
   if (pp) *pp = '.';                                                            //    with period
   pp = strchr(longi,',');
   if (pp) *pp = '.';

   location[0] = locatn;
   location[1] = country;
   coord[0] = lati;
   coord[1] = longi;

   if (*lati > ' ' && ! strmatch(lati,"null") &&                                 //  if coordinates present, validate   16.06
       *longi > ' ' && ! strmatch(longi,"null"))
   {
      err = validate_latlong(coord[0],coord[1],fcoord[0],fcoord[1]);
      if (err) {
         zmessageACK(Mwin,ZTX("bad latitude/longitude: %s %s"),coord[0],coord[1]);
         return 1;
      }
   }

   strncpy0(meta_location,locatn,99);                                            //  save geotags in image file EXIF    16.06
   strncpy0(meta_country,country,99);                                            //    and in search-index file
   strncpy0(meta_lati,lati,12);
   strncpy0(meta_longi,longi,12);

   put_geolocs(location,coord);                                                  //  update geolocs[*] table in memory

   save_filemeta(curr_file);                                                     //  save metadata changes to image file

   gtk_window_present(MWIN);                                                     //  keep focus on main window          16.06
   return 1;
}


/********************************************************************************/

//  manage tags function - auxilliary dialog

zdialog  *zdmanagetags = 0;

void manage_tags()
{
   void  manage_deftags_clickfunc(GtkWidget *widget, int line, int pos);
   int   managetags_dialog_event(zdialog *zd, cchar *event);

   GtkWidget   *widget;
   zdialog     *zd;

   if (zdmanagetags) return;
   zd = zdialog_new(ZTX("Manage Tags"),Mwin,ZTX("orphan tags"),Bdone,null);
   zdmanagetags = zd;

   zdialog_add_widget(zd,"hbox","hb7","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labcatg","hb7",ZTX("category"),"space=5");
   zdialog_add_widget(zd,"entry","catg","hb7",0,"size=12");
   zdialog_add_widget(zd,"label","space","hb7",0,"space=5");
   zdialog_add_widget(zd,"label","labtag","hb7",ZTX("tag"),"space=5");
   zdialog_add_widget(zd,"entry","tag","hb7",0,"size=20|expand");
   zdialog_add_widget(zd,"label","space","hb7",0,"space=5");
   zdialog_add_widget(zd,"button","create","hb7",Bcreate);
   zdialog_add_widget(zd,"button","delete","hb7",Bdelete);

   zdialog_add_widget(zd,"hbox","hb8","dialog");
   zdialog_add_widget(zd,"label","labdeftags","hb8",ZTX("Defined Tags:"),"space=5");
   zdialog_add_widget(zd,"hbox","hb9","dialog",0,"expand");
   zdialog_add_widget(zd,"frame","frame8","hb9",0,"space=5|expand");
   zdialog_add_widget(zd,"scrwin","scrwin8","frame8",0,"expand");
   zdialog_add_widget(zd,"text","deftags","scrwin8",0,"expand|wrap");

   widget = zdialog_widget(zd,"deftags");                                        //  tag widget mouse function
   textwidget_set_clickfunc(widget,manage_deftags_clickfunc);

   load_deftags();                                                               //  stuff defined tags into dialog
   deftags_stuff(zd,"ALL");

   zdialog_resize(zd,0,400);
   zdialog_run(zd,managetags_dialog_event);                                      //  run dialog
   zdialog_wait(zd);
   zdialog_free(zd); 

   return;
}


//  mouse click functions for widget having tags

void manage_deftags_clickfunc(GtkWidget *widget, int line, int pos)              //  tag or tag category was clicked
{
   char     *txline, *txword, end = 0;

   txline = textwidget_get_line(widget,line,0);
   if (! txline) return;

   txword = textwidget_get_word(txline,pos,",;:",end);
   if (! txword) { zfree(txline); return; }
   
   if (end == ':') zdialog_stuff(zdmanagetags,"catg",txword);                    //  selected category >> dialog widget
   else zdialog_stuff(zdmanagetags,"tag",txword);                                //  selected tag >> dialog widget

   zfree(txline);
   zfree(txword);
   return;
}


//  dialog event and completion callback function

int managetags_dialog_event(zdialog *zd, cchar *event)
{
   void tag_orphans();

   char        tag[tagcc], catg[tagcc];
   int         err, changed = 0;

   if (strmatch(event,"enter")) zd->zstat = 2;                                   //  [done]
   if (strmatch(event,"escape")) zd->zstat = 3;                                  //  escape = cancel                    15.07

   if (zd->zstat)
   {
      if (zd->zstat == 1) {                                                      //  report orphan tags
         zd->zstat = 0;                                                          //  keep dialog active
         tag_orphans();
      }

      else {                                                                     //  done or [x]
         zdialog_free(zd);
         if (zdeditmeta) zdialog_show(zdeditmeta,1);                             //  reinstate parent dialog
         zdmanagetags = 0;
         return 0;
      }
   }

   if (strmatch(event,"create")) {                                               //  add new tag to defined tags
      zdialog_fetch(zd,"catg",catg,tagcc);
      zdialog_fetch(zd,"tag",tag,tagcc);
      err = add_deftag(catg,tag);
      if (! err) changed++;
   }

   if (strmatch(event,"delete")) {                                               //  remove tag from defined tags
      zdialog_fetch(zd,"tag",tag,tagcc);
      del_deftag(tag);
      changed++;
   }

   if (changed) {
      save_deftags();                                                            //  save tag updates to file
      deftags_stuff(zd,"ALL");                                                   //  update dialog "deftags" window
      if (zdeditmeta)                                                            //  and edit metadata dialog if active
         deftags_stuff(zdeditmeta,"ALL");
      if (zdbatchtags)                                                           //  and batch tags dialog if active
         deftags_stuff(zdbatchtags,"ALL");
   }

   return 0;
}


/********************************************************************************/

//  edit EXIF/IPTC data - add or change specified EXIF/IPTC/etc. key

void m_meta_edit_any(GtkWidget *, cchar *menu)                                   //  overhauled                         16.01
{
   int   meta_edit_any_dialog_event(zdialog *zd, cchar *event);
   void  meta_edit_any_clickfunc(GtkWidget *, int line, int pos);

   zdialog     *zd;
   GtkWidget   *mtext;
   char        filename[200], buff[100];
   cchar       *pp1[1];
   char        *pp2[1], *pp;
   FILE        *fid;

   F1_help_topic = "edit_any_metadata";

   if (clicked_file) {                                                           //  use clicked file if present
      f_open(clicked_file,0,0,1,0);
      clicked_file = 0;
   }
   
   if (! curr_file) {
      if (zdexifedit) zdialog_free(zdexifedit);
      zdexifedit = 0;
      return;
   }
   
   if (FGWM == 'G') gallery(curr_file,"paint");                                  //  if gallery view, repaint           16.06

/***
       _____________________________________________________
      |  Click to Select   | Image File: filename.jpg       |
      |                    |                                |
      |  (metadata list)   | key name [___________________] |
      |                    | key value [__________________] |
      |                    |                                |
      |                    |                                |
      |                    |                                |
      |                    |      [Full List] [Save] [Done] |
      |____________________|________________________________|

***/

   if (! zdexifedit)                                                             //  popup dialog if not already
   {
      zd = zdialog_new(ZTX("Edit Any Metadata"),Mwin,ZTX("Full List"),Bsave,Bdone,null);
      zdexifedit = zd;

      zdialog_add_widget(zd,"hbox","hb1","dialog",0,"expand");
      zdialog_add_widget(zd,"vbox","vb1","hb1",0,"expand|space=3");
      zdialog_add_widget(zd,"label","lab1","vb1",ZTX("click to select"),"size=25");
      zdialog_add_widget(zd,"scrwin","scroll","vb1",0,"expand");
      zdialog_add_widget(zd,"text","mtext","scroll",0,"expand");
      zdialog_add_widget(zd,"vbox","vb2","hb1",0,"expand|space=3");
      zdialog_add_widget(zd,"hbox","hbf","vb2",0,"space=6");                     //  16.06
      zdialog_add_widget(zd,"label","labf","hbf",ZTX("Image File:"),"space=3");
      zdialog_add_widget(zd,"label","file","hbf","filename.jpg","space=5");
      zdialog_add_widget(zd,"hbox","hbkey","vb2",0,"space=2");
      zdialog_add_widget(zd,"hbox","hbdata","vb2",0,"space=2");
      zdialog_add_widget(zd,"label","labkey","hbkey",ZTX("key name"));
      zdialog_add_widget(zd,"entry","keyname","hbkey");
      zdialog_add_widget(zd,"label","labdata","hbdata",ZTX("key value"));
      zdialog_add_widget(zd,"entry","keydata","hbdata",0,"expand");

      zdialog_resize(zd,600,300);
      zdialog_run(zd,meta_edit_any_dialog_event);

      mtext = zdialog_widget(zd,"mtext");                                        //  make clickable metadata list
      wclear(mtext);

      snprintf(filename,200,"%s/metadata_short_list",get_zuserdir());
      fid = fopen(filename,"r");
      if (! fid) {
         zmessageACK(Mwin,"%s \n %s",filename,strerror(errno));
         return;
      }

      while (true) {
         pp = fgets_trim(buff,100,fid,1);
         if (! pp) break;
         if (*pp <= ' ') continue;                                               //  16.02
         wprintf(mtext,"%s \n",buff);
      }
      fclose(fid);

      wscroll(mtext,1);
      textwidget_set_clickfunc(mtext,meta_edit_any_clickfunc);
      
      *keyname = 0;
   }

   zd = zdexifedit;

   pp = strrchr(curr_file,'/');                                                  //  stuff file name in dialog          16.06
   if (pp) zdialog_stuff(zd,"file",pp+1);

   if (*keyname)                                                                 //  update live dialog
   {
      pp1[0] = keyname;                                                          //  look for key data
      exif_get(curr_file,pp1,pp2,1);
      if (pp2[0]) {
         strncpy0(keydata,pp2[0],exif_maxcc);
         zfree(pp2[0]);
      }
      else *keydata = 0;
      zdialog_stuff(zd,"keydata",keydata);                                       //  stuff into dialog
   }

   if (FGWM == 'F') gtk_window_present(MWIN);                                    //  keep focus on main window          16.06
   return;
}


//  dialog event and completion callback function

int meta_edit_any_dialog_event(zdialog *zd, cchar *event)
{
   cchar    *pp1[1];
   char     *pp2[1];
   int      err;
   
   if (strmatch(event,"escape")) zd->zstat = 3;                                  //  escape = cancel
   
   if (! curr_file) return 1;

   if (strmatch(event,"enter"))                                                  //  accept entered key name            16.06
   {
      zd->zstat = 0;                                                             //  keep dialog active

      zdialog_fetch(zd,"keyname",keyname,40);                                    //  get key name from dialog
      strCompress(keyname);
      pp1[0] = keyname;                                                          //  look for key data
      exif_get(curr_file,pp1,pp2,1);
      if (pp2[0]) {
         strncpy0(keydata,pp2[0],exif_maxcc);
         zfree(pp2[0]);
      }
      else *keydata = 0;
      zdialog_stuff(zd,"keydata",keydata);                                       //  stuff into dialog

      if (FGWM == 'F') gtk_window_present(MWIN);                                 //  keep focus on main window
   }

   if (! zd->zstat) return 1;                                                    //  wait for completion

   if (zd->zstat == 1) {                                                         //  show full list
      zd->zstat = 0;                                                             //  keep dialog active
      zmessageACK(Mwin,ZTX("The command: $ man Image::ExifTool::TagNames \n"
                           "will show over 15000 \"standard\" tag names"));
   }

   else if (zd->zstat == 2)                                                      //  save
   {
      zd->zstat = 0;                                                             //  keep dialog active
      zdialog_fetch(zd,"keyname",keyname,40);                                    //  get key name from dialog
      zdialog_fetch(zd,"keydata",keydata,exif_maxcc); 
      strCompress(keyname);                                                      //  bugfix                             16.06
      pp1[0] = keyname;
      pp2[0] = keydata;
      err = exif_put(curr_file,pp1,(cchar **) pp2,1);                            //  change metadata in image file
      if (err) zmessageACK(Mwin,"error: %s",strerror(err));
      load_filemeta(curr_file);                                                  //  update image index in case 
      update_image_index(curr_file);                                             //    searchable metadata item updated
      if (zdexifview) meta_view(0);                                              //  update exif view if active
   }

   else {
      zdialog_free(zd);                                                          //  done or cancel
      zdexifedit = 0;
   }

   if (FGWM == 'F') gtk_window_present(MWIN);                                    //  keep focus on main window
   return 1;
}


//  get clicked tag name from short list and insert into dialog

void meta_edit_any_clickfunc(GtkWidget *widget, int line, int pos) 
{
   char        *pp, *pp2[1];
   cchar       *pp1[1];
   
   if (! zdexifedit) return;
   if (! curr_file) return;

   pp = textwidget_get_line(widget,line,1);                                      //  get clicked line, highlight
   if (! pp) return;                                                             //  16.02
   zdialog_stuff(zdexifedit,"keyname",pp);

   zdialog_fetch(zdexifedit,"keyname",keyname,40);                               //  get key name from dialog
   strCompress(keyname);

   pp1[0] = keyname;                                                             //  look for key data
   exif_get(curr_file,pp1,pp2,1);
   if (pp2[0]) {
      strncpy0(keydata,pp2[0],exif_maxcc);
      zfree(pp2[0]);
   }
   else *keydata = 0;
   zdialog_stuff(zdexifedit,"keydata",keydata);                                  //  stuff into dialog

   if (FGWM == 'F') gtk_window_present(MWIN);                                    //  keep focus on main window
   return;
}


/********************************************************************************/

//  delete EXIF/IPTC data, specific key or all data

void m_meta_delete(GtkWidget *, cchar *menu)
{
   int   meta_delete_dialog_event(zdialog *zd, cchar *event);

   zdialog     *zd;

   F1_help_topic = "delete_metadata";
   if (! curr_file) return;

   zd = zdialog_new(ZTX("Delete Metadata"),Mwin,Bapply,Bcancel,null);
   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=5");
   zdialog_add_widget(zd,"radio","kall","hb1",ZTX("All"),"space=5");
   zdialog_add_widget(zd,"radio","key1","hb1",ZTX("One Key:"));
   zdialog_add_widget(zd,"entry","keyname","hb1",0,"size=20");
   zdialog_stuff(zd,"key1",1);

   zdialog_run(zd,meta_delete_dialog_event);
   return;
}


//  dialog event and completion callback function

int meta_delete_dialog_event(zdialog *zd, cchar *event)
{
   int         kall, key1;
   char        keyname[40];

   if (strmatch(event,"escape")) zd->zstat = 2;                                  //  escape = cancel                    15.07

   if (! zd->zstat) return 1;                                                    //  wait for completion

   if (zd->zstat != 1 || ! curr_file) {                                          //  canceled
      zdialog_free(zd);
      return 1;
   }

   zd->zstat = 0;                                                                //  dialog remains active
   gtk_window_present(MWIN);                                                     //  keep focus on main window

   zdialog_fetch(zd,"kall",kall);
   zdialog_fetch(zd,"key1",key1);
   zdialog_fetch(zd,"keyname",keyname,40);
   strCompress(keyname);

   if (kall)                                                                     //  update file metadata
      shell_ack("exiftool -m -q -overwrite_original -all=  \"%s\"",curr_file);
   else if (key1)
      shell_ack("exiftool -m -q -overwrite_original -%s=  \"%s\"",keyname,curr_file);
   else return 1;

   load_filemeta(curr_file);                                                     //  update image index in case a       15.03
   update_image_index(curr_file);                                                //    searchable metadata deleted

   if (zdexifview) meta_view(0);                                                 //  update exif view if active
   return 1;
}


/********************************************************************************/

//  menu function - add and remove tags for many files at once

namespace batchtags
{
   char        **filelist = 0;                                                   //  files to process
   int         filecount = 0;                                                    //  file count
   char        addtags[tagMcc];                                                  //  tags to add, list
   char        deltags[tagMcc];                                                  //  tags to remove, list
}


void m_batch_tags(GtkWidget *, cchar *)                                          //  combine batch add/del tags
{
   using namespace batchtags;

   void  batch_addtags_clickfunc(GtkWidget *widget, int line, int pos);
   void  batch_deltags_clickfunc(GtkWidget *widget, int line, int pos);
   void  batch_deftags_clickfunc(GtkWidget *widget, int line, int pos);
   int   batch_tags_dialog_event(zdialog *zd, cchar *event);

   char        *ptag, *file;
   int         zstat, ii, jj, err;
   zdialog     *zd;
   GtkWidget   *widget;

   F1_help_topic = "batch_tags";

   if (checkpend("all")) return;                                                 //  check nothing pending
   Fblock = 1;

/***
          ________________________________________________________
         |           Batch Add/Remove Tags                        |
         |                                                        |
         |  [Select Files]  NN files selected                     |
         |                                                        |
         |  (o) tags to add    [_________________________]        |
         |  (o) tags to remove [_________________________]        |
         |                                                        |
         |  Defined Tags Category [________________________|v]    |
         |   ___________________________________________________  |
         |  |                                                   | |
         |  |                                                   | |
         |  |                                                   | |
         |  |                                                   | |
         |  |                                                   | |
         |  |                                                   | |
         |  |___________________________________________________| |
         |                                                        |
         |                      [Manage Tags] [Proceed] [Cancel]  |
         |________________________________________________________|

***/

   zd = zdialog_new(ZTX("Batch Add/Remove Tags"),Mwin,Bmanagetags,Bproceed,Bcancel,null);
   zdbatchtags = zd;

   zdialog_add_widget(zd,"hbox","hbfiles","dialog",0,"space=3");
   zdialog_add_widget(zd,"button","files","hbfiles",Bselectfiles,"space=5");
   zdialog_add_widget(zd,"label","labcount","hbfiles",Bnofileselected,"space=10");

   zdialog_add_widget(zd,"hbox","hbtags","dialog",0,"space=3");
   zdialog_add_widget(zd,"vbox","vb1","hbtags",0,"space=3|homog");
   zdialog_add_widget(zd,"vbox","vb2","hbtags",0,"space=3|homog|expand");

   zdialog_add_widget(zd,"radio","radadd","vb1",ZTX("tags to add"));
   zdialog_add_widget(zd,"radio","raddel","vb1",ZTX("tags to remove"));

   zdialog_add_widget(zd,"frame","fradd","vb2",0,"expand");
   zdialog_add_widget(zd,"text","addtags","fradd",0,"expand|wrap");
   zdialog_add_widget(zd,"frame","frdel","vb2",0,"expand");
   zdialog_add_widget(zd,"text","deltags","frdel",0,"expand|wrap");

   zdialog_add_widget(zd,"hsep","sep","dialog",0,"space=3");

   zdialog_add_widget(zd,"hbox","space","dialog");
   zdialog_add_widget(zd,"hbox","hbdt1","dialog");
   zdialog_add_widget(zd,"label","labdt","hbdt1",ZTX("Defined Tags Category"),"space=3");
   zdialog_add_widget(zd,"combo","defcats","hbdt1",0,"expand|space=5");
   zdialog_add_widget(zd,"hbox","hbdt2","dialog",0,"expand");
   zdialog_add_widget(zd,"frame","frdt2","hbdt2",0,"expand|space=3");
   zdialog_add_widget(zd,"scrwin","swdt2","frdt2",0,"expand");
   zdialog_add_widget(zd,"text","deftags","swdt2",0,"wrap");

   zdialog_stuff(zd,"radadd",1);
   zdialog_stuff(zd,"raddel",0);

   load_deftags();                                                               //  stuff defined tags into dialog
   deftags_stuff(zd,"ALL");
   defcats_stuff(zd);                                                            //  and defined categories             15.08

   filelist = 0;
   filecount = 0;
   *addtags = *deltags = 0;

   widget = zdialog_widget(zd,"addtags");                                        //  tag widget mouse functions
   textwidget_set_clickfunc(widget,batch_addtags_clickfunc);

   widget = zdialog_widget(zd,"deltags");
   textwidget_set_clickfunc(widget,batch_deltags_clickfunc);

   widget = zdialog_widget(zd,"deftags");
   textwidget_set_clickfunc(widget,batch_deftags_clickfunc);

   zdialog_resize(zd,500,400);                                                   //  run dialog
   zdialog_run(zd,batch_tags_dialog_event);
   zstat = zdialog_wait(zd);                                                     //  wait for dialog completion
   zdialog_free(zd);                                                             //  kill dialog

   zdbatchtags = 0;

   if (zstat != 2)                                                               //  cancel
   {
      if (filecount) {
         for (ii = 0; filelist[ii]; ii++)
            zfree(filelist[ii]);
         zfree(filelist);
         filecount = 0;
      }
      Fblock = 0;
      return;
   }

   write_popup_text("open","Batch Tags",500,200,Mwin);                           //  status monitor popup window

   for (ii = 0; filelist[ii]; ii++)                                              //  loop all selected files
   {
      zmainloop();                                                               //  keep GTK alive                     16.04

      file = filelist[ii];                                                       //  display image
      err = f_open(file,0,0,0);
      if (err) continue;

      write_popup_text("write",file);                                            //  report progress

      for (jj = 1; ; jj++)                                                       //  remove tags if present
      {
         ptag = (char *) strField(deltags,",;",jj);
         if (! ptag) break;
         if (*ptag == ' ') continue;
         err = del_tag(ptag,meta_tags);
         if (err) continue;
      }

      for (jj = 1; ; jj++)                                                       //  add new tags unless already
      {
         ptag = (char *) strField(addtags,",;",jj);
         if (! ptag) break;
         if (*ptag == ' ') continue;
         err = add_tag(ptag,meta_tags,tagFcc);
         if (err == 2) {
            zmessageACK(Mwin,ZTX("%s \n too many tags"),file);
            break;
         }
      }

      save_filemeta(file);                                                       //  save tag changes
   }

   write_popup_text("write",Bcompleted);

   for (ii = 0; filelist[ii]; ii++)
      zfree(filelist[ii]);
   zfree(filelist);

   Fblock = 0;
   return;
}


//  mouse click functions for widgets holding tags

void batch_addtags_clickfunc(GtkWidget *widget, int line, int pos)               //  a tag in the add list was clicked
{
   using namespace batchtags;

   char     *txline, *txtag, end;

   txline = textwidget_get_line(widget,line,0);
   if (! txline) return;

   txtag = textwidget_get_word(txline,pos,",;",end);
   if (! txtag) { zfree(txline); return; }

   del_tag(txtag,addtags);                                                       //  remove tag from list
   zdialog_stuff(zdbatchtags,"addtags",addtags);

   zfree(txline);
   zfree(txtag);
   return;
}


void batch_deltags_clickfunc(GtkWidget *widget, int line, int pos)               //  a tag in the remove list was clicked
{
   using namespace batchtags;

   char     *txline, *txtag, end;

   txline = textwidget_get_line(widget,line,0);
   if (! txline) return;

   txtag = textwidget_get_word(txline,pos,",;",end);
   if (! txtag) { zfree(txline); return; }

   del_tag(txtag,deltags);                                                       //  remove tag from list
   zdialog_stuff(zdbatchtags,"deltags",deltags);

   zfree(txline);
   zfree(txtag);
   return;
}


void batch_deftags_clickfunc(GtkWidget *widget, int line, int pos)               //  a defined tag was clicked
{
   using namespace batchtags;

   char     *txline, *txtag, end;
   int      radadd;

   txline = textwidget_get_line(widget,line,0);
   if (! txline) return;

   txtag = textwidget_get_word(txline,pos,",;:",end);
   if (! txtag || end == ':') { zfree(txline); return; }                         //  tag category clicked, ignore       15.5

   zdialog_fetch(zdbatchtags,"radadd",radadd);                                   //  which radio button?

   if (radadd) {
      add_tag(txtag,addtags,tagMcc);                                             //  add defined tag to tag add list
      zdialog_stuff(zdbatchtags,"addtags",addtags);
   }
   else {
      add_tag(txtag,deltags,tagMcc);                                             //  add defined tag to tag remove list
      zdialog_stuff(zdbatchtags,"deltags",deltags);
   }

   zfree(txline);
   zfree(txtag);
   return;
}


//  batchTags dialog event function

int batch_tags_dialog_event(zdialog *zd, cchar *event)
{
   using namespace batchtags;

   int      ii;
   char     countmess[50], catgname[tagcc];

   if (strmatch(event,"escape")) zd->zstat = 3;                                  //  escape = cancel                    15.07

   if (zd->zstat)
   {
      if (zd->zstat == 1) {                                                      //  manage tags
         zd->zstat = 0;                                                          //  keep dialog active
         zdialog_show(zd,0);                                                     //  hide parent dialog
         manage_tags();
         zdialog_show(zd,1);
      }

      if (zd->zstat == 2) {                                                      //  proceed
         if (! filecount || (*addtags <= ' ' && *deltags <= ' ')) {
            zmessageACK(Mwin,ZTX("specify files and tags"));
            zd->zstat = 0;                                                       //  keep dialog active
         }
      }

      return 1;                                                                  //  cancel
   }

   if (strmatch(event,"files"))                                                  //  select images to process
   {
      if (filelist) {                                                            //  free prior list
         for (ii = 0; filelist[ii]; ii++)
            zfree(filelist[ii]);
         zfree(filelist);
      }

      zdialog_show(zd,0);                                                        //  hide parent dialog
      filelist = gallery_getfiles();                                             //  get file list from user
      zdialog_show(zd,1);

      if (filelist)                                                              //  count files in list
         for (ii = 0; filelist[ii]; ii++);
      else ii = 0;
      filecount = ii;

      snprintf(countmess,50,Bfileselected,filecount);
      zdialog_stuff(zd,"labcount",countmess);
   }

   if (strmatch(event,"defcats")) {                                              //  new tag category selection         15.08
      zdialog_fetch(zd,"defcats",catgname,tagcc);
      deftags_stuff(zd,catgname);
   }

   return 1;
}


/********************************************************************************/

//  Convert tag names for all image files, based on a list 
//    of old tag names and corresponding new tag names.

void m_batch_rename_tags(GtkWidget *, cchar *menu)                               //  15.07
{
   char     *tagfile, buff[200], logrec[200];
   char     *taglist[maxtags][2], *filelist[maximages];
   char     *oldtaglist[100], *newtaglist[100];
   char     *pp, *file;
   char     *oldtag, *newtag, *filetag;
   int      ntags = 0, nfiles = 0, nlist;
   int      err, ii, jj, kk, ff, yn, ftf = 1;
   FILE     *fid = 0;
   sxrec_t  sxrec;

   F1_help_topic = "batch_rename_tags";

   if (checkpend("all")) return;                                                 //  check nothing pending
   Fblock = 1;

   tagfile = zgetfile(ZTX("tag names file"),MWIN,"file",getenv("HOME"));         //  get tag names file from user
   if (! tagfile) {
      Fblock = 0;
      return;
   }

   fid = fopen(tagfile,"r");                                                     //  open file
   if (! fid) {
      zmessageACK(Mwin,strerror(errno));
      Fblock = 0;
      return;
   }

   write_popup_text("open","batch rename tags",600,400,Mwin);

   while (true)                                                                  //  read old and new tag names
   {
      pp = fgets_trim(buff,200,fid,1);
      if (! pp) break;
      if (blank_null(pp)) continue;
      oldtag = (char *) strField(buff,',',1);
      if (! oldtag) continue;
      taglist[ntags][0] = zstrdup(oldtag);                                       //  tag to replace
      newtag = (char *) strField(buff,',',2);
      if (newtag) taglist[ntags][1] = zstrdup(newtag);                           //  replacement tag
      else taglist[ntags][1] = 0;                                                //  none - tag will be deleted
      snprintf(logrec,200,"old tag: %s  new tag: %s",oldtag,newtag);
      write_popup_text("write",logrec);
      ntags++;
      if (ntags == maxtags) {
         zmessageACK(Mwin,"max tags exceeded: %d",maxtags);
         goto cleanup;
      }
   }
   
   fclose(fid);
   fid = 0;
   
   while (true)                                                                  //  read image index file
   {
      zmainloop();                                                               //  keep GTK alive                     16.04

      err = read_sxrec_seq(sxrec,ftf);                                           //  loop each image file
      if (err) break;
      ff = 0;
      if (sxrec.tags) {                                                          //  search for tags to rename
         for (ii = 1; ; ii++) {
            pp = (char *) strField(sxrec.tags,',',ii);
            if (! pp) break;
            if (strmatch(pp,"null")) continue;
            for (jj = 0; jj < ntags; jj++) {
               if (strmatchcase(pp,taglist[jj][0])) {                            //  this file has one or more tags
                  ff = 1;                                                        //    that will be renamed
                  break;
               }
            }
            if (ff) break;
         }
      }
      
      if (ff) {
         filelist[nfiles] = zstrdup(sxrec.file);                                 //  add to list of files to process
         nfiles++;
         snprintf(logrec,200,"file included: %s",sxrec.file);
         write_popup_text("write",logrec);
      }

      zfree(sxrec.file);                                                         //  free index rec. memory
      if (sxrec.tags) zfree(sxrec.tags);
      if (sxrec.capt) zfree(sxrec.capt);
      if (sxrec.comms) zfree(sxrec.comms);
      if (sxrec.gtags) zfree(sxrec.gtags);
   }
   
   yn = zmessageYN(Mwin,ZTX("%d tags to rename \n"
                            "in %d image files. \n"
                            "Proceed?"),ntags,nfiles);
   if (! yn) goto cleanup;

   for (ii = 0; ii < nfiles; ii++)                                               //  loop all selected files
   {
      zmainloop();                                                               //  keep GTK alive                     16.04

      file = filelist[ii];                                                       //  open image file
      err = f_open(file,0,0,0);
      if (err) continue;

      write_popup_text("write",file);                                            //  report progress

      nlist = 0;

      for (jj = 1; ; jj++) {                                                     //  loop file tags
         filetag = (char *) strField(meta_tags,',',jj);
         if (! filetag) break;
         for (kk = 0; kk < ntags; kk++) {                                        //  loop tag replacement list
            oldtag = taglist[kk][0];
            newtag = taglist[kk][1];
            if (strmatchcase(filetag,oldtag)) {                                  //  file tag matches tag to replace
               oldtaglist[nlist] = oldtag;                                       //  save old and new tags 
               newtaglist[nlist] = newtag;
               nlist++;
               break;                                                            //  next file tag
            }
         }
      }

      for (jj = 0; jj < nlist; jj++)                                             //  loop tags to remove
         err = del_tag(oldtaglist[jj],meta_tags);

      for (jj = 0; jj < nlist; jj++) {                                           //  loop tags to add
         if (! newtaglist[jj]) continue;                                         //  must be after removals
         write_popup_text("write",newtaglist[jj]);
         err = add_tag(newtaglist[jj],meta_tags,tagFcc);
         if (err && err != 1) write_popup_text("write","ERROR");                 //  ignore if already there
      }

      save_filemeta(file);                                                       //  save tag changes
   }

   write_popup_text("write",Bcompleted);

cleanup:                                                                         //  free resources

   Fblock = 0;

   if (fid) fclose(fid);
   
   for (ii = 0; ii < ntags; ii++) {
      zfree(taglist[ii][0]);
      if (taglist[ii][1]) zfree(taglist[ii][1]);
   }
   
   for (ii = 0; ii < nfiles; ii++)
      zfree(filelist[ii]);
      
   return;
}


/********************************************************************************/

//  batch add or change any EXIF/IPTC metadata

namespace batchchangemeta
{
   char        **filelist = 0;                                                   //  files to process
   int         filecount = 0;                                                    //  file count
   zdialog     *zd;
}


//  menu function

void m_batch_change_metadata(GtkWidget *, cchar *menu)                           //  overhauled                         16.01
{
   using namespace batchchangemeta;

   int  batch_change_metadata_dialog_event(zdialog *zd, cchar *event);
   void batch_change_metadata_clickfunc(GtkWidget *,int line, int pos);

   int         ii, jj, err, zstat, nkeys;
   char        keynameN[12] = "keynameN", keyvalueN[12] = "keyvalueN";
   char        keyname[40], keyvalue[exif_maxcc];
   cchar       *pp1[5], *pp2[5];
   char        *file, *pp, text[200];
   GtkWidget   *mtext;
   char        filename[200], buff[100];
   FILE        *fid;

   F1_help_topic = "batch_change_metadata";
   if (checkpend("all")) return;                                                 //  check nothing pending
   Fblock = 1;

/**
       _________________________________________________________________
      |  Click to Select   |           Batch Change Metadata            |
      |                    |                                            |
      |  (metadata list)   |  [Select Files]  NN files selected         |
      |                    |                                            |
      |                    |     key name           key value           |        
      |                    |  [______________]  [_____________________] |
      |                    |  [______________]  [_____________________] |
      |                    |  [______________]  [_____________________] |
      |                    |  [______________]  [_____________________] |
      |                    |  [______________]  [_____________________] |
      |                    |                                            |
      |                    |               [Full List] [apply] [cancel] |
      |____________________|____________________________________________|

**/

   zd = zdialog_new(ZTX("Batch Change Metadata"),Mwin,ZTX("Full List"),Bapply,Bcancel,null);
   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"expand");
   zdialog_add_widget(zd,"vbox","vb1","hb1");
   zdialog_add_widget(zd,"vbox","vb2","hb1",0,"expand|space=5");

   zdialog_add_widget(zd,"label","lab1","vb1",ZTX("click to select"),"size=25");
   zdialog_add_widget(zd,"scrwin","scroll","vb1",0,"expand");
   zdialog_add_widget(zd,"text","mtext","scroll",0,"expand");

   zdialog_add_widget(zd,"hbox","hbfiles","vb2",0,"space=3");
   zdialog_add_widget(zd,"button","files","hbfiles",Bselectfiles,"space=5");
   zdialog_add_widget(zd,"label","labcount","hbfiles",Bnofileselected,"space=10");
   
   zdialog_add_widget(zd,"hbox","hbkeys","vb2",0,"space=5");
   zdialog_add_widget(zd,"vbox","vbname","hbkeys");
   zdialog_add_widget(zd,"vbox","vbvalue","hbkeys",0,"expand");
   zdialog_add_widget(zd,"label","labkey","vbname",ZTX("key name"));
   zdialog_add_widget(zd,"label","labdata","vbvalue",ZTX("key value"));
   zdialog_add_widget(zd,"entry","keyname0","vbname",0,"size=25");
   zdialog_add_widget(zd,"entry","keyname1","vbname",0,"size=25");
   zdialog_add_widget(zd,"entry","keyname2","vbname",0,"size=25");
   zdialog_add_widget(zd,"entry","keyname3","vbname",0,"size=25");
   zdialog_add_widget(zd,"entry","keyname4","vbname",0,"size=25");
   zdialog_add_widget(zd,"entry","keyvalue0","vbvalue",0,"expand");
   zdialog_add_widget(zd,"entry","keyvalue1","vbvalue",0,"expand");
   zdialog_add_widget(zd,"entry","keyvalue2","vbvalue",0,"expand");
   zdialog_add_widget(zd,"entry","keyvalue3","vbvalue",0,"expand");
   zdialog_add_widget(zd,"entry","keyvalue4","vbvalue",0,"expand");
   zdialog_resize(zd,700,300);
   
   mtext = zdialog_widget(zd,"mtext");                                           //  make clickable metadata list
   wclear(mtext);

   snprintf(filename,200,"%s/metadata_short_list",get_zuserdir());
   fid = fopen(filename,"r");
   if (! fid) {
      zmessageACK(Mwin,"%s \n %s",filename,strerror(errno));
      return;
   }

   while (true) {
      pp = fgets_trim(buff,100,fid,1);
      if (! pp) break;
      wprintf(mtext,"%s \n",buff);
   }
   fclose(fid);

   wscroll(mtext,1);
   textwidget_set_clickfunc(mtext,batch_change_metadata_clickfunc);

   filelist = 0;
   nkeys = 0;

retry:

   zstat = zdialog_run(zd,batch_change_metadata_dialog_event);                   //  run dialog
   zstat = zdialog_wait(zd);                                                     //  wait for completion
   if (zstat != 2) goto cleanup;                                                 //  not [apply]
   
   for (ii = jj = 0; ii < 5; ii++)
   {
      keynameN[7] = '0' + ii;
      keyvalueN[8] = '0' + ii;
      zdialog_fetch(zd,keynameN,keyname,40);
      zdialog_fetch(zd,keyvalueN,keyvalue,exif_maxcc);
      strCompress(keyname);
      if (*keyname <= ' ') continue;
      pp1[jj] = zstrdup(keyname);
      pp2[jj] = zstrdup(keyvalue);
      jj++;
   }
   nkeys = jj;

   if (nkeys == 0) {
      zmessageACK(Mwin,ZTX("enter key names"));
      zfuncs::zdialog_busy--;
      goto retry;
   }
      
   if (filelist == 0) {
      zmessageACK(Mwin,ZTX("no files selected"));
      zfuncs::zdialog_busy--;
      goto retry;
   }
   
   write_popup_text("open","Batch Metadata",500,200,Mwin);                       //  status monitor popup window
   
   for (ii = 0; ii < nkeys; ii++)
   {
      if (*pp2[ii]) snprintf(text,200,"%s = %s",pp1[ii],pp2[ii]);
      else snprintf(text,200,"%s = DELETED",pp1[ii]);
      write_popup_text("write",text);
   }
   
   ii = zdialog_choose(Mwin,Bproceed,Bproceed,Bcancel,null);
   if (ii != 1) {
      zfuncs::zdialog_busy--;
      goto retry;
   }

   zdialog_free(zd);
   zd = 0;

   for (ii = 0; filelist[ii]; ii++)                                              //  loop all selected files
   {
      zmainloop();                                                               //  keep GTK alive                     16.04

      file = filelist[ii];                                                       //  display image
      err = f_open(file,0,0,0);
      if (err) continue;

      write_popup_text("write",file);                                            //  report progress

      err = exif_put(curr_file,pp1,pp2,nkeys);                                   //  change metadata in image file

      load_filemeta(curr_file);                                                  //  update image index in case
      update_image_index(curr_file);                                             //    indexed metadata updated

      if (zdexifview) meta_view(0);                                              //  update exif view if active
   }

   write_popup_text("write",Bcompleted);

cleanup:

   if (zd) zdialog_free(zd);                                                     //  kill dialog
   zd = 0;

   for (ii = 0; ii < nkeys; ii++) {                                              //  free memory
      zfree((char *) pp1[ii]);
      zfree((char *) pp2[ii]);
   }
   nkeys = 0;

   if (filelist) {
      for (ii = 0; filelist[ii]; ii++)                                           //  free memory
         zfree(filelist[ii]);
      zfree(filelist);
   }
   filelist = 0;

   Fblock = 0;
   return;
}


//  dialog event and completion callback function

int  batch_change_metadata_dialog_event(zdialog *zd, cchar *event)
{
   using namespace batchchangemeta;

   int         ii;
   char        countmess[50];

   if (strmatch(event,"escape")) zd->zstat = 2;                                  //  escape = cancel
   
   if (zd->zstat == 1)                                                           //  full list
   {
      zd->zstat = 0;                                                             //  keep dialog active
      zmessageACK(Mwin,ZTX("The command: $ man Image::ExifTool::TagNames \n"
                           "will show over 15000 \"standard\" tag names"));
   }
   
   if (strmatch(event,"files"))                                                  //  select images to process
   {
      if (filelist) {                                                            //  free prior list
         for (ii = 0; filelist[ii]; ii++)
            zfree(filelist[ii]);
         zfree(filelist);
         filelist = 0;
      }

      zdialog_show(zd,0);                                                        //  hide parent dialog
      filelist = gallery_getfiles();                                             //  get file list from user
      zdialog_show(zd,1);

      if (filelist)                                                              //  count files in list
         for (ii = 0; filelist[ii]; ii++);
      else ii = 0;

      snprintf(countmess,50,Bfileselected,ii);
      zdialog_stuff(zd,"labcount",countmess);
   }
   
   return 1;
}


//  get clicked tag name from short list and insert into dialog

void batch_change_metadata_clickfunc(GtkWidget *widget, int line, int pos)
{
   using namespace batchchangemeta;

   int      ii;
   char     *pp;
   char     keynameX[12] = "keynameX";
   char     keyname[60];

   pp = textwidget_get_line(widget,line,1);                                      //  get clicked line, highlight
   if (! pp || *pp <= ' ') return;
   
   for (ii = 0; ii < 5; ii++) {                                                  //  find 1st empty dialog key name
      keynameX[7] = '0' + ii;
      zdialog_fetch(zd,keynameX,keyname,60);
      if (*keyname <= ' ') break;
   }
   
   if (ii < 5) zdialog_stuff(zd,keynameX,pp);
   return;
}


/********************************************************************************/

//  batch report metadata for all image files in current gallery

namespace batchreportmeta
{
   char     **filelist = 0;                                                      //  files to process
   int      filecount = 0;                                                       //  file count
   char     items_file[200];
}


//  menu function

void m_batch_report_metadata(GtkWidget *, cchar *menu)                           //  15.12
{
   using namespace batchreportmeta;

   int  batch_report_metadata_dialog_event(zdialog *zd, cchar *event);

   zdialog     *zd;
   char        buff[100];
   char        *file, *pp;
   FILE        *fid = 0;
   int         zstat, ff, ii, err;
   int         nkx = 0;
   char        report_file[200];
   char        *keynamex[NK], *keyvalx[NK];
   
   F1_help_topic = "batch_report_metadata";

   snprintf(items_file,200,"%s/metadata_report_items",get_zuserdir());           //  file for items to report

/***
          ____________________________________________
         |           Batch Report Metadata            |
         |                                            |
         |  [Select Files]  NN files selected         |
         |  [Edit] list of reported metadata items    |
         |                                            |
         |                         [proceed] [cancel] |
         |____________________________________________|

***/

   zd = zdialog_new(ZTX("Batch Report Metadata"),Mwin,Bproceed,Bcancel,null);

   zdialog_add_widget(zd,"hbox","hbfiles","dialog",0,"space=3");
   zdialog_add_widget(zd,"button","files","hbfiles",Bselectfiles,"space=5");
   zdialog_add_widget(zd,"label","labcount","hbfiles",Bnofileselected,"space=10");
   zdialog_add_widget(zd,"hbox","hbedit","dialog",0,"space=3");
   zdialog_add_widget(zd,"button","edit","hbedit",Bedit,"space=5");
   zdialog_add_widget(zd,"label","labedit","hbedit","list of reported metadata items","space=10");

   zstat = zdialog_run(zd,batch_report_metadata_dialog_event);                   //  run dialog
   zstat = zdialog_wait(zd);                                                     //  wait for completion
   if (zstat != 1) goto cleanup;                                                 //  cancel
   zdialog_free(zd);
   
   if (filecount == 0) {
      zmessageACK(Mwin,ZTX("no files selected"));
      return;
   }

   fid = fopen(items_file,"r"); 
   if (! fid) {
      zmessageACK(Mwin,"no metadata items to report");
      return;
   }

   for (nkx = ii = 0; ii < 20; ii++)                                             //  read items to report
   {
      pp = fgets_trim(buff,100,fid,1);
      if (! pp) break;
      strCompress(pp);
      if (*pp <= ' ') continue;
      if (strmatchN(pp,"enteritems",5)) continue;
      keynamex[nkx] = zstrdup(pp);
      nkx++;
   }
   fclose(fid);
   
   if (! nkx) {
      zmessageACK(Mwin,"no metadata items to report");
      return;
   }

   snprintf(report_file,200,"%s/metadata_report",get_zuserdir());                //  open output file
   fid = fopen(report_file,"w");
   if (! fid) goto filerror;

   for (ff = 0; ff < filecount; ff++)                                            //  loop selected files
   {
      zmainloop(20);                                                             //  keep GTK alive

      file = filelist[ff];

      fprintf(fid,"%-24s : %s \n","FileName",file);                              //  file name

      err = exif_get(file,(cchar **) keynamex,keyvalx,nkx);                      //  get all report items
      if (err) {
         zmessageACK(Mwin,"exif failure");
         goto cleanup;
      }

      for (ii = 0; ii < nkx; ii++)                                               //  output keyword names and values
         if (keyvalx[ii]) fprintf(fid,"%-24s : %s \n",keynamex[ii],keyvalx[ii]);

      for (ii = 0; ii < nkx; ii++)                                               //  free memory
         if (keyvalx[ii]) zfree(keyvalx[ii]);
      
      fprintf(fid,"\n");                                                         //  blank line separator
   }
   
   shell_quiet("xdg-open %s",report_file);

cleanup:
   if (fid) fclose(fid);
   for (ii = 0; ii < nkx; ii++)                                                  //  free memory
      zfree(keynamex[ii]);
   return;

filerror:
   zmessageACK(Mwin,"file error: %s",strerror(errno));
   goto cleanup;
}


//  dialog event and completion function

int  batch_report_metadata_dialog_event(zdialog *zd, cchar *event)
{
   using namespace batchreportmeta;

   STATB    statb;
   int      ii, err;
   char     countmess[50], command[200];
   
   if (zd->zstat) zdialog_destroy(zd);
   
   if (strmatch(event,"files"))                                                  //  select images to process
   {
      if (filelist) {                                                            //  free prior list
         for (ii = 0; filelist[ii]; ii++)
            zfree(filelist[ii]);
         zfree(filelist);
         filelist = 0;
      }

      zdialog_show(zd,0);                                                        //  hide parent dialog
      filelist = gallery_getfiles();                                             //  get file list from user
      zdialog_show(zd,1);

      if (filelist)                                                              //  count files in list
         for (ii = 0; filelist[ii]; ii++);
      else ii = 0;

      snprintf(countmess,50,Bfileselected,ii);
      zdialog_stuff(zd,"labcount",countmess);
      filecount = ii;
   }

   if (strmatch(event,"edit")) 
   {
      err = stat(items_file,&statb);
      if (err) shell_quiet("echo -n \"enter items to report\n\" > %s",items_file);
      
      snprintf(command,200,"xdg-open %s",items_file);
      err = system(command);
      if (err) zmessageACK(Mwin,strerror(errno));
   }
   
   return 1;
}


/********************************************************************************/

//  Convert pretty date/time format from "yyyy-mm-dd" and "hh:mm:ss" to "yyyymmdd" and "hhmmss".
//  Missing month or day ("yyyy" or "yyyy-mm") is replaced with "-01".
//  Missing seconds ("hh:mm") are replaced with zero ("hh:mm:00").
//  Output user message and return null if not valid.

char * pdate_metadate(cchar *pdate)                                              //  "yyyy-mm-dd" >> "yyyymmdd"
{
   int         monlim[12] = { 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
   int         cc, year, mon, day;
   char        pdate2[12];
   static char mdate[12];

   cc = strlen(pdate);
   if (cc > 10) goto badformat;

   strcpy(pdate2,pdate);

   if (cc == 4)                                                                  //  conv. "yyyy" to "yyyy-01-01"
      strcat(pdate2,"-01-01");
   else if (cc == 7)                                                             //  conv. "yyyy-mm" to "yyyy-mm-01"
      strcat(pdate2,"-01");

   if (strlen(pdate2) != 10) goto badformat;
   if (pdate2[4] != '-' || pdate2[7] != '-') goto badformat;

   year = atoi(pdate2);
   mon = atoi(pdate2+5);
   day = atoi(pdate2+8);

   if (year < 0 || year > 2999) goto baddate;
   if (mon < 1 || mon > 12) goto baddate;
   if (day < 1 || day > monlim[mon-1]) goto baddate;
   if (mon == 2 && day == 29 && (year % 4)) goto baddate;

   memcpy(mdate,pdate2,4);                                                       //  return "yyyymmdd"
   memcpy(mdate+4,pdate2+5,2);
   memcpy(mdate+6,pdate2+8,3);
   return mdate;

badformat:
   zmessageACK(Mwin,ZTX("date format is YYYY-MM-DD"));
   return 0;

baddate:
   zmessageACK(Mwin,ZTX("date is invalid"));
   return 0;
}


char * ptime_metatime(cchar *ptime)                                              //  "hh:mm:ss" >> "hhmmss"
{
   int         cc, hour, min, sec;
   char        ptime2[12];
   static char mtime[8];

   cc = strlen(ptime);
   if (cc > 8) goto badformat;

   strcpy(ptime2,ptime);

   if (cc == 5) strcat(ptime2,":00");                                            //  conv. "hh:mm" to "hh:mm:00"

   if (strlen(ptime2) != 8) goto badformat;
   if (ptime2[2] != ':' || ptime2[5] != ':') goto badformat;

   hour = atoi(ptime2);
   min = atoi(ptime2+3);
   sec = atoi(ptime2+6);
   if (hour < 0 || hour > 23) goto badtime;
   if (min < 0 || min > 59) goto badtime;
   if (sec < 0 || sec > 59) goto badtime;

   memcpy(mtime,ptime2,2);                                                       //  return "hhmmss"
   memcpy(mtime+2,ptime2+3,2);
   memcpy(mtime+4,ptime2+6,2);
   return mtime;

badformat:
   zmessageACK(Mwin,ZTX("time format is HH:MM [:SS]"));
   return 0;

badtime:
   zmessageACK(Mwin,ZTX("time is invalid"));
   return 0;
}


//  Convert metadata date/time "yyyymmddhhmmss" to pretty format "yyyy-mm-dd" and "hh:mm:ss"

void metadate_pdate(cchar *metadate, char *pdate, char *ptime)
{
   if (*metadate) {
      memcpy(pdate,metadate,4);                                                  //  yyyymmdd to yyyy-mm-dd
      memcpy(pdate+5,metadate+4,2);
      memcpy(pdate+8,metadate+6,2);
      pdate[4] = pdate[7] = '-';
      pdate[10] = 0;

      memcpy(ptime,metadate+8,2);                                                //  hhmmss to hh:mm:ss
      memcpy(ptime+3,metadate+10,2);
      ptime[2] = ':';
      ptime[5] = 0;
      if (metadate[12] > '0' || metadate[13] > '0') {                            //  append :ss only if not :00
         memcpy(ptime+6,metadate+12,2);
         ptime[5] = ':';
         ptime[8] = 0;
      }
   }
   else *pdate = *ptime = 0;                                                     //  missing
   return;
}


//  validate a date/time string formatted "yyyymmddhhmmss" (ss optional)
//  return 0 if bad, 1 if OK
//  valid year is 0000 to 2099

int datetimeOK(char *datetime)                                                   //  15.08
{
   int      monlim[12] = { 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
   int      year, mon, day, hour, min, sec;
   char     temp[8];
   
   if (strlen(datetime) < 12) return 0;
   strncpy0(temp,datetime,5);
   year = atoi(temp);
   strncpy0(temp,datetime+4,3);
   mon = atoi(temp);
   strncpy0(temp,datetime+6,3);
   day = atoi(temp);
   strncpy0(temp,datetime+8,3);
   hour = atoi(temp);
   strncpy0(temp,datetime+10,3);
   min = atoi(temp);
   if (strlen(datetime) == 14) {
      strncpy0(temp,datetime+12,3);
      sec = atoi(temp);
   }
   else sec = 0;

   if (year < 0 || year > 2099) return 0;
   if (mon < 1 || mon > 12) return 0;
   if (day < 1 || day > monlim[mon-1]) return 0;
   if (mon == 2 && day == 29 && (year % 4)) return 0;
   if (hour < 0 || hour > 23) return 0;
   if (min < 0 || min > 59) return 0;
   if (sec < 0 || sec > 59) return 0;

   return 1;
}


/********************************************************************************/

//  add input tag to output tag list if not already there and enough room
//  returns:   0 = added OK     1 = already there (case ignored)
//             2 = overflow     3 = bad tag name     4 = null tag

int add_tag(char *tag, char *taglist, int maxcc)
{
   char     *pp1, *pp2, tag1[tagcc];
   int      cc, cc1, cc2;

   if (! tag) return 4;
   strncpy0(tag1,tag,tagcc);                                                     //  remove leading and trailing blanks
   cc = strTrim2(tag1);
   if (! cc) return 4;
   if (utf8_check(tag1)) return 3;                                               //  look for bad characters
   if (strpbrk(tag1,",;:")) return 3;
   strcpy(tag,tag1);

   pp1 = taglist;
   cc1 = strlen(tag);

   while (true)                                                                  //  check if already in tag list
   {
      while (*pp1 == ' ' || *pp1 == ',') pp1++;
      if (! *pp1) break;
      pp2 = pp1 + 1;
      while (*pp2 && *pp2 != ',') pp2++;
      cc2 = pp2 - pp1;
      if (cc2 == cc1 && strmatchcaseN(tag,pp1,cc1)) return 1;
      pp1 = pp2;
   }

   cc2 = strlen(taglist);                                                        //  append to tag list if space enough
   if (cc1 + cc2 + 3 > maxcc) return 2;
   strcpy(taglist + cc2,tag);
   strcpy(taglist + cc2 + cc1, ", ");                                            //  add delimiter + space

   if (taglist == meta_tags) Fmetamod++;                                         //  image tags were changed

   return 0;
}


//  remove tag from taglist, if present
//  returns: 0 if found and deleted, otherwise 1

int del_tag(char *tag, char *taglist)
{
   int         ii, ftcc, atcc, found;
   char        *temptags;
   cchar       *pp;

   temptags = zstrdup(taglist);

   *taglist = 0;
   ftcc = found = 0;

   for (ii = 1; ; ii++)
   {
      pp = strField(temptags,",;",ii);                                           //  next tag
      if (! pp) {
         zfree(temptags);
         if (found && taglist == meta_tags) Fmetamod++;                          //  image tags were changed
         return 1-found;
      }
      if (*pp == ' ') continue;

      if (strmatchcase(pp,tag)) {                                                //  skip matching tag
         found = 1;
         continue;
      }

      atcc = strlen(pp);                                                         //  copy non-matching tag
      strcpy(taglist + ftcc, pp);
      ftcc += atcc;
      strcpy(taglist + ftcc, ", ");                                              //  + delim + blank
      ftcc += 2;
   }
}


//  add new tag to recent tags, if not already.
//  remove oldest to make space if needed.

int add_recentag(char *tag)
{
   int         err;
   char        *pp, temptags[tagRcc];

   err = add_tag(tag,tags_recentags,tagRcc);                                     //  add tag to recent tags

   while (err == 2)                                                              //  overflow
   {
      strncpy0(temptags,tags_recentags,tagRcc);                                  //  remove oldest to make room
      pp = strpbrk(temptags,",;");
      if (! pp) return 0;
      strcpy(tags_recentags,pp+2);                                               //  delimiter + blank before tag
      err = add_tag(tag,tags_recentags,tagRcc);
   }

   return 0;
}


/********************************************************************************/

//  Load tags_defined file into tags_deftags[ii] => category: tag1, tag2, ...
//  Read image_index recs. and add unmatched tags: => nocatg: tag1, tag2, ...

void load_deftags()
{
   int tags_Ucomp(cchar *tag1, cchar *tag2);

   static int  Floaded = 0;
   FILE *      fid;
   sxrec_t     sxrec;
   int         ii, jj, ntags, err, cc, tcc, ftf;
   int         ncats, catoverflow;
   int         nocat, nocatcc;
   char        tag[tagcc], catg[tagcc];
   char        tagsbuff[tagGcc];
   char        *pp1, *pp2;
   char        ptags[maxtags][tagcc];                                            //  10000 * 50 = 0.5 MB

   if (Floaded) return;                                                          //  use memory tags if already there
   Floaded++;

   for (ii = 0; ii < maxtagcats; ii++)                                           //  clean memory
      tags_deftags[ii] = 0;

   ncats = catoverflow = 0;

   fid = fopen(tags_defined_file,"r");                                           //  read tags_defined file
   if (fid) {
      while (true) {
         pp1 = fgets_trim(tagsbuff,tagGcc,fid);
         if (! pp1) break;
         pp2 = strchr(pp1,':');                                                  //  isolate "category:"
         if (! pp2) continue;                                                    //  no colon
         cc = pp2 - pp1 + 1;
         if (cc > tagcc-1) continue;                                             //  category name too long
         strncpy0(catg,pp1,cc);                                                  //  (for error message)
         if (strlen(pp1) > tagGcc-2) goto cattoobig;                             //  all category tags too long
         pp2++;
         while (*pp2 == ' ') pp2++;
         if (strlen(pp2) < 3) continue;                                          //  category with no tags
         while (*pp2) { 
            if (*pp2 == ';') *pp2 = ',';                                         //  replace ';' with ',' for Fotoxx    15.07
            pp2++; 
         }
         tags_deftags[ncats] = zstrdup(pp1);                                     //  tags_deftags[ii]
         ncats++;                                                                //   = category: tag1, tag2, ... tagN,
         if (ncats == maxtagcats-1) goto toomanycats;                            //  leave 1 for "nocatg"
      }
      err = fclose(fid);
      fid = 0;                                                                   //  15.07
      if (err) goto deftagsfilerr;
   }

   nocat = ncats;                                                                //  make last category "nocatg" for
   ncats++;                                                                      //   unmatched tags in image_index recs.
   tags_deftags[nocat] = (char *) zmalloc(tagGcc);
   strcpy(tags_deftags[nocat],"nocatg: ");
   nocatcc = 8;

   ftf = 1;                                                                      //  read all image index recs.

   while (true)
   {
      err = read_sxrec_seq(sxrec,ftf);
      if (err) break;

      pp1 = sxrec.tags;                                                          //  may be "null,"

      while (true)
      {
         while (*pp1 && strchr(",; ",*pp1)) pp1++;                               //  next image tag start
         if (! *pp1) break;
         pp2 = strpbrk(pp1,",;");                                                //  end
         if (! pp2) pp2 = pp1 + strlen(pp1);
         cc = pp2 - pp1;
         if (cc > tagcc-1) {
            pp1 = pp2;
            continue;                                                            //  ignore huge tag
         }

         strncpy0(tag,pp1,cc+1);                                                 //  look for tag in defined tags
         if (find_deftag(tag)) {
            pp1 = pp2;                                                           //  found
            continue;
         }

         if (nocatcc + cc + 2 > tagGcc-2) {
            catoverflow = 1;                                                     //  nocatg: length limit reached
            break;
         }
         else {
            strcpy(tags_deftags[nocat] + nocatcc, tag);                          //  append tag to list
            nocatcc += cc;
            strcpy(tags_deftags[nocat] + nocatcc, ", ");                         //  + delim + blank
            nocatcc += 2;
         }

         pp1 = pp2;
      }

      zfree(sxrec.file);
      zfree(sxrec.tags);
      zfree(sxrec.comms);
      zfree(sxrec.capt);
      zfree(sxrec.gtags);
   }

   if (catoverflow) goto cattoobig;

//  parse all the tags in each category and sort in ascending order

   for (ii = 0; ii < ncats; ii++)
   {
      pp1 = tags_deftags[ii];
      pp2 = strchr(pp1,':');
      cc = pp2 - pp1 + 1;
      strncpy0(catg,pp1,cc);
      pp1 = pp2 + 1;
      while (*pp1 == ' ') pp1++;
      tcc = 0;

      for (jj = 0; jj < maxtags; jj++)
      {
         if (! *pp1) break;
         pp2 = strchr(pp1,',');
         if (pp2) cc = pp2 - pp1;
         else cc = strlen(pp1);
         if (cc > tagcc-1) cc = tagcc-1;
         strncpy0(ptags[jj],pp1,cc+1);
         pp1 += cc + 1;
         tcc += cc;
         while (*pp1 == ' ') pp1++;
      }

      ntags = jj;
      if (ntags == maxtags) goto toomanytags;                                    //  15.07
      HeapSort((char *) ptags,tagcc,ntags,tags_Ucomp);

      pp1 = tags_deftags[ii];
      tcc += strlen(catg) + 2 + 2 * ntags + 2;                                   //  category, all tags, delimiters
      pp2 = (char *) zmalloc(tcc);

      tags_deftags[ii] = pp2;                                                    //  swap memory
      zfree(pp1);

      strcpy(pp2,catg);
      pp2 += strlen(catg);
      strcpy(pp2,": ");                                                          //  pp2 = "category: "
      pp2 += 2;

      for (jj = 0; jj < ntags; jj++)                                             //  add the sorted tags
      {
         strcpy(pp2,ptags[jj]);                                                  //  append tag + delim + blank
         pp2 += strlen(pp2);
         strcpy(pp2,", ");
         pp2 += 2;
      }

      *pp2 = 0;
   }

//  sort the categories in ascending order
//  leave "nocatg" at the end

   for (ii = 0; ii < ncats-1; ii++)
   for (jj = ii+1; jj < ncats-1; jj++)
   {
      pp1 = tags_deftags[ii];
      pp2 = tags_deftags[jj];
      if (strcasecmp(pp1,pp2) > 0) {
         tags_deftags[ii] = pp2;
         tags_deftags[jj] = pp1;
      }
   }

   return;

toomanycats:
   zmessageACK(Mwin,"more than %d categories",maxtagcats);
   if (fid) fclose(fid);
   return;

cattoobig:
   zmessageACK(Mwin,"category %s is too big",catg);
   if (fid) fclose(fid);
   return;

toomanytags:                                                                     //  15.07
   zmessageACK(Mwin,"category %s has too many tags",catg);
   if (fid) fclose(fid);
   return;

deftagsfilerr:
   zmessageACK(Mwin,"tags_defined file error: %s",strerror(errno));
   return;
}


//  compare function for tag sorting

int tags_Ucomp(cchar *tag1, cchar *tag2)
{
   return strcasecmp(tag1,tag2);
}


//  write tags_deftags[] memory data to the defined tags file if any changes were made

void save_deftags()
{
   int         ii, err;
   FILE        *fid;

   fid = fopen(tags_defined_file,"w");                                           //  write tags_defined file
   if (! fid) goto deftagserr;

   for (ii = 0; ii < maxtagcats; ii++)
   {
      if (! tags_deftags[ii+1]) break;                                           //  omit last category, "nocatg"
      err = fprintf(fid,"%s\n",tags_deftags[ii]);                                //  each record:
      if (err < 0) goto deftagserr;                                              //    category: tag1, tag2, ... tagN,
   }

   err = fclose(fid);
   if (err) goto deftagserr;
   return;

deftagserr:
   zmessageACK(Mwin,"tags_defined file error: %s",strerror(errno));
   return;
}


//  find a given tag in tags_deftags[]
//  return: 1 = found, 0 = not found

int find_deftag(char *tag)
{
   int      ii, cc;
   char     tag2[tagcc+4];
   char     *pp;

   strncpy0(tag2,tag,tagcc);                                                     //  construct tag + delim + blank
   cc = strlen(tag2);
   strcpy(tag2+cc,", ");
   cc += 2;

   for (ii = 0; ii < maxtagcats; ii++)
   {
      pp = tags_deftags[ii];                                                     //  category: tag1, tag2, ... tagN,
      if (! pp) return 0;                                                        //  not found

      while (pp)
      {
         pp = strcasestr(pp,tag2);                                               //  look for delim + blank + tag + delim
         if (! pp) break;
         if (strchr(",;:", pp[-2])) return 1;                                    //  cat: tag,  or  priortag, tag,
         pp += cc;                                                               //       |                   |
      }                                                                          //       pp                  pp
   }

   return 1;
}


//  add new tag to tags_deftags[] >> category: tag1, tag2, ... newtag,
//  returns:   0 = added OK     1 = not unique (case ignored)
//             2 = overflow     3 = bad name     4 = null/blank tag
//  if tag present under another category, it is moved to new category

int add_deftag(char *catg, char *tag)
{
   int         ii, cc, cc1, cc2;
   char        catg1[tagcc], tag1[tagcc];
   char        *pp1, *pp2;

   if (! catg) strcpy(catg1,"nocatg");                                           //  revised 15.05
   else strncpy0(catg1,catg,tagcc);
   cc = strTrim2(catg1);                                                         //  remove leading and trailing blanks
   if (! cc) strcpy(catg1,"nocatg");
   if (utf8_check(catg1)) goto badcatname;                                       //  look for bad characters
   if (strpbrk(catg1,",;:\"")) goto badcatname;

   if (! tag) return 4;
   strncpy0(tag1,tag,tagcc);                                                     //  remove leading and trailing blanks
   cc = strTrim2(tag1);
   if (! cc) return 4;
   if (utf8_check(tag1)) goto badtagname;                                        //  look for bad characters
   if (strpbrk(tag1,",;:\"")) goto badtagname;

   del_deftag(tag1);                                                             //  delete tag if already there

   cc1 = strlen(catg1);

   for (ii = 0; ii < maxtagcats; ii++)                                           //  look for given category
   {
      pp1 = tags_deftags[ii];
      if (! pp1) goto newcatg;
      if (! strmatchN(catg1,pp1,cc1)) continue;                                  //  match on "catname:"
      if (pp1[cc1] == ':') goto oldcatg;
   }

newcatg:
   if (ii == maxtagcats) goto toomanycats;
   cc1 = strlen(catg1) + strlen(tag1) + 6;
   pp1 = (char *) zmalloc(cc1);
   *pp1 = 0;
   strncatv(pp1,cc1,catg1,": ",tag1,", ",null);                                  //  category: + tag + delim + blank
   tags_deftags[ii] = tags_deftags[ii-1];                                        //  move "nocatg" record to next slot
   tags_deftags[ii-1] = pp1;                                                     //  insert new record before
   return 0;

oldcatg:                                                                         //  logic simplified                   15.05
   pp2 = pp1 + cc1 + 2;                                                          //  char following "catname: "
   cc1 = strlen(tag1);
   while (true) {
      pp2 = strcasestr(pp2,tag1);                                                //  look for "tagname, "
      if (! pp2) break;                                                          //  not found
      if (strchr(",;",pp2[cc1])) return 1;                                       //  found, tag not unique
      pp2 += cc1;                                                                //  keep searching
   }
   cc2 = strlen(pp1);                                                            //  add new tag to old record
   if (cc1 + cc2 + 4 > tagGcc) goto cattoobig;
   pp2 = zstrdup(pp1,cc1+cc2+4);                                                 //  expand string
   zfree(pp1);
   tags_deftags[ii] = pp2;
   strcpy(pp2+cc2,tag1);                                                         //  old record + tag + delim + blank
   strcpy(pp2+cc2+cc1,", ");
   return 0;

badcatname:
   zmessageACK(Mwin,"bad category name");
   return 3;

badtagname:
   zmessageACK(Mwin,"bad tag name");
   return 3;

toomanycats:
   zmessageACK(Mwin,"too many categories");
   return 2;

cattoobig:
   zmessageACK(Mwin,"too many tags");
   return 2;
}


//  delete tag from defined tags list, tags_deftags[]
//  return: 0 = found and deleted, 1 = not found

int del_deftag(char *tag)
{
   int      ii, cc;
   char     tag2[tagcc+4];
   char     *pp, *pp1, *pp2;

   if (! tag || *tag <= ' ') return 1;                                           //  bad tag

   strncpy0(tag2,tag,tagcc);                                                     //  construct tag + delim + blank
   cc = strlen(tag2);
   strcpy(tag2+cc,", ");
   cc += 2;

   for (ii = 0; ii < maxtagcats; ii++)
   {
      pp = tags_deftags[ii];
      if (! pp) return 1;                                                        //  not found

      while (pp)
      {
         pp = strcasestr(pp,tag2);                                               //  look for prior delim or colon
         if (! pp) break;
         if (strchr(",;:", pp[-2])) goto found;                                  //  cat: tag,  or  priortag, tag,
         pp += cc;                                                               //       |                   |
      }                                                                          //       pp                  pp
   }

found:
   for (pp1 = pp, pp2 = pp+cc; *pp2; pp1++, pp2++)                               //  eliminate tag, delim, blank
      *pp1 = *pp2;
   *pp1 = 0;

   return 0;
}


//  Stuff text widget "deftags" with all tags in the given category.
//  If category "ALL", stuff all tags and format by category.

void deftags_stuff(zdialog *zd, cchar *acatg)                                    //  15.08
{
   GtkWidget      *widget;
   int            ii, ff, cc;
   char           catgname[tagcc+4];
   char           *pp1, *pp2;
   
   widget = zdialog_widget(zd,"deftags");
   wclear(widget);

   for (ii = 0; ii < maxtagcats; ii++)
   {
      pp1 = tags_deftags[ii];
      if (! pp1) break;
      pp2 = strchr(pp1,':');
      if (! pp2) continue;
      cc = pp2 - pp1;
      if (cc < 1) continue;
      if (cc > tagcc) continue;
      strncpy0(catgname,pp1,cc+1);

      if (! strmatch(acatg,"ALL")) {
         ff = strmatch(catgname,acatg);
         if (! ff) continue;
      }
      
      strcat(catgname,": ");
      wprintx(widget,0,catgname,1);                                              //  "category: " in bold text
      
      pp2++;
      if (*pp2 == ' ') pp2++;
      if (*pp2) wprintx(widget,0,pp2);                                           //  "cat1, cat2, ... catN," 
      wprintx(widget,0,"\n");
   }

   return;
}


//  Stuff combo box "defcats" with "ALL" + all defined categories

void defcats_stuff(zdialog *zd)                                                  //  15.08
{
   char     catgname[tagcc+2];
   int      ii, cc;
   char     *pp1, *pp2;

   zdialog_cb_clear(zd,"defcats");
   zdialog_cb_app(zd,"defcats","ALL");
   
   for (ii = 0; ii < maxtagcats; ii++)
   {
      pp1 = tags_deftags[ii];
      if (! pp1) break;
      pp2 = strchr(pp1,':');
      if (! pp2) continue;
      cc = pp2 - pp1;
      if (cc < 1) continue;
      if (cc > tagcc) continue;
      strncpy0(catgname,pp1,cc+1);
      zdialog_cb_app(zd,"defcats",catgname);
   }
   
   return;
}


//  report tags defined and not used in any image file

void tag_orphans()
{
   FILE        *fid;
   sxrec_t     sxrec;
   int         ii, cc, err, ftf;
   int         Ndeftags;
   char        **deftags;
   char        usedtag[tagcc], tagsbuff[tagGcc];
   char        *pp1, *pp2, text[20];

   deftags = (char **) zmalloc(maxtags * sizeof(char *));                        //  allocate memory
   Ndeftags = 0;

   fid = fopen(tags_defined_file,"r");                                           //  read tags_defined file
   if (fid) {
      while (true) {
         pp1 = fgets_trim(tagsbuff,tagGcc,fid);
         if (! pp1) break;
         pp1 = strchr(pp1,':');                                                  //  skip over "category:"
         if (! pp1) continue;
         cc = pp1 - tagsbuff;
         if (cc > tagcc) continue;                                               //  reject bad data (manual edit?)
         pp1++;
         for (ii = 1; ; ii++) {                                                  //  get tags: tag1, tag2, ...
            pp2 = (char *) strField(pp1,",;",ii);
            if (! pp2) break;
            if (strlen(pp2) < 3) continue;                                       //  reject bad data
            if (strlen(pp2) > tagcc) continue;
            deftags[Ndeftags] = zstrdup(pp2);
            Ndeftags++;
         }
      }
      fclose(fid);
   }

   ftf = 1;                                                                      //  read all image index recs.

   while (true)
   {
      err = read_sxrec_seq(sxrec,ftf);
      if (err) break;

      pp1 = sxrec.tags;                                                          //  image tags
      if (! pp1) continue;
      if (strmatchN(pp1,"null",4)) continue;

      while (true)
      {
         while (*pp1 && strchr(",; ",*pp1)) pp1++;                               //  next image tag start
         if (! *pp1) break;
         pp2 = strpbrk(pp1,",;");                                                //  end
         if (! pp2) pp2 = pp1 + strlen(pp1);
         cc = pp2 - pp1;
         if (cc > tagcc-1) {
            pp1 = pp2;
            continue;                                                            //  ignore huge tag
         }

         strncpy0(usedtag,pp1,cc+1);                                             //  used tag, without delimiter

         for (ii = 0; ii < Ndeftags; ii++)                                       //  find in defined tags
            if (strmatch(usedtag,deftags[ii])) break;

         if (ii < Ndeftags) {                                                    //  found
            zfree(deftags[ii]);
            Ndeftags--;
            while (ii < Ndeftags) {                                              //  defined tag is in use
               deftags[ii] = deftags[ii+1];                                      //  remove from list and pack down
               ii++;
            }
         }

         pp1 = pp2;
      }
   }

   write_popup_text("open","unused tags",200,200,Mwin);
   for (ii = 0; ii < Ndeftags; ii++)
      write_popup_text("write",deftags[ii]);
   snprintf(text,20,"%d unused tags",Ndeftags);
   write_popup_text("write",text);

   for (ii = 0; ii < Ndeftags; ii++)
      zfree(deftags[ii]);
   zfree(deftags);

   return;
}


/********************************************************************************/

//  image file EXIF/IPTC data >> memory data:
//    meta_pdate, meta_rating, meta_tags, meta_comments, meta_caption,
//    meta_location, meta_country, meta_lati, meta_longi

void load_filemeta(cchar *file)
{
   int      ii, jj, cc;
   char     *pp;
   cchar    *exifkeys[10] = { exif_date_key, iptc_keywords_key,
                              iptc_rating_key, exif_size_key,
                              exif_comment_key, iptc_caption_key,
                              exif_city_key, exif_country_key,
                              exif_lati_key, exif_longi_key };

   char     *ppv[10], *imagedate, *imagekeywords, *imagestars, *imagesize;
   char     *imagecomms, *imagecapt;
   char     *imageloc, *imagecountry, *imagelati, *imagelongi;

   strncpy0(p_meta_pdate,meta_pdate,16);                                         //  save for use by edit_metadata      16.06
   strncpy0(p_meta_rating,meta_rating,4);                                        //    [Prev] button
   strncpy0(p_meta_tags,meta_tags,tagFcc);
   strncpy0(p_meta_caption,meta_caption,exif_maxcc);
   strncpy0(p_meta_comments,meta_comments,exif_maxcc);
   strncpy0(p_meta_location,meta_location,100);
   strncpy0(p_meta_country,meta_country,100);
   strncpy0(p_meta_lati,meta_lati,20);
   strncpy0(p_meta_longi,meta_longi,20);

   *meta_tags = *meta_pdate = *meta_comments = *meta_caption = 0;
   strcpy(meta_rating,"0");
   *meta_location = *meta_country = *meta_lati = *meta_longi = 0;

   exif_get(file,exifkeys,ppv,10);                                               //  get metadata from image file
   imagedate = ppv[0];
   imagekeywords = ppv[1];
   imagestars = ppv[2];
   imagesize = ppv[3];
   imagecomms = ppv[4];
   imagecapt = ppv[5];
   imageloc = ppv[6];
   imagecountry = ppv[7];
   imagelati = ppv[8];
   imagelongi = ppv[9];

   if (imagedate) {
      exif_tagdate(imagedate,meta_pdate);                                        //  EXIF date/time >> yyyymmddhhmmss
      zfree(imagedate);
   }

   if (imagekeywords)
   {
      for (ii = 1; ; ii++)
      {
         pp = (char *) strField(imagekeywords,",;",ii);
         if (! pp) break;
         if (*pp == ' ') continue;
         cc = strlen(pp);
         if (cc >= tagcc) continue;                                              //  reject tags too big
         for (jj = 0; jj < cc; jj++)
            if (pp[jj] > 0 && pp[jj] < ' ') break;                               //  reject tags with control characters
         if (jj < cc) continue;
         add_tag(pp,meta_tags,tagFcc);                                           //  add to file tags if unique
      }

      zfree(imagekeywords);
   }

   if (imagestars) {
      meta_rating[0] = *imagestars;
      if (meta_rating[0] < '0' || meta_rating[0] > '5') meta_rating[0] = '0';
      meta_rating[1] = 0;
      zfree(imagestars);
   }

   if (imagesize) {
      strncpy0(meta_size,imagesize,15);
      zfree(imagesize);
   }

   if (imagecomms) {
      strncpy0(meta_comments,imagecomms,exif_maxcc);
      zfree(imagecomms);
   }

   if (imagecapt) {
      strncpy0(meta_caption,imagecapt,exif_maxcc);
      zfree(imagecapt);
   }

   if (imageloc) {                                                               //  geotags
      strncpy0(meta_location,imageloc,99);
      zfree(imageloc);
   }
   else strcpy(meta_location,"null");                                            //  replace missing data with "null"

   if (imagecountry) {
      strncpy0(meta_country,imagecountry,99);
      zfree(imagecountry);
   }
   else strcpy(meta_country,"null");

   if (imagelati) {
      strncpy0(meta_lati,imagelati,12);
      zfree(imagelati);
   }
   else strcpy(meta_lati,"null");

   if (imagelongi) {
      strncpy0(meta_longi,imagelongi,12);
      zfree(imagelongi);
   }
   else strcpy(meta_longi,"null");

   Fmetamod = 0;                                                                 //  no pending changes                 15.03
   return;
}


//  add metadata in memory to image file EXIF/IPTC data and image_index recs.

void save_filemeta(cchar *file)
{
   cchar    *exifkeys[10] = { exif_date_key, iptc_keywords_key,
                              iptc_rating_key, exif_size_key,
                              exif_comment_key, iptc_caption_key,
                              exif_city_key, exif_country_key,
                              exif_lati_key, exif_longi_key };
   cchar       *exifdata[10];
   char        imagedate[24];

   *imagedate = 0;
   if (*meta_pdate) tag_exifdate(meta_pdate,imagedate);                          //  yyyymmddhhmmss >> EXIF date/time

   exifdata[0] = imagedate;                                                      //  update file EXIF/IPTC data
   exifdata[1] = meta_tags;
   exifdata[2] = meta_rating;
   exifdata[3] = meta_size;
   exifdata[4] = meta_comments;
   exifdata[5] = meta_caption;

   if (*meta_location < ' ' || strmatch(meta_location,"null"))                   //  geotags                            16.06
      exifdata[6] = "";
   else exifdata[6] = meta_location;                                             //  if "null" erase EXIF

   if (*meta_country < ' ' || strmatch(meta_country,"null")) 
      exifdata[7] = "";
   else exifdata[7] = meta_country;

   if (*meta_lati < ' ' || strmatch(meta_lati,"null") || 
       *meta_longi < ' ' || strmatch(meta_longi,"null")) 
      exifdata[8] = exifdata[9] = "";
   else {
      exifdata[8] = meta_lati;
      exifdata[9] = meta_longi;
   }

   exif_put(file,exifkeys,exifdata,10);                                          //  write EXIF

   update_image_index(file);                                                     //  update image index file

   if (zdexifview) meta_view(0);                                                 //  live EXIF/IPTC update

   Fmetamod = 0;                                                                 //  no pending changes                 15.03
   return;
}


//  update image index record (replace updated file data)

void update_image_index(cchar *file)                                             //  overhauled
{
   char     gtags[200];
   int      err;
   sxrec_t  sxrec;
   STATB    statb;

   err = stat(file,&statb);                                                      //  build new metadata record to insert
   if (err) {                                                                    //    or replace
      zmessageACK(Mwin,ZTX("file not found"));
      return;
   }

   memset(&sxrec,0,sizeof(sxrec_t));

   sxrec.file = (char *) file;                                                   //  image filespec

   compact_time(statb.st_mtime,sxrec.fdate);                                     //  convert file date to "yyyymmddhhmmss"
   strncpy0(sxrec.pdate,meta_pdate,15);                                          //  photo date, "yyyymmddhhmmss"

   sxrec.rating[0] = meta_rating[0];                                             //  rating '0' to '5' stars
   sxrec.rating[1] = 0;

   strncpy0(sxrec.size,meta_size,15);                                            //  2345x12345

   if (*meta_tags)                                                               //  tags
      sxrec.tags = meta_tags;

   if (*meta_caption)                                                            //  user caption
      sxrec.capt = meta_caption;

   if (*meta_comments)                                                           //  user comments
      sxrec.comms = meta_comments;

   if (*meta_location <= ' ') strcpy(meta_location,"null");                      //  geotags                            16.06
   if (*meta_country <= ' ') strcpy(meta_country,"null");
   if (*meta_lati <= ' ') strcpy(meta_lati,"null");                              //  "null" for location/country is searchable
   if (*meta_longi <= ' ') strcpy(meta_longi,"null");

   snprintf(gtags,200,"%s^ %s^ %s^ %s",meta_location, meta_country,
                                  meta_lati, meta_longi);
   sxrec.gtags = gtags;

   put_sxrec(&sxrec,file);
   return;
}


//  delete given image file from image index recs.

void delete_image_index(cchar *file)
{
   put_sxrec(null,file);
   return;
}


/********************************************************************************/

//  batch add geotags - set geotags for multiple image files

char        **batch_geotags_filelist = 0;
int         batch_geotags_filecount = 0;

void m_batch_geotags(GtkWidget *, cchar *menu)
{
   int   batch_geotags_dialog_event(zdialog *zd, cchar *event);

   cchar       *title = ZTX("Batch Add Geotags");
   char        *file, **flist;
   zdialog     *zd;
   int         ii, err;
   char        *location[2], *coord[2];
   char        locatn[100], country[100];
   char        lati[20], longi[20], *pp;
   cchar       *mapquest1 = "Geocoding web service courtesy of";
   cchar       *mapquest2 = "http://www.mapquest.com";

   F1_help_topic = "batch_geotags";

   if (! init_geolocs()) return;                                                 //  initialize geotags
   if (checkpend("all")) return;                                                 //  check nothing pending (block below)

/**
                      Batch Add Geotags

            [select files]  NN files selected
            location [______________]  country [______________]
            latitude [_______] longitude [_______]

            [find] [web] [proceed] [cancel]
**/

   zd = zdialog_new(title,Mwin,Bfind,Bweb,Bproceed,Bcancel,null);

   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=3");
   zdialog_add_widget(zd,"button","files","hb1",Bselectfiles,"space=10");
   zdialog_add_widget(zd,"label","labcount","hb1",Bnofileselected,"space=10");
   zdialog_add_widget(zd,"hbox","hb2","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labloc","hb2",ZTX("location"),"space=5");
   zdialog_add_widget(zd,"entry","location","hb2",0,"expand");                   //  16.07
   zdialog_add_widget(zd,"label","space","hb2",0,"space=5");
   zdialog_add_widget(zd,"label","labcountry","hb2",ZTX("country"),"space=5");
   zdialog_add_widget(zd,"entry","country","hb2",0,"expand");
   zdialog_add_widget(zd,"hbox","hb3","dialog");
   zdialog_add_widget(zd,"label","lablat","hb3","Latitude","space=3");
   zdialog_add_widget(zd,"entry","lati","hb3",0,"size=10");
   zdialog_add_widget(zd,"label","space","hb3",0,"space=5");
   zdialog_add_widget(zd,"label","lablong","hb3","Longitude","space=3");
   zdialog_add_widget(zd,"entry","longi","hb3",0,"size=10");
   zdialog_add_widget(zd,"hbox","hbmq","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labmq","hbmq",mapquest1,"space=3");
   zdialog_add_widget(zd,"link","MapQuest","hbmq",mapquest2);

   batch_geotags_filelist = 0;
   batch_geotags_filecount = 0;

   flist = 0;
   zd_mapgeotags = zd;                                                           //  activate geomap clicks

   zdialog_run(zd,batch_geotags_dialog_event);                                   //  run dialog
   zdialog_wait(zd);                                                             //  wait for dialog completion

   if (zd->zstat != 3) goto cleanup;                                             //  status not [proceed]
   if (! batch_geotags_filecount) goto cleanup;                                  //  no files selected

   zdialog_fetch(zd,"location",locatn,99);                                       //  get city/location [country] from dialog
   zdialog_fetch(zd,"country",country,99);
   zdialog_fetch(zd,"lati",lati,20);                                             //  and latitude, longitude
   zdialog_fetch(zd,"longi",longi,20);
   
   pp = strchr(lati,',');                                                        //  replace comma decimal point        16.06
   if (pp) *pp = '.';                                                            //    with period
   pp = strchr(longi,',');
   if (pp) *pp = '.';

   location[0] = locatn;
   location[1] = country;
   coord[0] = lati;
   coord[1] = longi;

   zdialog_free(zd);                                                             //  kill dialog
   zd_mapgeotags = 0;                                                            //  deactivate geomap clicks

   flist = batch_geotags_filelist;                                               //  selected files
   if (! flist) goto cleanup;

   if (checkpend("all")) goto cleanup;                                           //  check nothing pending              15.10
   Fblock = 1;

   write_popup_text("open","Adding Geotags",500,200,Mwin);                       //  status monitor popup window

   for (ii = 0; flist[ii]; ii++)                                                 //  loop all selected files
   {
      zmainloop();                                                               //  keep GTK alive                     16.04

      file = flist[ii];                                                          //  display image
      err = f_open(file,0,0,0);
      if (err) continue;

      strncpy0(meta_location,location[0],99);                                    //  save geotags in image file EXIF    16.06
      strncpy0(meta_country,location[1],99);                                     //    and in search-index file
      strncpy0(meta_lati,coord[0],12);
      strncpy0(meta_longi,coord[1],12);

      Fmetamod++;                                                                //  16.06
      save_filemeta(file);
      
      put_geolocs(location,coord);                                               //  update geolocs[*]

      write_popup_text("write",file);                                            //  report progress
   }

   write_popup_text("write",Bcompleted);

   Fblock = 0;

cleanup:

   if (zd) zdialog_free(zd);
   zd_mapgeotags = 0;                                                            //  deactivate geomap clicks

   if (flist) {
      for (ii = 0; flist[ii]; ii++)
         zfree(flist[ii]);
      zfree(flist);
   }

   return;
}


//  batch_geotags dialog event function

int batch_geotags_dialog_event(zdialog *zd, cchar *event)
{
   int      ii, yn, zstat, Nmatch, err;
   char     **flist = batch_geotags_filelist;
   char     countmess[50];
   char     *location[2], *coord[2], *matches[20][2];
   char     locatn[100], country[100];
   char     lati[20], longi[20], *pp;
   cchar    *errmess;
   float    fcoord[2];
   
   if (strmatch(event,"escape")) zd->zstat = 4;                                  //  escape = cancel                    15.07

   if (strmatch(event,"files"))                                                  //  select images to add tags
   {
      if (flist) {                                                               //  free prior list
         for (ii = 0; flist[ii]; ii++)
            zfree(flist[ii]);
         zfree(flist);
      }

      zdialog_show(zd,0);                                                        //  hide parent dialog
      flist = gallery_getfiles();                                                //  get file list from user
      zdialog_show(zd,1);

      batch_geotags_filelist = flist;

      if (flist)                                                                 //  count files in list
         for (ii = 0; flist[ii]; ii++);
      else ii = 0;
      batch_geotags_filecount = ii;

      snprintf(countmess,50,Bfileselected,batch_geotags_filecount);
      zdialog_stuff(zd,"labcount",countmess);
   }

   if (! zd->zstat) return 1;                                                    //  wait for action button

   zstat = zd->zstat;
   zd->zstat = 0;                                                                //  keep dialog active

   zdialog_fetch(zd,"location",locatn,99);                                       //  get location [country] from dialog
   zdialog_fetch(zd,"country",country,99);
   zdialog_fetch(zd,"lati",lati,20);                                             //  and latitude, longitude
   zdialog_fetch(zd,"longi",longi,20);

   locatn[0] = toupper(locatn[0]);                                               //  capitalize                         16.06
   country[0] = toupper(country[0]);
   zdialog_stuff(zd,"location",locatn);
   zdialog_stuff(zd,"country",country);
   
   pp = strchr(lati,',');                                                        //  replace comma decimal point        16.06
   if (pp) *pp = '.';                                                            //    with period
   pp = strchr(longi,',');
   if (pp) *pp = '.';

   location[0] = locatn;
   location[1] = country;
   coord[0] = lati;
   coord[1] = longi;

   if (zstat == 1)                                                               //  [find]
   {
      Nmatch = get_geolocs(location,coord,matches);                              //  find in geolocs[*] table
      if (Nmatch == 0)                                                           //  no matches
         zmessageACK(Mwin,ZTX("location not found"));

      else if (Nmatch == 1) {                                                    //  one match
         zdialog_stuff(zd,"location",matches[0][0]);                             //  stuff matching location data into dialog
         zdialog_stuff(zd,"country",matches[0][1]);
         zdialog_stuff(zd,"lati",coord[0]);
         zdialog_stuff(zd,"longi",coord[1]);
      }

      else {                                                                     //  multiple matching cities
         zstat = geotags_chooseloc(location,coord);                              //  ask user to choose one
         if (zstat == 1) {                                                       //  response is available
            zdialog_stuff(zd,"location",location[0]);                            //  stuff matching location data into dialog
            zdialog_stuff(zd,"country",location[1]);
            zdialog_stuff(zd,"lati",coord[0]);
            zdialog_stuff(zd,"longi",coord[1]);
         }
      }
   }

   else if (zstat == 2)                                                          //  [web]
   {
      errmess = web_geocode(location,coord);                                     //  look-up in web service
      if (errmess)
         zmessageACK(Mwin,errmess);                                              //  fail
      else {
         zdialog_stuff(zd,"location",location[0]);                               //  success, return all data
         zdialog_stuff(zd,"country",location[1]);                                //  (location may have been completed)
         zdialog_stuff(zd,"lati",coord[0]);
         zdialog_stuff(zd,"longi",coord[1]);
      }
   }

   else if (zstat == 3)                                                          //  [proceed]
   {
      if (*lati > ' ' && ! strmatch(lati,"null") &&                              //  if coordinates present, validate   16.06
          *longi > ' ' && ! strmatch(longi,"null"))
      {
         err = validate_latlong(coord[0],coord[1],fcoord[0],fcoord[1]);
         if (err) goto badcoord;
      }

      if (! batch_geotags_filecount) goto nofiles;

      if (*locatn <= ' ' || *country <= ' ' || *lati <= ' ' || *longi <= ' ')    //  16.06
      {
         yn = zmessageYN(Mwin,ZTX("data is incomplete \n proceed?"));
         if (! yn) return 1;
      }

      zd->zstat = 3;                                                             //  OK to proceed
      zdialog_destroy(zd);
   }

   else zdialog_destroy(zd);                                                     //  [cancel] or [x]
   return 1;

badcoord:
   zmessageACK(Mwin,ZTX("bad latitude/longitude: %s %s"),coord[0],coord[1]);
   return 1;

nofiles:
   zmessageACK(Mwin,Bnofileselected);
   return 1;
}


/********************************************************************************/

//  dialog to choose one location from multiple options
//  location[2] is input location and optional country
//  (may be substrings, may have multiple matches in location geotags data)
//  location[2] is output unique location and country after user choice
//  coord[2] is output latitude, longitude

char     *geotags_chosenlocation[2];
char     *geotags_chosencoord[2];

int geotags_chooseloc(char *location[2], char *coord[2])
{
   int  geotags_chooseloc_event(zdialog *zd, cchar *event);

   char     *matches[20][2], text[200];
   int      Nmatch, ii, zstat;
   zdialog  *zd;

   Nmatch = get_geolocs(location,coord,matches);                                 //  get matching location geotags data

   if (Nmatch == 0) return 0;                                                    //  no match

   if (Nmatch == 1) {                                                            //  one match, done
      location[0] = matches[0][0];
      location[1] = matches[0][1];
      return 1;
   }

   zd = zdialog_new(ZTX("choose location"),Mwin,BOK,Bcancel,null);               //  multiple matches, start dialog
   zdialog_add_widget(zd,"comboE","cities","dialog",0,"space=5");
   for (ii = 0; ii < Nmatch; ii++) {                                             //  list matching cities to choose from
      snprintf(text,200,"%s | %s",matches[ii][0],matches[ii][1]);
      zdialog_cb_app(zd,"cities",text);                                          //  duplicates are removed
   }

   zdialog_resize(zd,300,100);
   zdialog_run(zd,geotags_chooseloc_event);                                      //  run dialog, wait for completion
   zstat = zdialog_wait(zd);
   zdialog_free(zd);

   if (zstat == 1) {                                                             //  valid response available
      location[0] = geotags_chosenlocation[0];
      location[1] = geotags_chosenlocation[1];
      coord[0] = geotags_chosencoord[0];
      coord[1] = geotags_chosencoord[1];
      return 1;
   }

   return 0;
}


//  dialog event function - get chosen location/country from multiple choices

int geotags_chooseloc_event(zdialog *zd, cchar *event)
{
   char           text[200];
   static char    locatn[100], country[100];
   char           *location[2], *coord[2];
   char           *matches[20][2];
   cchar          *pp;
   int            nn;
   static int     ftf = 1;

   if (strmatch(event,"enter")) zd->zstat = 1;                                   //  [OK]  
   if (strmatch(event,"escape")) zd->zstat = 2;                                  //  escape = cancel                    15.07

   if (ftf) {
      zdialog_cb_popup(zd,"cities");                                             //  first time, open combo box list
      ftf = 0;
   }

   if (strmatch(event,"cities")) {                                               //  OK
      zdialog_fetch(zd,"cities",text,200);

      pp = strField(text,'|',1);
      if (pp) strncpy0(locatn,pp,99);
      pp = strField(text,'|',2);
      if (pp) strncpy0(country,pp,99);
      strTrim2(locatn);
      strTrim2(country);
      location[0] = locatn;
      location[1] = country;

      nn = get_geolocs(location,coord,matches);                                  //  find in location geotags data
      if (nn) {
         geotags_chosenlocation[0] = location[0];                                //  use 1st match if > 1
         geotags_chosenlocation[1] = location[1];
         geotags_chosencoord[0] = coord[0];
         geotags_chosencoord[1] = coord[1];
         zd->zstat = 1;
      }
      else zd->zstat = 2;                                                        //  bad status
   }

   if (zd->zstat) ftf = 1;

   return 1;
}


/********************************************************************************/

//  Convert a location [country] to earth coordinates using the
//    MapQuest geocoding service.
//  (incomplete names may be completed with a bad guess)

cchar * web_geocode(char *location[2], char *coord[2])                           //  overhaul 13.05, 13.05.1
{
   int         err;
   static char lati[20], longi[20];
   char        outfile[100], URI[300];
   char        *pp1, *pp2, buffer[200];
   float       flati, flongi;
   FILE        *fid;
   cchar       *notfound = ZTX("not found");
   cchar       *badinputs = ZTX("location and country required");
   cchar       *query = "http://open.mapquestapi.com/geocoding/v1/address?"
                        "&key=Fmjtd%7Cluub2qa72d%2C20%3Do5-9u700a"
                        "&maxResults=1"
                        "&outFormat=csv";

   *coord[0] = *coord[1] = 0;                                                    //  null outputs
   *lati = *longi = 0;

   if (*location[0] <= ' ' || *location[1] <= ' ')
      return badinputs;

   snprintf(outfile,100,"%s/web-data",tempdir);                                  //  v.14.11
   snprintf(URI,299,"\"%s&location=%s,%s\"",query,location[0],location[1]);

   err = shell_quiet("wget -T 10 -o /dev/null -O %s %s",outfile,URI);
   if (err == 4) err = ECOMM;                                                    //  replace "interrupted system call"
   if (err) return strerror(err);

   fid = fopen(outfile,"r");                                                     //  get response
   if (! fid) return notfound;
   pp1 = fgets(buffer,200,fid);
   pp1 = fgets(buffer,200,fid);
   fclose(fid);
   if (! pp1) return notfound;
   printz("web geocode: %s \n",buffer);

   pp2 = (char *) strField(pp1,",",7);
   if (! pp2) return notfound;
   strncpy0(lati,pp2,12);

   pp2 = (char *) strField(pp1,",",8);
   if (! pp2) return notfound;
   strncpy0(longi,pp2,12);

   err = validate_latlong(lati,longi,flati,flongi);
   if (err) return notfound;

   pp1 = strchr(lati,'.');                                                       //  keep max. 4 decimal digits
   if (pp1) *(pp1+5) = 0;
   pp1 = strchr(longi,'.');
   if (pp1) *(pp1+5) = 0;

   coord[0] = lati;
   coord[1] = longi;
   return 0;
}


/********************************************************************************/

//  Initialize for geotag functions.
//  Load geolocations data into memory from image index files.
//  Returns no. geolocations loaded.

int init_geolocs()
{
   int  init_geolocs_comp(cchar *rec1, cchar *rec2);

   char     *gtags, *pp;
   char     locatn[100], country[100];
   char     lati[20], longi[20];
   float    flati, flongi;
   double   time0, time1;
   int      err, ftf, cc, ii, jj, nimages;
   sxrec_t  sxrec;

   if (Ngeolocs) return Ngeolocs;                                                //  already done

   time0 = get_seconds();
   Ffuncbusy = 1;

   cc = maxgeolocs * sizeof(geolocs_t);                                          //  get memory for geotag locations DB
   geolocs = (geolocs_t *) zmalloc(cc);

   //  populate geolocs[] from search-index file (= image EXIF data)

   nimages = 0;
   ftf = 1;

   while (true)
   {
      zmainloop(100);                                                            //  keep GTK alive

      if (Ngeolocs == maxgeolocs) {                                              //  table full
         zmessageACK(Mwin,"max. geotags %d exceeded",maxgeolocs);
         break;
      }

      err = read_sxrec_seq(sxrec,ftf);                                           //  read image index recs.
      if (err) break;

      nimages++;                                                                 //  image count

      gtags = sxrec.gtags;
      strcpy(locatn,"null");
      strcpy(country,"null");
      strcpy(lati,"null");
      strcpy(longi,"null");
      flati = flongi = 0;

      pp = (char *) strField(gtags,'^',1);                                       //  location name or "null"
      if (pp) strncpy0(locatn,pp,99);
      pp = (char *) strField(gtags,'^',2);                                       //  country
      if (pp) strncpy0(country,pp,99);
      pp = (char *) strField(gtags,'^',3);                                       //  latitude
      if (pp) strncpy0(lati,pp,12);
      pp = (char *) strField(gtags,'^',4);                                       //  longitude
      if (pp) strncpy0(longi,pp,12);

      zfree(sxrec.file);                                                         //  free sxrec allocations
      zfree(sxrec.tags);
      zfree(sxrec.capt);
      zfree(sxrec.comms);
      zfree(sxrec.gtags);

      if (strmatch(lati,"null") || strmatch(longi,"null")) {                     //  validate and replace bad data      16.06
         strcpy(lati,"null");
         strcpy(longi,"null");
         err = 1;
      }
      else err = validate_latlong(lati,longi,flati,flongi);

      if (err) {                                                                 //  16.06
         strcpy(lati,"null");
         strcpy(longi,"null");
         flati = flongi = 0;
      }

      if (Ngeolocs) {
         ii = Ngeolocs - 1;                                                      //  eliminate (frequent) duplicates    16.07
         if (strmatch(locatn,geolocs[ii].location) &&                            //    same location and country
             strmatch(country,geolocs[ii].country)) continue;
      }

      ii = Ngeolocs++;                                                           //  fill next entry in table
      geolocs[ii].location = zstrdup(locatn);
      geolocs[ii].country = zstrdup(country);
      geolocs[ii].lati = zstrdup(lati);
      geolocs[ii].longi = zstrdup(longi);
      geolocs[ii].flati = flati;
      geolocs[ii].flongi = flongi;
   }

   cc = sizeof(geolocs_t);
   HeapSort((char *) geolocs, cc, Ngeolocs, init_geolocs_comp);                  //  sort

   for (ii = 0, jj = 1; jj < Ngeolocs; jj++)                                     //  eliminate duplicates
   {
      if ((strmatch(geolocs[jj].location,geolocs[ii].location)) &&               //  use location/country names only    16.07
          (strmatch(geolocs[jj].country,geolocs[ii].country)))
      {
         zfree(geolocs[jj].country);                                             //  free redundant entries
         zfree(geolocs[jj].location);
         zfree(geolocs[jj].lati);
         zfree(geolocs[jj].longi);
      }
      else {
         ii++;
         geolocs[ii] = geolocs[jj];
      }
   }
   
   time1 = get_seconds() - time0;

   Ngeolocs = ii + 1;
   printz("%d images, %d locations  %.3f secs. \n",nimages,Ngeolocs,time1);      //  16.06
   zmessage_post(Mwin,3,"%d images, %d locations \n",nimages,Ngeolocs);

   Ffuncbusy = 0;
   return Ngeolocs;
}


//  Compare 2 geolocs records by country, location, latitude, longitude
//  return  <0  0  >0   for   rec1  <  ==  >  rec2.

int  init_geolocs_comp(cchar *rec1, cchar *rec2)                                 //  15.12
{
   float    diff;
   int      ii;

   geolocs_t *r1 = (geolocs_t *) rec1;
   geolocs_t *r2 = (geolocs_t *) rec2;
   
   ii = strcmp(r1->country,r2->country);
   if (ii) return ii;

   ii = strcmp(r1->location,r2->location);
   if (ii) return ii;
   
   if (strmatch(r1->lati,"null")) {                                              //  sort missing lat/long last         16.06
      if (strmatch(r2->lati,"null")) return 0;
      else return +1;
   }
   
   diff = r1->flati - r2->flati;
   if (diff < 0) return -1;
   if (diff > 0) return +1;

   diff = r1->flongi - r2->flongi;
   if (diff < 0) return -1;
   if (diff > 0) return +1;

   return 0;
}


/********************************************************************************/

//  get earth coordinates data for a location [ country ]
//  inputs:        location[0] = city/location
//                 location[1] = country (opt)
//  outputs:       coord[2] = latitude -90 to +90, longitude -180 to +180
//                 matches[20][2] up to 20 matching location/country
//  returns:       no. matches for input location [ country ]
//                 (max. 20)
//
//  use "null" or "" for missing location or country input (no NULL pointer)
//  coordinates are not returned if there are more than one matches
//

int get_geolocs(char *location[2], char *coord[2], char *matches[20][2])
{
   int      cc, ii, Nmatch;
   int      flocation = 0, fcountry = 0;

   coord[0] = coord[1] = (char *) "null";                                        //  initz. "null" (immutable)          16.06

   if (*location[0] > ' ' && ! strmatch(location[0],"null")) flocation = 1;      //  one of these must be present       16.06
   if (*location[1] > ' ' && ! strmatch(location[1],"null")) fcountry = 1;
   if (! flocation && ! fcountry) return 0;

   for (ii = Nmatch = 0; ii < Ngeolocs; ii++)                                    //  search for exact location [ country ]
   {
      if (flocation && ! strmatchcase(location[0],geolocs[ii].location)) continue;
      if (fcountry && ! strmatchcase(location[1],geolocs[ii].country)) continue;

      matches[Nmatch][0] = geolocs[ii].location;                                 //  save match
      matches[Nmatch][1] = geolocs[ii].country;
      Nmatch++;
      
      if (Nmatch == 1) {                                                         //  first match
         coord[0] = geolocs[ii].lati;                                            //  return lat/long
         coord[1] = geolocs[ii].longi;
      }
      
      if (Nmatch == 20) break;                                                   //  no more than 20 matches are reported
   }
   
   if (Nmatch > 1) coord[0] = coord[1] = (char *) "null";                        //  multiple matches, no coordinates   16.07
   if (Nmatch) return Nmatch;                                                    //  1 or more matches found

   for (ii = Nmatch  = 0; ii < Ngeolocs; ii++)                                   //  search for partial location [ country ]
   {
      if (strmatch(geolocs[ii].location,"null")) continue;

      if (flocation) {
         cc = strlen(location[0]);
         if (! strmatchcaseN(location[0],geolocs[ii].location,cc)) continue;
      }

      if (fcountry) {
         cc = strlen(location[1]);
         if (! strmatchcaseN(location[1],geolocs[ii].country,cc)) continue;
      }

      matches[Nmatch][0] = geolocs[ii].location;
      matches[Nmatch][1] = geolocs[ii].country;
      Nmatch++;

      if (Nmatch == 1) {                                                         //  first match
         coord[0] = geolocs[ii].lati;                                            //  return lat/long
         coord[1] = geolocs[ii].longi;
      }

      if (Nmatch == 20) break;                                                   //  no more than 20 matches are reported
   }

   if (Nmatch > 1) coord[0] = coord[1] = (char *) "null";                        //  multiple matches, no coordinates   16.06
   return Nmatch;
}


/********************************************************************************/

//  Update geolocations table  geolocs[*]
//
//  location[2] = city/location and country
//  coord[2] = latitude and longitude
//  return value:  0    OK, no geotag revision (incomplete data)
//                 1    OK, no geotag revision (matches existing data)
//                 2    OK, geotag lat/long updated
//                 3    OK, geotag new location added
//                -1    error, lat/long bad

int put_geolocs(char *location[2], char *coord[2])
{
   char        acoord[2][20];
   float       flati, flongi;
   int         ii, err, Fnew = 0, Fchange = 0;
   int         retval;

   if (! curr_file) return 0;

   err = validate_latlong(coord[0],coord[1],flati,flongi);
   if (err) {                                                                    //  1 = missing, 2 = bad
      if (err == 2) goto badcoord;                                               //  reject bad data
      strcpy(acoord[0],"null");                                                  //  replace missing data with "null"
      strcpy(acoord[1],"null");
      flati = flongi = 0;                                                        //  earth coordinates missing value
   }
   else {
      snprintf(acoord[0],20,"%.4f",flati);                                       //  reformat with std. precision
      snprintf(acoord[1],20,"%.4f",flongi);
   }

   if (*location[0] <= ' ' || strmatch(location[0],"null")) return 0;            //  quit here if location not complete
   if (*location[1] <= ' ' || strmatch(location[1],"null")) return 0;

   *location[0] = toupper(*location[0]);                                         //  force capitalization               16.07
   *location[1] = toupper(*location[1]);

   for (ii = 0; ii < Ngeolocs; ii++) {                                           //  search geotags for location, country
      if (! strmatchcase(location[0],geolocs[ii].location)) continue;            //  (case-insensitive compare)
      if (strmatchcase(location[1],geolocs[ii].country)) break;                  //  BANG  unknown 2016.04.19
   }

   if (ii < Ngeolocs) {                                                          //  found, check for revised lat/long
      if (! strmatch(geolocs[ii].lati,acoord[0])) Fchange = 1;
      if (! strmatch(geolocs[ii].longi,acoord[1])) Fchange = 1;
      if (! strmatch(geolocs[ii].location,location[0])) Fchange = 1;             //  or revised capitalization
      if (! strmatch(geolocs[ii].country,location[1])) Fchange = 1;
   }
   else Fnew = 1;                                                                //  new location

   if (Fnew + Fchange == 0) return 1;                                            //  no change

   if (Fchange)
   {
      zfree(geolocs[ii].location);                                               //  change geotag data in memory
      geolocs[ii].location = zstrdup(location[0]);                               //  (to be used subsequently)
      zfree(geolocs[ii].country);
      geolocs[ii].country = zstrdup(location[1]);                                //  presence in image EXIF will make
      zfree(geolocs[ii].lati);                                                   //    this the preferred version
      geolocs[ii].lati = zstrdup(acoord[0]);
      zfree(geolocs[ii].longi);
      geolocs[ii].longi = zstrdup(acoord[1]);
      geolocs[ii].flati = flati;
      geolocs[ii].flongi = flongi;
      retval = 2;
   }

   else if (Fnew)
   {
      if (Ngeolocs == maxgeolocs) {
         zmessageACK(Mwin,"max. geotags %d exceeded",maxgeolocs);
         return -1;
      }
      for (ii = Ngeolocs; ii > 0; ii--)                                          //  shift all geotag data up
         geolocs[ii] = geolocs[ii-1];
      Ngeolocs++;

      ii = 0;
      geolocs[ii].location = zstrdup(location[0]);                               //  new geotag is now first
      geolocs[ii].country = zstrdup(location[1]);                                //  (find again faster)
      geolocs[ii].lati = zstrdup(acoord[0]);
      geolocs[ii].longi = zstrdup(acoord[1]);
      geolocs[ii].flati = flati;
      geolocs[ii].flongi = flongi;
      retval = 3;
   }

   else return -1;                                                               //  should not happen
   return retval;

badcoord:
   zmessageACK(Mwin,ZTX("bad latitude/longitude: %s %s"),coord[0],coord[1]);
   return -1;
}


/********************************************************************************/

//  validate and convert earth coordinates, latitude and longitude
//  return: 0  OK
//          1  both are missing ("null")
//          2  invalid data
//  if status is > 0, 0.0 is returned for both values

int validate_latlong(char *lati, char *longi, float &flati, float &flongi)
{
   int      err;
   char     *pp;

   if ((! *lati || *lati == ' ' || strmatch(lati,"null")) && 
       (! *longi || *longi == ' ' || strmatch(longi,"null"))) goto status1;      //  both missing

   if ((! *lati || *lati == ' ' || strmatch(lati,"null")) || 
       (! *longi || *longi == ' ' || strmatch(longi,"null"))) goto status2;      //  one missing
   
   pp = strchr(lati,',');                                                        //  replace comma decimal point        16.06
   if (pp) *pp = '.';                                                            //    with period
   pp = strchr(longi,',');
   if (pp) *pp = '.';

   err = convSF(lati,flati,-90,+90);                                             //  convert to float and check limits  16.06
   if (err) goto status2;
   err = convSF(longi,flongi,-180,+180);
   if (err) goto status2;

   if (flati == 0.0 && flongi == 0.0) goto status2;                              //  reject both = 0.0
   return 0;

status1:
   flati = flongi = 0.0;                                                         //  both missing
   return 1;

status2:                                                                         //  one missing or invalid
   flati = flongi = 0.0;
   return 2;
}


/********************************************************************************/

//  compute the km distance between two earth coordinates

float earth_distance(float lat1, float lon1, float lat2, float lon2)             //  16.06
{
   float    dlat, dlon, mlat, dist;
   
   dlat = fabsf(lat2 - lat1);                                                    //  latitude distance
   dlon = fabsf(lon2 - lon1);                                                    //  longitude distance
   mlat = 0.5 * (lat1 + lat2);                                                   //  mean latitude
   mlat *= 0.01745;                                                              //  radians
   dlon = dlon * cosf(mlat);                                                     //  longitude distance * cos(latitude)
   dist = sqrtf(dlat * dlat + dlon * dlon);                                      //  distance in degrees
   dist *= 111.0;                                                                //  distance in km
   return dist;
}


/********************************************************************************/

//  Geolocation Map Functions (W view)
//  Maps of any scale can be user-installed.
//  Mercator projection is assumed (but unimportant for maps < 100 km).

namespace geomap
{
   char     mapname[100];
   int      mapww, maphh;                                                        //  map width, height
   float    mflati[2];                                                           //  latitude range, low - high
   float    mflongi[2];                                                          //  longitude range, low - high
}

float  geomap_range = 10;                                                        //  geomap search range, km

int   geomap_position(float flati, float flongi, int &mx, int &my);              //  earth coordinates > map position
int   geomap_coordinates(int mx, int my, float &flati, float &flongi);           //  map position > earth coordinates
void  find_geomap_images(float flati, float flongi, float km);                   //  find images within range of geolocation


/********************************************************************************/

//  load the default world map or a map chosen by the user

void m_load_geomap(GtkWidget *, cchar *menu)
{
   using namespace geomap;

   int  load_geomap_dialog_event(zdialog *zd, cchar *event);

   char     mapindex[200], mapfile[200];
   char     buff[200];
   cchar    *pp;
   zdialog  *zd;
   int      err, zstat;
   FILE     *fid;
   float    flati1, flati2, flongi1, flongi2;
   STATB    statb;

   F1_help_topic = "images_by_map";

   if (checkpend("all")) return;                                                 //  check, no block

   if (! init_geolocs()) return;                                                 //  insure geolocations are loaded

   if (Fnoindex) {
      zmessageACK(Mwin,ZTX("-noindex in use, disabled"));
      return;
   }

   snprintf(mapindex,200,"%s/maps_index",maps_dirk);                             //  check map index file exists
   err = stat(mapindex,&statb);                                                  //  (from fotoxx-maps package)
   if (err) goto nomapsinstalled;

   if (menu && strmatch(menu,"default")) {
      strcpy(mapname,"World.jpg");                                               //  use default world map
      goto load_map;
   }

   fid = fopen(mapindex,"r");                                                    //  open map index file
   if (! fid) goto nomapsinstalled;
   
   F1_help_topic = "choose_map";

   zd = zdialog_new(ZTX("choose map file"),Mwin,Bcancel,null);                   //  start map chooser dialog
   zdialog_add_widget(zd,"combo","mapname","dialog",0,"space=5");

   while (true)
   {
      pp = fgets_trim(buff,200,fid,1);                                           //  get map file names
      if (! pp) break;
      pp = strField(pp,",",1);
      if (! pp) continue;
      zdialog_cb_app(zd,"mapname",pp);                                           //  add to dialog popup list
   }

   fclose(fid);
   
   snprintf(mapindex,200,"%s/maps_index",user_maps_dirk);                        //  look for user map index file       15.12
   err = stat(mapindex,&statb);
   if (err) goto choose_map;

   fid = fopen(mapindex,"r");                                                    //  open user map index
   if (fid)
   {
      while (true)   
      {
         pp = fgets_trim(buff,200,fid,1);                                        //  get map file names
         if (! pp) break;
         pp = strField(pp,",",1);
         if (! pp) continue;
         zdialog_cb_app(zd,"mapname",pp);                                        //  add to dialog popup list
      }

      fclose(fid);
   }
   
choose_map:

   if (*mapname && Wstate.fpxb)                                                  //  show current map if any
      zdialog_stuff(zd,"mapname",mapname);

   zdialog_resize(zd,300,100);
   zdialog_run(zd,load_geomap_dialog_event);                                     //  run dialog, get user choice

   zstat = zdialog_wait(zd);
   if (zstat != 1) {
      zdialog_free(zd);                                                          //  cancel
      return;
   }

   zdialog_fetch(zd,"mapname",mapname,100);                                      //  user choice
   zdialog_free(zd);

load_map:

   snprintf(mapfile,200,"%s/%s",maps_dirk,mapname);                              //  check map file exists
   err = stat(mapfile,&statb);
   if (err) {
      snprintf(mapfile,200,"%s/%s",user_maps_dirk,mapname);                      //  check user maps also               15.12
      err = stat(mapfile,&statb);
   }
   if (err) goto mapfilemissing;                                                 //  not found

   snprintf(mapindex,200,"%s/maps_index",maps_dirk);                             //  read map index again
   fid = fopen(mapindex,"r");
   if (! fid) goto nomapsinstalled;

   while (true)
   {
      pp = fgets_trim(buff,200,fid,1);                                           //  find chosen map file
      if (! pp) break;
      pp = strField(buff,",",1);
      if (! pp) continue;
      if (strmatch(pp,mapname)) break;
   }

   fclose(fid);
   if (pp) goto get_lat_long;                                                    //  found

   snprintf(mapindex,200,"%s/maps_index",user_maps_dirk);                        //  read user map index again          15.12
   fid = fopen(mapindex,"r");
   if (! fid) goto nomapsinstalled;

   while (true)
   {
      pp = fgets_trim(buff,200,fid,1);                                           //  find chosen map file
      if (! pp) break;
      pp = strField(buff,",",1);
      if (! pp) continue;
      if (strmatch(pp,mapname)) break;
   }

   fclose(fid);
   if (! pp) goto mapfilemissing;                                                //  not found in either index

get_lat_long:

   flati1 = flati2 = flongi1 = flongi2 = 0;

   pp = strField(buff,",",2);                                                    //  get map earth coordinates range
   if (! pp) goto latlongerr;                                                    //    and verify data OK
   err = convSF(pp,flati1,-80,+80);
   if (err) goto latlongerr;

   pp = strField(buff,",",3);
   if (! pp) goto latlongerr;
   err = convSF(pp,flati2,-80,+80);
   if (err) goto latlongerr;

   pp = strField(buff,",",4);
   if (! pp) goto latlongerr;
   err = convSF(pp,flongi1,-200,+200);
   if (err) goto latlongerr;

   pp = strField(buff,",",5);
   if (! pp) goto latlongerr;
   err = convSF(pp,flongi2,-200,+200);
   if (err) goto latlongerr;

   if (flati2 < flati1 + 0.001) goto latlongerr;                                 //  require map range > 100m           15.12
   if (flongi2 < flongi1 + 0.001) goto latlongerr;

   printz("load geomap: %s \n",mapname);                                         //  no errors, commit to load map

   free_geomap();                                                                //  free prior map

   Ffuncbusy = 1;
   zmainloop();                                                                  //  16.06
   Wstate.fpxb = PXB_load(mapfile,1);                                            //  load map file (with diagnostic)
   Ffuncbusy = 0;
   if (! Wstate.fpxb) return;

   mapww = Wstate.fpxb->ww;                                                      //  save map pixel dimensions
   maphh = Wstate.fpxb->hh;

   mflati[0] = flati1;                                                           //  save map earth coordinates range
   mflati[1] = flati2;
   mflongi[0] = flongi1;
   mflongi[1] = flongi2;

   m_zoom(null,"fit");
   return;

nomapsinstalled:
   zmessageACK(Mwin,ZTX("fotoxx-maps package not installed \n"
                        "(see http://kornelix.net/packages and /tarballs)"));
   return;

mapfilemissing:
   zmessageACK(Mwin,ZTX("map file %s is missing"),mapname);
   return;

latlongerr:
   zmessageACK(Mwin,ZTX("map latitude/longitude data unreasonable \n"
                        " %.3f %.3f %.3f %.3f"),flati1,flati2,flongi1,flongi2);
   return;
}


//  dialog event and completion function

int  load_geomap_dialog_event(zdialog *zd, cchar *event)
{
   if (strmatch(event,"escape")) zd->zstat = 2;                                  //  escape = cancel                    15.07
   if (strmatch(event,"mapname")) zd->zstat = 1;
   return 1;
}


/********************************************************************************/

//  set the map search range from a clicked position, kilometers

void m_mapsearch_range(GtkWidget *, cchar *)
{
   zdialog  *zd;
   int      zstat;

   F1_help_topic = "map_search_range";
   
   zd = zdialog_new("map search range",Mwin,Bdone,null);
   zdialog_add_widget(zd,"hbox","hbox","dialog");
   zdialog_add_widget(zd,"label","label","hbox",ZTX("search range (km)"),"space=5");
   zdialog_add_widget(zd,"spin","range","hbox","0.1|100|0.1|10");                //  15.12
   zdialog_stuff(zd,"range",geomap_range);
   zdialog_restore_inputs(zd);                                                   //  16.02
   zdialog_run(zd);
   zstat = zdialog_wait(zd);
   if (zstat != 1) {
      zdialog_free(zd);
      return;
   }
   zdialog_fetch(zd,"range",geomap_range);
   zdialog_free(zd);
   return;
}


/********************************************************************************/

//  Convert latitude and longitude into map position px/py.
//  Return 0 if OK, +N if error (off the map).

int geomap_position(float flati, float flongi, int &px, int &py)
{
   using namespace geomap;

   float    flati1, flati2, flongi1, flongi2;
   float    zww, qy, qy2;

   flati1 = mflati[0];                                                           //  map latitude low - high range
   flati2 = mflati[1];
   flongi1 = mflongi[0];                                                         //  map longitude low - high range
   flongi2 = mflongi[1];

   px = py = 0;

   if (flati < flati1 || flati >= flati2) return 1;                              //  flati/flongi outside map limits
   if (flongi < flongi1 || flongi >= flongi2) return 1;

   px = (flongi - flongi1) / (flongi2 - flongi1) * mapww;                        //  return px position

   zww = mapww * 360.0 / (flongi2 - flongi1);                                    //  width for -180 to +180 longitude

   flati1 = flati1 / RAD;                                                        //  convert to radians
   flati2 = flati2 / RAD;
   flati = flati / RAD;

   qy2 = (zww/2/PI) * (log(tan(flati2/2 + PI/4)));                               //  flati2 distance from equator
   qy =  (zww/2/PI) * (log(tan(flati/2 + PI/4)));                                //  flati distance from equator
   py = qy2 - qy;                                                                //  return py position

   if (px < 2 || px > mapww-3) return 1;                                         //  out of bounds
   if (py < 2 || py > maphh-3) return 1;                                         //  includes margins for red dot

   return 0;
}


//  Convert map position px/py into latitude and longitude.
//  Return 0 if OK, +N if error (off the map).

int geomap_coordinates(int px, int py, float &flati, float &flongi)
{
   using namespace geomap;

   float    flati1, flati2, flongi1, flongi2;
   float    zww, qy, qy2;

   flati = flongi = 0;
   if (px < 0 || px > mapww) return 1;                                           //  px/py outside map size
   if (py < 0 || py > maphh) return 1;

   flati1 = mflati[0];                                                           //  map latitude low - high range
   flati2 = mflati[1];
   flongi1 = mflongi[0];                                                         //  map longitude low - high range
   flongi2 = mflongi[1];

   flongi = flongi1 + (1.0 * px / mapww) * (flongi2 - flongi1);                  //  return longitude

   zww = mapww * 360.0 / (flongi2 - flongi1);                                    //  width for -180 to +180 longitude

   flati1 = flati1 / RAD;                                                        //  convert to radians
   flati2 = flati2 / RAD;

   qy2 = (zww/2/PI) * (log(tan(flati2/2 + PI/4)));                               //  lat2 distance from equator
   qy2 = qy2 - py;                                                               //  py distance from equator
   qy = fabsf(qy2);

   flati = 2 * atan(exp(2*PI*qy/zww)) - PI/2;
   if (qy2 < 0) flati = -flati;

   flati = flati * RAD;                                                          //  return latitude
   return 0;
}


//  paint red dots corresponding to image locations on map

void geomap_paint_dots()                                                         //  15.01
{
   int      ii, err;
   int      mx, my, dx, dy;
   float    flati, flongi, radius;

   if (! Wstate.fpxb) return;                                                    //  no map loaded

   radius = map_dotsize / 2;                                                     //  16.05
   if (Cstate->mscale >= 1) radius = 4;

   cairo_set_source_rgb(mwcr,1,0,0);

   for (ii = 0; ii < Ngeolocs; ii++)                                             //  paint red dots on map
   {                                                                             //    where images are present
      flati = geolocs[ii].flati;
      flongi = geolocs[ii].flongi;
      err = geomap_position(flati,flongi,mx,my);
      if (err) continue;
      dx = Cstate->mscale * mx - Cstate->morgx + Cstate->dorgx;
      dy = Cstate->mscale * my - Cstate->morgy + Cstate->dorgy;
      if (dx < 0 || dx > Dww-1) continue;
      if (dy < 0 || dy > Dhh-1) continue;
      cairo_arc(mwcr,dx,dy,radius,0,2*PI);
      cairo_fill(mwcr);
   }

   return;
}


/********************************************************************************/

//  Respond to mouse movement and left clicks on geomap image.
//  Set longitude and latitude, and location and country.
//  Show images near clicked location.

void geomap_mousefunc()
{
   int         err, mx, my, px, py, ii, minii;
   char        *locatn, *country;
   float       flati, flongi, glati, glongi;
   float       dist, mindist;
   float       mscale = Cstate->mscale;
   static      char  *ploc = 0;
   char        text[20];

   if (checkpend("edit busy block")) return;                                     //  check nothing pending              15.10
   if (Cstate != &Wstate) return;                                                //  view mode not world maps
   if ((Mxdrag || Mydrag)) return;                                               //  pan/scroll - handle normally
   if (RMclick) return;                                                          //  zoom - fit window, handle normally
   if (LMclick && mscale < 1) return;                                            //  handle normally if not full size
   if (! Wstate.fpxb) return;

   mx = Mxposn;                                                                  //  mouse position, image space
   my = Myposn;

   err = geomap_coordinates(mx,my,flati,flongi);
   if (err) return;                                                              //  off the map

   dist = mindist = 999999;
   minii = 0;

   for (ii = 0; ii < Ngeolocs; ii++)                                             //  find nearest location/country
   {
      glati = geolocs[ii].flati;
      dist = (flati - glati) * (flati - glati);
      if (dist > mindist) continue;
      glongi = geolocs[ii].flongi;
      dist += (flongi - glongi) * (flongi - glongi);                             //  degrees**2
      if (dist > mindist) continue;
      mindist = dist;
      minii = ii;
   }
   
   ii = minii;
   glati = geolocs[ii].flati;                                                    //  closest known place
   glongi = geolocs[ii].flongi;
   locatn = geolocs[ii].location;
   country = geolocs[ii].country;

   err = geomap_position(glati,glongi,px,py);                                    //  corresp. map image position
   dist = sqrtf((px-mx) * (px-mx) + (py-my) * (py-my));
   dist = dist * mscale;                                                         //  (mouse - map) in pixels
   if (dist > 10) locatn = country = 0;                                          //   > capture distance

   if (LMclick)                                                                  //  left mouse click
   {
      LMclick = 0;
      poptext_window(0,0,0,0,0,0);                                               //  remove popup

      if (zd_mapgeotags) {
         zdialog_stuff(zd_mapgeotags,"location",locatn);                         //  stuff calling dialog
         zdialog_stuff(zd_mapgeotags,"country",country);
         zdialog_stuff(zd_mapgeotags,"lati",flati);
         zdialog_stuff(zd_mapgeotags,"longi",flongi);
         zdialog_send_event(zd_mapgeotags,"geomap");                             //  activate calling dialog
      }

      else if (locatn)
         find_geomap_images(flati,flongi,geomap_range);                          //  show images in range of location   16.06

      else {
         snprintf(text,20,"%.5f %.5f",flati,flongi);                             //  show coordinates                   15.12
         poptext_window(text,MWIN,Mwxposn,Mwyposn,0.1,3);
      }
   }

   else if (locatn) {                                                            //  mouse movement, no click
      if (! ploc || ! strmatch(locatn,ploc)) {
         poptext_window(locatn,MWIN,Mwxposn,Mwyposn,0.1,1);                      //  popup the city/location name at mouse
         ploc = locatn;
      }
   }

   else if (ploc) {
      poptext_window(0,0,0,0,0,0);                                               //  remove popup
      ploc = 0;
   }

   return;
}


/********************************************************************************/

//  find images within a KM range of a given geolocation, show gallery of images.
//  privat function for geomap_mousefunc(), called when a location is clicked

void find_geomap_images(float flati, float flongi, float km)
{
   int            ftf, err, nn = 0;
   char           imagefile[XFCC];
   float          glati, glongi, grange;
   cchar          *pp;
   FILE           *fid;
   sxrec_t        sxrec;

   fid = fopen(searchresults_file,"w");                                          //  open output file
   if (! fid) {
      zmessageACK(Mwin,"output file error: %s",strerror(errno));
      return;
   }

   ftf = 1;

   while (true)                                                                  //  read image index recs.
   {
      err = read_sxrec_seq(sxrec,ftf);
      if (err) break;

      strncpy0(imagefile,sxrec.file,XFCC);                                       //  save filespec

      pp = strField(sxrec.gtags,'^',3);                                          //  latitude
      if (! pp) goto freemem;
      if (strmatch(pp,"null")) goto freemem;
      glati = atof(pp);

      pp = strField(sxrec.gtags,'^',4);                                          //  longitude
      if (! pp) goto freemem;
      if (strmatch(pp,"null")) goto freemem;
      glongi = atof(pp);
      
      grange = earth_distance(flati,flongi,glati,glongi);                        //  16.06

      if (grange <= km) {                                                        //  within distance limit, select
         fprintf(fid,"%s\n",imagefile);                                          //  output matching file
         nn++;
      }

   freemem:
      zfree(sxrec.file);
      zfree(sxrec.tags);
      zfree(sxrec.capt);
      zfree(sxrec.comms);
      zfree(sxrec.gtags);
   }

   fclose(fid);

   if (! nn) {
      poptext_mouse(ZTX("No matching images found"),10,0,0,3);
      return;
   }

   free_resources();
   navi::gallerytype = SEARCH;                                                   //  search results
   gallery(searchresults_file,"initF");                                          //  generate gallery of matching files
   m_viewmode(0,"G");
   gallery(0,"paint",0);                                                         //  15.05

   return;
}


/********************************************************************************/

//  free memory lorge memory used for geomap image
//  used by edit_setup() to maximize available memory

void free_geomap()
{
   if (Wstate.fpxb) PXB_free(Wstate.fpxb);
   Wstate.fpxb = 0;
   return;
}


/********************************************************************************/

//  OSM maps using libchamplain (M view)                                         //  OSM                                16.05

namespace OSM_maps
{
   GtkWidget                  *mapwidget = 0;
   ChamplainView              *mapview = 0;
   ChamplainMapSourceFactory  *map_factory = 0;
   ChamplainMapSource         *map_source = 0;
   ChamplainMarkerLayer       *markerlayer = 0;
   ChamplainMarker            *marker[maxgeolocs];
   ClutterColor               *markercolor;
}

//  16.07.1 osm-mapquest no longer working
#define MAPSOURCE "osm-mapnik"

void OSM_mousefunc(GtkWidget *, GdkEventButton *, void *);                       //  mouse click function for OSM map
void find_OSM_images(float flati, float flongi);                                 //  find images at clicked position


/********************************************************************************/


//  initialize for OSM maps

void m_load_OSM_map(GtkWidget *, cchar *)                                        //  OSM                                16.05
{
   using namespace OSM_maps;

   int      ii;
   float    flati, flongi;

   F1_help_topic = "images_by_map";

   if (checkpend("all")) return;                                                 //  check, no block

   if (Fnoindex) {
      zmessageACK(Mwin,ZTX("-noindex in use, disabled"));
      return;
   }

   if (! init_geolocs()) return;                                                 //  failed

   Ffuncbusy = 1;
   zmainloop();

   if (markerlayer)                                                              //  refresh markers
   {
      champlain_marker_layer_remove_all(markerlayer);
      
      for (ii = 0; ii < Ngeolocs; ii++) {                                        //  add markers for all geolocations
         flati = geolocs[ii].flati;
         flongi = geolocs[ii].flongi;
         marker[ii] = (ChamplainMarker *) champlain_point_new_full(map_dotsize,markercolor);
         champlain_location_set_location(CHAMPLAIN_LOCATION(marker[ii]),flati,flongi);
         champlain_marker_layer_add_marker(markerlayer,marker[ii]);
      }
      
      gtk_widget_show_all(mapwidget);

      Ffuncbusy = 0;
      return;
   }

   mapwidget = gtk_champlain_embed_new();                                        //  OSM libchamplain map drawing area
   if (! mapwidget) goto fail;
   gtk_container_add(GTK_CONTAINER(Mvbox),mapwidget);

   mapview = gtk_champlain_embed_get_view(GTK_CHAMPLAIN_EMBED(mapwidget));
   if (! mapview) goto fail;
   champlain_view_set_min_zoom_level(mapview,3);
                                                                                 // prettier maps but slower internet access 
   map_factory = champlain_map_source_factory_dup_default();
   map_source = champlain_map_source_factory_create_cached_source(map_factory,MAPSOURCE);
   champlain_view_set_map_source(mapview,map_source);

   markerlayer = champlain_marker_layer_new_full(CHAMPLAIN_SELECTION_SINGLE);
   if (! markerlayer) goto fail;
   champlain_view_add_layer(mapview,CHAMPLAIN_LAYER(markerlayer));
   champlain_marker_layer_set_selection_mode(markerlayer,CHAMPLAIN_SELECTION_NONE);
   markercolor = clutter_color_new(255,0,0,255);

   champlain_marker_layer_remove_all(markerlayer);                               //  remove all markers

   for (ii = 0; ii < Ngeolocs; ii++) {                                           //  add markers for all geolocations
      flati = geolocs[ii].flati;
      flongi = geolocs[ii].flongi;
      marker[ii] = (ChamplainMarker *) champlain_point_new_full(map_dotsize,markercolor);
      champlain_location_set_location(CHAMPLAIN_LOCATION(marker[ii]),flati,flongi);
      champlain_marker_layer_add_marker(markerlayer,marker[ii]);
   }
   
   gtk_widget_add_events(mapwidget,GDK_BUTTON_PRESS_MASK);                       //  connect mouse events to OSM map
   G_SIGNAL(mapwidget,"button-press-event",OSM_mousefunc,0);
   G_SIGNAL(mapwidget,"button-release-event",OSM_mousefunc,0);
   G_SIGNAL(mapwidget,"motion-notify-event",OSM_mousefunc,0);

   gtk_widget_show_all(mapwidget);

   Ffuncbusy = 0;
   return;

fail:
   zmessageACK(Mwin,"OSM/libchamplain failure");
   Ffuncbusy = 0;
   return;
}


/********************************************************************************/

//  map zoom-in on given locaton

void m_OSM_zoomin(GtkWidget *, cchar *menu)                                      //  OSM                                16.06
{
   using namespace OSM_maps;

   int            err;
   static char    *file = 0;
   char           *lati, *longi;
   float          flati, flongi;
   sxrec_t        sxrec;
   
   if (file) zfree(file);
   file = 0;

   if (clicked_file) {                                                           //  use clicked file if present
      file = clicked_file;
      clicked_file = 0;
   }
   else if (curr_file)                                                           //  else current file
      file = zstrdup(curr_file);
   else return;
   
   err = get_sxrec(sxrec,file);
   if (err) return;
   
   lati = (char *) strField(sxrec.gtags,'^',3);
   longi = (char *) strField(sxrec.gtags,'^',4);
   if (! longi) goto nocoords;

   err = validate_latlong(lati,longi,flati,flongi);
   if (err) goto nocoords;
   
   m_viewmode(0,"M");

   if (! mapview) m_load_OSM_map(0,0);
   champlain_view_center_on(mapview,flati,flongi);
   champlain_view_set_zoom_level(mapview,12);
   goto freemem;

nocoords:
   zmessageACK(Mwin,ZTX("image file has no latitude/longitude"));

freemem:
   zfree(sxrec.file);
   zfree(sxrec.tags);
   zfree(sxrec.capt);
   zfree(sxrec.comms);
   zfree(sxrec.gtags);
   return;
}


/********************************************************************************/

//  Respond to mouse clicks on OSM image.

void OSM_mousefunc(GtkWidget *widget, GdkEventButton *event, void *)             //  OSM                                16.05
{
   using namespace OSM_maps;

   int         mx, my, px, py;
   int         ii, minii;
   int         capturedist = (map_dotsize + 2) / 2;                              //  mouse - marker capture distance
   char        *locatn, *country;
   float       flati, flongi, glati, glongi;
   float       dist, mindist;
   static      char  *ploc = 0;
   char        text[20];
   
   if (! mapview) return;                                                        //  OSM map not available

   mx = event->x;                                                                //  mouse position
   my = event->y;
   
   flati = champlain_view_y_to_latitude(mapview,my);                             //  corresp. map coordinates
   flongi = champlain_view_x_to_longitude(mapview,mx);

   dist = mindist = 999999;
   minii = 0;

   for (ii = 0; ii < Ngeolocs; ii++)                                             //  find nearest location/country
   {
      glati = geolocs[ii].flati;
      dist = (flati - glati) * (flati - glati);
      if (dist > mindist) continue;
      glongi = geolocs[ii].flongi;
      dist += (flongi - glongi) * (flongi - glongi);                             //  degrees**2
      if (dist > mindist) continue;
      mindist = dist;
      minii = ii;
   }
   
   ii = minii;
   glati = geolocs[ii].flati;                                                    //  closest known place
   glongi = geolocs[ii].flongi;
   locatn = geolocs[ii].location;
   country = geolocs[ii].country;
   
   px = champlain_view_longitude_to_x(mapview,glongi);                           //  corresp. map location
   py = champlain_view_latitude_to_y(mapview,glati);
   dist = sqrtf((px-mx) * (px-mx) + (py-my) * (py-my));                          //  distance in pixels
   if (dist > capturedist) locatn = country = 0;                                 //   > capture distance

   if (event->type == GDK_BUTTON_RELEASE)
   {
      poptext_window(0,0,0,0,0,0);                                               //  remove popup

      if (zd_mapgeotags) {
         zdialog_stuff(zd_mapgeotags,"location",locatn);                         //  stuff calling dialog
         zdialog_stuff(zd_mapgeotags,"country",country);
         zdialog_stuff(zd_mapgeotags,"lati",flati);
         zdialog_stuff(zd_mapgeotags,"longi",flongi);
         zdialog_send_event(zd_mapgeotags,"geomap");                             //  activate calling dialog
      }

      else if (event->button == 1 && locatn)                                     //  left click on marker
         find_OSM_images(flati,flongi);                                          //  show images for location 

      else if (event->button == 3) {                                             //  right click
         snprintf(text,20,"%.5f %.5f",flati,flongi);                             //  show coordinates
         poptext_window(text,MWIN,mx,my,0.1,3);
      }
   }

   else if (locatn) {                                                            //  mouse movement
      if (! ploc || ! strmatch(locatn,ploc)) {
         poptext_window(locatn,MWIN,mx,my,0.1,1);                                //  popup the location name at mouse
         ploc = locatn;
      }
   }

   else if (ploc) {
      poptext_window(0,0,0,0,0,0);                                               //  remove popup
      ploc = 0;
   }
   
   return;
}


//  find images for a given geolocation, show gallery of images.
//  privat function for OSM_mousefunc(), called when a location is clicked

void find_OSM_images(float flati, float flongi)                                  //  OSM                                16.05
{
   using namespace OSM_maps;

   int         ftf, err, nn = 0;
   int         x1, y1, x2, y2;
   int         capturedist = 1 + map_dotsize/2;                                  //  mouse - marker capture distance
   char        imagefile[XFCC];
   float       glati, glongi, grange;
   cchar       *pp;
   FILE        *fid;
   sxrec_t     sxrec;
   
   x1 = champlain_view_longitude_to_x(mapview,flongi);                           //  target map pixel location
   y1 = champlain_view_latitude_to_y(mapview,flati); 

   fid = fopen(searchresults_file,"w");                                          //  open output file
   if (! fid) {
      zmessageACK(Mwin,"output file error: %s",strerror(errno));
      return;
   }

   ftf = 1;

   while (true)                                                                  //  read image index recs.
   {
      err = read_sxrec_seq(sxrec,ftf);
      if (err) break;

      strncpy0(imagefile,sxrec.file,XFCC);                                       //  save filespec

      pp = strField(sxrec.gtags,'^',3);                                          //  latitude
      if (! pp) goto freemem;
      if (strmatch(pp,"null")) goto freemem;
      glati = atof(pp);

      pp = strField(sxrec.gtags,'^',4);                                          //  longitude
      if (! pp) goto freemem;
      if (strmatch(pp,"null")) goto freemem;
      glongi = atof(pp);
      
      x2 = champlain_view_longitude_to_x(mapview,glongi);                        //  image map pixel location
      y2 = champlain_view_latitude_to_y(mapview,glati); 
      
      grange = sqrtf((x1-x2)*(x1-x2) + (y1-y2)*(y1-y2));                         //  mouse - image pixel distance
      if (grange < 1.5 * capturedist) {                                          //  within distance limit, select
         fprintf(fid,"%s\n",imagefile);                                          //  output matching file
         nn++;
      }

   freemem:
      zfree(sxrec.file);
      zfree(sxrec.tags);
      zfree(sxrec.capt);
      zfree(sxrec.comms);
      zfree(sxrec.gtags);
   }

   fclose(fid);

   if (! nn) {
      poptext_mouse(ZTX("No matching images found"),10,0,0,3);
      return;
   }

   free_resources();
   navi::gallerytype = SEARCH;                                                   //  search results
   gallery(searchresults_file,"initF");                                          //  generate gallery of matching files
   m_viewmode(0,"G");
   gallery(0,"paint",0);

   return;
}


/********************************************************************************/

//  Search image tags, geotags, dates, stars, comments, captions
//  to find matching images. This is fast using the image index.
//  Search also any other metadata, but relatively slow.

namespace search_images
{
   zdialog  *zdsearchimages = 0;                                                 //  search images dialog

   char     searchDateFrom[16] = "";                                             //  search images
   char     searchDateTo[16] = "";
   char     searchStarsFrom[4] = "";
   char     searchStarsTo[4] = "";

   char     searchtags[tagScc] = "";                                             //  search tags list
   char     searchtext[tagScc] = "";                                             //  search comments & captions word list
   char     searchfiles[tagScc] = "";                                            //  search files list

   char     searchLocations[200] = "";                                           //  search locations

   int      Fscanall, Fscancurr, Fnewset, Faddset, Fremset;
   int      Fdates, Ftext, Ffiles, Ftags, Fstars, Flocs;
   int      Flastver, Fmeta;
   int      Falltags, Falltext, Fallfiles;
   int      Frepgallery, Frepmeta;

   int      Nsearchkeys = 0;
   char     *searchkeys[5];                                                      //  metadata keys to search
   char     *searchvalues[5];                                                    //  data values to search for
   char     matchtype[5];                                                        //  match type: string match or number < = >
}

using namespace search_images;


//  menu function

void m_search_images(GtkWidget *, cchar *)
{
   void  search_searchtags_clickfunc(GtkWidget *widget, int line, int pos);
   void  search_matchtags_clickfunc(GtkWidget *widget, int line, int pos);
   void  search_deftags_clickfunc(GtkWidget *widget, int line, int pos);
   int   searchimages_dialog_event(zdialog*, cchar *event);

   zdialog     *zd;
   GtkWidget   *widget;

   F1_help_topic = "search_images";

   if (checkpend("all")) return;                                                 //  check nothing pending

   if (Fnoindex) {
      zmessageACK(Mwin,ZTX("-noindex in use, disabled"));
      return;
   }

/***
          ___________________________________________________________
         |              Search Image Metadata                        |
         |                                                           |
         |  images to search: (o) all  (o) current set only          |
         |  matching images: (o) new set  (o) add to set  (o) remove |
         |  report type: (o) gallery  (o) metadata                   |
         |  date range   [___________] [___________] (yyyymmdd)      |
         |  stars range  [__] [__]   [x] last version only  all/any  |
         |  search tags  [________________________________] (o) (o)  |
         |  search text  [________________________________] (o) (o)  |
         |  search files [________________________________] (o) (o)  |
         |  search locations [_____________________________________] |
         |   - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
         |  search metadata [Add] (*)                                |
         |   - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
         |  Enter Search Tag [________________________]              |
         |  Matching Tags [_______________________________________]  |
         |   - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
         |  Defined Tags Category [______________________________|v] |
         |  |                                                      | |
         |  |                                                      | |
         |  |                                                      | |
         |  |                                                      | |
         |  |                                                      | |
         |  |                                                      | |
         |  |______________________________________________________| |
         |                                                           |
         |                                [clear] [proceed] [cancel] |
         |___________________________________________________________|

***/

   zd = zdialog_new(ZTX("Search Image Metadata"),Mwin,Bclear,Bproceed,Bcancel,null);
   zdsearchimages = zd;

   zdialog_add_widget(zd,"hbox","hbs1","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labs1","hbs1",ZTX("images to search:"),"space=5");
   zdialog_add_widget(zd,"radio","allimages","hbs1",ZTX("all"),"space=3");
   zdialog_add_widget(zd,"radio","currset","hbs1",ZTX("current set only"),"space=5");

   zdialog_add_widget(zd,"hbox","hbm1","dialog");
   zdialog_add_widget(zd,"label","labs1","hbm1",ZTX("matching images:"),"space=5");
   zdialog_add_widget(zd,"radio","newset","hbm1",ZTX("new set"),"space=5");
   zdialog_add_widget(zd,"radio","addset","hbm1",ZTX("add to set"),"space=5");
   zdialog_add_widget(zd,"radio","remset","hbm1",ZTX("remove"),"space=5");

   zdialog_add_widget(zd,"hbox","hbrt","dialog");
   zdialog_add_widget(zd,"label","labrt","hbrt",ZTX("report type:"),"space=5");
   zdialog_add_widget(zd,"radio","repgallery","hbrt",ZTX("gallery"),"space=5");
   zdialog_add_widget(zd,"radio","repmeta","hbrt","Metadata","space=5");

   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=3");
   zdialog_add_widget(zd,"vbox","vb1","hb1",0,"homog|space=5");
   zdialog_add_widget(zd,"vbox","vb2","hb1",0,"expand");

   zdialog_add_widget(zd,"label","labD","vb1",ZTX("date range"));
   zdialog_add_widget(zd,"label","labS","vb1",ZTX("stars range"));
   zdialog_add_widget(zd,"label","labT","vb1",ZTX("search tags"));
   zdialog_add_widget(zd,"label","labT","vb1",ZTX("search text"));
   zdialog_add_widget(zd,"label","labF","vb1",ZTX("search files"));
   
   zdialog_add_widget(zd,"hbox","hbD","vb2",0,"space=1");
   zdialog_add_widget(zd,"entry","datefrom","hbD",0,"size=12");
   zdialog_add_widget(zd,"entry","dateto","hbD",0,"size=12");
   zdialog_add_widget(zd,"label","labD","hbD",ZTX("(yyyymmdd)"),"space=5");

   zdialog_add_widget(zd,"hbox","hbS","vb2",0,"space=1");
   zdialog_add_widget(zd,"entry","starsfrom","hbS",0,"size=2");
   zdialog_add_widget(zd,"entry","starsto","hbS",0,"size=2");
   zdialog_add_widget(zd,"check","lastver","hbS",ZTX("last version only"),"space=10");
   zdialog_add_widget(zd,"label","space","hbS",0,"expand");
   zdialog_add_widget(zd,"label","all-any","hbS",ZTX("all/any"),"space=2");

   zdialog_add_widget(zd,"hbox","hbT","vb2",0,"space=3");
   zdialog_add_widget(zd,"frame","frameT","hbT",0,"expand");
   zdialog_add_widget(zd,"text","searchtags","frameT",0,"expand|wrap");
   zdialog_add_widget(zd,"radio","alltags","hbT",0);
   zdialog_add_widget(zd,"radio","anytags","hbT",0);

   zdialog_add_widget(zd,"hbox","hbC","vb2",0,"space=1|expand");
   zdialog_add_widget(zd,"entry","searchtext","hbC",0,"expand");
   zdialog_add_widget(zd,"radio","alltext","hbC",0);
   zdialog_add_widget(zd,"radio","anytext","hbC",0);

   zdialog_add_widget(zd,"hbox","hbF","vb2",0,"space=1|expand");
   zdialog_add_widget(zd,"entry","searchfiles","hbF",0,"expand");
   zdialog_add_widget(zd,"radio","allfiles","hbF",0);
   zdialog_add_widget(zd,"radio","anyfiles","hbF",0);

   zdialog_add_widget(zd,"hbox","hblocs","dialog");
   zdialog_add_widget(zd,"label","lablocs","hblocs",ZTX("search locations"),"space=5");
   zdialog_add_widget(zd,"entry","searchlocs","hblocs",0,"expand");

   zdialog_add_widget(zd,"hsep","sep","dialog",0,"space=3");

   zdialog_add_widget(zd,"hbox","hbmeta","dialog","space=3");
   zdialog_add_widget(zd,"label","labmeta","hbmeta",ZTX("search other metadata"),"space=5");
   zdialog_add_widget(zd,"button","addmeta","hbmeta",Badd,"space=8");
   zdialog_add_widget(zd,"label","metadata#","hbmeta","(  )");
   
   zdialog_add_widget(zd,"hsep","sep","dialog",0,"space=3");

   zdialog_add_widget(zd,"hbox","hbnt","dialog",0,"space=1");
   zdialog_add_widget(zd,"label","labnt","hbnt",ZTX("Enter Search Tag"),"space=3");
   zdialog_add_widget(zd,"entry","entertag","hbnt");

   zdialog_add_widget(zd,"hbox","hbmt","dialog",0,"space=1");
   zdialog_add_widget(zd,"label","labmt","hbmt",ZTX("Matching Tags"),"space=3");
   zdialog_add_widget(zd,"frame","frmt","hbmt",0,"space=3|expand");
   zdialog_add_widget(zd,"text","matchtags","frmt",0,"wrap");

   zdialog_add_widget(zd,"hsep","sep","dialog",0,"space=3");

   zdialog_add_widget(zd,"hbox","hbdt1","dialog");
   zdialog_add_widget(zd,"label","labdt","hbdt1",ZTX("Defined Tags Category"),"space=3");
   zdialog_add_widget(zd,"combo","defcats","hbdt1",0,"expand|space=5");
   zdialog_add_widget(zd,"hbox","hbdt2","dialog",0,"expand");
   zdialog_add_widget(zd,"frame","frdt2","hbdt2",0,"expand|space=3");
   zdialog_add_widget(zd,"scrwin","swdt2","frdt2",0,"expand");
   zdialog_add_widget(zd,"text","deftags","swdt2",0,"wrap");

   widget = zdialog_widget(zd,"searchtags");                                     //  tag widget mouse functions
   textwidget_set_clickfunc(widget,search_searchtags_clickfunc);

   widget = zdialog_widget(zd,"matchtags");
   textwidget_set_clickfunc(widget,search_matchtags_clickfunc);

   widget = zdialog_widget(zd,"deftags");
   textwidget_set_clickfunc(widget,search_deftags_clickfunc);

   zdialog_stuff(zd,"allimages",1);                                              //  defaults
   zdialog_stuff(zd,"currset",0);
   zdialog_stuff(zd,"newset",1);
   zdialog_stuff(zd,"addset",0);
   zdialog_stuff(zd,"remset",0);
   zdialog_stuff(zd,"repgallery",1);
   zdialog_stuff(zd,"repmeta",0);
   zdialog_stuff(zd,"lastver",0);
   zdialog_stuff(zd,"alltags",0);
   zdialog_stuff(zd,"anytags",1);
   zdialog_stuff(zd,"alltext",0);
   zdialog_stuff(zd,"anytext",1);
   zdialog_stuff(zd,"allfiles",0);
   zdialog_stuff(zd,"anyfiles",1);

   zdialog_restore_inputs(zd);                                                   //  preload prior user inputs

   load_deftags();                                                               //  stuff defined tags into dialog
   deftags_stuff(zd,"ALL");
   defcats_stuff(zd);                                                            //  and defined categories             15.08

   zdialog_resize(zd,0,700);                                                     //  start dialog
   zdialog_run(zd,searchimages_dialog_event,"save");
   zdialog_wait(zd);                                                             //  wait for dialog completion
   zdialog_free(zd);

   return;
}

//  mouse click functions for search tags and defined tags widgets

void search_searchtags_clickfunc(GtkWidget *widget, int line, int pos)           //  search tag clicked
{
   char     *txline, *txtag, end = 0;

   txline = textwidget_get_line(widget,line,0);
   if (! txline) return;

   txtag = textwidget_get_word(txline,pos,",;:",end);
   if (! txtag) { zfree(txline); return; }

   del_tag(txtag,searchtags);                                                    //  remove from search list
   zdialog_stuff(zdsearchimages,"searchtags",searchtags);

   zfree(txline);
   zfree(txtag);
   return;
}

void search_matchtags_clickfunc(GtkWidget *widget, int line, int pos)            //  matching tag was clicked           15.07
{
   char     *txline, *txtag, end = 0;

   txline = textwidget_get_line(widget,line,0);
   if (! txline) return;

   txtag = textwidget_get_word(txline,pos,",;",end);
   if (! txtag) { zfree(txline); return; }

   add_tag(txtag,searchtags,tagScc);                                             //  add to search tag list

   zdialog_stuff(zdsearchimages,"entertag","");                                  //  update dialog widgets
   zdialog_stuff(zdsearchimages,"matchtags","");
   zdialog_stuff(zdsearchimages,"searchtags",searchtags);

   zdialog_goto(zdsearchimages,"entertag");                                      //  focus back to entertag widget

   zfree(txline);
   zfree(txtag);
   return;
}

void search_deftags_clickfunc(GtkWidget *widget, int line, int pos)              //  defined tag clicked
{
   char     *txline, *txtag, end = 0;

   txline = textwidget_get_line(widget,line,0);
   if (! txline) return;

   txtag = textwidget_get_word(txline,pos,",;:",end);
   if (! txtag || end == ':') { zfree(txline); return; }                         //  tag category clicked, ignore       15.05

   add_tag(txtag,searchtags,tagScc);                                             //  add to search tag list
   zdialog_stuff(zdsearchimages,"searchtags",searchtags);

   zfree(txline);
   zfree(txtag);
   return;
}


//  search images dialog event and completion callback function

int searchimages_dialog_event(zdialog *zd, cchar *event)                         //  overhauled
{
   using namespace navi;

   int  searchimages_select(sxrec_t &sxrec);
   int  searchimages_metadata_dialog(zdialog *zd);
   int  searchimages_metadata_report();

   cchar    dateLoDefault[16] = "000001010000";                                  //  date: 0000/01/01  time: 00:00
   cchar    dateHiDefault[16] = "209912312359";                                  //  date: 2099/12/31  time: 23:59
   char     nowdatetime[16];

   char     *file;
   char     **flist, *pp, buffer[XFCC];
   int      ftf, match, ii, jj, cc, err;
   int      nt, cc1, cc2, ff;
   int      Nadded, Nremoved, Nleft, Npver;
   sxrec_t  sxrec;
   FILE     *fid;
   char     *pp1, *pp2;
   char     entertag[tagcc], matchtags[20][tagcc];
   char     matchtagstext[(tagcc+2)*20];
   char     catgname[tagcc];

   if (strmatch(event,"escape")) zd->zstat = 3;                                  //  escape = cancel                    15.07
   
   if (strmatch(event,"addmeta"))                                                //  get other metadata criteria
      Fmeta = searchimages_metadata_dialog(zd);

   if (Fmeta) zdialog_stuff(zd,"metadata#","(*)");                               //  show presense of metadata criteria
   else zdialog_stuff(zd,"metadata#","(_)");

   if (zd->zstat == 1)                                                           //  clear selection criteria 
   {
      zdialog_stuff(zd,"allimages",1);
      zdialog_stuff(zd,"currset",0);
      zdialog_stuff(zd,"newset",1);
      zdialog_stuff(zd,"addset",0);
      zdialog_stuff(zd,"remset",0);
      zdialog_stuff(zd,"repgallery",1);
      zdialog_stuff(zd,"repmeta",0);
      zdialog_stuff(zd,"lastver",0);
      zdialog_stuff(zd,"alltags",0);
      zdialog_stuff(zd,"anytags",1);
      zdialog_stuff(zd,"alltext",0);
      zdialog_stuff(zd,"anytext",1);
      zdialog_stuff(zd,"allfiles",0);
      zdialog_stuff(zd,"anyfiles",1);
      zdialog_stuff(zd,"datefrom","");
      zdialog_stuff(zd,"dateto","");
      zdialog_stuff(zd,"starsfrom","");
      zdialog_stuff(zd,"starsto","");
      zdialog_stuff(zd,"searchtags","");
      zdialog_stuff(zd,"searchtext","");
      zdialog_stuff(zd,"searchfiles","");
      zdialog_stuff(zd,"searchlocs","");                                         //  16.06

      *searchtags = 0;                                                           //  16.02

      Flocs = 0;                                                                 //  16.06
      *searchLocations = 0;

      zdialog_stuff(zd,"metadata#","( )");
      Fmeta = 0;
      Nsearchkeys = 0;

      zd->zstat = 0;                                                             //  keep dialog active
      return 1;
   }

   if (strmatch(event,"entertag"))                                               //  new tag is being typed in          15.07
   {
      zdialog_stuff(zd,"matchtags","");                                          //  clear matchtags in dialog

      zdialog_fetch(zd,"entertag",entertag,tagcc);                               //  get chars. typed so far
      cc1 = strlen(entertag);
      
      for (ii = jj = 0; ii <= cc1; ii++) {                                       //  remove foul characters
         if (strchr(",:;",entertag[ii])) continue;
         entertag[jj++] = entertag[ii];
      }
      
      if (jj < cc1) {                                                            //  something was removed
         entertag[jj] = 0;
         cc1 = jj;
         zdialog_stuff(zd,"entertag",entertag);
      }

      if (cc1 < 2) return 1;                                                     //  wait for at least 2 chars.

      for (ii = nt = 0; ii < maxtagcats; ii++)                                   //  loop all categories
      {
         pp2 = tags_deftags[ii];                                                 //  category: aaaaaa, bbbbb, ... tagN,
         if (! pp2) continue;                                                    //            |     |
         pp2 = strchr(pp2,':');                                                  //            pp1   pp2
         
         while (true)                                                            //  loop all deftags in category
         {
            pp1 = pp2 + 2;
            if (! *pp1) break;
            pp2 = strchr(pp1,',');
            if (! pp2) break;
            if (strmatchcaseN(entertag,pp1,cc1)) {                               //  deftag matches chars. typed so far
               cc2 = pp2 - pp1;
               strncpy(matchtags[nt],pp1,cc2);                                   //  save deftags that match
               matchtags[nt][cc2] = 0;
               if (++nt == 20) return 1;                                         //  quit if 20 matches or more
            }
         }
      }
      
      if (nt == 0) return 1;                                                     //  no matches

      pp1 = matchtagstext;

      for (ii = 0; ii < nt; ii++)                                                //  make deftag list: aaaaa, bbb, cccc ...
      {
         strcpy(pp1,matchtags[ii]);
         pp1 += strlen(pp1);
         strcpy(pp1,", ");
         pp1 += 2;
      }
      
      zdialog_stuff(zd,"matchtags",matchtagstext);                               //  stuff matchtags in dialog
      return 1;
   }

   if (strmatch(event,"enter"))                                                  //  KB Enter, tag finished             15.07
   {
      zdialog_fetch(zd,"entertag",entertag,tagcc);                               //  get finished tag
      cc1 = strlen(entertag);
      if (! cc1) return 1;
      if (entertag[cc1-1] == '\n') {                                             //  remove newline character
         cc1--;
         entertag[cc1] = 0;
      }

      for (ii = ff = 0; ii < maxtagcats; ii++)                                   //  loop all categories
      {
         pp2 = tags_deftags[ii];                                                 //  category: aaaaaa, bbbbb, ... tagN,
         if (! pp2) continue;                                                    //            |     |
         pp2 = strchr(pp2,':');                                                  //            pp1   pp2
         
         while (true)                                                            //  loop all deftags in category
         {
            pp1 = pp2 + 2;
            if (! *pp1) break;
            pp2 = strchr(pp1,',');
            if (! pp2) break;
            cc2 = pp2 - pp1;
            if (cc2 != cc1) continue;
            if (strmatchcaseN(entertag,pp1,cc1)) {                               //  entered tag matches deftag
               strncpy(entertag,pp1,cc1);                                        //  use deftag upper/lower case
               ff = 1;
               break;
            }
         }

         if (ff) break;
      }
      
      if (! ff) {
         zmessageACK(Mwin,ZTX("not a defined tag: %s"),entertag);
         return 1;
      }

      add_tag(entertag,searchtags,tagScc);                                       //  add to search tag list

      zdialog_stuff(zd,"entertag","");                                           //  update dialog widgets
      zdialog_stuff(zd,"searchtags",searchtags);
      zdialog_stuff(zd,"matchtags","");

      zdialog_goto(zd,"entertag");                                               //  put focus back on entertag widget
      return 1;
   }

   if (strmatch(event,"defcats")) {                                              //  new tag category selection         15.08
      zdialog_fetch(zd,"defcats",catgname,tagcc);
      deftags_stuff(zd,catgname);
   }
   
   if (! zd->zstat) return 1;                                                    //  wait for dialog completion
   if (zd->zstat != 2) return 1;                                                 //  cancel if not [proceed]

//  inputs are complete. perform the search.

   zdialog_fetch(zd,"allimages",Fscanall);                                       //  search all images
   zdialog_fetch(zd,"currset",Fscancurr);                                        //  search current set (gallery)
   zdialog_fetch(zd,"newset",Fnewset);                                           //  matching images --> new set
   zdialog_fetch(zd,"addset",Faddset);                                           //  add matching image to set
   zdialog_fetch(zd,"remset",Fremset);                                           //  remove matching images from set

   if (Fremset && Fscanall) {                                                    //  illogical search
      zmessageACK(Mwin,ZTX("to remove images from current set, \n"
                           "search current set"));
      zd->zstat = 0;                                                             //  keep dialog active
      return 1;
   }

   if (Faddset && Fscancurr) {
      zmessageACK(Mwin,ZTX("to add images to current set, \n"
                           "search all images"));
      zd->zstat = 0;                                                             //  keep dialog active
      return 1;
   }

   zdialog_fetch(zd,"repgallery",Frepgallery);                                   //  gallery report 
   zdialog_fetch(zd,"repmeta",Frepmeta);                                         //  metadata report
   zdialog_fetch(zd,"lastver",Flastver);                                         //  get last versions only

   zdialog_fetch(zd,"datefrom",searchDateFrom,15);                               //  get search date range
   zdialog_fetch(zd,"dateto",searchDateTo,15);
   zdialog_fetch(zd,"starsfrom",searchStarsFrom,2);                              //  get search stars range
   zdialog_fetch(zd,"starsto",searchStarsTo,2);
   zdialog_fetch(zd,"searchtags",searchtags,tagScc);                             //  get search tags
   zdialog_fetch(zd,"searchtext",searchtext,tagScc);                             //  get search text*
   zdialog_fetch(zd,"searchfiles",searchfiles,tagScc);                           //  get search /path*/file*
   zdialog_fetch(zd,"searchlocs",searchLocations,200);                           //  get search locations

   zdialog_fetch(zd,"alltags",Falltags);                                         //  get match all/any options
   zdialog_fetch(zd,"alltext",Falltext);
   zdialog_fetch(zd,"allfiles",Fallfiles);
   
   Fdates = 0;
   if (*searchDateFrom) Fdates++;                                                //  search date from was given
   else strcpy(searchDateFrom,"000001010000");                                   //  else search from begining of time

   if (*searchDateTo) Fdates++;                                                  //  search date to was given
   else strcpy(searchDateTo,"209912312359");                                     //  else search to end of time

   if (Fdates) {                                                                 //  complete partial date/time data
      cc = strlen(searchDateFrom);
      for (ii = cc; ii < 12; ii++)                                               //  default date from:
         searchDateFrom[ii] = dateLoDefault[ii];                                 //    date: 0000/01/01 time: 00:00
      cc = strlen(searchDateTo);
      for (ii = cc; ii < 12; ii++)                                               //  default date to:
         searchDateTo[ii] = dateHiDefault[ii];                                   //    date: 9999/12/31 time: 23:59

      ff = 0;                                                                    //  check search dates reasonable      15.08
      if (! datetimeOK(searchDateFrom)) ff = 1;                                  //  invalid year/mon/day (e.g. mon 13)
      if (! datetimeOK(searchDateTo)) ff = 1;                                    //    or hour/min/sec (e.g. hour 33)
      compact_time(time(0),nowdatetime);                                         //  get NOW date/time
      if (strcmp(searchDateFrom,nowdatetime) >= 0) ff = 1;                       //  search from date >= NOW
      if (strcmp(searchDateFrom,searchDateTo) >= 0) ff = 1;                      //  search from date >= search to date
      if (ff) {
         zmessageACK(Mwin,ZTX("search dates not reasonable \n %s  %s"),
                                    searchDateFrom,searchDateTo);
         zd->zstat = 0;
         return 1;
      }
   }

   Fstars = 0;
   if (*searchStarsFrom || *searchStarsTo) {
      Fstars = 1;                                                                //  stars was given
      ii = *searchStarsFrom;
      if (! ii) ii = '0';
      if (ii < '0' || ii > '5') Fstars = 0;                                      //  validate inputs
      jj = *searchStarsTo;
      if (! jj) jj = '5';
      if (jj < '0' || jj > '5') Fstars = 0;
      if (jj < ii) Fstars = 0;
      if (! Fstars) {
         zmessageACK(Mwin,ZTX("stars range not reasonable"));
         zd->zstat = 0;
         return 1;
      }
   }

   Ffiles = 0;
   if (! blank_null(searchfiles)) Ffiles = 1;                                    //  search path / file (fragment) was given

   Ftext = 0;
   if (! blank_null(searchtext)) Ftext = 1;                                      //  search text was given

   Ftags = 0;
   if (! blank_null(searchtags)) Ftags = 1;                                      //  search tags was given

   Flocs = 0;
   if (*searchLocations) Flocs = 1;                                              //  search locations was given

   Fmeta = 0;
   if (Nsearchkeys) Fmeta = 1;                                                   //  search other metadata was given

   if (checkpend("all")) return 1;                                               //  check nothing pending
   Ffuncbusy = 1;
   Fblock = 1;

   Nadded = Nremoved = Npver = Nleft = 0;                                        //  image counts

   //  search all images and keep those meeting search criteria
   //  result is gallery of images meeting criteria

   if (Fscanall && Fnewset)
   {
      fid = fopen(searchresults_file,"w");                                       //  open output file
      if (! fid) goto filerror;

      ftf = 1;

      while (true)
      {
         zmainloop(20);                                                          //  keep GTK alive

         err = read_sxrec_seq(sxrec,ftf);                                        //  scan all index recs.
         if (err) break;

         match = searchimages_select(sxrec);                                     //  test against select criteria
         if (match) {                                                            //  all criteria passed
            Nadded++;                                                            //  count matches
            fprintf(fid,"%s\n",sxrec.file);                                      //  save matching filename
         }

         zfree(sxrec.file);                                                      //  free allocated strings
         zfree(sxrec.tags);
         zfree(sxrec.capt);
         zfree(sxrec.comms);
         zfree(sxrec.gtags);
      }

      fclose(fid);
      Nleft = Nadded;
   }

   //  search all images and add those meeting search criteria
   //    to current image set (gallery)

   if (Fscanall && Faddset)
   {
      fid = fopen(searchresults_file,"w");                                       //  open output file
      if (! fid) goto filerror;

      for (ii = 0; ii < navi::Nfiles; ii++)                                      //  scan current gallery
      {
         file = gallery(0,"find",ii);
         if (! file) break;
         if (*file != '!') {                                                     //  skip directories
            fprintf(fid,"%s\n",file);                                            //  add image files to output
            Nleft++;
         }
         zfree(file);                                                            //  free memory
      }

      ftf = 1;

      while (true)
      {
         zmainloop(20);                                                          //  keep GTK alive

         err = read_sxrec_seq(sxrec,ftf);                                        //  scan all index recs.
         if (err) break;

         match = searchimages_select(sxrec);                                     //  test against select criteria
         if (match) {                                                            //  all criteria passed
            Nadded++;                                                            //  count matches
            fprintf(fid,"%s\n",sxrec.file);                                      //  save matching filename
         }

         zfree(sxrec.file);                                                      //  free memory
         zfree(sxrec.tags);
         zfree(sxrec.capt);
         zfree(sxrec.comms);
         zfree(sxrec.gtags);
      }

      fclose(fid);
      Nleft += Nadded;
   }

   //  search current image set and keep only those meeting search criteria

   if (Fscancurr && Fnewset)
   {
      fid = fopen(searchresults_file,"w");                                       //  open output file
      if (! fid) goto filerror;

      for (ii = 0; ii < navi::Nfiles; ii++)                                      //  scan current gallery
      {
         zmainloop(20);                                                          //  keep GTK alive

         file = gallery(0,"find",ii);
         if (! file) break;
         if (*file == '!') {                                                     //  skip directories
            zfree(file);
            continue;
         }

         err = get_sxrec(sxrec,file);
         if (err) {                                                              //  no metadata rec?
            zfree(file);
            continue;
         }

         match = searchimages_select(sxrec);                                     //  test against select criteria
         if (match) {                                                            //  passed
            Nleft++;                                                             //  count retained images
            fprintf(fid,"%s\n",file);                                            //  save retained filename
         }
         else Nremoved++;

         zfree(file);                                                            //  free memory

         zfree(sxrec.file);
         zfree(sxrec.tags);
         zfree(sxrec.capt);
         zfree(sxrec.comms);
         zfree(sxrec.gtags);
      }

      fclose(fid);
   }

   //  search current image set and remove those meeting search criteria

   if (Fscancurr && Fremset)
   {
      fid = fopen(searchresults_file,"w");                                       //  open output file
      if (! fid) goto filerror;

      for (ii = 0; ii < navi::Nfiles; ii++)                                      //  scan current gallery
      {
         zmainloop(20);                                                          //  keep GTK alive

         file = gallery(0,"find",ii);
         if (! file) break;
         if (*file == '!') {                                                     //  skip directories
            zfree(file);
            continue;
         }

         err = get_sxrec(sxrec,file);
         if (err) {                                                              //  no metadata rec?
            zfree(file);
            continue;
         }

         match = searchimages_select(sxrec);                                     //  test against select criteria
         if (! match) {                                                          //  failed
            Nleft++;
            fprintf(fid,"%s\n",file);                                            //  save retained filename
         }
         else Nremoved++;

         zfree(file);                                                            //  free memory

         zfree(sxrec.file);
         zfree(sxrec.tags);
         zfree(sxrec.capt);
         zfree(sxrec.comms);
         zfree(sxrec.gtags);
      }

      fclose(fid);
   }

   if (Flastver)                                                                 //  remove all but latest versions
   {
      cc = Nleft * sizeof(char *);
      flist = (char **) zmalloc(cc);

      fid = fopen(searchresults_file,"r");                                       //  read file of selected image files
      if (! fid) goto filerror;

      for (ii = 0; ii < Nleft; ii++)                                             //  build file list in memory
      {
         file = fgets_trim(buffer,XFCC,fid);
         if (! file) break;
         flist[ii] = zstrdup(file);
      }

      fclose(fid);

      for (ii = 1; ii < Nleft; ii++)                                             //  scan file list in memory
      {
         pp = strrchr(flist[ii],'/');                                            //  /directory.../filename.v...
         if (! pp) continue;                                                     //  |                     |
         pp = strstr(pp,".v");                                                   //  flist[ii]             pp
         if (! pp) continue;
         cc = pp - flist[ii] + 1;
         if (strmatchN(flist[ii],flist[ii-1],cc)) {                              //  compare each filespec with prior
            zfree(flist[ii-1]);                                                  //  match: remove prior from list
            flist[ii-1] = 0;
         }
      }

      fid = fopen(searchresults_file,"w");                                       //  write remaining file list
      if (! fid) goto filerror;                                                  //    to results file

      Npver = 0;
      for (ii = 0; ii < Nleft; ii++)
      {
         file = flist[ii];
         if (file) {
            fprintf(fid,"%s\n",file);
            zfree(file);
         }
         else Npver++;
      }

      fclose(fid);
      zfree(flist);
      
      if (Fscanall) Nadded -= Npver;                                             //  adjust counts                      16.05
      if (Fscancurr) Nremoved += Npver;
      Nleft -= Npver;
   }

   zmessageACK(Mwin,ZTX("images added: %d  removed: %d  new count: %d"),
                           Nadded, Nremoved, Nleft);
   if (Nleft == 0) {
      zmessageACK(Mwin,ZTX("no changes made"));
      zd->zstat = 0;                                                             //  stay in search dialog              15.07
      Ffuncbusy = 0;
      Fblock = 0;
      return 1;
   }

   free_resources();
   navi::gallerytype = SEARCH;                                                   //  search results
   gallery(searchresults_file,"initF");                                          //  generate gallery of matching files

   if (Frepmeta)                                                                 //  metadata report format
      searchimages_metadata_report();

   Ffuncbusy = 0;
   Fblock = 0;

   m_viewmode(0,"G");
   gallery(0,"paint",0);                                                         //  position at top
   return 1;

filerror:
   zmessageACK(Mwin,"file error: %s",strerror(errno));
   Ffuncbusy = 0;
   Fblock = 0;
   return 1;
}


//  test a given image against selection criteria, return match status

int searchimages_select(sxrec_t &sxrec)
{
   int  searchimages_metadata_select(char *imagefile);

   cchar    *pps, *ppf;
   int      iis, iif;
   int      Nmatch, Nnomatch;

   if (Ffiles)                                                                   //  file name match is wanted
   {
      Nmatch = Nnomatch = 0;

      for (iis = 1; ; iis++)
      {
         pps = strField(searchfiles,' ',iis);                                    //  step thru search file names
         if (! pps) break;
         ppf = strcasestr(sxrec.file,pps);                                       //  compare image file name
         if (ppf) Nmatch++;
         else Nnomatch++;
      }

      if (Nmatch == 0) return 0;                                                 //  no match any file
      if (Fallfiles && Nnomatch) return 0;                                       //  no match all files (dir & file names)
   }

   if (Fdates)                                                                   //  date match is wanted
   {
      if (strcmp(sxrec.pdate,searchDateFrom) < 0) return 0;
      if (strcmp(sxrec.pdate,searchDateTo) > 0) return 0;
   }

   if (Ftags)                                                                    //  tags match is wanted
   {
      Nmatch = Nnomatch = 0;

      for (iis = 1; ; iis++)                                                     //  step thru search tags
      {
         pps = strField(searchtags,",;",iis);                                    //  delimited
         if (! pps) break;
         if (*pps == ' ') continue;

         for (iif = 1; ; iif++)                                                  //  step thru file tags (delimited)
         {
            ppf = strField(sxrec.tags,",;",iif);
            if (! ppf) { Nnomatch++; break; }                                    //  count matches and fails
            if (*ppf == ' ') continue;
            if (strmatch(pps,ppf)) { Nmatch++; break; }                          //  match                              16.01
         }
      }

      if (Nmatch == 0) return 0;                                                 //  no match to any tag
      if (Falltags && Nnomatch) return 0;                                        //  no match to all tags
   }

   if (Fstars)                                                                   //  rating (stars) match is wanted
   {
      if (*searchStarsFrom && sxrec.rating[0] < *searchStarsFrom) return 0;
      if (*searchStarsTo && sxrec.rating[0] > *searchStarsTo) return 0;
   }

   if (Ftext)                                                                    //  text match is wanted
   {
      Nmatch = Nnomatch = 0;

      for (iis = 1; ; iis++)                                                     //  step through search words
      {
         pps = strField(searchtext,' ',iis);
         if (! pps) break;
         if (*pps == ' ') continue;

         ppf = strcasestr(sxrec.capt,pps);                                       //  match words in captions and comments
         if (! ppf) ppf = strcasestr(sxrec.comms,pps);

         if (ppf) Nmatch++;
         else Nnomatch++;
      }
      
      if (Nmatch == 0) return 0;                                                 //  no match to any word
      if (Falltext && Nnomatch) return 0;                                        //  no match to all words
   }

   if (Flocs )                                                                   //  location match is wanted           16.06
   {
      Nmatch = 0;

      for (iis = 1; ; iis++)                                                     //  step thru search locations
      {
         pps = strField(searchLocations,',',iis);                                //  comma delimiter                    16.07
         if (! pps) break;
         if (*pps == ' ') continue;

         for (iif = 1; iif <= 2; iif++)                                          //  step thru file geotags
         {                                                                       //    (location, country)
            ppf = strField(sxrec.gtags,'^',iif);
            if (! ppf) break;
            if (*ppf == ' ') continue;
            if (strcasestr(ppf,pps)) Nmatch = 1;                                 //  compare 
            if (Nmatch) break;
         }
         
         if (Nmatch) break;                                                      //  found a match
      }

      if (! Nmatch) return 0;                                                    //  no match found
   }

   if (Fmeta)                                                                    //  other metadata match
      if (! searchimages_metadata_select(sxrec.file)) return 0;

   return 1;
}


/********************************************************************************/

//  dialog to get metadata search criteria

int searchimages_metadata_dialog(zdialog *zdp)                                   //  overhauled                         16.07
{
   int searchimages_metadata_dialog_event(zdialog *zd, cchar *event);

   cchar *metamess = ZTX("These items are always reported: \n"
                         "date, stars, tags, caption, comment");
   zdialog  *zd;
   int      ii, zstat;
   char     matchx[8] = "matchx";

/***
         Search and Report Metadata

         These items are always reported:
         date, stars, tags, caption, comment

            Additional Items for Report
            Keyword                   Match Criteria
         [__________] [ matches  ] [__________________]
         [__________] [ contains ] [__________________]
         [__________] [  equal   ] [__________________]
         [__________] [   less   ] [__________________]
         [__________] [ greater  ] [__________________]

                   [clear] [apply] [cancel]
***/

   zd = zdialog_new("Search and Report Metadata",Mwin,Bclear,Bapply,Bcancel,null);
   zdialog_add_widget(zd,"label","labmeta","dialog",metamess,"space=5");
   zdialog_add_widget(zd,"label","labopts","dialog",ZTX("Additional Items for Report"));

   zdialog_add_widget(zd,"hbox","hb1","dialog");
   zdialog_add_widget(zd,"vbox","vb1","hb1",0,"space=5|homog");
   zdialog_add_widget(zd,"vbox","vb2","hb1",0,"space=5|homog");
   zdialog_add_widget(zd,"vbox","vb3","hb1",0,"space=5|homog|expand");

   zdialog_add_widget(zd,"label","lab1","vb1",ZTX("Keyword"));
   zdialog_add_widget(zd,"entry","key0","vb1");
   zdialog_add_widget(zd,"entry","key1","vb1");
   zdialog_add_widget(zd,"entry","key2","vb1");
   zdialog_add_widget(zd,"entry","key3","vb1");
   zdialog_add_widget(zd,"entry","key4","vb1");

   zdialog_add_widget(zd,"label","lab2","vb2");
   zdialog_add_widget(zd,"combo","match0","vb2");
   zdialog_add_widget(zd,"combo","match1","vb2");
   zdialog_add_widget(zd,"combo","match2","vb2");
   zdialog_add_widget(zd,"combo","match3","vb2");
   zdialog_add_widget(zd,"combo","match4","vb2");

   zdialog_add_widget(zd,"label","lab3","vb3",ZTX("Match Criteria"));
   zdialog_add_widget(zd,"entry","value0","vb3",0,"expand");
   zdialog_add_widget(zd,"entry","value1","vb3",0,"expand");
   zdialog_add_widget(zd,"entry","value2","vb3",0,"expand");
   zdialog_add_widget(zd,"entry","value3","vb3",0,"expand");
   zdialog_add_widget(zd,"entry","value4","vb3",0,"expand");

   if (! searchkeys[0])                                                          //  first call initialization
   {
      for (ii = 0; ii < 5; ii++) {
         searchkeys[ii] = (char *) zmalloc(40);
         searchvalues[ii] = (char *) zmalloc(100);
         *searchkeys[ii] = *searchvalues[ii] = 0;
      }
   }
   
   for (ii = 0; ii < 5; ii++) {
      matchx[5] = '0' + ii;
      zdialog_cb_app(zd,matchx,"matches");
      zdialog_cb_app(zd,matchx,"contains");
      zdialog_cb_app(zd,matchx,"number =");
      zdialog_cb_app(zd,matchx,"number =>");
      zdialog_cb_app(zd,matchx,"number <=");
   }
   
   zdialog_show(zdp,0);                                                          //  hide parent dialog
   zdialog_resize(zd,600,0);
   zdialog_restore_inputs(zd);                                                   //  preload prior user inputs
   zdialog_run(zd,searchimages_metadata_dialog_event);                           //  run dialog
   zstat = zdialog_wait(zd);                                                     //  wait for completion
   zdialog_free(zd);
   zdialog_show(zdp,1);                                                          //  restore parent dialog

   if (zstat == 1) return 1;                                                     //  search criteria were entered
   else return 0;                                                                //  not
}


//  dialog event and completion callback function

int searchimages_metadata_dialog_event(zdialog *zd, cchar *event)                //  overhauled                         16.07
{
   int      ii, jj, err;
   float    fnum;
   char     keyx[8] = "keyx", valuex[8] = "valuex", matchx[8] = "matchx";
   char     temp[100];

   
   if (strmatch(event,"enter")) zd->zstat = 2;                                   //  [apply]
   if (strmatch(event,"escape")) zd->zstat = 3;                                  //  escape = cancel
   
   if (! zd->zstat) return 1;                                                    //  wait for completion

   if (zd->zstat == 1) {
      for (ii = 0; ii < 5; ii++) {                                               //  clear
         keyx[3] = '0' + ii;
         valuex[5] = '0' + ii;
         matchx[5] = '0' + ii;
         zdialog_stuff(zd,keyx,"");
         zdialog_stuff(zd,valuex,"");
         zdialog_stuff(zd,matchx,"matches");
         zd->zstat = 0;                                                          //  keep dialog active
      }
      return 1;
   }

   if (zd->zstat != 2) {
      Nsearchkeys = 0;                                                           //  no search keys
      return 1;
   }

   Nsearchkeys = 0;                                                              //  apply

   for (ii = jj = 0; ii < 5; ii++)
   {
      keyx[3] = '0' + ii;                                                        //  get search key
      zdialog_fetch(zd,keyx,searchkeys[ii],40);
      strCompress(searchkeys[ii]);                                               //  remove all blanks from key names
      if (*searchkeys[ii] <= ' ') continue;
      memmove(searchkeys[jj],searchkeys[ii],40);                                 //  repack blank keys

      valuex[5] = '0' + ii;
      zdialog_fetch(zd,valuex,searchvalues[ii],100);                             //  get corresp. match value
      strTrim2(searchvalues[jj],searchvalues[ii]);                               //  trim leading and trailing blanks

      matchx[5] = '0' + ii;
      zdialog_fetch(zd,matchx,temp,20);                                          //  get corresp. match type
      if (strmatch(temp,"matches")) matchtype[jj] = 'm';
      if (strmatch(temp,"contains")) matchtype[jj] = 'c';
      if (strmatch(temp,"number =")) matchtype[jj] = '=';
      if (strmatch(temp,"number =>")) matchtype[jj] = '>';
      if (strmatch(temp,"number <=")) matchtype[jj] = '<';
      
      if (strstr(temp,"number")) {                                               //  check numeric values
         err = convSF(searchvalues[jj],fnum);
         if (err) {
            snprintf(temp,100,ZTX("bad number: %s"),searchvalues[jj]);
            zmessageACK(Mwin,temp);
            zd->zstat = 0;
            return 1;
         }
      }

      if (ii > jj) *searchkeys[ii] = *searchvalues[ii] = 0;
      jj++;
   }
   
   Nsearchkeys = jj;                                                             //  keys found, no blanks
   
   if (Nsearchkeys) zd->zstat = 1;                                               //  keys were entered
   else zd->zstat = 2;                                                           //  no keys were entered

   return 1;
}


//  Test image metadata against metadata select criteria.
//  Substrings also match, which is not always what is wanted.
//  e.g. search ISO = 100 will match metadata = 100 or 1000

int searchimages_metadata_select(char *file)                                     //  overhauled                         16.07
{
   char           *kvals[5];
   int            ii, nth;
   cchar          *pps, *ppf;
   float          fkey, fval;

   exif_get(file,(cchar **) searchkeys,kvals,Nsearchkeys);                       //  get the image metadata

   for (ii = 0; ii < Nsearchkeys; ii++) {                                        //  loop all metadata search keys
      if (*searchvalues[ii] > ' ') {                                             //  key match value(s) are present
         if (kvals[ii]) {                                                        //  key values present in metadata
            for (nth = 1; ; nth++) {                                             //  loop all match values
               pps = strField(searchvalues[ii],',',nth);                         //  comma delimiter
               if (! pps) return 0;                                              //  no more, no match found

               if (matchtype[ii] == 'm')                                         //  key matches any value
                  if (strmatchcase(kvals[ii],pps)) break;                        //  match not case sensitive

               if (matchtype[ii] == 'c') {                                       //  key contains any value
                  ppf = strcasestr(kvals[ii],pps);                               //  match not case sensitive
                  if (ppf) break;                                                //  found match
               }

               if (matchtype[ii] == '=') {                                       //  numeric key equals any value
                  fkey = atof(kvals[ii]);
                  fval = atof(pps);
                  if (fkey == fval) break;                                       //  found match
               }

               if (matchtype[ii] == '>') {                                       //  numeric key >= one value
                  fkey = atof(kvals[ii]);
                  fval = atof(pps);
                  if (fkey >= fval) break;                                       //  found match
               }

               if (matchtype[ii] == '<') {                                       //  numeric key <= one value
                  fkey = atof(kvals[ii]);
                  fval = atof(pps);
                  if (fkey <= fval) break;                                       //  found match
               }
            }
         }

         else if (strmatch(searchvalues[ii],"null")) break;                      //  found match for empty metadata     16.06
         else return 0;                                                          //  no match found
      }
   }

   return 1;
}


//  Report selected metadata using a gallery window layout
//  with image thumbnails and selected metadata text.

int searchimages_metadata_report()
{
   using namespace navi;

   cchar    *keys1[7] = { exif_date_key, iptc_rating_key, iptc_keywords_key,
                          exif_city_key, exif_country_key,
                          iptc_caption_key, exif_comment_key };
   cchar    *keys[20];
   char     *file, *kvals[20];
   char     text1[2*indexrecl], text2[200];
   int      Nkeys, ii, jj, cc;

   if (! mdlist) {
      cc = Nfiles * sizeof(char *);                                              //  allocate metadata list
      mdlist = (char **) zmalloc(cc);                                            //  Nfiles = curr. gallery files
      memset(mdlist,0,cc);                                                       //  mdlist = corresp. metadata list
   }                                                                             //  (zfree() in image_navigate)

   for (ii = 0; ii < 7; ii++)                                                    //  set first 6 key names, fixed
      keys[ii] = keys1[ii];

   for (ii = 0; ii < Nsearchkeys; ii++)                                          //  remaining key names from user
      keys[ii+7] = searchkeys[ii];

   Nkeys = 7 + Nsearchkeys;                                                      //  total keys to extract
   mdrows = Nkeys - 1;                                                           //  report rows (location/country 1 row)
   
   Fbusy_goal = Nfiles;
   Fbusy_done = 0;

   for (ii = 0; ii < Nfiles; ii++)                                               //  loop image gallery files
   {
      zmainloop(20);                                                             //  keep GTK alive
   
      file = gallery(0,"find",ii);
      if (! file) continue;

      exif_get(file,keys,kvals,Nkeys);                                           //  get the metadata

      snprintf(text2,200,"%s  %s",kvals[3],kvals[4]);                            //  combine location and country
      if (kvals[3]) zfree(kvals[3]);
      if (kvals[4]) zfree(kvals[4]);
      kvals[3] = zstrdup(text2);
      kvals[4] = 0;

      for (cc = jj = 0; jj < Nkeys; jj++)                                        //  add metadata to report
      {
         if (jj == 4) continue;                                                  //  skip country
         if (jj == 0 && kvals[0]) kvals[0][4] = kvals[0][7] = '-';               //  conv. yyyy:mm:dd to yyyy-mm-dd
         if (jj == 3) 
            snprintf(text2,200,"Location:  %s \n",kvals[jj]);                    //  replace "city" with "location"     16.07
         else 
            snprintf(text2,200,"%s:  %s \n",keys[jj], kvals[jj]);
         strcpy(text1+cc,text2);
         cc += strlen(text2);
      }

      if (mdlist[ii]) zfree(mdlist[ii]);                                         //  attach metadata for gallery paint
      mdlist[ii] = zstrdup(text1);

      for (jj = 0; jj < Nkeys; jj++)                                             //  free memory
         if (kvals[jj]) zfree(kvals[jj]);

      zfree(file);
      Fbusy_done++;
   }

   Fbusy_goal = Fbusy_done = 0;                                                  //  16.02

   gallerytype = METADATA;                                                       //  gallery type = search results/metadata
   return 0;
}


/********************************************************************************/

//  Group images by location and date, with a count of images in each group.
//  Click on a group to get a thumbnail gallery of all images in the group.

struct grec_t  {                                                                 //  image geotags data
   char        *location, *country;                                              //  group location
   char        pdate[12];                                                        //  nominal group date, yyyymmdd
   int         lodate, hidate;                                                   //  range, days since 0 CE
   int         count;                                                            //  images in group
};

grec_t   *grec = 0;
int      Ngrec = 0;
int      locs_groupby, locs_daterange;

int   locs_comp(cchar *rec1, cchar *rec2);
int   locs_comp2(cchar *rec1, cchar *rec2);
void  locs_click(GtkWidget *widget, int line, int pos);
int   locs_getdays(cchar *date);


//  menu function

void m_locations(GtkWidget *, cchar *)
{
   int  locations_dialog_event(zdialog *zd, cchar *event);

   zdialog        *zd;
   int            zstat, ftf, err, cc, cc1, cc2;
   int            ww, iix, iig, newgroup;
   char           country[100], locatn[100], buff[300], pdate[12];
   cchar          *pp;
   sxrec_t        sxrec;
   GtkWidget      *textwin;

   F1_help_topic = "image_locations";
   if (checkpend("all")) return;                                                 //  check nothing pending

   if (Fnoindex) {
      zmessageACK(Mwin,ZTX("-noindex in use, disabled"));
      return;
   }

/***
            Report Image Locations

         (o) Group by country
         (o) Group by country/location
         (o) Group by country/location/date
         (o) Group by date/country/location                                      //  16.08
             Combine within [ xx |-|+] days

                        [proceed]  [cancel]
***/

   zd = zdialog_new(ZTX("Report Image Locations"),Mwin,Bproceed,Bcancel,null);
   zdialog_add_widget(zd,"radio","country","dialog",ZTX("Group by country"));
   zdialog_add_widget(zd,"radio","location","dialog",ZTX("Group by country/location"));
   zdialog_add_widget(zd,"radio","date","dialog",ZTX("Group by country/location/date"));
   zdialog_add_widget(zd,"radio","date2","dialog",ZTX("Group by date/country/location"));                           //  16.08
   zdialog_add_widget(zd,"hbox","hbr","dialog");
   zdialog_add_widget(zd,"label","space","hbr",0,"space=10");
   zdialog_add_widget(zd,"label","labr1","hbr",ZTX("Combine within"),"space=10");
   zdialog_add_widget(zd,"spin","range","hbr","0|999|1|1");
   zdialog_add_widget(zd,"label","labr2","hbr",ZTX("days"),"space=10");

   zdialog_stuff(zd,"country",0);
   zdialog_stuff(zd,"location",1);                                               //  default by location
   zdialog_stuff(zd,"date",0);
   zdialog_stuff(zd,"date2",0);

   zdialog_resize(zd,300,0);
   zdialog_run(zd,locations_dialog_event);
   zstat = zdialog_wait(zd);
   if (zstat != 1) {
      zdialog_free(zd);
      return;
   }

   zdialog_fetch(zd,"country",iix);
   if (iix) locs_groupby = 1;                                                    //  group country
   zdialog_fetch(zd,"location",iix);
   if (iix) locs_groupby = 2;                                                    //  group country/location
   zdialog_fetch(zd,"date",iix);
   if (iix) locs_groupby = 3;                                                    //  group country/location/date-range
   zdialog_fetch(zd,"date2",iix);
   if (iix) locs_groupby = 4;                                                    //  group date-range/country/location  16.08
   
   zdialog_fetch(zd,"range",locs_daterange);                                     //  combine recs within date range

   zdialog_free(zd);

   if (Ngrec) {                                                                  //  free prior memory
      for (iix = 0; iix < Ngrec; iix++) {
         if (grec[iix].location) zfree(grec[iix].location);
         if (grec[iix].country) zfree(grec[iix].country);
      }
      zfree(grec);
   }

   cc = maximages * sizeof(grec_t);                                              //  allocate memory
   grec = (grec_t *) zmalloc(cc);
   memset(grec,0,cc);

   Ngrec = 0;
   ftf = 1;

   while (true)
   {
      err = read_sxrec_seq(sxrec,ftf);                                           //  read image index recs.
      if (err) break;

      iix = Ngrec;

      pp = strField(sxrec.gtags,'^',1);                                          //  get location
      if (pp) grec[iix].location = zstrdup(pp);
      else grec[iix].location = zstrdup("null");

      pp = strField(sxrec.gtags,'^',2);                                          //  country
      if (pp) grec[iix].country = zstrdup(pp);
      else grec[iix].country = zstrdup("null");

      strncpy0(grec[iix].pdate,sxrec.pdate,9);                                   //  photo date, truncate to yyyymmdd
      grec[iix].lodate = locs_getdays(sxrec.pdate);                              //  days since 0 CE
      grec[iix].hidate = grec[iix].lodate;

      if (++Ngrec == maximages) {
         zmessageACK(Mwin,"too many image files");
         return;
      }

      zfree(sxrec.file);
      zfree(sxrec.tags);
      zfree(sxrec.capt);
      zfree(sxrec.comms);
      zfree(sxrec.gtags);
   }

   if (! Ngrec) {
      zmessageACK(Mwin,"no geotags data found");
      return;
   }

   if (Ngrec > 1)                                                                //  sort grecs by country/location/date
      HeapSort((char *) grec, sizeof(grec_t), Ngrec, locs_comp);

   iig = 0;                                                                      //  1st group from grec[0]
   grec[iig].count = 1;                                                          //  group count = 1

   for (iix = 1; iix < Ngrec; iix++)                                             //  scan following grecs
   {
      newgroup = 0;

      if (! strmatch(grec[iix].country,grec[iig].country))
         newgroup = 1;                                                           //  new country >> new group

      if (locs_groupby >= 2)                                                     //  new location >> new group 
         if (! strmatch(grec[iix].location,grec[iig].location)) newgroup = 1;    //    if group by location

      if (locs_groupby >= 3)
         if (grec[iix].lodate - grec[iig].hidate > locs_daterange)               //  new date >> new group if group by date
            newgroup = 1;                                                        //    and date out of range

      if (newgroup)
      {
         iig++;                                                                  //  new group
         if (iix > iig) {
            grec[iig] = grec[iix];                                               //  copy and pack down
            grec[iix].location = grec[iix].country = 0;                          //  no zfree()
         }
         grec[iig].count = 1;                                                    //  group count = 1
      }
      else
      {
         zfree(grec[iix].location);                                              //  same group
         zfree(grec[iix].country);                                               //  free memory
         grec[iix].location = grec[iix].country = 0;
         grec[iig].hidate = grec[iix].lodate;                                    //  expand group date-range
         grec[iig].count++;                                                      //  increment group count
      }
   }

   Ngrec = iig + 1;                                                              //  unique groups count

   if (locs_groupby == 1) ww = 350;                                              //  group country                      16.07
   if (locs_groupby == 2) ww = 600;                                              //  group country/location
   if (locs_groupby == 3) ww = 650;                                              //  group country/location/date-range
   if (locs_groupby == 4) ww = 650;                                              //  group date-range/country/location  16.08

   textwin = write_popup_text("open",ZTX("Image Locations"),ww,400,Mwin);        //  write groups to popup window

   if (locs_groupby == 1)                                                        //  group by country
   {
      snprintf(buff,300,"%-30s  %5s ","Country","Count");
      write_popup_text("writebold",buff);

      for (iig = 0; iig < Ngrec; iig++)
      {
         utf8substring(country,grec[iig].country,0,30);
         cc1 = 30 + strlen(country) - utf8len(country);
         snprintf(buff,300,"%-*s  %5d ",cc1,country,grec[iig].count);
         write_popup_text("write",buff);
      }
   }

   if (locs_groupby == 2)                                                        //  group by country/location
   {
      snprintf(buff,300,"%-30s  %-30s  %5s ","Country","Location","Count");
      write_popup_text("writebold",buff);

      for (iig = 0; iig < Ngrec; iig++)
      {
         utf8substring(country,grec[iig].country,0,30);
         cc1 = 30 + strlen(country) - utf8len(country);
         utf8substring(locatn,grec[iig].location,0,30);
         cc2 = 30 + strlen(locatn) - utf8len(locatn);
         snprintf(buff,300,"%-*s  %-*s  %5d ",
                  cc1,country,cc2,locatn,grec[iig].count);
         write_popup_text("write",buff);
      }
   }

   if (locs_groupby == 3)                                                        //  group by country/location/date-range
   {
      snprintf(buff,300,"%-26s  %-26s  %-10s   %5s ","Country","Location","Date","Count");
      write_popup_text("writebold",buff);

      for (iig = 0; iig < Ngrec; iig++)
      {
         utf8substring(country,grec[iig].country,0,26);                          //  get graphic cc for UTF-8 names
         cc1 = 26 + strlen(country) - utf8len(country);
         utf8substring(locatn,grec[iig].location,0,26);
         cc2 = 26 + strlen(locatn) - utf8len(locatn);

         strncpy(pdate,grec[iig].pdate,8);                                       //  date, yyyymmdd
         if (! strmatch(pdate,"null")) {
            memcpy(pdate+8,pdate+6,2);                                           //  convert to yyyy-mm-dd
            memcpy(pdate+5,pdate+4,2);
            pdate[4] = pdate[7] = '-';
            pdate[10] = 0;
         }

         snprintf(buff,300,"%-*s  %-*s  %-10s  %6d ",
                  cc1,country,cc2,locatn,pdate,grec[iig].count);
         write_popup_text("write",buff);
      }
   }
                                                                                 //  16.08
   if (locs_groupby == 4)                                                        //  group by date-range/country/location
   {
      if (Ngrec > 1)                                                             //  re-sort by date/country/location
         HeapSort((char *) grec, sizeof(grec_t), Ngrec, locs_comp2);
   
      snprintf(buff,300,"%-10s  %-26s  %-26s   %5s ","Date","Country","Location","Count");
      write_popup_text("writebold",buff);

      for (iig = 0; iig < Ngrec; iig++)
      {
         utf8substring(country,grec[iig].country,0,26);                          //  get graphic cc for UTF-8 names
         cc1 = 26 + strlen(country) - utf8len(country);
         utf8substring(locatn,grec[iig].location,0,26);
         cc2 = 26 + strlen(locatn) - utf8len(locatn);

         strncpy(pdate,grec[iig].pdate,8);                                       //  date, yyyymmdd
         if (! strmatch(pdate,"null")) {
            memcpy(pdate+8,pdate+6,2);                                           //  convert to yyyy-mm-dd
            memcpy(pdate+5,pdate+4,2);
            pdate[4] = pdate[7] = '-';
            pdate[10] = 0;
         }

         snprintf(buff,300,"%-10s  %-*s  %-*s  %6d ",
                  pdate,cc1,country,cc2,locatn,grec[iig].count);
         write_popup_text("write",buff);
      }
   }

   write_popup_text("top");
   textwidget_set_clickfunc(textwin,locs_click);                                 //  response function for mouse click
   return;
}


//  dialog event and completion function

int locations_dialog_event(zdialog *zd, cchar *event)
{
   if (strmatch(event,"escape")) zd->zstat = 2;                                  //  escape = cancel                    15.07
   return 1;
}


//  Compare 2 grec records by geotags and date,
//  return < 0  = 0  > 0   for   rec1  <  =  >  rec2.

int locs_comp(cchar *rec1, cchar *rec2)
{
   int      ii;

   char * country1 = ((grec_t *) rec1)->country;                                 //  compare countries
   char * country2 = ((grec_t *) rec2)->country;
   ii = strcmp(country1,country2);
   if (ii) return ii;

   char * loc1 = ((grec_t *) rec1)->location;                                    //  compare cities
   char * loc2 = ((grec_t *) rec2)->location;
   ii = strcmp(loc1,loc2);
   if (ii) return ii;

   int date1 = ((grec_t *) rec1)->lodate;                                        //  compare dates
   int date2 = ((grec_t *) rec2)->lodate;
   ii = date1 - date2;
   return ii;
}


//  Compare 2 grec records by date and geotags,
//  return < 0  = 0  > 0   for   rec1  <  =  >  rec2.

int locs_comp2(cchar *rec1, cchar *rec2)                                         //  16.08
{
   int      ii;

   int date1 = ((grec_t *) rec1)->lodate;                                        //  compare dates
   int date2 = ((grec_t *) rec2)->lodate;
   ii = date1 - date2;
   if (ii) return ii;

   char * country1 = ((grec_t *) rec1)->country;                                 //  compare countries
   char * country2 = ((grec_t *) rec2)->country;
   ii = strcmp(country1,country2);
   if (ii) return ii;

   char * loc1 = ((grec_t *) rec1)->location;                                    //  compare cities
   char * loc2 = ((grec_t *) rec2)->location;
   ii = strcmp(loc1,loc2);
   return ii;
}


//  convert yyyymmdd date into days from 0 C.E.
//  "null" date returns 999999 (year 2737)

int locs_getdays(cchar *date)
{
   int   CEdays(int year, int mon, int day);

   int      year, month, day;
   char     temp[8];

   year = month = day = 0;

   strncpy0(temp,date,5);
   year = atoi(temp);

   strncpy0(temp,date+4,3);
   month = atoi(temp);

   strncpy0(temp,date+6,3);
   day = atoi(temp);

   return CEdays(year,month,day);
}


//   convert year/month/day into days since Jan 1, 0001 (day 0 CE)
//   year is 0001 to 9999, month is 1-12, day is 1-31

int CEdays(int year, int month, int day)
{
   int    montab[12] = { 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 };
   int    elaps;

   elaps = 365 * (year-1) + (year-1) / 4;                                        //  elapsed days in prior years
   elaps += montab[month-1];                                                     //  + elapsed days in prior months
   if (year % 4 == 0 && month > 2) elaps += 1;                                   //  + 1 for Feb. 29
   elaps += day-1;                                                               //  + elapsed days in month
   return elaps;
}


//  Receive clicks on report window and generate gallery of images
//  matching the selected country/location/date

void locs_click(GtkWidget *widget, int line, int pos)
{
   int      iix, ftf, err, lodate, hidate, datex;
   cchar    *pp;
   char     locatn[100], country[100];
   FILE     *fid;
   sxrec_t  sxrec;

   if (checkpend("all")) return;                                                 //  check nothing pending

   textwidget_get_line(widget,line,1);                                           //  hilite clicked line

   iix = line - 1;                                                               //  clicked grec[iix]
   if (iix < 0 || iix > Ngrec-1) return;

   strncpy0(country,grec[iix].country,99);                                       //  selected country/location/date-range
   strncpy0(locatn,grec[iix].location,99);
   lodate = grec[iix].lodate;
   hidate = grec[iix].hidate;

   fid = fopen(searchresults_file,"w");                                          //  open output file
   if (! fid) goto filerror;

   ftf = 1;

   while (true)                                                                  //  read image index recs.
   {
      err = read_sxrec_seq(sxrec,ftf);
      if (err) break;

      pp = strField(sxrec.gtags,'^',2);
      if (! pp) pp = "null";                                                     //  enable search for "null"

      if (! strmatch(pp,country)) goto freemem;                                  //  no country match

      if (locs_groupby >= 2) {
         pp = strField(sxrec.gtags,'^',1);
         if (! pp) pp = "null";
         if (! strmatch(pp,locatn)) goto freemem;                                //  no location match
      }

      if (locs_groupby >= 3) {                                                   //  bugfix                             16.08
         datex = locs_getdays(sxrec.pdate);
         if (datex < lodate || datex > hidate) goto freemem;                     //  no date match
      }

      fprintf(fid,"%s\n",sxrec.file);                                            //  output matching file

   freemem:
      zfree(sxrec.file);
      zfree(sxrec.tags);
      zfree(sxrec.capt);
      zfree(sxrec.comms);
      zfree(sxrec.gtags);
   }

   fclose(fid);

   free_resources();
   navi::gallerytype = SEARCH;                                                   //  search results
   gallery(searchresults_file,"initF");                                          //  generate gallery of matching files
   m_viewmode(0,"G");
   gallery(0,"paint",0);
   return;

filerror:
   zmessageACK(Mwin,"file error: %s",strerror(errno));
   return;
}


/********************************************************************************/

//  Produce a report of image counts by year and month.
//  Click on a report line to get a thumbnail gallery of images.

#define  Nyears 2100                         //  years 0000-2099
#define  Nperds (12 * Nyears)                //  periods (months)

//  menu function

void m_timeline(GtkWidget *, cchar *)                                            //  16.08
{
   void  timeline_click(GtkWidget *widget, int line, int pos);

   int         Ycount[Nyears], Pcount[Nperds];                                   //  image counts per year and period
   int         Ncount, Ecount;                                                   //  counts for "null" and invalid dates
   int         ii, ftf, err, cc;
   int         yy, mm, pp;
   char        pdate[8], buff[100];
   cchar       *pmon;
   sxrec_t     sxrec;
   GtkWidget   *textwin;
   cchar       *months = ZTX("Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec");

   F1_help_topic = "image_timeline";
   if (checkpend("all")) return;                                                 //  check nothing pending

   if (Fnoindex) {
      zmessageACK(Mwin,ZTX("-noindex in use, disabled"));
      return;
   }

   Ncount = Ecount = 0;                                                          //  clear null and error counts

   for (yy = 0; yy < Nyears; yy++)                                               //  clear totals per year
      Ycount[yy] = 0;
   
   for (pp = 0; pp < Nperds; pp++)                                               //  clear totals per period (month)
      Pcount[pp] = 0;

   ftf = 1;

   while (true)
   {
      err = read_sxrec_seq(sxrec,ftf);                                           //  read image index recs.
      if (err) break;

      strncpy0(pdate,sxrec.pdate,7);                                             //  photo date, truncate to yyyymm

      if (strmatch(pdate,"null")) {                                              //  if null, add to null count
         ++Ncount;
         goto freemem;
      }
      
      ii = atoi(pdate);                                                          //  photo date, 0 to 209912
      yy = ii / 100;                                                             //  year, 0000 to 2099
      mm = ii - yy * 100;                                                        //  month, 1 to 12
      if (yy < 0 || yy >= Nyears || mm < 1 || mm > 12) {
         ++Ecount;                                                               //  invalid, add to error count
         goto freemem;
      }
     
      ++Ycount[yy];                                                              //  add to year totals
      pp = yy * 12 + mm - 1;                                                     //  add to period totals
      ++Pcount[pp];

   freemem:
      zfree(sxrec.file);
      zfree(sxrec.tags);
      zfree(sxrec.capt);
      zfree(sxrec.comms);
      zfree(sxrec.gtags);
   }

   textwin = write_popup_text("open","Image Timeline",500,400,Mwin);             //  write report to popup window
   
   if (Ncount) {
      snprintf(buff,100,"null %d",Ncount);                                       //  images with no date
      write_popup_text("write",buff);
   }
   
   if (Ecount) {
      snprintf(buff,100,"invalid %d",Ecount);                                    //  images with invalid date
      write_popup_text("write",buff);
   }
   
   for (yy = 0; yy < Nyears; yy++)                                               //  loop years
   {
      if (! Ycount[yy]) continue;                                                //  omit years without images
      
      snprintf(buff,100,"%04d  %-6d ",yy,Ycount[yy]);                            //  output "yyyy  NNNNNN "
      cc = 13;

      for (mm = 0; mm < 12; mm++) {                                              //  loop months 0 - 11
         pp = yy * 12 + mm;                                                      //  period
         if (! Pcount[pp]) {
            strcpy(buff+cc,"--- ");                                              //  empty period, output "--- "
            cc += 4;
         }
         else {
            pmon = strField(months,' ',mm+1);                                    //  get month (utf8, 3+ chars.)
            strncatv(buff+cc,100-cc,pmon," ",0);                                 //  output "mmm... "
            cc += strlen(pmon) + 1;
         }
      }
      buff[cc] = 0;

      write_popup_text("write",buff);
   }
   
   wscroll(textwin,0);                                                           //  scroll to bottom of report

   textwidget_set_clickfunc(textwin,timeline_click);                             //  response function for mouse click
   return;
}


//  Receive clicks on report window and generate gallery of images
//  matching the selected period

void timeline_click(GtkWidget *widget, int line, int pos)
{
   int         ftf, err, ii, cc = 0;
   int         Fnull = 0, Finvalid = 0;
   int         yy, mm;
   char        *txline, pdate[8];
   cchar       *pmon;
   FILE        *fid;
   sxrec_t     sxrec;
   cchar       *months = ZTX("Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec");

   if (checkpend("all")) return;                                                 //  check nothing pending

   txline = textwidget_get_line(widget,line,0);                                  //  get clicked line
   if (! txline) return;
   
   if (strmatchN(txline,"null",4)) Fnull = 1;                                    //  find images with null date

   else if (strmatchN(txline,"invalid",7)) Finvalid = 1;                         //  find images with invalid date

   else if (pos < 13) {                                                          //  clicked on year or year count
      strncpy0(pdate,txline,5);                                                  //  have "yyyy"
      cc = 4;
   }
   
   else                                                                          //  month was clicked
   {
      while (pos && txline[pos] != ' ') pos--;                                   //  back-up to prior blank
      pos++;                                                                     //  1st char. of month (utf8 string)
      for (cc = 1; cc < 20; cc++) 
         if (! txline[pos+cc] || txline[pos+cc] == ' ') break;                   //  month cc from blank/null delimiter
      if (cc < 3 || cc == 20) return;
      for (mm = 0; mm < 12; mm++) {                                              //  loop 12 months
         pmon = strField(months,' ',mm+1);                                       //  get month name from list of 12
         if (! pmon) return;
         if (strmatchN(txline+pos,pmon,cc)) break;                               //  match with clicked month
      }
      if (mm == 12) return;                                                      //  not found
      ++mm;                                                                      //  month, 1-12
      strncpy(pdate,txline,4);                                                   //  "yyyy"
      pdate[4] = '0' + mm/10;
      pdate[5] = '0' + mm % 10;                                                  //  have "yyyymm"
      pdate[6] = 0;
      cc = 6;
   }

   fid = fopen(searchresults_file,"w");                                          //  open output file
   if (! fid) goto filerror;

   ftf = 1;

   while (true)                                                                  //  read image index recs.
   {
      err = read_sxrec_seq(sxrec,ftf);
      if (err) break;
      
      if (Fnull) {                                                               //  search for missing dates
         if (strmatch(sxrec.pdate,"null")) {
            fprintf(fid,"%s\n",sxrec.file);
            goto freemem;
         }
      }
      
      else if (Finvalid) {                                                       //  search for invalid dates
         strncpy0(pdate,sxrec.pdate,6);
         ii = atoi(pdate);                                                       //  0 to 209912
         yy = ii / 100;                                                          //  0 to 2099
         mm = ii - yy * 100;                                                     //  1 to 12
         if (yy < 0 || yy >= Nyears || mm < 1 || mm > 12) {
            fprintf(fid,"%s\n",sxrec.file);
            goto freemem;
         }
      }            

      else if (strmatchN(sxrec.pdate,pdate,cc))                                  //  screen for desired period
         fprintf(fid,"%s\n",sxrec.file);                                         //  output matching file

   freemem:
      zfree(sxrec.file);
      zfree(sxrec.tags);
      zfree(sxrec.capt);
      zfree(sxrec.comms);
      zfree(sxrec.gtags);
   }

   fclose(fid);

   free_resources();
   navi::gallerytype = SEARCH;                                                   //  search results
   gallery(searchresults_file,"initF");                                          //  generate gallery of matching files
   m_viewmode(0,"G");
   gallery(0,"paint",0);
   return;

filerror:
   zmessageACK(Mwin,"file error: %s",strerror(errno));
   return;
}


/********************************************************************************
   Functions to read and write exif/iptc or other metadata
*********************************************************************************/

//  get EXIF/IPTC metadata for given image file and EXIF/IPTC key(s)
//  returns array of pointers to corresponding key values
//  if a key is missing, corresponding pointer is null
//  returned strings belong to caller, are subject for zfree()
//  up to 20 keynames may be requested per call
//  returns: 0 = OK, +N = error

int exif_get(cchar *file, cchar **keys, char **kdata, int nkeys)
{
   char        *pp;
   char        *inputs[30], *outputs[99];
   int         cc, ii, jj, err;
   uint        ucc;
   
   if (nkeys < 1 || nkeys > 20) zappcrash("exif_get nkeys: %d",nkeys);

   cc = nkeys * sizeof(char *);                                                  //  clear outputs
   memset(kdata,0,cc);

   inputs[0] = (char *) "-m";                                                    //  options for exiftool
   inputs[1] = (char *) "-s2";
   inputs[2] = (char *) "-n";
   inputs[3] = (char *) "-fast";                                                 //  -fast2 loses maker notes           15.12
   jj = 4;

   for (ii = 0; ii < nkeys; ii++)                                                //  build exiftool inputs
   {
      cc = strlen(keys[ii]);                                                     //  -keyname
      inputs[jj] = (char *) zmalloc(cc+2);
      inputs[jj][0] = '-';
      strcpy(inputs[jj]+1,keys[ii]);
      if (strmatchcase(keys[ii],"location"))                                     //  make "location" = "city"           16.07 
         strcpy(inputs[jj]+1,"city"); 
      jj++;
   }

   inputs[jj] = zstrdup(file);                                                   //  filename last
   jj++;

   err = exif_server(jj,inputs,outputs);                                         //  get exif outputs
   if (err) return 1;                                                            //  exif_server() failure

   for (ii = 4; ii < jj; ii++)                                                   //  free memory
      zfree(inputs[ii]);

   for (ii = 0; ii < nkeys; ii++)                                                //  search outputs
   {
      pp = outputs[ii];                                                          //  keyname: keyvalue
      if (! pp) break;

      for (jj = 0; jj < nkeys; jj++)
      {
         if (strmatchcase(keys[jj],"location")) {                                //  "location" comes back "city"       16.07
            if (! strmatchcaseN(pp,"city",4)) continue;
            if (strlen(pp) > 6) kdata[jj] = zstrdup(pp+6);
         }
         else {
            ucc = strlen(keys[jj]);                                              //  look for matching input keyname
            if (! strmatchcaseN(pp,keys[jj],ucc)) continue;
            if (strlen(pp) > ucc+2) kdata[jj] = zstrdup(pp+ucc+2);
         }
      }

      zfree(pp);
   }

   return 0;
}


/********************************************************************************/

//  create or change EXIF/IPTC metadata for given image file and key(s)
//  up to 20 keys may be processed
//  command:
//    exiftool -m -overwrite_original -keyname="keyvalue" ... "file"
//
//  NOTE: exiftool replaces \n (newline) in keyvalue with . (period).

int exif_put(cchar *file, cchar **keys, cchar **kdata, int nkeys)
{
   int      ii, jj, cc;
   char     *inputs[30];

   if (nkeys < 1 || nkeys > 20) zappcrash("exif_put nkeys: %d",nkeys);

   inputs[0] = (char *) "-m";                                                    //  exiftool options
   inputs[1] = (char *) "-overwrite_original";                                   //  -P preserve date removed
   jj = 2;

   for (ii = 0; ii < nkeys; ii++)                                                //  build exiftool inputs
   {
      cc = strlen(keys[ii]) + strlen(kdata[ii]) + 3;
      inputs[jj] = (char *) zmalloc(cc);
      inputs[jj][0] = '-';                                                       //  -keyname=value

      if (strmatchcase(keys[ii],"location")) {                                   //  make "location" = "city"           16.07
         strcpy(inputs[jj]+1,"city");
         cc = 4;
      }
      else {
         strcpy(inputs[jj]+1,keys[ii]);
         cc = strlen(keys[ii]);
      }

      inputs[jj][cc+1] = '=';
      strcpy(inputs[jj]+cc+2,kdata[ii]);
      jj++;

      if (strmatchcase(keys[ii],"GPSLatitude")) {                                //  take care of latitude N/S
         if (*kdata[ii] == '-')
            inputs[jj] = zstrdup("-GPSLatitudeRef=S");
         else
            inputs[jj] = zstrdup("-GPSLatitudeRef=N");
         jj++;
      }

      if (strmatchcase(keys[ii],"GPSLongitude")) {                               //  and longitude E/W
         if (*kdata[ii] == '-')
            inputs[jj] = zstrdup("-GPSLongitudeRef=W");
         else
            inputs[jj] = zstrdup("-GPSLongitudeRef=E");
         jj++;
      }
   }

   inputs[jj] = zstrdup(file);                                                   //  last input is filename
   jj++;

   exif_server(jj,inputs,0);                                                     //  outputs discarded

   for (ii = 2; ii < jj; ii++)                                                   //  free memory
       zfree(inputs[ii]);

   return 0;
}


/********************************************************************************/

//  copy EXIF/IPTC data from one image file to new (edited) image file
//  if nkeys > 0, up to 20 keys may be replaced with new values
//  exiftool -m -tagsfromfile file1 -all -xmp -icc_profile [-keyname=newvalue ...]
//                file2 -overwrite_original

int exif_copy(cchar *file1, cchar *file2, cchar **keys, cchar **kdata, int nkeys)
{
   char     *inputs[30];                                                         //  revised
   int      cc, ii, jj;

   if (nkeys > 20) zappcrash("exif_copy() nkeys %d",nkeys);

   inputs[0] = (char *) "-m";                                                    //  -m               (suppress warnings)
   inputs[1] = (char *) "-tagsfromfile";                                         //  -tagsfromfile
   inputs[2] = zstrdup(file1);                                                   //  file1
   inputs[3] = (char *) "-all";                                                  //  -all
   inputs[4] = (char *) "-xmp";                                                  //  -xmp
   inputs[5] = (char *) "-icc_profile";                                          //  -icc_profile

   jj = 6;                                                                       //  count of inputs so far

   for (int ii = 0; ii < nkeys; ii++)                                            //  -keyname=keyvalue
   {
      cc = strlen(keys[ii]) + strlen(kdata[ii]) + 3;
      inputs[jj] = (char *) zmalloc(cc);
      *inputs[jj] = 0;
      strncatv(inputs[jj],cc,"-",keys[ii],"=",kdata[ii],null);
      jj++;
   }

   inputs[jj++] = zstrdup(file2);                                                //  file2
   inputs[jj++] = (char *) "-overwrite_original";                                //  -overwrite_original

   exif_server(jj,inputs,0);

   zfree(inputs[2]);                                                             //  free memory
   for (ii = 6; ii < jj-1; ii++)
   zfree(inputs[ii]);

   return 0;
}


/********************************************************************************

   int exif_server(int Nrecs, char **inputs, char **outputs)

   Server wrapper for exiftool for put/get exif/iptc data.
   This saves perl startup overhead for each call (0.1 >> 0.01 secs).

   Input records: -opt1
                  -opt2
                    ...
                  -keyword1=value1
                  -keyword2=value2
                    ...
                  filename.jpg
                  (null pointer)

   Returned: list of pointers to resulting output records.
   There are <= Nrecs outputs corresponding to keyword inputs.
   These are formatted keyword: text-string-value.
   If < Nrecs output, unused outputs are null pointers.
   These are subject to zfree().
   If outputs = null, outputs are discarded.

   Returns 0 if OK, +N if error.

   First call:
      Starts exiftool with pipe input and output files.
   Subsequent calls:
      The existing exiftool process is re-used so that the
      substantial startup overhead is avoided.
   To kill the exiftool process: exif_server(0,0,0).

*********************************************************************************/

int exif_server(int Nrecs, char **inputs, char **outputs)                        //  revised API
{
   static int   fcf = 1;                                                         //  first call flag
   static FILE  *fid1 = 0, *fid2 = 0;                                            //  exiftool input, output files
   static char  input_file[100] = "";
   int          ii, cc, err;
   char         *pp, command[100];
   char         outrec[exif_maxcc];                                              //  single output record

   if (! *input_file)                                                            //  exif_server input file
      snprintf(input_file,100,"%s/exiftool_input",tempdir);

   if (! Nrecs)                                                                  //  kill exiftool process
   {                                                                             //  (also orphaned process)
      fid1 = fopen(input_file,"a");
      if (fid1) {
         fprintf(fid1,"-stay_open\nFalse\n");                                    //  tell it to exit
         fclose(fid1);                                                           //  bugfix: fflush after fclose
      }
      remove(input_file);
      if (fid2) pclose(fid2);
      fid2 = 0;
      fcf = 1;                                                                   //  start exiftool process if called again
      return 0;
   }

   if (fcf)                                                                      //  first call only
   {
      fid1 = fopen(input_file,"w");                                              //  start exiftool input file
      if (! fid1) {
         zmessageACK(Mwin,"exif_server: %s \n",strerror(errno));
         return 1;
      }

      snprintf(command,100,"exiftool -stay_open True -@ %s",input_file);

      fid2 = popen(command,"r");                                                 //  start exiftool and output file
      if (! fid2) {
         zmessageACK(Mwin,"exif_server: %s \n",strerror(errno));
         fclose(fid1);
         return 2;
      }

      fcf = 0;
   }

   gallery_monitor("stop");                                                      //  stop excess gallery inits

   for (ii = 0; ii < Nrecs; ii++)
      fprintf(fid1,"%s\n",inputs[ii]);                                           //  write to exiftool, 1 per record

   err = fprintf(fid1,"-execute\n");                                             //  tell exiftool to process
   if (err < 0) {
      zmessageACK(Mwin,"exif_server: %s \n",strerror(errno));
      return 3;
   }

   fflush(fid1);                                                                 //  flush buffer

   if (outputs) {
      cc = Nrecs * sizeof(char *);                                               //  clear outputs
      memset(outputs,0,cc);
   }

   for (ii = 0; ii < 99; ii++)                                                   //  get ALL exiftool outputs
   {
      pp = fgets_trim(outrec,exif_maxcc,fid2,1);
      if (! pp) break;
      if (strncmp(outrec,"{ready}",7) == 0) break;                               //  look for output end
      if (outputs && ii < Nrecs)
         outputs[ii] = zstrdup(outrec);                                          //  add to returned records
   }

   gallery_monitor("start");
   return 0;
}


/********************************************************************************/

//  convert between EXIF and fotoxx tag date formats
//  EXIF date: yyyy:mm:dd hh:mm:ss        20 chars.
//  tag date: yyyymmddhhmmss              16 chars.
//

void exif_tagdate(cchar *exifdate, char *tagdate)
{
   int      cc;

   memset(tagdate,0,15);
   cc = strlen(exifdate);

   if (cc > 3) strncpy(tagdate+0,exifdate+0,4);
   if (cc > 6) strncpy(tagdate+4,exifdate+5,2);
   if (cc > 9) strncpy(tagdate+6,exifdate+8,2);
   if (cc > 12) strncpy(tagdate+8,exifdate+11,2);
   if (cc > 15) strncpy(tagdate+10,exifdate+14,2);
   if (cc > 18) strncpy(tagdate+12,exifdate+17,2);
   tagdate[14] = 0;
   return;
}

void tag_exifdate(cchar *tagdate, char *exifdate)
{
   int      cc;

   memset(exifdate,0,20);
   cc = strlen(tagdate);

   strcpy(exifdate,"1900:01:01 00:00:00");
   if (cc > 3) strncpy(exifdate+0,tagdate+0,4);
   if (cc > 5) strncpy(exifdate+5,tagdate+4,2);
   if (cc > 7) strncpy(exifdate+8,tagdate+6,2);
   if (cc > 9) strncpy(exifdate+11,tagdate+8,2);
   if (cc > 11) strncpy(exifdate+14,tagdate+10,2);
   if (cc > 13) strncpy(exifdate+17,tagdate+12,2);
   exifdate[19] = 0;
   return;
}


/********************************************************************************
   Functions to read and write image index records
*********************************************************************************/

//  Initialize for reading/writing to the image index.
//  Build a memory map of the first image file in each image index subfile.
//  Set refresh = 1 to initialize after an index file has been split
//    or the first image file in an image index file has changed.
//  Returns 0 if OK, +N otherwise.
//
//    /.../.fotoxx/image_index/index_001
//    /.../.fotoxx/image_index/index_002
//    /.../.fotoxx/image_index/index_...         up to 999
//
//  Each of these files contains up to 'image_index_max' image filespecs.


char     **image_index_map = 0;                                                  //  1st image file in each index file
FILE     *sxrec_fid1, *sxrec_fid2;                                               //  FIDs left open across calls        16.06

int   image_fcomp(cchar *file1, cchar *file2);                                   //  compare two image file names


int init_image_index(int refresh)
{
   int      ii, cc;
   char     *pp;
   char     indexfile[200];
   char     buff[indexrecl];
   FILE     *fid;

   if (image_index_map && ! refresh) return 0;                                   //  already initialized

   if (image_index_map) {                                                        //  free memory
      for (ii = 0; image_index_map[ii]; ii++)
         zfree(image_index_map[ii]);
      zfree(image_index_map);
   }

   cc = 1001 * sizeof(char *);                                                   //  allocate memory
   image_index_map = (char **) zmalloc(cc);
   memset(image_index_map,0,cc);

   for (ii = 0; ii < 999; ii++)                                                  //  read image index files 001 to 999
   {
      snprintf(indexfile,200,"%s/index_%03d",index_dirk,ii+1);                   //  image_index_map[ii] is for
      fid = fopen(indexfile,"r");                                                //    image_index_file_(ii+1)
      if (! fid) break;
      pp = fgets_trim(buff,indexrecl,fid);
      if (pp && strmatchN(pp,"file: ",6))
         pp = zstrdup(pp+6);                                                     //  map first image file
      else pp = zstrdup("empty");                                                //  if none, map "empty"
      image_index_map[ii] = pp;
      fclose(fid);
   }

   image_index_map[ii] = 0;                                                      //  mark EOL

   if (ii == 0) {
      zmessageACK(Mwin,ZTX("image index is missing"));
      m_quit(0,0);
   }

   if (ii == 999) {
      zmessageACK(Mwin,"too many image index files");
      m_quit(0,0);
   }

   return 0;
}


//  Return which image index file a given image file belongs in.

int find_image_index_file(cchar *file)
{
   char     *pp;
   int      ii, nn;

   init_image_index(0);

   for (ii = 0; (pp = image_index_map[ii]); ii++)
   {
      if (strmatch(pp,"empty")) continue;                                        //  same sort as image_index_compare()
      nn = strcasecmp(file,pp);
      if (nn == 0) nn = strcmp(file,pp);
      if (nn < 0) break;                                                         //  file < 1st index file, go back 1
   }

   if (ii == 0) ii = 1;                                                          //  file < 1st file in 1st index file

   while (ii > 1 && strmatch(image_index_map[ii-1],"empty")) ii--;
   return ii;                                                                    //  (map ii is for index file ii+1)
}


/********************************************************************************/

//  Split an index file when the number of entries > image_index_max
//  All following index files are renumbered +1.
//  Index_index file is refreshed.
//  Returns 0 if OK, else +N.

int split_image_index_file(int Nth)
{
   int      ii, err, nn, last, filecount;
   char     indexfile1[200], indexfile2[200], indexfile3[200];
   char     buff[indexrecl], *pp;
   FILE     *fid1 = 0, *fid2 = 0, *fid3 = 0;
   STATB    statb;
   
   if (sxrec_fid1) fclose(sxrec_fid1);                                           //  stop get_sxrec() and *_min()       16.06
   if (sxrec_fid2) fclose(sxrec_fid2);
   sxrec_fid1 = sxrec_fid2 = 0;

   for (last = Nth; last < 999; last++)                                          //  find last index file
   {
      snprintf(indexfile1,200,"%s/index_%03d",index_dirk,last);                  //  /.../.fotoxx/image_index/index_NNN
      err = stat(indexfile1,&statb);
      if (err) break;
   }

   last--;

   if (last == 999) {
      zmessageACK(Mwin,"too many image index files");
      return 1;
   }

   for (ii = last; ii > Nth; ii--)                                               //  rename files following Nth
   {                                                                             //    index_012 >> index_013 etc.
      snprintf(indexfile1,200,"%s/index_%03d",index_dirk,ii);
      snprintf(indexfile2,200,"%s/index_%03d",index_dirk,ii+1);
      err = rename(indexfile1,indexfile2);
      if (err) goto file_err;
   }

   snprintf(indexfile1,200,"%s/index_%03d",index_dirk,Nth);                      //  read Nth file
   snprintf(indexfile2,200,"%s/index_%03d_temp2",index_dirk,Nth);                //  write 2 temp. files
   snprintf(indexfile3,200,"%s/index_%03d_temp3",index_dirk,Nth);

   fid1 = fopen(indexfile1,"r");
   if (! fid1) goto file_err;
   fid2 = fopen(indexfile2,"w");
   if (! fid2) goto file_err;
   fid3 = fopen(indexfile3,"w");
   if (! fid3) goto file_err;

   filecount = 0;

   while (true)                                                                  //  copy half the entries
   {                                                                             //    to temp2 file
      pp = fgets_trim(buff,indexrecl,fid1);
      if (! pp) break;
      if (strmatchN(pp,"file: ",6)) filecount++;
      if (filecount > image_index_max/2) break;
      nn = fprintf(fid2,"%s\n",pp);
      if (! nn) goto file_err;
   }

   if (pp) {
      nn = fprintf(fid3,"%s\n",pp);                                              //  copy remaining record sets
      if (! nn) goto file_err;                                                   //    to temp3 file
   }

   while (true)
   {
      pp = fgets_trim(buff,indexrecl,fid1);
      if (! pp) break;
      nn = fprintf(fid3,"%s\n",pp);
      if (! nn) goto file_err;
   }

   err = fclose(fid1);
   err = fclose(fid2);
   err = fclose(fid3);
   fid1 = fid2 = fid3 = 0;
   if (err) goto file_err;

   err = rename(indexfile2,indexfile1);                                          //  temp2 file replaces Nth index file
   if (err) goto file_err;

   snprintf(indexfile1,200,"%s/index_%03d",index_dirk,Nth+1);                    //  temp3 file >> Nth+1 index file
   err = rename(indexfile3,indexfile1);
   if (err) goto file_err;

   err = init_image_index(1);                                                    //  update image index map
   return err;

file_err:
   zmessageACK(Mwin,"split_image_index error \n %s",strerror(errno));
   if (fid1) fclose(fid1);
   if (fid2) fclose(fid2);
   if (fid3) fclose(fid3);
   return 3;
}


/********************************************************************************/

//  Get the image index record for given image file.
//  Returns 0 if OK, 1 if not found, >1 if error (diagnosed).
//  Returned sxrec_t data has allocated fields subject to zfree().
//
//  Index file is kept open across calls to reduce overhead for the
//  normal case of records being accessed in image file sequence
//  (gallery paint). Random access works, but is slower.

int get_sxrec(sxrec_t &sxrec, cchar *file)
{
   FILE           *fid = 0;
   char           indexfile[200];
   static char    buff[indexrecl];
   static char    pfile[XFCC];
   int            err, Nth, nn = 0, Fcontinue;
   cchar          *pp, *pp2;
   static int     pNth, logerr = 0;
   STATB          statb;

   memset(&sxrec,0,sizeof(sxrec));                                               //  clear output record

   err = stat(file,&statb);                                                      //  check image file exists
   if (err) return 1;

   Fcontinue = 1;                                                                //  test if prior search can continue
   if (! sxrec_fid1) Fcontinue = 0;                                              //  no prior search still open
   Nth = find_image_index_file(file);                                            //  get index file for this image file
   if (Nth != pNth) Fcontinue = 0;                                               //  index file not the same
   if (image_fcomp(file,pfile) <= 0) Fcontinue = 0;                              //  gallery file sequence
   
   if (Fcontinue) fid = sxrec_fid1;
   else {
      if (sxrec_fid1) fclose(sxrec_fid1);
      snprintf(indexfile,200,"%s/index_%03d",index_dirk,Nth);                    //  /.../.fotoxx/image_index/index_NNN
      fid = fopen(indexfile,"r");                                                //  open index_NNN file
      sxrec_fid1 = fid;
      if (! fid) goto file_err;
      *buff = 0;                                                                 //  no prior data in buffer
   }

   if (! strmatchN(buff,"file: ",6) || ! strmatch(buff+6,file))                  //  file in buffer not my file
   {
      while (true)                                                               //  loop until my file found
      {
         pp = fgets_trim(buff,indexrecl,fid);                                    //  read existing records
         if (! pp) break;                                                        //  EOF
         if (! strmatchN(pp,"file: ",6)) continue;                               //  to start of next file set
         nn = image_fcomp(pp+6,file);
         if (nn >= 0) break;                                                     //  same or after my file
      }

      if (! pp || nn > 0) {                                                      //  EOF or after, file not found
         fclose(fid);
         sxrec_fid1 = 0;
         return 1;
      }
   }
   
   pNth = Nth;                                                                   //  set up for poss. continuation
   strcpy(pfile,file);

   sxrec.file = zstrdup(file);                                                   //  build sxrec

   while (true)                                                                  //  get recs following "file" rec.
   {
      pp = fgets_trim(buff,indexrecl,fid);
      if (! pp) break;

      if (strmatchN(pp,"file: ",6)) break;                                       //  ran into next file rec.

      if (strmatchN(pp,"date: ",6)) {                                            //  EXIF (photo) date
         pp += 6;
         pp2 = strField(pp,' ',1);                                               //  EXIF (photo) date, yyyymmddhhmmss
         if (pp2) strncpy0(sxrec.pdate,pp2,15);
         pp2 = strField(pp,' ',2);
         if (pp2) strncpy0(sxrec.fdate,pp2,15);                                  //  file date, yyyymmddhhmmss
      }

      else if (strmatchN(pp,"stars: ",7)) {                                      //  rating, '0' to '5' stars
         sxrec.rating[0] = *(pp+7);
         sxrec.rating[1] = 0;
      }

      else if (strmatchN(pp,"size: ",6))                                         //  size, "NNNNxNNNN"
         strncpy0(sxrec.size,pp+6,15);

      else if (strmatchN(pp,"tags: ",6))                                         //  tags
         sxrec.tags = zstrdup(pp+6);

      else if (strmatchN(pp,"capt: ",6))                                         //  caption
         sxrec.capt = zstrdup(pp+6);

      else if (strmatchN(pp,"comms: ",7))                                        //  comments
         sxrec.comms = zstrdup(pp+7);

      else if (strmatchN(pp,"gtags: ",7))                                        //  geotags
         sxrec.gtags = zstrdup(pp+7);
   }

   if (! sxrec.pdate[0])                                                         //  supply defaults for missing items
      strcpy(sxrec.pdate,"null");

   if (! sxrec.rating[0])
      strcpy(sxrec.rating,"0");

   if (! sxrec.size[0])
      strcpy(sxrec.size,"null");

   if (! sxrec.tags)
      sxrec.tags = zstrdup("null, ");

   if (! sxrec.capt)
      sxrec.capt = zstrdup("null");

   if (! sxrec.comms)
      sxrec.comms = zstrdup("null");

   if (! sxrec.gtags)
      sxrec.gtags = zstrdup("null^ null^ null^ null");

   return 0;

file_err:
   if (! logerr) printz("image index read error: %s \n",strerror(errno));
   logerr++;
   if (fid) fclose(fid);
   sxrec_fid1 = 0;
   return 3;
}


/********************************************************************************/

//  minimized version of get_sxrec() for use by gallery window.
//  fdate[16], pdate[16] and size[16] are provided by caller for returned data.
//  returns 0 if found, 1 if not, 2+ if error (diagnosed)
//  mean execution time: 0.3 millisecs per file

int get_sxrec_min(cchar *file, char *fdate, char *pdate, char *size)
{
   FILE           *fid = 0;
   char           indexfile[200];
   static char    buff[indexrecl];
   static char    pfile[XFCC];
   int            Nth, nn = 0, Fcontinue;
   cchar          *pp;
   static int     pNth, logerr = 0;

   strcpy(fdate,"");                                                             //  outputs = missing
   strcpy(pdate,"undated");
   strcpy(size,"");

   Fcontinue = 1;                                                                //  test if prior search can continue
   if (! sxrec_fid2) Fcontinue = 0;                                              //  no prior search still open
   Nth = find_image_index_file(file);                                            //  get index file for this image file
   if (Nth != pNth) Fcontinue = 0;                                               //  index file not the same
   if (image_fcomp(file,pfile) <= 0) Fcontinue = 0;                              //  req. file not after prior file

   if (Fcontinue) fid = sxrec_fid2;
   else {
      if (sxrec_fid2) fclose(sxrec_fid2);
      snprintf(indexfile,200,"%s/index_%03d",index_dirk,Nth);                    //  /.../.fotoxx/image_index/index_NNN
      fid = fopen(indexfile,"r");                                                //  open index_NNN file
      if (! fid) goto file_err;
      sxrec_fid2 = fid;
      *buff = 0;                                                                 //  no prior data in buffer
   }

   if (! strmatchN(buff,"file: ",6) || ! strmatch(buff+6,file))                  //  file in buffer not my file
   {
      while (true)                                                               //  loop until my file found
      {
         pp = fgets_trim(buff,indexrecl,fid);                                    //  read existing records
         if (! pp) break;                                                        //  EOF
         if (! strmatchN(pp,"file: ",6)) continue;                               //  to start of next file set
         nn = image_fcomp(pp+6,file);
         if (nn >= 0) break;                                                     //  same or after my file
      }

      if (! pp || nn > 0) {                                                      //  EOF or after, file not found
         fclose(fid);
         sxrec_fid2 = 0;
         return 1;
      }
   }

   pNth = Nth;                                                                   //  set up for poss. continuation
   strcpy(pfile,file);

   while (true)                                                                  //  get recs following "file" rec.
   {
      pp = fgets_trim(buff,indexrecl,fid);
      if (! pp) break;
      if (strmatchN(pp,"file: ",6)) break;                                       //  ran into next file rec.

      if (strmatchN(pp,"date: ",6))                                              //  date record
      {
         pp = strField(buff,' ',2);
         if (pp) {
            if (*pp != 'n') strncpy0(pdate,pp,15);                               //  photo date unless "null"
            pp = strField(buff,' ',3);
            if (pp) strncpy0(fdate,pp,15);                                       //  file date
         }
      }

      else if (strmatchN(pp,"size: ",6))                                         //  size, "NNNNxNNNN"
         strncpy0(size,pp+6,15);
   }

   return 0;

file_err:
   if (! logerr) printz("image index read error: %s \n",strerror(errno));
   logerr++;
   if (fid) fclose(fid);
   sxrec_fid2 = 0;
   return 3;
}


/********************************************************************************/

//  Add or update image index record for given image file.
//  If sxrec is null, delete index record.
//  Return 0 if success, +N if error.

int put_sxrec(sxrec_t *sxrec, cchar *file)
{
   FILE     *fid1 = 0, *fid2 = 0;
   char     indexfile[200], tempfile[200];
   char     buff[indexrecl];
   char     *pp;
   int      err, nn, Nth;
   int      filecount, Fcopy, Finsert, Finserted, Fnewfirst;
   STATB    statb, statb2;

   if (sxrec_fid1) fclose(sxrec_fid1);                                           //  stop get_sxrec() and *_min()       16.06
   if (sxrec_fid2) fclose(sxrec_fid2);
   sxrec_fid1 = sxrec_fid2 = 0;

   err = stat(file,&statb);                                                      //  check image file exists
   if (err && sxrec) return 1;

   Nth = find_image_index_file(file);                                            //  construct index file
   snprintf(indexfile,200,"%s/index_%03d",index_dirk,Nth);                       //  /.../.fotoxx/image_index/index_NNN

   err = stat(indexfile,&statb2);                                                //  missing? (don't clobber statb)     15.08
   if (err) {
      nn = creat(indexfile,0640);                                                //  create if needed
      if (nn < 0) goto file_err;
      close(nn);
   }

   fid1 = fopen(indexfile,"r");                                                  //  open index_NNN file
   if (! fid1) goto file_err;

   strcpy(tempfile,indexfile);                                                   //  temp image index file
   strcat(tempfile,"_temp");

   fid2 = fopen(tempfile,"w");                                                   //  open for output
   if (! fid2) goto file_err;

   Finsert = Finserted = Fcopy = Fnewfirst = 0;
   filecount = 0;

   while (true)                                                                  //  copy input to output
   {
      pp = fgets_trim(buff,indexrecl,fid1);                                      //  read existing records

      if (pp && strmatchN(pp,"file: ",6))                                        //  start of a file record set
      {
         Finsert = Fcopy = Fnewfirst = 0;

         nn = strcasecmp(file,pp+6);                                             //  compare input file to index file
         if (nn == 0) nn = strcmp(file,pp+6);
         if (nn <= 0 && sxrec && ! Finserted) Finsert = 1;                       //  input <= index file, insert sxrec here
         if (nn != 0) Fcopy = 1;                                                 //  input != index file, copy to output
         if (filecount == 0) {
            if (Finsert && nn < 0) Fnewfirst = 1;                                //  detect if first image file in index
            if (! Fcopy) Fnewfirst = 1;                                          //    file will be changed or deleted
         }
         filecount += Fcopy + Finsert;
      }

      if (! pp && sxrec && ! Finserted) {                                        //  input EOF, insert at the end
         Finsert = 1;
         if (filecount == 0) Fnewfirst = 1;
      }

      if (! pp && (Finserted || ! sxrec)) break;                                 //  done

      if (Finsert && ! Finserted)
      {
         Finserted = 1;                                                          //  new index file recs. go here

         nn = fprintf(fid2,"file: %s\n",file);                                   //  write new index file recs.
         if (! nn) goto file_err;

         compact_time(statb.st_mtime,sxrec->fdate);                              //  convert to "yyyymmddhhmmss"

         if (! sxrec->pdate[0]) strcpy(sxrec->pdate,"null");                     //  EXIF (photo) date, yyyy:mm:dd

         nn = fprintf(fid2,"date: %s  %s\n",sxrec->pdate,sxrec->fdate);          //  photo and file date rec.
         if (! nn) goto file_err;

         if (sxrec->rating[0])
            nn = fprintf(fid2,"stars: %s\n",sxrec->rating);                      //  rating rec.
         else nn = fprintf(fid2,"stars: 0\n");
         if (! nn) goto file_err;

         if (sxrec->size[0])
            nn = fprintf(fid2,"size: %s\n",sxrec->size);                         //  size rec.
         else nn = fprintf(fid2,"size: null\n");

         if (sxrec->tags)
            nn = fprintf(fid2,"tags: %s\n",sxrec->tags);                         //  tags rec.
         else nn = fprintf(fid2,"tags: null, ""\n");
         if (! nn) goto file_err;

         if (sxrec->capt)
            nn = fprintf(fid2,"capt: %s\n",sxrec->capt);                         //  caption rec.
         else nn = fprintf(fid2,"capt: null\n");
         if (! nn) goto file_err;

         if (sxrec->comms)
            nn = fprintf(fid2,"comms: %s\n",sxrec->comms);                       //  comments rec.
         else nn = fprintf(fid2,"comms: null\n");
         if (! nn) goto file_err;

         if (sxrec->gtags)
            nn = fprintf(fid2,"gtags: %s\n",sxrec->gtags);                       //  geotags rec.
         else nn = fprintf(fid2,"gtags: null^ null^ null^ null\n");
         if (! nn) goto file_err;

         nn = fprintf(fid2,"\n");                                                //  EOL blank rec.
         if (! nn) goto file_err;
      }

      if (pp && Fcopy) {                                                         //  copy input to output
         nn = fprintf(fid2,"%s\n",pp);                                           //    unless replaced by sxrec
         if (! nn) goto file_err;
      }
   }

   err = fclose(fid1);                                                           //  close input file
   err = fclose(fid2);                                                           //  close output file
   fid1 = fid2 = 0;
   if (err) goto file_err;

   err = rename(tempfile,indexfile);                                             //  replace index file with temp file
   if (err) goto file_err;

   if (filecount > image_index_max)                                              //  if index file too big, split
      split_image_index_file(Nth);
   else if (Fnewfirst)
      init_image_index(1);                                                       //  update image index map

   return 0;

file_err:
   zmessageACK(Mwin,"image index write error 3\n %s",strerror(errno));
   if (fid1) fclose(fid1);
   if (fid2) fclose(fid2);
   return 3;
}


/********************************************************************************/

//  Read image index files sequentially, return one index rec. per call.
//  Set ftf = 1 for first read, will be reset to 0.
//  Returns 0 if OK, 1 if EOF, 2 if error (diagnosed).
//  Returned sxrec_t data has allocated fields subject to zfree().

int read_sxrec_seq(sxrec_t &sxrec, int &ftf)
{
   char           indexfile[200];
   static FILE    *fid = 0;
   static char    buff[indexrecl];
   static int     Nth;
   cchar          *pp, *pp2;
   int            err;
   STATB          statb;

   if (ftf)                                                                      //  initial call
   {
      ftf = 0;
      snprintf(indexfile,200,"%s/index_001",index_dirk);                         //  first index file
      fid = fopen(indexfile,"r");
      if (! fid) return 2;                                                       //  no index file ?
      Nth = 1;
      *buff = 0;
   }

   while (true)
   {
      if (! strmatchN(buff,"file: ",6))                                          //  next file rec. may be already there
      {
         while (true)                                                            //  get start of next record set
         {
            pp = fgets_trim(buff,indexrecl,fid);
            if (pp && strmatchN(buff,"file: ",6)) break;
            if (pp) continue;
            fclose(fid);                                                         //  EOF, start next index file
            Nth++;
            snprintf(indexfile,200,"%s/index_%03d",index_dirk,Nth);
            fid = fopen(indexfile,"r");
            if (! fid) return 1;                                                 //  no more, final EOF
         }
      }

      *buff = 0;                                                                 //  no longer match "file: "
      err = stat(buff+6,&statb);                                                 //  check image file exists
      if (err) continue;
      if (S_ISREG(statb.st_mode)) break;
   }

   memset(&sxrec,0,sizeof(sxrec));                                               //  clear record

   sxrec.file = zstrdup(buff+6);                                                 //  image file name

   while (true)                                                                  //  get recs following "file" rec.
   {
      pp = fgets_trim(buff,indexrecl,fid);
      if (! pp) break;

      if (strmatchN(pp,"file: ",6)) break;                                       //  ran into next file rec.

      else if (strmatchN(pp,"date: ",6))
      {
         pp += 6;
         pp2 = strField(pp,' ',1);                                               //  EXIF (photo) date, yyyymmddhhmmss
         if (pp2) strncpy0(sxrec.pdate,pp2,15);
         pp2 = strField(pp,' ',2);
         if (pp2) strncpy0(sxrec.fdate,pp2,15);                                  //  file mod date, yyyymmddhhmmss
      }

      else if (strmatchN(pp,"stars: ",7)) {                                      //  rating, '0' to '5' stars
         sxrec.rating[0] = *(pp+7);
         sxrec.rating[1] = 0;
      }

      else if (strmatchN(pp,"size: ",6))                                         //  size, "NNNNxNNNN"
         strncpy0(sxrec.size,pp+6,15);

      else if (strmatchN(pp,"tags: ",6))                                         //  tags
         sxrec.tags = zstrdup(pp+6);

      else if (strmatchN(pp,"capt: ",6))                                         //  caption
         sxrec.capt = zstrdup(pp+6);

      else if (strmatchN(pp,"comms: ",7))                                        //  comments
         sxrec.comms = zstrdup(pp+7);

      else if (strmatchN(pp,"gtags: ",7))                                        //  geotags
         sxrec.gtags = zstrdup(pp+7);
   }

   if (! sxrec.pdate[0])                                                         //  supply defaults for missing items
      strcpy(sxrec.pdate,"null");

   if (! sxrec.rating[0])
      strcpy(sxrec.rating,"0");

   if (! sxrec.size[0])
      strcpy(sxrec.size,"null");

   if (! sxrec.tags)
      sxrec.tags = zstrdup("null, ");

   if (! sxrec.capt)
      sxrec.capt = zstrdup("null");

   if (! sxrec.comms)
      sxrec.comms = zstrdup("null");

   if (! sxrec.gtags)
      sxrec.gtags = zstrdup("null^ null^ null^ null");

   return 0;
}


/********************************************************************************/

//  Write the image index files sequentially, 1 rec. per call
//  Set ftf = 1 for first call, will be reset to 0.
//  Set sxrec = 0 to close file after last write.
//  Returns 0 if OK, otherwise +N.
//  Used by index image files function.

int write_sxrec_seq(sxrec_t *sxrec, int &ftf)
{
   static int     Nth, filecount;
   static FILE    *fid = 0;
   char           indexfile[200], oldirk[200];
   int            nn, err;
   STATB          statb;

   if (sxrec_fid1) fclose(sxrec_fid1);                                           //  stop get_sxrec() and *_min()       16.06
   if (sxrec_fid2) fclose(sxrec_fid2);
   sxrec_fid1 = sxrec_fid2 = 0;

   if (ftf)                                                                      //  first call
   {
      ftf = 0;
      err = stat(index_dirk,&statb);
      if (! err && S_ISREG(statb.st_mode)) {                                     //  rename old image_index
         strcpy(oldirk,index_dirk);
         strcat(oldirk,"_old");
         rename(index_dirk,oldirk);
      }

      err = stat(index_dirk,&statb);                                             //  create new image_index directory
      if (err) err = mkdir(index_dirk,0750);                                     //    if not already there
      if (err) goto file_err;

      err = shell_quiet("rm -f -v %s/index_* > /dev/null",index_dirk);           //  delete all image index files
      Nth = 1;
      snprintf(indexfile,200,"%s/index_%03d",index_dirk,Nth);                    //  create initial index file
      fid = fopen(indexfile,"w");
      if (! fid) goto file_err;
      filecount = 0;
   }

   if (! sxrec) {                                                                //  EOF call
      if (fid) {
         err = fclose(fid);
         fid = 0;
         if (err) goto file_err;
      }
      err = init_image_index(1);                                                 //  initz. image index map
      return err;
   }

   err = stat(sxrec->file,&statb);                                               //  check image file exists
   if (err || ! S_ISREG(statb.st_mode)) {
      printz("file %s not found \n",sxrec->file);
      return 0;
   }

   compact_time(statb.st_mtime,sxrec->fdate);                                    //  convert to "yyyymmddhhmmss"

   if (filecount > 0.8 * image_index_max) {                                      //  if 80% max reached,
      err = fclose(fid);                                                         //    start new index file
      fid = 0;
      if (err) goto file_err;
      if (++Nth > 999) {
         zmessageACK(Mwin,"too many image index files");
         return 2;
      }
      snprintf(indexfile,200,"%s/index_%03d",index_dirk,Nth);                    //  open/write next index file
      fid = fopen(indexfile,"w");
      if (! fid) goto file_err;
      filecount = 0;                                                             //  is empty
   }

   filecount++;                                                                  //  new image file record set

   nn = fprintf(fid,"file: %s\n",sxrec->file);                                   //  output: filename rec.
   if (! nn) goto file_err;

   if (! sxrec->pdate[0]) strcpy(sxrec->pdate,"null");                           //  EXIF (photo) date, yyyymmddhhmmss

   nn = fprintf(fid,"date: %s  %s\n",sxrec->pdate,sxrec->fdate);                 //  photo and file date rec.
   if (! nn) goto file_err;

   if (sxrec->rating[0])
      nn = fprintf(fid,"stars: %c\n",sxrec->rating[0]);                          //  rating rec.
   else nn = fprintf(fid,"stars: 0\n");
   if (! nn) goto file_err;

   if (sxrec->size[0])
      nn = fprintf(fid,"size: %s\n",sxrec->size);
   else nn = fprintf(fid,"size: null\n");

   if (sxrec->tags)
      nn = fprintf(fid,"tags: %s\n",sxrec->tags);                                //  tags rec.
   else nn = fprintf(fid,"tags: null, ""\n");
   if (! nn) goto file_err;

   if (sxrec->capt)
      nn = fprintf(fid,"capt: %s\n",sxrec->capt);                                //  caption rec.
   else nn = fprintf(fid,"capt: null\n");
   if (! nn) goto file_err;

   if (sxrec->comms)
      nn = fprintf(fid,"comms: %s\n",sxrec->comms);                              //  comments rec.
   else nn = fprintf(fid,"comms: null\n");
   if (! nn) goto file_err;

   if (sxrec->gtags)
      nn = fprintf(fid,"gtags: %s\n",sxrec->gtags);                              //  geotags rec.
   else nn = fprintf(fid,"gtags: null^ null^ null^ null\n");
   if (! nn) goto file_err;

   nn = fprintf(fid,"\n");                                                       //  EOL blank rec.
   if (! nn) goto file_err;

   return 0;

file_err:
   zmessageACK(Mwin,"image index write error 4\n %s",strerror(errno));
   if (fid) fclose(fid);
   fid = 0;
   return 3;
}


//  file name compare in image index sequence
//  use case blind compare first, then normal compare as a tiebreaker

int image_fcomp(cchar *file1, cchar *file2)
{
   int      nn;
   nn = strcasecmp(file1,file2);                                                 //  compare case blind
   if (nn != 0) return nn;
   nn = strcmp(file1,file2);                                                     //  if equal, use utf8 compare
   return nn;
}



