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

   Fotoxx      edit photos and manage collections

   Copyright 2007-2020 Michael Cornelison
   source code URL: https://kornelix.net
   contact: mkornelix@gmail.com

   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. See https://www.gnu.org/licenses

   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.

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

   Fotoxx image editor - image metadata functions.

   View and edit metadata
   ----------------------
   select_meta_keys           dialog to select metadata items
   m_meta_view_short          metadata short report
   m_meta_view_long           report all metadata
   m_meta_edit_main           primary edit metadata dialog
   m_meta_manage_tags         define tags (keywords) for image searching
   m_meta_edit_any            dialog to fetch and save any image metadata by name
   m_meta_delete              dialog to delete any image file metadata by name
   m_meta_captions            write captions and comments at top of current image
   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_photo_date_time    change or shift photo date/time
   m_batch_change_metadata    add/change or delete metadata for selected image files
   m_batch_report_metadata    batch metadata report to text file
   m_batch_geotags            add given geotag to selected set of images

   Image search utilities
   ----------------------
   m_meta_places_dates        find images by location and date range
   m_meta_timeline            find images by year and month
   m_autosearch               search function for use with scripts
   m_search_images            find images using any metadata and/or file names

   pdate_metadate             convert yyyy-mm-dd to yyyymmdd
   ptime_metatime             convert hh:mm[:ss] to hhmmss
   pdatetime_metadatetime     convert yyyy-mm-dd hh:mm[:ss] to yyyymmddhhmmss
   metadate_pdate             convert yyyymmddhhmmss to yyyy-mm-dd and hh:mm:ss
   datetimeOK                 validate yyyy-mm-dd hh:mm[:ss] date/time

   add_tag                    add tag to a tag list
   add_tag_fotoxx             add tag "fotoxx" for edited image (for f_save())
   set_meta_wwhh              set image width/height metadata
   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

   web_geocode                web service to map location to earth coordinates
   init_geolocs               load geolocations table from image index file
   find_location              find locations matching partial location/country input
   put_geolocs                put new location data in geolocations table
   validate_latlong           validate earth coordinates data
   earth_distance             compute km distance between two earth coordinates
   get_gallerymap             get map coordinates for current gallery files
   m_set_map_markers          set map markers: all images or current gallery

   Geotag mapping (W view)
   -----------------------
   m_load_filemap             load a geographic map file chosen by user
   filemap_position           convert earth coordinates to map pixel position
   filemap_coordinates        convert map pixel position to earth coordinates
   filemap_paint_dots         paint red dots on map where images are located
   filemap_mousefunc          respond to mouse movement and left clicks on map
   find_filemap_images        find images within range of geolocation
   free_filemap               free huge memory for filemap image

   Geotag mapping (M view)
   -----------------------
   m_netmap_source            choose net map source
   m_load_netmap              initialize net map
   netmap_paint_dots          paint red dots on map where images are located
   m_netmap_zoomin            zoom net map in on image location
   netmap_zoomto              callable with input zoom level
   netmapscale                get net map scale at zoom level
   netmap_mousefunc           respond to clicks on net map
   find_netmap_images         find images
   m_netmap_locs              save and recall net map locs (center, zoom level)

   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
   ---------------------
   get_xxrec                  get image index record for image file
   put_xxrec                  add or update index record for an image file
   read_xxrec_seq             read all index records sequentially, one per call
   write_xxrec_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[:ss]" to "hhmmss"
char  *pdatetime_metadatetime(cchar *pdatetime);                                 //  yyyy-mm-dd hh:mm[:ss] to yyyymmddhhmmss
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(int force);                                                   //  load tags_deftags from index data
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

cchar *web_geocode(zdialog *zd);                                                 //  find earth coordinates via web service
int   init_geolocs();                                                            //  load geolocs table from index file
int   geolocs_compare(cchar *rec1, cchar *rec2);                                 //  compare geolocs recs location
int   find_location(zdialog *zd);                                                //  find locations matching partial inputs
int   put_geolocs(zdialog *zd);                                                  //  Update geolocations table in memory
int   validate_latlong(char *lati, char *longi, float &flat, float &flong);      //  convert and validate earth coordinates
float earth_distance(float lat1, float long1, float lat2, float long2);          //  compute distance from earth coordinates
int   get_gallerymap();                                                          //  get map coordinates for gallery files
void  netmap_zoomto(float flati, float flongi, int zoomlev);                     //  zoom net map to location and zoom level
float netmapscale(int zoomlev, float flat, float flong);                         //  net map scale at given zoom and location

//  char variables with no data are always "", never a null pointer              //  20.0

namespace meta_names 
{
   char     meta_pdate[16];                                                      //  image (photo) date, yyyymmddhhmmss
   char     meta_rating[4];                                                      //  image rating in stars, "0" to "5"
   char     meta_wwhh[16];                                                       //  image width/height, "2345x1234"
   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: location, country
   char     meta_lati[20], meta_longi[20];                                       //  geolocs: earth coordinates (-123.4567)

   char     p_meta_pdate[16];                                                    //  previous file metadata
   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     *xmeta_data[Mxmeta];                                                 //  indexed metadata (xmeta_key[])

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

   char     keyname[40], keydata[exif_maxcc];

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

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

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

   struct gallerymap_t {                                                         //  geocoordinates for gallery files
      char     *file;
      float    flati, flongi;
   };

   gallerymap_t   *gallerymap = 0;
   int            Ngallerymap = 0;
}


using namespace meta_names;


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

//  Dialog to select metadata items (for index, view, report).
//  The item list is an input and an output. EOL marker is null.
//  Fexclude: exclude items indexed by default.
//  returns 0/1 = no changes / changes made

namespace select_meta_keys_names
{
   GtkWidget   *mtext1, *mtext2;
   int         Fexclude;
   zdialog     *zd;
}   

int select_meta_keys(char *itemlist[], int exclude)
{
   using namespace select_meta_keys_names;

   void  select_meta_keys_clickfunc1(GtkWidget *, int line, int pos, int kbkey);
   void  select_meta_keys_clickfunc2(GtkWidget *, int line, int pos, int kbkey);

   char        itemfile[200], buff[100], buff2[100], *pp;
   FILE        *fid;
   int         zstat, ii;
   
   Fexclude = exclude;

/***
       __________________________________________________________
      |                  Add Metadata Items                      |
      |                                                          |
      |       click to select            click to unselect       |
      |  _________________________   __________________________  |
      | | Orientation             | |                          | |
      | | Rotation                | |                          | |
      | | Exposure Time           | |                          | |
      | | Aperture                | |                          | |
      | |   ...                   | |                          | |
      | | Other Item ...          | |                          | |
      | |_________________________| |__________________________| |
      |                                                          |
      |                                          [done] [cancel] |
      |__________________________________________________________|

***/

   zd = zdialog_new(E2X("Add Metadata Items"),Mwin,Bdone,Bcancel,null);

   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",E2X("click to select"));
   zdialog_add_widget(zd,"scrwin","scroll1","vb1",0,"expand");
   zdialog_add_widget(zd,"text","mtext1","scroll1",0,"expand");

   zdialog_add_widget(zd,"vbox","vb2","hb1",0,"expand|space=3");
   zdialog_add_widget(zd,"label","lab2","vb2",E2X("click to unselect"));
   zdialog_add_widget(zd,"scrwin","scroll2","vb2",0,"expand");
   zdialog_add_widget(zd,"text","mtext2","scroll2",0,"expand");

   mtext1 = zdialog_widget(zd,"mtext1");
   textwidget_clear(mtext1);

   mtext2 = zdialog_widget(zd,"mtext2");
   textwidget_clear(mtext2);

   snprintf(itemfile,200,"%s/metadata_short_list",get_zhomedir());               //  metadata item list >> left widget
   fid = fopen(itemfile,"r");
   if (! fid) {
      zmessageACK(Mwin,"%s \n %s",itemfile,strerror(errno));
      return 0;
   }

   while (true) {
      pp = fgets_trim(buff,100,fid,1);
      if (! pp) break;
      if (*pp <= ' ') continue;
      if (Fexclude) {                                                            //  m_index() or m_meta_view_short()
         strCompress(buff2,buff);
         if (strmatch(buff2,iptc_keywords_key)) continue;                        //  exclude items indexed and
         if (strmatch(buff2,iptc_rating_key)) continue;                          //    reported by default
         if (strmatch(buff2,exif_date_key)) continue;
         if (strmatch(buff2,exif_comment_key)) continue;
         if (strmatch(buff2,exif_usercomment_key)) continue;
         if (strmatch(buff2,iptc_caption_key)) continue;
         if (strmatch(buff2,exif_city_key)) continue;
         if (strmatch(buff2,exif_country_key)) continue;
         if (strmatch(buff2,exif_lati_key)) continue;
         if (strmatch(buff2,exif_longi_key)) continue;
         if (strmatch(buff2,"ImageHistory")) continue;                           //  disallow                           19.10
      }
      textwidget_append(mtext1,0,"%s\n",buff);
   }
   
   textwidget_append(mtext1,0,"%s\n","Other Item ...");                          //  append "Other Item ..."

   fclose(fid);
   
   for (ii = 0; ii < Mxmeta; ii++)                                               //  input item list >> right widget
   {
      if (! itemlist[ii]) break;
      textwidget_append(mtext2,0,"%s\n",itemlist[ii]);
   }

   textwidget_set_eventfunc(mtext1,select_meta_keys_clickfunc1);                 //  set mouse/KB event function
   textwidget_set_eventfunc(mtext2,select_meta_keys_clickfunc2);

   zdialog_resize(zd,500,300);
   zdialog_set_modal(zd);
   zdialog_run(zd,0,0);                                                          //  run dialog
   zstat = zdialog_wait(zd);                                                     //  wait for dialog completion

   if (zstat != 1) {                                                             //  canceled
      zdialog_free(zd);
      return 0;                                                                  //  return "no change"
   }

   for (ii = 0; ii < Mxmeta; ii++) {                                             //  free input list
      if (! itemlist[ii]) break;
      zfree(itemlist[ii]);
   }

   for (ii = 0; ii < Mxmeta; ii++) {                                             //  replace input list with output
      pp = textwidget_line(mtext2,ii,1);                                         //  remove trailing \n
      if (! pp || ! *pp) break;
      itemlist[ii] = zstrdup(pp);
   }

   itemlist[ii] = 0;                                                             //  mark EOL

   zdialog_free(zd);
   return 1;                                                                     //  return "changes made"
}


//  get clicked tag name from input list and insert into output list

void select_meta_keys_clickfunc1(GtkWidget *widget, int line, int pos, int kbkey) 
{
   using namespace select_meta_keys_names;

   char     *pp;
   int      disallow = 0;
   
   if (kbkey == GDK_KEY_F1) {                                                    //  key F1 pressed, show help
      showz_docfile(Mwin,"userguide",F1_help_topic);
      return;
   }

   pp = textwidget_line(widget,line,1);                                          //  get clicked line, highlight
   if (! pp || ! *pp) return;
   textwidget_highlight_line(widget,line);
   
   if (strmatch(pp,"Other Item ...")) {                                          //  get manually input metadata name
      pp = zdialog_text(zd->dialog,"metadata item name",0);
      if (! pp) return;
      strCompress(pp);                                                           //  remove blanks
      if (Fexclude) {
         if (strmatchcase(pp,iptc_keywords_key)) disallow = 1;                   //  exclude items indexed by default   20.02
         if (strmatchcase(pp,iptc_rating_key)) disallow = 1;
         if (strmatchcase(pp,exif_date_key)) disallow = 1;
         if (strmatchcase(pp,exif_comment_key)) disallow = 1;
         if (strmatchcase(pp,exif_usercomment_key)) disallow = 1;
         if (strmatchcase(pp,iptc_caption_key)) disallow = 1;
         if (strmatchcase(pp,exif_city_key)) disallow = 1;
         if (strmatchcase(pp,exif_country_key)) disallow = 1;
         if (strmatchcase(pp,exif_lati_key)) disallow = 1;
         if (strmatchcase(pp,exif_longi_key)) disallow = 1;
         if (strmatchcase(pp,"ImageHistory")) disallow = 1;
         if (disallow) {
            zmessageACK(Mwin,"%s is already indexed",pp);
            return;
         }
      }
      
      textwidget_append2(mtext2,0,"%s\n",pp);                                    //  append to output list
      zfree(pp);
      return;
   }

   strCompress(pp);                                                              //  remove blanks
   textwidget_append2(mtext2,0,"%s\n",pp);                                       //  append to output list
   return;
}


//  get clicked tag name from output list and remove it

void select_meta_keys_clickfunc2(GtkWidget *widget, int line, int pos, int kbkey) 
{
   using namespace select_meta_keys_names;

   char     *pp1, *pp2;
   int      ii, Nitems = 0;
   char     *itemlist[Mxmeta];
   
   if (kbkey == GDK_KEY_F1) {                                                    //  key F1 pressed, show help
      showz_docfile(Mwin,"userguide",F1_help_topic);
      return;
   }

   pp1 = textwidget_line(widget,line,1);                                         //  get clicked line
   if (! pp1 || ! *pp1) return;
   
   for (line = 0; ; line++)
   {                                                                             //  read and save all lines
      pp2 = textwidget_line(mtext2,line,1);
      if (! pp2 || ! *pp2) break;
      if (strmatch(pp2,pp1)) continue;                                           //  skip clicked line
      itemlist[Nitems] = zstrdup(pp2);
      if (++Nitems == Mxmeta) break;
   }
   
   textwidget_clear(mtext2);                                                     //  clear output list
   
   for (ii = 0; ii < Nitems; ii++)                                               //  write new output list
   {
      textwidget_append(mtext2,0,"%s\n",itemlist[ii]);
      zfree(itemlist[ii]);
   }

   return;
}


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

//  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 zd_metaview is defined

void meta_view(int type)
{
   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 21                                                             //  19.16
   cchar       *keyname[NK] = 
               { "ImageSize", "FileSize", exif_date_key, "FileModifyDate", "Make", "Model",
                  exif_focal_length_35_key, 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           *focallength, chsec[12], **text;
   static char    *file, *filen, *chsize;
   float          fsecs;
   int            err, ii, nn, fsize;
   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 meta";

   if (FGWM != 'F' && FGWM != 'G') return;                                       //  19.0
   
   if (clicked_file) {                                                           //  use clicked file if present
      file = clicked_file;
      clicked_file = 0;
   }
   else if (curr_file) file = zstrdup(curr_file);
   else return;
   
   if (FGWM == 'G') gallery(0,"paint",-1);                                       //  if gallery view, repaint

   if (metadata_report_type != 1) {
      if (zd_metaview) zdialog_free(zd_metaview);
      zd_metaview = 0;
      metadata_report_type = 1;
   }
   
   if (! zd_metaview)                                                            //  popup dialog if not already
   {
      zd_metaview = zdialog_new(E2X("View Metadata"),Mwin,E2X("Extras"),Bcancel,null);
      zdialog_add_widget(zd_metaview,"scrwin","scroll","dialog",0,"expand");
      zdialog_add_widget(zd_metaview,"text","exifdata","scroll",0,"expand");
      zdialog_resize(zd_metaview,550,350);
      zdialog_run(zd_metaview,meta_view_dialog_event,"save");
      zdialog_can_focus(zd_metaview,0);                                          //  set no focus                       19.0
   }
   
   widget = zdialog_widget(zd_metaview,"exifdata");
   textwidget_clear(widget);

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

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

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

   if (keyval[2] && strlen(keyval[2]) > 19) keyval[2][19] = 0;                   //  truncate dates to yyyy-mm-dd hh:mm:ss
   if (keyval[3] && strlen(keyval[3]) > 19) keyval[3][19] = 0;
   
   pp = keyval[2];
   if (pp && strlen(pp) > 4 && pp[4] == ':') pp[4] = '-';                        //  change yyyy:mm:dd to yyyy-mm-dd
   if (pp && strlen(pp) > 7 && pp[7] == ':') pp[7] = '-';
   pp = keyval[3];
   if (pp && strlen(pp) > 4 && pp[4] == ':') pp[4] = '-';
   if (pp && strlen(pp) > 7 && pp[7] == ':') pp[7] = '-';

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

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

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

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

   if (keyval[15])
      textwidget_append(widget,0,"Keywords    %s \n",keyval[15]);                //  tags

   if (keyval[16])                                                               //  rating
      textwidget_append(widget,0,"Rating      %s \n",keyval[16]);

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

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

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

   if (keyval[20]) {                                                             //  edit history log
      nn = breakup_text(keyval[20],text,editdelims,40,60);
      textwidget_append(widget,0,"Edits       %s \n",text[0]);
      for (ii = 1; ii < nn; ii++)
         textwidget_append(widget,0,"            %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_zhomedir()); 
   fid = fopen(extras_file,"r"); 
   if (! fid) goto finished;                                                     //  no extras file

   for (nkx = 0; nkx < NK; nkx++) {                                              //  get list of user extras
      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;
   }

   textwidget_append(widget,0,"\n");                                             //  blank line
   
   for (ii = 0; ii < nkx; ii++)                                                  //  report user extra items
      if (keyvalx[ii]) 
         textwidget_append(widget,0,"%-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:
   zfree(file);
   return;
}


//  menu function - long report

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

   FILE           *fid;
   char           *file, *pp, buff[1000];
   GtkWidget      *widget;

   F1_help_topic = "view meta";

   if (FGWM != 'F' && FGWM != 'G') return;                                       //  19.0

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

   if (FGWM == 'G') gallery(0,"paint",-1);                                       //  if gallery view, repaint

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

   if (! zd_metaview)                                                            //  popup dialog if not already
   {
      zd_metaview = zdialog_new(E2X("View All Metadata"),Mwin,Bcancel,null);
      zdialog_add_widget(zd_metaview,"scrwin","scroll","dialog",0,"expand");
      zdialog_add_widget(zd_metaview,"text","exifdata","scroll",0,"expand|wrap");
      zdialog_resize(zd_metaview,700,700);
      zdialog_run(zd_metaview,meta_view_dialog_event,"save");
      zdialog_can_focus(zd_metaview,0);                                          //  set no focus                       19.0
   }

   widget = zdialog_widget(zd_metaview,"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
   textwidget_clear(widget);

   snprintf(command,CCC,"exiftool -s -e \"%s\" ",file);                          //  exiftool command
   fid = popen(command,"r");                                                     //  get command outputs                20.0
   if (fid) {
      while ((pp = fgets_trim(buff,1000,fid)))
         textwidget_append(widget,0,"%s\n",pp);                                  //  add to report window
   }
   pclose(fid);

   zfree(file);
   return;
}


//  dialog event and completion callback function

int meta_view_dialog_event(zdialog *zd, cchar *event)
{
   char     filex[200];
   int      zstat, ii, jj;
   char     *itemlist[Mxmeta], *pp, buff[100];
   FILE     *fid;

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

   zdialog_free(zd);                                                             //  kill dialog
   zd_metaview = 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_zhomedir());                  //  [extras] button
   
   itemlist[0] = 0;                                                              //  extra metadata items = empty

   fid = fopen(filex,"r");                                                       //  read file of extra metadata items
   if (fid) {
      for (ii = jj = 0; ii < Mxmeta; ii++) {
         pp = fgets_trim(buff,100,fid,1);
         if (! pp) break;
         if (*pp <= ' ') continue;
         itemlist[jj++] = zstrdup(pp);
      }
      
      itemlist[jj] = 0;                                                          //  mark EOL
      fclose(fid);
   }

   select_meta_keys(itemlist,1);                                                 //  user edit of extras list
   
   fid = fopen(filex,"w");                                                       //  update extras list file
   if (! fid) {
      zmessageACK(Mwin,"%s \n %s",filex,strerror(errno));
      return 1;
   }
   
   for (ii = 0; ii < Mxmeta; ii++)
   {
      if (! itemlist[ii]) break;
      fprintf(fid,"%s\n",itemlist[ii]);
   }

   fclose(fid);

   return 1;
}


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

//  edit metadata menu function

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

   cchar    *mapquest = "Geocoding by MapQuest";

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

   F1_help_topic = "edit meta";

   if (FGWM != 'F' && FGWM != 'G') return;                                       //  19.0

   if (clicked_file) {                                                           //  use clicked file if present
      if (! curr_file || ! strmatch(clicked_file,curr_file))                     //  avoid f_open() re-entry
         f_open(clicked_file,0,0,1,0);
      clicked_file = 0;
   }
   
   if (! curr_file) {
      if (zd_editmeta) zdialog_free(zd_editmeta);
      zd_editmeta = 0;
      zd_mapgeotags = 0;
      return;
   }
   
   err = access(curr_file,W_OK);                                                 //  test file can be written by me     19.1
   if (err) {
      zmessageACK(Mwin,"%s: %s",Bnowriteperm,curr_file);
      return;
   }
   
   if (! init_geolocs()) return;                                                 //  initialize geotags

   if (FGWM == 'G') gallery(0,"paint",-1);                                       //  if gallery view, repaint

/***
          ___________________________________________________________
         |                      Edit Metadata                        |
         |                                                           |
         |  File: filename.jpg                                       |
         |  Image Date: [__________]  Time: [________]  [prev]       |
         |  Rating (stars): (o) 1  (o) 2  (o) 3  (o) 4  (o) 5  (o) 6 |
         |  Caption [______________________________________________] |
         |  Comments [_____________________________________________] |
         |  - - - - - - - - - - - - - - - - - - - - - - - - - - - -  |
         |  location [_______________]  country [______________]     |
         |  latitude [__________]  longitude [__________]            |
         |  [Find] [Web] [Prev] [Clear]   Geocoding by MapQuest      |
         |  - - - - - - - - - - - - - - - - - - - - - - - - - - - -  |
         |  Current Tags [_________________________________________] |
         |  Recent Tags [__________________________________________] |
         |  Enter New Tag [__________]  [Add]                        |
         |  Matching Tags [________________________________________] |
         |  - - - - - - - - - - - - - - - - - - - - - - - - - - - -  |
         |  Defined Tags Category [______________________________|v] |
         |  |                                                      | |
         |  |                                                      | |
         |  |                                                      | |
         |  |                                                      | |
         |  |                                                      | |
         |  |                                                      | |
         |  |                                                      | |
         |  |______________________________________________________| |
         |                                                           |
         |                                  [Prev] [Apply] [Cancel]  |
         |___________________________________________________________|

***/

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

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

      //  Image Date [__________]  Time [_____]   [prev]
      zdialog_add_widget(zd,"hbox","hbdt","dialog",0,"space=1");
      zdialog_add_widget(zd,"label","labdate","hbdt",E2X("Image Date"),"space=3");
      zdialog_add_widget(zd,"zentry","date","hbdt",0,"size=12");
      zdialog_add_widget(zd,"label","space","hbdt",0,"space=5");
      zdialog_add_widget(zd,"label","labtime","hbdt",E2X("Time"),"space=3");
      zdialog_add_widget(zd,"zentry","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[:ss]");

      //  Rating (stars): ʘ 1  ʘ 2  ʘ 3  ʘ 4  ʘ 5  ʘ 6
      zdialog_add_widget(zd,"hbox","hbrate","dialog");
      zdialog_add_widget(zd,"label","labrate","hbrate",E2X("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",E2X("Caption"),"space=3");
      zdialog_add_widget(zd,"edit","caption","hbcap",0,"wrap|expand");

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

      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",E2X("Location"),"space=5");
      zdialog_add_widget(zd,"zentry","location","hbcc",0,"expand");
      zdialog_add_widget(zd,"label","space","hbcc",0,"space=5");
      zdialog_add_widget(zd,"label","labcountry","hbcc",E2X("Country"),"space=5");
      zdialog_add_widget(zd,"zentry","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,"zentry","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,"zentry","longi","hbll",0,"size=10");

      //  [Find] [Web] [Prev] [Clear]   Geocoding by MapQuest
      zdialog_add_widget(zd,"hbox","hbgeo","dialog",0,"space=3");
      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","geoprev","hbgeo",Bprev,"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");

      //  Image Tags [________________________________________] 
      zdialog_add_widget(zd,"hbox","hbit","dialog",0,"space=1");
      zdialog_add_widget(zd,"label","labit","hbit",E2X("Image Tags"),"space=3");
      zdialog_add_widget(zd,"frame","frit","hbit",0,"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",E2X("Recent Tags"),"space=3");
      zdialog_add_widget(zd,"frame","frrt","hbrt",0,"expand");
      zdialog_add_widget(zd,"text","recentags","frrt",0,"wrap");
      
      //  Enter New Tag [________________]  [Add]
      zdialog_add_widget(zd,"hbox","hbnt","dialog",0,"space=1");
      zdialog_add_widget(zd,"label","labnt","hbnt",E2X("Enter New Tag"),"space=3");
      zdialog_add_widget(zd,"zentry","newtag","hbnt",0,"size=20");
      zdialog_add_widget(zd,"zbutton","add","hbnt",Badd,"space=5");

      //  Matching Tags [_____________________________________]
      zdialog_add_widget(zd,"hbox","hbmt","dialog",0,"space=1");
      zdialog_add_widget(zd,"label","labmt","hbmt",E2X("Matching Tags"),"space=3");
      zdialog_add_widget(zd,"frame","frmt","hbmt",0,"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","hbdt1","dialog");
      zdialog_add_widget(zd,"label","labdt","hbdt1",E2X("Defined Tags Category"),"space=3");
      zdialog_add_widget(zd,"label","space","hbdt1",0,"expand");
      zdialog_add_widget(zd,"combo","defcats","hbdt1",0,"expand|space=5");

      zdialog_add_widget(zd,"hbox","hbdt2","dialog",0,"expand");
      zdialog_add_widget(zd,"scrwin","swdt2","hbdt2",0,"expand");
      zdialog_add_widget(zd,"text","deftags","swdt2",0,"wrap");
      
      zdialog_add_ttip(zd,"geofind",E2X("search known locations"));
      zdialog_add_ttip(zd,"geoweb",E2X("search using web service"));
      zdialog_add_ttip(zd,"geoprev",Bprevtip); 
      zdialog_add_ttip(zd,Bprev,Bprevtip); 

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

      widget = zdialog_widget(zd,"imagetags");                                   //  tag widget mouse/KB event functions
      textwidget_set_eventfunc(widget,edit_imagetags_clickfunc);

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

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

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

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

   zd = zd_editmeta;                                                             //  edit metadata active
   zd_mapgeotags = zd;                                                           //  map clicks active

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

   metadate_pdate(meta_pdate,pdate2,ptime2);                                     //  "yyyymmddhhmmss" to 
   zdialog_stuff(zd,"date",pdate2);                                              //    "yyyy-mm-dd" and "hh:mm:ss"
   zdialog_stuff(zd,"time",ptime2);

   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");                                    //  replace fake \n with real \n
   zdialog_stuff(zd,"caption",cctext);                                           //  (real \n not allowed in metadata)
   repl_1str(meta_comments,cctext,"\\n","\n");
   zdialog_stuff(zd,"comments",cctext);

   zdialog_stuff(zd,"location",meta_location);                                   //  geotags data >> dialog
   zdialog_stuff(zd,"country",meta_country);
   zdialog_stuff(zd,"lati",meta_lati);
   zdialog_stuff(zd,"longi",meta_longi);

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

   return;
}


//  mouse click functions for various text widgets for tags

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

   if (kbkey == GDK_KEY_F1) {                                                    //  key F1 pressed, show help
      showz_docfile(Mwin,"userguide",F1_help_topic);
      return;
   }

   txtag = textwidget_word(widget,line,pos,",;",end);
   if (! txtag) return; 

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

   zfree(txtag);
   return;
}


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

   if (kbkey == GDK_KEY_F1) {                                                    //  key F1 pressed, show help
      showz_docfile(Mwin,"userguide",F1_help_topic);
      return;
   }

   txtag = textwidget_word(widget,line,pos,",;",end);
   if (! txtag) return;

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

   zfree(txtag);
   return;
}


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

   if (kbkey == GDK_KEY_F1) {                                                    //  key F1 pressed, show help
      showz_docfile(Mwin,"userguide",F1_help_topic);
      return;
   }

   txtag = textwidget_word(widget,line,pos,",;",end);
   if (! txtag) return;

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

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

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

   zfree(txtag);
   return;
}


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

   if (kbkey == GDK_KEY_F1) {                                                    //  key F1 pressed, show help
      showz_docfile(Mwin,"userguide",F1_help_topic);
      return;
   }

   txtag = textwidget_word(widget,line,pos,",;:",end);
   if (! txtag || end == ':') return;                                            //  tag category clicked, ignore

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

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

   zfree(txtag);
   return;
}


//  dialog event and completion callback function

int editmeta_dialog_event(zdialog *zd, cchar *event)                             //  overhauled
{
   char     pdate2[12], ptime2[12];                                              //  yyyy-mm-dd  and  hh:mm:ss
   char     *metadate, *metatime;                                                //  yyyymmdd  and  hhmmss
   cchar    *errmess;
   int      ii, jj, nt, cc1, cc2, ff, err;
   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";
   char     location[100], country[100], lati[20], longi[20];
   float    flati, flongi;
   
   if (! curr_file) zd->zstat = 3;                                               //  current file gone

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

   if (zstrstr("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,pdate2,ptime2);
         zdialog_stuff(zd,"date",pdate2);
         zdialog_stuff(zd,"time",ptime2);
         Fmetamod++;
         return 1;
      }
   }

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

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

   if (strmatch(event,"geomap")) {                                               //  have geotags data from map click
      Fmetamod++;
      return 1;
   }
   
   if (strmatch(event,"geofind"))                                                //  [find] search location data
   {
      find_location(zd);                                                         //  find location data via user
      Fmetamod++;
      return 1;
   }

   if (strmatch(event,"geoweb"))                                                 //  [web] search location data
   {
      errmess = web_geocode(zd);                                                 //  look-up in web service
      if (errmess) zmessageACK(Mwin,errmess);                                    //  fail
      else Fmetamod++;
      return 1;
   }

   if (strmatch(event,"geoprev"))                                                //  [prev] stuff previous geotags
   {
      zdialog_stuff(zd,"location",p_meta_location);                              //  get last-used geotags
      zdialog_stuff(zd,"country",p_meta_country);
      zdialog_stuff(zd,"lati",p_meta_lati);
      zdialog_stuff(zd,"longi",p_meta_longi);
      Fmetamod++;
   }

   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
      zdialog_fetch(zd,"defcats",catgname,tagcc);
      deftags_stuff(zd,catgname);
   }

   if (strmatch(event,"newtag"))                                                 //  new tag is being typed in
   {
      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++)                                                //  matchtag 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,"add"))                                                    //  enter new tag finished
   {
      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)                                                           //  [prev] stuff previous file data
   {
      zd->zstat = 0;                                                             //  keep dialog active

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

      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
      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 != 2) {                                                         //  [cancel] or [x]
      zdialog_free(zd);                                                          //  kill dialog
      zd_editmeta = 0;
      zd_mapgeotags = 0;                                                         //  deactivate map clicks
      Fmetamod = 0;
      return 1;
   }

   zd->zstat = 0;                                                                //  [apply] - keep dialog active
   
   gtk_window_present(MWIN);                                                     //  return focus to main window

   if (! Fmetamod) return 1;                                                     //  no metadata changes

   zdialog_fetch(zd,"date",pdate2,12);                                           //  get photo date and time
   zdialog_fetch(zd,"time",ptime2,12);
   if (*pdate2) {                                                                //  date available
      metadate = pdate_metadate(pdate2);                                         //  validate
      if (! metadate) return 1;                                                  //  bad, re-input
      strcpy(meta_pdate,metadate);                                               //  convert to yyyymmdd
      if (*ptime2) {                                                             //  time available
         metatime = ptime_metatime(ptime2);                                      //  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",location,100);                                    //  get location from dialog
   zdialog_fetch(zd,"country",country,100);
   strTrim2(location);
   strTrim2(country);

   if (*location) {                                                              //  20.0
      *location = toupper(*location);                                            //  capitalize
      zdialog_stuff(zd,"location",location);
   }
   
   if (*country) {
      *country = toupper(*country);
      zdialog_stuff(zd,"country",country);
   }
   
   zdialog_fetch(zd,"lati",lati,20);                                             //  get latitude, longitude
   zdialog_fetch(zd,"longi",longi,20);
   strTrim2(lati);
   strTrim2(longi);
   
   if (*lati || *longi) {                                                        //  if coordinates present, validate
      err = validate_latlong(lati,longi,flati,flongi);
      if (err) {
         zmessageACK(Mwin,E2X("bad latitude/longitude: %s %s"),lati,longi);
         return 1;
      }
   }

   strncpy0(meta_location,location,100);                                         //  save geotags in image file EXIF
   strncpy0(meta_country,country,100);                                           //    and in image index file
   strncpy0(meta_lati,lati,20);
   strncpy0(meta_longi,longi,20);

   put_geolocs(zd);                                                              //  update geolocs table in memory
   save_filemeta(curr_file);                                                     //  save metadata changes to image file

   return 1;
}


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

//  manage tags function - auxiliary dialog

zdialog  *zdmanagetags = 0;

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

   GtkWidget   *widget;
   zdialog     *zd;
   
   F1_help_topic = "manage tags";

/***
          ______________________________________________________________
         |       Manage Tags                                            |
         |                                                              |
         | category [____________]  tag [___________] [Create] [Delete] |
         |                                                              |
         | Defined Tags: _____________________________________________  |
         | |                                                          | |
         | | category1: tag11, tag12, tag13 ...                       | |
         | | category2: tag21, tag22, tag23 ...                       | |
         | |  ...                                                     | |
         | |                                                          | |
         | |                                                          | |
         | |                                                          | |
         | |                                                          | |
         | |                                                          | |
         | |__________________________________________________________| |
         |                                                              |
         |                                         [orphan tags] [Done] |
         |______________________________________________________________|

***/

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

   zdialog_add_widget(zd,"hbox","hb7","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labcatg","hb7",E2X("category"),"space=5");
   zdialog_add_widget(zd,"zentry","catg","hb7",0,"size=12");
   zdialog_add_widget(zd,"label","space","hb7",0,"space=5");
   zdialog_add_widget(zd,"label","labtag","hb7",E2X("tag"),"space=5");
   zdialog_add_widget(zd,"zentry","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",E2X("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");                                        //  deftags widget mouse/KB event func
   textwidget_set_eventfunc(widget,manage_deftags_clickfunc);

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

   zdialog_resize(zd,0,400);
   zdialog_run(zd,managetags_dialog_event,0);                                    //  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, int kbkey)   //  tag or tag category was clicked
{
   char     *txword, end = 0;

   if (kbkey == GDK_KEY_F1) {                                                    //  key F1 pressed, show help
      showz_docfile(Mwin,"userguide",F1_help_topic);
      return;
   }

   txword = textwidget_word(widget,line,pos,",;:",end);
   if (! txword) return;
   
   if (end == ':') zdialog_stuff(zdmanagetags,"catg",txword);                    //  selected category >> dialog widget
   else zdialog_stuff(zdmanagetags,"tag",txword);                                //  selected tag >> dialog widget

   zfree(txword);
   return;
}


//  dialog event and completion callback function

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

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

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

      else {                                                                     //  done or [x]
         zdialog_free(zd);
         zdmanagetags = 0;
         return 1;
      }
   }

   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 (zd_editmeta)                                                           //  and edit metadata dialog if active
         deftags_stuff(zd_editmeta,"ALL");
      if (zd_batchtags)                                                          //  and batch tags dialog if active
         deftags_stuff(zd_batchtags,"ALL");
   }

   return 1;
}


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

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

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

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

   F1_help_topic = "edit any meta";

   if (FGWM != 'F' && FGWM != 'G') return;                                       //  19.0

   if (clicked_file) {                                                           //  use clicked file if present
      if (! curr_file || ! strmatch(clicked_file,curr_file))                     //  avoid f_open() re-entry
         f_open(clicked_file,0,0,1,0);
      clicked_file = 0;
   }

   if (! curr_file) {
      if (zd_editanymeta) zdialog_free(zd_editanymeta);
      zd_editanymeta = 0;
      return;
   }

   err = access(curr_file,W_OK);                                                 //  test file can be written by me     19.1
   if (err) {
      zmessageACK(Mwin,"%s: %s",Bnowriteperm,curr_file);
      return;
   }
   
   if (FGWM == 'G') gallery(0,"paint",-1);                                       //  if gallery view, repaint

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

***/

   if (! zd_editanymeta)                                                         //  popup dialog if not already
   {
      zd = zdialog_new(E2X("Edit Any Metadata"),Mwin,E2X("Full List"),Bsave,Bdone,null);
      zd_editanymeta = 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",E2X("click to select"),"size=25");
      zdialog_add_widget(zd,"frame","frb1","vb1",0,"expand");
      zdialog_add_widget(zd,"scrwin","scrb1","frb1",0,"expand");
      zdialog_add_widget(zd,"text","mtext","scrb1",0,"expand");
      zdialog_add_widget(zd,"vbox","vb2","hb1",0,"expand|space=3");
      zdialog_add_widget(zd,"hbox","hbf","vb2",0,"space=6");
      zdialog_add_widget(zd,"label","labf","hbf",E2X("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",E2X("key name"));
      zdialog_add_widget(zd,"zentry","keyname","hbkey",0,"expand");
      zdialog_add_widget(zd,"button","fetch","hbkey",Bfetch,"space=5");
      zdialog_add_widget(zd,"label","labdata","hbdata",E2X("key value"));
      zdialog_add_widget(zd,"zentry","keydata","hbdata",0,"expand");

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

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

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

      while (true) {                                                             //  populate dialog, left side
         pp = fgets_trim(buff,100,fid,1);
         if (! pp) break;
         if (*pp <= ' ') continue;
         if (strstr(buff,"White Balance")) continue;
         textwidget_append(mtext,0,"%s \n",buff);
      }
      fclose(fid);

      textwidget_set_eventfunc(mtext,meta_edit_any_clickfunc);                   //  set mouse/KB event function
      
      *keyname = 0;
   }

   zd = zd_editanymeta;

   pp = strrchr(curr_file,'/');                                                  //  stuff file name in dialog
   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
   }

   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 (! curr_file) return 1;

   if (strmatch(event,"fetch"))                                                  //  accept entered key name
   {
      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 (! zd->zstat) return 1;                                                    //  wait for completion

   if (zd->zstat == 1) {                                                         //  show full list
      zd->zstat = 0;                                                             //  keep dialog active
      zmessageACK(Mwin,"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);
      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 (zd_metaview) meta_view(0);                                             //  update exif view if active
   }

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

   return 1;
}


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

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

   if (kbkey == GDK_KEY_F1) {                                                    //  key F1 pressed, show help
      showz_docfile(Mwin,"userguide",F1_help_topic);
      return;
   }

   pp = textwidget_line(widget,line,1);                                          //  get clicked line, highlight
   if (! pp || ! *pp) return;
   textwidget_highlight_line(widget,line);

   zdialog_stuff(zd_editanymeta,"keyname",pp);

   zdialog_fetch(zd_editanymeta,"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_editanymeta,"keydata",keydata);                              //  stuff into dialog

   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;
   char        *pp;
   int         err;

   F1_help_topic = "delete meta";

   if (FGWM != 'F' && FGWM != 'G') return;                                       //  19.0

   if (clicked_file) {                                                           //  use clicked file if present
      if (! curr_file || ! strmatch(clicked_file,curr_file))                     //  avoid f_open() re-entry
         f_open(clicked_file,0,0,1,0);
      clicked_file = 0;
   }
   
   if (! curr_file) return;

   err = access(curr_file,W_OK);                                                 //  test file can be written by me     19.1
   if (err) {
      zmessageACK(Mwin,"%s: %s",Bnowriteperm,curr_file);
      return;
   }

   if (FGWM == 'G') gallery(0,"paint",-1);                                       //  if gallery view, repaint

/***
       _________________________________________
      |           Delete Metadata               |
      |                                         |
      | File: [______________________________]  |
      |                                         |
      | (o) ALL  (o) One Key: [______________]  |
      |                                         |
      |                       [apply] [cancel]  |
      |_________________________________________|
      
***/

   if (! zd_deletemeta)
   {
      zd = zdialog_new(E2X("Delete Metadata"),Mwin,Bapply,Bcancel,null);
      zd_deletemeta = zd;
      zdialog_add_widget(zd,"hbox","hbf","dialog");
      zdialog_add_widget(zd,"label","labf","hbf",E2X("File:"),"space=3");
      zdialog_add_widget(zd,"label","file","hbf",0,"space=5");
      zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=3");
      zdialog_add_widget(zd,"radio","kall","hb1",E2X("All"),"space=5");
      zdialog_add_widget(zd,"radio","key1","hb1",E2X("One Key:"));
      zdialog_add_widget(zd,"zentry","keyname","hb1",0,"size=20");
      zdialog_stuff(zd,"key1",1);
      zdialog_run(zd,meta_delete_dialog_event,"parent");
   }

   zd = zd_deletemeta;
   pp = (char *) "";
   if (curr_file) {
      pp = strrchr(curr_file,'/');
      if (pp) pp++;
      else pp = curr_file;
   }

   zdialog_stuff(zd,"file",pp);
   return;
}


//  dialog event and completion callback function

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

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

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

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

   if (! curr_file) return 1;

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

   if (! kall && ! key1) return 1;

   file = zescape_quotes(curr_file);

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

   zfree(file);

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

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


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

//  Show file name, caption, comment on top of current image in main window.
//  Menu call (menu arg not null): dialog to set user display options.
//  Non-menu call: write filename/caption/comment on image depending on options.

void m_meta_captions(GtkWidget *, cchar *menu)                                   //  rewrite                            20.0
{
   zdialog      *zd;
   int          nn;
   cchar        *keynames[2] = { iptc_caption_key, exif_comment_key };
   char         *pp, *keyvals[2];
   char         fname[200], caption[200], comment[200];
   static char  text[604];

   F1_help_topic = "captions";
   
   if (FGWM != 'F' && FGWM != 'G') return;

   if (menu)                                                                     //  user settings dialog
   {
      zd = zdialog_new(0,Mwin,"OK",0);
      zdialog_add_widget(zd,"hbox","hbchoose","dialog",0,"space=5");
      zdialog_add_widget(zd,"label","labchoose","hbchoose",E2X("choose options"));
      zdialog_add_widget(zd,"check","filename","dialog",E2X("file name"));
      zdialog_add_widget(zd,"check","caption","dialog",E2X("caption"));
      zdialog_add_widget(zd,"check","comment","dialog",E2X("comment"));
      zdialog_add_widget(zd,"check","ALL","dialog","ALL");
      
      if (Fcaptions & 1) zdialog_stuff(zd,"filename",1);
      if (Fcaptions & 2) zdialog_stuff(zd,"caption",1);
      if (Fcaptions & 4) zdialog_stuff(zd,"comment",1);
      if (Fcaptions & 8) zdialog_stuff(zd,"ALL",1);

      zdialog_set_decorated(zd,0);
      zdialog_resize(zd,200,0);
      zdialog_run(zd,0,"mouse");
      zdialog_wait(zd);
      
      Fcaptions = 0;
      zdialog_fetch(zd,"filename",nn);
      if (nn) Fcaptions += 1;
      zdialog_fetch(zd,"caption",nn);
      if (nn) Fcaptions += 2;
      zdialog_fetch(zd,"comment",nn);
      if (nn) Fcaptions += 4;
      zdialog_fetch(zd,"ALL",nn);
      if (nn) Fcaptions += 8;
      
      zdialog_free(zd);
   }

   if (! Fcaptions) return;
   if (! curr_file) return;
   
   *text = *fname = *caption = *comment = 0;                                     //  clear output text

   if (Fcaptions & (1 + 8))                                                      //  file name wanted
   {
      pp = strrchr(curr_file,'/');
      if (pp) pp++;
      else pp = curr_file;
      strncpy0(fname,pp,200);
   }   

   if (Fcaptions & (2 + 4 + 8))                                                  //  caption or comments wanted
   {
      exif_get(curr_file,keynames,keyvals,2);                                    //  get metadata from image file

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

      if (keyvals[1]) {
         strncpy0(comment,keyvals[1],200);
         zfree(keyvals[1]);
      }
      
      if (! (Fcaptions & (2 + 8))) *caption = 0;
      if (! (Fcaptions & (4 + 8))) *comment = 0;
   }

   if (*fname) strcpy(text,fname); 

   if (*caption) {
      if (*text) strcat(text,"\n");
      strcat(text,caption);
   }

   if (*comment) {
      if (*text) strcat(text,"\n");
      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");
   Fpaintnow();

   return;
}


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

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

namespace batchtags
{
   char        addtags[tagMcc];                                                  //  tags to add, list
   char        deltags[tagMcc];                                                  //  tags to remove, list
   int         radadd, raddel;                                                   //  dialog radio buttons
   char        countmess[80];
}


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

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

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

   F1_help_topic = "batch tags";

   if (FGWM != 'F' && FGWM != 'G') return;                                       //  19.0
   if (checkpend("all")) return;                                                 //  check nothing pending

   if (Findexvalid == 0) {
      zmessageACK(Mwin,Bnoindex);                                                //  no image index                     20.0
      return;
   }

   Fblock = 1;

/***
          ________________________________________________________
         |           Batch Add/Remove Tags                        |
         |                                                        |
         |  [Select Files]  NN files selected                     |
         |                                                        |
         |  (o) tags to add    [________________________________] |
         |  (o) tags to remove [________________________________] |
         |  - - - - - - - - - - - - - - - - - - - - - - - - - - - |
         |  Enter New Tag [___________] [Add]                     |
         |  Matching Tags [_____________________________________] |
         |  - - - - - - - - - - - - - - - - - - - - - - - - - - - |
         |  Defined Tags Category [___________________________|v] |
         |  |                                                   | |
         |  |                                                   | |
         |  |                                                   | |
         |  |                                                   | |
         |  |                                                   | |
         |  |                                                   | |
         |  |___________________________________________________| |
         |                                                        |
         |                                    [Proceed] [Cancel]  |
         |________________________________________________________|

***/

   zd = zdialog_new(E2X("Batch Add/Remove Tags"),Mwin,Bproceed,Bcancel,null);
   zd_batchtags = zd;

   //  [Select Files]  NN files selected
   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");

   //  (o) tags to add    [_______________________________________]
   //  (o) tags to remove [_______________________________________]
   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",E2X("tags to add"));
   zdialog_add_widget(zd,"radio","raddel","vb1",E2X("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");

   //  Enter New Tag [________________]  [Add]
   zdialog_add_widget(zd,"hbox","hbnt","dialog",0,"space=1");
   zdialog_add_widget(zd,"label","labnt","hbnt",E2X("Enter New Tag"),"space=3");
   zdialog_add_widget(zd,"zentry","newtag","hbnt");
   zdialog_add_widget(zd,"button","add","hbnt",Badd,"space=5");

   //  Matching Tags [____________________________________________]
   zdialog_add_widget(zd,"hbox","hbmt","dialog",0,"space=1");
   zdialog_add_widget(zd,"label","labmt","hbmt",E2X("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 [__________________________________|v]
   zdialog_add_widget(zd,"hbox","space","dialog");
   zdialog_add_widget(zd,"hbox","hbdt1","dialog");
   zdialog_add_widget(zd,"label","labdt","hbdt1",E2X("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);                                                 //  initz. radio buttons
   zdialog_stuff(zd,"raddel",0);

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

   *addtags = *deltags = 0;

   snprintf(countmess,80,Bfileselected,GScount);                                 //  show selected files count          20.0
   zdialog_stuff(zd,"labcount",countmess);

   widget = zdialog_widget(zd,"addtags");                                        //  tag widget mouse/KB event funcs
   textwidget_set_eventfunc(widget,batch_addtags_clickfunc);

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

   widget = zdialog_widget(zd,"matchtags");
   textwidget_set_eventfunc(widget,batch_matchtags_clickfunc);

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

   zdialog_resize(zd,500,500);                                                   //  run dialog

   zdialog_run(zd,batch_tags_dialog_event,0);         
   zstat = zdialog_wait(zd);                                                     //  wait for dialog completion
   zdialog_free(zd);

   zd_batchtags = 0;

   if (zstat != 1) {                                                             //  cancel                             20.0
      Fblock = 0;
      return;
   }

   zd2 = popup_report_open("Batch Tags",Mwin,500,200,0,0,0);                     //  status report popup window
   
   for (ii = 0; ii < GScount; ii++)                                              //  loop all selected files
   {
      zmainsleep(0.1);                                                           //  keep GTK alive

      file = GSfiles[ii];                                                        //  display image
      err = f_open(file,0,0,0);
      if (err) continue;
      
      popup_report_write(zd2,0,"%s \n",file);                                    //  report progress
      popup_report_bottom(zd2);

      err = access(file,W_OK);                                                   //  test file can be written by me     19.1
      if (err) {
         popup_report_write(zd2,0,"%s \n",Bnowriteperm);
         popup_report_bottom(zd2);
         continue;
      }

      for (jj = 1; ; jj++)                                                       //  remove tags if present
      {
         ptag = (char *) strField(deltags,",;",jj);
         if (! ptag) break;
         if (*ptag == 0) 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 == 0) continue;
         err = add_tag(ptag,meta_tags,tagFcc);
         if (err == 2) {
            zmessageACK(Mwin,E2X("%s \n too many tags"),file);
            break;
         }
      }

      save_filemeta(file);                                                       //  save tag changes
   }

   popup_report_write(zd2,0,"\n *** %s \n",Bcompleted);
   popup_report_bottom(zd2);
   
   load_deftags(1);                                                              //  update defined tags list           20.0

   Fblock = 0;
   
   yn = zmessageYN(Mwin,E2X("repeat with same files?"));                         //  allow repeat with same files
   if (yn) {
      popup_report_close(zd2,0);
      m_batch_tags(0,0);
   }

   return;
}


//  mouse click functions for widgets holding tags

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

   char     *txtag, end;

   if (kbkey == GDK_KEY_F1) {                                                    //  key F1 pressed, show help
      showz_docfile(Mwin,"userguide",F1_help_topic);
      return;
   }

   txtag = textwidget_word(widget,line,pos,",;",end);
   if (! txtag) return;

   del_tag(txtag,addtags);                                                       //  remove tag from list
   zdialog_stuff(zd_batchtags,"addtags",addtags);
   
   zfree(txtag);
   return;
}


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

   char     *txtag, end;

   if (kbkey == GDK_KEY_F1) {                                                    //  key F1 pressed, show help
      showz_docfile(Mwin,"userguide",F1_help_topic);
      return;
   }

   txtag = textwidget_word(widget,line,pos,",;",end);
   if (! txtag) return;

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

   zfree(txtag);
   return;
}


void batch_matchtags_clickfunc(GtkWidget *widget, int line, int pos, int kbkey)  //  matching tag was clicked
{
   using namespace batchtags;

   char     *txtag, end = 0;

   if (kbkey == GDK_KEY_F1) {                                                    //  key F1 pressed, show help
      showz_docfile(Mwin,"userguide",F1_help_topic);
      return;
   }

   txtag = textwidget_word(widget,line,pos,",;",end);
   if (! txtag) return;

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

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

   zdialog_stuff(zd_batchtags,"newtag","");                                      //  clear newtag and matchtags
   zdialog_stuff(zd_batchtags,"matchtags","");

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

   zfree(txtag);
   return;
}


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

   char     *txtag, end;
   int      radadd;

   if (kbkey == GDK_KEY_F1) {                                                    //  key F1 pressed, show help
      showz_docfile(Mwin,"userguide",F1_help_topic);
      return;
   }

   txtag = textwidget_word(widget,line,pos,",;:",end);
   if (! txtag || end == ':') return;                                            //  tag category clicked, ignore

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

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

   zfree(txtag);
   return;
}


//  batchTags dialog event function

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

   char     catgname[tagcc];
   int      ii, jj, nt, cc1, cc2, ff;
   char     *pp1, *pp2;
   char     newtag[tagcc], matchtags[20][tagcc];
   char     matchtagstext[(tagcc+2)*20];

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

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

      return 1;                                                                  //  cancel
   }

   if (strmatch(event,"files"))                                                  //  select images to process
   {
      zdialog_show(zd,0);                                                        //  hide parent dialog
      gallery_select();                                                          //  get new list
      zdialog_show(zd,1);
      snprintf(countmess,80,Bfileselected,GScount);
      zdialog_stuff(zd,"labcount",countmess);
   }
   
   if (zstrstr("radadd raddel",event)) {                                         //  get state of radio buttons
      zdialog_fetch(zd,"radadd",radadd);
      zdialog_fetch(zd,"raddel",raddel);
   }

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

   if (strmatch(event,"newtag"))                                                 //  new tag is being typed in
   {
      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,"add"))                                                    //  enter new tag finished
   {
      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;
      }

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

      add_tag(newtag,addtags,tagMcc);                                            //  add to tag add list
      zdialog_stuff(zd_batchtags,"addtags",addtags);

      zdialog_stuff(zd,"newtag","");                                             //  update dialog widgets
      zdialog_stuff(zd,"matchtags","");

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

   return 1;
}


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

//  menu function - rename multiple tags for selected image files

namespace batchrenametags
{
   int      Ntags;                                                               //  count, 1-100
   char     *oldtags[100];                                                       //  tags to rename
   char     *newtags[100];                                                       //  corresponding new name
   #define  tpcc (tagcc+tagcc+10)
   zdialog  *zd;
}


//  menu function

void m_batch_rename_tags(GtkWidget *, cchar *menu)
{
   using namespace batchrenametags;

   void  batchrenametags_deftags_clickfunc(GtkWidget *widget, int line, int pos, int kbkey);
   void  batchrenametags_taglist_clickfunc(GtkWidget *widget, int line, int pos, int kbkey);
   int   batchrenametags_dialog_event(zdialog *zd, cchar *event);

   char        *file;
   int         ii, jj, kk, ff, err, yn;
   int         zstat, Nfiles, Nlist;
   GtkWidget   *widget;
   char        **filelist;
   char        *pp, *filetag;
   char        *oldtaglist[100], *newtaglist[100];
   xxrec_t     *xxrec;
   zdialog     *zd2;

   F1_help_topic = "batch rename tags";

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

   if (Findexvalid == 0) {
      zmessageACK(Mwin,Bnoindex);                                                //  no image index                     20.0
      return;
   }

   Fblock = 1;
   Ntags = Nfiles = 0;
   filelist = 0;

/***
       ____________________________________________________________________________
      |        Batch Rename Tags                   |                               |
      |                                            | old tag name >> new tag name  |
      | Tag [_______]  Rename to [_________]  [->] | aaaaaaaa >> bbbbbbbbbbb       |
      |                                            | ccccccccccc >> ddddddddd      |
      | Defined Tags Category [________________|v| |                               |
      | |                                        | |                               |
      | |                                        | |                               |
      | |                                        | |                               |
      | |                                        | |                               |
      | |                                        | |                               |
      | |                                        | |                               |
      | |________________________________________| |_______________________________|
      |                                                                            |
      |                                                        [Proceed] [Cancel]  |
      |____________________________________________________________________________|

***/

   zd = zdialog_new(E2X("Batch Rename Tags"),Mwin,Bproceed,Bcancel,null);
   
   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"expand");
   zdialog_add_widget(zd,"vbox","vb1","hb1",0,"expand");
   zdialog_add_widget(zd,"vbox","vb2","hb1",0,"space=8|expand");

   //  tag [_________________]  rename to [___________________]  [-->]
   zdialog_add_widget(zd,"hbox","hbtags","vb1",0,"space=3");
   zdialog_add_widget(zd,"label","lab1","hbtags","Tag","space=3");
   zdialog_add_widget(zd,"frame","frot","hbtags");
   zdialog_add_widget(zd,"label","oldtag","frot",E2X("(click defined tag)"));
   zdialog_add_widget(zd,"label","space","hbtags",0,"space=5");
   zdialog_add_widget(zd,"label","lab2","hbtags",E2X("Rename to"),"space=3");
   zdialog_add_widget(zd,"zentry","newtag","hbtags",0,"expand");
   zdialog_add_widget(zd,"label","space","hbtags",0,"space=3");
   zdialog_add_widget(zd,"button","addtags","hbtags",">>");
   
   zdialog_add_widget(zd,"hsep","hsep1","vb1",0,"space=5");
   
   //  Defined Tags Category [_____________________|v]
   zdialog_add_widget(zd,"hbox","hbdt","vb1",0);
   zdialog_add_widget(zd,"label","labdt","hbdt",E2X("Defined Tags Category"),"space=3");
   zdialog_add_widget(zd,"combo","defcats","hbdt",0,"space=5");
   zdialog_add_widget(zd,"frame","frdt","vb1",0,"expand|space=3");
   zdialog_add_widget(zd,"scrwin","swdt","frdt",0,"expand");
   zdialog_add_widget(zd,"text","deftags","swdt",0,"wrap");
   
   //  old tag name >> new tag name
   zdialog_add_widget(zd,"hbox","hblist","vb2");
   zdialog_add_widget(zd,"label","lablist","hblist",E2X("old tag name >> new tag name"),"space=10");
   zdialog_add_widget(zd,"frame","frlist","vb2",0,"expand|space=3");
   zdialog_add_widget(zd,"scrwin","swlist","frlist");
   zdialog_add_widget(zd,"text","taglist","swlist");

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

   widget = zdialog_widget(zd,"deftags");                                        //  connect mouse to defined tags widget
   textwidget_set_eventfunc(widget,batchrenametags_deftags_clickfunc);

   widget = zdialog_widget(zd,"taglist");                                        //  connect mouse to taglist widget
   textwidget_set_eventfunc(widget,batchrenametags_taglist_clickfunc);

   zdialog_resize(zd,700,400);                                                   //  run dialog

   zdialog_run(zd,batchrenametags_dialog_event,0);
   zstat = zdialog_wait(zd);                                                     //  wait for dialog completion
   zdialog_free(zd);
   zd = 0;
   if (zstat != 1) goto cleanup;                                                 //  [cancel]

   filelist = (char **) zmalloc(Nxxrec * sizeof(char *));                        //  find all affected image files
   Nfiles = 0;
   
   zd2 = popup_report_open("rename tags",Mwin,500,300,0,0,0);

   for (ii = 0; ii < Nxxrec; ii++)                                               //  loop all index recs 
   {
      zmainloop(100);                                                            //  keep GTK alive

      xxrec = xxrec_tab[ii];   
      if (! xxrec->tags) continue;                                               //  search for tags to rename

      ff = 0;

      for (jj = 1; ; jj++) {
         pp = (char *) strField(xxrec->tags,',',jj);
         if (! pp) break;
         for (kk = 0; kk < Ntags; kk++) {
            if (strmatchcase(pp,oldtags[kk])) {                                  //  this file has one or more tags
               ff = 1;                                                           //    that will be renamed
               break;
            }
         }
         if (ff) break;
      }
      
      if (ff) {
         filelist[Nfiles] = zstrdup(xxrec->file);                                //  add to list of files to process
         Nfiles++;
         popup_report_write(zd2,0,"file included: %s \n",xxrec->file);
      }
   }
   
   yn = zmessageYN(Mwin,E2X("%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 

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

      popup_report_write(zd2,0,"%s \n",file);                                    //  report progress
      popup_report_bottom(zd2);

      err = access(file,W_OK);                                                   //  test file can be written by me     19.1
      if (err) {
         popup_report_write(zd2,0,"%s \n",Bnowriteperm);
         continue;
      }

      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
            if (strmatchcase(filetag,oldtags[kk])) {                             //  file tag matches tag to replace
               oldtaglist[Nlist] = oldtags[kk];                                  //  save old and new tags 
               newtaglist[Nlist] = newtags[kk];
               Nlist++;
               break;                                                            //  next file tag
            }
         }
      }

      for (jj = 0; jj < Nlist; jj++)                                             //  remove old tags
         err = del_tag(oldtaglist[jj],meta_tags);

      for (jj = 0; jj < Nlist; jj++) {                                           //  add new tags
         if (! newtaglist[jj]) continue;                                         //  must be after removals
         popup_report_write(zd2,0,"%s \n",newtaglist[jj]);
         err = add_tag(newtaglist[jj],meta_tags,tagFcc);
         if (err && err != 1) popup_report_write(zd2,1,"ERROR \n");              //  ignore already there, else report
      }

      save_filemeta(file);                                                       //  save tag changes
   }

   popup_report_write(zd2,0,"\n *** %s \n",Bcompleted);
   popup_report_bottom(zd2);
   
   load_deftags(1);                                                              //  update tag list                    20.0
   
cleanup:                                                                         //  free resources

   Fblock = 0;
   
   for (ii = 0; ii < Ntags; ii++) {
      zfree(oldtags[ii]);
      zfree(newtags[ii]);
   }
   
   for (ii = 0; ii < Nfiles; ii++)
      zfree(filelist[ii]);
   if (filelist) zfree(filelist);
      
   return;
}


//  a defined tag was clicked

void batchrenametags_deftags_clickfunc(GtkWidget *widget, int line, int pos, int kbkey)
{
   using namespace batchrenametags;

   char     *txtag, end;
   char     tagname[tagcc];

   if (kbkey == GDK_KEY_F1) {                                                    //  key F1 pressed, show help
      showz_docfile(Mwin,"userguide",F1_help_topic);
      return;
   }
   
   txtag = textwidget_word(widget,line,pos,",;:",end);                           //  clicked word
   if (! txtag || end == ':') return;                                            //  tag category clicked, ignore

   snprintf(tagname,tagcc," %s ",txtag);                                         //  add spaces for appearance
   zdialog_stuff(zd,"oldtag",tagname);
   zdialog_stuff(zd,"newtag","");

   zfree(txtag);
   return;
}


//  a tag list line was clicked

void batchrenametags_taglist_clickfunc(GtkWidget *widget, int line, int pos, int kbkey)
{
   using namespace batchrenametags;
   
   int      ii;

   if (kbkey == GDK_KEY_F1) {                                                    //  key F1 pressed, show help
      showz_docfile(Mwin,"userguide",F1_help_topic);
      return;
   }
   
   if (line >= Ntags) return;
   
   for (ii = line; ii < Ntags-1; ii++) {                                         //  remove tags pair corresponding
      oldtags[ii] = oldtags[ii+1];                                               //    to the line clicked
      newtags[ii] = newtags[ii+1];
   }
   Ntags--;
   
   widget = zdialog_widget(zd,"taglist");                                        //  rewrite dialog tag list
   textwidget_clear(widget);
   for (int ii = 0; ii < Ntags; ii++)
      textwidget_append2(widget,0,"%s >> %s\n",oldtags[ii],newtags[ii]);
   
   return;
}


//  batch rename tags dialog event function

int batchrenametags_dialog_event(zdialog *zd, cchar *event)
{
   using namespace batchrenametags;

   char        catgname[tagcc];
   char        oldtag[tagcc], newtag[tagcc];
   GtkWidget   *widget;

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

   if (zd->zstat) return 1;                                                      //  dialog completed

   if (strmatch(event,"defcats")) {                                              //  new tag category selection
      zdialog_fetch(zd,"defcats",catgname,tagcc);
      deftags_stuff(zd,catgname);
   }
   
   if (strmatch(event,"addtags")) {                                              //  [ --> ] button pressed
      zdialog_fetch(zd,"oldtag",oldtag,tagcc);                                   //  save new pair of tag names
      zdialog_fetch(zd,"newtag",newtag,tagcc);
      strTrim2(oldtag);
      strTrim2(newtag);
      if (*oldtag <= ' ' || *newtag <= ' ') return 1;
      if (Ntags == 100) {
         zmessageACK(Mwin,E2X("max tags exceeded"));
         return 1;
      }
      oldtags[Ntags] = zstrdup(oldtag);
      newtags[Ntags] = zstrdup(newtag);
      Ntags++;
   }
   
   widget = zdialog_widget(zd,"taglist");                                        //  rewrite dialog tag list
   textwidget_clear(widget);
   for (int ii = 0; ii < Ntags; ii++) 
      textwidget_append2(widget,0,"%s >> %s\n",oldtags[ii],newtags[ii]);

   return 1;
}


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

//  batch change or shift photo date/time

void m_batch_photo_date_time(GtkWidget *, cchar *menu)
{
   int  batch_photo_time_dialog_event(zdialog *zd, cchar *event);

   cchar       *keyname[1] = { "DateTimeOriginal" };
   char        *keyvalue[1];
   char        text[100];
   char        *file, olddatetime[24], newdatetime[24];                          //  exif format "yyyy:mm:dd hh:mm:ss"
   int         ii, nn, cc, err, zstat;
   int         Fyearonly, Fdateonly;
   int         Fsetnew, Fshift, Ftest;                                           //  check boxes
   time_t      timep;
   struct tm   DTold, DTnew;                                                     //  old and new date/time
   int         s_years, s_mons, s_mdays, s_hours, s_mins, s_secs;                //  shift amounts
   zdialog     *zd, *zd2;

   F1_help_topic = "batch photo date";

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

   if (Findexvalid == 0) {
      zmessageACK(Mwin,Bnoindex);                                                //  no image index                     20.0
      return;
   }

   Fblock = 1;

/***
       __________________________________________________
      |            Batch Photo Date/Time                 |
      |                                                  |
      |  [Select Files]  NN files selected               |
      |                                                  |
      | [x] set a new date/time: [_____________________] |
      |                           (yyyy-mm-dd hh:mm:ss)  |
      |                                                  |
      | [x] shift existing date/time:                    |
      |      years [__]  months [__]  days [__]          |
      |      hours [__]  minutes [__]  seconds [__]      |
      |                                                  |
      | [x] test: show changes, do not update files      |
      |                                                  |
      |                               [proceed] [cancel] |
      |__________________________________________________|

***/


   zd = zdialog_new(E2X("Batch Photo Date/Time"),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,"hsep","sep1","dialog",0,"space=5");
   zdialog_add_widget(zd,"hbox","hbsetnew","dialog",0,"space=3");
   zdialog_add_widget(zd,"check","Fsetnew","hbsetnew",E2X("set a new date/time:"),"space=3");
   zdialog_add_widget(zd,"zentry","newdatetime","hbsetnew",0,"expand|size=15");
   zdialog_add_widget(zd,"hbox","hbsetnew2","dialog");
   zdialog_add_widget(zd,"label","labspace","hbsetnew2","","expand");
   zdialog_add_widget(zd,"label","labtemplate","hbsetnew2","yyyy-mm-dd hh:mm[:ss]","space=5");
   
   zdialog_add_widget(zd,"hsep","sep1","dialog",0,"space=5");
   zdialog_add_widget(zd,"hbox","hbshift1","dialog",0,"space=3");
   zdialog_add_widget(zd,"check","Fshift","hbshift1",E2X("shift existing date/time:"),"space=3");

   zdialog_add_widget(zd,"hbox","hbshift2","dialog");
   zdialog_add_widget(zd,"label","space","hbshift2",0,"space=10");
   zdialog_add_widget(zd,"label","labyears","hbshift2","years","space=5");
   zdialog_add_widget(zd,"zspin","s_years","hbshift2","-99|+99|1|0");
   zdialog_add_widget(zd,"label","space","hbshift2",0,"space=5");
   zdialog_add_widget(zd,"label","labmons","hbshift2","months","space=5");
   zdialog_add_widget(zd,"zspin","s_mons","hbshift2","-11|+11|1|0");
   zdialog_add_widget(zd,"label","space","hbshift2",0,"space=5");
   zdialog_add_widget(zd,"label","labmdays","hbshift2","days","space=5");
   zdialog_add_widget(zd,"zspin","s_mdays","hbshift2","-30|+30|1|0");

   zdialog_add_widget(zd,"hbox","hbshift3","dialog");
   zdialog_add_widget(zd,"label","space","hbshift3",0,"space=10");
   zdialog_add_widget(zd,"label","labhours","hbshift3","hours","space=5");
   zdialog_add_widget(zd,"zspin","s_hours","hbshift3","-23|+23|1|0");
   zdialog_add_widget(zd,"label","space","hbshift3",0,"space=5");
   zdialog_add_widget(zd,"label","labmins","hbshift3","minutes","space=5");
   zdialog_add_widget(zd,"zspin","s_mins","hbshift3","-59|+59|1|0");
   zdialog_add_widget(zd,"label","space","hbshift3",0,"space=5");
   zdialog_add_widget(zd,"label","labsecs","hbshift3","seconds","space=5");
   zdialog_add_widget(zd,"zspin","s_secs","hbshift3","-59|+59|1|0");
   
   zdialog_add_widget(zd,"hsep","sep1","dialog",0,"space=5");
   zdialog_add_widget(zd,"hbox","hbtest","dialog",0,"space=5");
   zdialog_add_widget(zd,"check","Ftest","hbtest",E2X("test: show changes, do not update files"),"space=3");

   zdialog_restore_inputs(zd);

   snprintf(text,100,Bfileselected,GScount);                                     //  show selected files count          20.0
   zdialog_stuff(zd,"labcount",text);

   zstat = zdialog_run(zd,batch_photo_time_dialog_event,"parent");

retry:

   zstat = zdialog_wait(zd);                                                     //  wait for dialog, get status
   if (zstat != 1) {                                                             //  not [proceed]
      zdialog_free(zd);                                                          //  cancel
      Fblock = 0;
      return;
   }

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

   zdialog_fetch(zd,"Fsetnew",Fsetnew);                                          //  checkboxes
   zdialog_fetch(zd,"Fshift",Fshift);
   zdialog_fetch(zd,"Ftest",Ftest);
   
   if (Fsetnew + Fshift != 1) {
      zmessageACK(Mwin,E2X("please make a choice"));
      goto retry;
   }
   
   if (GScount == 0) {
      zmessageACK(Mwin,E2X("no files selected"));
      goto retry;
   }

   Fyearonly = Fdateonly = 0;
   
   if (Fsetnew)                                                                  //  input is new date/time
   {
      zdialog_fetch(zd,"newdatetime",newdatetime,24);
      strTrim2(newdatetime);                                                     //  strip leading and trailing blanks
      cc = strlen(newdatetime);
      
      if (cc == 4) {                                                             //  have only "yyyy"
         strcat(newdatetime,"-01-01 00:00:00");                                  //  append "-01-01 00:00:00"
         Fyearonly = 1;
         cc = 19;
      }

      if (cc == 10) {                                                            //  have only "yyyy-mm-dd"
         strcat(newdatetime," 00:00:00");                                        //  append " 00:00:00"
         Fdateonly = 1;                                                          //  flag, change date only
         cc = 19;
      }
      
      if (cc == 16) {                                                            //  have only "yyyy-mm-dd hh:mm"
         strcat(newdatetime,":00");                                              //  append ":00" 
         cc = 19;
      }
      
      if (cc != 19) {                                                            //  must have yyyy-mm-dd hh:mm:ss
         zmessageACK(Mwin,E2X("invalid date/time format"));
         goto retry;
      }
      
      nn = sscanf(newdatetime,"%d-%d-%d %d:%d:%d",                               //  yyyy-mm-dd hh:mm:ss >> DTnew
                     &DTnew.tm_year, &DTnew.tm_mon, &DTnew.tm_mday, 
                     &DTnew.tm_hour, &DTnew.tm_min, &DTnew.tm_sec);
      DTnew.tm_mon -= 1;                                                         //  mktime month is 0-11

      if (nn != 6) {                                                             //  check input format
         zmessageACK(Mwin,E2X("invalid date/time format"));
         goto retry;
      }
      
      timep = mktime(&DTnew);                                                    //  DTnew >> timep
      if (timep < 0) {                                                           //  validate DTnew by validating timep
         zmessageACK(Mwin,E2X("invalid date/time format"));
         goto retry;
      }
   }                                                                             //  DTnew is final value to use
   
   if (Fshift)
   {
      zdialog_fetch(zd,"s_years",s_years);                                       //  inputs are shifted date/time values
      zdialog_fetch(zd,"s_mons",s_mons);
      zdialog_fetch(zd,"s_mdays",s_mdays);
      zdialog_fetch(zd,"s_hours",s_hours);
      zdialog_fetch(zd,"s_mins",s_mins);
      zdialog_fetch(zd,"s_secs",s_secs);
   }
   
   zdialog_free(zd);

   zd2 = popup_report_open(E2X("Batch Photo Date/Time"),Mwin,500,200,0,0,0);     //  start popup report
   
   if (Fshift) {
      popup_report_write(zd2,0,"changes: year mon day  hours mins secs \n");
      popup_report_write(zd2,0,"         %4d %3d %3d  %5d %4d %4d \n",
                          s_years,s_mons,s_mdays,s_hours,s_mins,s_secs);
   }

   for (ii = 0; ii < GScount; ii++)                                              //  loop all selected files
   {
      file = GSfiles[ii];
      err = f_open(file,0,0,0);                                                  //  open image file
      if (err) continue;

      popup_report_write(zd2,0,"\n");                                            //  report progress
      popup_report_write(zd2,0,"%s \n",file);

      err = access(file,W_OK);                                                   //  test file can be written by me     19.1
      if (err) {
         popup_report_write(zd2,0,"%s \n",Bnowriteperm);
         continue;
      }

      exif_get(curr_file,keyname,(char **) keyvalue,1);                          //  metadata >> yyyy:mm:dd hh:mm:ss
      if (! keyvalue[0] && Fshift) {                                             //  ignore if Fsetnew 
         popup_report_write(zd2,0,"  *** no date/time available \n");
         continue;
      }
      
      if (keyvalue[0]) {
         strncpy0(olddatetime,keyvalue[0],20);                                   //  yyyy:mm:dd hh:mm:ss
         zfree(keyvalue[0]);
      }
      else strcpy(olddatetime,"0000:01:01 00:00:00");                            //  missing old date/time

      nn = sscanf(olddatetime,"%d:%d:%d %d:%d:%d",                               //  yyyy-mm-dd hh:mm:ss >> DTnew
                     &DTold.tm_year, &DTold.tm_mon, &DTold.tm_mday, 
                     &DTold.tm_hour, &DTold.tm_min, &DTold.tm_sec);
      DTold.tm_mon -= 1;                                                         //  mktime month is 0-11

      if (nn != 6 && Fshift) {
         popup_report_write(zd2,0,"  *** EXIF date/time invalid \n");
         continue;
      }
      
      if (nn != 6) strcpy(olddatetime,"0000:01:01 00:00:00");                    //  missing old date/time

      if (Fsetnew)                                                               //  set new date/time
      {
         if (Fyearonly)                                                          //  change year only, leave rest
         {
            DTnew.tm_mon = DTold.tm_mon;                                         //  >> revised DTnew
            DTnew.tm_mday = DTold.tm_mday;                                       //  set month/day/hour/min/sec only
            DTnew.tm_hour = DTold.tm_hour;                                       //  year remains fixed
            DTnew.tm_min = DTold.tm_min;
            DTnew.tm_sec = DTold.tm_sec;
         }

         if (Fdateonly)                                                          //  change year/mon/day only, leave time
         {
            DTnew.tm_hour = DTold.tm_hour;                                       //  >> revised DTnew
            DTnew.tm_min = DTold.tm_min;                                         //  set hour/min/sec only
            DTnew.tm_sec = DTold.tm_sec;                                         //  year/mon/day remains fixed
         }
      }
      
      if (Fshift)                                                                //  shift existing date/time values
      {
         DTnew.tm_year = DTold.tm_year + s_years;
         DTnew.tm_mon = DTold.tm_mon + s_mons;
         DTnew.tm_mday = DTold.tm_mday + s_mdays;
         DTnew.tm_hour = DTold.tm_hour + s_hours;
         DTnew.tm_min = DTold.tm_min + s_mins;
         DTnew.tm_sec = DTold.tm_sec + s_secs;
      }

      timep = mktime(&DTnew);
      if (timep < 0) {
         popup_report_write(zd2,0," %s  *** date/time conversion failed \n",olddatetime);
         continue;
      }

      DTnew = *localtime(&timep);

      snprintf(newdatetime,20,"%04d:%02d:%02d %02d:%02d:%02d",                   //  DTnew >> yyyy:mm:dd hh:mm:ss
                            DTnew.tm_year, DTnew.tm_mon+1, DTnew.tm_mday,        //  (tm_mon 0-11 >> 1-12)
                            DTnew.tm_hour, DTnew.tm_min, DTnew.tm_sec);
      
      olddatetime[4] = olddatetime[7] = newdatetime[4] = newdatetime[7] = '-';   //  format: yyyy-mm-dd
      popup_report_write(zd2,0," %s  %s \n",olddatetime,newdatetime);
      
      if (Ftest) continue;                                                       //  test only, no file updates

      newdatetime[4] = newdatetime[7] = ':';                                     //  format: yyyy:mm:dd for EXIF
      keyvalue[0] = (char *) &newdatetime;
      err = exif_put(curr_file,keyname,(cchar **) keyvalue,1);                   //  yyyy:mm:dd hh:mm:ss >> metadata

      load_filemeta(curr_file);                                                  //  get all indexed data for file

      snprintf(meta_pdate,16,"%04d%02d%02d%02d%02d%02d",                         //  update photo date, yyyymmddhhmmss
                            DTnew.tm_year, DTnew.tm_mon+1, DTnew.tm_mday,        //  (tm_mon 0-11 >> 1-12)
                            DTnew.tm_hour, DTnew.tm_min, DTnew.tm_sec);
      update_image_index(curr_file);                                             //  update image index rec.

      zmainloop();                                                               //  keep GTK alive
   }

   popup_report_write(zd2,0,"\n *** %s \n",Bcompleted);
   popup_report_bottom(zd2);

   gallery(0,"paint",-1);
   
   Fblock = 0;

   m_batch_photo_date_time(0,0);                                                 //  repeat
   return;
}


//  dialog event and completion callback function

int batch_photo_time_dialog_event(zdialog *zd, cchar *event)
{
   char     countmess[80];

   if (strmatch(event,"files"))                                                  //  select images to process
   {
      zdialog_show(zd,0);                                                        //  hide parent dialog
      gallery_select();                                                          //  get new file list
      zdialog_show(zd,1);

      snprintf(countmess,80,Bfileselected,GScount);
      zdialog_stuff(zd,"labcount",countmess);
   }
   
   if (zstrstr("Fsetnew Fshift",event)) {                                        //  bugfix                             19.0
      zdialog_stuff(zd,"Fsetnew",0);
      zdialog_stuff(zd,"Fshift",0);
      zdialog_stuff(zd,event,1);
   }

   return 1;
}


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

//  batch add or change any EXIF/IPTC metadata

namespace batchchangemeta
{
   zdialog     *zd;
}


//  menu function

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

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

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

   F1_help_topic = "batch change meta";

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

   if (Findexvalid == 0) {
      zmessageACK(Mwin,Bnoindex);                                                //  no image index                     20.0
      return;
   }

   Fblock = 1;

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

**/

   zd = zdialog_new(E2X("Batch Add/Change Metadata"),Mwin,E2X("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",E2X("click to select"),"size=30|space=3");
   zdialog_add_widget(zd,"frame","fr1","vb1",0,"expand");
   zdialog_add_widget(zd,"scrwin","scr1","fr1");
   zdialog_add_widget(zd,"text","mtext","scr1");

   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",E2X("key name"));
   zdialog_add_widget(zd,"label","labdata","vbvalue",E2X("key value"));
   zdialog_add_widget(zd,"zentry","keyname0","vbname",0,"size=20");
   zdialog_add_widget(zd,"zentry","keyname1","vbname",0,"size=20");
   zdialog_add_widget(zd,"zentry","keyname2","vbname",0,"size=20");
   zdialog_add_widget(zd,"zentry","keyname3","vbname",0,"size=20");
   zdialog_add_widget(zd,"zentry","keyname4","vbname",0,"size=20");
   zdialog_add_widget(zd,"zentry","keyname5","vbname",0,"size=20");
   zdialog_add_widget(zd,"zentry","keyname6","vbname",0,"size=20");
   zdialog_add_widget(zd,"zentry","keyname7","vbname",0,"size=20");
   zdialog_add_widget(zd,"zentry","keyname8","vbname",0,"size=20");
   zdialog_add_widget(zd,"zentry","keyname9","vbname",0,"size=20");
   zdialog_add_widget(zd,"zentry","keyvalue0","vbvalue",0,"size=20|expand");
   zdialog_add_widget(zd,"zentry","keyvalue1","vbvalue",0,"size=20|expand");
   zdialog_add_widget(zd,"zentry","keyvalue2","vbvalue",0,"size=20|expand");
   zdialog_add_widget(zd,"zentry","keyvalue3","vbvalue",0,"size=20|expand");
   zdialog_add_widget(zd,"zentry","keyvalue4","vbvalue",0,"size=20|expand");
   zdialog_add_widget(zd,"zentry","keyvalue5","vbvalue",0,"size=20|expand");
   zdialog_add_widget(zd,"zentry","keyvalue6","vbvalue",0,"size=20|expand");
   zdialog_add_widget(zd,"zentry","keyvalue7","vbvalue",0,"size=20|expand");
   zdialog_add_widget(zd,"zentry","keyvalue8","vbvalue",0,"size=20|expand");
   zdialog_add_widget(zd,"zentry","keyvalue9","vbvalue",0,"size=20|expand");

   snprintf(text,100,Bfileselected,GScount);                                     //  show selected files count          20.0
   zdialog_stuff(zd,"labcount",text);
   
   mtext = zdialog_widget(zd,"mtext");                                           //  make clickable metadata list
   textwidget_clear(mtext);

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

   while (true) {                                                                //  populate metadata list
      pp = fgets_trim(buff,100,fid,1);
      if (! pp) break;
      textwidget_append(mtext,0,"%s \n",buff);
   }
   fclose(fid);

   textwidget_set_eventfunc(mtext,batch_change_metadata_clickfunc);              //  set mouse/KB event function

   nkeys = 0;                                                                    //  nothing selected

   zstat = zdialog_run(zd,batch_change_metadata_dialog_event,0);                 //  run dialog

retry:
   zstat = zdialog_wait(zd);                                                     //  wait for completion
   if (zstat != 2) goto cleanup;                                                 //  not [apply]
   
   for (ii = jj = 0; ii < 10; 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,E2X("enter key names"));
      zd->zstat = 0;
      goto retry;
   }
      
   if (GScount == 0) {
      zmessageACK(Mwin,E2X("no files selected"));
      zd->zstat = 0;
      goto retry;
   }
   
   zd2 = popup_report_open("Batch Metadata",Mwin,500,200,0,0,0);                 //  popup report
   
   for (ii = 0; ii < nkeys; ii++)
   {
      if (*pp2[ii]) popup_report_write(zd2,0,"%s = %s \n",pp1[ii],pp2[ii]);
      else popup_report_write(zd2,0,"%s = DELETED \n",pp1[ii]);
   }
   
   ii = zdialog_choose(Mwin,"parent",Bproceed,Bproceed,Bcancel,null);
   if (ii != 1) {
      zd->zstat = 0;                                                             //  cancel
      popup_report_close(zd2,0);
      goto retry;
   }

   zdialog_free(zd);
   zd = 0;

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

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

      popup_report_write(zd2,0,"%s \n",file);                                    //  report progress

      err = access(file,W_OK);                                                   //  test file can be written by me     19.1
      if (err) {
         popup_report_write(zd2,0,"%s \n",Bnowriteperm);
         continue;
      }

      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 (zd_metaview) meta_view(0);                                             //  update exif view if active
   }

   popup_report_write(zd2,0,"\n *** %s \n",Bcompleted);
   popup_report_bottom(zd2);

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;

   Fblock = 0;
   return;
}


//  dialog event and completion callback function

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

   char        countmess[80];

   if (zd->zstat == 1)                                                           //  full list
   {
      zd->zstat = 0;                                                             //  keep dialog active
      zmessageACK(Mwin,E2X("The command: $ man Image::ExifTool::TagNames \n"
                           "will show over 15000 \"standard\" tag names"));
      return 1;                                                                  //  19.0
   }
   
   if (strmatch(event,"files"))                                                  //  select images to process
   {
      zdialog_show(zd,0);                                                        //  hide parent dialog
      gallery_select();                                                          //  get image file list
      zdialog_show(zd,1);

      snprintf(countmess,80,Bfileselected,GScount);
      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, int kbkey)
{
   using namespace batchchangemeta;

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

   if (kbkey == GDK_KEY_F1) {                                                    //  key F1 pressed, show help
      showz_docfile(Mwin,"userguide",F1_help_topic);
      return;
   }

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


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

//  batch report metadata for selected image files

namespace batchreportmeta
{
   char     filex[200];                                                          //  file for metadata item list
}


//  menu function

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

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

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

   if (Findexvalid == 0) {
      zmessageACK(Mwin,Bnoindex);                                                //  no image index                     20.0
      return;
   }

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

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

***/

   zd = zdialog_new(E2X("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",E2X("list of reported metadata items"),"space=10");

   snprintf(text,100,Bfileselected,GScount);                                     //  show selected files count          20.0
   zdialog_stuff(zd,"labcount",text);

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

   fid = fopen(filex,"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;
   }

   zd2 = popup_report_open("metadata report",Mwin,600,400,0,0,0);

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

      file = GSfiles[ff];
      popup_report_write(zd2,0,"%s \n",file);

      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]) 
            popup_report_write(zd2,0,"%-24s : %s \n",keynamex[ii],keyvalx[ii]);

      for (ii = 0; ii < nkx; ii++)                                               //  free memory
         if (keyvalx[ii]) zfree(keyvalx[ii]);
      
      popup_report_write(zd2,0,"\n");                                            //  blank line separator
      popup_report_bottom(zd2);
   }

   popup_report_write(zd2,0,"\n *** %s \n",Bcompleted);
   popup_report_bottom(zd2);
   
cleanup:
   for (ii = 0; ii < nkx; ii++)                                                  //  free memory
      zfree(keynamex[ii]);
   return;
}


//  dialog event and completion function

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

   char     countmess[80];
   char     *itemlist[Mxmeta], *pp, buff[100];
   FILE     *fid;
   int      ii, jj;
   
   if (zd->zstat) zdialog_destroy(zd);

   if (strmatch(event,"files"))                                                  //  select images to process
   {
      zdialog_show(zd,0);                                                        //  hide parent dialog
      gallery_select();                                                          //  get new list
      zdialog_show(zd,1);

      snprintf(countmess,80,Bfileselected,GScount);
      zdialog_stuff(zd,"labcount",countmess);
   }

   if (strmatch(event,"edit"))                                                   //  select metadata items to report
   {
      itemlist[0] = 0;                                                           //  empty list
      
      fid = fopen(filex,"r");                                                    //  read file of extra metadata items
      if (fid) {
         for (ii = jj = 0; ii < Mxmeta; ii++) {
            pp = fgets_trim(buff,100,fid,1);
            if (! pp) break;
            if (*pp <= ' ') continue;
            itemlist[jj++] = zstrdup(pp);
         }
         
         itemlist[jj] = 0;                                                       //  mark EOL
         fclose(fid);
      }

      select_meta_keys(itemlist,0);                                              //  user edit of extras list
      
      fid = fopen(filex,"w");                                                    //  update extras list file
      if (! fid) {
         zmessageACK(Mwin,"%s \n %s",filex,strerror(errno));
         return 1;
      }
      
      for (ii = 0; ii < Mxmeta; ii++)
      {
         if (! itemlist[ii]) break;
         fprintf(fid,"%s\n",itemlist[ii]);
      }

      fclose(fid);
   }

   return 1;
}


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

//  batch geotags - set geotags for multiple image files

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

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

   F1_help_topic = "batch geotags";

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

   if (Findexvalid == 0) {
      zmessageACK(Mwin,Bnoindex);                                                //  no image index                     20.0
      return;
   }

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

   Fblock = 1;                                                                   //  19.0

/***
       _____________________________________________________
      |                Batch 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",E2X("location"),"space=5");
   zdialog_add_widget(zd,"zentry","location","hb2",0,"expand");
   zdialog_add_widget(zd,"label","space","hb2",0,"space=5");
   zdialog_add_widget(zd,"label","labcountry","hb2",E2X("country"),"space=5");
   zdialog_add_widget(zd,"zentry","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,"zentry","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,"zentry","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);

   snprintf(text,100,Bfileselected,GScount);                                     //  show selected files count          20.0
   zdialog_stuff(zd,"labcount",text);

   zd_mapgeotags = zd;                                                           //  activate map clicks

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

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

   zdialog_fetch(zd,"location",location,100);                                    //  get location from dialog
   zdialog_fetch(zd,"country",country,100);
   zdialog_fetch(zd,"lati",lati,20);                                             //  and latitude, longitude
   zdialog_fetch(zd,"longi",longi,20);
   
   zdialog_free(zd);                                                             //  kill dialog
   zd = zd_mapgeotags = 0;                                                       //  20.0

   if (GScount == 0) goto cleanup;

   zd2 = popup_report_open(E2X("Adding Geotags"),Mwin,500,200,0,0,0);            //  status monitor popup window

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

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

      err = access(file,W_OK);                                                   //  test file can be written by me     19.1
      if (err) {
         popup_report_write(zd2,0,"%s: %s \n",Bnowriteperm,file);
         continue;
      }

      if (*location) strncpy0(meta_location,location,100);                       //  save geotags in image file EXIF
      if (*country) strncpy0(meta_country,country,100);                          //    and in image index file
      if (*lati) strncpy0(meta_lati,lati,20);                                    //  do not stuff missing data 
      if (*longi) strncpy0(meta_longi,longi,20);

      Fmetamod++;
      save_filemeta(file);                                                       //  update file metadata & index

      popup_report_write(zd2,0,"%s \n",file);                                    //  report progress
      popup_report_bottom(zd2);
   }

   popup_report_write(zd2,0,"\n *** %s \n",Bcompleted);
   popup_report_bottom(zd2);

cleanup:

   Fblock = 0;
   if (zd) zdialog_free(zd);
   zd_mapgeotags = 0;
   return;
}


//  batch_geotags dialog event function

int batch_geotags_dialog_event(zdialog *zd, cchar *event)
{
   int      yn, zstat, err;
   char     countmess[80];
   char     location[100], country[100];
   char     lati[20], longi[20];
   cchar    *errmess;
   float    flati, flongi;
   
   if (strmatch(event,"files"))                                                  //  select images to add tags
   {
      zdialog_show(zd,0);                                                        //  hide parent dialog
      gallery_select();                                                          //  get file list from user
      zdialog_show(zd,1);

      snprintf(countmess,80,Bfileselected,GScount);
      zdialog_stuff(zd,"labcount",countmess);
   }

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

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

   if (zstat == 1)                                                               //  [find]
   {
      find_location(zd);                                                         //  find location data via user
      return 1;
   }

   else if (zstat == 2)                                                          //  [web]
   {
      errmess = web_geocode(zd);                                                 //  look-up in web service
      if (errmess) zmessageACK(Mwin,errmess);                                    //  fail
      return 1;                                                                  //  19.0
   }

   else if (zstat == 3)                                                          //  [proceed]
   {
      zdialog_fetch(zd,"location",location,100);                                 //  get location from dialog
      zdialog_fetch(zd,"country",country,100);
      strTrim2(location);
      strTrim2(country);

      if (*location) {                                                           //  20.0
         *location = toupper(*location);                                         //  capitalize
         zdialog_stuff(zd,"location",location);
      }
      
      if (*country) {
         *country = toupper(*country);
         zdialog_stuff(zd,"country",country);
      }

      zdialog_fetch(zd,"lati",lati,20);                                          //  get latitude, longitude
      zdialog_fetch(zd,"longi",longi,20);
      strTrim2(lati);
      strTrim2(longi);
      
      if (*lati || *longi) {                                                     //  if coordinates present, validate
         err = validate_latlong(lati,longi,flati,flongi);
         if (err) goto badcoord;
      }

      if (! GScount) goto nofiles;

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

      put_geolocs(zd);                                                           //  update geolocs table

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

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

badcoord:
   zmessageACK(Mwin,E2X("bad latitude/longitude: %s %s"),lati,longi);
   return 1;

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


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

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

namespace locs_names
{
   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      pline;

   int   locs_comp(cchar *rec1, cchar *rec2);
   int   locs_comp2(cchar *rec1, cchar *rec2);
   void  locs_clickfunc(GtkWidget *widget, int line, int pos, int kbkey);
   int   locs_getdays(cchar *date);
}


//  menu function

void m_meta_places_dates(GtkWidget *, cchar *)
{
   using namespace locs_names;

   zdialog        *zd, *zd2;
   int            zstat, ii, cc, cc1, cc2;
   int            ww, iix, iig, newgroup;
   char           country[100], location[100], pdate[12];
   xxrec_t        *xxrec;

   F1_help_topic = "places/dates";

   if (Findexvalid == 0) {
      zmessageACK(Mwin,Bnoindex);                                                //  no image index                     20.0
      return;
   }
   
   if (Findexvalid == 1) zmessage_post_bold(Mwin,"20/10",2,Boldindex);           //  warn, index missing new files

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

/***
          ______________________________________
         |      Report Image Locations          |
         |                                      |
         | (o) Group by country                 |
         | (o) Group by country/location        |
         | (o) Group by country/location/date   |
         | (o) Group by date/country/location   |
         |    Combine within [ xx ] days        |
         |                                      |
         |                   [proceed] [cancel] |
         |______________________________________|

***/

   zd = zdialog_new(E2X("Report Image Locations"),Mwin,Bproceed,Bcancel,null);
   zdialog_add_widget(zd,"radio","country","dialog",E2X("Group by country"));
   zdialog_add_widget(zd,"radio","location","dialog",E2X("Group by country/location"));
   zdialog_add_widget(zd,"radio","date","dialog",E2X("Group by country/location/date"));
   zdialog_add_widget(zd,"radio","date2","dialog",E2X("Group by date/country/location"));
   zdialog_add_widget(zd,"hbox","hbr","dialog");
   zdialog_add_widget(zd,"label","space","hbr",0,"space=10");
   zdialog_add_widget(zd,"label","labr1","hbr",E2X("Combine within"),"space=10");
   zdialog_add_widget(zd,"zspin","range","hbr","0|999|1|1");
   zdialog_add_widget(zd,"label","labr2","hbr",E2X("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_restore_inputs(zd);
   zdialog_resize(zd,300,0);
   zdialog_run(zd,null,"parent");
   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
   
   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 = Nxxrec * sizeof(grec_t);                                                 //  allocate memory
   grec = (grec_t *) zmalloc(cc);
   memset(grec,0,cc);

   if (! Nxxrec) {
      zmessageACK(Mwin,"no geotags data found");
      return;
   }
   
   for (ii = 0; ii < Nxxrec; ii++)                                               //  loop all index recs
   {
      xxrec = xxrec_tab[ii];

      grec[ii].location = zstrdup(xxrec->location);                              //  get location and country
      grec[ii].country = zstrdup(xxrec->country);

      if (strmatch(grec[ii].location,""))                                        //  replace "" with "null"             20.0
         grec[ii].location = zstrdup("null");                                    //    for reported location
      if (strmatch(grec[ii].country,"")) 
         grec[ii].country = zstrdup("null");
      
      strncpy0(grec[ii].pdate,xxrec->pdate,9);                                   //  photo date, truncate to yyyymmdd
      if (grec[ii].pdate[0] == 0) strcpy(grec[ii].pdate,"null");
      grec[ii].lodate = locs_getdays(xxrec->pdate);                              //  days since 0 CE
      grec[ii].hidate = grec[ii].lodate;
   }
   
   Ngrec = Nxxrec;

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

   zd2 = popup_report_open(E2X("Image Locations"),Mwin,ww,400,                   //  write groups to popup window
                                     1,locs_clickfunc,"find","done",0);          //  20.0

   if (locs_groupby == 1)                                                        //  group by country
   {
      popup_report_header(zd2,1,"%-30s  %5s","Country","Count");

      for (iig = 0; iig < Ngrec; iig++)
      {
         utf8substring(country,grec[iig].country,0,30);
         cc1 = 30 + strlen(country) - utf8len(country);
         popup_report_write(zd2,0,"%-*s  %5d \n",cc1,country,grec[iig].count);
      }
   }

   if (locs_groupby == 2)                                                        //  group by country/location
   {
      popup_report_header(zd2,1,"%-30s  %-30s  %5s","Country","Location","Count");

      for (iig = 0; iig < Ngrec; iig++)
      {
         utf8substring(country,grec[iig].country,0,30);
         cc1 = 30 + strlen(country) - utf8len(country);
         utf8substring(location,grec[iig].location,0,30);
         cc2 = 30 + strlen(location) - utf8len(location);
         popup_report_write(zd2,0,"%-*s  %-*s  %5d \n",
                        cc1,country,cc2,location,grec[iig].count);
      }
   }

   if (locs_groupby == 3)                                                        //  group by country/location/date-range
   {
      popup_report_header(zd2,1,"%-26s  %-26s  %-10s   %5s","Country","Location","Date","Count");

      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(location,grec[iig].location,0,26);
         cc2 = 26 + strlen(location) - utf8len(location);

         strncpy0(pdate,grec[iig].pdate,9);                                      //  date, yyyymmdd                     19.0
         if (! strmatch(pdate,"null")) {
            memmove(pdate+8,pdate+6,2);                                          //  convert to yyyy-mm-dd
            memmove(pdate+5,pdate+4,2);
            pdate[4] = pdate[7] = '-';
            pdate[10] = 0;
         }

         popup_report_write(zd2,0,"%-*s  %-*s  %-10s  %6d \n",
                              cc1,country,cc2,location,pdate,grec[iig].count);
      }
   }

   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);
   
      popup_report_header(zd2,1,"%-10s  %-26s  %-26s   %5s","Date","Country","Location","Count");

      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(location,grec[iig].location,0,26);
         cc2 = 26 + strlen(location) - utf8len(location);

         strncpy0(pdate,grec[iig].pdate,9);                                      //  date, yyyymmdd                     19.0
         if (! strmatch(pdate,"null")) {
            memmove(pdate+8,pdate+6,2);                                          //  convert to yyyy-mm-dd
            memmove(pdate+5,pdate+4,2);
            pdate[4] = pdate[7] = '-';
            pdate[10] = 0;
         }

         popup_report_write(zd2,0,"%-10s  %-*s  %-*s  %6d \n",
                              pdate,cc1,country,cc2,location,grec[iig].count);
      }
   }

   pline = 0;
   return;
}


//  Compare 2 grec records by geotags and date,
//  return < 0  = 0  > 0   for   rec1  <  =  >  rec2.

int locs_names::locs_comp(cchar *rec1, cchar *rec2)
{
   using namespace locs_names;

   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_names::locs_comp2(cchar *rec1, cchar *rec2)
{
   using namespace locs_names;

   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 0001 C.E.

int locs_names::locs_getdays(cchar *pdate)
{
   using namespace locs_names;

   int      year, month, day;
   char     temp[8];
   int      montab[12] = { 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 };
   int      elaps;
   
   if (strmatch(pdate,"null")) return 0;

   year = month = day = 0;

   strncpy0(temp,pdate,5);
   year = atoi(temp);
   if (year <= 0) year = 1;                                                      //  bugfix                             19.0

   strncpy0(temp,pdate+4,3);
   month = atoi(temp);
   if (month <= 0) month = 1;                                                    //                                     19.0

   strncpy0(temp,pdate+6,3);
   day = atoi(temp);
   if (day <= 0) day = 1;                                                        //                                     19.0

   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_names::locs_clickfunc(GtkWidget *widget, int line, int pos, int kbkey)
{
   using namespace locs_names;

   int            ii, lodate, hidate, datex;
   char           location[100], country[100];
   FILE           *fid;
   xxrec_t        *xxrec;
   
   if (checkpend("all")) return;                                                 //  check nothing pending
   
   if (line >= 0)                                                                //  line clicked
   {
      textwidget_scroll(widget,line);                                            //  keep line on screen
      textwidget_highlight_line(widget,line);                                    //  highlight
      pline = line;                                                              //  remember last line selected
   }
   
   else                                                                          //  KBkey pressed
   {
      if (kbkey == GDK_KEY_F1) {                                                 //  key F1 pressed, show help 
         showz_docfile(Mwin,"userguide",F1_help_topic);
         return;
      }

      if (kbkey >= 0xfd00) {
         if (kbkey == GDK_KEY_Up) pline--;                                       //  KB arrow key navigation
         if (kbkey == GDK_KEY_Down) pline++;
         if (kbkey == GDK_KEY_Page_Up) pline -= 10;
         if (kbkey == GDK_KEY_Page_Down) pline += 10;
         if (kbkey == GDK_KEY_Home) pline = 0;
         if (kbkey == GDK_KEY_End) pline = Ngrec - 1;
         if (pline < 0) pline = 0;
         if (pline > Ngrec-1) pline = Ngrec - 1;
         textwidget_highlight_line(widget,pline);                                //  highlight line
      }
   }

   textwidget_scroll(widget,pline);                                              //  keep line on screen

   strncpy0(country,grec[pline].country,100);                                    //  selected country/location/date-range
   strncpy0(location,grec[pline].location,100);
   lodate = grec[pline].lodate;
   hidate = grec[pline].hidate;

   fid = fopen(searchresults_file,"w");                                          //  open output file
   if (! fid) goto filerror;

   for (ii = 0; ii < Nxxrec; ii++)                                               //  loop all index recs
   {
      zmainloop(100);                                                            //  keep GTK alive

      xxrec = xxrec_tab[ii];

      if (! strmatch(xxrec->country,country)) continue;                          //  no country match
      
      if (locs_groupby >= 2)
         if (! strmatch(xxrec->location,location)) continue;                     //  no location match 

      if (locs_groupby >= 3) { 
         datex = locs_getdays(xxrec->pdate);
         if (xxrec->pdate[0] == 0 || strmatch(xxrec->pdate,"null")) datex = 0;
         if (datex < lodate || datex > hidate) continue;                         //  no date match
      }

      fprintf(fid,"%s\n",xxrec->file);                                           //  output matching file
   }

   fclose(fid);

   free_resources();
   navi::gallerytype = SEARCH;                                                   //  search results
   gallery(searchresults_file,"initF",0);                                        //  generate gallery of matching files
   gallery(0,"paint",0);
   m_viewmode(0,"G");
   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.

namespace timeline_names
{
   int      Nyears = 2100;
   int      Nperds = 12 * Nyears;
   int      Nyears2 = 0;
   cchar    *months = E2X("Jan  Feb  Mar  Apr  May  Jun  Jul  Aug  Sep  Oct  Nov  Dec");
   int      colpos[14] = { 0, 6, 13, 18, 23, 28, 33, 38, 43, 48, 53, 58, 63, 68 };
}


//  menu function

void m_meta_timeline(GtkWidget *, cchar *)
{
   using namespace timeline_names;

   void  timeline_clickfunc(GtkWidget *widget, int line, int pos, int kbkey);

   int         Ycount[Nyears], Pcount[Nperds];                                   //  image counts per year and period
   int         Mcount, Ecount;                                                   //  counts for missing and invalid dates
   int         ii, jj, cc;
   int         yy, mm, pp;
   char        pdate[8], nnnnn[8], buff[100];
   xxrec_t     *xxrec;
   zdialog     *zd2;

   F1_help_topic = "timeline";
   
   if (Findexvalid == 0) {
      zmessageACK(Mwin,Bnoindex);                                                //  no image index                     20.0
      return;
   }
   
   if (Findexvalid == 1) zmessage_post_bold(Mwin,"20/10",2,Boldindex);           //  warn, index missing new files

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

   Mcount = Ecount = 0;                                                          //  clear missing 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;

   for (ii = 0; ii < Nxxrec; ii++)                                               //  loop all index recs
   {
      zmainloop(100);                                                            //  keep GTK alive

      xxrec = xxrec_tab[ii];

      strncpy0(pdate,xxrec->pdate,7);                                            //  photo date, truncate to yyyymm

      if (strmatch(pdate,"")) {                                                  //  if missing, count
         ++Mcount;
         continue;
      }
      
      jj = atoi(pdate);                                                          //  photo date, 0 to 209912
      yy = jj / 100;                                                             //  year, 0000 to 2099
      mm = jj - yy * 100;                                                        //  month, 1 to 12
      if (yy < 0 || yy >= Nyears || mm < 1 || mm > 12) {
         ++Ecount;                                                               //  invalid, add to error count
         continue;
      }
     
      ++Ycount[yy];                                                              //  add to year totals
      pp = yy * 12 + mm - 1;                                                     //  add to period totals
      ++Pcount[pp];
   }

   zd2 = popup_report_open("Image Timeline",Mwin,600,400,1,                      //  write report to popup window
                                          timeline_clickfunc,0);

   popup_report_header(zd2,1,"year  count  %s",months);                          //  "year   count  Jan  Feb  ... "
   
   if (Mcount) 
      popup_report_write(zd2,0,"null  %-6d \n",Mcount);                          //  images with no date
   
   if (Ecount) 
      popup_report_write(zd2,0,"invalid %-4d \n",Ecount);                        //  images with invalid date
   
   Nyears2 = 0;

   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
         snprintf(nnnnn,6,"%-5d",Pcount[pp]);                                    //  output "NNNNN" 
         memcpy(buff+cc,nnnnn,5);                                                //  19.0
         cc += 5;
      }

      buff[cc] = 0;
      popup_report_write(zd2,0,"%s \n",buff);

      Nyears2++;                                                                 //  count reported years
   }

   popup_report_bottom(zd2);
   return;
}


//  Receive clicks on report window and generate gallery of images
//  matching the selected period

void timeline_clickfunc(GtkWidget *widget, int line, int pos, int kbkey)
{
   using namespace timeline_names;

   int         ii, jj, cc;
   int         Fnull = 0, Finvalid = 0;
   int         yy, mm;
   static int  pline, ppos;
   char        *txline, pdate[8], *pp, end;
   FILE        *fid;
   xxrec_t     *xxrec;

   if (checkpend("all")) return;                                                 //  check nothing pending
   
   if (kbkey == GDK_KEY_F1) {                                                    //  key F1 pressed, show help 
      showz_docfile(Mwin,"userguide",F1_help_topic);
      return;
   }
   
   if (line == -1)                                                               //  arrow key navigation
   {
      for (ii = 0; ii < 14; ii++)                                                //  current report column
         if (ppos == colpos[ii]) break;

      if (kbkey == GDK_KEY_Left) {                                               //  prior month
         if (ii > 2) ppos = colpos[ii-1];
         else { 
            pline -= 1;
            ppos = colpos[13]; 
         }
      }

      else if (kbkey == GDK_KEY_Right) {                                         //  next month
         if (ii < 13) ppos = colpos[ii+1];
         else {
            pline += 1;
            ppos = colpos[2];
         }
      }

      else if (kbkey == GDK_KEY_Up) pline -= 1;                                  //  prior year
      else if (kbkey == GDK_KEY_Down) pline += 1;                                //  next year

      line = pline;
      pos = ppos;
   }
   
   if (line < 0) line = 0;
   if (line > Nyears2) line = Nyears2;
   if (pos < 0) pos = 0;

   for (ii = 0; ii < 14; ii++)
      if (pos < colpos[ii]) break;
   pos = colpos[ii-1];

   textwidget_scroll(widget,line);                                               //  keep line on screen

   pline = line;                                                                 //  remember chosen line, position 
   ppos = pos;

   pp = textwidget_word(widget,line,pos," ",end);                                //  hilite clicked word
   if (pp) textwidget_highlight_word(widget,line,pos,strlen(pp));

   txline = textwidget_line(widget,line,1);                                      //  get clicked line
   if (! txline || ! *txline) return;

   cc = 0;

   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
   {
      mm = (pos - 13) / 5 + 1;                                                   //  month, 1-12
      if (mm < 1 || mm > 12) return;
      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;

   for (ii = 0; ii < Nxxrec; ii++)                                               //  loop all index recs
   {
      zmainloop(100);                                                            //  keep GTK alive

      xxrec = xxrec_tab[ii];
      
      if (Fnull) {                                                               //  search for missing dates
         if (strmatch(xxrec->pdate,"")) {
            fprintf(fid,"%s\n",xxrec->file);
            continue;
         }
      }
      
      else if (Finvalid) {                                                       //  search for invalid dates
         if (strmatch(xxrec->pdate,"")) continue;                                //  omit missing dates
         strncpy0(pdate,xxrec->pdate,7);                                         //  yyyymm
         jj = atoi(pdate);                                                       //  0 to 209912
         yy = jj / 100;                                                          //  0 to 2099
         mm = jj - yy * 100;                                                     //  1 to 12
         if (yy < 0 || yy >= Nyears || mm < 1 || mm > 12) {
            fprintf(fid,"%s\n",xxrec->file);
            continue;
         }
      }            

      else if (strmatchN(xxrec->pdate,pdate,cc)) {                               //  screen for desired period
         if (strmatch(xxrec->pdate,"")) continue;                                //  omit missing dates
         strncpy0(pdate,xxrec->pdate,7);                                         //  yyyymm
         jj = atoi(pdate);                                                       //  0 to 209912
         yy = jj / 100;                                                          //  0 to 2099
         mm = jj - yy * 100;                                                     //  1 to 12
         if (yy < 0 || yy >= Nyears || mm < 1 || mm > 12) continue;              //  invalid, reject
         fprintf(fid,"%s\n",xxrec->file);                                        //  output matching file
      }
   }

   fclose(fid);

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

filerror:
   zmessageACK(Mwin,"file error: %s",strerror(errno));
   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[20] = "";                                             //  search images
   char     searchDateTo[20] = "";                                               //  format is "yyyy-mm-dd hh:mm"
   char     searchdatelo[16], searchdatehi[16];                                  //  format is yyyymmddhhmmss

   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      Ftext, Ffiles, Ftags, Fstars, Flocs;
   int      Fdates, Fphotodate, Ffiledate, Fnulldate;
   int      Flastver, Forglast, Fallvers, Fnochange;
   int      Falltags, Falltext, Fallfiles, Falllocs;
   int      Fmeta, Frepgallery, Frepmeta, Fautosearch;
   int      Ncurrset;                                                            //  current set (gallery) count        20.0

   int      Nsearchkeys = 0;
   char     *searchkeys[5];                                                      //  metadata keys to search
   char     *searchvals[5];                                                      //  data values to search for
   char     matchtype[5];                                                        //  match type: string or number < = >
   int      keyindexed[5];                                                       //  key included in indexed metadata
   int      allkeysindexed;                                                      //  all search keys are indexed
}

using namespace search_images;


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

//  Search function for use in scripts
//     $ fotoxx -m autosearch settingsfile
//  A search is performed using the specified search settings file.
//  The list of image files found by the search is written to stdout.

void m_autosearch(GtkWidget *, cchar *)                                          //  20.0
{
   FILE     *fid;
   zdialog  *zd;
   char     settingsfile[200];

   snprintf(settingsfile,200,"%s/search_images/%s",get_zhomedir(),commandparam);

   fid = fopen(settingsfile,"r");                                                //  open settings file
   if (! fid) zexit("%s %s",commandparam,strerror(errno));

   Fautosearch = 1;
   m_search_images(0,0);                                                         //  open search dialog
   zd = zdsearchimages;

   func_load_widgets(zd,null,null,fid);                                          //  load settings into dialog
   fclose(fid);

   zdialog_send_event(zd,"proceed");                                             //  execute search
   zdialog_wait(zd);
   
   printz("search results: %s/search_results \n",get_zhomedir());                //  output file

   zexit("autosearch exit");
}


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

//  menu function

void m_search_images(GtkWidget *, cchar *)
{
   void  search_searchtags_clickfunc(GtkWidget *widget, int line, int pos, int kbkey);
   void  search_matchtags_clickfunc(GtkWidget *widget, int line, int pos, int kbkey);
   void  search_deftags_clickfunc(GtkWidget *widget, int line, int pos, int kbkey);
   int   searchimages_dialog_event(zdialog*, cchar *event);

   zdialog     *zd;
   GtkWidget   *widget;

   F1_help_topic = "search images";

   if (Findexvalid == 0) {
      zmessageACK(Mwin,Bnoindex);                                                //  no image index                     20.0
      return;
   }
   
   if (checkpend("all")) return;                                                 //  check nothing pending

   if (Findexvalid == 1) zmessage_post_bold(Mwin,"20/10",2,Boldindex);           //  warn, index missing new files

/***
          _________________________________________________________________
         |                   Search Images                                 |
         |                                                                 |
         |  images to search: (o) all  (o) current set only                |
         |  matching images: (o) make new set  (o) add to set  (o) remove  |
         |   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
         |  [x] select last version only  [x] original + last version      |
         |  [x] original + all versions  [x] no change                     |     //  20.0
         |   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
         |  report type: (o) gallery  (o) metadata                         |
         |   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
         |  date range   [______________] [______________]                 |
         |   (o) photo date  (o) file date  (yyyy-mm-dd)                   |
         |   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
         |  rating range  [__] [__]                                        |
         |   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
         |                                                        all/any  |
         |  search tags  [_______________________________________] (o) (o) |
         |  search text  [_______________________________________] (o) (o) |
         |  search files [_______________________________________] (o) (o) |
         |  search locations [___________________________________] (o) (o) |
         |   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
         |  search metadata [Add] (*)                                      |
         |   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
         |  Enter Search Tag [________________________]                    |
         |  Matching Tags [______________________________________________] |
         |   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
         |  Defined Tags Category [____________________________________|v] |
         |  |                                                            | |
         |  |                                                            | |
         |  |                                                            | |
         |  |                                                            | |
         |  |                                                            | |
         |  |                                                            | |
         |  |____________________________________________________________| |
         |                                                                 |
         |                        [load] [save] [clear] [proceed] [cancel] |
         |_________________________________________________________________|

***/

   zd = zdialog_new(E2X("Search Images"),Mwin,Bload,Bsave,Bclear,Bproceed,Bcancel,null);
   zdsearchimages = zd;

   zdialog_add_widget(zd,"hbox","hbs1","dialog");
   zdialog_add_widget(zd,"label","labs1","hbs1",E2X("images to search:"),"space=3");
   zdialog_add_widget(zd,"radio","allimages","hbs1",E2X("all"),"space=3");
   zdialog_add_widget(zd,"radio","currset","hbs1",E2X("current set only"),"space=3");

   zdialog_add_widget(zd,"hbox","hbm1","dialog");
   zdialog_add_widget(zd,"label","labs1","hbm1",E2X("matching images:"),"space=3");
   zdialog_add_widget(zd,"radio","newset","hbm1",E2X("make new set"),"space=5");
   zdialog_add_widget(zd,"radio","addset","hbm1",E2X("add to set"),"space=5");
   zdialog_add_widget(zd,"radio","remset","hbm1",E2X("remove"),"space=5");

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

   zdialog_add_widget(zd,"hbox","hbsel1","dialog");
   zdialog_add_widget(zd,"check","lastver","hbsel1",E2X("select last version only"),"space=3");
   zdialog_add_widget(zd,"check","org+last","hbsel1",E2X("original + last version"),"space=10");
   zdialog_add_widget(zd,"hbox","hbsel2","dialog");
   zdialog_add_widget(zd,"check","allvers","hbsel2",E2X("original + all versions"),"space=3");                      //  20.0
   zdialog_add_widget(zd,"check","nochange","hbsel2",E2X("no change"),"space=10");                                  //  20.0

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

   zdialog_add_widget(zd,"hbox","hbrt","dialog");
   zdialog_add_widget(zd,"label","labrt","hbrt",E2X("report type:"),"space=3");
   zdialog_add_widget(zd,"radio","repgallery","hbrt",E2X("gallery"),"space=5");
   zdialog_add_widget(zd,"radio","repmeta","hbrt","Metadata","space=5");

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

   zdialog_add_widget(zd,"hbox","hbd1","dialog");
   zdialog_add_widget(zd,"label","labD","hbd1",E2X("date range"),"space=3");
   zdialog_add_widget(zd,"zentry","datefrom","hbd1",0,"size=12|space=5");
   zdialog_add_widget(zd,"zentry","dateto","hbd1",0,"size=12");
   
   zdialog_add_widget(zd,"hbox","hbd2","dialog");
   zdialog_add_widget(zd,"radio","photodate","hbd2",E2X("photo date"),"space=8");
   zdialog_add_widget(zd,"radio","filedate","hbd2",E2X("file date"));
   zdialog_add_widget(zd,"label","labD","hbd2",E2X("(yyyy-mm-dd)"),"space=20");

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

   zdialog_add_widget(zd,"hbox","hbstars","dialog");
   zdialog_add_widget(zd,"label","labS","hbstars",E2X("rating range"),"space=5");
   zdialog_add_widget(zd,"zentry","starsfrom","hbstars",0,"size=3|space=8");     //  19.13
   zdialog_add_widget(zd,"zentry","starsto","hbstars",0,"size=3|space=8");

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

   zdialog_add_widget(zd,"hbox","hbaa","dialog");
   zdialog_add_widget(zd,"label","space","hbaa",0,"expand");
   zdialog_add_widget(zd,"label","all-any","hbaa",E2X("all/any"));

   zdialog_add_widget(zd,"hbox","hbtags","dialog","space=1");
   zdialog_add_widget(zd,"label","labtags","hbtags",E2X("search tags"),"space=3");
   zdialog_add_widget(zd,"frame","frtags","hbtags",0,"expand");
   zdialog_add_widget(zd,"text","searchtags","frtags",0,"expand|wrap");
   zdialog_add_widget(zd,"radio","alltags","hbtags",0,"space=5");
   zdialog_add_widget(zd,"radio","anytags","hbtags",0,"space=5");

   zdialog_add_widget(zd,"hbox","hbtext","dialog","space=1");
   zdialog_add_widget(zd,"label","labtext","hbtext",E2X("search text"),"space=3");
   zdialog_add_widget(zd,"zentry","searchtext","hbtext",0,"expand");
   zdialog_add_widget(zd,"radio","alltext","hbtext",0,"space=5");
   zdialog_add_widget(zd,"radio","anytext","hbtext",0,"space=5");

   zdialog_add_widget(zd,"hbox","hbfiles","dialog","space=1");
   zdialog_add_widget(zd,"label","labfiles","hbfiles",E2X("search files"),"space=3");
   zdialog_add_widget(zd,"zentry","searchfiles","hbfiles",0,"expand");
   zdialog_add_widget(zd,"radio","allfiles","hbfiles",0,"space=5");
   zdialog_add_widget(zd,"radio","anyfiles","hbfiles",0,"space=5");

   zdialog_add_widget(zd,"hbox","hblocs","dialog","space=1");
   zdialog_add_widget(zd,"label","lablocs","hblocs",E2X("search locations"),"space=3");
   zdialog_add_widget(zd,"zentry","searchlocs","hblocs",0,"expand");
   zdialog_add_widget(zd,"radio","alllocs","hblocs",0,"space=5");
   zdialog_add_widget(zd,"radio","anylocs","hblocs",0,"space=5");
   zdialog_add_ttip(zd,"searchlocs",E2X("enter cities, countries")); 

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

   zdialog_add_widget(zd,"hbox","hbmeta","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labmeta","hbmeta",E2X("search other metadata"),"space=3");
   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=2");

   zdialog_add_widget(zd,"hbox","hbnt","dialog",0,"space=1");
   zdialog_add_widget(zd,"label","labnt","hbnt",E2X("Enter Search Tag"),"space=3");
   zdialog_add_widget(zd,"zentry","entertag","hbnt",0,"size=20");

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

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

   zdialog_add_widget(zd,"hbox","hbdt1","dialog");
   zdialog_add_widget(zd,"label","labdt","hbdt1",E2X("Defined Tags Category"),"space=3");
   zdialog_add_widget(zd,"label","space","hbdt1",0,"expand");
   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");

   if (Fautosearch) {                                                            //  script file                        20.0
      zdialog_run(zd,searchimages_dialog_event,"save");                          //  bypass interactive stuff
      return;
   }

   widget = zdialog_widget(zd,"searchtags");                                     //  tag widget mouse/KB event function
   textwidget_set_eventfunc(widget,search_searchtags_clickfunc);

   widget = zdialog_widget(zd,"matchtags");
   textwidget_set_eventfunc(widget,search_matchtags_clickfunc);

   widget = zdialog_widget(zd,"deftags");
   textwidget_set_eventfunc(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,"photodate",1);
   zdialog_stuff(zd,"filedate",0);
   zdialog_stuff(zd,"lastver",0);
   zdialog_stuff(zd,"org+last",0);                                               //  20.0
   zdialog_stuff(zd,"allvers",0);
   zdialog_stuff(zd,"nochange",1);                                               //  20.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,"alllocs",0);
   zdialog_stuff(zd,"anylocs",1);
   
   zdialog_restore_inputs(zd);                                                   //  preload prior user inputs
   zdialog_fetch(zd,"searchtags",searchtags,tagScc);
   strcat(searchtags," ");                                                       //  trailing blank after "tagname,"    20.0

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

   zdialog_resize(zd,0,800);                                                     //  start dialog
   zdialog_run(zd,searchimages_dialog_event,"save");

   zdialog_wait(zd);                                                             //  wait for dialog completion
   zdialog_free(zd);
   return;
}


//  search tag was clicked

void search_searchtags_clickfunc(GtkWidget *widget, int line, int pos, int kbkey)
{
   char     *txtag, end = 0;

   if (kbkey == GDK_KEY_F1) {                                                    //  key F1 pressed, show help
      showz_docfile(Mwin,"userguide",F1_help_topic);
      return;
   }

   txtag = textwidget_word(widget,line,pos,",;:",end);
   if (! txtag) return;

   del_tag(txtag,searchtags);                                                    //  remove from search list
   zdialog_stuff(zdsearchimages,"searchtags",searchtags);

   zfree(txtag);
   return;
}


//  matching tag was clicked 

void search_matchtags_clickfunc(GtkWidget *widget, int line, int pos, int kbkey) 
{
   char     *txtag, end = 0;

   if (kbkey == GDK_KEY_F1) {                                                    //  key F1 pressed, show help 
      showz_docfile(Mwin,"userguide",F1_help_topic);
      return;
   }

   txtag = textwidget_word(widget,line,pos,",;",end);
   if (! txtag) 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(txtag);
   return;
}


//  defined tag was clicked

void search_deftags_clickfunc(GtkWidget *widget, int line, int pos, int kbkey)
{
   char     *txtag, end = 0;

   if (kbkey == GDK_KEY_F1) {                                                    //  key F1 pressed, show help
      showz_docfile(Mwin,"userguide",F1_help_topic);
      return;
   }

   txtag = textwidget_word(widget,line,pos,",;:",end);
   if (! txtag || end == ':') return;                                            //  tag category clicked, ignore

   add_tag(txtag,searchtags,tagScc);                                             //  add to search tag list
   zdialog_stuff(zdsearchimages,"searchtags",searchtags);

   zfree(txtag);
   return;
}


//  search images dialog event and completion callback function

int searchimages_dialog_event(zdialog *zd, cchar *event)
{
   int  datetimeOK(char *datetime);
   int  searchimages_select(xxrec_t *xxrec);
   int  searchimages_metadata_dialog(zdialog *zd);
   int  searchimages_metadata_report(void);

   cchar    dateLoDefault[20] = "0000-01-01 00:00:00";                           //  start of time
   cchar    dateHiDefault[20] = "2099-12-31 23:59:59";                           //  end of time

   char     *file, *file2, **vlist;
   char     **flist, *pp, buffer[XFCC];
   char     mm[4] = "mm";
   int      match, Nmatch;
   int      ii, jj, kk, err, cc;
   int      nt, cc1, cc2, ff, nf;
   xxrec_t  *xxrec;
   FILE     *fid;
   char     *pp1, *pp2;
   char     entertag[tagcc], matchtags[20][tagcc];
   char     matchtagstext[(tagcc+2)*20];
   char     catgname[tagcc];
   char     albumfile[200];
   
   if (strmatch(event,"proceed")) zd->zstat = 4;                                 //  "proceed" from autosearch          20.0

   if (strmatch(event,"addmeta"))                                                //  get other metadata criteria
      Fmeta = searchimages_metadata_dialog(zd);

   if (Fmeta) zdialog_stuff(zd,"metadata#","(*)");                               //  show presence of metadata criteria
   else zdialog_stuff(zd,"metadata#","(_)");

   if (zd->zstat == 1) {                                                         //  [load] settings from file          20.0
      zd->zstat = 0;
      func_load_widgets(zd,null,"search_images",null);
      zdialog_fetch(zd,"searchtags",searchtags,tagScc);
      strcat(searchtags," ");                                                    //  trailing blank after "tagname,"    20.0
   }

   if (zd->zstat == 2) {                                                         //  [save] settings to file            20.0
      zd->zstat = 0;
      func_save_widgets(zd,null,"search_images",null);
   }
   
   if (zd->zstat == 3)                                                           //  [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,"org+last",0);                                            //  20.0
      zdialog_stuff(zd,"allvers",0); 
      zdialog_stuff(zd,"nochange",1);                                            //  20.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,"photodate",1);
      zdialog_stuff(zd,"filedate",0);
      zdialog_stuff(zd,"starsfrom","");
      zdialog_stuff(zd,"starsto","");
      zdialog_stuff(zd,"searchtags","");
      zdialog_stuff(zd,"searchtext","");
      zdialog_stuff(zd,"searchfiles","");
      zdialog_stuff(zd,"searchlocs","");

      *searchtags = 0;

      Flocs = 0;
      *searchLocations = 0;

      zdialog_stuff(zd,"metadata#","( )");
      Fmeta = 0;
      Nsearchkeys = 0;

      zd->zstat = 0;                                                             //  keep dialog active
      return 1;
   }
   
   if (zstrstr("lastver org+last allvers nochange",event)) {                     //  if any selected,                   20.0
      zdialog_stuff(zd,"lastver",0);                                             //    deselect the others
      zdialog_stuff(zd,"org+last",0);
      zdialog_stuff(zd,"allvers",0);
      zdialog_stuff(zd,"nochange",0);
      zdialog_stuff(zd,event,1);
   }

   if (strmatch(event,"entertag"))                                               //  new tag is being typed in
   {
      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,"defcats")) {                                              //  new tag category selection 
      zdialog_fetch(zd,"defcats",catgname,tagcc);
      deftags_stuff(zd,catgname);
   }
   
   if (zd->zstat != 4) return 1;                                                 //  cancel if not [proceed]

//  Inputs are complete. Validate all inputs. -----------------------------------

   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,E2X("to remove images from current set, \n"
                           "search current set"));
      zd->zstat = 0;                                                             //  keep dialog active
      return 1;
   }

   if (Faddset && Fscancurr) {
      zmessageACK(Mwin,E2X("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,"org+last",Forglast);                                        //  get original + last version        20.0
   zdialog_fetch(zd,"allvers",Fallvers);                                         //  get all vers. of matching image

   zdialog_fetch(zd,"datefrom",searchDateFrom,20);                               //  get search date range
   zdialog_fetch(zd,"dateto",searchDateTo,20);
   zdialog_fetch(zd,"photodate",Fphotodate);                                     //  photo or file date
   zdialog_fetch(zd,"filedate",Ffiledate);
   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);
   zdialog_fetch(zd,"alllocs",Falllocs);
   
   Fdates = 0;
   if (*searchDateFrom) Fdates++;                                                //  search date from was given
   else strcpy(searchDateFrom,dateLoDefault);                                    //  else search from start of time

   if (*searchDateTo) Fdates++;                                                  //  search date to was given
   else strcpy(searchDateTo,dateHiDefault);                                      //  else search to end of time

   Fnulldate = 0;
   if (strmatchcase(searchDateFrom,"null")) {                                    //  search for missing photo date
      Fnulldate = 1;                                                             //  (user input "null")
      Fdates = Ffiledate = Fphotodate = 0;
      zdialog_stuff(zd,"photodate",1);
      zdialog_stuff(zd,"filedate",0);
   }
   
   if (Fdates)                                                                   //  complete partial date/time data
   {
      cc = strlen(searchDateFrom);
      for (ii = cc; ii < 20; ii++)                                               //  default date from:
         searchDateFrom[ii] = dateLoDefault[ii];                                 //    0000-01-01 00:00:00

      cc = strlen(searchDateTo);
      for (ii = cc; ii < 20; ii++)                                               //  default date to:
         searchDateTo[ii] = dateHiDefault[ii];                                   //    2099-12-31 23:59:59

      if (cc == 7) {                                                             //  input was yyyy-mm 
         strncpy(mm,searchDateTo+5,2);                                           //  get mm = "01" .. "12"
         if (strstr("04 06 09 11",mm)) memmove(searchDateTo+8,"30",2);           //  set dd = 30 for these months       19.0
         if (strmatch(mm,"02")) {
            memmove(searchDateTo+8,"28",2);                                      //  set dd = 28 for month 02
            ii = atoi(searchDateTo);
            if (ii == (ii/4)*4) memmove(searchDateTo+8,"29",2);                  //  set dd = 29 if leap year
         }
      }

      ff = 0;                                                                    //  check search dates reasonable 
      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)
      if (strcmp(searchDateFrom,searchDateTo) > 0) ff = 1;                       //  search-from date > search-to date
      if (ff) {
         zmessageACK(Mwin,E2X("search dates not reasonable \n %s  %s"),
                                    searchDateFrom,searchDateTo);
         zd->zstat = 0;
         return 1;
      }

      pp = pdatetime_metadatetime(searchDateFrom);                               //  convert to yyyymmddhhmmss
      if (! pp) return 1;                                                        //    for metadata comparisons
      strncpy0(searchdatelo,pp,16);
      pp = pdatetime_metadatetime(searchDateTo);
      if (! pp) return 1;
      strncpy0(searchdatehi,pp,16);
   }

   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,E2X("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

   //  Begin search. ------------------------------------------------------------

   if (checkpend("all")) return 1;                                               //  check nothing pending
   Fblock = 1;
   Ffuncbusy = 1;
   m_viewmode(0,"G");

   Nmatch = 0;                                                                   //  matching images found              20.0
   
   //  Search all images, make NEW image set from matching images.

   if (Fscanall && Fnewset)
   {
      fid = fopen(searchresults_file,"w");                                       //  open output file
      if (! fid) goto filerror;

      for (ii = 0; ii < Nxxrec; ii++)                                            //  loop all index recs
      {
         zmainloop(10);                                                          //  keep GTK alive
         if (Fescape) goto usercancel;                                           //  user cancel                        19.0

         xxrec = xxrec_tab[ii];

         match = searchimages_select(xxrec);                                     //  test against select criteria
         if (match) {                                                            //  all criteria passed
            Nmatch++;                                                            //  count matches
            fprintf(fid,"%s\n",xxrec->file);                                     //  save matching file
            snprintf(paneltext,200,"images found: %d",Nmatch);                   //  update progress
         }
      }

      fclose(fid);
      Ncurrset = Nmatch;                                                         //  current set count
   }

   //  Search all images, ADD matching images to current image set.

   if (Fscanall && Faddset)
   {
      fid = fopen(searchresults_file,"a");                                       //  open output file APPEND            20.0
      if (! fid) goto filerror;

      for (ii = 0; ii < Nxxrec; ii++)                                            //  loop all index recs
      {
         zmainloop(10);                                                          //  keep GTK alive
         if (Fescape) goto usercancel;                                           //  user cancel                        19.0

         xxrec = xxrec_tab[ii];

         match = searchimages_select(xxrec);                                     //  test against select criteria
         if (match) {                                                            //  all criteria passed
            Nmatch++;                                                            //  count matches
            fprintf(fid,"%s\n",xxrec->file);                                     //  save matching filename
            snprintf(paneltext,200,"images added: %d",Nmatch);                   //  update progress
         }
      }

      fclose(fid);
      Ncurrset += Nmatch;                                                        //  new current set count              20.0
   }

   //  Search current image set, 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(10);                                                          //  keep GTK alive
         if (Fescape) goto usercancel;                                           //  user cancel                        19.0

         file = gallery(0,"get",ii);
         if (! file) break;
         if (*file == '!') {                                                     //  skip folders
            zfree(file);
            continue;
         }

         xxrec = get_xxrec(file);
         if (! xxrec) {                                                          //  no image index rec?
            zfree(file);
            continue;
         }

         match = searchimages_select(xxrec);                                     //  test against select criteria
         if (match) {                                                            //  passed
            Nmatch++;                                                            //  count matching images
            fprintf(fid,"%s\n",file);                                            //  output matching image file
         }
         zfree(file);                                                            //  free memory
         snprintf(paneltext,200,"images kept: %d",Nmatch);                       //  update progress                    20.0
      }

      fclose(fid);
      Ncurrset = Nmatch;                                                         //  new current set count              20.0
   }

   //  Search current image set, 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(10);                                                          //  keep GTK alive
         if (Fescape) goto usercancel;                                           //  user cancel                        19.0

         file = gallery(0,"get",ii);
         if (! file) break;
         if (*file == '!') {                                                     //  skip folders
            zfree(file);
            continue;
         }

         xxrec = get_xxrec(file);
         if (! xxrec) {                                                          //  no image index rec?
            zfree(file);
            continue;
         }

         match = searchimages_select(xxrec);                                     //  test against select criteria
         if (match) Nmatch++;                                                    //  if match, count and skip
         else fprintf(fid,"%s\n",file);                                          //  save retained file
         zfree(file);                                                            //  free memory
         snprintf(paneltext,200,"images removed: %d",Nmatch);                    //  update progress                    20.0
      }

      fclose(fid);
      Ncurrset -= Nmatch;                                                        //  new current set count              20.0
   }

   //  Include only last version of matching images

   if (Flastver && Ncurrset)
   {
      cc = Ncurrset * 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 < Ncurrset; 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 < Ncurrset; ii++)                                          //  scan file list in memory
      {
         pp = strrchr(flist[ii],'/');                                            //  /folder.../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 search results file

      Nmatch = 0;

      for (ii = 0; ii < Ncurrset; ii++)
      {
         file = flist[ii];
         if (! file) continue;
         file2 = file_newest_version(file);                                      //  insure last version                20.0
         if (! file2) continue;                                                  //  should not happen
         fprintf(fid,"%s\n",file2);
         zfree(file);
         zfree(file2);
         Nmatch++;
         snprintf(paneltext,200,"included files: %d",Nmatch);                    //  update progress                    20.0
      }

      fclose(fid);
      zfree(flist);
      Ncurrset = Nmatch;                                                         //  new current set count              20.0
   }
   
   //  Include original and last version of matching images                      //  20.0
   
   if (Forglast && Ncurrset)
   {
      cc = Ncurrset * sizeof(char *);                                            //  first, reduce selected images
      flist = (char **) zmalloc(cc);                                             //    to one version each

      fid = fopen(searchresults_file,"r");                                       //  read file of selected image files
      if (! fid) goto filerror;

      for (ii = 0; ii < Ncurrset; ii++)                                          //  build file list in memory
      {
         file = fgets_trim(buffer,XFCC,fid);
         flist[ii] = zstrdup(file);
      }

      fclose(fid);

      for (ii = 1; ii < Ncurrset; ii++)                                          //  scan file list in memory
      {
         pp = strrchr(flist[ii],'/');
         if (! pp) continue;
         pp = strrchr(pp,'.');
         if (! pp) continue;                                                     //  /folder.../filename.ext
         if (strmatchN(pp-4,".v",2)) pp -= 4;                                    //  (or)   .../filename.vNN.ext
         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 search results file
      
      for (ii = 0; ii < Ncurrset; ii++)
      {
         if (flist[ii]) {
            fprintf(fid,"%s\n",flist[ii]);
            zfree(flist[ii]);
         }
      }

      fclose(fid);
      zfree(flist);

      flist = (char **) zmalloc(maxgallery * sizeof(char *));                    //  new list for original + last ver.

      fid = fopen(searchresults_file,"r");                                       //  read file of selected image files
      if (! fid) goto filerror;
      
      for (ii = Nmatch = 0; Nmatch < maxgallery-2; ii++)                         //  get all versions for selected files
      {
         file = fgets_trim(buffer,XFCC,fid);
         if (! file) break;
         vlist = file_all_versions(file,nf);
         if (! vlist) continue;
         if (nf > 0) flist[Nmatch++] = zstrdup(vlist[0]);                        //  original
         if (nf > 1) flist[Nmatch++] = zstrdup(vlist[nf-1]);                     //  last version
         for (kk = 0; kk < nf; kk++)
            zfree(vlist[kk]);
         zfree(vlist);
         snprintf(paneltext,200,"included files: %d",Nmatch);                    //  update progress                    20.0
      }

      fclose(fid);
      
      if (Nmatch > maxgallery-2)
         zmessageACK(Mwin,Bgallerytruncated,maxgallery);
      
      fid = fopen(searchresults_file,"w");                                       //  new search results file
      if (! fid) goto filerror;

      for (ii = 0; ii < Nmatch; ii++)                                            //  write all versions to file
      {
         fprintf(fid,"%s\n",flist[ii]);
         zfree(flist[ii]);
      }

      fclose(fid);
      zfree(flist);
      Ncurrset = Nmatch;                                                         //  new current set count              20.0
   }
   
   //  Include original and all versions of matching images.

   if (Fallvers && Ncurrset)
   {
      cc = Ncurrset * sizeof(char *);                                            //  first, reduce selected images
      flist = (char **) zmalloc(cc);                                             //    to one version each

      fid = fopen(searchresults_file,"r");                                       //  read file of selected image files
      if (! fid) goto filerror;

      for (ii = 0; ii < Ncurrset; ii++)                                          //  build file list in memory
      {
         file = fgets_trim(buffer,XFCC,fid);
         flist[ii] = zstrdup(file);
      }

      fclose(fid);

      for (ii = 1; ii < Ncurrset; ii++)                                          //  scan file list in memory
      {
         pp = strrchr(flist[ii],'/');
         if (! pp) continue;
         pp = strrchr(pp,'.');
         if (! pp) continue;                                                     //  /folder.../filename.ext
         if (strmatchN(pp-4,".v",2)) pp -= 4;                                    //  (or)   .../filename.vNN.ext
         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 search results file
      
      for (ii = 0; ii < Ncurrset; ii++)
      {
         if (flist[ii]) {
            fprintf(fid,"%s\n",flist[ii]);
            zfree(flist[ii]);
         }
      }

      fclose(fid);
      zfree(flist);

      flist = (char **) zmalloc(maxgallery * sizeof(char *));                    //  new list for all versions

      fid = fopen(searchresults_file,"r");                                       //  read file of selected image files
      if (! fid) goto filerror;
      
      for (ii = Nmatch = 0; Nmatch < maxgallery-100; ii++)                       //  get all versions for selected files
      {
         file = fgets_trim(buffer,XFCC,fid);
         if (! file) break;
         vlist = file_all_versions(file,nf);
         if (! vlist) continue;
         for (jj = 0; jj < nf; jj++)
            flist[Nmatch++] = vlist[jj];
         zfree(vlist);
         snprintf(paneltext,200,"included files: %d",Nmatch);                    //  update progress                    20.0
      }

      fclose(fid);
      
      if (Nmatch > maxgallery-100)
         zmessageACK(Mwin,Bgallerytruncated,maxgallery);
      
      fid = fopen(searchresults_file,"w");                                       //  new search results file
      if (! fid) goto filerror;

      for (ii = 0; ii < Nmatch; ii++)                                            //  write all versions to file
      {
         fprintf(fid,"%s\n",flist[ii]);
         zfree(flist[ii]);
      }

      fclose(fid);
      zfree(flist);
      Ncurrset = Nmatch;                                                         //  new current set count              20.0
   }

   //  Search complete. ---------------------------------------------------------

   printz("search count: %d \n", Ncurrset);

   *paneltext = 0;                                                               //  remove progress counter
   Ffuncbusy = 0;
   Fblock = 0;

   if (Ncurrset == 0) {                                                          //  no images found
      zd->zstat = 0;                                                             //  stay in search dialog
      return 1;
   }
   
   snprintf(albumfile,200,"%s/Search Results",albums_folder);                    //  save search results in the
   err = copyFile(searchresults_file,albumfile);                                 //    album "Search Results"
   if (err) zmessageACK(Mwin,strerror(err));

   free_resources();

   navi::gallerytype = SEARCH;                                                   //  normal search results
   gallery(searchresults_file,"initF",0);                                        //  generate gallery of matching files

   if (Frepmeta) {                                                               //  metadata report format
      searchimages_metadata_report();                                            //  get metadata from image files
      navi::gallerytype = META;                                                  //  report                             20.0
   }

   gallery(0,"paint",0);                                                         //  position at top
   m_viewmode(0,"G");
   zdialog_free(zd);
   return 1;

usercancel:                                                                      //  cancel via escape key              19.0
   printz("search canceled \n");
   Fescape = 0;
   zmessage_post_bold(Mwin,"parent",2,Bcancel);
   *paneltext = 0;
   Ffuncbusy = 0;
   Fblock = 0;
   zd->zstat = 0;
   return 1;

filerror:
   zmessageACK(Mwin,"file error: %s",strerror(errno));
   *paneltext = 0;
   Ffuncbusy = 0;
   Fblock = 0;
   return 1;
}


//  Test a given image against selection criteria, return match status.

int searchimages_select(xxrec_t *xxrec)
{
   int  searchimages_metadata_select(xxrec_t *xxrec);

   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(xxrec->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 (Fnulldate && ! strmatch(xxrec->pdate,"")) return 0;                       //  missing photo date wanted

   if (Fdates)                                                                   //  date match is wanted
   {
      if (Fphotodate) {
         if (strcmp(xxrec->pdate,searchdatelo) < 0) return 0;                    //  test photo date
         if (strcmp(xxrec->pdate,searchdatehi) > 0) return 0;
      }

      if (Ffiledate) {                                                           //  test file mode date 
         if (strcmp(xxrec->fdate,searchdatelo) < 0) return 0;
         if (strcmp(xxrec->fdate,searchdatehi) > 0) return 0;
      }
   }

   if (Ftags)                                                                    //  tags match is wanted
   {
      if (! xxrec->tags) return 0;                                               //  should not happen                  19.10

      Nmatch = Nnomatch = 0;

      for (iis = 1; ; iis++)                                                     //  step thru search tags
      {
         pps = strField(searchtags,",;",iis);                                    //  delimited
         if (! pps) break;
         if (*pps == 0) continue;

         for (iif = 1; ; iif++)                                                  //  step thru file tags (delimited)
         {
            ppf = strField(xxrec->tags,",;",iif);
            if (! ppf) { Nnomatch++; break; }                                    //  count matches and fails
            if (*ppf == 0) continue;
            if (strmatch(pps,ppf)) { Nmatch++; break; }                          //  match
         }
      }

      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 && xxrec->rating[0] < *searchStarsFrom) return 0;
      if (*searchStarsTo && xxrec->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 == 0) continue;

         ppf = strcasestr(xxrec->capt,pps);                                      //  match words in captions and comments
         if (! ppf) ppf = strcasestr(xxrec->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
   {
      Nmatch = Nnomatch = 0;

      for (iis = 1; ; iis++)                                                     //  step thru search locations
      {
         pps = strField(searchLocations,", ",iis);                               //  comma or blank delimiter 
         if (! pps) break;
         if (strmatch(pps,"null")) {                                             //  search for missing location        20.0
            if (strmatch(xxrec->location,"")) Nmatch++;
            if (strmatch(xxrec->country,"")) Nmatch++;
         }
         else if (strcasestr(xxrec->location,pps)) Nmatch++;
         else if (strcasestr(xxrec->country,pps)) Nmatch++;
         else Nnomatch++;
      }

      if (! Nmatch) return 0;                                                    //  no match found
      if (Falllocs && Nnomatch) return 0;
   }

   if (Fmeta)                                                                    //  other metadata match
      if (! searchimages_metadata_select(xxrec)) return 0;

   return 1;
}


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

//  dialog to get metadata search criteria

int searchimages_metadata_dialog(zdialog *zdp)
{
   int searchimages_metadata_dialog_event(zdialog *zd, cchar *event);

   cchar *metamess = E2X("Always reported: date, stars, tags, caption, comment");
   zdialog  *zd;
   int      ii, jj, zstat;
   char     matchx[8] = "matchx";
   char     metafile[200], buff[100];
   cchar    *pp;
   FILE     *fid;
   cchar     *excludelist = "Comment, User Comment, Caption Abstract, Rating, "
                            "Date Time Original, Location, Country, Keywords";

/***
          ________________________________________________________
         |             Search and Report Metadata                 |
         |                                                        |
         |  Always reported: date, stars, tags, caption, comment  |
         |                                                        |
         |             Additional Items for Report                |
         |     Keyword                       Match Criteria       |
         | [_____________|v] [ report  ] [______________________] |
         | [_____________|v] [ matches ] [______________________] |
         | [_____________|v] [contains ] [______________________] |
         | [_____________|v] [number = ] [______________________] |
         | [_____________|v] [number >=] [______________________] |
         | [_____________|v] [number <=] [______________________] |
         |                                                        |
         |                               [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",E2X("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",E2X("Keyword"));                   //  19.0
   zdialog_add_widget(zd,"comboE","key0","vb1");
   zdialog_add_widget(zd,"comboE","key1","vb1");
   zdialog_add_widget(zd,"comboE","key2","vb1");
   zdialog_add_widget(zd,"comboE","key3","vb1");
   zdialog_add_widget(zd,"comboE","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",E2X("Match Criteria"));
   zdialog_add_widget(zd,"zentry","value0","vb3",0,"expand");
   zdialog_add_widget(zd,"zentry","value1","vb3",0,"expand");
   zdialog_add_widget(zd,"zentry","value2","vb3",0,"expand");
   zdialog_add_widget(zd,"zentry","value3","vb3",0,"expand");
   zdialog_add_widget(zd,"zentry","value4","vb3",0,"expand");
   
   if (! searchkeys[0])                                                          //  first call initialization
   {
      for (ii = 0; ii < 5; ii++) {
         searchkeys[ii] = (char *) zmalloc(40);
         searchvals[ii] = (char *) zmalloc(100);
         *searchkeys[ii] = *searchvals[ii] = 0;
      }
   }
   
   for (ii = 0; ii < 5; ii++) {
      matchx[5] = '0' + ii;
      zdialog_cb_app(zd,matchx,"report");
      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 <=");
   }
   
   snprintf(metafile,200,"%s/metadata_short_list",get_zhomedir());               //  metadata list >> keyword widgets   19.0
   fid = fopen(metafile,"r");
   if (! fid) {
      zmessageACK(Mwin,"%s \n %s",metafile,strerror(errno));
      return 0;
   }
   
   while (true) {
      pp = fgets_trim(buff,100,fid,1);
      if (! pp) break;
      if (*pp == 0) continue;
      for (jj = 1; ; jj++) {
         pp = strField(excludelist,',',jj);
         if (! pp) break;
         if (strmatch(pp,buff)) break;
      }
      if (pp) continue;
      zdialog_cb_app(zd,"key0",buff);
      zdialog_cb_app(zd,"key1",buff);
      zdialog_cb_app(zd,"key2",buff);
      zdialog_cb_app(zd,"key3",buff);
      zdialog_cb_app(zd,"key4",buff);
   }
   
   fclose(fid);
   
   zdialog_show(zdp,0);                                                          //  hide parent dialog
   zdialog_restore_inputs(zd);                                                   //  preload prior user inputs
   zdialog_run(zd,searchimages_metadata_dialog_event,"save");                    //  run dialog
   zstat = zdialog_wait(zd);                                                     //  wait for completion
   zdialog_free(zd);
   zdialog_show(zdp,1);                                                          //  restore parent dialog
   
   if (zstat != 1) return 0;                                                     //  no search criteria entered
   
   for (ii = 0; ii < Nsearchkeys; ii++) {                                        //  loop search keys
      keyindexed[ii] = 0;
      for (jj = 0; jj < Mxmeta; jj++) {                                          //  compare to xmeta keys
         if (! xmeta_keys[jj]) break;
         if (strmatchcase(searchkeys[ii],xmeta_keys[jj]))
            keyindexed[ii] = 1;                                                  //  found, search key is indexed
      }
   }

   allkeysindexed = 1;                                                           //  all search keys indexed
   for (ii = 0; ii < Nsearchkeys; ii++)
      if (! keyindexed[ii]) allkeysindexed = 0;                                  //  NOT

   return 1;
}


//  dialog event and completion callback function

int searchimages_metadata_dialog_event(zdialog *zd, cchar *event)                //  overhauled
{
   int      ii, jj, err;
   float    fnum;
   char     keyx[8] = "keyx", valuex[8] = "valuex", matchx[8] = "matchx";
   char     temp[100];
   
   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,searchvals[ii],100);                               //  get corresp. match value
      strTrim2(searchvals[jj],searchvals[ii]);                                   //  trim leading and trailing blanks

      matchx[5] = '0' + ii;
      zdialog_fetch(zd,matchx,temp,20);                                          //  get corresp. match type
      if (strmatch(temp,"report")) matchtype[jj] = 'r';                          //  report the key, no match test      19.0
      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(searchvals[jj],fnum);
         if (err) {
            snprintf(temp,100,E2X("bad number: %s"),searchvals[jj]);
            zmessageACK(Mwin,temp);
            zd->zstat = 0;
            return 1;
         }
      }

      if (ii > jj) *searchkeys[ii] = *searchvals[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 file metadata against metadata select criteria.
//  indexed metadata = xmeta: keyname1=keyvalue1^ keyname2=keyvalue2^ ...

int searchimages_metadata_select(xxrec_t *xxrec)
{
   int searchmeta_test1(cchar *keyval, cchar matchtype, cchar *searchvals);

   char        *keyvals[5];
   int         ii, jj, cc, nx = 0, pass = 1;
   cchar       *pps, *ppf;
   char        *file = xxrec->file;
   char        *xmeta = xxrec->xmeta;                                            //  indexed metadata list
   char        xkey[Mxmeta][60], xval[Mxmeta][100];
   
   pps = xmeta;                                                                  //  unpack indexed metadata 
   for (ii = 0; ii < Mxmeta; ii++) {                                             //    to xkey[], xdata[]
      ppf = strchr(pps,'=');
      if (! ppf) break;
      cc = ppf-pps;
      if (cc > 59) break;
      strncpy0(xkey[ii],pps,cc+1);
      pps = ppf + 1;
      ppf = strchr(pps,'^');
      if (! ppf) break;
      cc = ppf - pps;
      if (cc > 99) break;
      strncpy0(xval[ii],pps,cc+1);
      pps = ppf + 1;
      while (*pps == ' ') pps++;
   }
   
   nx = ii;                                                                      //  xmeta keys found

   for (ii = 0; ii < Nsearchkeys; ii++) {                                        //  loop search keys
      for (jj = 0; jj < nx; jj++) {                                              //  find xmeta key
         if (strmatchcase(searchkeys[ii],xkey[jj])) {                            //  if found, test xmeta data
            pass = searchmeta_test1(xval[jj],matchtype[ii],searchvals[ii]);      //    against search criteria
            if (! pass) return 0;                                                //  fail, no more testing needed
            break;
         }
      }
      if (jj == nx && keyindexed[ii]) {                                          //  search key indexed, not present
         pass = searchmeta_test1(0,matchtype[ii],searchvals[ii]);                //  test for "null" select criteria
         if (! pass) return 0;                                                   //  fail
      }
   }

   if (allkeysindexed) return 1;                                                 //  all search keys indexed, pass
   
   exif_get(file,(cchar **) searchkeys,keyvals,Nsearchkeys);                     //  get metadata from image file

   for (ii = 0; ii < Nsearchkeys; ii++) {                                        //  loop search keys
      pass = searchmeta_test1(keyvals[ii],matchtype[ii],searchvals[ii]);         //  test image data 
      if (! pass) break;                                                         //  fail, no more testing needed
   }
      
   for (ii = 0; ii < Nsearchkeys; ii++)                                          //  bugfix: memory leak
      if (keyvals[ii]) zfree(keyvals[ii]);

   return pass;                                                                  //  all values pass
}


//  test a single metadata key/value against select criteria

int searchmeta_test1(cchar *keyval, cchar matchtype, cchar *searchvals)
{
   int         nth;
   cchar       *pps;
   float       fkey, fval;

   if (matchtype == 'r') return 1;                                               //  key value reported, not tested     19.0

   if (! keyval)                                                                 //  no metadata present
   {
      if (strmatch(searchvals,"null")) return 1;                                 //  search for empty data, pass
      return 0;                                                                  //  fail
   }   

   if (*searchvals <= ' ') return 1;                                             //  no search values, pass

   for (nth = 1; ; nth++)                                                        //  loop all search values
   {
      pps = strField(searchvals,',',nth);                                        //  comma delimiter
      if (! pps) return 0;                                                       //  no more, no match found
      
      if (matchtype == 'm') {                                                    //  key matches any [wild] value 
         if (MatchWildIgnoreCase(pps,keyval) == 0) return 1;                     //  match not case sensitive
      }

      else if (matchtype == 'c') {                                               //  key contains any value
         if (strcasestr(keyval,pps)) return 1;                                   //  match not case sensitive
      }

      else if (matchtype == '=') {                                               //  numeric key equals any value
         fkey = atof(keyval);
         fval = atof(pps);
         if (fkey == fval) return 1;                                             //  found match
      }

      else if (matchtype == '>') {                                               //  numeric key >= one value
         fkey = atof(keyval);
         fval = atof(pps);
         if (fkey >= fval) return 1;                                             //  found match
      }

      else if (matchtype == '<') {                                               //  numeric key <= one value
         fkey = atof(keyval);
         fval = atof(pps);
         if (fkey <= fval) return 1;                                             //  found match
      }
      
      else return 0;                                                             //  should not happen
   }
   
   return 0;
}


//  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        **gmdlist2;
   char        *file, *kvals[20], blank[4] = " ";
   char        text1[2*indexrecl], text2[200];
   int         Nkeys, ii, jj, cc;
   xxrec_t     *xxrec;
   char        wwhh[16], fsize[16];
   
   if (! Nfiles) {
      printz("metadata report, 0 files \n");
      return 0;
   }
   
   cc = Nfiles * sizeof(char *);                                                 //  allocate metadata list             20.0
   gmdlist2 = (char **) zmalloc(cc);                                             //  Nfiles = curr. gallery files
   memset(gmdlist2,0,cc);                                                        //  corresp. metadata list

   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
   Gmdrows = 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();                                                               //  keep GTK alive
   
      file = gallery(0,"get",ii);
      if (! file) continue;
      
      xxrec = get_xxrec(file);                                                   //  get size metadata
      if (xxrec) {
         snprintf(wwhh,16,"%dx%d",xxrec->ww,xxrec->hh);                          //  2345x1234                          19.0
         snprintf(fsize,16,"%.2fmb",xxrec->fsize/FMEGA);                         //  12.34mb                            19.0
      }
      else {
         strcpy(wwhh,blank);                                                     //  19.0
         strcpy(fsize,blank);
      }

      exif_get(file,keys,kvals,Nkeys);                                           //  get the metadata

      for (cc = jj = 0; jj < Nkeys; jj++)                                        //  add metadata to report
      {
         if (jj == 0 && kvals[0]) {
            kvals[0][4] = kvals[0][7] = '-';                                     //  conv. yyyy:mm:dd to yyyy-mm-dd
            snprintf(text2,200,"%s:  %s \n",keys[jj], kvals[jj]);
         }
         else if (jj == 1) {
            snprintf(text2,200,"%s: %s   Size: %s  %s\n",                        //  Rating: N  size: 1234x2345 12.34mb
                        keys[1], kvals[1], wwhh, fsize);
         }
         else if (jj == 3)                                                       //  combine city and country 
            snprintf(text2,200,"Location:  %s  %s\n",kvals[3],kvals[4]);         //  Location:  xxxxxxxx xxxxxxxxx
         else if (jj == 4) continue;                                             //  skip country
         else 
            snprintf(text2,200,"%s:  %s \n",keys[jj], kvals[jj]);                //  all others

         strcpy(text1+cc,text2);
         cc += strlen(text2);
      }

      gmdlist2[ii] = zstrdup(text1);                                             //  save metadata text per image file  20.0

      for (jj = 0; jj < Nkeys; jj++)                                             //  free memory
         if (kvals[jj]) zfree(kvals[jj]);

      zfree(file);
      Fbusy_done++;
   }

   if (Gmdlist) printz("*** memory leak Gmdlist[] \n");                          //  Gmdlist[] metadata container       20.0
   Gmdlist = gmdlist2;                                                           //  gallery("init/F") clears it

   for (ii = 0; ii < Nfiles; ii++)                                               //  GFlist entry --> Gmdlist entry     20.0
      GFlist[ii].mdindex = ii;                                                   //  (GFlist can be later sorted)

   Fbusy_goal = Fbusy_done = 0;

   gallerytype = META;                                                           //  gallery type = search results/metadata
   return 0;
}


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

//  Convert date format from "yyyy-mm-dd" to "yyyymmdd".
//  Missing month or day ("yyyy" or "yyyy-mm") is replaced with "01".
//  Output user message and return null if input is 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,E2X("date format is YYYY-MM-DD"));
   return 0;

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


//  Convert time format from "hh:mm:ss" to "hhmmss".
//  Missing seconds ("hh:mm") are replaced with zero ("hhmm00").
//  Output user message and return null if input not valid.

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,E2X("time format is HH:MM [:SS]"));
   return 0;

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


//  convert yyyy-mm-dd hh:mm[:ss] to yyyymmddhhmmss

char * pdatetime_metadatetime(cchar *pdatetime)
{
   char        pdate[12], ptime[12];
   static char metadatetime[20];
   char        *pp;
   int         cc;
   
   strncpy0(pdate,pdatetime,11);                                                 //  yyyy-mm-dd
   strncpy0(ptime,pdatetime+11,9);                                               //  hh:mm[:ss]

   cc = strlen(ptime);
   if (cc == 5) strcat(ptime,":00");                                             //  hh:mm >> hh:mm:00
   else if (cc != 8) return 0;

   pp = pdate_metadate(pdate);
   if (! pp) return 0;
   strncpy0(metadatetime,pp,9);

   pp = ptime_metatime(ptime);
   if (! pp) return 0;
   strncpy0(metadatetime+8,pp,7);
   
   return metadatetime;
}


//  Convert metadata date/time "yyyymmddhhmmss" to "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;
      memcpy(ptime+6,metadate+12,2);
      ptime[5] = ':';
      ptime[8] = 0;
   }
   else *pdate = *ptime = 0;                                                     //  missing
   return;
}


//  validate a date/time string formatted "yyyy-mm-dd hh:mm[:ss]"
//  valid year is 0000 to 2099
//  return 0 if bad, 1 if OK

int datetimeOK(char *datetime)                                                   //  format changed
{
   int      monlim[12] = { 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
   int      cc, year, mon, day, hour, min, sec;
   
   cc = strlen(datetime);
   if (cc != 16 && cc != 19) return 0;

   if (datetime[4] != '-') return 0;
   if (datetime[7] != '-') return 0;
   if (datetime[13] != ':') return 0;
   if (cc == 19 && datetime[16] != ':') return 0;
      
   year = atoi(datetime);
   mon = atoi(datetime+5);
   day = atoi(datetime+8);
   hour = atoi(datetime+11);
   min = atoi(datetime+14);
   if (cc == 19) sec = atoi(datetime+17);
   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;
}


//  add tag "fotoxx" when an edited image file is saved
//  used by f_save()

int add_tag_fotoxx(cchar *file)
{
   char  fotoxx[8] = "fotoxx";
   add_tag(fotoxx,meta_tags,tagFcc);
   return 1;
}


//  set image ww/hh metadata for change outside of m_meta_edit_main()
//  used by file_save() in case file size is changed

void set_meta_wwhh(int ww, int hh)
{
   snprintf(meta_wwhh,16,"%dx%d",ww,hh);
   return;
}


//  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 == 0) 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, ...
//  force: read image index and build deftags list

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

   static int  Floaded = 0;
   FILE *      fid;
   xxrec_t     *xxrec;
   int         ii, jj, ntags, err, cc, tcc;
   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 (Findexvalid == 0) {
      zmessageACK(Mwin,Bnoindex);                                                //  no image index                     20.0
      return;
   }

   if (Floaded && ! force) 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
            pp2++; 
         }
         tags_deftags[ncats] = zstrdup(pp1);                                     //  tags_deftags[ii]
         ncats++;                                                                //   = category: tag1, tag2, ... tagN,
         if (ncats == maxtagcats) goto toomanycats;
      }
      err = fclose(fid);
      fid = 0;
      if (err) goto deftagsfilerr;
   }

//  sort the categories in ascending order

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

//  move category "nocatg" to the end of the list

   for (ii = 0; ii < ncats; ii++)
   {
      pp1 = tags_deftags[ii];
      if (strmatchN(pp1,"nocatg:",7)) {
         for (jj = ii; jj < ncats-1; jj++)
            tags_deftags[jj] = tags_deftags[jj+1];
         tags_deftags[jj] = pp1;
         break;
      }
   }

//  if not already there, add category "nocatg" to the end of the list
   
   pp1 = 0;
   if (ncats > 0) pp1 = tags_deftags[ncats-1];                                   //  last tag category
   if (pp1 && strmatchN(pp1,"nocatg:",7)) {                                      //  already 'nocatg'
      nocat = ncats - 1;
      nocatcc = strlen(pp1);
      pp2 = (char *) zmalloc(tagGcc);                                            //  re-allocate max. size
      tags_deftags[nocat] = pp2;                                                 //    for following phase
      strcpy(pp2,pp1);
      zfree(pp1);
   }
   else {
      nocat = ncats;                                                             //  add to end of list
      ncats++;
      tags_deftags[nocat] = (char *) zmalloc(tagGcc);                            //  allocate max. size
      strcpy(tags_deftags[nocat],"nocatg: ");
      nocatcc = 8;
   }

//  search image index recs for all tags in all images
//  for tags not found in defined tags list, add to 'nocatg' list

   for (ii = 0; ii < Nxxrec; ii++)                                               //  loop all index recs
   {
      zmainloop(100);                                                            //  keep GTK alive

      xxrec = xxrec_tab[ii];

      pp1 = xxrec->tags;                                                         //  should never be null

      while (pp1)                                                                //  was: while (true)                  19.10
      {
         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;
      }
   }

   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,':');
      if (! pp2) {
         printz("defined tags file format error: %s \n",pp1);
         continue;
      }
      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;
      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;
   }
   
   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:
   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]) break;
      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");
   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
   save_deftags();
   return 0;

oldcatg:                                                                         //  logic simplified
   pp2 = pp1 + cc1 + 2;                                                          //  char following "catname: "
   cc1 = strlen(tag1);
   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,", ");
   save_deftags();
   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)
{
   GtkWidget      *widget;
   int            ii, ff, cc;
   char           catgname[tagcc+4];
   char           *pp1, *pp2;
   
   widget = zdialog_widget(zd,"deftags");
   textwidget_clear(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,": ");
      textwidget_append(widget,1,catgname);                                      //  "category: " in bold text
      
      pp2++;
      if (*pp2 == ' ') pp2++;
      if (*pp2) textwidget_append(widget,0,pp2);                                 //  "cat1, cat2, ... catN," 
      textwidget_append(widget,0,"\n");
   }

   return;
}


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

void defcats_stuff(zdialog *zd)
{
   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(GtkWidget *parent)
{
   FILE        *fid;
   xxrec_t     *xxrec;
   int         ii, jj, cc;
   int         Ndeftags;
   char        **deftags;
   char        usedtag[tagcc], tagsbuff[tagGcc];
   char        *pp1, *pp2;
   zdialog     *zd2;

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

   for (ii = 0; ii < Nxxrec; ii++)                                               //  loop all index recs
   {
      zmainloop(100);                                                            //  keep GTK alive

      xxrec = xxrec_tab[ii];

      pp1 = xxrec->tags;                                                         //  image tags
      if (! pp1) continue;                                                       //  should not happen

      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 (jj = 0; jj < Ndeftags; jj++)                                       //  find in defined tags
            if (strmatch(usedtag,deftags[jj])) break;

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

         pp1 = pp2;
      }
   }

   zd2 = popup_report_open("unused tags",parent,200,200,0,0,0);
   for (ii = 0; ii < Ndeftags; ii++)
      popup_report_write(zd2,0,"%s \n",deftags[ii]);
   popup_report_write(zd2,0,"%d unused tags \n",Ndeftags);

   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, nkey;
   int      ww, hh;
   char     *pp;
   cchar    *exifkeys[100] = { exif_date_key, iptc_keywords_key,
                               iptc_rating_key, exif_ww_key, exif_hh_key,        //  replace exif_wwhh_key              19.15
                               exif_comment_key, iptc_caption_key, 
                               exif_city_key, exif_country_key,
                               exif_lati_key, exif_longi_key };

   char     *ppv[100], *imagedate, *imagetags, *imagestars;
   char     *imageww, *imagehh, *imagecomms, *imagecapt;
   char     *imageloc, *imagecountry, *imagelati, *imagelongi;
   
   strncpy0(p_meta_pdate,meta_pdate,15);                                         //  save prior metadata for use by
   strncpy0(p_meta_rating,meta_rating,4);                                        //    edit_metadata [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;
   
   nkey = 11;                                                                    //  add keys for indexed metadata
   for (ii = 0; ii < Mxmeta; ii++) {                                             //    from exifkeys[11];
      if (! xmeta_keys[ii]) break;
      exifkeys[nkey] = xmeta_keys[ii];
      nkey++;
   }
   
   exif_get(file,exifkeys,ppv,nkey);                                             //  get metadata from image file
   imagedate = ppv[0];
   imagetags = ppv[1];
   imagestars = ppv[2];
   imageww = ppv[3];
   imagehh = ppv[4];
   imagecomms = ppv[5];
   imagecapt = ppv[6];
   imageloc = ppv[7];
   imagecountry = ppv[8];
   imagelati = ppv[9];
   imagelongi = ppv[10];

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

   if (imagetags)
   {
      for (ii = 1; ; ii++)
      {
         pp = (char *) strField(imagetags,",;",ii);
         if (! pp) break;
         if (*pp == 0) 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(imagetags);
   }

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

   strcpy(meta_wwhh,"no data");

   if (imageww && imagehh) {                                                     //  19.15
      convSI(imageww,ww);
      convSI(imagehh,hh);
      if (ww > 0 && hh > 0) 
         snprintf(meta_wwhh,15,"%dx%d",ww,hh);
   }
   
   if (imageww) zfree(imageww);
   if (imagehh) zfree(imagehh);

   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,100);
      zfree(imageloc);
   }
   else strcpy(meta_location,"");                                                //  replace missing data with ""

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

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

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

   for (ii = 0; ii < Mxmeta; ii++) {                                             //  get indexed metadata if any
      if (! xmeta_keys[ii]) break;
      if (xmeta_data[ii]) zfree(xmeta_data[ii]);
      if (ppv[ii+11]) xmeta_data[ii] = ppv[ii+11];
      else xmeta_data[ii] = zstrdup("");
   }

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


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

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

   err = access(file,W_OK);                                                      //  test file can be written by me     19.1
   if (err) {
      zmessageACK(Mwin,"%s: %s \n",Bnowriteperm,file);
      return;
   }

   *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_comments;
   exifdata[4] = meta_caption;

   exifdata[5] = meta_location;                                                  //  if "" erase EXIF
   exifdata[6] = meta_country;
   if (strmatch(meta_lati,"") || strmatch(meta_longi,"")) 
      exifdata[7] = exifdata[8] = "";
   else {
      exifdata[7] = meta_lati;
      exifdata[8] = meta_longi;
   }
   
   nkey = 9;                                                                     //  add keys for indexed metadata 
   for (ii = 0; ii < Mxmeta; ii++) {                                             //    from exifkeys[9];
      if (! xmeta_keys[ii]) break;
      exifkeys[nkey] = xmeta_keys[ii];                                           //  missing data ("") will be erased   20.0
      exifdata[nkey] = xmeta_data[ii];
      nkey++;
   }

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

   update_image_index(file);                                                     //  update image index file

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

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


//  update image index record (replace updated file data)
//  meta_xxxx data in memory >> image index record

void update_image_index(cchar *file)
{
   int      ii, err, xcc;
   int      nn, ww, hh;
   char     xmetarec[1000];
   xxrec_t  xxrec;
   STATB    statb;
   
   err = stat(file,&statb);
   if (err) {
      zmessageACK(Mwin,Bfilenotfound);
      return;
   }

   memset(&xxrec,0,sizeof(xxrec_t));                                             //  new metadata record to make

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

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

   xxrec.rating[0] = meta_rating[0];                                             //  rating '0' to '5' stars
   xxrec.rating[1] = 0;                                                          //  make string "0" to "5"
   
   nn = sscanf(meta_wwhh,"%dx%d",&ww,&hh);
   if (nn == 2) {
      xxrec.ww = ww;                                                             //  pixel dimensions                   19.0
      xxrec.hh = hh;
   }
   
   xxrec.fsize = statb.st_size;                                                  //  file size, bytes                   19.0
   xxrec.tags = meta_tags;
   xxrec.capt = meta_caption;
   xxrec.comms = meta_comments;

   xxrec.location = meta_location;
   xxrec.country = meta_country;
   
   if (strmatch(meta_lati,"") || strmatch(meta_longi,""))
      xxrec.flati = xxrec.flongi = 0;
   else {
      xxrec.flati = atof(meta_lati);
      xxrec.flongi = atof(meta_longi);
      if (xxrec.flati < -90.0 || xxrec.flati > 90.0) 
         xxrec.flati = xxrec.flongi = 0;
      if (xxrec.flongi < -180.0 || xxrec.flongi > 180.0) 
         xxrec.flati = xxrec.flongi = 0;
   }

   xcc = 0;

   for (ii = 0; ii < Mxmeta; ii++) {                                             //  add indexed metadata if any
      if (! xmeta_keys[ii]) break;
      if (strlen(xmeta_keys[ii]) + strlen(xmeta_data[ii]) > 100) continue;       //  impractical for image search
      strcpy(xmetarec+xcc,xmeta_keys[ii]);                                       //  construct series
      xcc += strlen(xmeta_keys[ii]);                                             //    "keyname=keydata^ "
      xmetarec[xcc++] = '=';
      strcpy(xmetarec+xcc,xmeta_data[ii]);
      xcc += strlen(xmeta_data[ii]); 
      strcpy(xmetarec+xcc,"^ ");
      xcc += 2;
      if (xcc > 895) {
         printz("file metadata exceeds record size: %s \n",file);
         break;
      }
   }

   if (xcc > 0) xxrec.xmeta = zstrdup(xmetarec);
   else xxrec.xmeta = zstrdup("");

   put_xxrec(&xxrec,file);                                                       //  update image index
   gallery(file,"update",0);                                                     //  update gallery record

   return;
}


//  delete given image file from image index recs.

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


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

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

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

   zdialog_fetch(zd,"location",location,100);                                    //  get zdialog inputs
   zdialog_fetch(zd,"country",country,100);
   if (*location <= ' ' || *country <= ' ') return badinputs;
   
   *location = toupper(*location);                                               //  capitalize
   *country = toupper(*country);

   snprintf(outfile,100,"%s/web-data",temp_folder);
   snprintf(URI,299,"\"%s&location=%s,%s\"",query,location,country);

   err = shell_quiet("wget -T 10 -o /dev/null -O %s %s",outfile,URI);
   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,",",4);                                           //  look for returned location name
   if (! pp2 || ! *pp2) return notfound;

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

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

   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;

   zdialog_stuff(zd,"lati",lati);                                                //  stuff output to zdialog
   zdialog_stuff(zd,"longi",longi);

   return 0;
}


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

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

int init_geolocs()
{
   char     location[100], country[100];
   float    flati, flongi;
   double   time0, time1;
   int      cc, ii, jj;
   xxrec_t  *xxrec;
   
   if (Ngeolocs) return Ngeolocs;                                                //  already done

   if (Findexvalid == 0) {
      zmessageACK(Mwin,Bnoindex);                                                //  no image index                     20.0
      return 0;
   }

   time0 = get_seconds();
   Ffuncbusy = 1;

   cc = (Nxxrec+1) * sizeof(geolocs_t *);                                        //  get memory for geolocs table
   geolocs = (geolocs_t **) zmalloc(cc);                                         //  room for Nxxrec entries

   geolocs[0] = (geolocs_t *) zmalloc(sizeof(geolocs_t));                        //  add entry for location "" (missing)
   geolocs[0]->location = zstrdup("");
   geolocs[0]->country = zstrdup("");
   geolocs[0]->flati = 0;
   geolocs[0]->flongi = 0;
   Ngeolocs = 1;

   //  populate geolocs from image index table

   for (ii = 0; ii < Nxxrec; ii++)                                               //  loop all index recs
   {
      xxrec = xxrec_tab[ii];
      
      strncpy0(location,xxrec->location,100); 
      strncpy0(country,xxrec->country,100);
      flati = xxrec->flati;
      flongi = xxrec->flongi;
      
      if (! *location) strcpy(location,"null");                                  //  geocoordinates but no location     20.0
      if (! *country) strcpy(country,"null");

      if (Ngeolocs) {
         jj = Ngeolocs - 1;                                                      //  eliminate sequential duplicates
         if (strmatch(location,geolocs[jj]->location) &&
             strmatch(country,geolocs[jj]->country) &&
             flati == geolocs[jj]->flati &&
             flongi == geolocs[jj]->flongi) continue;
      }

      jj = Ngeolocs++;                                                           //  fill next entry in table
      geolocs[jj] = (geolocs_t *) zmalloc(sizeof(geolocs_t));
      geolocs[jj]->location = zstrdup(location);
      geolocs[jj]->country = zstrdup(country);
      geolocs[jj]->flati = flati;
      geolocs[jj]->flongi = flongi;
   }

   if (Ngeolocs > 1)
      HeapSort((char **) geolocs, Ngeolocs, geolocs_compare);                    //  sort

   for (ii = 0, jj = 1; jj < Ngeolocs; jj++)                                     //  eliminate duplicates
   {
      if (strmatch(geolocs[jj]->location,geolocs[ii]->location) &&
          strmatch(geolocs[jj]->country,geolocs[ii]->country) &&
          geolocs[jj]->flati == geolocs[ii]->flati &&
          geolocs[jj]->flongi == geolocs[ii]->flongi)
      {
         zfree(geolocs[jj]->country);                                            //  free redundant entries
         zfree(geolocs[jj]->location);
         zfree(geolocs[jj]);
      }
      else {
         ii++;                                                                   //  count unique entries
         if (ii < jj) geolocs[ii] = geolocs[jj];                                 //  pack down the table
      }
   }
   
   Ngeolocs = ii + 1;                                                            //  final geolocs table size

   time1 = get_seconds() - time0;
   Ffuncbusy = 0;

   printz("%d images, %d locations  %.3f secs. \n",Nxxrec,Ngeolocs,time1);
   return Ngeolocs;
}


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

int  geolocs_compare(cchar *rec1, cchar *rec2)
{
   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 (r1->flati == 0 && r1->flongi == 0) {                                      //  sort missing lat/long last 
      if (r2->flati == 0 && r2->flongi == 0) 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;
}


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

//  find a geolocation from partial zdialog inputs and user choice of options
//  zdialog widgets: location, country, lati, longi
//  location and country are inputs (may be partial leading strings)
//  all four widgets are outputs (found location and geocoordinates)

int find_location(zdialog *zd)
{
   int  find_location_dialog_event(zdialog *zd, cchar *event);

   zdialog     *zd2;
   cchar       *pp;
   GtkWidget   *parent;
   int         cc, ii, jj, kk, Nmatch, zstat, zoomlev;
   int         flocation = 0, fcountry = 0;
   char        location[100], country[100], text[200];
   char        lati[20], longi[20], *matches[20][2];
   float       flati1 = 999, flati2 = -999;
   float       flongi1 = 999, flongi2 = -999;
   float       flatic, flongic, kmrange, fmpp;
   
   init_geolocs();                                                               //  if not already

   zdialog_fetch(zd,"location",location,100);                                    //  get dialog inputs
   zdialog_fetch(zd,"country",country,100);
   strTrim2(location);
   strTrim2(country);

   if (*location) flocation = 1;                                                 //  one of these must be present
   if (*country) fcountry = 1;
   if (! flocation && ! fcountry) return 0;

   for (ii = Nmatch = 0; ii < Ngeolocs; ii++)                                    //  search for exact location match
   {
      if (flocation && ! strmatchcase(location,geolocs[ii]->location)) continue;
      if (fcountry && ! strmatchcase(country,geolocs[ii]->country)) continue;
      strncpy0(location,geolocs[ii]->location,100);                              //  save matching location
      strncpy0(country,geolocs[ii]->country,100);
      goto found_location;
   }

   for (ii = kk = Nmatch = 0; ii < Ngeolocs; ii++)                               //  search for partial location match
   {
      if (flocation) {
         cc = strlen(location);
         if (! strmatchcaseN(location,geolocs[ii]->location,cc)) continue;
      }
      if (fcountry) {
         cc = strlen(country);
         if (! strmatchcaseN(country,geolocs[ii]->country,cc)) continue;
      }

      for (jj = 0; jj < Nmatch; jj++)                                            //  reject duplicate match
      {
         if (strmatch(geolocs[ii]->location,matches[jj][0]) &&
            (strmatch(geolocs[ii]->country,matches[jj][1]))) break;
      }
      if (jj < Nmatch) continue;
      
      matches[Nmatch][0] = geolocs[ii]->location;                                //  save match
      matches[Nmatch][1] = geolocs[ii]->country;
      if (Nmatch == 20) return 0;                                                //  >20 matches >> no match
      Nmatch++;                                                                  //  count matches
      if (Nmatch == 1) kk = ii;                                                  //  note first match
   }
   
   if (Nmatch == 0) return 0;                                                    //  no matches

   if (Nmatch == 1) {                                                            //  one match
      strncpy0(location,geolocs[kk]->location,100);                              //  save matching location
      strncpy0(country,geolocs[kk]->country,100);
      goto found_location;
   }

   parent = zd->widget[0].widget;
   zd2 = zdialog_new(E2X("choose location"),parent,Bdone,Bcancel,null);          //  multiple matches, start dialog
   zdialog_add_widget(zd2,"comboE","locations","dialog",0,"space=5");
   for (ii = 0; ii < Nmatch; ii++) {                                             //  list locations to choose from
      snprintf(text,200,"%s | %s",matches[ii][0],matches[ii][1]);
      zdialog_cb_app(zd2,"locations",text);
   }

   zdialog_resize(zd2,300,100);
   zdialog_set_modal(zd2);
   zdialog_run(zd2,find_location_dialog_event,"mouse");                          //  run dialog, wait for completion
   zdialog_cb_popup(zd2,"locations");                                            //  open combo box list
   zstat = zdialog_wait(zd2);

   if (zstat != 1) {                                                             //  no choice made
      zdialog_free(zd2);
      return 0;
   }

   zdialog_fetch(zd2,"locations",text,200);
   pp = strField(text,'|',1);
   if (pp) strncpy0(location,pp,100);                                            //  user choice, location and country
   pp = strField(text,'|',2);
   if (pp) strncpy0(country,pp,100);
   strTrim2(location);
   strTrim2(country);
   zdialog_free(zd2);

found_location:

   zdialog_stuff(zd,"location",location);                                        //  return location data to zdialog
   zdialog_stuff(zd,"country",country);
   
   for (ii = 0; ii < Ngeolocs; ii++)                                             //  search for location & country
   {
      if (strmatchcase(location,geolocs[ii]->location) &&
          strmatchcase(country,geolocs[ii]->country)) 
      {
         if (geolocs[ii]->flati == 0 && geolocs[ii]->flongi == 0) continue;      //  ignore missing values
         if (geolocs[ii]->flati < flati1) flati1 = geolocs[ii]->flati;           //  save range of geocoordinates found
         if (geolocs[ii]->flati > flati2) flati2 = geolocs[ii]->flati;
         if (geolocs[ii]->flongi < flongi1) flongi1 = geolocs[ii]->flongi;
         if (geolocs[ii]->flongi > flongi2) flongi2 = geolocs[ii]->flongi;
      }
   }
   
   if (flati1 == 999) {                                                          //  no match, return nulls
      zdialog_stuff(zd,"lati","");
      zdialog_stuff(zd,"longi","");
      return 0;
   }
   
   if (flati1 == flati2 && flongi1 == flongi2) {                                 //  one match, return geocoordinates
      snprintf(lati,20,"%.4f",flati1);                                           //  reformat with std. precision
      snprintf(longi,20,"%.4f",flongi1);
      zdialog_stuff(zd,"lati",lati);
      zdialog_stuff(zd,"longi",longi);
      return 1;
   }
   
   flatic = 0.5 * (flati1 + flati2);                                             //  multiple matches
   flongic = 0.5 * (flongi1 + flongi2);                                          //  center of enclosing rectangle
   kmrange = earth_distance(flati1,flongi1,flati2,flongi2);                      //  length of diagonal
   if (kmrange > 100) kmrange = 100;
   
   for (zoomlev = 12; zoomlev < 20; zoomlev++)                                   //  loop small to large scale 
   {
      fmpp = netmapscale(zoomlev,flatic,flongic);                                //  meters per pixel at zoom level
      fmpp = 0.001 * fmpp * 100.0;                                               //  km span of 100 pixels
      if (fmpp < kmrange) break;                                                 //  stop when kmrange > 100 pixels
   }
   
   netmap_zoomto(flatic,flongic,zoomlev);                                        //  map click --> stuff zdialog lat/long

   return 0;
}


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

int find_location_dialog_event(zdialog *zd, cchar *event)
{
   if (strmatch(event,"locations")) zd->zstat = 1;
   return 1;
}


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

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

int put_geolocs(zdialog *zd)
{
   char        location[100], country[100];
   char        lati[20], longi[20];
   float       flati, flongi;
   int         ii, err, cc, nn, found = 0;

   zdialog_fetch(zd,"location",location,100);                                    //  get location and geocoordinates
   zdialog_fetch(zd,"country",country,100);
   strTrim2(location);
   strTrim2(country);
   if (! *location || ! *country) return 0;                                      //  location not complete

   *location = toupper(*location);                                               //  capitalize
   *country = toupper(*country);
   zdialog_stuff(zd,"location",location);
   zdialog_stuff(zd,"country",country);

   zdialog_fetch(zd,"lati",lati,20);
   zdialog_fetch(zd,"longi",longi,20);
   strTrim2(lati);
   strTrim2(longi);
   
   if (! *lati && ! *longi) return 0;                                            //  geocoordinates not complete        20.0
   
   err = validate_latlong(lati,longi,flati,flongi);
   if (err) {                                                                    //  1 = missing, 2 = bad
      zmessageACK(Mwin,E2X("bad latitude/longitude: %s %s"),lati,longi);
      return -1;
   }
   else {
      snprintf(lati,20,"%.4f",flati);                                            //  reformat with std. precision
      snprintf(longi,20,"%.4f",flongi);
   }

   for (ii = 0; ii < Ngeolocs; ii++)                                             //  search geotags for location
   {
      if (! strmatchcase(location,geolocs[ii]->location)) continue;              //  case-insensitive compare
      if (! strmatchcase(country,geolocs[ii]->country)) continue;
      if (! strmatch(location,geolocs[ii]->location)) {
         zfree(geolocs[ii]->location);                                           //  revise capitalization
         geolocs[ii]->location = zstrdup(location);
      }
      if (! strmatch(country,geolocs[ii]->country)) {
         zfree(geolocs[ii]->country);
         geolocs[ii]->country = zstrdup(country);
      }
      if (flati == geolocs[ii]->flati && flongi == geolocs[ii]->flongi) found++;
   }
   
   if (found) return 1;
   
   geolocs_t  *geolocsA = (geolocs_t *) zmalloc(sizeof(geolocs_t));
   geolocs_t  **geolocsB;
   
   geolocsA->location = zstrdup(location);                                       //  new geolocs record
   geolocsA->country = zstrdup(country);
   geolocsA->flati = flati;
   geolocsA->flongi = flongi;
   
   cc = (Ngeolocs + 1) * sizeof(geolocs_t *);
   geolocsB = (geolocs_t **) zmalloc(cc);
   
   for (ii = 0; ii < Ngeolocs; ii++) {                                           //  copy geolocs before new geoloc
      nn = geolocs_compare((cchar *) geolocs[ii], (cchar *) geolocsA);
      if (nn > 0) break;
      geolocsB[ii] = geolocs[ii];
   }
   
   geolocsB[ii] = geolocsA;                                                      //  insert new geolocs

   for (NOP; ii < Ngeolocs; ii++)                                                //  copy geolocs after new geoloc
      geolocsB[ii+1] = geolocs[ii];

   zfree(geolocs);                                                               //  geolocs --> new table
   geolocs = geolocsB;
   Ngeolocs += 1;
   
   return 2;
}


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

//  validate and convert earth coordinates, latitude and longitude
//  return: 0  OK
//          1  both are missing ("")
//          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 && ! *longi) goto status1;                                        //  both missing
   if (! *lati || ! *longi) goto status2;                                        //  one missing
   
   pp = strchr(lati,',');                                                        //  replace comma decimal point
   if (pp) *pp = '.';                                                            //    with period
   pp = strchr(longi,',');
   if (pp) *pp = '.';

   err = convSF(lati,flati,-90,+90);                                             //  convert to float and check limits
   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 long1, float lat2, float long2)
{
   float    dlat, dlong, mlat, dist;
   
   dlat = fabsf(lat2 - lat1);                                                    //  latitude distance
   dlong = fabsf(long2 - long1);                                                 //  longitude distance
   mlat = 0.5 * (lat1 + lat2);                                                   //  mean latitude
   mlat *= 0.01745;                                                              //  radians
   dlong = dlong * cosf(mlat);                                                   //  longitude distance * cos(latitude)
   dist = sqrtf(dlat * dlat + dlong * dlong);                                    //  distance in degrees
   dist *= 111.0;                                                                //  distance in km
   return dist;
}


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

//  generate a list of files and geocoordinates from the current gallery file list

int get_gallerymap()
{
   int         ii, jj, cc;
   xxrec_t     *xxrec;
   
   if (! navi::Nfiles) {                                                         //  bugfix
      zmessageACK(Mwin,E2X("gallery is empty"));
      return 0;
   }

   if (gallerymap) {                                                             //  free prior gallerymap
      for (ii = 0; ii < Ngallerymap; ii++) 
         zfree(gallerymap[ii].file);
      zfree(gallerymap);
      gallerymap = 0;
   }

   cc = sizeof(gallerymap_t);
   gallerymap = (gallerymap_t *) zmalloc(navi::Nfiles * cc);   

   for (jj = 0, ii = navi::Nfolders; ii < navi::Nfiles; ii++)                    //  loop gallery files
   {
      xxrec = get_xxrec(navi::GFlist[ii].file);                                  //  look up in xxrec_tab
      if (! xxrec) continue;
      gallerymap[jj].flati = xxrec->flati; 
      gallerymap[jj].flongi = xxrec->flongi;
      gallerymap[jj].file = zstrdup(navi::GFlist[ii].file);
      jj++;
   }

   Ngallerymap = jj;
   return Ngallerymap;
}


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

//  choose to mark map locations for all images or current gallery only

void m_set_map_markers(GtkWidget *, cchar *) 
{
   zdialog        *zd;
   int            zstat, showall = 0;

   F1_help_topic = "markers";

   if (FGWM != 'W' && FGWM != 'M') m_viewmode(0,"?");                            //  set last used W/M mode             19.0

/***
          _____________________________
         |      Set Map Markers        |
         |                             |
         | (o) mark all image files    |
         | (o) mark current gallery    |
         |                             |
         |                    [apply]  |
         |_____________________________|

***/

   zd = zdialog_new(E2X("Set Map Markers"),Mwin,Bapply,null);
   zdialog_add_widget(zd,"radio","all","dialog",E2X("mark all image files"));
   zdialog_add_widget(zd,"radio","gallery","dialog",E2X("mark current gallery"));
   zdialog_stuff(zd,"all",1);
   zdialog_stuff(zd,"gallery",0);

   zdialog_restore_inputs(zd);
   zdialog_resize(zd,200,0);
   zdialog_set_modal(zd);
   zdialog_run(zd,null,"mouse");

   zstat = zdialog_wait(zd);
   if (zstat != 1) {
      zdialog_free(zd);
      return;
   }
   
   zdialog_fetch(zd,"all",showall);                                              //  show all images
   zdialog_free(zd);

   if (showall) {
      if (gallerymap) {                                                          //  free gallerymap
         for (int ii = 0; ii < Ngallerymap; ii++) 
            zfree(gallerymap[ii].file);
         zfree(gallerymap);
         gallerymap = 0;
      }
   }

   else get_gallerymap();                                                        //  show gallery images only
   
   if (FGWM == 'W') Fpaint2();
   if (FGWM == 'M') netmap_paint_dots();

   return;
}


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

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

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

int   filemap_position(float flati, float flongi, int &mx, int &my);             //  earth coordinates > map position
int   filemap_coordinates(int mx, int my, float &flati, float &flongi);          //  map position > earth coordinates
void  find_filemap_images(float flati, float flongi);                            //  find images within range of geolocation


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

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

void m_load_filemap(GtkWidget *, cchar *menu)
{
   using namespace filemap;

   int  load_filemap_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 = "file map";

   if (Findexvalid == 0) {
      zmessageACK(Mwin,Bnoindex);                                                //  no image index                     20.0
      return;
   }

   if (Findexvalid == 1) zmessage_post_bold(Mwin,"20/10",2,Boldindex);           //  warn, index missing new files

   if (! init_geolocs()) return;                                                 //  insure geolocations are loaded
   
   m_viewmode(0,"W");                                                            //  set W view mode                    19.0

   snprintf(mapindex,200,"%s/maps_index",maps_folder);                           //  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(E2X("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_folder);                      //  look for user map index file
   err = stat(mapindex,&statb);
   if (err) goto choose_worldmap;

   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_worldmap:

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

   zdialog_resize(zd,300,100);
   zdialog_set_modal(zd);
   zdialog_run(zd,load_filemap_dialog_event,"mouse");                            //  run dialog, get user choice
   zdialog_set_decorated(zd,0);                                                  //  20.0
   zdialog_cb_popup(zd,"mapname");                                               //  make the list pop-up               20.0

   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_folder,mapname);                            //  check map file exists
   err = stat(mapfile,&statb);
   if (err) {
      snprintf(mapfile,200,"%s/%s",user_maps_folder,mapname);                    //  check user maps also
      err = stat(mapfile,&statb);
   }
   if (err) goto mapfilemissing;                                                 //  not found

   snprintf(mapindex,200,"%s/maps_index",maps_folder);                           //  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_folder);                      //  read user 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 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
   if (flongi2 < flongi1 + 0.001) goto latlongerr;

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

   free_filemap();                                                               //  free prior map

   Ffuncbusy = 1;
   zmainloop();
   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");                                                           //  fit window (small image >> 1x)
   return;

nomapsinstalled:
   zmessageACK(Mwin,E2X("fotoxx-maps package not installed \n"
                        "(see https://kornelix.net)"));
   return;

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

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


//  dialog event and completion function

int  load_filemap_dialog_event(zdialog *zd, cchar *event)
{
   using namespace filemap;

   zdialog_fetch(zd,"mapname",mapname,100);
   if (! *mapname) return 1;                                                     //  20.0
   if (strmatch(event,"mapname")) zd->zstat = 1;
   return 1;
}


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

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

int filemap_position(float flati, float flongi, int &px, int &py)
{
   using namespace filemap;

   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 filemap_coordinates(int px, int py, float &flati, float &flongi)
{
   using namespace filemap;

   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 filemap_paint_dots()
{
   int      ii, err;
   int      mx, my, dx, dy;
   float    flati, flongi, radius;
   float    plati = 999, plongi = 999;

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

   cairo_t *cr = draw_context_create(gdkwin,draw_context);

   radius = map_dotsize / 2;

   cairo_set_source_rgb(cr,1,0,0);

   if (gallerymap)                                                               //  use gallerymap[] if present
   {                                                                             //  mark gallery images on map
      for (ii = 0; ii < Ngallerymap; ii++)
      {
         flati = gallerymap[ii].flati;
         flongi = gallerymap[ii].flongi;
         if (flati == plati && flongi == plongi) continue;                       //  skip repititions
         plati = flati;
         plongi = flongi;
         err = filemap_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(cr,dx,dy,radius,0,2*PI);
         cairo_fill(cr);
      }   
   }
   
   else
   {   
      for (ii = 0; ii < Ngeolocs; ii++)                                          //  mark all image locations on map
      {
         flati = geolocs[ii]->flati;
         flongi = geolocs[ii]->flongi;
         err = filemap_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(cr,dx,dy,radius,0,2*PI);
         cairo_fill(cr);
      }
   }

   draw_context_destroy(draw_context); 
   return;
}


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

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

void filemap_mousefunc()
{
   int         err, mx, my, px, py, ii, minii;
   char        *location, *country;
   float       flati, flongi, glati, glongi;
   float       dist, mindist;
   float       mscale = Cstate->mscale;
   int         capturedist = (map_dotsize + 2) / 2;                              //  mouse - marker capture distance
   int         Fusedot = 0;
   static      char  *ploc = 0;
   char        text[20];
   zdialog     *zd = zd_mapgeotags;

   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 = filemap_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;
   location = geolocs[ii]->location;
   country = geolocs[ii]->country;

   err = filemap_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 <= capturedist) Fusedot = 1;                                         //  mouse is within marker dot

   if (LMclick)                                                                  //  left mouse click
   {
      LMclick = 0;

      if (zd)                                                                    //  stuff calling dialog
      {
         if (Fusedot) {                                                          //  click within dot
            zdialog_stuff(zd,"location",location);                               //  use dot location data
            zdialog_stuff(zd,"country",country);
            zdialog_stuff(zd,"lati",glati,"%.5f");                               //  5 decimal places
            zdialog_stuff(zd,"longi",glongi,"%.5f");
         }
         else {
            zdialog_stuff(zd,"lati",flati,"%.5f");                               //  use clicked geocoordinaes only
            zdialog_stuff(zd,"longi",flongi,"%.5f");
         }
         zdialog_send_event(zd,"geomap");                                        //  activate calling dialog
         m_viewmode(0,&PFGWM);                                                   //  restore prior view mode            7.10
      }

      else if (location)
         find_filemap_images(flati,flongi);                                      //  show images in range of location

      else {
         snprintf(text,20,"%.5f %.5f",flati,flongi);                             //  show coordinates
         poptext_mouse(text,20,-20,0.1,3);
      }
   }

   else if (location && Fusedot) {                                               //  mouse movement, no click
      if (! ploc || ! strmatch(location,ploc)) {                                 //  popup the location name at mouse
         if (strmatch(location,"null"))
            poptext_mouse(country,20,-20,0.1,1);                                 //  20.0
         else poptext_mouse(location,20,-20,0.1,1);
         ploc = location;
      }
   }

   else ploc = 0;

   return;
}


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

//  find images within the marker size, show gallery of images.
//  privat function for filemap_mousefunc(), called when a location is clicked

void find_filemap_images(float flati, float flongi)
{
   int         ii, nn = 0;
   int         x1, y1, x2, y2;
   int         capturedist = (map_dotsize + 2) / 2;                              //  mouse - marker capture distance
   float       glati, glongi, grange;
   xxrec_t     *xxrec;
   FILE        *fid;

   if (Findexvalid == 0) {
      zmessageACK(Mwin,Bnoindex);                                                //  no image index                     20.0
      return;
   }

   filemap_position(flati,flongi,x1,y1);                                         //  target map pixel location

   fid = fopen(searchresults_file,"w");                                          //  open output file
   if (! fid) {
      zmessageACK(Mwin,"output file error: %s",strerror(errno));
      return;
   }
   
   if (gallerymap)                                                               //  show gallery images at location
   {
      for (ii = 0; ii < Ngallerymap; ii++)                                       //  loop all gallery files
      {
         zmainloop(100);                                                         //  keep GTK alive
         
         glati = gallerymap[ii].flati;                                           //  file geocoordinates
         glongi = gallerymap[ii].flongi;
         filemap_position(glati,glongi,x2,y2);                                   //  image map pixel location
         
         grange = sqrtf((x1-x2)*(x1-x2) + (y1-y2)*(y1-y2));                      //  target - image pixel distance
         if (grange < 1.5 * capturedist) {                                       //  within distance limit, select
            fprintf(fid,"%s\n",gallerymap[ii].file);                             //  output matching file
            nn++;
         }
      }
   }
   
   else
   {
      for (ii = 0; ii < Nxxrec; ii++)                                            //  show all images at location
      {
         zmainloop(100);                                                         //  keep GTK alive

         xxrec = xxrec_tab[ii];

         glati = xxrec->flati;
         glongi = xxrec->flongi;
         filemap_position(glati,glongi,x2,y2);                                   //  image map pixel location
         
         grange = sqrtf((x1-x2)*(x1-x2) + (y1-y2)*(y1-y2));                      //  target - image pixel distance
         if (grange < 1.5 * capturedist) {                                       //  within distance limit, select
            fprintf(fid,"%s\n",xxrec->file);                                     //  output matching file
            nn++;
         }
      }
   }
   
   fclose(fid);

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

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

   return;
}


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

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

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


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

//  net maps using libchamplain (M view)
//  working map sources: "net-mapnik" "net-transportmap"

namespace netmaps
{
   GtkWidget                   *mapwidget = 0;
   ChamplainView               *mapview = 0;
   ChamplainMapSourceFactory   *map_factory = 0;
   ChamplainMapSource          *map_source = 0;
   ChamplainMarkerLayer        *markerlayer = 0;
   ChamplainMarker             *marker[maximages];
   ClutterColor                *markercolor;
   ChamplainRenderer           *renderer;
   ChamplainMapSource          *error_source;
   ChamplainNetworkTileSource  *tile_source;
   ChamplainFileCache          *file_cache;
   ChamplainMemoryCache        *memory_cache;
   ChamplainMapSourceChain     *source_chain;

// char     *mapbox_access_key;                                                  //  in fotoxx.h 
   cchar    *mapbox_license_text = "";
   cchar    *mapbox_license_uri = "https://www.mapbox.com/tos/";
   cchar    *mapbox_access_uri = "https://api.mapbox.com/v4/mapbox.light/#Z#/#X#/#Y#@2x.jpg70?access_token=";
   int      mapbox_tile_size = 512;
   int      mapbox_min_zoom = 1;                                                 //  for 512x512 tiles
   int      mapbox_max_zoom = 17;
   int      mapbox_file_cache = 1000 * MEGA;                                     //  tile file cache max. size
   int      mapbox_memory_cache = 1000;                                          //  tile memory cache max. tiles
}


void netmap_mousefunc(GtkWidget *, GdkEventButton *, void *);                    //  mouse click function for net map
void find_netmap_images(float flati, float flongi);                              //  find images at clicked position


//  menu function - choose net map source

void m_netmap_source(GtkWidget *, cchar *)
{
   using namespace netmaps;

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

   zdialog  *zd;
   int      zstat, nn;
   char     temp[200];

   F1_help_topic = "net source";
   
   m_viewmode(0,"M");                                                            //  19.0

/***
       ________________________________
      |          Net Map Source        |
      |                                |
      |  [_] mapnik (default)          |
      |  ----------------------------- |
      |  [x] mapbox                    |
      |  Access Key [________________] |
      |                                |
      |                [Done] [Cancel] |
      |________________________________|

***/

   zd = zdialog_new(E2X("Net Map Source"),Mwin,Bdone,Bcancel,null);
   zdialog_add_widget(zd,"hbox","hb1","dialog");
   zdialog_add_widget(zd,"check","mapnik","hb1","mapnik (default)","space=3");
   zdialog_add_widget(zd,"hsep","hsep1","dialog",0,"space=3");
   zdialog_add_widget(zd,"hbox","hb2","dialog");
   zdialog_add_widget(zd,"check","mapbox","hb2","mapbox","space=3");
   zdialog_add_widget(zd,"hbox","hb4","dialog");
   zdialog_add_widget(zd,"label","labkey","hb4","Access Key","space=3");
   zdialog_add_widget(zd,"zentry","key","hb4","","space=3|expand");

   zdialog_restore_inputs(zd);

   zdialog_set_modal(zd);
   zdialog_run(zd,netmap_source_dialog_event,"mouse");
   zstat = zdialog_wait(zd);

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

   zdialog_fetch(zd,"mapnik",nn);
   if (nn) netmap_source = zstrdup("mapnik");
   zdialog_fetch(zd,"mapbox",nn);
   if (nn) netmap_source = zstrdup("mapbox");

   zdialog_fetch(zd,"key",temp,200);                                             //  save key in parameters file
   mapbox_access_key = zstrdup(temp);                                            //    immediately
   save_params();
   
   if (mapwidget) gtk_widget_destroy(mapwidget);                                 //  remove prior map if any
   markerlayer = 0;
   m_load_netmap(0,0);

   zdialog_free(zd);
   return;
}


//  dialog event and completion function

int netmap_source_dialog_event(zdialog *zd, cchar *event)
{
   if (zstrstr("mapnik mapbox",event)) {                                         //  unset and set two checkboxes
      zdialog_stuff(zd,"mapnik",0);
      zdialog_stuff(zd,"mapbox",0);
      zdialog_stuff(zd,event,1);
   }
   
   return 1;
}


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

//  initialize for net maps

void m_load_netmap(GtkWidget *, cchar *)
{
   using namespace netmaps;

   char        mapbox_total_uri[200];

   F1_help_topic = "net map";

   m_viewmode(0,"M");                                                            //  19.0

   if (Findexvalid == 0) {
      zmessageACK(Mwin,Bnoindex);                                                //  no image index                     20.0
      return;
   }
   
   if (Findexvalid == 1) zmessage_post_bold(Mwin,"20/10",2,Boldindex);           //  warn, index missing new files

   if (! init_geolocs()) return;                                                 //  failed

   Ffuncbusy = 1;

   if (markerlayer) {                                                            //  refresh map markers
      netmap_paint_dots(); 
      Ffuncbusy = 0;
      return;
   }

   mapwidget = gtk_champlain_embed_new();                                        //  libchamplain map drawing area
   if (! mapwidget) goto fail;
   gtk_container_add(GTK_CONTAINER(Mvbox),mapwidget);

/***
   GdkWindow   *gdkwin;
   gdkwin = gtk_widget_get_window(mapwidget);                                    //  replace "hand" cursor with arrow   20.0
   gdk_window_set_cursor(gdkwin,null);                                           //  these have no effect      FIXME
   gdk_window_set_cursor(gdkwin,arrowcursor);
   gdk_window_set_device_cursor(gdkwin,zfuncs::mouse,arrowcursor);
***/

   mapview = gtk_champlain_embed_get_view(GTK_CHAMPLAIN_EMBED(mapwidget));
   if (! mapview) goto fail;

   if (strmatch(netmap_source,"mapnik"))                                         //  mapnik map source (free) 
   {
      map_factory = champlain_map_source_factory_dup_default();
      map_source = champlain_map_source_factory_create_cached_source(map_factory,"osm-mapnik");
/**   map_source = champlain_map_source_factory_create_cached_source(map_factory,"osm-transportmap");  alternative  **/ 
      champlain_view_set_min_zoom_level(mapview,3);
   }
   
   else if (strmatch(netmap_source,"mapbox"))                                    //  mapbox map source (licensed)
   {
      renderer = CHAMPLAIN_RENDERER(champlain_image_renderer_new());
      map_factory = champlain_map_source_factory_dup_default();

      strcpy(mapbox_total_uri,mapbox_access_uri);
      strcat(mapbox_total_uri,mapbox_access_key);

      tile_source = champlain_network_tile_source_new_full (
            "mapbox", "mapbox", mapbox_license_text, mapbox_license_uri, 
            mapbox_min_zoom, mapbox_max_zoom, mapbox_tile_size, 
            CHAMPLAIN_MAP_PROJECTION_MERCATOR, mapbox_total_uri, renderer);

      error_source = champlain_map_source_factory_create_error_source(map_factory,256);
      file_cache = champlain_file_cache_new_full(mapbox_file_cache, null, renderer);
      memory_cache = champlain_memory_cache_new_full(mapbox_memory_cache, renderer);

      source_chain = champlain_map_source_chain_new();
      champlain_map_source_chain_push(source_chain, error_source);
      champlain_map_source_chain_push(source_chain, CHAMPLAIN_MAP_SOURCE(tile_source));
      champlain_map_source_chain_push(source_chain, CHAMPLAIN_MAP_SOURCE(file_cache));
      champlain_map_source_chain_push(source_chain, CHAMPLAIN_MAP_SOURCE(memory_cache));

      map_source = CHAMPLAIN_MAP_SOURCE(source_chain);
   }
   
   else {
      zmessageACK(Mwin,"unknown map source: %s \n",netmap_source);
      Ffuncbusy = 0;
      return;
   }

   champlain_view_set_map_source(mapview,map_source);                            //  mapnik or mapbox

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

   gtk_widget_add_events(mapwidget,GDK_BUTTON_PRESS_MASK);                       //  connect mouse events to net map
   G_SIGNAL(mapwidget,"button-press-event",netmap_mousefunc,0);
   G_SIGNAL(mapwidget,"button-release-event",netmap_mousefunc,0);
   G_SIGNAL(mapwidget,"motion-notify-event",netmap_mousefunc,0);

   netmap_paint_dots();                                                          //  paint map markers where images 
   Ffuncbusy = 0;
   return;

fail:
   zmessageACK(Mwin,"net/libchamplain failure");
   Ffuncbusy = 0;
   return;
}


//  paint red dots corresponding to image locations on map

void netmap_paint_dots()
{
   using namespace netmaps;

   float    flati, flongi;
   float    plati = 999, plongi = 999;

   champlain_marker_layer_remove_all(markerlayer);

   if (gallerymap)                                                               //  use gallerymap[] if present
   {                                                                             //  mark gallery images on map
      for (int ii = 0; ii < Ngallerymap; ii++)
      {
         flati = gallerymap[ii].flati;                                           //  image geocoordinates
         flongi = gallerymap[ii].flongi;
         if (flati == plati && flongi == plongi) continue;                       //  skip repititions
         plati = flati;
         plongi = 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]);
      }   
   }
   
   else
   {   
      for (int ii = 0; ii < Ngeolocs; ii++)                                      //  mark all images on map
      {
         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);
   return;
}


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

//  map zoom-in on location of a selected image file

void m_netmap_zoomin(GtkWidget *, cchar *menu)
{
   using namespace netmaps;

   static char    *file = 0;
   float          flati, flongi;
   xxrec_t        *xxrec;
   
   F1_help_topic = "show on map";

   m_viewmode(0,"M");                                                            //  19.0

   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;
   
   xxrec = get_xxrec(file);
   if (! xxrec) return;
   
   flati = xxrec->flati; 
   flongi = xxrec->flongi;
   if (flati == 0 && flongi == 0) return;

   netmap_zoomto(flati,flongi,11);                                               //  20.0
   return;
}


//  map zoom-in on specified location with specified zoom level

void netmap_zoomto(float flati, float flongi, int zoomlev)
{
   using namespace netmaps;
   
   m_load_netmap(0,0);                                                           //  19.11
   champlain_view_center_on(mapview,flati,flongi);
   champlain_view_set_zoom_level(mapview,zoomlev);
   return;
}


//  get current map scale (meters/pixel) at given zoom level and geocoordinates

float netmapscale(int zoomlev, float flat, float flong)
{
   using namespace netmaps;
   float fmpp = champlain_map_source_get_meters_per_pixel(map_source,zoomlev,flat,flong);
   return fmpp;
}


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

//  Respond to mouse clicks on net map image.

void netmap_mousefunc(GtkWidget *widget, GdkEventButton *event, void *)
{
   using namespace netmaps;

   int         mx, my, px, py;
   int         mapww, maphh;
   int         ii, minii;
   int         capturedist = map_dotsize / 2 + 3;                                //  mouse - marker capture distance    20.0
   int         Fusedot = 0;
   char        *location = 0, *country;
   float       flati, flongi, glati, glongi;
   float       dist, mindist;
   static char *ploc = 0;
   static int  downtime;
   char        text[20];
   zdialog     *zd = zd_mapgeotags;
   
   if (! mapview) return;                                                        //  net map not available

   mx = event->x;                                                                //  mouse position in map widget
   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;                                                   //  nearest known location (dot marker)
   glongi = geolocs[ii]->flongi;
   location = 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) Fusedot = 1;                                         //  mouse is within marker dot
   
   if (event->type == GDK_BUTTON_PRESS) {
      downtime = event->time;
      return;
   }

   if (event->type == GDK_BUTTON_RELEASE)                                        //  detect button click
   {                                                                             //  to ignore drags
      if (event->time - downtime > 600) return;

      if (zd)                                                                    //  stuff calling dialog
      {
         if (Fusedot) {                                                          //  click within dot
            zdialog_stuff(zd,"location",location);                               //  use nominal dot location data
            zdialog_stuff(zd,"country",country);
            zdialog_stuff(zd,"lati",glati,"%.5f");                               //  5 decimal places
            zdialog_stuff(zd,"longi",glongi,"%.5f");
         }
         else {
            zdialog_stuff(zd,"lati",flati,"%.5f");                               //  use clicked geocoordinates only
            zdialog_stuff(zd,"longi",flongi,"%.5f");
         }
         zdialog_send_event(zd,"geomap");                                        //  activate calling dialog
         m_viewmode(0,&PFGWM);                                                   //  restore prior view mode            7.10
      }
      
      else if (event->button == 1) {                                             //  left click
         if (Fusedot) find_netmap_images(flati,flongi);                          //  on marker - show corresp. images
         else {
            champlain_view_center_on(mapview,flati,flongi);                      //  zoom-in at clicked location        19.0
            champlain_view_zoom_in(mapview);
            mapww = gtk_widget_get_allocated_width(mapwidget);                   //  move mouse to center
            maphh = gtk_widget_get_allocated_height(mapwidget);
            move_pointer(mapwidget,mapww/2,maphh/2);
            snprintf(text,20,"%.5f %.5f",flati,flongi);                          //  show coordinates                   19.0
            poptext_mouse(text,20,-20,0.1,3);
         }
      }

      else if (event->button == 3)                                               //  right click
         champlain_view_zoom_out(mapview);                                       //  zoom out                           19.0
      
      return;
   }
   
   downtime = 0;                                                                 //  mouse motion
   
   if (location && Fusedot) {
      if (! ploc || ! strmatch(location,ploc)) {                                 //  popup the location name at mouse
         if (strmatch(location,"null"))
            poptext_mouse(country,20,-20,0.1,1);                                 //  20.0
         else poptext_mouse(location,20,-20,0.1,1);
         ploc = location;
      }
   }

   else ploc = 0;
   
   return;
}


//  find images within the marker size, show gallery of images.
//  privat function for netmap_mousefunc(), called when a location is clicked

void find_netmap_images(float flati, float flongi)
{
   using namespace netmaps;

   int         ii, nn = 0;
   int         x1, y1, x2, y2;
   int         capturedist = (map_dotsize + 2) / 2;                              //  mouse - marker capture distance
   float       glati, glongi, grange;
   FILE        *fid;
   xxrec_t     *xxrec;
   
   if (Findexvalid == 0) {
      zmessageACK(Mwin,Bnoindex);                                                //  no image index                     20.0
      return;
   }

   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;
   }
   
   if (gallerymap)                                                               //  show gallery images at location 
   {
      for (ii = 0; ii < Ngallerymap; ii++)                                       //  loop all gallery files
      {
         zmainloop(100);                                                         //  keep GTK alive
         
         glati = gallerymap[ii].flati;                                           //  image geocoordinates
         glongi = gallerymap[ii].flongi;

         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",gallerymap[ii].file);                             //  output matching file
            nn++;
         }
      }
   }
   
   else                                                                          //  show all images at location
   {
      for (ii = 0; ii < Nxxrec; ii++)
      {
         zmainloop(100);                                                         //  keep GTK alive

         xxrec = xxrec_tab[ii];

         glati = xxrec->flati;
         glongi = xxrec->flongi;
         if (glati == 0 && glongi == 0) continue;
         
         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",xxrec->file);                                     //  output matching file
            nn++;
         }
      }
   }
   
   fclose(fid); 

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

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

   return;
}


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

//  Save current map location (center and scale) with a given name,
//    or retrieve a previously saved map location.

namespace netmap_locs_names
{
   zdialog     *zdnetmaploc = 0;
   char        locname[60];
   double      loclati = 0, loclongi = 0;
   int         loczoom = 12;
   char        netmaplocfile[200];
   char        buff[100];
}


//  menu function

void m_netmap_locs(GtkWidget *, cchar *)
{
   using namespace netmap_locs_names;

   int   netmap_locs_dialog_event(zdialog *zd, cchar *event);
   void  netmap_locs_clickfunc(GtkWidget *, int line, int pos, int kbkey);

   zdialog     *zd;
   GtkWidget   *mtext;
   cchar       *pp;
   FILE        *fid;

   F1_help_topic = "net locs";

   m_viewmode(0,"M");                                                            //  19.0

   snprintf(netmaplocfile,200,"%s/netmap_locations",get_zhomedir());             //  net map locations file

/***
       ________________________________
      |      Net Map Locations         |
      | ______________________________ |
      ||                              ||
      || map location name 1          ||
      || long map location name 2     ||     scrolling window
      || map location name 3          ||
      ||  ...                         ||
      ||______________________________||
      |                                |
      | map location: [______________] |     text entry for location name
      |                                |
      |          [add] [delete] [done] |
      |________________________________|
      

      [location]     empty until filled-in or a location from the list is clicked
      [add]          current location is added to list or replaced 
      [delete]       current location is deleted from list
      
      location position and scale is from current map location
      location list is kept in alphabetic order
      
***/

   if (zdnetmaploc) return;                                                      //  already active

   zd = zdialog_new(E2X("Net Map Locations"),Mwin,Badd,Bdelete,Bdone,null);
   zdnetmaploc = zd;
   zdialog_add_widget(zd,"frame","frlocs","dialog",0,"expand");
   zdialog_add_widget(zd,"scrwin","scrlocs","frlocs",0,"expand");
   zdialog_add_widget(zd,"text","mtext","scrlocs");
   zdialog_add_widget(zd,"hbox","hbvn","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labvn","hbvn",E2X("map location:"),"space=3");
   zdialog_add_widget(zd,"zentry","locname","hbvn","","space=3");
   
   zdialog_resize(zd,200,300);
   zdialog_run(zd,netmap_locs_dialog_event,"mouse");         

   mtext = zdialog_widget(zd,"mtext");                                           //  map location list in dialog
   textwidget_clear(mtext);

   fid = fopen(netmaplocfile,"r");                                               //  map location list file
   if (fid) {
      while (true) {
         pp = fgets_trim(buff,100,fid,1);                                        //  read location | lati | longi | zoom
         if (! pp) break;
         pp = strField(buff,'|',1);                                              //  isolate location
         if (! pp) continue;
         if (strlen(pp) < 2) continue;
         textwidget_append(mtext,0,"%s \n",pp);                                  //  write into dialog list
      }
      fclose(fid);
   }

   textwidget_set_eventfunc(mtext,netmap_locs_clickfunc);                        //  set mouse/KB event function
   return;
}


//  dialog event and completion callback function

int netmap_locs_dialog_event(zdialog *zd, cchar *event)
{
   using namespace netmaps;
   using namespace netmap_locs_names;
   
   int         ff, nn;
   cchar       *pp, *pp2;
   GtkWidget   *mtext;
   FILE        *fidr;
   
   if (! zd->zstat) return 1;                                                    //  wait for completion
   
   if (zd->zstat == 1)                                                           //  [add] new map location record
   {
      zd->zstat = 0;                                                             //  keep dialog active

      zdialog_fetch(zd,"locname",locname,60);
      if (strTrim2(locname) < 2) {
         zmessageACK(Mwin,E2X("supply a reasonable name"));
         return 1;
      }
      
      loclati = champlain_view_get_center_latitude(mapview);                     //  get current map location
      loclongi = champlain_view_get_center_longitude(mapview);
      loczoom = champlain_view_get_zoom_level(mapview);
      
      snprintf(buff,100,"%s|%.4f|%.4f|%d",locname,loclati,loclongi,loczoom);     //  prepare new location rec.

      linedit_open(netmaplocfile);
      
      ff = 0;

      while (true)                                                               //  read next location record
      {
         pp = linedit_get();
         if (! pp) break;
         nn = strcasecmp(locname,pp);                                            //  compare new location with location record
         if (! ff && nn < 0) {                                                   //  new location < location record
            linedit_put(buff);
            ff = 1;
         }
         linedit_put(pp);                                                        //  write location record
      }
      
      if (! ff) linedit_put(buff);                                               //  insert new location last

      linedit_close();
      goto update_dialog;
   }

   if (zd->zstat == 2)                                                           //  [delete] selected map location record
   {
      zd->zstat = 0;                                                             //  keep dialog active

      zdialog_fetch(zd,"locname",locname,60);
      
      linedit_open(netmaplocfile);
      
      while (true)                                                               //  read next location record
      {
         pp = linedit_get();
         if (! pp) break;
         pp2 = strField(pp,'|',1);                                               //  get location name
         if (! pp2) continue;
         if (strmatch(locname,pp2)) continue;                                    //  omit deleted location
         linedit_put(pp);                                                        //  write location record
      }
      
      linedit_close();
      goto update_dialog;
   }

   zdialog_destroy(zd);                                                          //  [done] or [x]
   zdnetmaploc = 0;
   return 1;

update_dialog:

   mtext = zdialog_widget(zd,"mtext");                                           //  map location name list in dialog
   textwidget_clear(mtext);                                                      //  clear list

   fidr = fopen(netmaplocfile,"r");                                              //  update dialog list from file
   if (! fidr) return 1;

   while (true) {
      pp = fgets_trim(buff,100,fidr,1);                                          //  read location | lati | longi | zoom
      if (! pp) break;
      pp = strField(buff,'|',1);                                                 //  isolate location
      if (! pp) continue;
      if (strlen(pp) < 2) continue;
      textwidget_append2(mtext,0,"%s \n",pp);                                    //  write into dialog list
   }
   fclose(fidr);

   return 1;
}      


//  get clicked location name and set corresponding map location and zoom level

void netmap_locs_clickfunc(GtkWidget *widget, int line, int pos, int kbkey)
{
   using namespace netmap_locs_names;

   cchar    *pp1, *pp2;
   FILE     *fidr;
   zdialog  *zd = zdnetmaploc;
   
   if (! zd) return;
   
   if (kbkey == GDK_KEY_F1) {                                                    //  key F1 pressed, show help
      showz_docfile(Mwin,"userguide",F1_help_topic);
      return;
   }

   pp1 = textwidget_line(widget,line,1);                                         //  get clicked line, highlight
   if (! pp1 || ! *pp1) return;
   textwidget_highlight_line(widget,line);

   strTrim2(locname,pp1);
   zdialog_stuff(zd,"locname",locname);

   fidr = fopen(netmaplocfile,"r");                                              //  open/read netmap locs file 
   if (! fidr) {
      zmessageACK(Mwin,strerror(errno));
      return;
   }
   
   while (true)                                                                  //  read next location record
   {
      pp2 = fgets_trim(buff,100,fidr);
      if (! pp2) break;
      pp2 = strField(buff,'|',1);
      if (! pp2) continue;
      if (strmatch(locname,pp2)) break;                                          //  found matching record
   }
   
   fclose(fidr);
   if (! pp2 || ! strmatch(locname,pp2)) goto notfound;

   loclati = loclongi = loczoom = 0;

   pp1 = strField(buff,'|',2);                                                   //  get map location data from record
   if (! pp1) goto baddata;
   loclati = atof(pp1);
   if (loclati <= -90 || loclati >= +90) goto baddata;

   pp1 = strField(buff,'|',3);
   if (! pp1) goto baddata;
   loclongi = atof(pp1);
   if (loclongi <= -180 || loclongi >= +180) goto baddata;

   pp1 = strField(buff,'|',4);
   if (! pp1) goto baddata;
   loczoom = atoi(pp1);
   if (loczoom < 1 || loczoom > 20) goto baddata;
   
   netmap_zoomto(loclati,loclongi,loczoom);                                      //  set this map location
   return;

notfound:
   printz("net map location not found: %s \n",locname);
   return;
   
baddata:
   printz("net map location invalid: %s %.4f %.4f %d \n",
                  locname,loclati,loclongi,loczoom);
   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 100 keynames may be requested per call
//  returns: 0 = OK, +N = error

int exif_get(cchar *file, cchar **keys, char **kdata, int nkeys)
{
   char        *pp, geostring[20];
   char        *inputs[120], *outputs[100];                                      //  higher limits
   int         cc, ii, jj, err;
   float       geofloat;
   uint        ucc;
   
   if (nkeys < 1 || nkeys > 99) 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
   jj = 4;

   for (ii = 0; ii < nkeys; ii++)                                                //  build exiftool inputs
   {
      cc = strlen(keys[ii]);                                                     //  -keyname
      if (! cc) {
         printz("exif_get() null key \n");
         return 1;
      }
      inputs[jj] = (char *) zmalloc(cc+2);
      inputs[jj][0] = '-';
      strcpy(inputs[jj]+1,keys[ii]);
      if (strmatchcase(keys[ii],"location"))                                     //  make "location" = "city"
         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;

      if (utf8_check(pp)) {                                                      //  detect invalid UTF-8 string        20.0
         if (strlen(pp) > 40) pp[40] = 0;
         printz("invalid UTF-8 metadata: %s \n  %s \n",pp,file);
         zfree(pp);
         continue;
      }

      for (jj = 0; jj < nkeys; jj++)
      {
         if (strmatchcase(keys[jj],"location")) {                                //  "location" comes back "city"
            if (! strmatchcaseN(pp,"city",4)) continue;
            if (strlen(pp) > 6) kdata[jj] = zstrdup(pp+6);
         }
         else if (strmatchN(keys[jj],"GPSLatitude",11)) {                        //  round to 5 decimal places
            if (! strmatchN(pp,"GPSLatitude",11)) continue;
            err = convSF(pp+13,geofloat);
            if (err > 1) continue;
            snprintf(geostring,20,"%.5f",geofloat);
            kdata[jj] = zstrdup(geostring);
         }
         else if (strmatchN(keys[jj],"GPSLongitude",12)) {                       //  round to 5 decimal places
            if (! strmatchN(pp,"GPSLongitude",12)) continue;
            err = convSF(pp+14,geofloat);
            if (err > 1) continue;
            snprintf(geostring,20,"%.5f",geofloat);
            kdata[jj] = zstrdup(geostring);
         }
         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)
//
//  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, err;
   char     *inputs[40];

   if (nkeys < 1 || nkeys > 30) zappcrash("exif_put nkeys: %d",nkeys);           //  higher limit

   err = access(file,W_OK);                                                      //  test file can be written by me     19.1
   if (err) {
      printz("%s: %s \n",Bnowriteperm,file);
      return 1;
   }

   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
   {
      if (! kdata[ii]) continue;                                                 //  skip missing data
      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"
         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: added keys[] to be replaced with new values kdata[]
//  exiftool -m -tagsfromfile file1 -all -xmp -iptc -icc_profile 
//                 [-keyname=newvalue ...]  file2 -overwrite_original

int exif_copy(cchar *file1, cchar *file2, cchar **keys, cchar **kdata, int nkeys)
{
   char     *inputs[40];                                                         //  revised
   int      err, cc, ii, jj, nfixed;

   if (nkeys > 30) zappcrash("exif_copy() nkeys %d",nkeys);                      //  higher limit

   err = access(file2,W_OK);                                                     //  test file can be written by me     19.1
   if (err) {
      printz("%s: %s \n",Bnowriteperm,file2);
      return 1;
   }

   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 *) "-iptc";                                                 //  -iptc
   inputs[6] = (char *) "-icc_profile";                                          //  -icc_profile

   nfixed = jj = 7;                                                              //  no. fixed inputs

   for (int ii = 0; ii < nkeys; ii++)                                            //  -keyname=keyvalue
   {
      if (! kdata[ii]) continue;                                                 //  skip missing data
      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 = nfixed; ii < nfixed+nkeys; 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)

   Returned: list of pointers to resulting output records.
   There are <= Nrecs outputs corresponding to keyword inputs.
   These are formatted "keyword: value" (text string).
   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).
   
   There are two servers which can run parallel if called by two threads.
   
   The exiftool perl process has a significant memory leak that can reach
   gigabytes if Fotoxx image index function processes 100K files or more. 
   Solution: exit perl process after 1000 calls and start over with next call.

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

#include <sys/prctl.h>

namespace exif_server_names                                                      //  19.0
{
   char     inputfile1[200];
   char     inputfile2[200];
}


int exif_server(int Nrecs, char **inputs, char **outputs)
{
   using namespace exif_server_names;

   int exif_server1(int Nrecs, char **inputs, char **outputs);                   //  2 parallel server processes
   int exif_server2(int Nrecs, char **inputs, char **outputs);

   static int  fcf = 1, busy1 = 0, busy2 = 0;
   double      time0, time1;
   int         err;
   
   if (! Nrecs) {
      printz("stop EXIF servers \n");
      zmainsleep(0.1);
      exif_server1(0,0,0);
      exif_server2(0,0,0);
      return 0;
   }

   if (fcf)                                                                      //  first call                         19.0
   {
      fcf = 0;
      snprintf(inputfile1,200,"%s/exiftool_input1",temp_folder);                 //  exiftool input files
      snprintf(inputfile2,200,"%s/exiftool_input2",temp_folder); 
   }

   time0 = get_seconds();

   while (true)
   {
      if (resource_lock(busy1)) {                                                //  use resource locks
         err = exif_server1(Nrecs,inputs,outputs);
         resource_unlock(busy1);
         return err;
      }
      
      if (resource_lock(busy2)) {
         err = exif_server2(Nrecs,inputs,outputs);
         resource_unlock(busy2);
         return err;
      }
      
      time1 = get_seconds();
      if (time1 - time0 > 2) break;
      zsleep(0.001);
   }
   
   zexit("exiftool (perl) is not responding");
   return 0;                                                                     //  not executed
}


int exif_server1(int Nrecs, char **inputs, char **outputs)                       //  exiftool process 1
{
   using namespace exif_server_names;

   char         *inputfile = inputfile1;                                         //  only difference in 2 servers       NOTE

   static int   fcf = 1;                                                         //  first call flag
   static int   Ncalls = 0;
   static FILE  *fid1 = 0, *fid2 = 0;                                            //  exiftool input, output files
   int          ii, cc, err;
   char         *pp, command[200];
   char         outrec[exif_maxcc];                                              //  single output record

   if (! Nrecs)                                                                  //  kill exiftool if requested
   {
      if (fcf) return 0;                                                         //  never started
      fid1 = fopen(inputfile,"a");
      if (fid1) {
         fprintf(fid1,"-stay_open\nFalse\n");                                    //  tell it to exit
         fclose(fid1);
      }

      if (fid2) pclose(fid2);
      fcf = 1;                                                                   //  restart if called again
      return 0;
   }

   if (fcf)                                                                      //  first call only
   {
      fid1 = fopen(inputfile,"w");                                               //  start exiftool input file
      if (! fid1) {
         zmessageACK(Mwin,"exif_server: %s \n",strerror(errno));
         return 1;
      }

      snprintf(command,200,"exiftool -stay_open True -@ %s; exit",inputfile);    //  exit shell when exit exiftool

      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;
      Ncalls = 0;
   }
   
   Ncalls++;                                                                     //  count calls

   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
   }

   return 0;
}


int exif_server2(int Nrecs, char **inputs, char **outputs)                       //  exiftool process 2
{
   using namespace exif_server_names;

   char         *inputfile = inputfile2;                                         //  only difference in 2 servers       NOTE

   static int   fcf = 1;                                                         //  first call flag
   static int   Ncalls = 0;
   static FILE  *fid1 = 0, *fid2 = 0;                                            //  exiftool input, output files
   int          ii, cc, err;
   char         *pp, command[200];
   char         outrec[exif_maxcc];                                              //  single output record

   if (! Nrecs)                                                                  //  kill exiftool if requested
   {
      if (fcf) return 0;                                                         //  never started
      fid1 = fopen(inputfile,"a");
      if (fid1) {
         fprintf(fid1,"-stay_open\nFalse\n");                                    //  tell it to exit
         fclose(fid1);
      }

      if (fid2) pclose(fid2);
      fcf = 1;                                                                   //  restart if called again
      return 0;
   }

   if (fcf)                                                                      //  first call only
   {
      fid1 = fopen(inputfile,"w");                                               //  start exiftool input file
      if (! fid1) {
         zmessageACK(Mwin,"exif_server: %s \n",strerror(errno));
         return 1;
      }

      snprintf(command,200,"exiftool -stay_open True -@ %s; exit",inputfile);    //  exit shell when exit exiftool

      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;
      Ncalls = 0;
   }
   
   Ncalls++;                                                                     //  count calls

   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
   }

   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 file on disk
   and update the image index memory table (xxrec table).
*********************************************************************************/

//  Get the image index record for the given image file.
//  Returns pointer to xxrec or null if not found.
//  Returned xxrec fields are NOT subjects for zfree()
//  If file is not indexed or modified since index,                              //  19.13
//    a dummy xxrec in static memory is returned.
//      (only good until next call to get_xxrec()).

xxrec_t * get_xxrec(cchar *file)
{
   int      ii, jj, kk, rkk, last, err;
   STATB    statb;
   char     fdate[16], *RP;

   static xxrec_t  xxrec;
   
   if (! file || *file != '/') return 0;
   
   RP = f_realpath(file);                                                        //  use real path                      20.0
   if (! RP) return 0;

   err = stat(RP,&statb);
   if (err) goto ret0;

   if (image_file_type(RP) > VIDEO) goto ret0;                                   //  not indexable                      19.10

   if (! Findexvalid) goto noxxrec;                                              //  index not valid                    19.10
   if (! Nxxrec) goto noxxrec;                                                   //  index empty                        19.10

   ii = Nxxrec / 2;                                                              //  next table entry to search
   jj = (ii + 1) / 2;                                                            //  next increment
   last = Nxxrec - 1;                                                            //  last entry
   rkk = 0;

   while (true)                                                                  //  binary search 
   {
      kk = strcmp(xxrec_tab[ii]->file,RP);                                       //  compare table entry to file2

      if (kk > 0) {
         ii -= jj;                                                               //  too high, go back in table
         if (ii < 0) break;
      }

      else if (kk < 0) {
         ii += jj;                                                               //  too low, go forward in table
         if (ii > last) break;
      }

      else {
         compact_time(statb.st_mtime,fdate);                                     //  index record found
         if (! strmatch(fdate,xxrec_tab[ii]->fdate)) break;                      //  check mod time matches             19.13
         return xxrec_tab[ii];
      }

      jj = jj / 2;                                                               //  reduce increment

      if (jj == 0) {
         jj = 1;                                                                 //  step by 1 element
         if (! rkk) rkk = kk;                                                    //  save last direction
         else {
            if (rkk > 0 && kk < 0) break;                                        //  if direction change, fail
            if (rkk < 0 && kk > 0) break;
         }
      }
   }
   
noxxrec:                                                                         //  file not in index or index stale   19.13
   memset(&xxrec,0,sizeof(xxrec_t));                                             //  build dummy xxrec
   xxrec.file = RP;
   compact_time(statb.st_mtime,xxrec.fdate);
   strcpy(xxrec.pdate,"null");
   xxrec.fsize = statb.st_size;
   xxrec.tags = zstrdup("null");
   xxrec.capt = zstrdup("null");
   xxrec.comms = zstrdup("null");
   xxrec.location = zstrdup("null");
   xxrec.country = zstrdup("null");
   xxrec.xmeta = zstrdup("null");
   return &xxrec;

ret0:
   zfree(RP);
   return 0;
}


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

//  Add or update the image index record for the given image file.
//  If xxrec is null, delete the table entry for the file.
//  Append new index record data to the image index file on disk.
//  (previous record, if any, is now superseded but still present)
//  Return 0 if success, +N if error (diagnosed).

int put_xxrec(xxrec_t *xxrec, cchar *file)
{
   int         ii, iix, nn, err;
   int         Fadd, Freplace, Fdelete;
   char        *RP;
   xxrec_t     *xxrec_new = 0;
   FILE        *fid;
   STATB       statb;
   
   if (! file || *file != '/') {
      zmessageACK(Mwin,"put_xxrec() file: %s",file);
      return 0;
   }
   
   if (! Findexvalid) return 1;                                                  //  image index not valid
   
   RP = f_realpath(file);                                                        //  use real path                      20.0
   if (! RP) xxrec = null;

   if (RP) {
      err = stat(RP,&statb);                                                     //  check for regular file
      if (err || ! S_ISREG(statb.st_mode)) xxrec = null;                         //  if not, set xxrec = null
   }

   if (xxrec)                                                                    //  caller xxrec with valid file
   {
      xxrec_new = (xxrec_t *) zmalloc(sizeof(xxrec_t));                          //  make new xxrec with data from caller
      memset(xxrec_new,0,sizeof(xxrec_t));

      xxrec_new->file = RP;
   
      compact_time(statb.st_mtime,xxrec_new->fdate);                             //  refresh file mod date/time
      xxrec_new->fsize = statb.st_size;                                          //  refresh file size                  19.0
   
      if (xxrec->pdate[0]) strcpy(xxrec_new->pdate,xxrec->pdate);
      else strcpy(xxrec_new->pdate,"null");

      xxrec_new->ww = xxrec->ww;                                                 //  image pixel dimensions             19.0
      xxrec_new->hh = xxrec->hh;

      if (xxrec->rating[0]) strcpy(xxrec_new->rating,xxrec->rating);
      else strcpy(xxrec_new->rating,"0");
      
      if (xxrec->tags) xxrec_new->tags = zstrdup(xxrec->tags);
      else xxrec_new->tags = zstrdup("null");

      if (xxrec->capt) xxrec_new->capt = zstrdup(xxrec->capt);
      else xxrec_new->capt = zstrdup("null");

      if (xxrec->comms) xxrec_new->comms = zstrdup(xxrec->comms);
      else xxrec_new->comms = zstrdup("null");

      if (xxrec->location) xxrec_new->location = zstrdup(xxrec->location);       //  19.13
      else xxrec_new->location = zstrdup("null");

      if (xxrec->country) xxrec_new->country = zstrdup(xxrec->country);          //  19.13
      else xxrec_new->country = zstrdup("null");

      xxrec_new->flati = xxrec->flati;
      xxrec_new->flongi = xxrec->flongi;

      if (xxrec->xmeta) xxrec_new->xmeta = zstrdup(xxrec->xmeta);                //  19.13
      else xxrec_new->xmeta = zstrdup("null");
   }

   nn = -1;                                                                      //  empty xxrec_tab[] >> not found

   if (RP) {
      for (iix = 0; iix < Nxxrec; iix++) {                                       //  find file in xxrec_tab[]           19.0
         nn = strcmp(RP,xxrec_tab[iix]->file);
         if (nn <= 0) break;                                                     //  file goes before or at posn iix
      }                                                                          //    = posn to add/replace/delete
   }
   
   Fadd = Freplace = Fdelete = 0;
   if (nn != 0 && ! xxrec) return 0;                                             //  nothing to do
   if (nn != 0 && xxrec) Fadd = 1;                                               //  add new xxrec
   if (nn == 0 && ! xxrec) Fdelete = 1;                                          //  delete existing xxrec
   if (nn == 0 && xxrec) Freplace = 1;                                           //  replace existing xxrec
   
   if (Fdelete)
   {
      if (RP) zfree(RP);
      zfree(xxrec_tab[iix]->file);                                               //  delete existing entry
      zfree(xxrec_tab[iix]->tags);
      zfree(xxrec_tab[iix]->capt);
      zfree(xxrec_tab[iix]->comms);
      zfree(xxrec_tab[iix]->location); 
      zfree(xxrec_tab[iix]->country);
      zfree(xxrec_tab[iix]->xmeta);
      zfree(xxrec_tab[iix]);
      Nxxrec--;
      for (ii = iix; ii < Nxxrec; ii++)                                          //  pack down
         xxrec_tab[ii] = xxrec_tab[ii+1];
      return 0;
   }

   if (Fadd)
   {
      if (Nxxrec == maximages) {
         zmessageACK(Mwin,"exceed %d max files, cannot continue",maximages);
         quitxx();
      }
      
      for (ii = Nxxrec; ii > iix; ii--)                                          //  make empty slot
         xxrec_tab[ii] = xxrec_tab[ii-1];
      xxrec_tab[iix] = xxrec_new;                                                //  insert new entry
      Nxxrec++;
   }

   if (Freplace) 
   {
      zfree(xxrec_tab[iix]->file);                                               //  replace old entry                  20.0
      zfree(xxrec_tab[iix]->tags);
      zfree(xxrec_tab[iix]->capt);
      zfree(xxrec_tab[iix]->comms);
      zfree(xxrec_tab[iix]->location); 
      zfree(xxrec_tab[iix]->country);
      zfree(xxrec_tab[iix]->xmeta); 
      zfree(xxrec_tab[iix]);
      xxrec_tab[iix] = xxrec_new;                                                //  replace with new entry
   }

   fid = fopen(index_file,"a");                                                  //  append new record to image index file
   if (! fid) goto file_err;

   nn = fprintf(fid,"file: %s\n",RP);                                            //  file real path name
   if (! nn) goto file_err;
   
   nn = fprintf(fid,"data: %s %s %s %d %d %d\n",                                 //  file date, photo date, rating,
                  xxrec_new->fdate, xxrec_new->pdate, xxrec_new->rating,         //    width, height, file size
                  xxrec_new->ww, xxrec_new->hh, xxrec_new->fsize);
   if (! nn) goto file_err;

   nn = fprintf(fid,"tags: %s\n",xxrec_new->tags);                               //  tags record
   if (! nn) goto file_err;

   nn = fprintf(fid,"capt: %s\n",xxrec_new->capt);                               //  caption record
   if (! nn) goto file_err;

   nn = fprintf(fid,"comms: %s\n",xxrec_new->comms);                             //  comments record
   if (! nn) goto file_err;

   nn = fprintf(fid,"gtags: %s^ %s^ %.4f^ %.4f\n",
         xxrec_new->location, xxrec_new->country, 
         xxrec_new->flati, xxrec_new->flongi);
   if (! nn) goto file_err;
   
   nn = fprintf(fid,"xmeta: %s\n",xxrec_new->xmeta);
   if (! nn) goto file_err;

   nn = fprintf(fid,"END\n");                                                    //  EOL
   if (! nn) goto file_err;

   err = fclose(fid);
   if (err) goto file_err;

   return 0;

file_err:
   zmessageACK(Mwin,"image index write error\n %s",strerror(errno));
   if (fid) fclose(fid);
   fid = 0;
   return 3;
}


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

//  Read image index files sequentially, return one index record per call.
//  Set ftf = 1 for first read, will be reset to 0.
//  Returns xxrec or null for EOF or error.
//  Returned xxrec_t and its allocated pointers are subject to zfree().
//  Used by index_rebuild() function.

xxrec_t * read_xxrec_seq(int &ftf)
{
   xxrec_t        *xxrec = 0;
   static FILE    *fid = 0;
   static char    buff[indexrecl];
   cchar          *pp, *pp2;
   float          flati, flongi;
   char           fdate[16], pdate[16], rating[4];
   int            nn, ww, hh, fsize;

   if (ftf)                                                                      //  initial call
   {
      ftf = 0;
      fid = fopen(index_file,"r");
      if (! fid) return 0;                                                       //  no index file ?
      *buff = 0;                                                                 //  insure no leftover data
   }

   while (true)                                                                  //  read to next "file: " record
   {
      pp = fgets_trim(buff,indexrecl,fid);
      if (! pp) {
         fclose(fid);                                                            //  EOF
         return 0;
      }
      if (strmatchN(pp,"file: ",6)) break;
   }

   xxrec = (xxrec_t *) zmalloc(sizeof(xxrec_t));                                 //  allocate returned xxrec
   memset(xxrec,0,sizeof(xxrec_t));

   xxrec->file = zstrdup(buff+6);                                                //  image file name

   while (true)                                                                  //  get recs following "file" record
   {
      pp = fgets_trim(buff,indexrecl,fid);
      if (! pp) break;
      
      if (strmatchN(pp,"END",3)) break;                                          //  end of recs for this file 

      else if (strmatchN(pp,"data: ",6)) {                                       //  new format                         19.0
         nn = sscanf(pp+6,"%15s %15s %1s %d %d %d", 
                           fdate, pdate, rating, &ww, &hh, &fsize);              //  file date, photo date, rating,
         if (nn == 6) {                                                          //    width, height, file size
            strncpy0(xxrec->fdate,fdate,16);
            if (strmatch(pdate,"undated")) strcpy(xxrec->pdate,"");
            else strncpy0(xxrec->pdate,pdate,16);
            strncpy0(xxrec->rating,rating,2);
            xxrec->ww = ww;
            xxrec->hh = hh;
            xxrec->fsize = fsize;
         }
      }

      else if (strmatchN(pp,"tags: ",6))                                         //  tags
         xxrec->tags = zstrdup(pp+6);

      else if (strmatchN(pp,"capt: ",6))                                         //  caption
         xxrec->capt = zstrdup(pp+6);

      else if (strmatchN(pp,"comms: ",7))                                        //  comments
         xxrec->comms = zstrdup(pp+7);

      else if (strmatchN(pp,"gtags: ",7)) {                                      //  geotags
         pp += 7;
         pp2 = strField(pp,"^",1);
         if (pp2) xxrec->location = zstrdup(pp2);
         else xxrec->location = zstrdup("");
         pp2 = strField(pp,"^",2);
         if (pp2) xxrec->country = zstrdup(pp2);
         else xxrec->country = zstrdup("");
         flati = flongi = 999;
         pp2 = strField(pp,"^",3);
         if (pp2) flati = atof(pp2);
         pp2 = strField(pp,"^",4);
         if (pp2) flongi = atof(pp2);
         if (flati < -90.0 || flati > 90.0) flati = flongi = 0;
         if (flongi < -180.0 || flongi > 180.0) flati = flongi = 0;
         xxrec->flati = flati;
         xxrec->flongi = flongi;
      }
      
      else if (strmatchN(pp,"xmeta: ",7))                                        //  indexed metadata 
         xxrec->xmeta = zstrdup(pp+7);
   }

   if (! xxrec->fdate[0])                                                        //  supply defaults for missing items
      strcpy(xxrec->fdate,"");

   if (! xxrec->pdate[0])
      strcpy(xxrec->pdate,"");

   if (! xxrec->rating[0])
      strcpy(xxrec->rating,"0");

   if (! xxrec->tags)
      xxrec->tags = zstrdup("");

   if (! xxrec->capt)
      xxrec->capt = zstrdup("");

   if (! xxrec->comms)
      xxrec->comms = zstrdup("");

   if (! xxrec->location)
      xxrec->location = zstrdup("");

   if (! xxrec->country)
      xxrec->country = zstrdup("");
   
   if (! xxrec->xmeta)
      xxrec->xmeta = zstrdup("");

   return xxrec;
}


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

//  Write the image index files sequentially, 1 record per call
//  Set ftf = 1 for first call, will be reset to 0.
//  Set xxrec = 0 to close file after last write.
//  Returns 0 if OK, otherwise +N (diagnosed).
//  Used by index_rebuild() function.

int write_xxrec_seq(xxrec_t *xxrec, int &ftf)                                    //  file need not exist                19.10
{
   static FILE    *fid = 0;
   int            err, nn;
   char           pdate[16];

   if (ftf)                                                                      //  first call
   {
      ftf = 0;
      fid = fopen(index_file,"w");
      if (! fid) goto file_err;
   }

   if (! xxrec) {                                                                //  EOF call
      if (fid) {
         err = fclose(fid);
         fid = 0;
         if (err) goto file_err;
      }
      return 0;
   }
   
   nn = fprintf(fid,"file: %s\n",xxrec->file);                                   //  output: filename record
   if (! nn) goto file_err;
   
   strncpy0(pdate,xxrec->pdate,16);                                              //  20.0
   if (! *pdate) strcpy(pdate,"undated");

   nn = fprintf(fid,"data: %s %s %s %d %d %d\n",                                 //  file date, photo date, rating,     19.0
                  xxrec->fdate, pdate, xxrec->rating,                            //    width, height, file size
                  xxrec->ww, xxrec->hh, xxrec->fsize);
   if (! nn) goto file_err;

   if (xxrec->tags)
      nn = fprintf(fid,"tags: %s\n",xxrec->tags);                                //  tags: aaaaa, bbbbb, ...
   else nn = fprintf(fid,"tags: \n");
   if (! nn) goto file_err;

   if (xxrec->capt)
      nn = fprintf(fid,"capt: %s\n",xxrec->capt);                                //  caption: text
   else nn = fprintf(fid,"capt: \n");
   if (! nn) goto file_err;

   if (xxrec->comms)
      nn = fprintf(fid,"comms: %s\n",xxrec->comms);                              //  comments: text
   else nn = fprintf(fid,"comms: \n");
   if (! nn) goto file_err;

   nn = fprintf(fid,"gtags: %s^ %s^ %.4f^ %.4f\n",
         xxrec->location, xxrec->country, xxrec->flati, xxrec->flongi);
   if (! nn) goto file_err;

   if (xxrec->xmeta)
      nn = fprintf(fid,"xmeta: %s\n",xxrec->xmeta);                              //  xmeta: name1^ name2^ ...
   else nn = fprintf(fid,"xmeta: \n");
   if (! nn) goto file_err;

   nn = fprintf(fid,"END\n");                                                    //  EOL
   if (! nn) goto file_err;

   return 0;

file_err:
   zmessageACK(Mwin,"image index write error\n %s",strerror(errno));
   if (fid) fclose(fid);
   fid = 0;
   quitxx();                                                                     //  unconditional exit                 20.05
   return 2;
}


