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

   Fotocx - edit photos and manage collections

   Copyright 2007-2026 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.

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

   Fotocx image editor - image metadata functions.

   View and edit metadata
   ----------------------
   int metatagtype            classify tag: xxrec_tab[], extra indexed, not indexed
   select_meta_tags           dialog to select metadata tags
   m_meta_view_main           metadata short report
   m_meta_view_all            report all metadata
   meta_edit_text             dialog for editing large metadata text
   m_meta_edit_main           primary edit main metadata dialog
   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_copy                copy metadata from one image file to another
   m_meta_fix                 fix file with malformed metadata
   m_meta_manage_keywords     define keywords for image searching
   m_meta_choose_caps         choose metadata tags for image captions
   meta_show_caps             show captions on current image
   meta_popup_title           show title & description in popup window
   m_meta_toggle_caps         toggle display of image captions on/off

   m_batch_keywords           batch add and delete keywords for selected image files
   m_batch_rename_keywords    convert keyword names for all image files
   m_batch_purge_keywords     purge unwanted keywords from all image files
   m_batch_photo_date_time    change or shift photo date/time
   m_batch_change_meta        add/change or delete metadata for selected files
   m_batch_report_meta        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_meta_tags                find images by metadata tag value
   m_autosearch               search function for use with scripts
   m_search_images            find images using any metadata and/or file names

   checkDT                    validate date [time] input
   add_keyword                add keyword to a keyword list
   del_keyword                remove keyword from a keyword list
   add_recenkeyword           add keyword to recent keywords list, remove oldest if needed
   load_defkeywords           load defined keywords list from keywords file and image index
   save_defkeywords           save defined keywords list to keywords file
   find_defkeyword            check if given keyword is in defined keywords list
   add_defkeyword             add new keyword to defined keywords list or change category
   del_defkeyword             remove keyword from defined keywords list
   del_defcatg                remove category from defined categories list (if no keywords assigned)
   defkeywords_stuff          stuff defined keywords into dialog text widget
   defcats_stuff              stuff defined categories into dialog combobox widget

   glocs_compare              compare two geolocation records
   load_imagelocs             load geolocations table from image files
   load_worldlocs             load geolocations table from world cities file
   find_location              find geolocations from image data and world locatioins table
   choose_location            choose a location/country given leading substring(s)
   nerest_loc                 find nearest known location name for given geocoordinates

   put_imagelocs              put new location data in geolocations table
   get_gps_data               validate and return GPS coordinates as type float
   earth_distance             compute km distance between two earth coordinates
   get_gallerymap             get map coordinates for current gallery files

   Geotag mapping (internet world map)
   -----------------------------------
   m_worldmap                 initialize internet world map
   paint_map_markers          paint map markers where images are located
   m_map_regions              add custom map regions, goto region
   m_map_location             input a (partial) location name, goto map location
   m_set_map_markers          show map markers for all images or current gallery
   m_map_zoomin               zoom map in on an image location
   map_zoomto                 callable with input geocoordinates and zoom level
   mapscale                   get map scale at zoom level
   map_mousefunc              respond to clicks on map
   find_map_images            find images at map marker

   metadata store and retrieve
   ---------------------------
   meta_get                   get image file metadata from list of tags
   meta_getN                  same for multiple files, using multiple threads
   meta_put                   update image metadata from list of tags and data
   meta_copy                  copy metadata from file to file, with revisions

   Image index functions
   ---------------------
   file_to_xxrec              update xxrec_tab[] record from file metadata
   xxrec_index                get xxrec_tab[] index for given image file
   get_xxrec                  get image xxrec_tab[] for image file
   read_xxrec_seq             read xxrec_tab[] records sequentially, one per call
   write_xxrec_seq            write xxrec_tab[] records sequentially

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

#define EX extern                                                                      //  enable extern declarations

#include <champlain-gtk/champlain-gtk.h>
#include "fotocx.h"                                                                    //  (variables in fotocx.h are refs)

using namespace zfuncs;

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

int   checkDT(ch *datetime);                                                           //  validate date [time] input
int   add_keyword(ch *keyword, ch *keywordlist, int maxcc);                            //  add keyword if unique and enough space
int   del_keyword(ch *keyword, ch *keywordlist);                                       //  remove keyword from keyword list
int   add_recenkeyword(ch *keyword);                                                   //  add to recent keywords, keep recent
void  load_defkeywords(int force);                                                     //  load defined_keywords from index data
void  save_defkeywords();                                                              //  defined_keywords[] >> file
int   find_defkeyword(ch *keyword);                                                    //  find keyword in defined_keywords[]
int   add_defkeyword(ch *catg, ch *keyword);                                           //  add keyword to defined_keywords[]
int   del_defkeyword(ch *keyword);                                                     //  remove keyword from defined_keywords[]
int   del_defcatg(ch *catg);                                                           //  remove category from defined categories[]
void  defkeywords_stuff(zdialog *zd, ch *catg);                                        //  defined_keywords[] >> zd defkeywords
void  defcats_stuff(zdialog *zd);                                                      //  defined categories >> " widget defcats

int   glocs_compare(ch *rec1, ch *rec2);                                               //  compare geocoordinate records
int   load_imagelocs();                                                                //  load image geolocations table
int   load_worldlocs();                                                                //  load cities geolocations table
int   find_location(zdialog *zd);                                                      //  get geolocation from image and world data
float nearest_loc(float flati, float flongi, int &iim, int &iic);                      //  find nearest loc, image or world city

int   put_imagelocs(zdialog *zd);                                                      //  Update geolocations table in memory
int   get_gps_data(ch *gps_data, float &flat, float &flong);                           //  convert and validate GPS data
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  paint_map_markers();                                                             //  paint markers for image locations on map
void  map_zoomto(float flati, float flongi, int zoomlev);                              //  zoom map to location and zoom level
float mapscale(int zoomlev, float flat, float flong);                                  //  map scale at given zoom and location

namespace meta_names
{
   ch     edit_keywords[filekeywordsXcc] = "";                                         //  edited keywords: keyw1, keyw2, ...
   ch     *defined_keywords[maxkeywordcats];                                           //  defined keywords: catg: keyw1, keyw2, ...
   ch     recent_keywords[recentkeywordsXcc] = "";                                     //  recently added keywords list

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

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

   glocs_t   **imagelocs = 0;                                                          //  image geolocations table
   int         Nimagelocs = 0;                                                         //  size of geolocations table

   glocs_t   **worldlocs = 0;                                                          //  cities geolocations table
   int         Nworldlocs = 0;                                                         //  size of geolocations table

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

   gallerymap_t   *gallerymap = 0;
   int            Ngallerymap = 0;

   enum { xxNULL, xxFNAME, xxFDATE, xxFSIZE, xxPDATE, xxPSIZE, xxBPC, xxRATE,          //  xxrec_tab[] members
          xxKWDS, xxTITL, xxDESC, xxLOC, xxCNTR, xxMAKE, xxMODL, xxLENS,               //  indexed metadata tags - avoid zero
          xxEXP, xxFNUM, xxFLNG, xxISO } ;
}

using namespace meta_names;


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

//  Get tag type from tag name: 
//  returns: 1-19   member sequence in xxrec_tab[] 
//            100   present in extra indexed metadata
//              0   not indexed (must be searched in image metadata)

int metatagtype(ch *tagname)                                                           //  26.0
{
   if (strmatchcase(tagname,"filename")) return xxFNAME; 
   if (strmatchcase(tagname,"filedate")) return xxFDATE; 
   if (strmatchcase(tagname,"filesize")) return xxFSIZE;
   if (strmatchcase(tagname,"photodate")) return xxPDATE;
   if (strmatchcase(tagname,"datetimeoriginal")) return xxPDATE;
   if (strmatchcase(tagname,"imagesize")) return xxPSIZE;
   if (strmatchcase(tagname,"bitspersample")) return xxBPC;
   if (strmatchcase(tagname,"rating")) return xxRATE;
   if (strmatchcase(tagname,"keywords")) return xxKWDS;
   if (strmatchcase(tagname,"subject")) return xxKWDS;                                 //  jxl
   if (strmatchcase(tagname,"title")) return xxTITL;
   if (strmatchcase(tagname,"description")) return xxDESC;
   if (strmatchcase(tagname,"city")) return xxLOC;
   if (strmatchcase(tagname,"location")) return xxLOC;                                 //  jxl
   if (strmatchcase(tagname,"country")) return xxCNTR;
   if (strmatchcase(tagname,"make")) return xxMAKE;
   if (strmatchcase(tagname,"model")) return xxMODL;
   if (strmatchcase(tagname,"lens")) return xxLENS;
   if (strmatchcase(tagname,"exposuretime")) return xxEXP;
   if (strmatchcase(tagname,"fnumber")) return xxFNUM;
   if (strmatchcase(tagname,"focallength")) return xxFLNG;
   if (strmatchcase(tagname,"iso")) return xxISO;

   for (int ii = 0; ii < xmetaNtags; ii++)                                             //  search extra indexed metadata tags
      if (strmatchcase(tagname,xmeta_tags[ii])) return 100;                            //  tag is extra indexed metadata tag

   return 0;
}


//  Dialog to select metadata tags (for index, view, edit, search, report).
//  Input list is replaced by user-edited list if changes were made.
//  exclude: exclude tags which are indexed by default.
//  returns 0/1 = no changes / changes made

namespace select_meta_tags_names
{
   GtkWidget   *mtext1, *mtext2;
   int         Fexclude, Fchange;
   zdialog     *zd;
   ch          *pp;
}

int select_meta_tags(zlist_t *mlist, int maxout, int exclude)
{
   using namespace select_meta_tags_names;

   int  select_meta_tags_clickfunc1(GtkWidget *, int line, int pos, ch *input);
   int  select_meta_tags_clickfunc2(GtkWidget *, int line, int pos, ch *input);

   int         zstat, ii, jj, nn;
   ch          *pp, ppc1[80], ppc2[80];
   zlist_t     *picklist;

   Fexclude = exclude;
   Fchange = 0;

/***
       __________________________________________________________
      |                Select Metadata Tags                      |
      |                                                          |
      |       click to select            click to unselect       |
      |  _________________________   __________________________  |
      | | Orientation             | |                          | |
      | | Rotation                | |                          | |
      | | Exposure Time           | |                          | |
      | | Aperture                | |                          | |
      | |   ...                   | |                          | |
      | | Other Tag ...           | |                          | |
      | |_________________________| |__________________________| |
      |                                                          |
      |                                                 [OK] [X] |
      |__________________________________________________________|

***/

   zd = zdialog_new(TX("Select Metadata tags"),Mwin,"OK","X",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",TX("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",TX("click to unselect"));
   zdialog_add_widget(zd,"scrwin","scroll2","vb2",0,"expand");
   zdialog_add_widget(zd,"text","mtext2","scroll2",0,"expand");

   mtext1 = zdialog_gtkwidget(zd,"mtext1");
   txwidget_clear(mtext1);

   mtext2 = zdialog_gtkwidget(zd,"mtext2");
   txwidget_clear(mtext2);

   picklist = zlist_from_file(meta_picklist_file);                                     //  metadata picklist
   if (! picklist) {
      zmessageACK(Mwin,TX("metadata picklist file not found %s"),meta_picklist_file);
      return 0;
   }

   for (ii = 0; ii < zlist_count(picklist); ii++) {
      pp = zlist_get(picklist,ii);
      if (Fexclude) {
         strCompress(ppc1,pp);                                                         //  exclude tags indexed by default
         for (jj = 0; jj < NKX; jj++) {
            strCompress(ppc2,tagnamex[jj]);
            if (strcasestr(ppc1,ppc2)) break;
         }
         if (jj < NKX) continue;
      }
      txwidget_append(mtext1,0,"%s\n",pp);                                             //  picklist >> left widget
   }

   txwidget_append(mtext1,0,"%s\n",TX("Other Tag ..."));                               //  append "Other tag ..."

   zlist_free(picklist);                                                               //  free memory

   txwidget_clear(mtext2);
   for (ii = 0; ii < zlist_count(mlist); ii++)                                         //  user list >> right widget
      txwidget_append(mtext2,0,"%s\n",zlist_get(mlist,ii));

   txwidget_set_eventfunc(mtext1,select_meta_tags_clickfunc1);                         //  set mouse/KB event function
   txwidget_set_eventfunc(mtext2,select_meta_tags_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 || ! Fchange) {                                                      //  no changes made
      zdialog_free(zd);
      return 0;
   }

   nn = txwidget_linecount(mtext2);

   if (nn > maxout) {
      zmessageACK(Mwin,TX("selection exceeds %d tags"),maxout);
      zdialog_free(zd);
      return 0;
   }

   zlist_clear(mlist,0);                                                               //  replace input list with output list

   for (ii = 0; ii < nn; ii++) {
      pp = txwidget_line(mtext2,ii,1);
      if (! *pp) continue;
      strCompress(ppc1,pp);                                                            //  exiftool: no embedded blanks
      zlist_append(mlist,ppc1,1);
   }

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


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

int select_meta_tags_clickfunc1(GtkWidget *widget, int line, int pos, ch *input)
{
   using namespace select_meta_tags_names;

   ch       *pp, ppc[80];
   int      ii;

   if (*input == GDK_KEY_F1) {                                                         //  key F1 pressed, show help
      showz_docfile(Mwin,UGfile,F1_help_topic);
      return 1;
   }

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

   if (strmatch(pp,"Other Tag ...")) {                                                 //  get manually input metadata name      26.0
      pp = zdialog_text(zd->dialog,"metadata tag name",0);
      if (! pp) return 1;
      strCompress(pp);                                                                 //  remove blanks

      if (Fexclude) {
         for (ii = 0; ii < NKX; ii++) {
            strCompress(ppc,tagnamex[ii]);
            if (strmatch(pp,ppc)) {
               zmessageACK(Mwin,TX("%s is already indexed"),pp);
               zfree(pp);
               return 1;
            }
         }
      }
   }

   strCompress(ppc,pp);                                                                //  exiftool: no embedded blanks
   zfree(pp);

   txwidget_append2(mtext2,0,"%s\n",ppc);                                              //  append to output list

   Fchange = 1;
   return 1;
}


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

int select_meta_tags_clickfunc2(GtkWidget *widget, int line, int pos, ch *input)
{
   using namespace select_meta_tags_names;

   ch     *pp;

   if (*input == GDK_KEY_F1) {                                                         //  key F1 pressed, show help
      showz_docfile(Mwin,UGfile,F1_help_topic);
      return 1;
   }

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

   txwidget_delete(widget,line);                                                       //  delete line

   Fchange = 1;
   return 1;
}


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

//  menu function and popup dialog to show metadata
//  window is updated when navigating to another image

#define  maxextraview   20

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_all(0,0);
   else
      m_meta_view_main(0,0);
   return;
}


//  menu function - metadata short report

void m_meta_view_main(GtkWidget *, ch *menu)
{
   void  meta_view_tag_report(ch *tagname, ch *tagval, GtkWidget *widget);
   int   meta_view_short_dialog_event(zdialog *zd, ch *event);

   ch             *tagvalx[NKX];                                                       //  xxrec_tab[] tags

   ch             *edithisttag[1] = { meta_edithist_tag };                             //  edit history data
   ch             *edithistval[1];

   ch             *tagname2[maxextraview];                                             //  extra metadata tags to view
   ch             *tagval2[maxextraview];

   ch             buff[metatagXcc], *pp;

   ch             *pixels, *bpc;
   ch             *focallength, chsec[12];
   ch             *text1, **text2;
   static ch      *file, *filen;
   float          mb, fsecs;
   int            err, ii, nn, cc, nk2;
   GtkWidget      *widget;

   FILE           *fid;

   F1_help_topic = TX("VIEW MAIN META");

   printf("m_meta_view_main \n");

   if (FGM != 'F' && FGM != 'G') return;

   if (clicked_file) {                                                                 //  use clicked file if present
      file = clicked_file;
      clicked_file = 0;
   }
   else if (curr_file) file = zstrdup(curr_file,"meta-view");
   else return;

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

   if (! zd_metaview)                                                                  //  create if not already
   {
      zd_metaview = zdialog_new(TX("View Main Metadata"),Mwin,"Extras","X",null);
      zdialog_add_widget(zd_metaview,"scrwin","scroll","dialog",0,"expand");
      zdialog_add_widget(zd_metaview,"text","metadata","scroll",0,"expand");
      zdialog_resize(zd_metaview,550,350);
      zdialog_run(zd_metaview,meta_view_short_dialog_event,"save");
   }

   widget = zdialog_gtkwidget(zd_metaview,"metadata");                                 //  clear prior report
   txwidget_clear(widget);

   err = meta_get(file,tagnamex,tagvalx,NKX);                                          //  xxrec_tab[] tags
   if (err) return;

   meta_get(file,edithisttag,edithistval,1);                                           //  edit history data

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

   if (tagvalx[0]) tagvalx[0][19] = 0;                                                 //  truncate dates to yyyy:mm:dd hh:mm:ss
   if (tagvalx[2]) tagvalx[2][19] = 0;
   if (tagvalx[2]) tagvalx[2][4] = tagvalx[2][7] = ':';                                //  metadata mixed yyyy:mm:dd, yyyy-mm-dd

   txwidget_append(widget,0,"File         %s \n",filen);

   mb = atof(tagvalx[1]) / MEGA;
   pixels = tagvalx[3];
   pp = strchr(pixels,' ');
   if (pp && pp-pixels < (int64) (strlen(pixels) - 2)) *pp = 'x';
   bpc = tagvalx[4];

   txwidget_append(widget,0,"Size         %.3f mb  pixels %s  bit depth %s \n",
                                                mb,pixels,bpc);

   txwidget_append(widget,0,"Dates        photo: %s  file: %s \n",
                              tagvalx[2], tagvalx[0]);

   if (tagvalx[12] || tagvalx[13] || tagvalx[14])
      txwidget_append(widget,0,"Camera       make: %s  model: %s  lens: %s \n",
                                       tagvalx[12], tagvalx[13], tagvalx[14]);

   if (tagvalx[15] || tagvalx[16] || tagvalx[17] || tagvalx[18])                       //  photo exposure data
   {
      if (tagvalx[17]) focallength = tagvalx[17];                                      //  focal length
      else focallength = 0;                                                            //  missing
      *chsec = 0;
      if (tagvalx[15]) {                                                               //  expose time
         fsecs = atofz(tagvalx[15]);                                                   //  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.5 && fsecs < 2)                                            //  23/1
            snprintf(chsec,12,"%.1f",fsecs);
         else snprintf(chsec,12,"%.0f",fsecs);
      }
      txwidget_append(widget,0,"Exposure     %s sec  %s mm  F%s  ISO %s \n",           //  secs Fleng Fnumber ISO
                        chsec,focallength,tagvalx[16],tagvalx[18]);
   }

   if (tagvalx[9] || tagvalx[10] || tagvalx[11])                                       //  location, country, GPS data
      txwidget_append(widget,0,"Location     %s %s  %s \n",
                                tagvalx[9],tagvalx[10],tagvalx[11]);

   if (tagvalx[6]) {                                                                   //  keywords
      cc = strlen(tagvalx[6]) - 1;
      if (tagvalx[6][cc] == ',') tagvalx[6][cc] = 0;
      nn = breakup_text(tagvalx[6],text2,"|",80,99);                                   //  wrap long lines
      txwidget_append(widget,0,"Keywords     %s \n",text2[0]);
      for (ii = 1; ii < nn; ii++)
         txwidget_append(widget,0,"             %s \n",text2[ii]);
      for (ii = 0; ii < nn; ii++)
         zfree(text2[ii]);
      zfree(text2);
   }

   if (tagvalx[5])                                                                     //  rating
      txwidget_append(widget,0,"Rating       %s \n",tagvalx[5]);

   if (edithistval[0]) {                                                               //  edit history data
      cc = strlen(edithistval[0]) + 100;
      text1 = (ch *) zmalloc(cc,"meta-view");
      repl_1str(edithistval[0],text1,cc,"|","\n");
      nn = breakup_text(text1,text2,"|",80,99);
      txwidget_append(widget,0,"Edits        %s \n",text2[0]);
      for (ii = 1; ii < nn; ii++)
         txwidget_append(widget,0,"             %s \n",text2[ii]);
      for (ii = 0; ii < nn; ii++)
         zfree(text2[ii]);
      zfree(text2);
      zfree(text1);
   }

   if (tagvalx[7])                                                                     //  title
      meta_view_tag_report(tagnamex[7],tagvalx[7],widget);

   if (tagvalx[8])                                                                     //  description
      meta_view_tag_report(tagnamex[8],tagvalx[8],widget);

   txwidget_append(widget,0,"\n");

   for (ii = 0; ii < NKX; ii++)                                                        //  free memory
      if (tagvalx[ii]) zfree(tagvalx[ii]);
   if (edithistval[0]) zfree(edithistval[0]);

   //  append extra report tags if any

   fid = fopen(meta_view_extras_file,"r");
   if (! fid) goto finished;                                                           //  no extras file

   for (nk2 = 0; nk2 < maxextraview; nk2++) {                                          //  get list of user extras to view
      pp = fgets_trim(buff,metatagXcc,fid,1);
      if (! pp) break;
      strCompress(pp);
      if (*pp <= ' ') { nk2--; continue; }
      tagname2[nk2] = zstrdup(pp,"meta-view");
   }
   fclose(fid);

   if (nk2 == 0) goto finished;                                                        //  empty file

   err = meta_get(file,tagname2,tagval2,nk2);                                          //  get all extra tags at once
   if (err) goto finished;

   for (ii = 0; ii < nk2; ii++)                                                        //  report user extra tags
      meta_view_tag_report(tagname2[ii],tagval2[ii],widget);

   for (ii = 0; ii < nk2; ii++) {                                                      //  free memory
      zfree(tagname2[ii]);
      if (tagval2[ii]) zfree(tagval2[ii]);
   }

finished:
   zfree(file);
   return;
}


//  m_meta_view_main() helper function
//  write tag name and tag value to report, breaking up long text where needed.

void meta_view_tag_report(ch *tagname, ch *tagval, GtkWidget *widget)
{
   int      ii, nn;
   ch       **longtext;

   if (! tagval || ! *tagval) return;

   if (strlen(tagval) < 60) {                                                          //  25.1
      txwidget_append(widget,0,"%-12s %s \n",tagname,tagval);
      return;
   }

   txwidget_append(widget,0,"\n");                                                     //  blank line
   txwidget_append(widget,0,"%s: \n",tagname);                                         //  tag name
   nn = breakup_text(tagval,longtext,0,80,99);                                         //  break up long tag value               25.1
   for (ii = 0; ii < nn; ii++)
      txwidget_append(widget,0,"   %s \n",longtext[ii]);                               //  output each piece
   for (ii = 0; ii < nn; ii++)
      zfree(longtext[ii]);                                                             //  free memory
   zfree(longtext);

   return;
}


//  dialog event and completion callback function

int meta_view_short_dialog_event(zdialog *zd, ch *event)
{
   zlist_t  *mlist;
   int      zstat, nn;

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

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

   if (zstat != 1) return 1;                                                           //  not [extras] button

   mlist = zlist_from_file(meta_view_extras_file);                                     //  get metadata extras list
   if (! mlist) mlist = zlist_new(0);

   nn = select_meta_tags(mlist,maxextraview,1);                                        //  user edit of view extras list
   if (nn) zlist_to_file(mlist,meta_view_extras_file);                                 //  update extras file

   zlist_free(mlist);
   return 1;
}


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

//  menu function - metadata long report

void m_meta_view_all(GtkWidget *, ch *menu)
{
   int   meta_view_long_dialog_event(zdialog *zd, ch *event);

   FILE           *fid;
   ch             *file, *file2;
   ch             *pp, buff[10000];
   GtkWidget      *widget;
   int            err;
   ch             *tooloptions = "-m -n -S -c \"%+.5f\" -d \"%Y:%m:%d %H:%M:%S\"";

   F1_help_topic = TX("VIEW MAIN META");

   printf("m_meta_view_all \n");

   if (FGM != 'F' && FGM != 'G') return;

   if (clicked_file) {                                                                 //  use clicked file if present
      file = clicked_file;
      clicked_file = 0;
   }
   else if (curr_file) file = zstrdup(curr_file,"meta-view");
   else return;

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

   if (zd_metaview) zdialog_free(zd_metaview);
   zd_metaview = zdialog_new(TX("View All Metadata"),Mwin,"X",null);
   zdialog_add_widget(zd_metaview,"scrwin","scroll","dialog",0,"expand");
   zdialog_add_widget(zd_metaview,"text","metadata","scroll",0,"expand|wrap");
   zdialog_resize(zd_metaview,700,700);
   zdialog_run(zd_metaview,meta_view_long_dialog_event,"save");

   widget = zdialog_gtkwidget(zd_metaview,"metadata");
   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
   txwidget_clear(widget);

   file2 = zescape_quotes(file);
   snprintf(command,CCC,"exiftool %s \"%s\" ",tooloptions,file2);                      //  exiftool command
   zfree(file2);

   fid = popen(command,"r");                                                           //  get command outputs
   if (fid) {
      while ((pp = fgets_trim(buff,10000,fid))) {
         err = utf8_clean(pp);
         if (err) txwidget_append(widget,0,TX("*** bad utf8 detected *** \n"));        //  25.1
         txwidget_append(widget,0,"%s\n",pp);                                          //  add to report window
      }
      pclose(fid);
   }

   zfree(file);
   return;
}


//  dialog event and completion callback function

int meta_view_long_dialog_event(zdialog *zd, ch *event)
{
   if (! zd->zstat) return 1;                                                          //  wait for completion
   zdialog_free(zd);                                                                   //  kill dialog
   zd_metaview = 0;
   return 1;
}


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

//  Popup dialog to edit a large metadata text block.
//  Fake \n characters are replaced with real \n for editing,
//    and the reverse is done when editing is done.
//  Real \n characters are not allowed in metadata.

int meta_edit_text(ch *&text)
{
   zdialog     *zd;
   ch          text2[metadataXcc], text3[metadataXcc];
   int         zstat;

   if (text) strncpy0(text2,text,metadataXcc);                                         //  input text
   else *text2 = 0;

   repl_1str(text2,text3,metadataXcc,"\\n","\n");                                      //  replace fake \n with real \n

   zd = zdialog_new(TX("Edit Metadata"),Mwin,TX("Apply"),"X",null);                    //  build edit dialog
   zdialog_add_widget(zd,"scrwin","scroll","dialog",0,"expand");
   zdialog_add_widget(zd,"zedit","text","scroll",0,"wrap|expand");

   zdialog_stuff(zd,"text",text3);                                                     //  metadata --> dialog

   zdialog_resize(zd,600,400);
   zdialog_run(zd,0,"parent");                                                         //  run dialog, edit text
   zstat = zdialog_wait(zd);

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

   zdialog_fetch(zd,"text",text3,metadataXcc);                                         //  get edited text
   repl_1str(text3,text2,metadataXcc,"\n","\\n");                                      //  replace real \n with fake \n

   if (text) zfree(text);                                                              //  return edited text
   text = zstrdup(text2,"meta_edit_text");
   zdialog_free(zd);
   return 1;
}


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

//  edit main metadata menu function

namespace edit_main_names
{
   xxrec_t  *xxrec;

   int   ftf = 1;

   ch    pdate[20], rating[4];                                                         //  editable metadata
   ch    *keywords = 0, *title = 0, *desc = 0;
   ch    location[40], country[40], gps_data[24];

   ch    p_pdate[20], p_rating[4];                                                     //  previous data
   ch    *p_keywords, *p_title, *p_desc;                                               //    for use by [prev] button
   ch    p_location[40], p_country[40], p_gps_data[24];
};


void m_meta_edit_main(GtkWidget *, ch *menu)
{
   using namespace edit_main_names;

   int  edit_keywords_clickfunc(GtkWidget *widget, int line, int pos, ch *input);
   int  edit_recenkeywords_clickfunc(GtkWidget *widget, int line, int pos, ch *input);
   int  edit_matchkeywords_clickfunc(GtkWidget *widget, int line, int pos, ch *input);
   int  edit_defkeywords_clickfunc(GtkWidget *widget, int line, int pos, ch *input);
   int  editmeta_dialog_event(zdialog *zd, ch *event);

   GtkWidget   *widget;
   zdialog     *zd;
   ch          *tagvalx[NKX];
   ch          *pp, text50[50];
   int         ii, err;
   float       flati, flongi;

   F1_help_topic = TX("EDIT MAIN META");

   printf("m_meta_edit_main \n");

   if (ftf) {                                                                          //  first time processing
      ftf = 0;
      *p_pdate = 0;                                                                    //  prior metadata is empty
      *p_rating = 0;
      p_keywords = 0;
      p_title = 0;
      p_desc = 0;
      *p_location = 0;
      *p_country = 0;
      *p_gps_data = 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);
      clicked_file = 0;
   }

   if (! curr_file) {
      zmessageACK(Mwin,TX("no current file"));
      if (zd_editmeta) zdialog_free(zd_editmeta);
      zd_editmeta = 0;
      zd_mapgeotags = 0;
      return;
   }

   err = access(curr_file,W_OK);                                                       //  test if file can be written
   if (err) {
      zmessageACK(Mwin,"%s: %s",TX("no write permission"),curr_file);
      return;
   }

   //  get all editable metadata from meta_get()

   err = meta_get(curr_file,tagnamex,tagvalx,NKX);
   if (err) return;

   if (tagvalx[2]) {
      strncpy0(pdate,tagvalx[2],20);                                                   //  photo date, yyyy:mm:dd ... or null
      pdate[4] = pdate[7] = pdate[13] = pdate[16] = ':';                               //  stop variable formats
   }
   else *pdate = 0;

   if (tagvalx[5]) strncpy0(rating,tagvalx[5],4);                                      //  rating 0-5
   else strcpy(rating,"0");

   if (keywords) zfree(keywords);
   if (tagvalx[6]) {                                                                   //  image keywords
      keywords = tagvalx[6];
      tagvalx[6] = 0;
      strncpy0(edit_keywords,keywords,filekeywordsXcc);                                //  edit_keywords: where keywords are edited
   }
   else {
      keywords = 0;
      *edit_keywords = 0;
   }

   if (title) zfree(title);                                                            //  image title
   if (tagvalx[7]) {
      title = tagvalx[7];
      tagvalx[7] = 0;
   }
   else title = 0;

   if (desc) zfree(desc);                                                              //  image description
   if (tagvalx[8]) {
      desc = tagvalx[8];
      tagvalx[8] = 0;
   }
   else desc = 0;

   if (tagvalx[9]) strncpy0(location,tagvalx[9],40);                                   //  location (aka city)
   else *location = 0;

   if (tagvalx[10]) strncpy0(country,tagvalx[10],40);                                  //  country
   else *country = 0;

   if (tagvalx[11]) strncpy0(gps_data,tagvalx[11],24);                                 //  gps data
   else *gps_data = 0;
   get_gps_data(gps_data,flati,flongi);

   for (ii = 0; ii < NKX; ii++)
      if (tagvalx[ii]) zfree(tagvalx[ii]);

   load_imagelocs();                                                                   //  initialize image geolocs[] data
   load_worldlocs();                                                                   //  initialize world geolocs[] data

/***
          ___________________________________________________________
         |                 Edit Main Metadata                        |
         |                                                           |
         |  File: filename.jpg                                       |
         |          ________________________________________         |
         |  Title  |________________________________________| [edit] |
         |          ________________________________________         |
         |  Desc.  |________________________________________| [edit] |
         |                                                           |
         |  Photo Date: [__________________]  Rating: [__]           |
         |                                                           |
         |  location [____________] [____________]  GPS [__________] |
         |  [Find] [Previous] [Clear]                                |
         |                                                           |
         |  Image Keywords [_______________________________________] |
         |  - - - - - - - - - - - - - - - - - - - - - - - - - - - -  |
         |  Recent Keywords [__________________________________] [X] |
         |  Enter Keyword [______________]  [_] add new keyword      |
         |  Matching Keywords [____________________________________] |
         |                                                           |
         |  Keywords Category [_________________|v]                | |
         |  |                                                      | |
         |  |                                                      | |
         |  |                                                      | |
         |  |                                                      | |
         |  |                                                      | |
         |  |                                                      | |
         |  |                                                      | |
         |  |______________________________________________________| |
         |                                                           |
         |                                        [Prev] [Apply] [X] |
         |___________________________________________________________|

***/

   if (! zd_editmeta)                                                                  //  (re)start edit dialog
   {
      zd = zdialog_new(TX("Edit Main Metadata"),Mwin,TX("Prev"),TX("Apply"),"X",null);
      zd_editmeta = zd;

      zdialog_add_ttip(zd,"Apply",TX("save metadata to image file"));

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

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

      //  Title  |______________________________________________| [edit]

      zdialog_add_widget(zd,"hbox","hbtl","dialog",0,"space=1");
      zdialog_add_widget(zd,"label","labtl","hbtl","Title","space=3");
      zdialog_add_widget(zd,"text","title","hbtl",0,"expand");
      zdialog_add_widget(zd,"button","edittitle","hbtl",TX("edit"),"space=3");

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

      //  Desc.  |______________________________________________| [edit]

      zdialog_add_widget(zd,"hbox","hbds","dialog",0,"space=1");
      zdialog_add_widget(zd,"label","labds","hbds","Desc.","space=3");
      zdialog_add_widget(zd,"text","desc","hbds",0,"expand");
      zdialog_add_widget(zd,"button","editdesc","hbds",TX("edit"),"space=3");

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

      //  Photo Date [_______________]  Rating: [__]
      zdialog_add_widget(zd,"hbox","hbdt","dialog",0,"space=1");
      zdialog_add_widget(zd,"label","labdate","hbdt","Photo Date","space=3");
      zdialog_add_widget(zd,"zentry","pdate","hbdt",0,"size=20");
      zdialog_add_widget(zd,"label","space","hbdt",0,"space=5");
      zdialog_add_widget(zd,"label","labrate","hbdt","Rating:","space=3");
      zdialog_add_widget(zd,"zspin","rating","hbdt","0|5|1|0","space=3");
      zdialog_add_ttip(zd,"pdate","yyyy:mm:dd hh:mm[:ss]");

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

      //  location [_____________] [____________]  GPS [________________]
      zdialog_add_widget(zd,"hbox","hbloc","dialog",0,"space=3");
      zdialog_add_widget(zd,"label","labloc","hbloc","Location","space=3");
      zdialog_add_widget(zd,"zentry","location","hbloc",0,"expand");
      zdialog_add_widget(zd,"label","space","hbloc",0,"space=5");
      zdialog_add_widget(zd,"zentry","country","hbloc",0,"expand");
      zdialog_add_widget(zd,"label","space","hbloc",0,"space=5");
      zdialog_add_widget(zd,"label","labgps","hbloc","GPS","space=3");
      zdialog_add_widget(zd,"zentry","gps_data","hbloc",0,"size=10");

      //  [Find] [Previous] [Clear]
      zdialog_add_widget(zd,"hbox","hbgeo","dialog",0,"space=3");
      zdialog_add_widget(zd,"button","geofind","hbgeo",TX("Find"),"space=5");
      zdialog_add_widget(zd,"button","geoprev","hbgeo",TX("Previous"),"space=5");      //  25.1
      zdialog_add_widget(zd,"button","geoclear","hbgeo",TX("Clear"),"space=5");

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

      //  Image Keywords [________________________________________]
      zdialog_add_widget(zd,"hbox","hbit","dialog",0,"space=1");
      zdialog_add_widget(zd,"label","labit","hbit",TX("Image Keywords"),"space=3");
      zdialog_add_widget(zd,"text","keywords","hbit",0,"expand|wrap");

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

      //  Recent Keywords [_______________________________________]
      zdialog_add_widget(zd,"hbox","hbrt","dialog",0,"space=1");
      zdialog_add_widget(zd,"label","labrt","hbrt",TX("Recent Keywords"),"space=3");
      zdialog_add_widget(zd,"text","recenkeywords","hbrt",0,"expand|wrap");
      zdialog_add_widget(zd,"button","clear recent","hbrt","[X]","space=3");

      //  Enter Keyword [________________]  [_] add new keyword
      zdialog_add_widget(zd,"hbox","hbnt","dialog",0,"space=1");
      zdialog_add_widget(zd,"label","labnt","hbnt",TX("Enter Keyword"),"space=3");
      zdialog_add_widget(zd,"zentry","newkeyword","hbnt",0,"size=20");
      zdialog_add_widget(zd,"zbutton","add","hbnt",TX("add new keyword"),"space=8");

      //  Matching Keywords [_____________________________________]
      zdialog_add_widget(zd,"hbox","hbmt","dialog",0,"space=1");
      zdialog_add_widget(zd,"label","labmt","hbmt",TX("Matching Keywords"),"space=3");
      zdialog_add_widget(zd,"text","matchkeywords","hbmt",0,"expand|wrap");

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

      //  Keywords Category [______________________________]
      zdialog_add_widget(zd,"hbox","hbdt1","dialog");
      zdialog_add_widget(zd,"label","labdt","hbdt1","Keywords Category","space=3");
      zdialog_add_widget(zd,"combo","defcats","hbdt1",0,"expand|space=10|size=20");

      zdialog_add_widget(zd,"hbox","hbdt2","dialog",0,"expand");
      zdialog_add_widget(zd,"scrwin","swdt2","hbdt2",0,"expand");
      zdialog_add_widget(zd,"text","defkeywords","swdt2",0,"wrap");

      zdialog_add_ttip(zd,"labdate",TX("format: yyyy:mm:dd [hh:mm:ss]"));
      zdialog_add_ttip(zd,"geofind",TX("search known locations"));
      zdialog_add_ttip(zd,"geolookup",TX("find via table lookup"));
      zdialog_add_ttip(zd,"geoprev",TX("use previous location"));
      zdialog_add_ttip(zd,"geoclear",TX("clear inputs"));

      load_defkeywords(0);                                                             //  stuff defined keywords into dialog
      defkeywords_stuff(zd,"ALL");
      defcats_stuff(zd);                                                               //  and defined categories

      widget = zdialog_gtkwidget(zd,"keywords");                                       //  keyword widget mouse/KB event functions
      txwidget_set_eventfunc(widget,edit_keywords_clickfunc);

      widget = zdialog_gtkwidget(zd,"recenkeywords");
      txwidget_set_eventfunc(widget,edit_recenkeywords_clickfunc);

      widget = zdialog_gtkwidget(zd,"matchkeywords");
      txwidget_set_eventfunc(widget,edit_matchkeywords_clickfunc);

      widget = zdialog_gtkwidget(zd,"defkeywords");
      txwidget_set_eventfunc(widget,edit_defkeywords_clickfunc);

      zdialog_resize(zd,500,900);                                                      //  run dialog
      zdialog_run(zd,editmeta_dialog_event,"save");
   }

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

   pp = strrchr(curr_file,'/');
   zdialog_stuff(zd,"file",pp+1);                                                      //  stuff dialog data from curr. file
   zdialog_stuff(zd,"pdate",pdate);
   zdialog_stuff(zd,"rating",rating);
   zdialog_stuff(zd,"keywords",keywords);

   if (title) {
      strncpy0(text50,title,50);                                                       //  limit 50 chars.
      if (strlen(text50) > 48) strcpy(text50+45," ...");
   }
   else *text50 = 0;
   zdialog_stuff(zd,"title",text50);

   if (desc) {
      strncpy0(text50,desc,50);
      if (strlen(text50) > 48) strcpy(text50+45," ...");
   }
   else *text50 = 0;
   zdialog_stuff(zd,"desc",text50);

   zdialog_stuff(zd,"location",location);
   zdialog_stuff(zd,"country",country);
   zdialog_stuff(zd,"gps_data",gps_data);

   zdialog_stuff(zd,"recenkeywords",recent_keywords);                                  //  stuff recent keywords list

   return;
}


//  mouse click functions for various text widgets for keywords

int edit_keywords_clickfunc(GtkWidget *widget, int line, int pos, ch *input)           //  existing image keyword was clicked
{
   using namespace edit_main_names;

   ch     *txkeyword, end = 0;

   if (*input == GDK_KEY_F1) {                                                         //  key F1 pressed, show help
      showz_docfile(Mwin,UGfile,F1_help_topic);
      return 1;
   }

   txkeyword = txwidget_word(widget,line,pos,",;",end);
   if (! txkeyword) return 1;

   del_keyword(txkeyword,edit_keywords);                                               //  remove keyword from file keywords
   zdialog_stuff(zd_editmeta,"keywords",edit_keywords);
   Fmetamod++;                                                                         //  note change

   zfree(txkeyword);
   return 1;
}


int edit_recenkeywords_clickfunc(GtkWidget *widget, int line, int pos, ch *input)      //  recent keyword was clicked
{
   using namespace edit_main_names;

   ch    *txkeyword, end = 0;

   if (*input == GDK_KEY_F1) {                                                         //  key F1 pressed, show help
      showz_docfile(Mwin,UGfile,F1_help_topic);
      return 1;
   }

   txkeyword = txwidget_word(widget,line,pos,",;",end);
   if (! txkeyword) return 1;

   add_keyword(txkeyword,edit_keywords,filekeywordsXcc);                               //  add recent keyword to keyword list
   zdialog_stuff(zd_editmeta,"keywords",edit_keywords);
   Fmetamod++;                                                                         //  note change

   zfree(txkeyword);
   return 1;
}


int edit_matchkeywords_clickfunc(GtkWidget *widget, int line, int pos, ch *input)      //  matching keyword was clicked
{
   using namespace edit_main_names;

   ch     *txkeyword, end = 0;

   if (*input == GDK_KEY_F1) {                                                         //  key F1 pressed, show help
      showz_docfile(Mwin,UGfile,F1_help_topic);
      return 1;
   }

   txkeyword = txwidget_word(widget,line,pos,",;",end);
   if (! txkeyword) return 1;

   add_keyword(txkeyword,edit_keywords,filekeywordsXcc);                               //  add matching keyword to image
   Fmetamod++;                                                                         //  note change
   add_recenkeyword(txkeyword);                                                        //  and add to recent keywords

   zdialog_stuff(zd_editmeta,"keywords",edit_keywords);                                //  update dialog widgets
   zdialog_stuff(zd_editmeta,"recenkeywords",recent_keywords);
   zdialog_stuff(zd_editmeta,"newkeyword","");
   zdialog_stuff(zd_editmeta,"matchkeywords","");

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

   zfree(txkeyword);
   return 1;
}


int edit_defkeywords_clickfunc(GtkWidget *widget, int line, int pos, ch *input)        //  defined keyword was clicked
{
   using namespace edit_main_names;

   ch     *txkeyword, end = 0;

   if (*input == GDK_KEY_F1) {                                                         //  key F1 pressed, show help
      showz_docfile(Mwin,UGfile,F1_help_topic);
      return 1;
   }

   txkeyword = txwidget_word(widget,line,pos,",;:",end);
   if (! txkeyword || end == ':') return 1;                                            //  nothing or keyword category, ignore

   add_keyword(txkeyword,edit_keywords,filekeywordsXcc);                               //  add new keyword to file keywords list
   zdialog_stuff(zd_editmeta,"keywords",edit_keywords);                                //    from defined keywords list
   Fmetamod++;                                                                         //  note change

   add_recenkeyword(txkeyword);                                                        //  and add to recent keywords
   zdialog_stuff(zd_editmeta,"recenkeywords",recent_keywords);

   zfree(txkeyword);
   return 1;
}


//  dialog event and completion callback function

int editmeta_dialog_event(zdialog *zd, ch *event)
{
   using namespace edit_main_names;

   ch       pdate[20];                                                                 //  yyyy:mm:dd hh:mm:ss
   int      ii, jj, nn, nt, cc1, cc2, err, ff;
   float    flati, flongi;
   ch       *pp1, *pp2;
   ch       catgname[keywordXcc];
   ch       newkeyword[keywordXcc], matchkeywords[20][keywordXcc];
   ch       matchkeywordstext[(keywordXcc+2)*20];
   ch       text50[50];

   if (! curr_file) zd->zstat = 3;                                                     //  current file gone

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

   if (strmatch(event,"edittitle")) {                                                  //  edit title
      meta_edit_text(title);
      if (title) strncpy0(text50,title,50);
      else *text50 = 0;
      if (strlen(text50) > 48) strcpy(text50+45," ...");
      zdialog_stuff(zd,"title",text50);
      Fmetamod++;
      return 1;
   }

   if (strmatch(event,"editdesc")) {                                                   //  edit description
      meta_edit_text(desc);
      if (desc) strncpy0(text50,desc,50);
      else *text50 = 0;
      if (strlen(text50) > 48) strcpy(text50+45," ...");
      zdialog_stuff(zd,"desc",text50);
      Fmetamod++;
      return 1;
   }

   if (zstrstr("pdate rating location country gps_data",event)) {                      //  note change, process later
      Fmetamod++;
      return 1;
   }

   if (zstrstr("geomap",event)) {                                                      //  gps_data modified via map click
      Fmetamod++;
      return 1;
   }

   if (strmatch(event,"geofind"))                                                      //  [find]   search both tables           26.0
   {
      zdialog_fetch(zd,"location",location,40);                                        //  ignore blank location
      if (! *location) return 1;
      nn = find_location(zd);                                                          //  search image location data            26.0
      if (nn) Fmetamod++;                                                              //  success
      return 1;
   }

   if (strmatch(event,"geoprev"))                                                      //  [previous] use previous location      25.1
   {
      zdialog_stuff(zd,"location",p_location);
      zdialog_stuff(zd,"country",p_country);
      zdialog_stuff(zd,"gps_data",p_gps_data);
   }

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

   if (strmatch(event,"defcats")) {                                                    //  new keyword category selection
      zdialog_fetch(zd,"defcats",catgname,keywordXcc);
      defkeywords_stuff(zd,catgname);
   }

   if (strmatch(event,"clear recent")) {                                               //  clear recent keywords list
      zdialog_stuff(zd,"recenkeywords","");
      *recent_keywords = 0;
   }

   if (strmatch(event,"newkeyword"))                                                   //  new keyword is being typed in
   {
      zdialog_stuff(zd,"matchkeywords","");                                            //  clear matchkeywords in dialog

      zdialog_fetch(zd,"newkeyword",newkeyword,keywordXcc);                            //  get chars. typed so far
      cc1 = strlen(newkeyword);

      for (ii = jj = 0; ii <= cc1; ii++) {                                             //  remove foul characters
         if (strchr(",:;",newkeyword[ii])) continue;
         newkeyword[jj++] = newkeyword[ii];
      }

      if (jj < cc1) {                                                                  //  something was removed
         newkeyword[jj] = 0;
         cc1 = jj;
         zdialog_stuff(zd,"newkeyword",newkeyword);
      }

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

      for (ii = nt = 0; ii < maxkeywordcats; ii++)                                     //  loop all categories
      {
         pp2 = defined_keywords[ii];                                                   //  category: aaaaaa, bbbbb, ... keywordN,
         if (! pp2) continue;                                                          //            |     |
         pp2 = strchr(pp2,':');                                                        //            pp1   pp2

         while (true)                                                                  //  loop all defkeywords in category
         {
            pp1 = pp2 + 2;
            if (! *pp1) break;
            pp2 = strchr(pp1,',');
            if (! pp2) break;
            if (strmatchcaseN(newkeyword,pp1,cc1)) {                                   //  defkeyword matches chars. typed so far
               cc2 = pp2 - pp1;
               strncpy(matchkeywords[nt],pp1,cc2);                                     //  save defkeywords that match
               matchkeywords[nt][cc2] = 0;
               if (++nt == 20) return 1;                                               //  quit if 20 matches or more
            }
         }
      }

      if (nt == 0) return 1;                                                           //  no matches

      pp1 = matchkeywordstext;

      for (ii = 0; ii < nt; ii++)                                                      //  matchkeyword list: aaaaa, bbb, cccc ...
      {
         strcpy(pp1,matchkeywords[ii]);
         pp1 += strlen(pp1);
         strcpy(pp1,", ");
         pp1 += 2;
      }

      zdialog_stuff(zd,"matchkeywords",matchkeywordstext);                             //  stuff matchkeywords in dialog
      return 1;
   }

   if (strmatch(event,"add"))                                                          //  enter new keyword finished
   {
      zdialog_fetch(zd,"newkeyword",newkeyword,keywordXcc);                            //  get finished keyword
      cc1 = strlen(newkeyword);
      if (! cc1) return 1;
      if (newkeyword[cc1-1] == '\n') {                                                 //  remove newline character
         cc1--;
         newkeyword[cc1] = 0;
      }

      for (ii = ff = 0; ii < maxkeywordcats; ii++)                                     //  loop all categories
      {
         pp2 = defined_keywords[ii];                                                   //  category: aaaaaa, bbbbb, ... keywordN,
         if (! pp2) continue;                                                          //            |     |
         pp2 = strchr(pp2,':');                                                        //            pp1   pp2

         while (true)                                                                  //  loop all defkeywords in category
         {
            pp1 = pp2 + 2;
            if (! *pp1) break;
            pp2 = strchr(pp1,',');
            if (! pp2) break;
            cc2 = pp2 - pp1;
            if (cc2 != cc1) continue;
            if (strmatchcaseN(newkeyword,pp1,cc1)) {                                   //  entered keyword matches defkeyword
               strncpy(newkeyword,pp1,cc1);                                            //  use defkeyword upper/lower case
               ff = 1;
               break;
            }
         }

         if (ff) break;
      }

      add_keyword(newkeyword,edit_keywords,filekeywordsXcc);                           //  add to file keywords list
      add_recenkeyword(newkeyword);                                                    //  and add to recent keywords
      Fmetamod++;                                                                      //  note change

      if (! ff) {                                                                      //  if new keyword, add to defined keywords
         add_defkeyword("nocatg",newkeyword);
         defkeywords_stuff(zd,"ALL");                                                  //  refresh keyword list
      }

      zdialog_stuff(zd,"newkeyword","");                                               //  update dialog widgets
      zdialog_stuff(zd,"keywords",edit_keywords);
      zdialog_stuff(zd,"recenkeywords",recent_keywords);
      zdialog_stuff(zd,"matchkeywords","");

      zdialog_goto(zd,"newkeyword");                                                   //  put focus back on newkeyword 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 (! *pdate && *p_pdate)                                                        //  stuff photo date only if none
         zdialog_stuff(zd,"pdate",p_pdate);

      zdialog_stuff(zd,"rating",p_rating);

      if (! p_keywords || ! *p_keywords) {                                             //  no previous keywords
         zdialog_stuff(zd,"keywords","");
         *edit_keywords = 0;                                                           //  stuff no keywords
      }
      else {                                                                           //  stuff previous keywords
         zdialog_stuff(zd,"keywords",p_keywords);
         strncpy0(edit_keywords,p_keywords,filekeywordsXcc);                           //  sync edited keywords list
      }

      if (p_title && *p_title) {
         if (title) zfree(title);
         title = zstrdup(p_title,"edit_main");
         strncpy0(text50,title,50);                                                    //  limit to 50 chars.
         if (strlen(text50) > 48) strcpy(text50+45," ...");
         zdialog_stuff(zd,"title",text50);
      }
      else {
         if (title) zfree(title);
         title = 0;
         zdialog_stuff(zd,"title","");
      }

      if (p_desc && *p_desc) {
         if (desc) zfree(desc);
         desc = zstrdup(p_desc,"edit_main");
         strncpy0(text50,p_desc,50);
         if (strlen(text50) > 48) strcpy(text50+45," ...");
         zdialog_stuff(zd,"desc",text50);
      }
      else {
         if (desc) zfree(desc);
         desc = 0;
         zdialog_stuff(zd,"desc","");
      }

      zdialog_stuff(zd,"location",p_location);
      zdialog_stuff(zd,"country",p_country);
      zdialog_stuff(zd,"gps_data",p_gps_data);

      Fmetamod++;
      return 1;
   }

   if (zd->zstat != 2) {                                                               //  cancel
      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

   //  edits finished
   //  get all data from dialog, validate, save back to image file

   zdialog_fetch(zd,"pdate",pdate,20);
   if (*pdate && ! checkDT(pdate)) {                                                   //  validate photo date
      zmessageACK(Mwin,TX("date format is yyyy:mm:dd [hh:mm:ss]"));
      return 1;
   }

   zdialog_fetch(zd,"rating",rating,4);

   zdialog_fetch(zd,"location",location,40);                                           //  clean extra blanks
   zdialog_fetch(zd,"country",country,40);
   strTrim2(location);
   strTrim2(country);

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

   if (*country) {
      *country = toupper(*country);
      zdialog_stuff(zd,"country",country);
   }

   zdialog_fetch(zd,"gps_data",gps_data,24);
   if (*gps_data) {
      err = get_gps_data(gps_data,flati,flongi);
      if (err) {
         zmessageACK(Mwin,TX("invalid GPS data"));
         return 1;
      }
   }

   if (keywords) zfree(keywords);
   keywords = zstrdup(edit_keywords,"edit_main");                                      //  get edited keywords

   ch    *tagname[8] = { meta_pdate_tag, meta_rating_tag,                              //  setup for meta_put()
                          meta_keywords_tag, meta_title_tag, meta_desc_tag,
                          meta_city_tag, meta_country_tag, meta_gps_tag };

   ch    *tagval[8] = { pdate, rating, keywords, title, desc,
                         location, country, gps_data };

   meta_put(curr_file,tagname,tagval,8);                                               //  update image file and xxrec_tab[]

   Fmetamod = 0;                                                                       //  no unsaved metadata edits

   if (zd_metaview) meta_view(0);                                                      //  if active, update metadata report     25.3

   if (*location && *country && *gps_data)                                             //  update geolocs table in memory
      put_imagelocs(zd);

   strncpy0(p_pdate,pdate,20);                                                         //  copy data for use by [prev] button
   strncpy0(p_rating,rating,4);
   if (p_keywords) zfree(p_keywords);
   p_keywords = keywords, keywords = 0;
   if (p_title) zfree(p_title);
   p_title = 0;
   if (title) p_title = zstrdup(title,"edit_main");                                    //  26.0
   if (p_desc) zfree(p_desc);
   p_desc = 0;
   if (desc) p_desc = zstrdup(desc,"edit_main");                                       //  26.0
   strncpy0(p_location,location,40);
   strncpy0(p_country,country,40);
   strncpy0(p_gps_data,gps_data,24);

   return 1;
}


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

//  edit any metadata - add or change specified metadata tag data

namespace meta_edit_any_names
{
   ch     tagname[metatagXcc];
   ch     tagval[metadataXcc];
}


//  menu function

void m_meta_edit_any(GtkWidget *, ch *menu)
{
   using namespace meta_edit_any_names;

   int   meta_edit_any_dialog_event(zdialog *zd, ch *event);
   int   meta_edit_any_clickfunc(GtkWidget *, int line, int pos, ch *input);

   GtkWidget      *mtext;
   int            err;
   zdialog        *zd;
   ch             *tagname1[1];
   ch             *tagval1[1], *pp;

   F1_help_topic = TX("EDIT ANY META");

   printf("m_meta_edit_any \n");

   if (FGM != 'F' && FGM != 'G') return;

   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);
      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
   if (err) {
      zmessageACK(Mwin,"%s: %s",TX("no write permission"),curr_file);
      return;
   }

/***
       ____________________________________________________________________
      |  Click to Select             | File: filename.jpg                  |
      |------------------------------|                                     |
      |  (metadata list)             | tag name [________________________] |
      |                              | tag value [_______________________] |
      |                              |                                     |
      |                              |          [fetch] [update] [delete]  |
      |                              |                                     |
      |                              |                                     |
      |                              |                                     |
      |                              |                                     |
      |                              |                                     |
      |--------------------------------------------------------------------|
      |                                       [Short List] [Full List] [X] |
      |____________________________________________________________________|

***/

   if (! zd_editanymeta)                                                               //  popup dialog if not already
   {
      zd = zdialog_new(TX("Edit Any Metadata"),Mwin,TX("Short List"),TX("Full List"),"X",null);
      zd_editanymeta = zd;
      zdialog_add_widget(zd,"hbox","hb1","dialog",0,"expand");
      zdialog_add_widget(zd,"vbox","vb1","hb1",0,"space=3");
      zdialog_add_widget(zd,"label","lab1","vb1",TX("click to select"),"size=30");
      zdialog_add_widget(zd,"scrwin","scrb1","vb1",0,"expand");
      zdialog_add_widget(zd,"text","mtext","scrb1");
      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",TX("File:"),"space=3");
      zdialog_add_widget(zd,"label","file","hbf","filename.jpg","space=5");
      zdialog_add_widget(zd,"hbox","hbtag","vb2",0,"space=2");
      zdialog_add_widget(zd,"label","labtag","hbtag",TX("tag name"),"space=5");
      zdialog_add_widget(zd,"zentry","tagname","hbtag",0,"size=30");
      zdialog_add_widget(zd,"hbox","hbdata","vb2",0,"space=2");
      zdialog_add_widget(zd,"label","labdata","hbdata",TX("tag value"),"space=5");
      zdialog_add_widget(zd,"zedit","tagval","hbdata",0,"expand|wrap");
      zdialog_add_widget(zd,"hbox","hbb","vb2",0,"space=10");
      zdialog_add_widget(zd,"label","space","hbb",0,"expand");
      zdialog_add_widget(zd,"button","fetch","hbb",TX("fetch"),"space=3");
      zdialog_add_widget(zd,"button","update","hbb",TX("update"),"space=3");
      zdialog_add_widget(zd,"button","delete","hbb",TX("delete"),"space=3");

      zdialog_resize(zd,700,400);
      zdialog_run(zd,meta_edit_any_dialog_event,0);                                    //  start dialog

      mtext = zdialog_gtkwidget(zd,"mtext");                                           //  make clickable metadata list
      txwidget_set_eventfunc(mtext,meta_edit_any_clickfunc);                           //  set mouse/KB event function

      *tagname = 0;
   }

   zd = zd_editanymeta;                                                                //  dialog can stay open

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

   zdialog_send_event(zd,"initz");                                                     //  initz. dialog tag list

   if (*tagname)                                                                       //  update current tag value
   {
      tagname1[0] = tagname;                                                           //  look for tag data
      meta_get(curr_file,tagname1,tagval1,1);
      if (tagval1[0]) {
         strncpy0(tagval,tagval1[0],metadataXcc);
         zfree(tagval1[0]);
      }
      else *tagval = 0;
      zdialog_stuff(zd,"tagval",tagval);                                               //  stuff into dialog
   }

   return;
}


//  dialog event and completion callback function

int meta_edit_any_dialog_event(zdialog *zd, ch *event)
{
   using namespace meta_edit_any_names;

   GtkWidget   *mtext;
   ch          buff[1000];
   FILE        *fid;
   ch          *file2;
   ch          *pp, *ppp;
   ch          *tagname1[1];
   ch          *tagval1[1];
   ch          ppc1[80], ppc2[80];
   int         ii, err;
   static int  Fwarn = 1;
   static int  whichlist = 1;                                                          //  1/2 = short/full list

   ch          *warnmess = TX("Caution: Use the function Edit Main Metadata \n"        //  25.3
                              "to changed tags available there, to insure \n"
                              "related updates are made.");

   if (strmatch(event,"initz"))
   {
      if (whichlist == 1) zd->zstat = 1;
      if (whichlist == 2) zd->zstat = 2;
   }

   if (! curr_file) return 1;

   if (strmatch(event,"fetch"))
   {
      zdialog_fetch(zd,"tagname",tagname,metatagXcc);                                  //  get tag name from dialog
      strCompress(tagname);
      tagname1[0] = tagname;                                                           //  look for tag data
      meta_get(curr_file,tagname1,tagval1,1);
      if (tagval1[0]) {
         strncpy0(tagval,tagval1[0],metadataXcc);
         zfree(tagval1[0]);
      }
      else *tagval = 0;
      zdialog_stuff(zd,"tagval",tagval);                                               //  stuff into dialog
   }

   if (strmatch(event,"update"))
   {
      zdialog_fetch(zd,"tagname",tagname,metatagXcc);                                  //  get tag name from dialog
      zdialog_fetch(zd,"tagval",tagval,metadataXcc);
      strCompress(tagname);
      tagname1[0] = tagname;
      tagval1[0] = tagval;
      err = meta_put(curr_file,tagname1,tagval1,1);                                    //  metadata --> file
      if (err) zmessageACK(Mwin,TX("metadata update error"));
      if (zd_metaview) meta_view(0);                                                   //  update metadata view if active
   }

   if (strmatch(event,"delete"))
   {
      zdialog_fetch(zd,"tagname",tagname,metatagXcc);                                  //  get tag name from dialog
      zdialog_stuff(zd,"tagval","");                                                   //  clear tag data in dialog
      *tagval = 0;                                                                     //  and in memory
      strCompress(tagname);
      tagname1[0] = tagname;
      tagval1[0] = tagval;
      err = meta_put(curr_file,tagname1,tagval1,1);                                    //  metadata --> file
      if (err) zmessageACK(Mwin,TX("metadata update error"));
      if (zd_metaview) meta_view(0);                                                   //  update metadata view if active
   }

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

   if (zd->zstat == 1)                                                                 //  short list
   {
      zd->zstat = 0;                                                                   //  keep dialog active
      mtext = zdialog_gtkwidget(zd,"mtext");                                           //  make clickable metadata list
      txwidget_clear(mtext);

      fid = fopen(meta_picklist_file,"r");                                             //  get list of metadata tags
      if (fid) {
         while ((pp = fgets_trim(buff,1000,fid))) {
            if (strlen(pp) > 79) pp[79] = 0;
            strCompress(ppc1,pp);                                                      //  exclude tags indexed by default       25.1
            for (ii = 0; ii < NKX; ii++) {
               strCompress(ppc2,tagnamex[ii]);
               if (strcasestr(ppc1,ppc2)) break;
            }
            if (ii < NKX) continue;
            txwidget_append(mtext,0,"%s\n",pp);
         }
         fclose(fid);
      }

      whichlist = 1;
   }

   else if (zd->zstat == 2)                                                            //  full list
   {
      if (Fwarn) zmessageACK(Mwin,warnmess);                                           //  25.1
      Fwarn = 0;

      zd->zstat = 0;                                                                   //  keep dialog active
      mtext = zdialog_gtkwidget(zd,"mtext");                                           //  make clickable metadata list
      txwidget_clear(mtext);

      file2 = zescape_quotes(curr_file);
      snprintf(command,CCC,"exiftool -m -S \"%s\" ",file2);                            //  exiftool command
      zfree(file2);

      fid = popen(command,"r");                                                        //  get command outputs
      if (fid) {
         while ((pp = fgets_trim(buff,1000,fid))) {
            ppp = strchr(pp,':');
            if (! ppp) continue;
            *ppp = 0;
            txwidget_append(mtext,0,"%s\n",pp);                                        //  add to report window
         }
         pclose(fid);
      }

      whichlist = 2;
   }

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

   return 1;
}


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

int meta_edit_any_clickfunc(GtkWidget *widget, int line, int pos, ch *input)
{
   using namespace meta_edit_any_names;

   ch        *pp, *tagval1[1];
   ch        *tagname1[1];

   if (! zd_editanymeta) return 1;
   if (! curr_file) return 1;

   if (*input == GDK_KEY_F1) {                                                         //  key F1 pressed, show help
      showz_docfile(Mwin,UGfile,F1_help_topic);
      return 1;
   }

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

   zdialog_stuff(zd_editanymeta,"tagname",pp);

   zdialog_fetch(zd_editanymeta,"tagname",tagname,metatagXcc);                         //  get tag name from dialog
   strCompress(tagname);

   tagname1[0] = tagname;                                                              //  look for tag data
   meta_get(curr_file,tagname1,tagval1,1);
   if (tagval1[0]) {
      strncpy0(tagval,tagval1[0],metadataXcc);
      zfree(tagval1[0]);
   }
   else *tagval = 0;
   zdialog_stuff(zd_editanymeta,"tagval",tagval);                                      //  stuff into dialog

   return 1;
}


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

//  delete metadata, specific tag or all data

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

   zdialog     *zd;
   ch          *pp;
   int         err;

   F1_help_topic = TX("DELETE META");

   printf("m_meta_delete \n");

   if (FGM != 'F' && FGM != 'G') return;

   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);
      clicked_file = 0;
   }

   if (! curr_file) {
      zmessageACK(Mwin,TX("no current file"));
      return;
   }

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

/***
       _________________________________________
      |           Delete Metadata               |
      |                                         |
      | File: [______________________________]  |
      |                                         |
      | (o) ALL  (o) One Tag: [______________]  |
      |                                         |
      |                             [Apply] [X] |
      |_________________________________________|

***/

   if (! zd_deletemeta)
   {
      zd = zdialog_new(TX("Delete Metadata"),Mwin,TX("Apply"),"X",null);
      zd_deletemeta = zd;
      zdialog_add_widget(zd,"hbox","hbf","dialog");
      zdialog_add_widget(zd,"label","labf","hbf",TX("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","tall","hb1",TX("All"),"space=5");
      zdialog_add_widget(zd,"radio","tag1","hb1",TX("One Tag:"));
      zdialog_add_widget(zd,"zentry","tagval","hb1",0,"size=20");
      zdialog_stuff(zd,"tag1",1);
      zdialog_run(zd,meta_delete_dialog_event,"parent");
   }

   zd = zd_deletemeta;
   pp = "";
   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, ch *event)
{
   int         tall, tag1;
   ch          *file2;
   ch          tagval[200];

   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,"tall",tall);
   zdialog_fetch(zd,"tag1",tag1);
   zdialog_fetch(zd,"tagval",tagval,200);
   strCompress(tagval);

   if (! tall && ! tag1) return 1;

   file2 = zescape_quotes(curr_file);

   if (tall)                                                                           //  update file metadata
      zshell("log ack","exiftool -m -q -overwrite_original -all= \"%s\"",file2);
   else if (tag1)
      zshell("log ack","exiftool -m -q -overwrite_original -%s= \"%s\"",tagval,file2);
   zfree(file2);

   file_to_xxrec(curr_file);                                                           //  update xxrec_tab[]

   if (zd_metaview) meta_view(0);                                                      //  update metadata view if active

   return 1;
}


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

//  copy metadata from one image to another

void m_meta_copy(GtkWidget *, ch *menu)
{
   int  meta_copy_dialog_event(zdialog *zd, ch *event);

   F1_help_topic = TX("COPY META");

   printf("m_meta_copy \n");

   viewmode('G');

/***
       _______________________________________________
      |              Copy Metadata                    |
      |                                               |
      | source file: [_____________________] [Browse] |
      | target file: [_____________________] [Browse] |
      |                                               |
      |                                   [Apply] [X] |
      |_______________________________________________|

***/

   zdialog *zd = zdialog_new(TX("Copy Metadata"),Mwin,TX("Apply"),"X",null);
   zdialog_add_widget(zd,"hbox","hbs","dialog",0,"expand|space=3");
   zdialog_add_widget(zd,"label","labs","hbs",TX("source file:"),"space=3");
   zdialog_add_widget(zd,"zentry","sfile","hbs",0,"expand|space=3");
   zdialog_add_widget(zd,"button","sbrowse","hbs",TX("Browse"),"space=3");
   zdialog_add_widget(zd,"hbox","hbt","dialog",0,"expand|space=3");
   zdialog_add_widget(zd,"label","labt","hbt",TX("target file:"),"space=3");
   zdialog_add_widget(zd,"zentry","tfile","hbt",0,"expand|space=3");
   zdialog_add_widget(zd,"button","tbrowse","hbt",TX("Browse"),"space=3");

   zdialog_resize(zd,400,0);
   zdialog_run(zd,meta_copy_dialog_event,"parent");

   return;
}


//  dialog event and completion callback function

int  meta_copy_dialog_event(zdialog *zd, ch *event)
{
   int      err = 0;
   ch       *pp;
   ch       sfile[XFCC], tfile[XFCC];

   if (strmatch(event,"sbrowse"))                                                      //  choose source file
   {
      zdialog_show(zd,0);
      pp = select_files1(0);
      if (pp) zdialog_stuff(zd,"sfile",pp);
      if (pp) zfree(pp);
      zdialog_show(zd,1);
   }

   if (strmatch(event,"tbrowse"))                                                      //  choose target file
   {
      zdialog_show(zd,0);
      pp = select_files1(0);
      if (pp) zdialog_stuff(zd,"tfile",pp);
      if (pp) zfree(pp);
      zdialog_show(zd,1);
   }

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

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

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

   zdialog_fetch(zd,"sfile",sfile,XFCC);                                               //  get source and target files
   zdialog_fetch(zd,"tfile",tfile,XFCC);

   if (! regfile(sfile)) {                                                             //  validate source file
      zmessageACK(Mwin,TX("file not found: %s"),sfile);
      return 1;
   }

   if (! regfile(tfile)) {                                                             //  validate target file
      zmessageACK(Mwin,TX("file not found: %s"),tfile);
      return 1;
   }

   err = access(tfile,W_OK);                                                           //  test target file permissions
   if (err) {
      zmessageACK(Mwin,TX("no write permission: %s"),tfile);
      return 1;
   }

   printf("copy metadata from %s \n   to %s \n",sfile,tfile);
   err = meta_copy(sfile,tfile,0,0,0);                                                 //  copy metadata, update xxrec_tab[]
   if (err) zmessageACK(Mwin,TX("metadata update error: %s"),tfile);

   zdialog_free(zd);                                                                   //  done

   return 1;
}


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

//  menu function
//  fix malformed metadata that prevents exiftool() from working

void m_meta_fix(GtkWidget *, ch *menu)
{
   int  meta_fix_dialog_event(zdialog *zd, ch *event);

   int      yn, zstat;
   ch       file[XFCC], *file2;
   FTYPE    ftype;
   ch       *pp, command[XFCC+100];
   ch       *tooloptions = "-all= -tagsfromfile @ -all:all -unsafe "
                           "-icc_profile -overwrite_original";

   F1_help_topic = TX("FIX META");

   printf("m_meta_fix \n");
   
/***
          ______________________________________
         |        Fix Broken Metadata           |
         |                                      |
         | File: [____________________________] |
         |                                      |
         |                          [Apply] [X] |
         |______________________________________|

***/

   zdialog *zd = zdialog_new(TX("Fix Broken Metadata"),Mwin,TX("Apply"),"X",null);     //  allow any file                        26.3
   zdialog_add_widget(zd,"hbox","hbf","dialog",0,"expand|space=3");
   zdialog_add_widget(zd,"label","labf","hbf",TX("File"),"space=3");
   zdialog_add_widget(zd,"zentry","file","hbf",0,"expand|space=3");
   
   if (curr_file) zdialog_stuff(zd,"file",curr_file);                                  //  default current file                  26.3
   
   zdialog_resize(zd,400,0);
   zdialog_run(zd,0,"parent");
   zstat = zdialog_wait(zd);

   zdialog_fetch(zd,"file",file,XFCC);
   zdialog_free(zd);
   if (zstat != 1) return;
   
   ftype = image_file_type(file);
   if (ftype != IMAGE && ftype != RAW) {
      zmessageACK(Mwin,TX("not a known image file"));
      return;
   }

   pp = strrchr(file,'/');
   if (! pp) return;
   yn = zmessageYN(Mwin,TX("repair metadata for %s"),pp+1);
   if (! yn) return;

   file2 = zescape_quotes(file);
   snprintf(command,XFCC+100,"exiftool %s \"%s\" ",tooloptions,file2);
   zshell("log",command);
   zfree(file2);

   file_to_xxrec(file);                                                                //  update xxrec_tab[]

   zmessageACK(Mwin,TX("completed"));
   return;
}


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

//  manage keywords function - auxiliary dialog

zdialog  *zdmanagekeywords = 0;

void m_meta_manage_keywords(GtkWidget *, ch *menu)
{
   int   manage_defkeywords_clickfunc(GtkWidget *widget, int line, int pos, ch *input);
   int   managekeywords_dialog_event(zdialog *zd, ch *event);

   GtkWidget   *widget;
   zdialog     *zd;

   F1_help_topic = TX("MANAGE KEYWORDS");

   printf("m_meta_manage_keywords \n");

/***
          ______________________________________________________________
         |                     Manage Keywords                          |
         |                                                              |
         | category [__________]  keyword [_________] [Create] [Delete] |
         |                                                              |
         | Defined Keywords: _________________________________________  |
         | |                                                          | |
         | | category1: keyword11, keyword12, keyword13 ...           | |
         | | category2: keyword21, keyword22, keyword23 ...           | |
         | |  ...                                                     | |
         | |                                                          | |
         | |                                                          | |
         | |                                                          | |
         | |__________________________________________________________| |
         |                                                              |
         |                                                         [OK] |
         |______________________________________________________________|

***/

   if (zdmanagekeywords) return;
   zd = zdialog_new(TX("Manage Keywords"),Mwin,"OK",null);
   zdmanagekeywords = zd;

   zdialog_add_widget(zd,"hbox","hb7","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labcatg","hb7","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","labkeyword","hb7","keyword","space=5");
   zdialog_add_widget(zd,"zentry","keyword","hb7",0,"size=20|expand");
   zdialog_add_widget(zd,"label","space","hb7",0,"space=5");
   zdialog_add_widget(zd,"button","create","hb7",TX("Create"));
   zdialog_add_widget(zd,"button","delete","hb7",TX("Delete"),"space=3");

   zdialog_add_widget(zd,"hbox","hb8","dialog");
   zdialog_add_widget(zd,"label","labdefkeywords","hb8",TX("Defined Keywords:"),"space=5");
   zdialog_add_widget(zd,"hbox","hb9","dialog",0,"expand");
   zdialog_add_widget(zd,"scrwin","scrwin8","hb9",0,"expand");
   zdialog_add_widget(zd,"text","defkeywords","scrwin8",0,"wrap");

   widget = zdialog_gtkwidget(zd,"defkeywords");                                       //  defkeywords widget mouse/KB event func
   txwidget_set_eventfunc(widget,manage_defkeywords_clickfunc);

   load_defkeywords(0);                                                                //  stuff defined keywords into dialog
   defkeywords_stuff(zd,"ALL");

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

   return;
}


//  mouse click functions for widget having keywords

int manage_defkeywords_clickfunc(GtkWidget *widget, int line, int pos, ch *input)      //  keyword or keyword category was clicked
{
   ch     *txkeyword, end = 0;

   if (*input == GDK_KEY_F1) {                                                         //  key F1 pressed, show help
      showz_docfile(Mwin,UGfile,F1_help_topic);
      return 1;
   }

   txkeyword = txwidget_word(widget,line,pos,",;:",end);
   if (! txkeyword) return 1;

   if (end == ':') zdialog_stuff(zdmanagekeywords,"catg",txkeyword);                   //  selected category >> dialog widget
   else zdialog_stuff(zdmanagekeywords,"keyword",txkeyword);                           //  selected keyword >> dialog widget

   zfree(txkeyword);
   return 1;
}


//  dialog event and completion callback function

int managekeywords_dialog_event(zdialog *zd, ch *event)
{

   ch          keyword[keywordXcc], catg[keywordXcc];
   int         err, changed = 0;

   if (zd->zstat)                                                                      //  [OK] or [x]
   {
      zdialog_free(zd);
      zdmanagekeywords = 0;
      return 1;
   }

   if (strmatch(event,"create")) {                                                     //  add new keyword to defined keywords
      zdialog_fetch(zd,"catg",catg,keywordXcc);
      zdialog_fetch(zd,"keyword",keyword,keywordXcc);
      err = add_defkeyword(catg,keyword);
      if (! err) changed++;
   }

   if (strmatch(event,"delete")) {                                                     //  remove keyword from defined keywords
      zdialog_fetch(zd,"keyword",keyword,keywordXcc);
      zdialog_fetch(zd,"catg",catg,keywordXcc);
      if (*keyword) {
         del_defkeyword(keyword);
         changed++;
      }
      else if (*catg) {
         del_defcatg(catg);
         changed++;
      }
   }

   if (changed) {
      save_defkeywords();                                                              //  save keyword updates to file
      defkeywords_stuff(zd,"ALL");                                                     //  update dialog "defkeywords" window
      if (zd_editmeta)                                                                 //  and edit metadata dialog if active
         defkeywords_stuff(zd_editmeta,"ALL");
      if (zd_batchkeywords)                                                            //  and batch keywords dialog if active
         defkeywords_stuff(zd_batchkeywords,"ALL");
   }

   return 1;
}


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

//  Choose metadata tags for captions on top of the current image.

void m_meta_choose_caps(GtkWidget *, ch *menu)
{
   zlist_t  *Zcapstags;

   F1_help_topic = TX("CAPTIONS");

   printf("m_meta_choose_caps \n");

   Zcapstags = zlist_from_file(capstags_file);                                         //  get current metadata tags

   if (! Zcapstags) {
      Zcapstags = zlist_new(0);                                                        //  file missing, make default list
      zlist_append(Zcapstags,"filename",1);
   }

   select_meta_tags(Zcapstags,maxcaptags,0);                                           //  user edit tag list

   zlist_to_file(Zcapstags,capstags_file);                                             //  save changes to file

   meta_show_caps(1);
   return;
}


//  show captions text on current image

void meta_show_caps(int show)
{
   zlist_t  *Zcapstags;
   ch       *pp, *mtags[maxcaptags], *metatext[maxcaptags];
   ch       captext1[capsXcc], **captext2;
   int      ii, nn, Ncaps, cc1, cc2;

   if (! curr_file) return;

   if (! show)
   {
      erase_toptext(1);
      Fpaintnow();
      Fcaps = 0;
      return;
   }

   Zcapstags = zlist_from_file(capstags_file);                                         //  get current metadata tags

   if (! Zcapstags) {
      Zcapstags = zlist_new(1);                                                        //  file missing, make default list
      zlist_put(Zcapstags,"filename",0);                                               //    (show file name)    bugfix
   }

   Ncaps = zlist_count(Zcapstags);                                                     //  tag count
   if (! Ncaps) return;

   for (ii = 0; ii < Ncaps; ii++) {                                                    //  get metadata tags
      pp = zlist_get(Zcapstags,ii);
      mtags[ii] = zstrdup(pp,"capstags");
   }

   meta_get(curr_file,mtags,metatext,Ncaps);                                           //  get metadata text for input tags

   cc1 = 0;

   for (ii = 0; ii < Ncaps; ii++)                                                      //  put text strings together
   {                                                                                   //    with \n separators
      if (! metatext[ii]) continue;
      cc2 = strlen(metatext[ii]);
      if (cc1 + 2 + cc2 > capsXcc) cc2 = capsXcc - cc1 - 2;
      if (cc2 < 1) break;
      if (cc1 > 0) captext1[cc1++] = '\n';
      strncpy(captext1+cc1,metatext[ii],cc2);
      cc1 += cc2;
   }

   captext1[cc1] = 0;

   if (cc1 == 0) return;                                                               //  no captions

   nn = breakup_text(captext1,captext2,0,captext_cc[0],captext_cc[1]);                 //  break into lines within user limits

   cc1 = 0;
   for (ii = 0; ii < nn; ii++) {                                                       //  combine lines with \n separators
      cc2 = strlen(captext2[ii]);
      if (cc1 + cc2 + 2 > capsXcc) cc2 = capsXcc - cc1 - 2;
      if (cc2 < 1) break;
      if (cc1) captext1[cc1++] = '\n';
      strcpy(captext1+cc1,captext2[ii]);
      cc1 += cc2;
      zfree(captext2[ii]);
   }

   erase_toptext(1);
   add_toptext(1,0,0,captext1,zfuncs::appfont);
   Fpaintnow();
   Fcaps = 1;

   return;
}


//  Show popup window with title and description in image top left corner

void meta_popup_title(int onoff)                                                       //  25.0
{
   static int  Fonoff = 0;

   ch    *title, *description;
   ch    *tagname[2] = { meta_title_tag, meta_desc_tag };
   ch    *tagval[2];
   ch    text[1000];
   ch    **text2;
   int   ii, nn;

   if (! curr_file) return;                                                            //  no image file
   if (CEF) return;                                                                    //  edit function active
   if (onoff == Fonoff) return;                                                        //  no change in popup status

   Fonoff = onoff;                                                                     //  toggle status on <--> off

   if (onoff)                                                                          //  show popup
   {
      tagval[0] = tagval[1] = 0;
      meta_get(curr_file,tagname,tagval,2);                                            //  get image title and description
      if (tagval[0]) title = tagval[0];
      else title = TX("no title");
      if (tagval[1]) description = tagval[1];
      else description = TX("no description");
      snprintf(text,1000,"%s\n%s",title,description);                                  //  combine title & description

      nn = breakup_text(text,text2,".,;-",captext_cc[0],captext_cc[1]);                //  wrap text lines (user min/max setting)

      *text = 0;
      for (ii = 0; ii < nn; ii++) {
         if (ii < nn-1) strncatv(text,1000,text2[ii],"\n",null);
         else strncatv(text,1000,text2[ii],null);
      }

      poptext_mouse(text,20,20,0,0);                                                   //  popup window, no time limit

      for (ii = 0; ii < nn; ii++)                                                      //  free memory
         zfree(text2[ii]);
      zfree(text2);

      if (tagval[0]) zfree(tagval[0]);                                                 //  free memory
      if (tagval[1]) zfree(tagval[1]);
   }

   else
   {
      poptext_killnow();
      onoff = 0;
   }

   return;
}


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

//  toggle display of metadata text at the top of the displayed image file

void m_meta_toggle_caps(GtkWidget *, ch *menu)
{
   F1_help_topic = TX("CAPTIONS");

   Fcaps = 1 - Fcaps;
   meta_show_caps(Fcaps);
   return;
}


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

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

namespace batchkeywords
{
   ch          addkeywords[batchkeywordsXcc];                                          //  keywords to add, list
   ch          delkeywords[batchkeywordsXcc];                                          //  keywords to remove, list
   int         radadd, raddel;                                                         //  dialog radio buttons
   ch          countmess[80];
}


void m_batch_keywords(GtkWidget *, ch *menu)                                           //  combine batch add/del keywords
{
   using namespace batchkeywords;

   int  batch_addkeywords_clickfunc(GtkWidget *widget, int line, int pos, ch *input);
   int  batch_delkeywords_clickfunc(GtkWidget *widget, int line, int pos, ch *input);
   int  batch_matchkeywords_clickfunc(GtkWidget *widget, int line, int pos, ch *input);
   int  batch_defkeywords_clickfunc(GtkWidget *widget, int line, int pos, ch *input);
   int  batch_keywords_dialog_event(zdialog *zd, ch *event);

   ch          *pkeyword, *file;
   int         zstat, ii, jj, err;
   zdialog     *zd, *zdpop;
   GtkWidget   *widget;
   xxrec_t     *xxrec;
   ch          *tagname[1], *tagval[1];

   F1_help_topic = TX("BATCH KEYWORDS");

   printf("m_batch_keywords \n");

   if (Xindexlev < 1) {
      index_rebuild(1,0);                                                              //  25.1
      if (Nxxrec == 0) {
         zmessageACK(Mwin,TX("image index required"));
         return;
      }
   }

   if (Fblock("batch keywords")) return;

/***
          ________________________________________________________
         |           Batch Add/Remove Keywords                    |
         |                                                        |
         |  [Select Files]  NN files selected                     |
         |                                                        |
         |  (o) keywords to add    [____________________________] |
         |  (o) keywords to remove [____________________________] |
         |  - - - - - - - - - - - - - - - - - - - - - - - - - - - |
         |  Enter New Keyword [___________] [Add]                 |
         |  Matching Keywords [_________________________________] |
         |  - - - - - - - - - - - - - - - - - - - - - - - - - - - |
         |  Keywords Category [________________|v]              | |
         |  |                                                   | |
         |  |                                                   | |
         |  |                                                   | |
         |  |                                                   | |
         |  |                                                   | |
         |  |                                                   | |
         |  |___________________________________________________| |
         |                                                        |
         |                                          [Proceed] [X] |
         |________________________________________________________|

***/

   zd = zdialog_new(TX("Batch Add/Remove Keywords"),Mwin,TX("Proceed"),"X",null);
   zd_batchkeywords = zd;

   //  [Select Files]  NN files selected
   zdialog_add_widget(zd,"hbox","hbfiles","dialog",0,"space=3");
   zdialog_add_widget(zd,"button","files","hbfiles",TX("Select Files"),"space=5");
   zdialog_add_widget(zd,"label","labcount","hbfiles",TX("no files selected"),"space=10");

   //  (o) keywords to add    [_______________________________________]
   //  (o) keywords to remove [_______________________________________]
   zdialog_add_widget(zd,"hbox","hbkeywords","dialog",0,"space=3");
   zdialog_add_widget(zd,"vbox","vb1","hbkeywords",0,"space=3|homog");
   zdialog_add_widget(zd,"vbox","vb2","hbkeywords",0,"space=3|homog|expand");
   zdialog_add_widget(zd,"radio","radadd","vb1",TX("keywords to add"));
   zdialog_add_widget(zd,"radio","raddel","vb1",TX("keywords to remove"));
   zdialog_add_widget(zd,"text","addkeywords","vb2",0,"expand|wrap");
   zdialog_add_widget(zd,"text","delkeywords","vb2",0,"expand|wrap");

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

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

   //  Matching Keywords [____________________________________________]
   zdialog_add_widget(zd,"hbox","hbmt","dialog",0,"space=1");
   zdialog_add_widget(zd,"label","labmt","hbmt",TX("Matching Keywords"),"space=3");
   zdialog_add_widget(zd,"text","matchkeywords","hbmt",0,"expand|wrap");

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

   //  Keywords Category [____________________|v]
   zdialog_add_widget(zd,"hbox","hbdt1","dialog");
   zdialog_add_widget(zd,"label","labdt","hbdt1",TX("Keywords Category"),"space=3");
   zdialog_add_widget(zd,"combo","defcats","hbdt1",0,"expand|space=10|size=20");

   zdialog_add_widget(zd,"hbox","hbdt2","dialog",0,"expand");
   zdialog_add_widget(zd,"scrwin","swdt2","hbdt2",0,"expand");
   zdialog_add_widget(zd,"text","defkeywords","swdt2",0,"wrap");

   zdialog_stuff(zd,"radadd",1);                                                       //  initz. radio buttons
   zdialog_stuff(zd,"raddel",0);

   load_defkeywords(0);                                                                //  stuff defined keywords into dialog
   defkeywords_stuff(zd,"ALL");
   defcats_stuff(zd);                                                                  //  and defined categories

   *addkeywords = *delkeywords = 0;

   snprintf(countmess,80,TX("%d image files selected"),SFcount);                       //  show selected files count
   zdialog_stuff(zd,"labcount",countmess);

   widget = zdialog_gtkwidget(zd,"addkeywords");                                       //  keyword widget mouse/KB event funcs
   txwidget_set_eventfunc(widget,batch_addkeywords_clickfunc);

   widget = zdialog_gtkwidget(zd,"delkeywords");
   txwidget_set_eventfunc(widget,batch_delkeywords_clickfunc);

   widget = zdialog_gtkwidget(zd,"matchkeywords");
   txwidget_set_eventfunc(widget,batch_matchkeywords_clickfunc);

   widget = zdialog_gtkwidget(zd,"defkeywords");
   txwidget_set_eventfunc(widget,batch_defkeywords_clickfunc);

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

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

   zdialog_free(zd);
   zd_batchkeywords = 0;

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

   zdpop = popup_report_open("Batch Keywords",Mwin,500,200,0,0,0,"X",null);            //  status report popup window

   zadd_locked(NFbusy,+1);

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

      if (! zdialog_valid(zdpop)) break;                                               //  report canceled

      file = SelFiles[ii];                                                             //  display image

      popup_report_write2(zdpop,0,"%s \n",file);                                       //  report progress

      err = access(file,W_OK);                                                         //  test file can be written by me
      if (err) {
         popup_report_write2(zdpop,1,TX("no write permission \n"));
         continue;
      }

      xxrec = get_xxrec(file);
      if (! xxrec) continue;                                                           //  deleted, not image file

      if (xxrec->keywords)
         strncpy0(edit_keywords,xxrec->keywords,filekeywordsXcc);                      //  copy keywords for editing

      for (jj = 1; ; jj++)                                                             //  remove keywords if present
      {
         pkeyword = (ch *) substring(delkeywords,",;",jj);
         if (! pkeyword) break;
         if (*pkeyword == 0) continue;
         err = del_keyword(pkeyword,edit_keywords);
         if (err) continue;
      }

      for (jj = 1; ; jj++)                                                             //  add new keywords unless already
      {
         pkeyword = (ch *) substring(addkeywords,",;",jj);
         if (! pkeyword) break;
         if (*pkeyword == 0) continue;
         err = add_keyword(pkeyword,edit_keywords,filekeywordsXcc);
         if (err == 2) {
            zmessageACK(Mwin,TX("%s: too many keywords"),file);
            break;
         }
      }

      tagname[0] = meta_keywords_tag;                                                  //  update file metadata
      tagval[0] = edit_keywords;
      err = meta_put(file,tagname,tagval,1);
      if (err) popup_report_write2(zdpop,1,TX("file metadata update failed"));
   }

   if (! zdialog_valid(zdpop))
      printf("*** report canceled \n");
   else popup_report_write2(zdpop,0,TX("\n*** COMPLETED \n"));
   popup_report_bottom(zdpop);

   zadd_locked(NFbusy,-1);

   load_defkeywords(1);                                                                //  update defined keywords list

   Fblock(0);
   return;
}


//  mouse click functions for widgets holding keywords

int batch_addkeywords_clickfunc(GtkWidget *widget, int line, int pos, ch *input)       //  a keyword in the add list was clicked
{
   using namespace batchkeywords;

   ch     *txkeyword, end;

   if (*input == GDK_KEY_F1) {                                                         //  key F1 pressed, show help
      showz_docfile(Mwin,UGfile,F1_help_topic);
      return 1;
   }

   txkeyword = txwidget_word(widget,line,pos,",;",end);
   if (! txkeyword) return 1;

   del_keyword(txkeyword,addkeywords);                                                 //  remove keyword from list
   zdialog_stuff(zd_batchkeywords,"addkeywords",addkeywords);

   zfree(txkeyword);
   return 1;
}


int batch_delkeywords_clickfunc(GtkWidget *widget, int line, int pos, ch *input)       //  a keyword in the remove list was clicked
{
   using namespace batchkeywords;

   ch     *txkeyword, end;

   if (*input == GDK_KEY_F1) {                                                         //  key F1 pressed, show help
      showz_docfile(Mwin,UGfile,F1_help_topic);
      return 1;
   }

   txkeyword = txwidget_word(widget,line,pos,",;",end);
   if (! txkeyword) return 1;

   del_keyword(txkeyword,delkeywords);                                                 //  remove keyword from list
   zdialog_stuff(zd_batchkeywords,"delkeywords",delkeywords);

   zfree(txkeyword);
   return 1;
}


int batch_matchkeywords_clickfunc(GtkWidget *widget, int line, int pos, ch *input)     //  matching keyword was clicked
{
   using namespace batchkeywords;

   ch     *txkeyword, end = 0;

   if (*input == GDK_KEY_F1) {                                                         //  key F1 pressed, show help
      showz_docfile(Mwin,UGfile,F1_help_topic);
      return 1;
   }

   txkeyword = txwidget_word(widget,line,pos,",;",end);
   if (! txkeyword) return 1;

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

   if (radadd) {
      add_keyword(txkeyword,addkeywords,batchkeywordsXcc);                             //  add recent keyword to keyword add list
      zdialog_stuff(zd_batchkeywords,"addkeywords",addkeywords);
   }
   else {
      add_keyword(txkeyword,delkeywords,batchkeywordsXcc);                             //  add recent keyword to keyword remove list
      zdialog_stuff(zd_batchkeywords,"delkeywords",delkeywords);
   }

   zdialog_stuff(zd_batchkeywords,"newkeyword","");                                    //  clear newkeyword and matchkeywords
   zdialog_stuff(zd_batchkeywords,"matchkeywords","");

   zdialog_goto(zd_batchkeywords,"newkeyword");                                        //  put focus back on newkeyword widget

   zfree(txkeyword);
   return 1;
}


int batch_defkeywords_clickfunc(GtkWidget *widget, int line, int pos, ch *input)       //  a defined keyword was clicked
{
   using namespace batchkeywords;

   ch       *txkeyword, end;
   int      radadd;

   if (*input == GDK_KEY_F1) {                                                         //  key F1 pressed, show help
      showz_docfile(Mwin,UGfile,F1_help_topic);
      return 1;
   }

   txkeyword = txwidget_word(widget,line,pos,",;:",end);
   if (! txkeyword || end == ':') return 1;                                            //  nothing or keyword category, ignore

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

   if (radadd) {
      add_keyword(txkeyword,addkeywords,batchkeywordsXcc);                             //  add defined keyword to keyword add list
      zdialog_stuff(zd_batchkeywords,"addkeywords",addkeywords);
   }
   else {
      add_keyword(txkeyword,delkeywords,batchkeywordsXcc);                             //  add defined keyword to keyword remove list
      zdialog_stuff(zd_batchkeywords,"delkeywords",delkeywords);
   }

   zfree(txkeyword);
   return 1;
}


//  batchkeywords dialog event function

int batch_keywords_dialog_event(zdialog *zd, ch *event)
{
   using namespace batchkeywords;

   ch       catgname[keywordXcc];
   int      ii, jj, nt, cc1, cc2, ff;
   ch       *pp1, *pp2;
   ch       newkeyword[keywordXcc], matchkeywords[20][keywordXcc];
   ch       matchkeywordstext[(keywordXcc+2)*20];

   if (zd->zstat)                                                                      //  dialog completed
   {
      if (zd->zstat == 1) {                                                            //  proceed
         if (! SFcount || (*addkeywords <= ' ' && *delkeywords <= ' ')) {
            zmessageACK(Mwin,TX("specify files and keywords"));
            zd->zstat = 0;                                                             //  keep dialog active
         }
      }
      else zd_batchkeywords = 0;
      return 1;                                                                        //  cancel
   }

   if (strmatch(event,"files"))                                                        //  select images to process
   {
      zdialog_show(zd,0);                                                              //  hide parent dialog
      select_files(0);                                                                 //  get new list
      zdialog_show(zd,1);
      snprintf(countmess,80,TX("%d image files selected"),SFcount);
      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 keyword category selection
      zdialog_fetch(zd,"defcats",catgname,keywordXcc);
      defkeywords_stuff(zd,catgname);
   }

   if (strmatch(event,"newkeyword"))                                                   //  new keyword is being typed in
   {
      zdialog_stuff(zd,"matchkeywords","");                                            //  clear matchkeywords in dialog

      zdialog_fetch(zd,"newkeyword",newkeyword,keywordXcc);                            //  get chars. typed so far
      cc1 = strlen(newkeyword);

      for (ii = jj = 0; ii <= cc1; ii++) {                                             //  remove foul characters
         if (strchr(",:;",newkeyword[ii])) continue;
         newkeyword[jj++] = newkeyword[ii];
      }

      if (jj < cc1) {                                                                  //  something was removed
         newkeyword[jj] = 0;
         cc1 = jj;
         zdialog_stuff(zd,"newkeyword",newkeyword);
      }

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

      for (ii = nt = 0; ii < maxkeywordcats; ii++)                                     //  loop all categories
      {
         pp2 = defined_keywords[ii];                                                   //  category: aaaaaa, bbbbb, ... keywordN,
         if (! pp2) continue;                                                          //            |     |
         pp2 = strchr(pp2,':');                                                        //            pp1   pp2

         while (true)                                                                  //  loop all defkeywords in category
         {
            pp1 = pp2 + 2;
            if (! *pp1) break;
            pp2 = strchr(pp1,',');
            if (! pp2) break;
            if (strmatchcaseN(newkeyword,pp1,cc1)) {                                   //  defkeyword matches chars. typed so far
               cc2 = pp2 - pp1;
               strncpy(matchkeywords[nt],pp1,cc2);                                     //  save defkeywords that match
               matchkeywords[nt][cc2] = 0;
               if (++nt == 20) return 1;                                               //  quit if 20 matches or more
            }
         }
      }

      if (nt == 0) return 1;                                                           //  no matches

      pp1 = matchkeywordstext;

      for (ii = 0; ii < nt; ii++)                                                      //  make defkeyword list: aaaaa, bbb, cccc ...
      {
         strcpy(pp1,matchkeywords[ii]);
         pp1 += strlen(pp1);
         strcpy(pp1,", ");
         pp1 += 2;
      }

      zdialog_stuff(zd,"matchkeywords",matchkeywordstext);                             //  stuff matchkeywords in dialog
      return 1;
   }

   if (strmatch(event,"add"))                                                          //  enter new keyword finished
   {
      zdialog_fetch(zd,"newkeyword",newkeyword,keywordXcc);                            //  get finished keyword
      cc1 = strlen(newkeyword);
      if (! cc1) return 1;
      if (newkeyword[cc1-1] == '\n') {                                                 //  remove newline character
         cc1--;
         newkeyword[cc1] = 0;
      }

      for (ii = ff = 0; ii < maxkeywordcats; ii++)                                     //  loop all categories
      {
         pp2 = defined_keywords[ii];                                                   //  category: aaaaaa, bbbbb, ... keywordN,
         if (! pp2) continue;                                                          //            |     |
         pp2 = strchr(pp2,':');                                                        //            pp1   pp2

         while (true)                                                                  //  loop all defkeywords in category
         {
            pp1 = pp2 + 2;
            if (! *pp1) break;
            pp2 = strchr(pp1,',');
            if (! pp2) break;
            cc2 = pp2 - pp1;
            if (cc2 != cc1) continue;
            if (strmatchcaseN(newkeyword,pp1,cc1)) {                                   //  entered keyword matches defkeyword
               strncpy(newkeyword,pp1,cc1);                                            //  use defkeyword upper/lower case
               ff = 1;
               break;
            }
         }

         if (ff) break;
      }

      if (! ff) {                                                                      //  if new keyword, add to defined keywords
         add_defkeyword("nocatg",newkeyword);
         defkeywords_stuff(zd,"ALL");
      }

      add_keyword(newkeyword,addkeywords,batchkeywordsXcc);                            //  add to keyword add list
      zdialog_stuff(zd_batchkeywords,"addkeywords",addkeywords);

      zdialog_stuff(zd,"newkeyword","");                                               //  update dialog widgets
      zdialog_stuff(zd,"matchkeywords","");

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

   return 1;
}


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

//  Rename multiple keywords for selected image files or all files.
//  Keywords may also be deleted.                                                      //  26.0

namespace batchrenamekeywords
{
   int      Nkeywords;                                                                 //  count, 1-100
   ch       *oldkeywords[100];                                                         //  keywords to rename
   ch       *newkeywords[100];                                                         //  corresponding new name
   #define  tpcc (keywordXcc+keywordXcc+10)
   zdialog  *zd;
}


//  menu function

void m_batch_rename_keywords(GtkWidget *, ch *menu)
{
   using namespace batchrenamekeywords;

   int  batchrenamekeywords_defkeywords_clickfunc(GtkWidget *widget, int line, int pos, ch *input);
   int  batchrenamekeywords_keywordlist_clickfunc(GtkWidget *widget, int line, int pos, ch *input);
   int  batchrenamekeywords_dialog_event(zdialog *zd, ch *event);

   ch          *file;
   int         ii, jj, kk, ff, err, yn;
   int         zstat, Nfiles, Nlist;
   GtkWidget   *widget;
   ch          **filelist;
   ch          *pp, *filekeyword;
   ch          *oldkeywordlist[100], *newkeywordlist[100];
   ch          *tagname[1], *tagval[1];
   xxrec_t     *xxrec;
   zdialog     *zdpop;

   F1_help_topic = TX("BATCH RENAME KEYWORDS");

   printf("m_batch_rename_keywords \n");

   if (Xindexlev < 1) {
      index_rebuild(1,0);                                                              //  25.1
      if (Nxxrec == 0) {
         zmessageACK(Mwin,TX("image index required"));
         return;
      }
   }

   if (Fblock("batch rename keywords")) return;

   Nkeywords = Nfiles = 0;
   filelist = 0;

/***
       ________________________________________________________________________________________
      |        Batch Rename Keywords                   |                                       |
      |                                                | old keyword name >> new keyword name  |
      | Keyword [_______]  Rename to [_________]  [->] | unwanted keyword >> delete            |                                 26.0
      |                                                |                                       |
      | Keywords Category [________________________|v| |                                       |
      | |                                            | |                                       |
      | |                                            | |                                       |
      | |                                            | |                                       |
      | |                                            | |                                       |
      | |                                            | |                                       |
      | |                                            | |                                       |
      | |____________________________________________| |_______________________________________|
      |                                                                                        |
      |                                                                          [Proceed] [X] |
      |________________________________________________________________________________________|

***/

   zd = zdialog_new(TX("Batch Rename Keywords"),Mwin,TX("Proceed"),"X",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");

   //  keyword [_________________]  rename to [___________________]  [-->]
   zdialog_add_widget(zd,"hbox","hbkeywords","vb1",0,"space=3");
   zdialog_add_widget(zd,"label","lab1","hbkeywords","Keyword","space=3");
   zdialog_add_widget(zd,"frame","frot","hbkeywords");
   zdialog_add_widget(zd,"label","oldkeyword","frot",TX("(click below)"));
   zdialog_add_widget(zd,"label","space","hbkeywords",0,"space=5");
   zdialog_add_widget(zd,"label","lab2","hbkeywords",TX("Rename to"),"space=3");
   zdialog_add_widget(zd,"zentry","newkeyword","hbkeywords",0,"expand");
   zdialog_add_widget(zd,"label","space","hbkeywords",0,"space=3");
   zdialog_add_widget(zd,"button","addkeywords","hbkeywords",">>");

   zdialog_add_widget(zd,"hsep","hsep1","vb1",0,"space=5");

   //  Keywords Category [_____________________|v]
   zdialog_add_widget(zd,"hbox","hbdt","vb1",0);
   zdialog_add_widget(zd,"label","labdt","hbdt",TX("Keywords Category"),"space=3");
   zdialog_add_widget(zd,"combo","defcats","hbdt",0,"expand|space=10|size=20");

   zdialog_add_widget(zd,"scrwin","swdt","vb1",0,"expand");
   zdialog_add_widget(zd,"text","defkeywords","swdt",0,"wrap");

   //  old keyword name >> new keyword name
   zdialog_add_widget(zd,"hbox","hblist","vb2");
   zdialog_add_widget(zd,"label","lablist","hblist",TX("old keyword name >> new keyword name"),"space=10");
   zdialog_add_widget(zd,"scrwin","swlist","vb2",0,"expand");
   zdialog_add_widget(zd,"text","keywordlist","swlist");

   load_defkeywords(0);                                                                //  stuff defined keywords into dialog
   defkeywords_stuff(zd,"ALL");
   defcats_stuff(zd);                                                                  //  and defined categories

   widget = zdialog_gtkwidget(zd,"defkeywords");                                       //  connect mouse to defined keywords widget
   txwidget_set_eventfunc(widget,batchrenamekeywords_defkeywords_clickfunc);

   widget = zdialog_gtkwidget(zd,"keywordlist");                                       //  connect mouse to keywordlist widget
   txwidget_set_eventfunc(widget,batchrenamekeywords_keywordlist_clickfunc);

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

   zdialog_run(zd,batchrenamekeywords_dialog_event,0);
   zstat = zdialog_wait(zd);                                                           //  wait for dialog completion
   zdialog_free(zd);
   zd = 0;
   if (zstat != 1) goto cleanup;                                                       //  canceled

   filelist = (ch **) zmalloc(Nxxrec * sizeof(ch *),"batch keywords");                 //  find all affected image files
   Nfiles = 0;

   zdpop = popup_report_open(TX("rename keywords"),Mwin,500,300,0,0,0,"X",null);       //  log report

   zadd_locked(NFbusy,+1);

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

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

      ff = 0;

      for (jj = 1; ; jj++) {
         pp = (ch *) substring(xxrec->keywords,',',jj);
         if (! pp) break;
         for (kk = 0; kk < Nkeywords; kk++) {
            if (strmatchcase(pp,oldkeywords[kk])) {                                    //  this file has one or more keywords
               ff = 1;                                                                 //    that will be renamed
               break;
            }
         }
         if (ff) break;
      }

      if (ff) {
         filelist[Nfiles] = zstrdup(xxrec->file,"batch keywords");                     //  add to list of files to process
         Nfiles++;
         popup_report_write2(zdpop,0,TX("file included: %s \n"),xxrec->file);
      }
   }

   zadd_locked(NFbusy,-1);

   yn = zmessageYN(Mwin,TX("%d keywords to rename \n"
                           "in %d image files. \n"
                           "Proceed?"),Nkeywords,Nfiles);
   if (! yn) goto cleanup;
   if (! Nkeywords) goto cleanup;

   zadd_locked(NFbusy,+1);

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

      if (! zdialog_valid(zdpop)) break;                                               //  report canceled

      file = filelist[ii];
      popup_report_write2(zdpop,0,"%s \n",file);                                       //  report file

      err = access(file,W_OK);                                                         //  test file can be written by me
      if (err) {
         popup_report_write2(zdpop,1,TX("no write permission \n"));
         continue;
      }

      xxrec = get_xxrec(file);
      if (! xxrec) {
         popup_report_write2(zdpop,1,TX("not an indexed image file \n"));                  //  25.3
         continue;
      }

      Nlist = 0;

      for (jj = 1; ; jj++) {                                                           //  loop file keywords
         filekeyword = (ch *) substring(xxrec->keywords,',',jj);
         if (! filekeyword) break;
         for (kk = 0; kk < Nkeywords; kk++) {                                          //  loop keyword replacement list
            if (strmatchcase(filekeyword,oldkeywords[kk])) {                           //  file keyword matches keyword to replace
               oldkeywordlist[Nlist] = oldkeywords[kk];                                //  save old and new keywords
               newkeywordlist[Nlist] = newkeywords[kk];
               Nlist++;
               break;                                                                  //  next file keyword
            }
         }
      }

      strncpy0(edit_keywords,xxrec->keywords,filekeywordsXcc);                         //  copy keywords for editing

      for (jj = 0; jj < Nlist; jj++)                                                   //  remove old keywords
         err = del_keyword(oldkeywordlist[jj],edit_keywords);

      for (jj = 0; jj < Nlist; jj++) {                                                 //  add new keywords - after removals
         if (! newkeywordlist[jj]) continue;                                           //  EOL
         if (strmatch(newkeywordlist[jj],"DELETE")) continue;                          //  no replacement                        26.0
         popup_report_write2(zdpop,0,"%s \n",newkeywordlist[jj]);
         err = add_keyword(newkeywordlist[jj],edit_keywords,filekeywordsXcc);
         if (err && err != 1) popup_report_write2(zdpop,1,"ERROR \n");                 //  ignore already there, else report
      }

      tagname[0] = meta_keywords_tag;                                                  //  update image file keywords
      tagval[0] = edit_keywords;
      err = meta_put(file,tagname,tagval,1);
      if (err) popup_report_write2(zdpop,1,TX("metadata update failed \n"));
   }

   zadd_locked(NFbusy,-1);

   if (! zdialog_valid(zdpop))
      printf("*** report canceled \n");
   else popup_report_write2(zdpop,0,TX("\n*** COMPLETED \n"));

   popup_report_bottom(zdpop);

   load_defkeywords(1);                                                                //  update keyword list

cleanup:                                                                               //  free resources

   Fblock(0);

   for (ii = 0; ii < Nkeywords; ii++) {
      zfree(oldkeywords[ii]);
      zfree(newkeywords[ii]);
   }

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

   return;
}


//  a defined keyword was clicked

int batchrenamekeywords_defkeywords_clickfunc(GtkWidget *widget, int line, int pos, ch *input)
{
   using namespace batchrenamekeywords;

   ch     *txkeyword, end;
   ch     keywordname[keywordXcc];

   if (*input == GDK_KEY_F1) {                                                         //  key F1 pressed, show help
      showz_docfile(Mwin,UGfile,F1_help_topic);
      return 1;
   }

   txkeyword = txwidget_word(widget,line,pos,",;:",end);                               //  clicked word
   if (! txkeyword || end == ':') return 1;                                            //  nothing or keyword category, ignore

   snprintf(keywordname,keywordXcc," %s ",txkeyword);                                  //  add spaces for appearance
   zdialog_stuff(zd,"oldkeyword",keywordname);
   zdialog_stuff(zd,"newkeyword","");

   zfree(txkeyword);
   return 1;
}


//  a keyword list line was clicked - remove it

int batchrenamekeywords_keywordlist_clickfunc(GtkWidget *widget, int line, int pos, ch *input)
{
   using namespace batchrenamekeywords;

   int      ii;

   if (*input == GDK_KEY_F1) {                                                         //  key F1 pressed, show help
      showz_docfile(Mwin,UGfile,F1_help_topic);
      return 1;
   }

   if (line >= Nkeywords) return 1;

   for (ii = line; ii < Nkeywords-1; ii++) {                                           //  remove keywords pair corresponding
      oldkeywords[ii] = oldkeywords[ii+1];                                             //    to the line clicked
      newkeywords[ii] = newkeywords[ii+1];
   }
   Nkeywords--;

   widget = zdialog_gtkwidget(zd,"keywordlist");                                       //  rewrite dialog keyword list
   txwidget_clear(widget);
   for (int ii = 0; ii < Nkeywords; ii++)
      txwidget_append2(widget,0,"%s >> %s\n",oldkeywords[ii],newkeywords[ii]);

   return 1;
}


//  batch rename keywords dialog event function

int batchrenamekeywords_dialog_event(zdialog *zd, ch *event)
{
   using namespace batchrenamekeywords;

   int         ii;
   ch          catgname[keywordXcc];
   ch          oldkeyword[keywordXcc], newkeyword[keywordXcc];
   GtkWidget   *widget;

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

   if (strmatch(event,"defcats")) {                                                    //  new keyword category selection
      zdialog_fetch(zd,"defcats",catgname,keywordXcc);
      defkeywords_stuff(zd,catgname);
   }

   if (strmatch(event,"addkeywords")) {                                                //  [ --> ] button pressed
      zdialog_fetch(zd,"oldkeyword",oldkeyword,keywordXcc);                            //  save new pair of keyword names
      zdialog_fetch(zd,"newkeyword",newkeyword,keywordXcc);
      strTrim2(oldkeyword);
      strTrim2(newkeyword);
      if (*oldkeyword <= ' ' || *newkeyword <= ' ') return 1;
      if (strmatch(newkeyword,"delete")) strcpy(newkeyword,"DELETE");                  //  allow "delete" or "DELETE"            26.0
      if (Nkeywords == 100) {
         zmessageACK(Mwin,TX("max keywords exceeded"));
         return 1;
      }

      for (ii = 0; ii < Nkeywords; ii++)                                               //  check for duplicate                   26.0
         if (strmatchcase(oldkeyword,oldkeywords[ii])) break;
      if (ii < Nkeywords) {
         zmessageACK(Mwin,TX("remove duplicate"));
         return 1;
      }

      oldkeywords[Nkeywords] = zstrdup(oldkeyword,"batch keywords");
      newkeywords[Nkeywords] = zstrdup(newkeyword,"batch keywords");
      Nkeywords++;
   }

   widget = zdialog_gtkwidget(zd,"keywordlist");                                       //  rewrite dialog keyword list
   txwidget_clear(widget);
   for (ii = 0; ii < Nkeywords; ii++)
      txwidget_append2(widget,0,"%s >> %s\n",oldkeywords[ii],newkeywords[ii]);

   return 1;
}


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

//  delete up to 100 keywords from all image files

namespace batchpurgekeywords
{
   int      Nkeywords;                                                                 //  count, 1-100
   ch       *keywordlist[100];                                                         //  keywords to purge
   zdialog  *zd;
}


//  menu function

void m_batch_purge_keywords(GtkWidget *, ch *menu)                                     //  26.0
{
   using namespace batchpurgekeywords;

   int  batchpurgekeywords_defkeywords_clickfunc(GtkWidget *widget, int line, int pos, ch *input);
   int  batchpurgekeywords_keywordlist_clickfunc(GtkWidget *widget, int line, int pos, ch *input);
   int  batchpurgekeywords_dialog_event(zdialog *zd, ch *event);

   ch          *file;
   int         ii, jj, kk, ff, err, yn;
   int         zstat, Nfiles, Nlist;
   ch          **filelist;
   ch          *pp, *filekeyword;
   ch          *tagname[1], *tagval[1];
   GtkWidget   *widget;
   xxrec_t     *xxrec;
   zdialog     *zdpop;

   F1_help_topic = TX("BATCH PURGE KEYWORDS");

   printf("m_batch_purge_keywords \n");

   if (Xindexlev < 1) {
      index_rebuild(1,0);
      if (Nxxrec == 0) {
         zmessageACK(Mwin,TX("image index required"));
         return;
      }
   }

   if (Fblock("batch purge keywords")) return;

   Nkeywords = Nfiles = 0;
   filelist = 0;

/***
       _______________________________________________________________________
      |           Batch Purge Keywords                 |                      |
      |                                                | keyword name 1       |
      | Keywords Category [________________________|v| | keyword name 2       |
      | |                                            | | ...                  |
      | |                                            | |                      |
      | |                                            | |                      |
      | |                                            | |                      |
      | |                                            | |                      |
      | |                                            | |                      |
      | |____________________________________________| |______________________|
      |                                                                       |
      |                                                         [Proceed] [X] |
      |_______________________________________________________________________|

***/

   zd = zdialog_new(TX("Batch Purge Keywords"),Mwin,TX("Proceed"),"X",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");

   //  Keywords Category [_____________________|v]
   zdialog_add_widget(zd,"hbox","hbdt","vb1",0);
   zdialog_add_widget(zd,"label","labdt","hbdt",TX("Keywords Category"),"space=3");
   zdialog_add_widget(zd,"combo","defcats","hbdt",0,"expand|space=10|size=20");
   zdialog_add_widget(zd,"scrwin","swdt","vb1",0,"expand");
   zdialog_add_widget(zd,"text","defkeywords","swdt",0,"wrap");

   //  keyword list to purge
   zdialog_add_widget(zd,"hbox","hblist","vb2");
   zdialog_add_widget(zd,"scrwin","swlist","vb2",0,"expand");
   zdialog_add_widget(zd,"text","keywordlist","swlist");

   load_defkeywords(0);                                                                //  stuff defined keywords into dialog
   defkeywords_stuff(zd,"ALL");
   defcats_stuff(zd);                                                                  //  and defined categories

   widget = zdialog_gtkwidget(zd,"defkeywords");                                       //  connect mouse to defined keywords widget
   txwidget_set_eventfunc(widget,batchpurgekeywords_defkeywords_clickfunc);

   widget = zdialog_gtkwidget(zd,"keywordlist");                                       //  connect mouse to keywordlist widget
   txwidget_set_eventfunc(widget,batchpurgekeywords_keywordlist_clickfunc);

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

   zdialog_run(zd,batchpurgekeywords_dialog_event,0);
   zstat = zdialog_wait(zd);                                                           //  wait for dialog completion
   zdialog_free(zd);
   zd = 0;
   if (zstat != 1) goto cleanup;                                                       //  canceled

   filelist = (ch **) zmalloc(Nxxrec * sizeof(ch *),"batch keywords");                 //  find all affected image files
   Nfiles = 0;

   zdpop = popup_report_open("batch purge keywords",Mwin,500,300,0,0,0,"X",null);      //  log report

   zadd_locked(NFbusy,+1);

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

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

      ff = 0;

      for (jj = 1; ; jj++) {
         pp = (ch *) substring(xxrec->keywords,',',jj);
         if (! pp) break;
         for (kk = 0; kk < Nkeywords; kk++) {
            if (strmatchcase(pp,keywordlist[kk])) {                                    //  this file has one or more keywords
               ff = 1;                                                                 //    that will be purged
               break;
            }
         }
         if (ff) break;
      }

      if (ff) {
         filelist[Nfiles] = zstrdup(xxrec->file,"batch keywords");                     //  add to list of files to process
         Nfiles++;
         popup_report_write2(zdpop,0,TX("file included: %s \n"),xxrec->file);
      }
   }

   zadd_locked(NFbusy,-1);

   yn = zmessageYN(Mwin,TX("%d keywords to purge \n"
                           "in %d image files. \n"
                           "Proceed?"),Nkeywords,Nfiles);
   if (! yn) goto cleanup;
   if (! Nkeywords) goto cleanup;

   zadd_locked(NFbusy,+1);

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

      if (! zdialog_valid(zdpop)) break;                                               //  report canceled

      file = filelist[ii];
      popup_report_write2(zdpop,0,"%s \n",file);                                       //  report file

      err = access(file,W_OK);                                                         //  test file can be written by me
      if (err) {
         popup_report_write2(zdpop,1,TX("no write permission \n"));
         continue;
      }

      xxrec = get_xxrec(file);
      if (! xxrec) {
         popup_report_write2(zdpop,1,TX("not an indexed image file \n"));              //  26.0
         continue;
      }

      Nlist = 0;

      for (jj = 1; ; jj++) {                                                           //  loop file keywords
         filekeyword = (ch *) substring(xxrec->keywords,',',jj);
         if (! filekeyword) break;
         for (kk = 0; kk < Nkeywords; kk++) {                                          //  loop keyword replacement list
            if (strmatchcase(filekeyword,keywordlist[kk])) {                           //  file keyword matches keyword to replace
               Nlist++;
               break;                                                                  //  next file keyword
            }
         }
      }

      strncpy0(edit_keywords,xxrec->keywords,filekeywordsXcc);                         //  copy keywords for editing

      for (jj = 0; jj < Nlist; jj++)                                                   //  remove keywords
         err = del_keyword(keywordlist[jj],edit_keywords);

      tagname[0] = meta_keywords_tag;                                                  //  update image file keywords
      tagval[0] = edit_keywords;
      err = meta_put(file,tagname,tagval,1);                                           //  also updates xxrec_tab[]
      if (err) popup_report_write2(zdpop,1,TX("metadata update failed \n"));
   }

   zadd_locked(NFbusy,-1);

   if (! zdialog_valid(zdpop))
      printf("*** report canceled \n");
   else popup_report_write2(zdpop,0,TX("\n*** COMPLETED \n"));

   popup_report_bottom(zdpop);

   load_defkeywords(1);                                                                //  update keyword list

cleanup:                                                                               //  free resources

   Fblock(0);

   for (ii = 0; ii < Nkeywords; ii++)
      zfree(keywordlist[ii]);

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

   return;
}


//  a defined keyword was clicked

int batchpurgekeywords_defkeywords_clickfunc(GtkWidget *widget, int line, int pos, ch *input)
{
   using namespace batchpurgekeywords;

   int      ii;
   ch       *txkeyword, end;
   ch       keywordname[keywordXcc];

   if (*input == GDK_KEY_F1) {                                                         //  key F1 pressed, show help
      showz_docfile(Mwin,UGfile,F1_help_topic);
      return 1;
   }

   txkeyword = txwidget_word(widget,line,pos,",;:",end);                               //  clicked word
   if (! txkeyword || end == ':') return 1;                                            //  nothing or keyword category, ignore

   strncpy0(keywordname,txkeyword,keywordXcc);

   if (Nkeywords == 100) {
      zmessageACK(Mwin,TX("max keywords exceeded"));
      return 1;
   }

   for (ii = 0; ii < Nkeywords; ii++)                                                  //  check for duplicate
      if (strmatchcase(keywordname,keywordlist[ii])) break;
   if (ii < Nkeywords) {
      zmessageACK(Mwin,TX("duplicate keyword"));
      return 1;
   }

   keywordlist[Nkeywords] = zstrdup(keywordname,"batch_keywords");
   Nkeywords++;

   widget = zdialog_gtkwidget(zd,"keywordlist");                                       //  rewrite dialog keyword list
   txwidget_clear(widget);
   for (ii = 0; ii < Nkeywords; ii++)
      txwidget_append2(widget,0,"%s \n",keywordlist[ii]);

   zfree(txkeyword);
   return 1;
}


//  a keyword list line was clicked - remove it

int batchpurgekeywords_keywordlist_clickfunc(GtkWidget *widget, int line, int pos, ch *input)
{
   using namespace batchpurgekeywords;

   int      ii;

   if (*input == GDK_KEY_F1) {                                                         //  key F1 pressed, show help
      showz_docfile(Mwin,UGfile,F1_help_topic);
      return 1;
   }

   if (line >= Nkeywords) return 1;

   for (ii = line; ii < Nkeywords-1; ii++) {                                           //  remove keyword corresponding
      keywordlist[ii] = keywordlist[ii+1];                                             //    to the line clicked
   }
   Nkeywords--;

   widget = zdialog_gtkwidget(zd,"keywordlist");                                       //  rewrite dialog keyword list
   txwidget_clear(widget);
   for (int ii = 0; ii < Nkeywords; ii++)
      txwidget_append2(widget,0,"%s \n",keywordlist[ii]);

   return 1;
}


//  batch purge keywords dialog event function

int batchpurgekeywords_dialog_event(zdialog *zd, ch *event)
{
   using namespace batchpurgekeywords;

   ch    catgname[keywordXcc];

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

   if (strmatch(event,"defcats")) {                                                    //  new keyword category selection
      zdialog_fetch(zd,"defcats",catgname,keywordXcc);
      defkeywords_stuff(zd,catgname);
   }

   return 1;
}


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

//  batch change or shift photo date/time

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

   ch          *tagname[1] = { meta_pdate_tag };
   ch          *tagval[1];
   ch          text[100];
   ch          *file, olddatetime[24], newdatetime[24];                                //  metadata 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, *zdpop;

   F1_help_topic = TX("BATCH PHOTO DATE");

   printf("m_batch_photo_date_time \n");

   if (Xindexlev < 1) {
      index_rebuild(1,0);                                                              //  25.1
      if (Nxxrec == 0) {
         zmessageACK(Mwin,TX("image index required"));
         return;
      }
   }

   if (Fblock("batch photo date")) return;

/***
       __________________________________________________
      |            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] [X] |
      |__________________________________________________|

***/

   zd = zdialog_new(TX("Batch Photo Date/Time"),Mwin,TX("Proceed"),"X",null);

   zdialog_add_widget(zd,"hbox","hbfiles","dialog",0,"space=3");
   zdialog_add_widget(zd,"button","files","hbfiles",TX("Select Files"),"space=5");
   zdialog_add_widget(zd,"label","labcount","hbfiles",TX("no files selected"),"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",TX("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",TX("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",TX("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",TX("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",TX("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",TX("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",TX("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",TX("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",TX("test: show changes, do not update files"),"space=3");

   zdialog_load_inputs(zd);

   snprintf(text,100,TX("%d image files selected"),SFcount);                           //  show selected files count
   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,TX("please make a choice"));
      goto retry;
   }

   if (SFcount == 0) {
      zmessageACK(Mwin,TX("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,TX("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);

      if (nn != 6) {                                                                   //  check input format
         zmessageACK(Mwin,TX("invalid date/time format"));
         goto retry;
      }

      DTnew.tm_year -= 1900;                                                           //  deal with stupid offsets              25.1
      DTnew.tm_mon -= 1;

      timep = mktime(&DTnew);                                                          //  DTnew >> timep

      DTnew.tm_year += 1900;
      DTnew.tm_mon += 1;
      DTnew.tm_hour += DTnew.tm_gmtoff/3600;                                           //  poorly understood kludge              25.1

      if (timep < 0) {                                                                 //  validate DTnew by validating timep
         zmessageACK(Mwin,TX("invalid date/time format"));
         goto retry;
      }
   }                                                                                   //  DTnew is final value to use

   zdpop = popup_report_open(TX("Photo Date/Time"),Mwin,500,200,0,0,0,"X",null);       //  log report

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

      popup_report_write2(zdpop,0,"changes: year mon day  hours mins secs \n");
      popup_report_write2(zdpop,0,"         %4d %3d %3d  %5d %4d %4d \n",
                          s_years,s_mons,s_mdays,s_hours,s_mins,s_secs);
   }

   zdialog_free(zd);

   zadd_locked(NFbusy,+1);

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

      if (! zdialog_valid(zdpop)) break;                                               //  report canceled

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

      popup_report_write2(zdpop,0,"\n");                                               //  report progress
      popup_report_write2(zdpop,0,"%s \n",file);

      err = access(file,W_OK);                                                         //  test file can be written by me
      if (err) {
         popup_report_write2(zdpop,0,"%s",TX("no write permission \n"));
         continue;
      }

      meta_get(curr_file,tagname,tagval,1);                                            //  metadata >> yyyy:mm:dd hh:mm:ss
      if (! tagval[0] && Fshift) {                                                     //  ignore if Fsetnew
         popup_report_write2(zdpop,0,TX("*** no date/time available \n"));
         continue;
      }

      if (tagval[0]) {
         strcpy(olddatetime,"0000:01:01 00:00:00");                                    //  append for missing time               25.1
         cc = strlen(tagval[0]);
         if (cc > 19) cc = 19;
         strncpy(olddatetime,tagval[0],cc);
         zfree(tagval[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);

      if (nn < 6) DTold.tm_sec = 0;                                                    //  fix truncated date-time
      if (nn < 5) DTold.tm_min = 0;
      if (nn < 4) DTold.tm_hour = 0;
      if (nn < 3) DTold.tm_mday = 1;
      if (nn < 2) DTold.tm_mon = 0;
      if (nn < 1) DTold.tm_year = 0;

      if (nn < 3 && Fshift) {                                                          //  require at least yyyy:mm:dd
         popup_report_write2(zdpop,0,TX("*** file date/time invalid \n"));
         continue;
      }

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

      DTnew.tm_year -= 1900;                                                           //  deal with stupid offsets              25.1
      DTnew.tm_mon -= 1;

      timep = mktime(&DTnew);                                                          //  convert to local time

      if (timep < 0) {
         popup_report_write2(zdpop,0,TX("%s *** date/time conversion failed \n"),olddatetime);
         continue;
      }

      DTnew = *localtime(&timep);                                                      //  convert back to date and time

      DTnew.tm_year += 1900;                                                           //  deal with stupid offsets              25.1
      DTnew.tm_mon += 1;

      snprintf(newdatetime,20,"%04d:%02d:%02d %02d:%02d:%02d",                         //  DTnew >> yyyy:mm:dd hh:mm:ss
                            DTnew.tm_year, DTnew.tm_mon, DTnew.tm_mday,
                            DTnew.tm_hour, DTnew.tm_min, DTnew.tm_sec);

      olddatetime[4] = olddatetime[7] = newdatetime[4] = newdatetime[7] = ':';         //  format: yyyy:mm:dd
      popup_report_write2(zdpop,0," %s  %s \n",olddatetime,newdatetime);

      if (Ftest) continue;                                                             //  test only, no file updates

      newdatetime[4] = newdatetime[7] = ':';                                           //  format: yyyy:mm:dd for metadata
      tagval[0] = (ch *) &newdatetime;
      err = meta_put(curr_file,(ch **) tagname,tagval,1);                              //  yyyy:mm:dd hh:mm:ss >> metadata
      if (err) {
         popup_report_write2(zdpop,0,TX("*** metadata update error \n"));
         continue;
      }
   }

   zadd_locked(NFbusy,-1);

   if (! zdialog_valid(zdpop))
      printf("*** report canceled \n");
   else popup_report_write2(zdpop,0,TX("\n*** COMPLETED \n"));
   popup_report_bottom(zdpop);

   Fblock(0);
   return;
}


//  dialog event and completion callback function

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

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

      snprintf(countmess,80,TX("%d image files selected"),SFcount);
      zdialog_stuff(zd,"labcount",countmess);
   }

   if (zstrstr("Fsetnew Fshift",event)) {
      zdialog_stuff(zd,"Fsetnew",0);
      zdialog_stuff(zd,"Fshift",0);
      zdialog_stuff(zd,event,1);
   }

   return 1;
}


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

//  batch add or change any meta metadata

namespace batchchangemeta
{
   zdialog     *zd;
}


//  menu function

void m_batch_change_meta(GtkWidget *, ch *menu)
{
   using namespace batchchangemeta;

   int  batch_change_meta_dialog_event(zdialog *zd, ch *event);
   int  batch_change_meta_clickfunc(GtkWidget *, int line, int pos, ch *input);

   int            ii, jj, yn, err, zstat, ntags;
   ch             tagnameN[10] = "tagnameN", tagvalN[10] = "tagvalN";
   ch             *tagname[10], *tagval[10];
   ch             *file, text[metadataXcc];
   GtkWidget      *mtext;
   static int     nx, ftf = 1;
   static ch      **taglist;
   zdialog        *zdpop;

   F1_help_topic = TX("BATCH CHANGE META");

   printf("m_batch_change_meta \n");

   if (Xindexlev < 1) {
      index_rebuild(1,0);                                                              //  25.1
      if (Nxxrec == 0) {
         zmessageACK(Mwin,TX("image index required"));
         return;
      }
   }

   if (Fblock("batch change meta")) return;

   if (ftf) {
      ftf = 0;
      nx = zreadfile(meta_picklist_file,taglist);                                      //  get list of metadata tags
   }

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

**/

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

   zdialog_add_widget(zd,"hbox","hbfiles","vb2",0,"space=3");
   zdialog_add_widget(zd,"button","files","hbfiles",TX("Select Files"),"space=5");
   zdialog_add_widget(zd,"label","labcount","hbfiles",TX("no files selected"),"space=10");

   zdialog_add_widget(zd,"hbox","hbtags","vb2",0,"space=5");
   zdialog_add_widget(zd,"vbox","vbname","hbtags");
   zdialog_add_widget(zd,"vbox","vbval","hbtags",0,"expand");
   zdialog_add_widget(zd,"label","labtag","vbname",TX("tag name"));
   zdialog_add_widget(zd,"label","labdata","vbval",TX("tag value"));
   zdialog_add_widget(zd,"zentry","tagname0","vbname",0,"size=20");
   zdialog_add_widget(zd,"zentry","tagname1","vbname",0,"size=20");
   zdialog_add_widget(zd,"zentry","tagname2","vbname",0,"size=20");
   zdialog_add_widget(zd,"zentry","tagname3","vbname",0,"size=20");
   zdialog_add_widget(zd,"zentry","tagname4","vbname",0,"size=20");
   zdialog_add_widget(zd,"zentry","tagname5","vbname",0,"size=20");
   zdialog_add_widget(zd,"zentry","tagname6","vbname",0,"size=20");
   zdialog_add_widget(zd,"zentry","tagname7","vbname",0,"size=20");
   zdialog_add_widget(zd,"zentry","tagname8","vbname",0,"size=20");
   zdialog_add_widget(zd,"zentry","tagname9","vbname",0,"size=20");
   zdialog_add_widget(zd,"zentry","tagval0","vbval",0,"size=20|expand");
   zdialog_add_widget(zd,"zentry","tagval1","vbval",0,"size=20|expand");
   zdialog_add_widget(zd,"zentry","tagval2","vbval",0,"size=20|expand");
   zdialog_add_widget(zd,"zentry","tagval3","vbval",0,"size=20|expand");
   zdialog_add_widget(zd,"zentry","tagval4","vbval",0,"size=20|expand");
   zdialog_add_widget(zd,"zentry","tagval5","vbval",0,"size=20|expand");
   zdialog_add_widget(zd,"zentry","tagval6","vbval",0,"size=20|expand");
   zdialog_add_widget(zd,"zentry","tagval7","vbval",0,"size=20|expand");
   zdialog_add_widget(zd,"zentry","tagval8","vbval",0,"size=20|expand");
   zdialog_add_widget(zd,"zentry","tagval9","vbval",0,"size=20|expand");

   snprintf(text,100,TX("%d image files selected"),SFcount);                           //  show selected files count
   zdialog_stuff(zd,"labcount",text);

   mtext = zdialog_gtkwidget(zd,"mtext");                                              //  make clickable metadata list
   txwidget_clear(mtext);

   for (ii = 0; ii < nx; ii++)                                                         //  stuff metadata pick list
      txwidget_append(mtext,0,"%s \n",taglist[ii]);

   txwidget_set_eventfunc(mtext,batch_change_meta_clickfunc);                          //  set mouse/KB event function

   ntags = 0;                                                                          //  nothing selected

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

retry:
   zstat = zdialog_wait(zd);                                                           //  wait for completion
   if (zstat != 2) {
      zdialog_free(zd);
      Fblock(0);
      return;
   }

   for (ii = jj = 0; ii < 10; ii++)
   {
      tagnameN[7] = '0' + ii;                                                          //  get metadata list from dialog
      tagvalN[6] = '0' + ii;
      zdialog_fetch(zd,tagnameN,text,metatagXcc);                                      //  tag name
      strCompress(text);                                                               //  remove blanks
      if (*text <= ' ') continue;
      tagname[jj] = zstrdup(text,"batch-metadata");
      zdialog_fetch(zd,tagvalN,text,metadataXcc);                                      //  tag data, may be ""
      tagval[jj] = zstrdup(text,"batch-metadata");
      jj++;
   }

   ntags = jj;

   if (ntags == 0) {
      zmessageACK(Mwin,TX("enter tag names"));
      zd->zstat = 0;
      goto retry;
   }

   if (SFcount == 0) {
      zmessageACK(Mwin,TX("no files selected"));
      zd->zstat = 0;
      goto retry;
   }

   zdpop = popup_report_open(TX("Batch Metadata"),Mwin,500,200,0,0,0,"OK",null);       //  log report

   for (ii = 0; ii < ntags; ii++)
      popup_report_write2(zdpop,0,"tag %s = %s \n",tagname[ii],tagval[ii]);

   yn = zmessageYN(Mwin,TX("Proceed?"));                                               //  25.0
   if (yn != 1) {
      zd->zstat = 0;                                                                   //  cancel
      popup_report_close(zdpop,0);
      goto retry;
   }

   zdialog_free(zd);
   zd = 0;

   zadd_locked(NFbusy,+1);

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

      if (! zdialog_valid(zdpop)) break;                                               //  report canceled

      file = SelFiles[ii];                                                             //  display image

      popup_report_write2(zdpop,0,"%s \n",file);                                       //  report progress

      err = access(file,W_OK);                                                         //  test file can be written by me
      if (err) {
         popup_report_write2(zdpop,1,TX("no write permission \n"));
         continue;
      }

      err = meta_put(file,tagname,tagval,ntags);
      if (err) {
         popup_report_write2(zdpop,1,TX("metadata update error \n"));
         continue;
      }

      if (zd_metaview) meta_view(0);                                                   //  update metadata view if active
   }

   zadd_locked(NFbusy,-1);

   if (! zdialog_valid(zdpop))
      popup_report_write2(zdpop,1,TX("\n*** CANCELED \n"));
   else popup_report_write2(zdpop,1,TX("\n*** COMPLETED \n"));
   popup_report_bottom(zdpop);

   for (ii = 0; ii < ntags; ii++) {                                                    //  free memory
      zfree(tagname[ii]);
      zfree(tagval[ii]);
   }

   Fblock(0);
   return;
}


//  dialog event and completion callback function

int  batch_change_meta_dialog_event(zdialog *zd, ch *event)
{
   using namespace batchchangemeta;

   ch        countmess[80];

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

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

      snprintf(countmess,80,TX("%d image files selected"),SFcount);
      zdialog_stuff(zd,"labcount",countmess);
   }

   return 1;
}


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

int batch_change_meta_clickfunc(GtkWidget *widget, int line, int pos, ch *input)
{
   using namespace batchchangemeta;

   int      ii;
   ch       *pp;
   ch       tagnameX[9] = "tagnameX";
   ch       tagname[metatagXcc];

   if (*input == GDK_KEY_F1) {                                                         //  key F1 pressed, show help
      showz_docfile(Mwin,UGfile,F1_help_topic);
      return 1;
   }

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

   for (ii = 0; ii < 10; ii++) {                                                       //  find 1st empty dialog tag name
      tagnameX[7] = '0' + ii;
      zdialog_fetch(zd,tagnameX,tagname,metatagXcc);
      if (*tagname <= ' ') break;
   }

   if (ii < 10) zdialog_stuff(zd,tagnameX,pp);
   return 1;
}


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

//  batch report metadata for selected image files
//  menu function

void m_batch_report_meta(GtkWidget *, ch *menu)
{
   int  batch_report_meta_dialog_event(zdialog *zd, ch *event);

   zdialog     *zd, *zdpop;
   ch          *file, text[100];
   int         zstat, ff, ii, err;
   int         brm, brmx = 20;
   ch          **taglist, *tagname2[brmx], *tagval2[brmx];

   F1_help_topic = TX("BATCH REPORT META");

   printf("m_batch_report_meta \n");

   if (Xindexlev < 1) {
      index_rebuild(1,0);                                                              //  25.1
      if (Nxxrec == 0) {
         zmessageACK(Mwin,TX("image index required"));
         return;
      }
   }

   if (Fblock("batch report meta")) return;

/***
          ____________________________________________
         |           Batch Report Metadata            |
         |                                            |
         |  [Select Files]  NN files selected         |
         |  [Edit]  metadata tags to report           |
         |                                            |
         |                              [Proceed] [X] |
         |____________________________________________|

***/

   zd = zdialog_new(TX("Batch Report Metadata"),Mwin,TX("Proceed"),"X",null);
   zdialog_add_widget(zd,"hbox","hbfiles","dialog",0,"space=3");
   zdialog_add_widget(zd,"button","files","hbfiles",TX("Select Files"),"space=5");
   zdialog_add_widget(zd,"label","labcount","hbfiles",TX("no files selected"),"space=10");
   zdialog_add_widget(zd,"hbox","hbedit","dialog",0,"space=3");
   zdialog_add_widget(zd,"button","edit","hbedit","Edit","space=5");
   zdialog_add_widget(zd,"label","labedit","hbedit",TX("metadata tags to report"),"space=10");

   snprintf(text,100,TX("%d image files selected"),SFcount);                           //  show selected files count
   zdialog_stuff(zd,"labcount",text);

   zstat = zdialog_run(zd,batch_report_meta_dialog_event,"parent");                    //  run dialog
   zstat = zdialog_wait(zd);                                                           //  wait for completion
   zdialog_free(zd);
   if (zstat != 1) {                                                                   //  cancel
      Fblock(0);
      return;
   }

   if (SFcount == 0) {
      zmessageACK(Mwin,TX("no files selected"));
      Fblock(0);
      return;
   }

   brm = zreadfile(meta_report_tags_file,taglist);
   if (brm > brmx) brm = brmx;
   for (ii = 0; ii < brm; ii++)
      tagname2[ii] = taglist[ii];

   if (taglist) zfree(taglist);

   if (! brm) {
      zmessageACK(Mwin,TX("no metadata tags to report"));
      return;
   }

   zdpop = popup_report_open("metadata",Mwin,600,400,0,0,0,"Save","X",null);           //  log report

   zadd_locked(NFbusy,+1);

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

      if (! zdialog_valid(zdpop)) break;                                               //  report canceled

      popup_report_write2(zdpop,0,"\n");                                               //  blank line separator

      file = SelFiles[ff];
      popup_report_write2(zdpop,0,"%s \n",file);

      if (image_file_type(file) != IMAGE) {                                            //  file deleted?
         popup_report_write2(zdpop,1,TX("*** invalid file \n"));
         continue;
      }

      err = meta_get(file,tagname2,tagval2,brm);                                       //  get all report tags
      if (err) continue;

      for (ii = 0; ii < brm; ii++)                                                     //  output tag names and values
         if (tagval2[ii])
            popup_report_write2(zdpop,0,"%-24s : %s \n",tagname2[ii],tagval2[ii]);

      for (ii = 0; ii < brm; ii++)                                                     //  free memory
         if (tagval2[ii]) zfree(tagval2[ii]);
   }

   zadd_locked(NFbusy,-1);

   if (! zdialog_valid(zdpop))
      printf("*** report canceled \n");
   else
      popup_report_write2(zdpop,1,TX("\n*** COMPLETED \n"));

   Fblock(0);
   return;
}


//  dialog event and completion function

int  batch_report_meta_dialog_event(zdialog *zd, ch *event)
{
   ch       countmess[80];
   zlist_t  *mlist;
   int      nn;

   if (zd->zstat) zdialog_destroy(zd);

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

      snprintf(countmess,80,TX("%d image files selected"),SFcount);
      zdialog_stuff(zd,"labcount",countmess);
   }

   if (strmatch(event,"edit"))                                                         //  select metadata tags to report
   {
      mlist = zlist_from_file(meta_report_tags_file);                                  //  load metadata report list
      if (! mlist) mlist = zlist_new(0);
      nn = select_meta_tags(mlist,maxbatchtags,0);                                     //  user edit of metadata list
      if (nn) zlist_to_file(mlist,meta_report_tags_file);                              //  replace file
      zlist_free(mlist);
   }

   return 1;
}


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

//  batch geotags - set geotags for multiple image files

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

   int         ii, err, zstat;
   ch          *file;
   ch          location[40], country[40];
   ch          gps_data[24], text[100];
   zdialog     *zd, *zdpop;
   ch          *tagname[3] = { meta_city_tag, meta_country_tag, meta_gps_tag };
   ch          *tagval[3];

   F1_help_topic = TX("BATCH GEOTAGS");

   printf("m_batch_geotags \n");

   if (Xindexlev < 1) {
      index_rebuild(1,0);                                                              //  25.1
      if (Nxxrec == 0) {
         zmessageACK(Mwin,TX("image index required"));
         return;
      }
   }

   if (Fblock("batch geotags")) return;

   load_imagelocs();                                                                   //  initialize image geolocs[] data

/***
       _____________________________________________________
      |                Batch Set Geotags                    |
      |                                                     |
      | [select files]  NN files selected                   |
      | location [______________]  country [______________] |
      | GPS_data [_____________________]                    |
      |                                                     |
      |                        [Find] [Clear] [Proceed] [X] |
      |_____________________________________________________|

***/

   zd = zdialog_new(TX("Batch Set Geotags"),Mwin,TX("Find"),TX("Clear"),TX("Proceed"),"X",null);

   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=3");
   zdialog_add_widget(zd,"button","files","hb1",TX("Select Files"),"space=10");
   zdialog_add_widget(zd,"label","labcount","hb1",TX("no files selected"),"space=10");
   zdialog_add_widget(zd,"hbox","hb2","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labloc","hb2","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","country","space=5");
   zdialog_add_widget(zd,"zentry","country","hb2",0,"expand");
   zdialog_add_widget(zd,"hbox","hb3","dialog");
   zdialog_add_widget(zd,"label","labgps","hb3","GPS_data","space=3");
   zdialog_add_widget(zd,"zentry","gps_data","hb3",0,"size=30");

   zdialog_add_ttip(zd,"Find",TX("search known locations"));
   zdialog_add_ttip(zd,"Lookup",TX("find via table lookup"));
   zdialog_add_ttip(zd,"Clear",TX("clear inputs"));

   snprintf(text,100,TX("%d image files selected"),SFcount);                           //  show selected files count
   zdialog_stuff(zd,"labcount",text);

   zd_mapgeotags = zd;                                                                 //  activate map clicks

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

   if (zstat != 4) goto cleanup;                                                       //  status not [proceed]
   if (! SFcount) goto cleanup;                                                        //  no files selected

   put_imagelocs(zd);                                                                  //  update geolocs table

   zdialog_fetch(zd,"location",location,40);                                           //  get location from dialog
   zdialog_fetch(zd,"country",country,40);
   zdialog_fetch(zd,"gps_data",gps_data,24);

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

   if (SFcount == 0) goto cleanup;

   zdpop = popup_report_open("Add Geotags",Mwin,500,200,0,0,0,"X",null);               //  log report

   zadd_locked(NFbusy,+1);

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

      if (! zdialog_valid(zdpop)) break;                                               //  report canceled

      file = SelFiles[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
      if (err) {
         popup_report_write2(zdpop,1,TX("%s: no write permission"),file);
         continue;
      }

      tagval[0] = location;
      tagval[1] = country;
      tagval[2] = gps_data;

      err = meta_put(file,tagname,tagval,3);
      if (err) {
         popup_report_write2(zdpop,1,TX("%s: metadata update failed"),file);
         continue;
      }

      popup_report_write2(zdpop,0,"%s \n",file);                                       //  report progress
   }

   if (! zdialog_valid(zdpop))
      printf("*** report canceled \n");
   else popup_report_write2(zdpop,0,TX("\n*** COMPLETED \n"));
   popup_report_bottom(zdpop);

   zadd_locked(NFbusy,-1);

cleanup:

   Fblock(0);

   load_imagelocs();                                                                   //  refresh geolocatons table

   if (zd) zdialog_free(zd);
   zd_mapgeotags = 0;

   return;
}


//  batch_geotags dialog event function

int batch_geotags_dialog_event(zdialog *zd, ch *event)
{
   int      yn, zstat, err;
   ch       countmess[80];
   ch       location[40], country[40];
   ch       gps_data[24];
   float    flati, flongi;

   if (strmatch(event,"files"))                                                        //  select images to add tags
   {
      zdialog_show(zd,0);                                                              //  hide parent dialog
      select_files(0);                                                                 //  get file list from user
      zdialog_show(zd,1);

      snprintf(countmess,80,TX("%d image files selected"),SFcount);
      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);                                                               //  search image location data            26.0
      return 1;
   }

   else if (zstat == 2)                                                                //  [clear]
   {
      zdialog_stuff(zd,"location","");                                                 //  erase dialog fields
      zdialog_stuff(zd,"country","");
      zdialog_stuff(zd,"gps_data","");
      return 1;
   }

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

      if (*location) {                                                                 //  allow "" to erase location
         *location = toupper(*location);                                               //  capitalize
         zdialog_stuff(zd,"location",location);
      }

      if (*country) {
         *country = toupper(*country);
         zdialog_stuff(zd,"country",country);
      }

      zdialog_fetch(zd,"gps_data",gps_data,24);                                        //  get GPS data

      if (*gps_data) {                                                                 //  if present, validate
         err = get_gps_data(gps_data,flati,flongi);
         if (err) goto badcoord;
      }

      if (! SFcount) goto nofiles;

      if (*location <= ' ' || *country <= ' ' || *gps_data <= ' ') {
         yn = zmessageYN(Mwin,TX("data is incomplete, proceed?"));
         if (! yn) return 1;
      }

      zd->zstat = 4;                                                                   //  mark complete
      return 1;
   }

   zdialog_free(zd);                                                                   //  canceled
   return 1;

badcoord:
   zmessageACK(Mwin,TX("invalid GPS data: %s %s"),gps_data);
   return 1;

nofiles:
   zmessageACK(Mwin,TX("no files selected"));
   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 places_dates_names
{
   struct grec_t  {                                                                    //  image geotags data
      ch          *location, *country;                                                 //  group location
      ch          pdate[12];                                                           //  nominal group date, yyyy:mm:dd
      int         lodate, hidate;                                                      //  range, days since 0 CE
      int         count;                                                               //  images in group
   };

   grec_t   *grec = 0;
   zlist_t  *filelist = 0;
   int      Ngrec = 0;
   int      groupby, daterange;
   int      Fusesearch, Nsearch;
   int      pline;

   zdialog  *zdpop = 0;
}


//  menu function

void m_meta_places_dates(GtkWidget *, ch *)                                            //  26.0
{
   using namespace places_dates_names;

   int   places_dates_getdays(ch *date);
   int   places_dates_comp(ch *rec1, ch *rec2);
   int   places_dates_clickfunc(GtkWidget *widget, int line, int pos, ch *input);

   zdialog     *zd;
   int         zstat, ii, jj, cc, cc1, cc2;
   int         ww, iix, iig, newgroup;
   ch          location[40], country[40];
   ch          albumfile[200];
   xxrec_t     *xxrec;

   F1_help_topic = TX("PLACES/DATES");

   printf("m_meta_places_dates \n");

   if (Xindexlev < 1) {
      index_rebuild(1,0);                                                              //  25.1
      if (Nxxrec == 0) {
         zmessageACK(Mwin,TX("image index required"));
         return;
      }
   }

/***
          __________________________________________
         |        Report Image Places/Dates         |
         |                                          |
         | Include: (o) all images  (o) last search |
         | (o) Group by country                     |          1
         | (o) Group by country/location            |          2
         | (o) Group by country/date                |          3                       //  26.0
         | (o) Group by country/location/date       |          4
         | (o) Group by date/country                |          5                       //  26.0
         | (o) Group by date/country/location       |          6
         | Combine within [ xx ] days               |
         |                                          |
         |                            [Proceed] [X] |
         |__________________________________________|

***/

   zd = zdialog_new(TX("Report Image Places/Dates"),Mwin,TX("Proceed"),"X",null);
   zdialog_add_widget(zd,"hbox","hbincl","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labincl","hbincl",TX("Include:"),"space=3");
   zdialog_add_widget(zd,"radio","all images","hbincl",TX("all images"),"space=5");
   zdialog_add_widget(zd,"radio","last search","hbincl",TX("last search"));
   zdialog_add_widget(zd,"radio","1","dialog",TX("Group by country"));
   zdialog_add_widget(zd,"radio","2","dialog",TX("Group by country/location"));
   zdialog_add_widget(zd,"radio","3","dialog",TX("Group by country/date"));
   zdialog_add_widget(zd,"radio","4","dialog",TX("Group by country/location/date"));
   zdialog_add_widget(zd,"radio","5","dialog",TX("Group by date/country"));
   zdialog_add_widget(zd,"radio","6","dialog",TX("Group by date/country/location"));
   zdialog_add_widget(zd,"hbox","hbr","dialog",0,"space=8");
   zdialog_add_widget(zd,"label","labr1","hbr",TX("Combine within"),"space=3");
   zdialog_add_widget(zd,"zspin","range","hbr","0|9999|1|1","space=3");
   zdialog_add_widget(zd,"label","labr2","hbr",TX("days"),"space=3");

   zdialog_stuff(zd,"all images",1);                                                   //  default, use all images
   zdialog_stuff(zd,"last search",0);

   zdialog_stuff(zd,"1",0);
   zdialog_stuff(zd,"2",1);                                                            //  default by country/location
   zdialog_stuff(zd,"3",0);
   zdialog_stuff(zd,"4",0);
   zdialog_stuff(zd,"5",0);
   zdialog_stuff(zd,"6",0);

   zdialog_load_inputs(zd);
   zdialog_resize(zd,300,0);
   zdialog_run(zd,0,"parent");
   zstat = zdialog_wait(zd);
   if (zstat != 1) {
      zdialog_free(zd);
      return;
   }

   zdialog_fetch(zd,"last search",Fusesearch);                                         //  use last search results

   zdialog_fetch(zd,"1",iix);
   if (iix) groupby = 1;                                                               //  group country
   zdialog_fetch(zd,"2",iix);
   if (iix) groupby = 2;                                                               //  group country/location
   zdialog_fetch(zd,"3",iix);
   if (iix) groupby = 3;                                                               //  group country/date
   zdialog_fetch(zd,"4",iix);
   if (iix) groupby = 4;                                                               //  group country/location/date
   zdialog_fetch(zd,"5",iix);
   if (iix) groupby = 5;                                                               //  group date/country
   zdialog_fetch(zd,"6",iix);
   if (iix) groupby = 6;                                                               //  group date/country/location

   zdialog_fetch(zd,"range",daterange);                                                //  combine recs within date range

   zdialog_free(zd);

   if (Ngrec) {                                                                        //  free prior memory
      zfree(grec);
      Ngrec = 0;
   }

   if (filelist) zlist_free(filelist);
   filelist = 0;

   if (! Nxxrec) {
      zmessageACK(Mwin,TX("no files found"));                                          //  no image files
      return;
   }

   if (Fusesearch)                                                                     //  use last search results
   {
      snprintf(albumfile,200,"%s/%s",albums_folder,"search_results");                  //  get image list from last search
      filelist = zlist_from_file(albumfile);
      Nsearch = zlist_count(filelist);
      if (! Nsearch) {
         zlist_free(filelist);
         zmessageACK(Mwin,TX("no files found"));
         return;
      }

      cc = Nsearch * sizeof(grec_t);                                                   //  allocate memory
      grec = (grec_t *) zmalloc(cc,"meta-places");

      for (ii = jj = 0; ii < Nsearch; ii++)                                            //  loop files in search results
      {
         xxrec = get_xxrec(zlist_get(filelist,ii));
         if (! xxrec) continue;                                                        //  deleted, not image file

         grec[jj].location = xxrec->location;                                          //  get location and country
         grec[jj].country = xxrec->country;
         strncpy0(grec[jj].pdate,xxrec->pdate,11);                                     //  photo date, truncate to yyyy:mm:dd
         grec[jj].lodate = places_dates_getdays(xxrec->pdate);                         //  days since 0 CE
         grec[jj].hidate = grec[jj].lodate;

         jj++;
      }

      Ngrec = jj;
   }

   else                                                                                //  use all image files
   {
      cc = Nxxrec * sizeof(grec_t);                                                    //  allocate memory
      grec = (grec_t *) zmalloc(cc,"meta-places");

      for (ii = 0; ii < Nxxrec; ii++)                                                  //  loop all index recs
      {
         xxrec = xxrec_tab[ii];

         grec[ii].location = xxrec->location;                                          //  get location and country
         grec[ii].country = xxrec->country;
         strncpy0(grec[ii].pdate,xxrec->pdate,11);                                     //  photo date, truncate to yyyy:mm:dd
         grec[ii].lodate = places_dates_getdays(xxrec->pdate);                         //  days since 0 CE
         grec[ii].hidate = grec[ii].lodate;
      }

      Ngrec = Nxxrec;
   }

   if (Ngrec > 1)                                                                      //  sort grecs according to groupby
      HeapSort((ch *) grec, sizeof(grec_t), Ngrec, places_dates_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 (! strmatch(grec[iix].location,grec[iig].location)                            //  new location >> new group
       && (groupby == 2 || groupby == 4 || groupby == 6))
         newgroup = 1;                                                                 //  new country >> new group

      if ((grec[iix].lodate - grec[iig].hidate > daterange) && (groupby >= 3))         //  new date >> new group
         newgroup = 1;                                                                 //  new country >> new group

      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
      {
         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 (groupby == 1) ww = 500;                                                         //  group country
   if (groupby == 2) ww = 800;                                                         //  group country/location
   if (groupby == 3) ww = 600;                                                         //  group country/date-range
   if (groupby == 4) ww = 980;                                                         //  group country/location/date-range
   if (groupby == 5) ww = 600;                                                         //  group date-range/country
   if (groupby == 6) ww = 980;                                                         //  group date-range/country/location

   if (zdialog_valid(zdpop)) popup_report_close(zdpop,0);                              //  close prior if any

   zdpop = popup_report_open(TX("Image Locations"),Mwin,ww,400,0,                      //  write groups to popup window
                                     1,places_dates_clickfunc,"Find","X",null);

   if (groupby == 1)                                                                   //  group by country
   {
      popup_report_header(zdpop,1,"%-40s  %5s","Country","Count");

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

   if (groupby == 2)                                                                   //  group by country/location
   {
      popup_report_header(zdpop,1,"%-40s  %-40s  %5s",
                                 "Country","Location","Count");

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

   if (groupby == 3)                                                                   //  group by country/date-range
   {
      popup_report_header(zdpop,1,"%-40s  %-10s  %5s",
                                 "Country","Date","Count");

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

   if (groupby == 4)                                                                   //  group by country/location/date-range
   {
      popup_report_header(zdpop,1,"%-40s  %-40s  %-10s  %5s",
                                    "Country","Location","Date","Count");

      for (iig = 0; iig < Ngrec; iig++)
      {
         utf8substring(country,grec[iig].country,0,40);
         cc1 = 40 + strlen(country) - utf8len(country);
         utf8substring(location,grec[iig].location,0,40);
         cc2 = 40 + strlen(location) - utf8len(location);

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

   if (groupby == 5)                                                                   //  group by date-range/country
   {
      popup_report_header(zdpop,1,"%-10s  %-40s  %5s",
                                 "Date","Country","Count");

      for (iig = 0; iig < Ngrec; iig++)
      {
         utf8substring(country,grec[iig].country,0,40);
         cc1 = 40 + strlen(country) - utf8len(country);

         popup_report_write2(zdpop,0,"%-10s  %-*s  %5d \n",
                grec[iig].pdate,cc1,country,grec[iig].count);
      }
   }

   if (groupby == 6)                                                                   //  group by date-range/country/location
   {
      popup_report_header(zdpop,1,"%-10s  %-40s  %-40s  %5s",
                                 "Date","Country","Location","Count");

      for (iig = 0; iig < Ngrec; iig++)
      {
         utf8substring(country,grec[iig].country,0,40);
         cc1 = 40 + strlen(country) - utf8len(country);
         utf8substring(location,grec[iig].location,0,40);
         cc2 = 40 + strlen(location) - utf8len(location);

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

   pline = 0;                                                                          //  initial report line
   return;
}


//  Compare 2 grec records using groupby sequence
//  return < 0  = 0  > 0   for   rec1  <  =  >  rec2.

int places_dates_comp(ch *rec1, ch *rec2)
{
   using namespace places_dates_names;

   int      ii, date1, date2;
   ch       *loc1, *loc2;

   if (groupby == 1)                                                                   //  compare country
   {
      ch   * country1 = ((grec_t *) rec1)->country;
      ch   * country2 = ((grec_t *) rec2)->country;
      ii = strcmp(country1,country2);
      return ii;
   }

   if (groupby == 2)                                                                   //  compare country, location
   {
      ch   * country1 = ((grec_t *) rec1)->country;
      ch   * country2 = ((grec_t *) rec2)->country;
      ii = strcmp(country1,country2);
      if (ii) return ii;
      loc1 = ((grec_t *) rec1)->location;
      loc2 = ((grec_t *) rec2)->location;
      ii = strcmp(loc1,loc2);
      return ii;
   }

   if (groupby == 3)                                                                   //  compare country, date
   {
      ch   * country1 = ((grec_t *) rec1)->country;
      ch   * country2 = ((grec_t *) rec2)->country;
      ii = strcmp(country1,country2);
      if (ii) return ii;
      date1 = ((grec_t *) rec1)->lodate;
      date2 = ((grec_t *) rec2)->lodate;
      ii = date1 - date2;
      return ii;
   }

   if (groupby == 4)                                                                   //  compare country, location, date
   {
      ch   * country1 = ((grec_t *) rec1)->country;
      ch   * country2 = ((grec_t *) rec2)->country;
      ii = strcmp(country1,country2);
      if (ii) return ii;
      loc1 = ((grec_t *) rec1)->location;
      loc2 = ((grec_t *) rec2)->location;
      ii = strcmp(loc1,loc2);
      if (ii) return ii;
      date1 = ((grec_t *) rec1)->lodate;
      date2 = ((grec_t *) rec2)->lodate;
      ii = date1 - date2;
      return ii;
   }

   if (groupby == 5)                                                                   //  compare date, country
   {
      date1 = ((grec_t *) rec1)->lodate;
      date2 = ((grec_t *) rec2)->lodate;
      ii = date1 - date2;
      if (ii) return ii;
      ch   * country1 = ((grec_t *) rec1)->country;
      ch   * country2 = ((grec_t *) rec2)->country;
      ii = strcmp(country1,country2);
      return ii;
   }

   if (groupby == 6)                                                                   //  compare date, country, location
   {
      date1 = ((grec_t *) rec1)->lodate;
      date2 = ((grec_t *) rec2)->lodate;
      ii = date1 - date2;
      if (ii) return ii;
      ch   * country1 = ((grec_t *) rec1)->country;
      ch   * country2 = ((grec_t *) rec2)->country;
      ii = strcmp(country1,country2);
      if (ii) return ii;
      loc1 = ((grec_t *) rec1)->location;
      loc2 = ((grec_t *) rec2)->location;
      ii = strcmp(loc1,loc2);
      return ii;
   }

   return 0;
}


//  convert yyyy:mm:dd date into days from 0001:01:01 C.E.

int places_dates_getdays(ch *pdate)
{
   using namespace places_dates_names;

   int      year, month, day;
   ch       temp[8];
   int      montab[12] = { 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 };
   int      elaps;

   if (! *pdate) return 0;

   year = month = day = 0;

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

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

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

   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

int places_dates_clickfunc(GtkWidget *widget, int line, int pos, ch *input)
{
   using namespace places_dates_names;

   int            ii, jj, lodate, hidate, datex;
   ch             location[40], country[40];
   ch             places_file[200];
   FILE           *fid;
   xxrec_t        *xxrec;

   if (line >= 0)                                                                      //  line clicked
   {
      txwidget_scroll(widget,line);                                                    //  keep line on screen
      txwidget_highlight_line(widget,line);                                            //  highlight
      pline = line;                                                                    //  remember last line selected
   }

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

   snprintf(places_file,200,"%s/places_dates",albums_folder);                          //  open output file
   fid = fopen(places_file,"w");
   if (! fid) goto filerror;

   if (Fusesearch)                                                                     //  loop files in search results
   {
      for (ii = jj = 0; ii < Nsearch; ii++)
      {
         xxrec = get_xxrec(zlist_get(filelist,ii));
         if (! xxrec) continue;                                                        //  deleted, not image file

         if (! strmatch(xxrec->country,country)) continue;                             //  no country match

         if (groupby == 2 || groupby == 4 || groupby == 6)
            if (! strmatch(xxrec->location,location)) continue;                        //  no location match

         if (groupby >= 3) {
            datex = places_dates_getdays(xxrec->pdate);
            if (! *xxrec->pdate) datex = 0;
            if (datex < lodate || datex > hidate) continue;                            //  no date match
         }

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

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

         xxrec = xxrec_tab[ii];

         if (! strmatch(xxrec->country,country)) continue;                             //  no country match

         if (groupby == 2 || groupby == 4 || groupby == 6)
            if (! strmatch(xxrec->location,location)) continue;                        //  no location match

         if (groupby >= 3) {
            datex = places_dates_getdays(xxrec->pdate);
            if (! *xxrec->pdate) datex = 0;
            if (datex < lodate || datex > hidate) continue;                            //  no date match
         }

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

   fclose(fid);

   navi::gallerytype = SEARCH;                                                         //  search results
   gallery(places_file,"initF",0);                                                     //  generate gallery of matching files
   gallery(0,"paint",0);
   viewmode('G');
   return 1;

filerror:
   zmessageACK(Mwin,TX("file error: %s"),strerror(errno));
   return 1;
}


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

//  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      Fusesearch, Nsearch = 0;
   zlist_t  *filelist;

   int      Nyears = 2100;
   int      Nperds = 12 * Nyears;
   int      Nyears2 = 0;
   ch       *months = "Jan   Feb   Mar   Apr   May   Jun   Jul   Aug   Sep   Oct   Nov   Dec";
   int      colpos[14] = { 0, 6, 13, 19, 25, 31, 37, 43, 49, 55, 61, 67, 73, 79 };
   zdialog  *zdpop = 0;
}


//  menu function

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

   int  timeline_clickfunc(GtkWidget *widget, int line, int pos, ch *input);

   ch          albumfile[200];
   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;
   ch          pdate[8], nnnnnn[8], buff[100];
   xxrec_t     *xxrec;

   F1_help_topic = TX("TIMELINE");

   printf("m_meta_timeline \n");

   if (Xindexlev < 1) {
      index_rebuild(1,0);                                                              //  25.1
      if (Nxxrec == 0) {
         zmessageACK(Mwin,TX("image index required"));
         return;
      }
   }

   if (Nsearch) zlist_free(filelist);                                                  //  free prior memory
   Nsearch = 0;

   ii = zdialog_choose(Mwin,"mouse",TX("images to report"),                            //  query user
                              TX("all images"), TX("last search"),null);
   Fusesearch = ii - 1;                                                                //  0/1 = all images / search results

   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;

   if (Fusesearch)                                                                     //  include search results only
   {
      snprintf(albumfile,200,"%s/%s",albums_folder,"search_results");                  //  get image list from last search
      filelist = zlist_from_file(albumfile);
      Nsearch = zlist_count(filelist);
      if (! Nsearch) {
         zlist_free(filelist);
         zmessageACK(Mwin,TX("no files found"));
         return;
      }

      for (ii = jj = 0; ii < Nsearch; ii++)
      {
         xxrec = get_xxrec(zlist_get(filelist,ii));
         if (! xxrec) continue;                                                        //  deleted, not image file

         strncpy0(pdate,xxrec->pdate,8);                                               //  photo date, truncate to yyyy:mm

         if (! *pdate) {                                                               //  if missing, count
            ++Mcount;
            continue;
         }

         yy = atoi(pdate);
         mm = atoi(pdate+5);

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

   else
   {                                                                                   //  include all image files
      for (ii = 0; ii < Nxxrec; ii++)
      {
         zmainloop();                                                                  //  keep GTK alive

         xxrec = xxrec_tab[ii];

         strncpy0(pdate,xxrec->pdate,8);                                               //  photo date, truncate to yyyy:mm

         if (! *pdate) {                                                               //  if missing, count
            ++Mcount;
            continue;
         }

         yy = atoi(pdate);
         mm = atoi(pdate+5);

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

   if (zdialog_valid(zdpop)) popup_report_close(zdpop,0);                              //  close prior if any

   zdpop = popup_report_open(TX("Image Timeline"),Mwin,700,400,0,1,                    //  write report to popup window
                                          timeline_clickfunc,"X",null);

   popup_report_header(zdpop,1,"year  count  %s",months);                              //  "year   count  Jan  Feb  ... "

   if (Mcount)
      popup_report_write2(zdpop,0,"null  %-6d \n",Mcount);                             //  images with no date

   if (Ecount)
      popup_report_write2(zdpop,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(nnnnnn,7,"%-6d",Pcount[pp]);                                         //  output "NNNNNN"
         memcpy(buff+cc,nnnnnn,6);
         cc += 6;
      }

      buff[cc] = 0;
      popup_report_write2(zdpop,0,"%s \n",buff);

      Nyears2++;                                                                       //  count reported years
   }

   return;
}


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

int timeline_clickfunc(GtkWidget *widget, int line, int pos, ch *input)
{
   using namespace timeline_names;

   int         ii, mm, cc;
   int         Fnull = 0, ustat = 0;
   static int  pline, ppos;
   ch          *txline, pdate[8], *pp, end;
   ch          albumfile[200];
   FILE        *fid;
   xxrec_t     *xxrec;
   static int  busy = 0;

   if (busy) return 0;                                                                 //  stop re-entry
   busy++;

   if (line == -1)                                                                     //  arrow key navigation
   {
      if (input && strstr("left right up down",input)) ustat = 1;                      //  handled here

      for (ii = 0; ii < 14; ii++)                                                      //  current report column
         if (ppos == colpos[ii]) break;

      if (strmatch(input,"left")) {                                                    //  prior month
         if (ii > 2) ppos = colpos[ii-1];
         else {
            pline -= 1;
            ppos = colpos[13];
         }
      }

      if (strmatch(input,"right")) {                                                   //  next month
         if (ii < 13) ppos = colpos[ii+1];
         else {
            pline += 1;
            ppos = colpos[2];
         }
      }

      if (strmatch(input,"up")) pline -= 1;                                            //  prior year
      if (strmatch(input,"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];

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

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

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

   txline = txwidget_line(widget,line,1);                                              //  get clicked line
   if (! txline || ! *txline) goto retx;

   cc = 0;

   if (strmatchN(txline,"null",4)) Fnull = 1;                                          //  find images with null 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) / 6 + 1;                                                         //  month, 1-12
      if (mm < 1 || mm > 12) goto retx;
      strncpy(pdate,txline,4);                                                         //  "yyyy"
      pdate[4] = ':';
      pdate[5] = '0' + mm/10;
      pdate[6] = '0' + mm % 10;                                                        //  have "yyyy:mm"
      pdate[7] = 0;
      cc = 7;
   }

   snprintf(albumfile,200,"%s/timeline",albums_folder);
   fid = fopen(albumfile,"w");                                                         //  open output file
   if (! fid) {
      zmessageACK(Mwin,TX("file error: %s"),strerror(errno));
      goto retx;
   }

   if (Fusesearch)                                                                     //  include prior search results
   {
      for (ii = 0; ii < Nsearch; ii++)                                                 //  loop search results
      {
         zmainloop(100);                                                               //  keep GTK alive

         xxrec = get_xxrec(zlist_get(filelist,ii));                                    //  bugfix   26.0
         if (! xxrec) continue;

         if (Fnull) {                                                                  //  search for missing dates
            if (! *xxrec->pdate) {
               fprintf(fid,"%s\n",xxrec->file);
               continue;
            }
         }

         else if (strmatchN(xxrec->pdate,pdate,cc))                                    //  screen for desired period
            fprintf(fid,"%s\n",xxrec->file);                                           //  output matching file
      }
   }

   else                                                                                //  include all image files
   {
      for (ii = 0; ii < Nxxrec; ii++)
      {
         zmainloop(100);                                                               //  keep GTK alive

         xxrec = xxrec_tab[ii];
         if (! xxrec) continue;

         if (Fnull) {                                                                  //  search for missing dates
            if (! *xxrec->pdate) {
               fprintf(fid,"%s\n",xxrec->file);
               continue;
            }
         }

         else if (strmatchN(xxrec->pdate,pdate,cc))                                    //  screen for desired period
            fprintf(fid,"%s\n",xxrec->file);                                           //  output matching file
      }
   }

   fclose(fid);

   navi::gallerytype = SEARCH;                                                         //  search results
   gallery(albumfile,"initF",0);                                                       //  generate gallery of matching files
   gallery(0,"paint",0);
   viewmode('G');

retx:
   busy = 0;
   return ustat;
}


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

//  Report images by metadata tag value or keyword value.
//  Imput a metadata tag name (rating, keywords, title, description, camera)
//  Count images by tag value and make a list of tag values and image counts.
//  If tag name is keywords, the multiple contained values are treated separately.
//  Output a report of tag values and corresponding count of images with tag value.
//  Click on a tag value to get a thumbnail gallery of all images with the tag value.

namespace meta_tags_names
{
   ch       *tagname;                                       //  input tag name
   ch       **tags;                                         //  array, all tag values found in image files
   int      *files;                                         //  array, count of files having each tag value
   int      Ntags;                                          //  count of the above two arrays
   int      Fxmeta;                                         //  flag, tag name is in extra metadata
   zdialog  *zdpop = 0;
}


//  menu function

void m_meta_tags(GtkWidget *, ch *)                                                    //  26.0
{
   using namespace meta_tags_names;

   int meta_tags_clickfunc(GtkWidget *widget, int line, int pos, ch *input);

   int      ii, jj, cc, tcc, vcc;
   ch       *pp, *pp1, *pp2;
   ch       buff[100];
   xxrec_t  *xxrec;
   ch       tagvalue[80], megapix[8];
   int      maxNtags;
   int      Nnotag;
   zlist_t  *zlist;

   printf("m_meta_tags \n");

   F1_help_topic = TX("METADATA TAGS");

   if (Xindexlev < 1) {
      index_rebuild(1,0);
      if (Nxxrec == 0) {
         zmessageACK(Mwin,TX("image index required"));
         return;
      }
   }

   zlist = zlist_new(0);
   zlist_append(zlist,"rating",0);                                                     //  list of tag names to choose from
   zlist_append(zlist,"keywords",0);
   zlist_append(zlist,"title",0);
   zlist_append(zlist,"description",0);
   zlist_append(zlist,"camera",0);
   zlist_append(zlist,"location",0);

   for (ii = 0; ii < xmetaNtags; ii++)                                                 //  add extra metadata tags
      zlist_append(zlist,xmeta_tags[ii],0);

   tagname = popup_choose(zlist);                                                      //  get tag name from user
   if (! tagname) return;

   maxNtags = Nxxrec;                                                                  //  max. unique tag values (1 per file)
   if (strmatch(tagname,"keywords")) maxNtags *= 20;                                   //  if keywords, allow 20 per file

   cc = maxNtags * sizeof(ch *);
   tags = (ch **) zmalloc(cc,"meta_tags");                                             //  allocate tag values

   cc = maxNtags * sizeof(int);
   files = (int *) zmalloc(cc,"meta_tags");                                            //  allocate corresp. file counts

   Ntags = 0;
   Nnotag = 0;
   Fxmeta = 0;

   if (strmatch(tagname,"rating"))                                                     //  rating is wanted
   {
      for (ii = 0; ii < Nxxrec; ii++)
      {
         zmainloop();

         xxrec = xxrec_tab[ii];
         if (! xxrec->rating[0]) Nnotag++;
         else {
            strncpy0(tagvalue,xxrec->rating,2);
            tags[Ntags++] = zstrdup(tagvalue,"meta_tags");
         }
      }
   }

   else if (strmatch(tagname,"keywords"))                                              //  keywords is wanted
   {
      for (ii = 0; ii < Nxxrec; ii++)
      {
         zmainloop();

         xxrec = xxrec_tab[ii];
         if (! xxrec->keywords) {
            Nnotag++;
            continue;
         }

         for (jj = 1; ; jj++)
         {
            pp = substring(xxrec->keywords,",",jj);                                    //  get all keywords
            if (! pp) break;
            if (Ntags < maxNtags)
               tags[Ntags++] = zstrdup(pp,"meta_tags");
            else {
               zmessageACK(Mwin,TX("max keywords exceeded"));
               goto cleanup;
            }
         }
      }
   }

   else if (strmatch(tagname,"title"))                                                 //  title is wanted
   {
      for (ii = 0; ii < Nxxrec; ii++)
      {
         zmainloop();

         xxrec = xxrec_tab[ii];
         if (! xxrec->title) Nnotag++;
         else {
            strncpy0(tagvalue,xxrec->title,80);
            tags[Ntags++] = zstrdup(tagvalue,"meta_tags");
         }
      }
   }

   else if (strmatch(tagname,"description"))                                           //  description is wanted
   {
      for (ii = 0; ii < Nxxrec; ii++)
      {
         zmainloop();

         xxrec = xxrec_tab[ii];
         if (! xxrec->desc) Nnotag++;
         else {
            strncpy0(tagvalue,xxrec->desc,80);
            tags[Ntags++] = zstrdup(tagvalue,"meta_tags");
         }
      }
   }

   else if (strmatch(tagname,"camera"))                                                //  camera is wanted
   {
      for (ii = 0; ii < Nxxrec; ii++)
      {
         zmainloop();

         xxrec = xxrec_tab[ii];
         if (! xxrec->make[0]) Nnotag++;
         else {
            *tagvalue = 0;
            strncatv(tagvalue,80,xxrec->make,"-",xxrec->model,"-",xxrec->lens,null);
            tags[Ntags++] = zstrdup(tagvalue,"meta_tags");
         }
      }
   }

   else if (strmatch(tagname,"location"))                                              //  location is wanted
   {
      for (ii = 0; ii < Nxxrec; ii++)
      {
         zmainloop();

         xxrec = xxrec_tab[ii];
         if (! xxrec->country[0] && ! xxrec->location[0]) Nnotag++;
         else {
            *tagvalue = 0;
            strncatv(tagvalue,80,xxrec->country," ",xxrec->location,null);
            tags[Ntags++] = zstrdup(tagvalue,"meta_tags");
         }
      }
   }

   else                                                                                //  look for extra metadata tag
   {
      for (ii = 0; ii < xmetaNtags; ii++) 
         if (strmatch(tagname,xmeta_tags[ii])) break;

      if (ii == xmetaNtags) {
         zmessageACK(Mwin,TX("tag name %s not in xmeta tag list"),tagname);            //  should not happen
         return;
      }

      Fxmeta = 1;                                                                      //  extra metadata tag is wanted

      tcc = strlen(tagname);                                                           //  extra metadata tag name and cc

      for (ii = 0; ii < Nxxrec; ii++)                                                  //  search for extra metadata
      {
         zmainloop();

         xxrec = xxrec_tab[ii];
         pp = xxrec->xmeta;                                                            //  tag1=value1^ tag2=value2^ ...
                                                                                       //               |    |     |
         while (true)                                                                  //               pp   pp1   pp2
         {
            if (! pp || ! *pp) break;
            if (strmatchN(tagname,pp,tcc) && pp[tcc] == '=') break;
            pp = strchr(pp,'^');
            if (! pp) break;
            pp++;
            if (*pp == ' ') pp++;
         }

         if (! pp || ! *pp) {                                                          //  extra metadata tag not found
            Nnotag++;
            continue;
         }

         pp1 = pp + tcc + 1;
         pp2 = strchr(pp1,'^');
         if (! pp2) strcpy(tagvalue,"invalid xmeta");
         else {
            vcc = pp2 - pp1 + 1;
            if (vcc > 80) vcc = 80;
            strncpy0(tagvalue,pp1,vcc);
         }
         if (strmatch(tagname,"Megapixels"))                                           //  if megapixels
            snprintf(megapix,8,"%3.0f",atof(tagvalue));                                //  round to nearest whole value
         tags[Ntags++] = zstrdup(megapix,"meta_tags");
      }
   }

   if (! Ntags) goto report;

   HeapSort(tags,Ntags);                                                               //  sort tag values

   files[0] = 1;

   for (ii = 0, jj = 1; jj < Ntags; jj++)                                              //  pack down, eliminate duplicates
   {
      if (strmatch(tags[ii],tags[jj])) {
         zfree(tags[jj]);
         tags[jj] = 0;
         files[ii]++;
      }
      else {
         ii++;
         if (jj > ii) {
            if (tags[ii]) zfree(tags[ii]);
            tags[ii] = tags[jj];
            tags[jj] = 0;
         }
         files[ii] = 1;
      }
   }

   Ntags = ii + 1;                                                                     //  reduced tag count

report:

   if (zdialog_valid(zdpop)) popup_report_close(zdpop,0);                              //  close prior if any

   zdpop = popup_report_open("Metadata Tags",Mwin,700,400,0,1,                         //  write report to popup window
                                     meta_tags_clickfunc,"X",null);
   popup_report_header(zdpop,1,"   files  %s",tagname);

   snprintf(buff,100,"%8d  %-80s",Nnotag,"(no tag value)");                            //  output no-tag-value first
   popup_report_write(zdpop,0,"%s \n",buff);

   for (ii = 0; ii < Ntags; ii++)                                                      //  loop tags
   {
      snprintf(buff,100,"%8d  %-80s",files[ii],tags[ii]);                              //  80 = tagvalue max cc
      popup_report_write(zdpop,0,"%s \n",buff);
   }

cleanup:

   for (ii = 0; ii < Ntags; ii++)
      zfree(tags[ii]);                                                                 //  free memory

   zfree(tags);
   zfree(files);
   zlist_free(zlist);

   return;
}


int meta_tags_clickfunc(GtkWidget *widget, int line, int pos, ch *input)               //  26.0
{
   using namespace meta_tags_names;

   int         ii, jj, cc;
   FILE        *fid;
   xxrec_t     *xxrec;
   ch          tags_file[200];
   ch          tagvalue[80], megapix[8];
   ch          camera[80], location[80];
   ch          *pp, *pp1, *pp2;
   int         tcc, vcc;


   if (line < 0) return 1;

   txwidget_scroll(widget,line);                                                       //  keep line on screen
   txwidget_highlight_line(widget,line);                                               //  highlight

   pp = popup_report_line(zdpop,line,0);                                               //  get report line
   if (! pp) return 1;

   strncpy0(tagvalue,pp+10,80);                                                        //  get tag value clicked
   strTrim(tagvalue);
   cc = strlen(tagvalue);

   snprintf(tags_file,200,"%s/Meta Tags",albums_folder);                               //  open output file
   fid = fopen(tags_file,"w");
   if (! fid) goto filerror;

   if (strmatch(tagname,"rating"))                                                     //  rating is wanted
   {
      for (ii = 0; ii < Nxxrec; ii++)                                                  //  loop all files
      {
         zmainloop();                                                                  //  keep GTK alive

         xxrec = xxrec_tab[ii];

         if (strmatch(tagvalue,"(no tag value)")) {                                    //  no rating is wanted
            if (xxrec->rating[0]) continue;
            fprintf(fid,"%s\n",xxrec->file);                                           //  output matching file
         }
         else if (xxrec->rating[0] == tagvalue[0])                                     //  if selected rating,
            fprintf(fid,"%s\n",xxrec->file);                                           //    output matching file
      }
   }

   if (strmatch(tagname,"keywords"))                                                   //  keywords is wanted
   {
      for (ii = 0; ii < Nxxrec; ii++)                                                  //  loop all files
      {
         zmainloop();                                                                  //  keep GTK alive

         xxrec = xxrec_tab[ii];

         if (strmatch(tagvalue,"(no tag value)")) {                                    //  no keyword is wanted
            if (xxrec->keywords) continue;
            fprintf(fid,"%s\n",xxrec->file);                                           //  output matching file
         }

         for (jj = 1; ; jj++)
         {
            pp = substring(xxrec->keywords,",",jj);                                    //  get all keywords for image file
            if (! pp) break;
            if (strmatch(pp,tagvalue)) {                                               //  if selected keyword,
               fprintf(fid,"%s\n",xxrec->file);                                        //    output matching file
               break;
            }
         }
      }
   }

   if (strmatch(tagname,"title"))                                                      //  title is wanted
   {
      for (ii = 0; ii < Nxxrec; ii++)                                                  //  loop all files
      {
         zmainloop();                                                                  //  keep GTK alive

         xxrec = xxrec_tab[ii];

         if (strmatch(tagvalue,"(no tag value)")) {                                    //  no title is wanted
            if (xxrec->title) continue;
            fprintf(fid,"%s\n",xxrec->file);                                           //  output matching file
         }
         else if (xxrec->title && strmatchN(xxrec->title,tagvalue,cc))                 //  if selected title
            fprintf(fid,"%s\n",xxrec->file);                                           //  output matching file
      }
   }

   if (strmatch(tagname,"description"))                                                //  description is wanted
   {
      for (ii = 0; ii < Nxxrec; ii++)                                                  //  loop all files
      {
         zmainloop();                                                                  //  keep GTK alive

         xxrec = xxrec_tab[ii];

         if (strmatch(tagvalue,"(no tag value)")) {                                    //  no title is wanted
            if (xxrec->desc) continue;
            fprintf(fid,"%s\n",xxrec->file);                                           //  output matching file
         }
         else if (xxrec->desc && strmatchN(xxrec->desc,tagvalue,cc))                   //  if selected description,
            fprintf(fid,"%s\n",xxrec->file);                                           //  output matching file
      }
   }

   if (strmatch(tagname,"camera"))                                                     //  camera is wanted
   {
      for (ii = 0; ii < Nxxrec; ii++)                                                  //  loop all files
      {
         zmainloop();                                                                  //  keep GTK alive

         xxrec = xxrec_tab[ii];

         if (strmatch(tagvalue,"(no tag value)")) {                                    //  no title is wanted
            if (xxrec->make[0]) continue;
            fprintf(fid,"%s\n",xxrec->file);                                           //  output matching file
         }
         else if (xxrec->make[0]) {
            *camera = 0;
            strncatv(camera,80,xxrec->make,"-",xxrec->model,"-",xxrec->lens,null);
            strTrim(camera);
            if (strmatch(camera,tagvalue))                                             //  if selected camera,
               fprintf(fid,"%s\n",xxrec->file);                                        //  output matching file
         }
      }
   }

   if (strmatch(tagname,"location"))                                                   //  location is wanted
   {
      for (ii = 0; ii < Nxxrec; ii++)                                                  //  loop all files
      {
         zmainloop();                                                                  //  keep GTK alive

         xxrec = xxrec_tab[ii];

         if (strmatch(tagvalue,"(no tag value)")) {                                    //  no location is wanted
            if (xxrec->country[0] || xxrec->location[0]) continue;
            fprintf(fid,"%s\n",xxrec->file);                                           //  output matching file
         }
         else if (xxrec->country[0] || xxrec->location[0]) {
            *location = 0;
            strncatv(location,80,xxrec->country," ",xxrec->location,null);
            strTrim(location);
            if (strmatch(location,tagvalue))                                           //  if selected location,
               fprintf(fid,"%s\n",xxrec->file);                                        //  output matching file
         }
      }
   }

   if (Fxmeta)                                                                         //  extra metadata tag is wanted
   {
      tcc = strlen(tagname);
      vcc = strlen(tagvalue);                                                          //  extra metadata tag name and value

      for (ii = 0; ii < Nxxrec; ii++)                                                  //  search for extra metadata
      {
         xxrec = xxrec_tab[ii];
         pp = xxrec->xmeta;                                                            //  tag1=value1^ tag2=value2^ ...
                                                                                       //               |    |     |
         while (true)                                                                  //               pp   pp1   pp2
         {
            if (! pp || ! *pp) break;
            if (strmatchN(tagname,pp,tcc) && pp[tcc] == '=') break;
            pp = strchr(pp,'^');
            if (! pp) break;
            pp++;
            if (*pp == ' ') pp++;
         }

         if (! pp || ! *pp) {                                                          //  extra metadata tag not found
            if (strmatch(tagvalue,"(no tag value)"))                                   //  no tag value is wanted
               fprintf(fid,"%s\n",xxrec->file);                                        //  output matching file
            continue;
         }

         pp1 = pp + tcc + 1;                                                           //  get tag value
         pp2 = strchr(pp1,'^');
         if (! pp2) {                                                                  //  invalid xmeta
            if (strmatch(tagvalue,"invalid xmeta"))                                    //  invalid xmeta is wanted
               fprintf(fid,"%s\n",xxrec->file);                                        //  output matching file
            continue;
         }
         
         if (strmatch(tagname,"Megapixels")) {                                         //  megapixels
            snprintf(megapix,8,"%3.0f",atof(pp1));                                     //  round to whole value
            if (strmatch(megapix,tagvalue)) 
               fprintf(fid,"%s\n",xxrec->file);
            continue;
         }

         if (pp2 - pp1 < vcc) continue;                                                //  tag value cc
         if (strmatchN(pp1,tagvalue,vcc))                                              //  if selected tag value within vcc
            fprintf(fid,"%s\n",xxrec->file);                                           //  output matching file
      }
   }

   fclose(fid);

   navi::gallerytype = SEARCH;                                                         //  search results
   gallery(tags_file,"initF",0);                                                       //  generate gallery of matching files
   gallery(0,"paint",0);
   viewmode('G');
   return 1;

filerror:
   zmessageACK(Mwin,TX("file error: %s"),strerror(errno));
   return 1;
}


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

//  Search image keywords, geotags, dates, ratings, titles, descriptions               //  overhauled
//  to find matching images. This is fast using the image index.
//  Search also any other metadata, but relatively slow.

namespace search_images
{
   zdialog  *zd_search = 0;                                                            //  search images dialog

   ch       shDateFrom[20] = "";                                                       //  search images
   ch       shDateTo[20] = "";                                                         //  format is "yyyy:mm:dd hh:mm"

   ch       shRatingFr[4] = "";
   ch       shRatingTo[4] = "";

   ch       shkeywords[searchkeywordsXcc] = "";                                        //  search keywords list
   ch       shtext[searchkeywordsXcc] = "";                                            //  search title and description text list
   ch       shfiles[searchkeywordsXcc] = "";                                           //  search files list

   ch       shLocs[200] = "";                                                          //  search locations

   int      Fscanall, Fscancurr;
   int      Forgver, Flastver, Fallvers;                                               //  25.1
   int      Fnewset, Faddset, Fremset;
   int      Frepgallery, Frepmeta, Fautosearch = 0;
   int      Fphotodate, Ffiledate, Fdaterange, Fnulldate;
   int      Ftext, Ffiles, Fkeywords, Frating, Flocs;
   int      Fallkeywords, Falltext, Fallfiles, Falllocs;

   #define  Nxtagsmax 3                                                                //  max. extra search tags
   int      Nxtags = 0;                                                                //  search tags in use (user selections)
   ch       *srchtags[Nxtagsmax];                                                      //  metadata tags to search
   ch       *machvals[Nxtagsmax];                                                      //  data values to search for
   ch       machtyp[Nxtagsmax];                                                        //  match type: string or number < = >
   int      tagindexed[Nxtagsmax];                                                     //  tag included in extra indexed metadata

   ch       **scanfiles = 0;                                                           //  files to search and select
   int      *passfiles;                                                                //  selected files, ii --> xxrec_tab[ii]
   int      Nscan = 0, Npass = 0;
   int      Ncurrset;
}


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

//  Search function for use in scripts
//     $ fotocx -m autosearch settingsfile
//  A search is performed using the specified search settings file.
//  Upon completion, "search results: <filename>" is written to stdout,
//  where <filename> is a file containing a list of all image files
//  found - those matching the parameters in the search settings file.
//  A search settings file is made using the search dialog the [save] button.

void m_autosearch(GtkWidget *, ch *)
{
   using namespace search_images;

   FILE     *fid;

   printf("m_autosearch \n");
   printf("search parameters: %s \n",initial_file);

   fid = fopen(initial_file,"r");                                                      //  open search parameters file
   if (! fid) zexit(0,"%s: %s",initial_file,strerror(errno));

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

   zdialog_load_widgets(zd_search,0,0,fid);                                            //  load parameters into dialog
   fclose(fid);

   zdialog_send_event(zd_search,"proceed");                                            //  execute search
   zdialog_wait(zd_search);                                                            //  wait for completion
   zdialog_free(zd_search);

   zexit(0,"autosearch exit");
}


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

//  menu function                                                                      //  reduced dialog height

void m_search_images(GtkWidget *, ch *)
{
   using namespace search_images;

   int  search_searchkeywords_clickfunc(GtkWidget *widget, int line, int pos, ch *input);
   int  search_matchkeywords_clickfunc(GtkWidget *widget, int line, int pos, ch *input);
   int  search_defkeywords_clickfunc(GtkWidget *widget, int line, int pos, ch *input);
   int  search_dialog_event(zdialog*, ch *event);

   zdialog     *zd;
   GtkWidget   *widget;

   int         ii, nk;
   static ch   **mlist = 0;
   ch          matchx[8] = "matchx";

   F1_help_topic = TX("SEARCH IMAGES");

   printf("m_search_images \n");

   if (Xindexlev < 1) {
      index_rebuild(1,0);                                                              //  25.1
      if (Nxxrec == 0) {
         zmessageACK(Mwin,TX("image index required"));
         return;
      }
   }

   if (Fblock("search images")) return;

   if (! srchtags[0])                                                                  //  first call initialization
   {
      for (ii = 0; ii < Nxtagsmax; ii++) {
         srchtags[ii] = (ch *) zmalloc(metatagXcc,"search");
         machvals[ii] = (ch *) zmalloc(metadataXcc,"search");
      }
   }

/***
       __________________________________________________________________
      |                     Search Images                                |
      |                                                                  |
      | search: (o) all images  (o) current set only                     |       Fscanall Fscancurr
      | include: [x] original  [x] last version  [x] all versions        |       Forgver Flastver Fallvers                       25.1
      | current set: (o) replace  (o) add to set  (o) remove from set    |       Fnewset Faddset Fremset
      |  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
      | report type: (o) gallery  (o) metadata                           |       Frepgallery Frepmeta
      |  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
      | date range [_______] [_______]  (o) photo  (o) file (yyyy:mm:dd) |
      |  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
      | rating range  [__] [__]  0-5 stars                               |
      |  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
      |                                                          all/any |
      | keywords  [____________________________________________] (o) (o) |
      | title/desc.  [_________________________________________] (o) (o) |
      | file names [___________________________________________] (o) (o) |
      | locations [____________________________________________] (o) (o) |
      |  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
      |     tagname        condition           match values           X  |       these are called 'extra search tags'
      | [_____________|v] [ report   ] [___________________________] [x] |         in the code and comments
      | [_____________|v] [ matches  ] [___________________________] [x] |
      | [_____________|v] [ number = ] [___________________________] [x] |
      |  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
      | Enter Keyword [______________]  Matches [______________________] |
      |  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
      | Keywords Category [___________________|v]                      | |
      | |                                                              | |
      | |                                                              | |
      | |                                                              | |
      | |                                                              | |
      | |                                                              | |
      | |                                                              | |
      | |______________________________________________________________| |
      |                                                                  |
      |                              [Load] [Save] [Clear] [Proceed] [X] |
      |__________________________________________________________________|

***/

   zd = zdialog_new(TX("Search Images"),Mwin,TX("Load"),TX("Save"),TX("Clear"),TX("Proceed"),"X",null);
   zd_search = zd;

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

   zdialog_add_widget(zd,"hbox","hbsell","dialog");
   zdialog_add_widget(zd,"label","labrep","hbsell",TX("include:"),"space=3");
   zdialog_add_widget(zd,"check","org ver","hbsell",TX("original"),"space=3");             //  25.1
   zdialog_add_widget(zd,"check","last ver","hbsell",TX("last version"),"space=3");
   zdialog_add_widget(zd,"check","all vers","hbsell",TX("all versions"),"space=3");

   zdialog_add_widget(zd,"hbox","hbm1","dialog");
   zdialog_add_widget(zd,"label","labs1","hbm1",TX("current set:"),"space=3");
   zdialog_add_widget(zd,"radio","newset","hbm1",TX("replace"),"space=5");
   zdialog_add_widget(zd,"radio","addset","hbm1",TX("add to set"),"space=5");
   zdialog_add_widget(zd,"radio","remset","hbm1",TX("remove from set"),"space=5");

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

   zdialog_add_widget(zd,"hbox","hbrt","dialog");
   zdialog_add_widget(zd,"label","labrt","hbrt",TX("report type:"),"space=3");
   zdialog_add_widget(zd,"radio","repgallery","hbrt",TX("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","hbdt","dialog");
   zdialog_add_widget(zd,"label","labd1","hbdt",TX("date range"),"space=3");
   zdialog_add_widget(zd,"zentry","datefrom","hbdt",0,"size=10|space=5");
   zdialog_add_widget(zd,"zentry","dateto","hbdt",0,"size=10");
   zdialog_add_widget(zd,"radio","photodate","hbdt",TX("photo"),"space=5");
   zdialog_add_widget(zd,"radio","filedate","hbdt",TX("file"));
   zdialog_add_widget(zd,"label","labd2","hbdt","(yyyy:mm:dd)","space=8");

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

   zdialog_add_widget(zd,"hbox","hbrating","dialog");
   zdialog_add_widget(zd,"label","labrat","hbrating",TX("rating range"),"space=5");
   zdialog_add_widget(zd,"zentry","ratingfrom","hbrating",0,"size=3|space=8");
   zdialog_add_widget(zd,"zentry","ratingto","hbrating",0,"size=3|space=8");
   zdialog_add_widget(zd,"label","stars","hbrating",TX("0-5 stars"),"space=5"); 

   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","all/any");

   zdialog_add_widget(zd,"hbox","hbkeywords","dialog","space=3");
   zdialog_add_widget(zd,"label","labkeywords","hbkeywords","keywords","space=3");
   zdialog_add_widget(zd,"text","shkeywords","hbkeywords",0,"expand|wrap|space=3");
   zdialog_add_widget(zd,"radio","allkeywords","hbkeywords",0,"space=5");
   zdialog_add_widget(zd,"radio","anykeywords","hbkeywords",0,"space=5");

   zdialog_add_widget(zd,"hbox","hbtext","dialog","space=3");
   zdialog_add_widget(zd,"label","labtext","hbtext",TX("title/desc."),"space=3");
   zdialog_add_widget(zd,"zentry","shtext","hbtext",0,"expand|space=3");
   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=3");
   zdialog_add_widget(zd,"label","labfiles","hbfiles",TX("file names"),"space=3");
   zdialog_add_widget(zd,"zentry","shfiles","hbfiles",0,"expand|space=3");
   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=3");
   zdialog_add_widget(zd,"label","lablocs","hblocs",TX("locations"),"space=3");
   zdialog_add_widget(zd,"zentry","searchlocs","hblocs",0,"expand|space=3");
   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",TX("enter cities/locations, countries"));

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

   zdialog_add_widget(zd,"hbox","hbmeta","dialog");
   zdialog_add_widget(zd,"vbox","vbtag","hbmeta",0,"space=2|homog");
   zdialog_add_widget(zd,"vbox","vbmatch","hbmeta",0,"space=2|homog");
   zdialog_add_widget(zd,"vbox","vbvalue","hbmeta",0,"space=2|homog|expand");
   zdialog_add_widget(zd,"vbox","vbclear","hbmeta",0,"space=2|homog");

   zdialog_add_widget(zd,"label","lab1","vbtag","tag name");
   zdialog_add_widget(zd,"label","lab2","vbmatch",TX("condition"));
   zdialog_add_widget(zd,"label","lab3","vbvalue",TX("match values"));
   zdialog_add_widget(zd,"label","lab0","vbclear","X");

   zdialog_add_widget(zd,"combo","tag0","vbtag",0,"size=15");                          //  must match Nxtagsmax (now 3)
   zdialog_add_widget(zd,"combo","tag1","vbtag",0,"size=15");
   zdialog_add_widget(zd,"combo","tag2","vbtag",0,"size=15");

   zdialog_add_widget(zd,"combo","match0","vbmatch");                                  //  must match
   zdialog_add_widget(zd,"combo","match1","vbmatch");
   zdialog_add_widget(zd,"combo","match2","vbmatch");

   zdialog_add_widget(zd,"zentry","value0","vbvalue",0,"expand");                      //  must match
   zdialog_add_widget(zd,"zentry","value1","vbvalue",0,"expand");
   zdialog_add_widget(zd,"zentry","value2","vbvalue",0,"expand");

   zdialog_add_widget(zd,"button","clear0","vbclear","x");                             //  must match
   zdialog_add_widget(zd,"button","clear1","vbclear","x");
   zdialog_add_widget(zd,"button","clear2","vbclear","x");

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

   zdialog_add_widget(zd,"hbox","hbnt","dialog",0,"space=1");
   zdialog_add_widget(zd,"label","labnt","hbnt",TX("Enter Keyword"),"space=3");
   zdialog_add_widget(zd,"zentry","enterkeyword","hbnt",0,"size=12");
   zdialog_add_widget(zd,"label","space","hbnt","","space=5");
   zdialog_add_widget(zd,"label","labnt","hbnt",TX("matches"),"space=3");
   zdialog_add_widget(zd,"text","matchkeywords","hbnt",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","Keywords Category","space=3");
   zdialog_add_widget(zd,"combo","defcats","hbdt1",0,"space=10|size=20");

   zdialog_add_widget(zd,"hbox","hbdefkeywords","dialog",0,"expand");                  //  scroll window for defined keywords
   zdialog_add_widget(zd,"scrwin","scrwdefkeywords","hbdefkeywords",0,"expand");
   zdialog_add_widget(zd,"text","defkeywords","scrwdefkeywords",0,"wrap");             //  defined keywords window

   zdialog_add_ttip(zd,"shkeywords",TX("click a defined keyword (below) to add a search keyword"));                           // 25.1
   zdialog_add_ttip(zd,"shtext",TX("search for text within image title or description"));
   zdialog_add_ttip(zd,"shfiles",TX("search for text within file folders or file name"));
   zdialog_add_ttip(zd,"searchlocs",TX("search for names in image location metadata"));

   if (Fautosearch) {                                                                  //  autosearch caller
      zdialog_run(zd,search_dialog_event,"save");                                      //  bypass interactive stuff
      return;
   }

   widget = zdialog_gtkwidget(zd,"shkeywords");                                        //  keyword widget mouse/KB event function
   txwidget_set_eventfunc(widget,search_searchkeywords_clickfunc);

   widget = zdialog_gtkwidget(zd,"matchkeywords");
   txwidget_set_eventfunc(widget,search_matchkeywords_clickfunc);

   widget = zdialog_gtkwidget(zd,"defkeywords");
   txwidget_set_eventfunc(widget,search_defkeywords_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,"org ver",0);
   zdialog_stuff(zd,"last ver",0);
   zdialog_stuff(zd,"all vers",0);
   zdialog_stuff(zd,"allkeywords",0);
   zdialog_stuff(zd,"anykeywords",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);

   nk = zreadfile(meta_picklist_file,mlist);                                           //  get metadata picklist

   for (ii = 0; ii < nk; ii++) {
      zdialog_stuff(zd,"tag0",mlist[ii]);                                              //  metadata picklist > tag picklist
      zdialog_stuff(zd,"tag1",mlist[ii]);
      zdialog_stuff(zd,"tag2",mlist[ii]);
   }

   zreadfile_free(mlist);

   zdialog_stuff(zd,"tag0","(other)");                                                 //  add "other" choice
   zdialog_stuff(zd,"tag1","(other)");
   zdialog_stuff(zd,"tag2","(other)");

   zdialog_stuff(zd,"tag0","");                                                        //  clear picklist choices
   zdialog_stuff(zd,"tag1","");
   zdialog_stuff(zd,"tag2","");

   for (ii = 0; ii < Nxtagsmax; ii++) {                                                //  add operator options
      matchx[5] = '0' + ii;
      zdialog_stuff(zd,matchx,"report");
      zdialog_stuff(zd,matchx,"reportx");
      zdialog_stuff(zd,matchx,"matches");
      zdialog_stuff(zd,matchx,"contains");
      zdialog_stuff(zd,matchx,"number =");
      zdialog_stuff(zd,matchx,"number =>");
      zdialog_stuff(zd,matchx,"number <=");
   }

   zdialog_load_inputs(zd);                                                            //  preload prior user inputs
   zdialog_fetch(zd,"shkeywords",shkeywords,searchkeywordsXcc);
   strcat(shkeywords," ");                                                             //  trailing blank after "keywordname,"

   load_defkeywords(0);                                                                //  stuff defined keywords into dialog
   defkeywords_stuff(zd,"ALL");
   defcats_stuff(zd);                                                                  //  and defined categories

   zdialog_resize(zd,600,900);                                                         //  start dialog
   zdialog_run(zd,search_dialog_event,"save");
   zdialog_wait(zd);                                                                   //  wait for dialog completion
   zdialog_free(zd);
   Fblock(0);
   return;
}


//  search keyword was clicked

int search_searchkeywords_clickfunc(GtkWidget *widget, int line, int pos, ch *input)
{
   using namespace search_images;

   ch     *txkeyword, end = 0;

   if (*input == GDK_KEY_F1) {                                                         //  key F1 pressed, show help
      showz_docfile(Mwin,UGfile,F1_help_topic);
      return 1;
   }

   txkeyword = txwidget_word(widget,line,pos,",;:",end);
   if (! txkeyword) return 1;

   del_keyword(txkeyword,shkeywords);                                                  //  remove from search list
   zdialog_stuff(zd_search,"shkeywords",shkeywords);

   zfree(txkeyword);
   return 1;
}


//  matching keyword was clicked

int search_matchkeywords_clickfunc(GtkWidget *widget, int line, int pos, ch *input)
{
   using namespace search_images;

   ch     *txkeyword, end = 0;

   if (*input == GDK_KEY_F1) {                                                         //  key F1 pressed, show help
      showz_docfile(Mwin,UGfile,F1_help_topic);
      return 1;
   }

   txkeyword = txwidget_word(widget,line,pos,",;",end);
   if (! txkeyword) return 1;

   add_keyword(txkeyword,shkeywords,searchkeywordsXcc);                                //  add to search keyword list

   zdialog_stuff(zd_search,"enterkeyword","");                                         //  update dialog widgets
   zdialog_stuff(zd_search,"matchkeywords","");
   zdialog_stuff(zd_search,"shkeywords",shkeywords);

   zdialog_goto(zd_search,"enterkeyword");                                             //  focus back to enterkeyword widget

   zfree(txkeyword);
   return 1;
}


//  defined keyword was clicked

int search_defkeywords_clickfunc(GtkWidget *widget, int line, int pos, ch *input)
{
   using namespace search_images;

   ch     *txkeyword, end = 0;

   if (*input == GDK_KEY_F1) {                                                         //  key F1 pressed, show help
      showz_docfile(Mwin,UGfile,F1_help_topic);
      return 1;
   }

   txkeyword = txwidget_word(widget,line,pos,",;:",end);
   if (! txkeyword || end == ':') return 1;                                            //  nothing or keyword category, ignore

   add_keyword(txkeyword,shkeywords,searchkeywordsXcc);                                //  add to search keyword list
   zdialog_stuff(zd_search,"shkeywords",shkeywords);

   zfree(txkeyword);
   return 1;
}


//  search images dialog event and completion callback function

int search_dialog_event(zdialog *zd, ch *event)
{
   using namespace search_images;

   int   search_metadata_dialog(zdialog *zd);
   void  search_main();
   void  search_xmeta();
   void  search_add_related_files(void);
   int   search_metadata_report(void);

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

   ch       *file;
   ch       mm[4] = "mm";
   int      ii, jj, err, cc;
   int      nt, cc1, cc2, ff;
   float    fnum;
   ch       *pp, *pp1, *pp2;
   ch       enterkeyword[keywordXcc], matchkeywords[20][keywordXcc];
   ch       matchkeywordstext[(keywordXcc+2)*20];
   ch       catgname[keywordXcc];
   ch       albumfile[200];
   ch       tagx[8] = "tagx", valuex[8] = "valuex", matchx[8] = "matchx";
   ch       wname[8], temp[100];
   FILE     *srfid;                                                                    //  search results output file

   if (strmatch(event,"proceed")) zd->zstat = 4;                                       //  "proceed" from autosearch

   if (zd->zstat < 0 || zd->zstat == 5) return 1;                                      //  canceled

   if (zd->zstat == 1) {                                                               //  [load] settings from file
      zd->zstat = 0;
      zdialog_load_widgets(zd,0,"saved_searches",0);                                   //  folder name under get_zhomedir()
      zdialog_fetch(zd,"shkeywords",shkeywords,searchkeywordsXcc);
      strcat(shkeywords," ");                                                          //  trailing blank after "keywordname,"
      return 1;
   }

   if (zd->zstat == 2) {                                                               //  [save] settings to file
      zd->zstat = 0;
      zdialog_save_widgets(zd,0,"saved_searches",0);
      return 1;
   }

   if (zd->zstat == 3)                                                                 //  [clear] selection criteria
   {
      zd->zstat = 0;                                                                   //  keep dialog active
      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,"org ver",0);
      zdialog_stuff(zd,"last ver",0);
      zdialog_stuff(zd,"all vers",0);
      zdialog_stuff(zd,"allkeywords",0);
      zdialog_stuff(zd,"anykeywords",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,"ratingfrom","");
      zdialog_stuff(zd,"ratingto","");
      zdialog_stuff(zd,"shkeywords","");
      zdialog_stuff(zd,"shtext","");
      zdialog_stuff(zd,"shfiles","");
      zdialog_stuff(zd,"searchlocs","");

      *shkeywords = 0;
      Flocs = 0;
      *shLocs = 0;
      Nxtags = 0;

      for (ii = 0; ii < Nxtagsmax; ii++) {                                             //  erase extra search tags
         tagx[3] = '0' + ii;
         valuex[5] = '0' + ii;
         matchx[5] = '0' + ii;
         zdialog_stuff(zd,tagx,"");
         zdialog_stuff(zd,matchx,"");
         zdialog_stuff(zd,valuex,"");
      }

      return 1;
   }

   if (zd->zstat == 4) {                                                               //  [proceed] with search
      zd->zstat = 0;                                                                   //  keep dialog active
      goto validate;
   }

   if (strmatch(event,"org ver"))                                                      //  get original image version            25.1
      zdialog_stuff(zd,"all vers",0);                                                  //  implies not all versions

   if (strmatch(event,"last ver"))                                                     //  get last version
      zdialog_stuff(zd,"all vers",0);                                                  //  implies not all versions

   if (strmatch(event,"all vers")) {                                                   //  get all versions
      zdialog_stuff(zd,"org ver",0);                                                   //  implies not original version
      zdialog_stuff(zd,"last ver",0);                                                  //  implies not last version
   }

   if (strmatch(event,"enterkeyword"))                                                 //  new keyword is being typed in
   {
      zdialog_stuff(zd,"matchkeywords","");                                            //  clear matchkeywords in dialog

      zdialog_fetch(zd,"enterkeyword",enterkeyword,keywordXcc);                        //  get chars. typed so far
      cc1 = strlen(enterkeyword);

      for (ii = jj = 0; ii <= cc1; ii++) {                                             //  remove foul characters
         if (strchr(",:;",enterkeyword[ii])) continue;
         enterkeyword[jj++] = enterkeyword[ii];
      }

      if (jj < cc1) {                                                                  //  something was removed
         enterkeyword[jj] = 0;
         cc1 = jj;
         zdialog_stuff(zd,"enterkeyword",enterkeyword);
      }

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

      for (ii = nt = 0; ii < maxkeywordcats; ii++)                                     //  loop all categories
      {
         pp2 = defined_keywords[ii];                                                   //  category: aaaaaa, bbbbb, ... keywordN,
         if (! pp2) continue;                                                          //            |     |
         pp2 = strchr(pp2,':');                                                        //            pp1   pp2

         while (true)                                                                  //  loop all defkeywords in category
         {
            pp1 = pp2 + 2;
            if (! *pp1) break;
            pp2 = strchr(pp1,',');
            if (! pp2) break;
            if (strmatchcaseN(enterkeyword,pp1,cc1)) {                                 //  defkeyword matches chars. typed so far
               cc2 = pp2 - pp1;
               strncpy(matchkeywords[nt],pp1,cc2);                                     //  save defkeywords that match
               matchkeywords[nt][cc2] = 0;
               if (++nt == 20) return 1;                                               //  quit if 20 matches or more
            }
         }
      }

      if (nt == 0) return 1;                                                           //  no matches

      pp1 = matchkeywordstext;

      for (ii = 0; ii < nt; ii++)                                                      //  make list: aaaaa, bbb, cccc ...
      {
         strcpy(pp1,matchkeywords[ii]);
         pp1 += strlen(pp1);
         strcpy(pp1,", ");
         pp1 += 2;
      }

      zdialog_stuff(zd,"matchkeywords",matchkeywordstext);                             //  stuff matchkeywords in dialog
      return 1;
   }

   if (strmatch(event,"defcats")) {                                                    //  new keyword category selection
      zdialog_fetch(zd,"defcats",catgname,keywordXcc);
      defkeywords_stuff(zd,catgname);
   }

   if (strstr(event,"tag"))                                                            //  metadata tagN selected
   {
      snprintf(wname,8,"match%c",event[3]);                                            //  widget tagN >> widget matchN
      zdialog_stuff(zd,wname,"report");                                                //  set "report" default operator
   }

   if (strstr(event,"clear"))                                                          //  extra search tag clear button
   {
      ii = event[5];                                                                   //  character N
      tagx[3] = ii;
      valuex[5] = ii;
      matchx[5] = ii;
      zdialog_stuff(zd,tagx,"");
      zdialog_stuff(zd,matchx,"");
      zdialog_stuff(zd,valuex,"");
   }

   if (strstr(event,"tag"))
   {
      for (ii = 0; ii < Nxtagsmax; ii++)                                               //  if extra search tag "(other)"
      {                                                                                //     get tag name from user
         tagx[3] = '0' + ii;
         valuex[5] = '0' + ii;
         zdialog_fetch(zd,tagx,temp,100);
         if (strmatch(temp,"(other)")) {
            pp = zdialog_text1(zd->parent,TX("enter tag name"),0);
            if (pp) {
               zdialog_stuff(zd,tagx,pp);
               zfree(pp);
            }
            else zdialog_stuff(zd,tagx,"");
         }
      }
   }
   
   for (ii = 0; ii < Nxtagsmax; ii++)                                                  //  if extra search tag match type
   {                                                                                   //    "report" or "reportx", clear match value
      valuex[5] = '0' + ii;
      matchx[5] = '0' + ii;
      zdialog_fetch(zd,matchx,temp,20);
      if (strstr("report reportx",temp))
         zdialog_stuff(zd,valuex,"");                                                  //  causes gtk error but works OK
   }

   return 1;                                                                           //  wait for dialog completion

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

validate:

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

   if (Faddset && Fscancurr) {
      zmessageACK(Mwin,TX("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,"org ver",Forgver);                                                //  get original image                    25.1
   zdialog_fetch(zd,"last ver",Flastver);                                              //  get last version
   zdialog_fetch(zd,"all vers",Fallvers);                                              //  get original and all versions

   zdialog_fetch(zd,"datefrom",shDateFrom,20);                                         //  get search date range
   zdialog_fetch(zd,"dateto",shDateTo,20);
   zdialog_fetch(zd,"photodate",Fphotodate);                                           //  photo or file date
   zdialog_fetch(zd,"filedate",Ffiledate);
   zdialog_fetch(zd,"ratingfrom",shRatingFr,2);                                        //  get search rating range
   zdialog_fetch(zd,"ratingto",shRatingTo,2);

   zdialog_fetch(zd,"shkeywords",shkeywords,searchkeywordsXcc);                        //  get search keywords
   zdialog_fetch(zd,"shtext",shtext,searchkeywordsXcc);                                //  get search text*
   zdialog_fetch(zd,"shfiles",shfiles,searchkeywordsXcc);                              //  get search /path*/file*
   zdialog_fetch(zd,"searchlocs",shLocs,200);                                          //  get search locations

   zdialog_fetch(zd,"allkeywords",Fallkeywords);                                       //  get match all/any options
   zdialog_fetch(zd,"alltext",Falltext);
   zdialog_fetch(zd,"allfiles",Fallfiles);
   zdialog_fetch(zd,"alllocs",Falllocs);

   Fnulldate = 0;
   Fdaterange = 0;

   if (strmatchcase(shDateFrom,"null")) {                                              //  search for missing photo date
      Fnulldate = 1;                                                                   //  (user input "null")
      Fphotodate = 1;                                                                  //  force photo date search
      Ffiledate = 0;
      Fdaterange = 0;
      zdialog_stuff(zd,"photodate",1);                                                 //  photo date will be tested
      zdialog_stuff(zd,"filedate",0);
   }

   if (! Fnulldate && (*shDateFrom || *shDateTo))                                      //  complete partial date/time data
   {
      Fdaterange = 1;

      cc = strlen(shDateFrom);
      for (ii = cc; ii < 20; ii++)                                                     //  default date from:
         shDateFrom[ii] = dateLoDefault[ii];                                           //    0000-01-01 00:00:00

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

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

      ff = 0;                                                                          //  check search dates reasonable
      if (! checkDT(shDateFrom)) ff = 1;                                               //  invalid year/mon/day (e.g. mon 13)
      if (! checkDT(shDateTo)) ff = 1;                                                 //    or hour/min/sec (e.g. hour 33)
      if (strcmp(shDateFrom,shDateTo) > 0) ff = 1;                                     //  search-from date > search-to date
      if (ff) {
         zmessageACK(Mwin,TX("invalid date range: %s %s"),shDateFrom,shDateTo);
         zd->zstat = 0;
         Fdaterange = 0;
         return 1;
      }
   }

   Frating = 0;
   if (*shRatingFr || *shRatingTo) {
      Frating = 1;                                                                     //  rating was given
      ii = *shRatingFr;
      if (! ii) ii = '0';
      if (ii < '0' || ii > '5') Frating = 0;                                           //  validate inputs
      jj = *shRatingTo;
      if (! jj) jj = '5';
      if (jj < '0' || jj > '5') Frating = 0;
      if (jj < ii) Frating = 0;
      if (! Frating) {
         zmessageACK(Mwin,TX("invalid rating range"));
         zd->zstat = 0;
         return 1;
      }
   }

   Ffiles = 0;
   if (*shfiles) Ffiles = 1;                                                           //  search path / file (fragment) was given

   Ftext = 0;
   if (*shtext) Ftext = 1;                                                             //  search text was given

   Fkeywords = 0;
   if (*shkeywords) Fkeywords = 1;                                                     //  search keywords was given

   Flocs = 0;
   if (*shLocs) Flocs = 1;                                                             //  search locations was given

   Nxtags = 0;

   for (ii = jj = 0; ii < Nxtagsmax; ii++)                                             //  process extra search tags
   {
      tagx[3] = '0' + ii;
      matchx[5] = '0' + ii;
      valuex[5] = '0' + ii;

      zdialog_fetch(zd,tagx,srchtags[ii],metatagXcc);                                  //  get search tag
      strCompress(srchtags[ii]);                                                       //  remove all blanks from tag names
      if (*srchtags[ii] <= ' ') {
         zdialog_stuff(zd,matchx,"");                                                  //  empty search tag position
         zdialog_stuff(zd,valuex,"");
         continue;
      }

      memmove(srchtags[jj],srchtags[ii],metatagXcc);                                   //  repack blank tags

      zdialog_fetch(zd,matchx,temp,20);                                                //  get corresp. match type
      if      (strmatch(temp,"report")) machtyp[jj] = 'r';
      else if (strmatch(temp,"reportx")) machtyp[jj] = 'x';
      else if (strmatch(temp,"matches")) machtyp[jj] = 'm';
      else if (strmatch(temp,"contains")) machtyp[jj] = 'c';
      else if (strmatch(temp,"number =")) machtyp[jj] = '=';
      else if (strmatch(temp,"number =>")) machtyp[jj] = '>';
      else if (strmatch(temp,"number <=")) machtyp[jj] = '<';
      else {
         zdialog_stuff(zd,matchx,"report");                                            //  unspecified >> report
         machtyp[jj] = 'r';
      }
      
      if (machtyp[jj] == 'r' || machtyp[jj] == 'x') {                                  //  force metadata report type            26.0
         zdialog_stuff(zd,"repgallery",0);
         zdialog_stuff(zd,"repmeta",1);
         Frepgallery = 0;
         Frepmeta = 1;
      }

      zdialog_fetch(zd,valuex,machvals[ii],100);                                       //  get corresp. match value
      strTrim2(machvals[jj],machvals[ii]);                                             //  trim leading and trailing blanks

      if (strstr(temp,"number")) {                                                     //  check numeric values
         err = convSF(machvals[jj],fnum);
         if (err) {
            snprintf(temp,100,TX("need numeric match value: %s"),srchtags[jj]);
            zmessageACK(Mwin,temp);
            zd->zstat = 0;
            return 1;
         }
      }

      if (ii > jj) *srchtags[ii] = *machvals[ii] = 0;
      jj++;
   }

   Nxtags = jj;                                                                        //  extra search tags count

   for (ii = 0; ii < Nxtags; ii++)                                                     //  classify extra search tags            26.0
      tagindexed[ii] = metatagtype(srchtags[ii]);

   //  Begin search -------------------------------------------------------------
   //  Scan all files or current set (gallery)
   //  Test files against select criteria in search dialog

   if (Fscanall) Nscan = Nxxrec;                                                       //  scan all files
   if (Fscancurr) Nscan = navi::Gfiles;                                                //  scan current set (current gallery)
   if (! Nscan) {
      Ncurrset = 0;
      goto search_complete;
   }

   cc = Nscan * sizeof(ch *);                                                          //  list of files to scan
   scanfiles = (ch **) zmalloc(cc,"search");

   for (ii = 0; ii < Nscan; ii++)                                                      //  create scanfiles[] list
   {
      file = 0;
      if (Fscanall) file = xxrec_tab[ii]->file;                                        //  all files from xxrec_tab[]
      if (Fscancurr) file = gallery(0,"getR",ii);                                      //  current gallery files
      scanfiles[ii] = file;
   }

   cc = maximages * sizeof(int);                                                       //  memory for selected files
   passfiles = (int *) zmalloc(cc,"search");                                           //  (ii --> xxrec_tab[ii])                25.1
   Npass = 0;

   search_main();                                                                      //  test main select criteria

   search_xmeta();                                                                     //  test extra search tags                26.0

   search_add_related_files();                                                         //  add related files (org/last/all versions)

   if (Fescape) goto usercancel;                                                       //  user killed search

   if (Fnewset) { /* do nothing */ }                                                   //  new set: no changes

   if (Faddset)                                                                        //  add results to prior results          25.1
   {
      for (ii = 0; ii < navi::Gfiles; ii++)                                            //  add gallery files (prior results)
         passfiles[Npass++] = xxrec_index(gallery(0,"getR",ii));                       //    to selected files

      HeapSort(passfiles,Npass);                                                       //  sort passfiles

      for (ii = jj = 0; ii < Npass; ii++) {                                            //  eliminate duplicates
         if (passfiles[ii] == passfiles[jj]) continue;
         passfiles[++jj] = passfiles[ii];
      }
      Npass = jj + 1;
   }

   if (Fremset)                                                                        //  remove results from prior results     25.1
   {
      for (ii = 0; ii < navi::Gfiles; ii++)                                            //  add gallery files (prior results)
         passfiles[Npass++] = xxrec_index(gallery(0,"getR",ii));                       //    to selected files

      HeapSort(passfiles,Npass);                                                       //  sort passfiles

      for (ii = 0; ii < Npass-1; ii++) {                                               //  duplicate pairs = -1
         if (passfiles[ii] == passfiles[ii+1]) {
            passfiles[ii] = passfiles[ii+1] = -1;
            ii++;
         }
      }

      for (ii = jj = 0; ii < Npass; ii++) {                                            //  remove -1 pairs
         if (passfiles[ii] > -1)
            passfiles[jj++] = passfiles[ii];
      }

      Npass = jj;
   }

   if (Npass)
   {
      srfid = fopen(searchresults_file,"w");                                           //  open new output file                  25.1
      if (! srfid) goto filerror;

      for (ii = 0; ii < Npass; ii++)                                                   //  passfiles[] --> search results file
      {
         jj = passfiles[ii];
         file = xxrec_tab[jj]->file;
         cc = fprintf(srfid,"%s\n",file);
         if (! cc) break;
      }

      fclose(srfid);
      if (! cc) goto filerror;
   }

   Ncurrset = Npass;                                                                   //  current set, including last results

//  search complete -------------------------------------------------------------

search_complete:

   Fescape = 0;

   if (scanfiles) zfree(scanfiles);
   scanfiles = 0;
   if (passfiles) zfree(passfiles);
   passfiles = 0;

   printf("search count: %d \n", Ncurrset);
   if (Ncurrset == 0) {
      if (Fnewset || Faddset) zmessageACK(Mwin,TX("nothing found"));
      if (Fremset) zmessageACK(Mwin,TX("nothing left, no change made"));
      return 1;
   }

   snprintf(albumfile,200,"%s/search_results",albums_folder);                          //  save search results in the
   err = cp_copy(searchresults_file,albumfile);                                        //    album "search_results"
   if (err) zmessageACK(Mwin,strerror(err));

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

   if (Frepmeta) {                                                                     //  metadata report format
      navi::gallerytype = META;                                                        //  report
      search_metadata_report();
      m_metaview(0,0);
   }
   else m_thumbview(0,0);

   return 1;

usercancel:                                                                            //  cancel via escape key
   zmessage_post(Mwin,"parent",1,TX("function canceled"));
   Fescape = 0;

   if (scanfiles) zfree(scanfiles);
   scanfiles = 0;
   if (passfiles) zfree(passfiles);
   passfiles = 0;
   return 1;

filerror:
   zmessageACK(Mwin,TX("file error: %s"),strerror(errno));
   Fescape = 0;

   if (scanfiles) zfree(scanfiles);
   scanfiles = 0;
   if (passfiles) zfree(passfiles);
   passfiles = 0;
   return 1;
}


//  Test image files against main selection criteria
//  Mark matching files

void search_main()
{
   using namespace search_images;

   int      ii, jj, ff, iis, iit, iif;
   int      Nmatch, Nnomatch, match1;
   ch       *pps, *ppf, *ppt;
   ch       *file;
   xxrec_t  *xxrec;

   if (! Nscan) {
      Npass = 0;
      return;
   }

   for (ff = 0; ff < Nscan; ff++)                                                      //  loop through files to scan
   {
      zmainloop();

      if (Fescape) {                                                                   //  25.1
         Npass = 0;
         return;
      }

      file = scanfiles[ff];

      xxrec = get_xxrec(file);
      if (! xxrec) goto nomatch;                                                       //  deleted, not an image file

      if (Ffiles)                                                                      //  file name match is wanted
      {
         Nmatch = Nnomatch = 0;

         for (ii = 1; ; ii++)
         {
            pps = substringR(shfiles," ,",ii);                                         //  step thru search file names
            if (! pps) break;
            if (strcasestr(file,pps)) Nmatch++;                                        //  use substring matching
            else Nnomatch++;
            zfree(pps);
         }

         if (Nmatch == 0) goto nomatch;                                                //  no match any file
         if (Fallfiles && Nnomatch) goto nomatch;                                      //  no match all files
      }

      if (Fnulldate && *xxrec->pdate) goto nomatch;                                    //  missing photo date wanted

      else if (Fdaterange)                                                             //  from-to date range specified
      {
         if (Fphotodate) {
            if (! *xxrec->pdate) goto nomatch;                                         //  test photo date
            if (strcmp(xxrec->pdate,shDateFrom) < 0) goto nomatch;
            if (strcmp(xxrec->pdate,shDateTo) > 0) goto nomatch;
         }

         if (Ffiledate) {                                                              //  test file mod date
            if (strcmp(xxrec->fdate,shDateFrom) < 0) goto nomatch;
            if (strcmp(xxrec->fdate,shDateTo) > 0) goto nomatch;
         }
      }

      if (Fnulldate && *xxrec->pdate) goto nomatch;                                    //  select for null photo date

      if (Fkeywords)                                                                   //  keywords match is wanted
      {
         Nmatch = Nnomatch = 0;

         if (! xxrec->keywords)                                                        //  file has no keywords                  25.1
         {
            for (iis = 1; ; iis++)                                                     //  step thru search keywords
            {
               pps = substringR(shkeywords,",;",iis);                                  //  delimited
               if (! pps) break;
               if (strmatch(pps,"null")) Nmatch++;                                     //  "null" keyword wanted, match
               zfree(pps);
            }
         }

         for (iis = 1; ; iis++)                                                        //  step thru search keywords
         {
            pps = substringR(shkeywords,",;",iis);                                     //  delimited
            if (! pps) break;
            if (*pps == 0) {
               zfree(pps);
               continue;
            }

            for (iif = 1; ; iif++)                                                     //  step thru file keywords
            {
               ppf = substringR(xxrec->keywords,",;",iif);                             //  count matches and fails
               if (! ppf) {
                  Nnomatch++;                                                          //  no keyword matched
                  break;
               }
               if (strmatch(pps,ppf)) {
                  Nmatch++;                                                            //  1 or more keyword matches
                  zfree(ppf);
                  break;
               }
               else zfree(ppf);
            }

            zfree(pps);
         }

         if (Nmatch == 0) goto nomatch;                                                //  no match to any keyword
         if (Fallkeywords && Nnomatch) goto nomatch;                                   //  no match to all keywords
      }

      if (Frating)                                                                     //  rating match is wanted
      {
         if (*shRatingFr && xxrec->rating[0] < *shRatingFr) goto nomatch;
         if (*shRatingTo && xxrec->rating[0] > *shRatingTo) goto nomatch;
      }

      if (Ftext)                                                                       //  text match is wanted
      {
         Nmatch = Nnomatch = 0;

         for (iis = 1; ; iis++)                                                        //  step through search words
         {
            pps = substringR(shtext,", ",iis);
            if (! pps) break;

            match1 = 0;

            for (iit = 1; ; iit++)                                                     //  step through title words
            {
               ppt = substringR(xxrec->title," ,.;:?/'\"",iit);                        //  delimiters: blank , . ; : ? / ' "
               if (! ppt) break;
               if (strcasestr(ppt,pps)) match1 = 1;                                    //  match search amd title words
               zfree(ppt);
               if (match1) break;
            }

            if (! match1)
            {
               for (iit = 1; ; iit++)                                                  //  step through description words
               {
                  ppt = substringR(xxrec->desc," ,.;:?/'\"",iit);
                  if (! ppt) break;
                  if (strcasestr(ppt,pps)) match1 = 1;                                 //  match search and description words
                  zfree(ppt);
                  if (match1) break;
               }
            }

            if (match1) Nmatch++;                                                      //  count words matched and not matched
            else Nnomatch++;

            zfree(pps);
         }

         if (Nmatch == 0) goto nomatch;                                                //  no match to any word
         if (Falltext && Nnomatch) goto nomatch;                                       //  no match to all words
      }

      if (Flocs )                                                                      //  location match is wanted
      {
         Nmatch = Nnomatch = 0;

         for (iis = 1; ; iis++)                                                        //  step thru search locations
         {
            pps = substringR(shLocs,',',iis);                                          //  comma delimiter                       25.3
            if (! pps) break;
            if (strcasestr(xxrec->location,pps)) Nmatch++;
            else if (strcasestr(xxrec->country,pps)) Nmatch++;
            else Nnomatch++;
            zfree(pps);
         }

         if (! Nmatch) goto nomatch;                                                   //  no match found
         if (Falllocs && Nnomatch) goto nomatch;
      }

      continue;                                                                        //  file passed main select criteria

    nomatch:                                                                           //  file does not match
      scanfiles[ff] = 0;                                                               //  remove from scanfiles list
      continue;
   }

   for (ii = jj = 0; ii < Nscan; ii++)                                                 //  passfiles[] = remaining scanfiles[]
      if (scanfiles[ii])
         passfiles[jj++] = xxrec_index(scanfiles[ii]);                                 //  25.1

   Npass = jj;                                                                         //  count of passed files

   return;
}


//  Test extra search tags against select criteria.
//  Use meta_getN() to get all search tags:
//    in xxrec_tab[], in extra indexed metadata, not indexed.

void search_xmeta()
{
   using namespace search_images;

   int  searchmeta_test1(ch *tagval, ch  machtyp, ch *machvals);

   ch       **passfiles2;
   ch       **tagval2;
   ch       *xtag[xmetaXtags], *xval[xmetaXtags];
   int      ii, jj, jj1, jj2;
   int      ff, cc, NK, pass;

   if (! Npass) return;
   if (! Nxtags) return;

   NK = Nxtags;

   cc = Npass * sizeof(ch *);                                                          //  allocate memory for file names
   passfiles2 = (ch **) zmalloc(cc,"search");

   for (ii = 0; ii < Npass; ii++) {                                                    //  convert list of xxrec_tab indexes
      jj = passfiles[ii];                                                              //    to list of file names
      passfiles2[ii] = xxrec_tab[jj]->file;
   }

   cc = Npass * NK * sizeof(ch *);                                                     //  allocate space for returned data
   tagval2 = (ch **) zmalloc(cc,"search");

   meta_getN(passfiles2,Npass,srchtags,tagval2,NK,0);                                  //  get metadata for all files

   for (ff = 0; ff < Npass; ff++)                                                      //  loop through files to scan
   {
      zmainloop();

      if (Fescape) {
         Npass = 0;
         goto retx;
      }

      jj1 = ff * NK;                                                                   //  tagval2[] range for file[ff]
      jj2 = jj1 + NK;

      for (ii = 0, jj = jj1; jj < jj2; ii++, jj++) {                                   //  get tag names and values for file[ff]
         xtag[ii] = srchtags[ii];
         xval[ii] = tagval2[jj];
      }

      for (ii = 0; ii < NK; ii++)                                                      //  loop search tags
      {
         for (jj = 0; jj < NK; jj++)                                                   //  find matching file tag data
         {
            if (strmatchcase(srchtags[ii],xtag[jj])) {                                 //  if found, test metadata
               pass = searchmeta_test1(xval[jj],machtyp[ii],machvals[ii]);             //     against select criteria
               if (! pass) goto nomatch;                                               //  fail, no more testing needed
               break;
            }
         }
      }

      continue;                                                                        //  file metadata fits criteria, next file

    nomatch:                                                                           //  metadata does not match
      passfiles[ff] = -1;                                                              //  remove file from pass list
      continue;                                                                        //  next file
   }

   for (ii = jj = 0; ii < Npass; ii++)                                                 //  remove non-matching files from list   25.1
      if (passfiles[ii] >= 0)
         passfiles[jj++] = passfiles[ii];

   Npass = jj;                                                                         //  count of passed files

retx:

   for (ii = 0; ii < Npass * NK; ii++)                                                 //  free memory from meta_getN()
      if (tagval2[ii]) zfree(tagval2[ii]);
   zfree(tagval2);

   zfree(passfiles2);                                                                  //  free file names list

   return;
}


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

int searchmeta_test1(ch *tagval, ch machtyp, ch *machvals)
{
   using namespace search_images;

   int         nth, n1, n2, n3, mm;
   ch          *pps, *ppm;
   float       Ftagval = 0, Fsearchval;
   
   if (machtyp == 'r') return 1;                                                       //  tag value reported, not tested

   if (machtyp == 'x') {                                                               //  exists, reported, not tested
      if (tagval && *tagval > ' ') return 1;                                           //  tag value present, pass               26.0
      else return 0;                                                                   //  no data, fail
   }

   if (! tagval || ! *tagval) {                                                        //  no metadata present                   26.0
      if (! machvals || *machvals <= ' ') return 1;                                    //  search is for empty data, pass
      return 0;                                                                        //  fail
   }

   if (strchr("= > <",machtyp)) {                                                      //  real value, look for N/N format
      Ftagval = atofz(tagval);
      n1 = sscanf(tagval,"%d/%d",&n2,&n3);
      if (n1 == 2) Ftagval = 1.0 * n2 / n3;
   }

   for (nth = 1; ; nth++)                                                              //  loop all search values
   {
      pps = substringR(machvals,',',nth);                                              //  comma delimiter
      if (! pps) return 0;                                                             //  no more, no match found

      if (machtyp == 'm') {                                                            //  tag matches any value
         mm = strcasecmp(tagval,pps);                                                  //  match not case sensitive
         zfree(pps);
         if (mm == 0) return 1;                                                        //  match
      }

      else if (machtyp == 'c') {                                                       //  tag contains any value
         ppm = strcasestr(tagval,pps);                                                 //  match not case sensitive
         zfree(pps);
         if (ppm) return 1;                                                            //  found
      }

      else if (machtyp == '=') {                                                       //  numeric tag equals any value
         Fsearchval = atofz(pps);
         zfree(pps);
         if (Ftagval == Fsearchval) return 1;                                          //  found match
      }

      else if (machtyp == '>') {                                                       //  numeric tag >= one value
         Fsearchval = atofz(pps);
         zfree(pps);
         if (Ftagval >= Fsearchval) return 1;                                          //  found match
      }

      else if (machtyp == '<') {                                                       //  numeric tag <= one value
         Fsearchval = atofz(pps);
         zfree(pps);
         if (Ftagval <= Fsearchval) return 1;                                          //  found match
      }

      else {
         printf("*** searchmeta invalid machtyp %c \n",machtyp);
         zfree(pps);
         return 0;
      }
   }
}


//  add related files to search results if wanted
//  (original image, last version, all versions)

void search_add_related_files()                                                        //  overhauled                            25.1
{
   using namespace search_images;

   int      cc, ii, jj, nv, Npass2;
   int      *flist;
   ch       **vlist;
   ch       *file, *file2;
   ch       *pp1, *pp2;

   if (! Npass) return;                                                                //  no search result
   if (Forgver + Flastver + Fallvers == 0) return;                                     //  no add related files

   cc = Npass * sizeof(int);
   flist = (int *) zmalloc(cc,"search");
   for (ii = 0; ii < Npass; ii++)                                                      //  copy search results to flist
      flist[ii] = passfiles[ii];

   for (ii = 1; ii < Npass; ii++)                                                      //  reduce flist to one file version
   {                                                                                   //     for each group of file versions
      zmainloop();

      if (Fescape) {
         Npass = 0;
         goto retx;
      }

      jj = flist[ii];                                                                  //  search results file
      file = xxrec_tab[jj]->file;
      pp1 = strrchr(file,'/');
      if (! pp1) continue;
      pp1 = strrchr(pp1,'.');
      if (! pp1) continue;                                                             //  /.../filename.vNN.ext
      pp2 = pp1 - 4;                                                                   //  |            |   |
      if (strmatchN(pp2,".v",2) &&                                                     //  file        pp2  pp1
           (pp2[2] >= '0' && pp2[2] <= '9') &&
              (pp2[3] >= '0' && pp2[3] <= '9'))
         cc = pp2 - file + 1;                                                          //  versioned file: filename.vNN.ext
      else
         cc = pp1 - file + 1;                                                          //  original filename

      jj = flist[ii-1];                                                                //  prior search results file
      file2 = xxrec_tab[jj]->file;
      if (strmatchN(file,file2,cc))                                                    //  if same base name, eliminate prior
         flist[ii-1] = -1;
   }                                                                                   //  retain only last file in family

   Npass2 = Npass;

   for (ii = 0; ii < Npass; ii++)                                                      //  loop all files in flist
   {
      zmainloop();

      jj = flist[ii];
      if (jj < 0) continue;                                                            //  skip removed file
      file = xxrec_tab[jj]->file;
      vlist = file_all_versions(file,nv);                                              //  get file original + all versions
      if (! vlist) continue;                                                           //  should not happen

      if (Forgver && Flastver && nv == 1) {                                            //  original + last version wanted
         passfiles[Npass2++] = xxrec_index(vlist[0]);                                  //    and only 1 file exists
         zfree(vlist);                                                                 //  add file to search results
         continue;                                                                     //  stop here
      }

      if (Forgver)                                                                     //  original (or 1st vers.) wanted
         passfiles[Npass2++] = xxrec_index(vlist[0]);                                  //  add to search resulta

      if (Flastver)                                                                    //  last version wanted
         passfiles[Npass2++] = xxrec_index(vlist[nv-1]);                               //  add to search results

      if (Fallvers) {                                                                  //  all versions wanted
         for (jj = 0; jj < nv; jj++)                                                   //  (excludes original and last)
            passfiles[Npass2++] = xxrec_index(vlist[jj]);                              //  add to search results
      }

      zfree(vlist);
   }

   Npass = Npass2;                                                                     //  new search results count
   HeapSort(passfiles,Npass);                                                          //  sort passfiles

   for (ii = jj = 0; ii < Npass; ii++) {                                               //  eliminate duplicates
      if (passfiles[ii] == passfiles[jj]) continue;
      passfiles[++jj] = passfiles[ii];
   }
   Npass = jj + 1;                                                                     //  new passfiles count

retx:

   zfree(flist);                                                                       //  free flist
   return;
}


//  Report selected files using a gallery window layout
//  with image thumbnails and selected metadata text.

int search_metadata_report()
{
   using namespace search_images;
   using namespace navi;

   int      ff, ii, jj, cc;
   ch       *file = 0, **repfiles = 0, **tagval = 0;
   ch       *pp;
   ch       psize2[20];
   float    fsize;
   ch       text1[2000], text2[400];                                                   //  note text1 limit
   xxrec_t  *xxrec;

   if (! Gfiles) {                                                                     //  curr. gallery files
      printf("metadata report, 0 files \n");
      return 0;
   }

   if (Nxtags)
   {
      cc = Gfiles * sizeof(ch *);                                                      //  make file list from curr. gallery
      repfiles = (ch **) zmalloc(cc,"search");

      for (ff = 0; ff < Gfiles; ff++)
         repfiles[ff] = gallery(0,"getR",ff);

      cc = Gfiles * Nxtags * sizeof(ch **);                                            //  allocate pointers for returned metadata
      tagval = (ch **) zmalloc(cc,"search");

      meta_getN(repfiles,Gfiles,srchtags,tagval,Nxtags,0);                             //  get Nxtags tagval per repfile
   }

   for (ff = 0; ff < Gfiles; ff++)                                                     //  scan all images in gallery
   {
      zmainloop();

      file = gallery(0,"getR",ff);
      if (! file) continue;

      xxrec = get_xxrec(file);                                                         //  get metadata available in index table
      if (! xxrec) continue;                                                           //  deleted, not an image file

      snprintf(text2,400,TX("Photo Date: %s File Date: %s \n"),                        //  25.1
                                    xxrec->pdate, xxrec->fdate);
      strcpy(text1,text2);
      cc = strlen(text1);

      strncpy0(psize2,xxrec->psize,20);                                                //  pixel size, 12345 6789
      pp = strchr(psize2,' ');
      if (pp && psize2 - pp < (int) strlen(psize2) - 2) *pp = 'x';                     //  12345x6789

      fsize = atof(xxrec->fsize)/MEGA;
      snprintf(text2,400,TX("Rating: %s Size: %s %.2fmb \n"),
                                 xxrec->rating, psize2, fsize);
      strcpy(text1+cc,text2);
      cc += strlen(text2);

      snprintf(text2,400,"Camera: %s %s %s\n",xxrec->make,xxrec->model,xxrec->lens);
      strcpy(text1+cc,text2);
      cc += strlen(text2);

      snprintf(text2,400,"Keywords: %s\n",xxrec->keywords);
      strcpy(text1+cc,text2);
      cc += strlen(text2);

      snprintf(text2,400,"Location: %s %s\n",xxrec->location,xxrec->country);
      strcpy(text1+cc,text2);
      cc += strlen(text2);

      snprintf(text2,400,TX("Title: %s\n"),xxrec->title);
      strcpy(text1+cc,text2);
      cc += strlen(text2);

      snprintf(text2,400,TX("Description: %s\n"),xxrec->desc);
      strcpy(text1+cc,text2);
      cc += strlen(text2);

      if (Gindex[ff].mdata1) zfree(Gindex[ff].mdata1);                                 //  standard metadata report text
      Gindex[ff].mdata1 = zstrdup(text1,"search");

      if (Gindex[ff].mdata2) zfree(Gindex[ff].mdata2);                                 //  clear user selected metadata
      Gindex[ff].mdata2 = 0;

      if (Nxtags)                                                                      //  get user selected metadata to report
      {
         ii = ff * Nxtags;                                                             //  metadata values for this file

         for (cc = jj = 0; jj < Nxtags; jj++, ii++)
         {
            if (strmatch(srchtags[jj],"FileName"))                                     //  meta_getN() only reports seq. no.     26.0
               snprintf(text2,400,TX("File Name: %s \n"),file);
            else snprintf(text2,400,"%s:  %s \n",srchtags[jj], tagval[ii]);
            if (cc + strlen(text2) > 1999) break;
            strcpy(text1+cc,text2);
            cc += strlen(text2);
         }

         Gindex[ff].mdata2 = zstrdup(text1,"search");                                  //  user selected metadata report text
      }
   }

   Gmdrows = 6 + Nxtags;                                                               //  report rows

   if (Nxtags)
   {
      zfree(repfiles);

      for (ii = 0; ii < Gfiles * Nxtags; ii++)                                         //  free tagval memory
         if (tagval[ii]) zfree(tagval[ii]);
      zfree(tagval);
   }

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


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

//  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 checkDT(ch *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 < 10) return 0;

   if (datetime[4] != ':') return 0;                                                   //  check yyyy:mm:dd
   if (datetime[7] != ':') return 0;
   year = atoi(datetime);
   mon = atoi(datetime+5);
   day = atoi(datetime+8);

   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 (cc == 10) return 1;                                                             //  year only is present
   if (cc == 11 && datetime[10] == ' ') {                                              //  allow trailing blank, remove it
      datetime[10] = 0;
      return 1;
   }

   if (datetime[10] != ' ') return 0;                                                  //  1 blank between date and time
   if (datetime[13] != ':') return 0;                                                  //  check hh:mm
   if (cc == 19 && datetime[16] != ':') return 0;                                      //  or hh:mm:ss

   hour = atoi(datetime+11);
   min = atoi(datetime+14);
   if (cc == 19) sec = atoi(datetime+17);
   else sec = 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 keyword to output keyword list if not already there and enough room
//  returns:   0 = added OK     1 = already there (case ignored)
//             2 = overflow     3 = bad keyword name     4 = null/blank keyword

int add_keyword(ch *keyword, ch *keywordlist, int maxcc)
{
   ch       *pp1, *pp2, keyword1[keywordXcc];
   int      cc, cc1, cc2;

   if (! keyword || ! *keyword) return 4;
   strncpy0(keyword1,keyword,keywordXcc);                                              //  remove leading and trailing blanks
   cc = strTrim2(keyword1);
   if (! cc) return 4;
   if (utf8_check(keyword1)) return 3;                                                 //  look for bad characters
   if (strpbrk(keyword1,",;:")) return 3;
   strcpy(keyword,keyword1);

   pp1 = keywordlist;
   cc1 = strlen(keyword);

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

   cc1 = strlen(keywordlist);
   cc2 = strlen(keyword);

   while (cc1 > 0 && keywordlist[cc1-1] == ' ') cc1--;                                 //  remove ',' and ' ' from end if present
   while (cc1 > 0 && keywordlist[cc1-1] == ',') cc1--;                                 //    (prior fotocx quirk)

   if (cc1 + cc2 + 3 > maxcc) return 2;                                                //  no room for ", " + new keyword

   if (cc1 > 0) {
      strcpy(keywordlist + cc1,", ");                                                  //  oldkeyword, oldkeyword, newkeyword
      cc1 += 2;
   }
   strcpy(keywordlist + cc1, keyword);

   return 0;
}


//  remove keyword from keywordlist, if present
//  returns: 0 if found and deleted, otherwise 1

int del_keyword(ch *keyword, ch *keywordlist)
{
   int         ii, cc, ftcc, atcc, found;
   ch          *tempkeywords;
   ch          *pp;

   tempkeywords = zstrdup(keywordlist,"delete-keyword");

   *keywordlist = 0;
   ftcc = found = 0;

   for (ii = 1; ; ii++)
   {
      pp = substring(tempkeywords,",;",ii);                                            //  next keyword
      if (! pp) break;

      if (*pp == 0) continue;

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

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

   cc = strlen(keywordlist);
   while(cc > 0 && keywordlist[cc-1] == ' ') cc--;                                     //  remove trailing ", " if present
   while(cc > 0 && keywordlist[cc-1] == ',') cc--;
   keywordlist[cc] = 0;

   zfree(tempkeywords);
   return 1-found;
}


//  add new keyword to recent keywords, if not already.
//  remove oldest to make space if needed.

int add_recenkeyword(ch *keyword)
{
   int         err;
   ch          *pp, tempkeywords[recentkeywordsXcc];

   err = add_keyword(keyword,recent_keywords,recentkeywordsXcc);                       //  add keyword to recent keywords

   while (err == 2)                                                                    //  overflow
   {
      strncpy0(tempkeywords,recent_keywords,recentkeywordsXcc);                        //  remove oldest to make room
      pp = strpbrk(tempkeywords,",;");
      if (! pp) return 0;
      strcpy(recent_keywords,pp+2);                                                    //  delimiter + blank before keyword
      err = add_keyword(keyword,recent_keywords,recentkeywordsXcc);
   }

   return 0;
}


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

//  Load keywords_defined file into defined_keywords[ii] => category: keyword1, keyword2, ...
//  Read image_index recs. and add unmatched keywords: => nocatg: keyword1, keyword2, ...
//  force: read image index and build defkeywords list

void load_defkeywords(int force)
{
   static int  Floaded = 0;
   FILE *      fid;
   xxrec_t     *xxrec;
   int         ii, jj, nkeywords, err, cc, tcc;
   int         ncats, catoverflow;
   int         nocat, nocatcc;
   ch          keyword[keywordXcc], catg[keywordXcc];
   ch          keywordsbuff[catgkeywordsXcc];
   ch          *pp1, *pp2;
   ch          pkeywords[maxkeywords][keywordXcc];                                     //  10000 * 50 = 0.5 MB

   if (Floaded && ! force) return;                                                     //  use memory keywords if already there
   Floaded++;

   for (ii = 0; ii < maxkeywordcats; ii++) {                                           //  clean memory
      if (defined_keywords[ii]) zfree(defined_keywords[ii]);
      defined_keywords[ii] = 0;
   }

   ncats = catoverflow = 0;

   fid = fopen(keywords_defined_file,"r");                                             //  read keywords_defined file
   if (fid) {
      while (true) {
         pp1 = fgets_trim(keywordsbuff,catgkeywordsXcc,fid);
         if (! pp1) break;
         pp2 = strchr(pp1,':');                                                        //  isolate "category:"
         if (! pp2) continue;                                                          //  no colon
         cc = pp2 - pp1 + 1;
         if (cc > keywordXcc-1) continue;                                              //  category name too long
         strncpy0(catg,pp1,cc);                                                        //  (for error message)
         if (strlen(pp1) > catgkeywordsXcc-2) goto cattoobig;                          //  all category keywords too long
         pp2++;
         while (*pp2 == ' ') pp2++;
         while (*pp2) {
            if (*pp2 == ';') *pp2 = ',';                                               //  replace ';' with ',' for Fotocx
            pp2++;
         }
         defined_keywords[ncats] = zstrdup(pp1,"load-defkeywords");                    //  defined_keywords[ii]
         ncats++;                                                                      //   = category: kword1, kword2, ... kwordN,
         if (ncats == maxkeywordcats) goto toomanycats;
      }
      err = fclose(fid);
      fid = 0;
      if (err) goto defkeywordsfilerr;
   }

//  sort the categories in ascending order

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

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

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

//  if not already there, add category "nocatg" to the end of the list

   pp1 = 0;
   if (ncats > 0) pp1 = defined_keywords[ncats-1];                                     //  last keyword category
   if (pp1 && strmatchN(pp1,"nocatg:",7)) {                                            //  already 'nocatg'
      nocat = ncats - 1;
      nocatcc = strlen(pp1);
      pp2 = (ch *) zmalloc(catgkeywordsXcc,"load-defkeywords");                        //  re-allocate max. size
      defined_keywords[nocat] = pp2;                                                   //    for following phase
      strcpy(pp2,pp1);
      zfree(pp1);
   }
   else {
      nocat = ncats;                                                                   //  add to end of list
      ncats++;
      defined_keywords[nocat] = (ch *) zmalloc(catgkeywordsXcc,"load-defkeywords");    //  allocate max. size
      strcpy(defined_keywords[nocat],"nocatg: ");
      nocatcc = 8;
   }

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

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

      xxrec = xxrec_tab[ii];

      pp1 = xxrec->keywords;

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

         strncpy0(keyword,pp1,cc+1);                                                   //  look for keyword in defined keywords
         if (find_defkeyword(keyword)) {
            pp1 = pp2;                                                                 //  found
            continue;
         }

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

         pp1 = pp2;
      }
   }

   if (catoverflow) goto cattoobig;

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

   for (ii = 0; ii < ncats; ii++)
   {
      pp1 = defined_keywords[ii];
      pp2 = strchr(pp1,':');
      if (! pp2) {
         printf("*** defined keywords 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 < maxkeywords; jj++)
      {
         if (! *pp1) break;
         pp2 = strchr(pp1,',');
         if (pp2) cc = pp2 - pp1;
         else cc = strlen(pp1);
         if (cc > keywordXcc-1) cc = keywordXcc-1;
         strncpy0(pkeywords[jj],pp1,cc+1);
         pp1 += cc + 1;
         tcc += cc;
         while (*pp1 == ' ') pp1++;
      }

      nkeywords = jj;
      if (nkeywords == maxkeywords) goto toomanykeywords;
      HeapSort((ch *) pkeywords,keywordXcc,nkeywords,zstrcasecmp);

      pp1 = defined_keywords[ii];
      tcc += strlen(catg) + 2 + 2 * nkeywords + 2;                                     //  category, all keywords, delimiters
      pp2 = (ch *) zmalloc(tcc,"load-defkeywords");

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

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

      for (jj = 0; jj < nkeywords; jj++)                                               //  add the sorted keywords
      {
         strcpy(pp2,pkeywords[jj]);                                                    //  append keyword + delim + blank
         pp2 += strlen(pp2);
         strcpy(pp2,", ");
         pp2 += 2;
      }

      *pp2 = 0;
   }

   return;

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

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

toomanykeywords:
   zmessageACK(Mwin,TX("category %s has too many keywords"),catg);
   if (fid) fclose(fid);
   return;

defkeywordsfilerr:
   zmessageACK(Mwin,TX("keywords_defined file error: %s"),strerror(errno));
   return;
}


//  write defined_keywords[] memory data to the defined keywords file if any changes were made

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

   printf("update defined keywords \n");

   fid = fopen(keywords_defined_file,"w");                                             //  write keywords_defined file
   if (! fid) goto defkeywordserr;

   for (ii = 0; ii < maxkeywordcats; ii++)
   {
      if (! defined_keywords[ii]) break;
      err = fprintf(fid,"%s\n",defined_keywords[ii]);                                  //  each record:
      if (err < 0) goto defkeywordserr;                                                //    category: kword1, kword2, ... kwordN,
   }

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

defkeywordserr:
   zmessageACK(Mwin,TX("keywords_defined file error: %s"),strerror(errno));
   return;
}


//  find a given keyword in defined_keywords[]
//  return: 1 = found, 0 = not found

int find_defkeyword(ch *keyword)
{
   int      ii, cc;
   ch       keyword2[keywordXcc+4];
   ch       *pp;

   strncpy0(keyword2,keyword,keywordXcc);                                              //  construct keyword + delim + blank
   cc = strlen(keyword2);
   strcpy(keyword2+cc,", ");
   cc += 2;

   for (ii = 0; ii < maxkeywordcats; ii++)
   {
      pp = defined_keywords[ii];                                                       //  category: kword1, kword2, ... kwordN,
      if (! pp) return 0;                                                              //  not found

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

   return 1;
}


//  add new keyword to defined_keywords[] >> category: keyword1, keyword2, ... newkeyword,
//  returns:   0 = added OK     1 = not unique (case ignored)
//             2 = overflow     3 = bad name     4 = null/blank keyword
//  if keyword present under another category, it is moved to new category

int add_defkeyword(ch *catg, ch *keyword)
{
   int         ii, cc, cc1, cc2;
   ch          catg1[keywordXcc], keyword1[keywordXcc];
   ch          *pp1, *pp2;

   if (! catg) strcpy(catg1,"nocatg");
   else strncpy0(catg1,catg,keywordXcc);
   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 (! keyword) return 4;
   strncpy0(keyword1,keyword,keywordXcc);                                              //  remove leading and trailing blanks
   cc = strTrim2(keyword1);
   if (! cc) return 4;
   if (utf8_check(keyword1)) goto badkeywordname;                                      //  look for bad characters
   if (strpbrk(keyword1,",;:\"")) goto badkeywordname;

   del_defkeyword(keyword1);                                                           //  delete keyword if already there

   cc1 = strlen(catg1);

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

newcatg:
   if (ii == maxkeywordcats) goto toomanycats;
   cc1 = strlen(catg1) + strlen(keyword1) + 6;
   pp1 = (ch *) zmalloc(cc1,"add-defkeyword");
   *pp1 = 0;
   strncatv(pp1,cc1,catg1,": ",keyword1,", ",null);                                    //  category: + keyword + delim + blank
   defined_keywords[ii] = defined_keywords[ii-1];                                      //  move "nocatg" record to next slot
   defined_keywords[ii-1] = pp1;                                                       //  insert new record before
   save_defkeywords();
   return 0;

oldcatg:                                                                               //  logic simplified
   pp2 = pp1 + cc1 + 2;                                                                //  ch following "catname: "
   cc1 = strlen(keyword1);
   cc2 = strlen(pp1);                                                                  //  add new keyword to old record
   if (cc1 + cc2 + 4 > catgkeywordsXcc) goto cattoobig;
   pp2 = zstrdup(pp1,"add-defkeyword",cc1+cc2+4);                                      //  expand string
   zfree(pp1);
   defined_keywords[ii] = pp2;
   strcpy(pp2+cc2,keyword1);                                                           //  old record + keyword + delim + blank
   strcpy(pp2+cc2+cc1,", ");
   save_defkeywords();
   return 0;

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

badkeywordname:
   zmessageACK(Mwin,TX("bad keyword name"));
   return 3;

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

cattoobig:
   zmessageACK(Mwin,TX("too many keywords in a category"));
   return 2;
}


//  delete keyword from defined keywords list, defined_keywords[]
//  return: 0 = found and deleted, 1 = not found

int del_defkeyword(ch *keyword)
{
   int      ii, cc;
   ch       keyword2[keywordXcc+4];
   ch       *pp, *pp1, *pp2;

   if (! keyword || ! *keyword || *keyword == ' ') return 1;                           //  bad keyword (utf8 can be < ' ')

   strncpy0(keyword2,keyword,keywordXcc);                                              //  construct keyword + delim + blank
   cc = strlen(keyword2);
   strcpy(keyword2+cc,", ");
   cc += 2;

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

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

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

   return 0;
}


//  delete category from defined keywords list, defined_keywords[]
//  return: 0 = found and deleted, 1 = not found
//          2 = not deleted because category has keywords assigned

int del_defcatg(ch *catg)
{
   int      ii, jj, cc;
   ch       catg2[keywordXcc+2];
   ch       *pp;

   if (! catg || ! *catg || *catg == ' ') return 1;                                    //  bad catg (utf8 can be < ' ')

   strncpy0(catg2,catg,keywordXcc);                                                    //  construct "catgname:"
   cc = strlen(catg2);
   strcpy(catg2+cc,":");
   cc += 1;

   for (ii = 0; ii < maxkeywordcats; ii++)
   {
      pp = defined_keywords[ii];
      if (! pp) return 1;                                                              //  catg not found
      if (strmatchN(pp,catg2,cc)) break;
   }

   for (jj = cc; pp[jj]; jj++)                                                         //  check nothing following ':'
      if (pp[jj] != ' ') return 2;                                                     //  catg not empty

   zfree(pp);                                                                          //  delete table entry

   for (jj = ii; jj < maxkeywordcats-1; jj++)                                          //  close hole in table
      defined_keywords[jj] = defined_keywords[jj+1];

   return 0;                                                                           //  found and deleted
}


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

void defkeywords_stuff(zdialog *zd, ch *acatg)
{
   GtkWidget      *widget;
   int            ii, ff, cc;
   ch             catgname[keywordXcc+4];
   ch             *pp1, *pp2;

   widget = zdialog_gtkwidget(zd,"defkeywords");
   txwidget_clear(widget);

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

      if (! strmatch(acatg,"ALL")) {
         ff = strmatch(catgname,acatg);
         if (! ff) continue;
      }

      strcat(catgname,": ");
      txwidget_append(widget,1,catgname);                                              //  "category: " in bold text

      pp2++;
      if (*pp2 == ' ') pp2++;
      if (*pp2) txwidget_append(widget,0,pp2);                                         //  "cat1, cat2, ... catN,"
      txwidget_append(widget,0,"\n");
   }

   return;
}


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

void defcats_stuff(zdialog *zd)
{
   ch       catgname[keywordXcc+2];
   int      ii, cc;
   ch       *pp1, *pp2;

   zdialog_combo_clear(zd,"defcats");
   zdialog_stuff(zd,"defcats","ALL");

   for (ii = 0; ii < maxkeywordcats; ii++)
   {
      pp1 = defined_keywords[ii];
      if (! pp1) break;
      pp2 = strchr(pp1,':');
      if (! pp2) continue;
      cc = pp2 - pp1;
      if (cc < 1) continue;
      if (cc > keywordXcc) continue;
      strncpy0(catgname,pp1,cc+1);
      zdialog_stuff(zd,"defcats",catgname);
   }

   zdialog_stuff(zd,"defcats","ALL");                                                  //  default selection

   return;
}


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

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

int glocs_compare(ch *rec1, ch *rec2)
{
   float    diff;
   int      ii;

   glocs_t *r1 = (glocs_t *) rec1;
   glocs_t *r2 = (glocs_t *) rec2;

   ii = strcmp(r1->country,r2->country);
   if (ii) return ii;

   ii = strcmp(r1->location,r2->location);
   if (ii) return ii;

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


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

//  Load image geolocations data into memory from image index table.
//  Returns no. geolocations loaded.

int load_imagelocs()
{
   ch       location[40], country[40];
   float    flati, flongi;
   int      cc, ii, jj;
   xxrec_t  *xxrec;

   if (Xindexlev < 1) {                                                                //  25.1
      printf("*** load_imagelocs: no image index \n");
      return 0;
   }

   if (Nimagelocs) return Nimagelocs;                                                  //  already done

   cc = (Nxxrec+1) * sizeof(glocs_t *);                                                //  get memory for geolocs table
   imagelocs = (glocs_t **) zmalloc(cc,"load_imagelocs");                              //  room for Nxxrec entries

   Nimagelocs = 0;

   //  populate imagelocs from image index table

   for (ii = 0; ii < Nxxrec; ii++)                                                     //  loop all index recs
   {
      xxrec = xxrec_tab[ii];

      strncpy0(location,xxrec->location,40);
      strncpy0(country,xxrec->country,40);
      get_gps_data(xxrec->gps_data,flati,flongi);

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

      jj = Nimagelocs++;                                                               //  fill next entry in table
      imagelocs[jj] = (glocs_t *) zmalloc(sizeof(glocs_t),"load_imagelocs");
      imagelocs[jj]->location = zstrdup(location,"load_imagelocs");
      imagelocs[jj]->country = zstrdup(country,"load_imagelocsC");
      imagelocs[jj]->flati = flati;
      imagelocs[jj]->flongi = flongi;
   }

   if (Nimagelocs > 1)
      HeapSort((ch **) imagelocs, Nimagelocs, glocs_compare);                          //  sort

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

   Nimagelocs = ii + 1;                                                                //  final geolocs table size
   printf("total image geolocations: %d \n",Nimagelocs);

/***
      for (ii = 0; ii < Nimagelocs; ii++) {
         printf("%s %s %.4f %.4f \n",
                  imagelocs[ii]->location, imagelocs[ii]->country,
                  imagelocs[ii]->flati, imagelocs[ii]->flongi);
      }
***/

   return Nimagelocs;
}


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

//  load cities geolocations table into memory
//  worldcities file must be sorted by location, country

int load_worldlocs()
{
   ch       worldcitiesfile[200], wcfbuff[200];
   ch       location[40], country[40];
   ch       plocation[40], pcountry[40];
   float    flati, flongi;
   ch       *pp;
   int      nwl, cc;
   FILE     *fid;

   if (Xindexlev < 1) {                                                                //  25.1
      printf("*** load_worldlocs: no image index \n");
      return 0;
   }

   if (Nworldlocs) return Nworldlocs;                                                  //  already done

   cc = (maxworldcities) * sizeof(glocs_t *);                                          //  memory for geolocs table
   worldlocs = (glocs_t **) zmalloc(cc,"load_worldlocs");

   Nworldlocs = 0;

   snprintf(worldcitiesfile,200,"%s/worldcities.txt",get_zdatadir());
   fid = fopen(worldcitiesfile,"r");
   if (! fid) {
      printf("*** worldcities.txt file missing \n");
      goto retx;
   }

   nwl = 0;
   *plocation = *pcountry = '?';

   while (true)
   {
      pp = fgets(wcfbuff,200,fid);                                                     //  read location, country, lati, longi
      if (! pp) break;

      pp = substring(wcfbuff,',',1);
      if (! pp) continue;
      strncpy0(location,pp,40);

      pp = substring(wcfbuff,',',2);
      if (! pp) continue;
      strncpy0(country,pp,40);

      strcpy(plocation,location);
      strcpy(pcountry,country);

      pp = substring(wcfbuff,',',3);
      if (! pp) continue;
      flati = atof(pp);
      if (! flati || flati < -90 || flati > 90) continue;

      pp = substring(wcfbuff,',',4);
      if (! pp) continue;
      flongi = atof(pp);
      if (! flongi || flongi < -180 || flongi > 180) continue;

      worldlocs[nwl] = (glocs_t *) zmalloc(sizeof(glocs_t),"load_worldlocs");
      worldlocs[nwl]->location = zstrdup(location,"load_worldlocs");
      worldlocs[nwl]->country = zstrdup(country,"load_worldlocs");
      worldlocs[nwl]->flati = flati;
      worldlocs[nwl]->flongi = flongi;

      nwl++;
      if (nwl == maxworldcities) break;
   }

   fclose(fid);
   Nworldlocs = nwl;

retx:
   printf("total world locations: %d \n",Nworldlocs);
   return Nworldlocs;
}


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

   Find a geolocation from partial zdialog inputs and user choice of options.
   Uses locations and geocoordinates from image files and world locations table.
   Input zdialog widgets: location, country, gps_data
   Location and country may be partial leading strings
   All three widgets are outputs (found location, country, geocoordinates)
   return: 1 = zdialog updated, 0 = no updates made

********/

int find_location(zdialog *zd)                                                         //  combined imiage and world search      26.0
{
   int      nn, cc, ii;
   int      Npick, zoomlev;
   ch       location[40], country[40], gps_data[24]; 
   ch       text[100];
   ch       *pp, *choice;
   zlist_t  *picklist;
   float    flati1 = 999, flati2 = -999;
   float    flongi1 = 999, flongi2 = -999;
   float    flatic, flongic, kmrange, fmpp;
   
   nn = load_imagelocs();                                                              //  load image and world locations tables
   nn += load_worldlocs();                                                             //  (if not already)
   if (! nn) return 0;

   zdialog_fetch(zd,"location",location,40);                                           //  get zdialog inputs
   zdialog_fetch(zd,"country",country,40);
   strTrim2(location);
   strTrim2(country);

   if (! *location && ! *country) return 0;                                            //  one of these must be present

   picklist = zlist_new(20);                                                           //  room for 20 location matches
   Npick = 0;
   
   for (ii = 0; ii < Nimagelocs; ii++)                                                 //  search all image files 
   {                                                                                   //    for partial location match
      if (*location) {
         cc = strlen(location);
         if (! strmatchcaseN(location,imagelocs[ii]->location,cc)) continue;
      }
      if (*country) {
         cc = strlen(country);
         if (! strmatchcaseN(country,imagelocs[ii]->country,cc)) continue;
      }

      snprintf(text,100,"%s | %s",imagelocs[ii]->location,imagelocs[ii]->country);     //  potential picklist entry
      if (zlist_find(picklist,text,0) >= 0) continue;                                  //  reject duplicate
      
      zlist_put(picklist,text,Npick++);                                                //  new picklist entry

      if (Npick == 20) {                                                               //  if 20 or more, give up
         zmessageACK(Mwin,TX("more than 20 matches"));
         zlist_free(picklist);
         return 0;
      }
   }

   for (ii = 0; ii < Nworldlocs; ii++)                                                 //  search world locations table
   {                                                                                   //    for partial location match
      if (*location) {
         cc = strlen(location);
         if (! strmatchcaseN(location,worldlocs[ii]->location,cc)) continue;
      }
      if (*country) {
         cc = strlen(country);
         if (! strmatchcaseN(country,worldlocs[ii]->country,cc)) continue;
      }

      snprintf(text,100,"%s | %s",worldlocs[ii]->location,worldlocs[ii]->country);     //  potential picklist entry
      if (zlist_find(picklist,text,0) >= 0) continue;                                  //  reject duplicate

      zlist_put(picklist,text,Npick++);                                                //  new picklist entry

      if (Npick == 20) {                                                               //  if 20 or more, give up
         zmessageACK(Mwin,TX("more than 20 matches"));
         zlist_free(picklist);
         return 0;
      }
   }

   if (Npick == 0) {                                                                   //  no matches found
      zlist_free(picklist);
      return 0;
   }

   choice = popup_choose(picklist);                                                    //  show picklist, choose
   zlist_free(picklist);
   if (! choice) return 0;                                                             //  no choice

   pp = substring(choice,'|',1);
   if (pp) strncpy0(location,pp,40);                                                   //  user choice, location and country
   pp = substring(choice,'|',2);
   if (pp) strncpy0(country,pp,40);
   strTrim2(location);
   strTrim2(country);

   zdialog_stuff(zd,"location",location);                                              //  return location data to zdialog
   zdialog_stuff(zd,"country",country);

   for (ii = 0; ii < Nimagelocs; ii++)                                                 //  search image files for
   {                                                                                   //    location and country
      if (strmatchcase(location,imagelocs[ii]->location) &&
          strmatchcase(country,imagelocs[ii]->country))
      {
         if (imagelocs[ii]->flati == 0 && imagelocs[ii]->flongi == 0) continue;        //  ignore missing values
         if (imagelocs[ii]->flati < flati1) flati1 = imagelocs[ii]->flati;             //  save range of geocoordinates found
         if (imagelocs[ii]->flati > flati2) flati2 = imagelocs[ii]->flati;
         if (imagelocs[ii]->flongi < flongi1) flongi1 = imagelocs[ii]->flongi;
         if (imagelocs[ii]->flongi > flongi2) flongi2 = imagelocs[ii]->flongi;
      }
   }

   for (ii = 0; ii < Nworldlocs; ii++)                                                 //  search world locations table
   {                                                                                   //    for location and country
      if (strmatchcase(location,worldlocs[ii]->location) &&
          strmatchcase(country,worldlocs[ii]->country))
      {
         if (worldlocs[ii]->flati < flati1) flati1 = worldlocs[ii]->flati;             //  save range of geocoordinates found
         if (worldlocs[ii]->flati > flati2) flati2 = worldlocs[ii]->flati;
         if (worldlocs[ii]->flongi < flongi1) flongi1 = worldlocs[ii]->flongi;
         if (worldlocs[ii]->flongi > flongi2) flongi2 = worldlocs[ii]->flongi;
         break;
      }
   }

   if (flati1 == 999) {                                                                //  no match, return "" geocoordinates
      zdialog_stuff(zd,"gps_data","");
      return 1;
   }

   if (flati1 == flati2 && flongi1 == flongi2) {                                       //  one match, return geocoordinates
      snprintf(gps_data,24,"%.4f %.4f",flati1,flongi1);
      zdialog_stuff(zd,"gps_data",gps_data);
      return 1;
   }

   flatic = 0.5 * (flati1 + flati2);                                                   //  multiple matches
   flongic = 0.5 * (flongi1 + flongi2);                                                //  center of enclosing rectangle

   kmrange = 100;                                                                      //  show 100km area                       25.3

   viewmode('M');                                                                      //  show world map
   
   for (zoomlev = 12; zoomlev < 20; zoomlev++)                                         //  loop small to large scale
   {
      fmpp = mapscale(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
   }

   map_zoomto(flatic,flongic,zoomlev);                                                 //  map click --> zdialog geocoordinates
   zd_mapgeotags = zd;                                                                 //  map clicks active

   zmessage_post(zd->dialog,"parent",3,"choose from mutiple locations\n"               //  26.0
                                       "(or set a new location)");
   return 1;
}


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

//  Find a location/country name matching a (partial) location and country name.
//  If more than 1 match found, present a picklist for the user to choose.
//  Picklist: matching location and country names, geocoordinates, image counts.
//  Returned: location, country, latitude, longitude
//  Returned status: 0 = OK, 1 = no match or no choice, 2 = too many matches

int choose_location(ch *location, ch *country, float &flati, float &flongi)
{
   int      ii, nn, cc;
   int      Nmatch;
   ch       *mloc[20], *mcon[20];                                                      //  matching locations
   float    mflati[20], mflongi[20];                                                   //  matching geocoordinates
   ch       Fimage[20];                                                                //  images present '*' or blank
   ch       *choice, text[200];
   ch       *pp;
   zlist_t  *picklist;

   nn = load_imagelocs();
   nn += load_worldlocs();
   if (! nn) {
      printf("*** no location data is available \n");
      return 1;
   }

   if (! *location && ! *country) {
      printf("*** no location name given \n");
      return 1;
   }

   Nmatch = 0;                                                                         //  match count

   for (ii = 0; ii < Nimagelocs; ii++)                                                 //  search image locations table
   {
      if (*location) {
         cc = strlen(location);
         if (! strmatchcaseN(location,imagelocs[ii]->location,cc)) continue;
      }
      if (*country) {
         cc = strlen(country);
         if (! strmatchcaseN(country,imagelocs[ii]->country,cc)) continue;
      }

      mloc[Nmatch] = imagelocs[ii]->location;                                          //  save match
      mcon[Nmatch] = imagelocs[ii]->country;
      mflati[Nmatch] = imagelocs[ii]->flati;
      mflongi[Nmatch] = imagelocs[ii]->flongi;
      Fimage[Nmatch] = '*';                                                            //  images present at location

      Nmatch++;                                                                        //  count matches
      if (Nmatch == 10) break;                                                         //  limit 10 locations with images
   }

   for (ii = 0; ii < Nworldlocs; ii++)                                                 //  search world locations table
   {
      if (*location) {
         cc = strlen(location);
         if (! strmatchcaseN(location,worldlocs[ii]->location,cc)) continue;
      }
      if (*country) {
         cc = strlen(country);
         if (! strmatchcaseN(country,worldlocs[ii]->country,cc)) continue;
      }

      mloc[Nmatch] = worldlocs[ii]->location;                                          //  save match
      mcon[Nmatch] = worldlocs[ii]->country;
      mflati[Nmatch] = worldlocs[ii]->flati;
      mflongi[Nmatch] = worldlocs[ii]->flongi;
      Fimage[Nmatch] = ' ';                                                            //  no images at location

      Nmatch++;                                                                        //  count matches
      if (Nmatch == 20) break;
   }

   if (! Nmatch) {
      zmessageACK(Mwin,TX("no matching locations"));
      return 1;
   }

   picklist = zlist_new(Nmatch);

   for (ii = 0; ii < Nmatch; ii++) {                                                   //  build picklist of locations
      snprintf(text,200,"%s / %s | %.4f / %.4f  %c",
                  mloc[ii], mcon[ii], mflati[ii], mflongi[ii], Fimage[ii]);
      zlist_put(picklist,text,ii);
   }

   choice = popup_choose(picklist);                                                    //  show picklist, choose one
   if (choice) strncpy0(text,choice,200);                                              //  preserve from zfree()
   zlist_free(picklist);

   if (! choice) return 1;

   pp = substring(text,"/|*",1);                                                       //  parse returned text
   if (! pp) return 1;                                                                 //  location / country | NN.NNN / NN.NNN
   strncpy0(location,pp,40);

   pp = substring(text,"/|*",2);
   if (! pp) return 1;
   strncpy0(country,pp,40);

   pp = substring(text,"/|*",3);
   if (! pp) return 1;
   sscanf(pp,"%f",&flati);

   pp = substring(text,"/|*",4);
   if (! pp) return 1;
   sscanf(pp,"%f",&flongi);

   return 0;
}


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

//  Find nearest known location for input geocoordinates.
//  Return km to location and which table has closest location:
//     iim: image table index   iic: world cities table index
//     other index returned = -1
//  execution time is 2 ms on 4 GHz computer with 15K images and 43K world locations

float nearest_loc(float lati, float longi, int &iim, int &iic)
{
   float    km1, km2;

   km1 = 99999;
   iim = iic = -1;

   load_imagelocs();                                                                   //  load images geolocations table
   load_worldlocs();                                                                   //  load cities geolocations table

   for (int ii = 0; ii < Nimagelocs; ii++)                                             //  search image locations
   {
      if (imagelocs[ii]->location[0] <= ' ') continue;                                 //  ignore blank locations                26.0
      km2 = earth_distance(lati,longi,imagelocs[ii]->flati,imagelocs[ii]->flongi);
      if (km2 < km1) {
         km1 = km2;
         iim = ii;
      }
   }

   for (int ii = 0; ii < Nworldlocs; ii++)                                             //  search world cities locations
   {
      km2 = earth_distance(lati,longi,worldlocs[ii]->flati,worldlocs[ii]->flongi);
      if (km2 < km1) {
         km1 = km2;
         iic = ii;
      }
   }

   if (iic > -1) iim = -1;                                                             //  if world cities, unset image index
   return km1;
}


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

//  Update image locations table  imagelocs[*]
//
//  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_imagelocs(zdialog *zd)
{
   ch          location[40], country[40];
   ch          gps_data[24];
   float       flati, flongi;
   int         ii, err, cc, nn, found = 0;

   if (Xindexlev < 1) {
      index_rebuild(1,0);                                                              //  25.1
      if (Nxxrec == 0) {
         zmessageACK(Mwin,TX("image index required"));
         return 0;
      }
   }

   zdialog_fetch(zd,"location",location,40);                                           //  get location and geocoordinates
   zdialog_fetch(zd,"country",country,40);
   strTrim2(location);
   strTrim2(country);
   if (! *location || ! *country) return 0;                                            //  location not complete
   if (! *location) return 0;                                                          //  or ""
   if (! *country) return 0;

   *location = toupper(*location);                                                     //  capitalize location names
   *country = toupper(*country);
   zdialog_stuff(zd,"location",location);
   zdialog_stuff(zd,"country",country);

   zdialog_fetch(zd,"gps_data",gps_data,24);
   err = get_gps_data(gps_data,flati,flongi);
   if (err) {                                                                          //  1 = missing, 2 = bad
      zmessageACK(Mwin,TX("invalid GPS data: %s"),gps_data);
      return -1;
   }

   for (ii = 0; ii < Nimagelocs; ii++)                                                 //  search geotags for location
   {
      if (! strmatchcase(location,imagelocs[ii]->location)) continue;                  //  case-insensitive compare
      if (! strmatchcase(country,imagelocs[ii]->country)) continue;
      if (! strmatch(location,imagelocs[ii]->location)) {
         zfree(imagelocs[ii]->location);                                               //  revise capitalization
         imagelocs[ii]->location = zstrdup(location,"put-geolocs");
      }
      if (! strmatch(country,imagelocs[ii]->country)) {
         zfree(imagelocs[ii]->country);
         imagelocs[ii]->country = zstrdup(country,"put-geolocs");
      }
      if (flati == imagelocs[ii]->flati && flongi == imagelocs[ii]->flongi) found++;
   }

   if (found) return 1;

   glocs_t  *glocsA = (glocs_t *) zmalloc(sizeof(glocs_t),"put-geolocs");
   glocs_t  **glocsB;

   glocsA->location = zstrdup(location,"put-geolocs");                                 //  new geolocs record
   glocsA->country = zstrdup(country,"put-geolocs");
   glocsA->flati = flati;
   glocsA->flongi = flongi;

   cc = (Nimagelocs + 1) * sizeof(glocs_t *);
   glocsB = (glocs_t **) zmalloc(cc,"put-geolocs");

   for (ii = 0; ii < Nimagelocs; ii++) {                                               //  copy geolocs before new geoloc
      nn = glocs_compare((ch *) imagelocs[ii], (ch *) glocsA);
      if (nn > 0) break;
      glocsB[ii] = imagelocs[ii];
   }

   glocsB[ii] = glocsA;                                                                //  insert new geolocs

   for (NOP; ii < Nimagelocs; ii++)                                                    //  copy geolocs after new geoloc
      glocsB[ii+1] = imagelocs[ii];

   if (Nimagelocs) zfree(imagelocs);                                                   //  geolocs --> new table          bugfix 26.2
   imagelocs = glocsB;
   Nimagelocs += 1;

   return 2;
}


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

//  validate and convert earth coordinates, latitude and longitude
//  return: 0  OK
//          1  missing or blank
//          2  invalid data
//  if return status > 0, 0.0 is returned for both values
//  if conversion is valid, input string is returned minus excess blanks
//  if input string >23 char, reformat  ±nn.nnnn ±nnn.nnnn (max. 18 char. + null)
//  execution time is 0.2 microseconds on 4 GHz processor

int get_gps_data(ch *gps_data, float &flati, float &flongi)
{
   int      cc, err;
   ch       *pp, *pp1, *pp2, *pp3, *pp4;
   
   if (! gps_data || ! *gps_data) goto status1;

   cc = strlen(gps_data);
   if (cc < 7) goto status2;

   for (pp1 = gps_data; *pp1 == ' '; pp1++);                                           //  "  nn.nnnnn     nn.nnnnn  "
   if (! *pp1) goto status2;                                                           //     |       |    |       |
   for (pp2 = pp1+1; *pp2 > ' '; pp2++);                                               //     pp1     pp2  pp3     pp4
   if (*pp2 != ' ') goto status2;
   for (pp3 = pp2+1; *pp3 == ' '; pp3++);
   if (! *pp3) goto status2;
   for (pp4 = pp3+1; *pp4 > ' '; pp4++);
   if (pp2 - pp1 < 3) goto status2;
   if (pp4 - pp3 < 3) goto status2;

   pp = strchr(pp1,',');                                                               //  replace comma decimal point
   if (pp) *pp = '.';                                                                  //    with period
   pp = strchr(pp3,',');
   if (pp) *pp = '.';

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

   snprintf(gps_data,24,"%.4f %.4f",flati,flongi);                                     //  reduce to standard precision
   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;
   float       flati, flongi;
   xxrec_t     *xxrec;

   if (Xindexlev < 1) {
      index_rebuild(1,0);                                                              //  25.1
      if (Nxxrec == 0) {
         zmessageACK(Mwin,TX("image index required"));
         return 0;
      }
   }

   if (! navi::Gfiles) {
      zmessageACK(Mwin,TX("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::Gfiles * cc,"gallerymap");

   for (jj = 0, ii = navi::Gfolders; ii < navi::Gfiles; ii++)                          //  loop gallery files
   {
      xxrec = get_xxrec(navi::Gindex[ii].file);                                        //  look up in xxrec_tab
      if (! xxrec) continue;                                                           //  deleted, not an image file
      get_gps_data(xxrec->gps_data,flati,flongi);
      gallerymap[jj].flati = flati;
      gallerymap[jj].flongi = flongi;
      gallerymap[jj].file = zstrdup(navi::Gindex[ii].file,"gallerymap");
      jj++;
   }

   Ngallerymap = jj;
   return Ngallerymap;
}


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

//  internet map using libchamplain (M view)

namespace maps
{
   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;
// ch                          *map_source_chain = "mff-relief";
   ch                          *map_source_chain = "osm-mapnik";
}


void map_mousefunc(GtkWidget *, GdkEventButton *, void *);                             //  mouse click function for map view
void find_map_images(float flati, float flongi);                                       //  find images at clicked position


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

//  initialize for internet map

void m_worldmap(GtkWidget *, ch *menu)
{
   using namespace maps;

   if (menu) F1_help_topic = TX("WORLD MAP");

   printf("m_load_map \n");

   if (Xindexlev < 1) {
      index_rebuild(1,0);                                                              //  25.1
      if (Nxxrec == 0) {
         zmessageACK(Mwin,TX("image index required"));
         return;
      }
   }

   load_imagelocs();                                                                   //  load image geolocs[] data

   if (markerlayer) {                                                                  //  refresh map markers
      paint_map_markers();
      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
   gdk_window_set_cursor(gdkwin,0);                                                    //  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;

   champlain_view_set_min_zoom_level(mapview,3);
   map_factory = champlain_map_source_factory_dup_default();
   map_source = champlain_map_source_factory_create_cached_source(map_factory,map_source_chain);
   champlain_view_set_map_source(mapview,map_source);

   markerlayer = champlain_marker_layer_new_full(CHAMPLAIN_SELECTION_SINGLE);
   if (! markerlayer) goto fail;
   champlain_view_add_layer(mapview,CHAMPLAIN_LAYER(markerlayer));
   champlain_marker_layer_set_selection_mode(markerlayer,CHAMPLAIN_SELECTION_NONE);
   markercolor = clutter_color_new(255,0,0,255);

   gtk_widget_add_events(mapwidget,GDK_BUTTON_PRESS_MASK);                             //  connect mouse events to map
   G_SIGNAL(mapwidget,"button-press-event",map_mousefunc,0);
   G_SIGNAL(mapwidget,"button-release-event",map_mousefunc,0);
   G_SIGNAL(mapwidget,"motion-notify-event",map_mousefunc,0);

   paint_map_markers();                                                                //  paint map markers where images
   return;

fail:
   zmessageACK(Mwin,TX("libchamplain failure"));
   return;
}


//  paint markers corresponding to image locations on map

void paint_map_markers()
{
   using namespace maps;

   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 repetitions
         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 < Nimagelocs; ii++)                                          //  mark all images on map
      {
         flati = imagelocs[ii]->flati;
         flongi = imagelocs[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;
}


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

//  Save current map region (center and scale) with a given name,
//    or retrieve a previously saved map region.

namespace map_regions_names
{
   zdialog     *zdmapreg = 0;
   ch          regname[80];
   double      reglati = 0, reglongi = 0;
   int         regzoom = 12;
   ch          buff[100];
}


//  menu function

void m_map_regions(GtkWidget *, ch *)
{
   using namespace map_regions_names;

   int   map_regions_dialog_event(zdialog *zd, ch *event);
   int   map_regions_clickfunc(GtkWidget *, int line, int pos, ch *input);

   zdialog     *zd;
   GtkWidget   *mtext;
   ch          *pp;
   FILE        *fid;

   F1_help_topic = TX("MAP REGIONS");

   printf("m_map_regions \n");

   if (Xindexlev < 1) {
      index_rebuild(1,0);                                                              //  25.1
      if (Nxxrec == 0) {
         zmessageACK(Mwin,TX("image index required"));
         return;
      }
   }

   viewmode('M');

/***
       ________________________________
      |           Map regions          |
      | ______________________________ |
      ||                              ||
      || map region name 1            ||
      || long map region name 2       ||     scrolling window
      || map region name 3            ||
      ||  ...                         ||
      ||______________________________||
      |                                |
      | map region: [________________] |     text entry for region name
      |                                |
      |             [Add] [Delete] [X] |
      |________________________________|


      [region]       empty until filled-in or a region from the list is clicked
      [add]          current region is added to list or replaced
      [delete]       current region is deleted from list

      region position and scale is from current map location and scale
      region list is kept in alphabetic order

***/

   if (zdmapreg) return;                                                               //  already active

   zd = zdialog_new(TX("Map regions"),Mwin,TX("Add"),TX("Delete"),"X",null);
   zdmapreg = zd;
   zdialog_add_widget(zd,"scrwin","scrregs","dialog",0,"expand");
   zdialog_add_widget(zd,"text","mtext","scrregs");
   zdialog_add_widget(zd,"hbox","hbvn","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labvn","hbvn",TX("map region:"),"space=3");
   zdialog_add_widget(zd,"zentry","regname","hbvn","","space=3");

   zdialog_resize(zd,200,300);
   zdialog_run(zd,map_regions_dialog_event,"mouse");

   mtext = zdialog_gtkwidget(zd,"mtext");                                              //  map region list in dialog
   txwidget_clear(mtext);

   fid = fopen(map_regions_file,"r");                                                  //  map region list file
   if (fid) {
      while (true) {
         pp = fgets_trim(buff,100,fid,1);                                              //  read region | lati | longi | zoom
         if (! pp) break;
         pp = substring(buff,'|',1);                                                   //  isolate region
         if (! pp) continue;
         if (strlen(pp) < 2) continue;
         txwidget_append(mtext,0,"%s \n",pp);                                          //  write into dialog list
      }
      fclose(fid);
   }

   txwidget_set_eventfunc(mtext,map_regions_clickfunc);                                //  set mouse/KB event function
   return;
}


//  dialog event and completion callback function

int map_regions_dialog_event(zdialog *zd, ch *event)
{
   using namespace maps;
   using namespace map_regions_names;

   int         ii, cc;
   ch          *pp;
   GtkWidget   *mtext;
   FILE        *fidr;
   zlist_t     *ZLregs = 0;
   ch          regname2[100];

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

   if (zd->zstat == 1)                                                                 //  [add] new map region record
   {
      zdialog_fetch(zd,"regname",regname,80);
      if (strTrim2(regname) < 2) {
         zmessageACK(Mwin,TX("supply a reasonable name"));
         return 1;
      }

      reglati = champlain_view_get_center_latitude(mapview);                           //  get current map region
      reglongi = champlain_view_get_center_longitude(mapview);
      regzoom = champlain_view_get_zoom_level(mapview);

      snprintf(buff,100,"%s|%.4f|%.4f|%d",regname,reglati,reglongi,regzoom);           //  prepare new region rec.

      ZLregs = zlist_from_file(map_regions_file);                                      //  get region list

      strcpy(regname2,regname);                                                        //  get regname|
      strcat(regname2,"|");
      cc = strlen(regname2);

      for (ii = 0; ii < zlist_count(ZLregs); ii++)                                     //  remove matching name from region list
         if (strmatchcaseN(regname2,zlist_get(ZLregs,ii),cc))
            zlist_remove(ZLregs,ii);

      for (ii = 0; ii < zlist_count(ZLregs); ii++) {
         if (strcasecmp(regname2,zlist_get(ZLregs,ii)) < 0) {                          //  insert new region in sort order
            zlist_insert(ZLregs,buff,ii);
            break;
         }
      }

      if (ii == zlist_count(ZLregs))                                                   //  new region is last
         zlist_append(ZLregs,buff,0);

      zlist_to_file(ZLregs,map_regions_file);                                          //  replace file
      goto update_dialog;
   }

   if (zd->zstat == 2)                                                                 //  [delete] selected map region record
   {
      zdialog_fetch(zd,"regname",regname,80);

      ZLregs = zlist_from_file(map_regions_file);                                      //  get region list

      strcpy(regname2,regname);                                                        //  get regname|
      strcat(regname2,"|");
      cc = strlen(regname2);

      for (ii = 0; ii < zlist_count(ZLregs); ii++)                                     //  remove matching name from region list
         if (strmatchcaseN(regname2,zlist_get(ZLregs,ii),cc))
            zlist_remove(ZLregs,ii);

      zlist_to_file(ZLregs,map_regions_file);                                          //  replace file
      goto update_dialog;
   }

   zdialog_free(zd);                                                                   //  cancel
   zdmapreg = 0;
   return 1;

update_dialog:

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

   if (ZLregs) zlist_free(ZLregs);

   mtext = zdialog_gtkwidget(zd,"mtext");                                              //  map region name list in dialog
   txwidget_clear(mtext);                                                              //  clear list

   fidr = fopen(map_regions_file,"r");                                                 //  update dialog list from file
   if (! fidr) return 1;

   while (true) {
      pp = fgets_trim(buff,100,fidr,1);                                                //  read region | lati | longi | zoom
      if (! pp) break;
      pp = substring(buff,'|',1);                                                      //  isolate region
      if (! pp) continue;
      if (strlen(pp) < 2) continue;
      txwidget_append2(mtext,0,"%s \n",pp);                                            //  write into dialog list
   }
   fclose(fidr);

   return 1;
}


//  get clicked region name and set corresponding map region and zoom level

int map_regions_clickfunc(GtkWidget *widget, int line, int pos, ch *input)
{
   using namespace map_regions_names;

   ch       *pp1, *pp2;
   FILE     *fidr;
   zdialog  *zd = zdmapreg;

   if (! zd) return 1;

   if (*input == GDK_KEY_F1) {                                                         //  key F1 pressed, show help
      showz_docfile(Mwin,UGfile,F1_help_topic);
      return 1;
   }

   pp1 = txwidget_line(widget,line,1);                                                 //  get clicked line, highlight
   if (! pp1 || ! *pp1) return 1;
   txwidget_highlight_line(widget,line);

   strTrim2(regname,pp1);
   zdialog_stuff(zd,"regname",regname);

   fidr = fopen(map_regions_file,"r");                                                 //  open/read map regs file
   if (! fidr) {
      zmessageACK(Mwin,strerror(errno));
      return 1;
   }

   while (true)                                                                        //  read next region record
   {
      pp2 = fgets_trim(buff,100,fidr);
      if (! pp2) break;
      pp2 = substring(buff,'|',1);
      if (! pp2) continue;
      if (strmatch(regname,pp2)) break;                                                //  found matching record
   }

   fclose(fidr);
   if (! pp2 || ! strmatch(regname,pp2)) goto notfound;

   reglati = reglongi = regzoom = 0;

   pp1 = substring(buff,'|',2);                                                        //  get map region data from record
   if (! pp1) goto baddata;
   reglati = atofz(pp1);
   if (reglati <= -90 || reglati >= +90) goto baddata;

   pp1 = substring(buff,'|',3);
   if (! pp1) goto baddata;
   reglongi = atofz(pp1);
   if (reglongi <= -180 || reglongi >= +180) goto baddata;

   pp1 = substring(buff,'|',4);
   if (! pp1) goto baddata;
   regzoom = atoi(pp1);
   if (regzoom < 1 || regzoom > 20) goto baddata;

   map_zoomto(reglati,reglongi,regzoom);                                               //  set this map region
   return 1;

notfound:
   printf("*** map region not found: %s \n",regname);
   return 0;

baddata:
   printf("*** map region invalid: %s %.4f %.4f %d \n",
                  regname,reglati,reglongi,regzoom);
   return 0;
}


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

//  Input a (partial) location name, choose full name from picklist,
//  goto corresponding map location

void m_map_location(GtkWidget *, ch *)
{
   zdialog  *zd;
   int      zstat, err;
   ch       location[40], country[40];
   float    flati = 0, flongi = 0;

   F1_help_topic = TX("MAP LOCATION");

/***
          ________________________________
         |     Go to map location         |
         |                                |
         | enter (partial) location names |
         | location [___________________] |
         | country  [___________________] |
         |                                |
         |                       [OK] [X] |
         |________________________________|

***/

   zd = zdialog_new(TX("Go to map location"),Mwin,"OK","X",null);
   zdialog_add_widget(zd,"hbox","hbent","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","labent","hbent","enter (partial) location names","space=3");           //  26.0
   zdialog_add_widget(zd,"hbox","hbloc","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labloc","hbloc","location","space=3");
   zdialog_add_widget(zd,"zentry","location","hbloc",0,"space=3|expand");
   zdialog_add_widget(zd,"hbox","hbcon","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labcon","hbcon","country","space=3");
   zdialog_add_widget(zd,"zentry","country","hbcon",0,"space=3|expand");

   zdialog_resize(zd,300,0);
   zdialog_load_inputs(zd);
   zdialog_run(zd,0,"mouse");

   zstat = zdialog_wait(zd);                                                           //  get partial location input
   if (zstat != 1) {
      zdialog_free(zd);
      return;
   }

   zdialog_fetch(zd,"location",location,40);
   zdialog_fetch(zd,"country",country,40);
   zdialog_free(zd);

   err = choose_location(location,country,flati,flongi);                               //  present picklist, choose location     26.0
   if (err) return;

   map_zoomto(flati,flongi,11);                                                        //  goto map location                     26.0
   printf("location: %s %s  %.4f %.4f \n",location,country,flati,flongi);

   return;
}


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

//  choose to mark map locations for all images or current gallery only

void m_set_map_markers(GtkWidget *, ch *)
{
   zdialog        *zd;
   int            zstat, showall = 0;

   F1_help_topic = TX("MAP MARKERS");

   printf("m_map_markers \n");

   if (Xindexlev < 1) {
      index_rebuild(1,0);                                                              //  25.1
      if (Nxxrec == 0) {
         zmessageACK(Mwin,TX("image index required"));
         return;
      }
   }

   viewmode('M');                                                                      //  set view mode M

/***
          _____________________________
         |      Set Map Markers        |
         |                             |
         | (o) mark all image files    |
         | (o) mark current gallery    |
         |                             |
         |                 [Apply] [X] |
         |_____________________________|

***/

   zd = zdialog_new("Set Map Markers",Mwin,TX("Apply"),"X",null);
   zdialog_add_widget(zd,"radio","all","dialog","mark all image files");
   zdialog_add_widget(zd,"radio","gallery","dialog","mark current gallery");
   zdialog_stuff(zd,"all",1);
   zdialog_stuff(zd,"gallery",0);

   zdialog_load_inputs(zd);
   zdialog_resize(zd,200,0);
   zdialog_set_modal(zd);
   zdialog_run(zd,0,"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 (FGM == 'M') paint_map_markers();                                                //  map view

   return;
}


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

//  map zoom-in on location of a selected image file

void m_map_zoomin(GtkWidget *, ch *menu)
{
   using namespace maps;

   static ch      *file = 0;
   float          flati, flongi;
   xxrec_t        *xxrec;

   printf("m_map_zoomin \n");

   F1_help_topic = TX("SHOW ON MAP");

   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,"map-zoomin");
   else return;

   xxrec = get_xxrec(file);
   if (! xxrec) return;                                                                //  deleted, not an image file

   get_gps_data(xxrec->gps_data,flati,flongi);
   if (flati == 0 && flongi == 0) return;

   viewmode('M');
   map_zoomto(flati,flongi,18);                                                        //  25.1
   return;
}


//  map zoom-in on specified location with specified zoom level

void map_zoomto(float flati, float flongi, int zoomlev)
{
   using namespace maps;

   m_worldmap(0,0);
   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 mapscale(int zoomlev, float flat, float flong)
{
   using namespace maps;
   float fmpp = champlain_map_source_get_meters_per_pixel(map_source,zoomlev,flat,flong);
   return fmpp;
}


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

//  Respond to mouse clicks on map image.

void map_mousefunc(GtkWidget *widget, GdkEventButton *event, void *)
{
   using namespace maps;

   int         mx, my, px, py;
   int         iim, iic, Fuseloc;
   int         KBshift, KBalt, button;
   ch          *location = 0, *country = 0;
   float       flati, flongi, glati, glongi, km;
   int         dist, capturedist = map_dotsize + 2;                                    //  mouse - marker capture distance
   static int  downtime;
   ch          text[100], gps_data[24];
   int         mapww, maphh;
   zdialog     *zd = zd_mapgeotags;

   static ChamplainMarker *marker = 0;
   ClutterColor *newcolor = clutter_color_new(0,0,255,255);

   if (! mapview) return;                                                              //  map not available

   mx = event->x;                                                                      //  mouse position in map widget
   my = event->y;

   KBshift = event->state & GDK_SHIFT_MASK;                                            //  state of shift key
   KBalt = event->state & GDK_MOD1_MASK;

   button = event->button;
   if (button == 1 && KBalt) button = 3;                                               //  left butt + ALT key >> right butt

   flati = champlain_view_y_to_latitude(mapview,my);                                   //  corresp. map geocoordinates
   flongi = champlain_view_x_to_longitude(mapview,mx);

   glati = glongi = 0;

   km = nearest_loc(flati,flongi,iim,iic);                                             //  find nearest known location

   if (iim > -1) {
      glati = imagelocs[iim]->flati;                                                   //  image location
      glongi = imagelocs[iim]->flongi;
      location = imagelocs[iim]->location;
      country = imagelocs[iim]->country;
   }

   if (iic > -1) {                                                                     //  world cities location
      glati = worldlocs[iic]->flati;
      glongi = worldlocs[iic]->flongi;
      location = worldlocs[iic]->location;
      country = worldlocs[iic]->country;
   }
   
   if (km > 20) location = 0;                                                          //  nothing within 20 km                  26.0
   
   Fuseloc = 0;

   if (location) {
      px = champlain_view_longitude_to_x(mapview,glongi);                              //  map pixel 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) Fuseloc = 1;                                            //  mouse is within capture distance
   }

   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 (Fuseloc) {
            zdialog_stuff(zd,"location",location);                                     //  use nearest location data
            zdialog_stuff(zd,"country",country);
            snprintf(gps_data,24,"%.4f %.4f",glati,glongi);                            //  25.1
            zdialog_stuff(zd,"gps_data",gps_data);
         }

         else {
            snprintf(gps_data,24,"%.4f %.4f",flati,flongi);                            //  new location clicked                  25.1
            zdialog_stuff(zd,"gps_data",gps_data);

            if (marker) champlain_marker_layer_remove_marker(markerlayer,marker);      //  add map marker                        26.3
            marker = (ChamplainMarker *) champlain_point_new_full(map_dotsize,newcolor);
            champlain_location_set_location(CHAMPLAIN_LOCATION(marker),flati,flongi);
            champlain_marker_layer_add_marker(markerlayer,marker);

            if (location) {
               zdialog_stuff(zd,"location",location);                                  //  location if found
               zdialog_stuff(zd,"country",country);
            }
            else {
               zdialog_stuff(zd,"location","");
               zdialog_stuff(zd,"country","");
            }

            zdialog_send_event(zd,"geomap");                                           //  activate calling dialog
         }
      }

      else if (button == 1)                                                            //  left click
      {
         if (KBshift) {
            snprintf(text,20,"%.4f %.4f",flati,flongi);                                //  shift key - show coordinates
            poptext_mouse(text,20,-20,0.1,3);
         }

         else if (flati || flongi)                                                     //  on marker - show corresp. images      26.1
            find_map_images(flati,flongi);

         else {
            champlain_view_center_on(mapview,flati,flongi);                            //  zoom-in at clicked location
            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);
         }
      }

      else if (button == 3)                                                            //  right click
         champlain_view_zoom_out(mapview);                                             //  zoom out

      return;
   }

   downtime = 0;                                                                       //  mouse motion

   if (location) {
      snprintf(text,100,"%s \n %.4f %.4f",location,flati,flongi);                      //  show location, coordinates
      poptext_mouse(text,20,-20,0.1,3);
   }

   else {
      snprintf(text,20,"%.4f %.4f",flati,flongi);                                      //  show coordinates
      poptext_mouse(text,20,-20,0.1,3);
   }

   return;
}


//  find images within the marker size, show gallery of images.
//  privat function for map_mousefunc(), called when a location is clicked

void find_map_images(float flati, float flongi)
{
   using namespace maps;

   int         ii, nn = 0;
   int         x1, y1, x2, y2;
   int         capturedist = map_dotsize + 2;                                          //  mouse - marker capture distance
   float       glati, glongi, grange;
   FILE        *fid;
   xxrec_t     *xxrec;

   if (Xindexlev < 1) {
      index_rebuild(1,0);                                                              //  25.1
      if (Nxxrec == 0) {
         zmessageACK(Mwin,TX("image index required"));
         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,TX("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();                                                                  //  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();                                                                  //  keep GTK alive

         xxrec = xxrec_tab[ii];

         if (! *xxrec->gps_data) continue;

         get_gps_data(xxrec->gps_data,glati,glongi);

         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(TX("No matching images found"),10,0,0,3);
      return;
   }

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

   return;
}


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

   get metadata for one image file and set of metadata tag names

   file                 image file to retrieve metadata
   tagname[NK]            set of tag names, e.g. keywords, rating, ISO, city ...
   tagval[NK]            returned tag data
                        (caller must zmalloc() before and zfree() after)

   return status: 0 = OK, +N = system error (errno)
                          -1 = exiftool failure

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

int meta_get(ch *file, ch **tagname, ch **tagval, int NK)
{
   int      ii, kk, cc, err;
   int      Fjxl = 0;
   ch       *file2;
   ch       *pp1, *pp2, *pp3;
   ch       exifcommand[XFCC+500], buff[metadataXcc+100];
   FILE     *fid = 0;

   zfuncs::zappcrash_context1 = file;

   pp1 = strrchr(file,'.');                                                            //  flag JXL file                         jxl
   if (pp1 && strmatchcase(pp1,".jxl")) Fjxl = 1;

   for (ii = 0; ii < NK; ii++)                                                         //  clear outputs
      tagval[ii] = 0;

//  build exiftool command:
//    exiftool -m -S -n -fast                                                          //  processing options
//             -c "%.4f"                                                               //  geocoordinate format
//             -tagname1 -tagname2 ... -tagnameN                                       //  tag names to extract
//             "/.../filename.jpg"                                                     //  input file

   strcpy(exifcommand,"exiftool -m -S -n -fast -c \"%.4f\" ");
   cc = strlen(exifcommand);

   for (kk = 0; kk < NK; kk++)
   {                                                                                   //  append "-tagname " for each tag name
      if (! tagname[kk] || *tagname[kk] <= ' ')
         tagname[kk] = zstrdup("??","meta_get");                                       //  stop deadly exiftool input
      exifcommand[cc++] = '-';

      if (Fjxl && strmatchcase(tagname[kk],"keywords")) {                              //  if JXL file, substitute               jxl
         strcpy(exifcommand+cc,"subject");                                             //     'subject' for 'keywords'
         cc += 7;
      }
      else if (Fjxl && strmatchcase(tagname[kk],"city")) {                             //  if JXL file, substitute               jxl
         strcpy(exifcommand+cc,"location");                                            //     'location' for 'city'
         cc += 8;
      }
      else if (strmatchcase(tagname[kk],"photodate")) {                                //  replace photodate                     26.0
         strcpy(exifcommand+cc,"datetimeoriginal");
         cc += 16;
      }
      else {
         strcpy(exifcommand+cc,tagname[kk]);
         cc += strlen(tagname[kk]);
      }

      exifcommand[cc++] = ' ';
   }

   file2 = zescape_quotes(file);

   exifcommand[cc++] = '"';
   strcpy(exifcommand+cc,file2);                                                       //  append input file
   cc += strlen(file2);
   exifcommand[cc++] = '"';
   exifcommand[cc] = 0;
   zfree(file2);

//  execute exiftool command and read outputs, filenames followed by tag values

   fid = popen(exifcommand,"r");
   if (! fid) goto exiferr;

   while (true)                                                                        //  loop exiftool outputs
   {
      pp1 = fgets_trim(buff,metadataXcc+100,fid,1);                                    //  next exiftool output record
      if (! pp1) break;                                                                //  EOF

      pp2 = strchr(pp1,':');                                                           //  this is a tag data record
      if (! pp2) continue;                                                             //  format is: tagname: tagval
      if (strlen(pp2) < 2) continue;                                                   //             |        |
      *pp2 = 0;                                                                        //             pp1       pp2
      pp2 += 2;

      if (Fjxl && strmatchcase(pp1,"subject"))                                         //  replace 'subject' with 'keywords'     jxl
         pp1 = "keywords";
      if (Fjxl && strmatchcase(pp1,"location"))                                        //  replace 'location' with 'city'        jxl
         pp1 = "city";

      for (kk = 0; kk < NK; kk++)                                                      //  look for tag name match
         if (strmatchcase(pp1,tagname[kk])) break;
      if (kk == NK) continue;                                                          //  should not happen

      cc = strlen(pp2);
      if (cc == 1 && *pp2 == '\n') continue;                                           //  ignore blank line data
      if (cc >= metadataXcc) pp2[cc] = 0;                                              //  25.1

      pp3 = zstrdup(pp2,"meta_get");                                                   //  get tag data

      err = utf8_clean(pp3);                                                           //  replace bad utf8 with '?'
      if (err) printf("*** bad utf8 detected *** %s \n",file);                         //  25.1

      tagval[kk] = pp3;                                                                //  return tag data
   }

   goto OKret;

exiferr:
   errno = -1;
   goto retxx;

OKret:
   errno = 0;

retxx:
   if (fid) pclose(fid);
   if (errno) printf("*** meta_get(): %s \n %s \n",file, strerror(errno));
   return 0;
}


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

   get metadata for set of image files and set of metadata tag names

   files[NF]            set of image files to retrieve metadata
   tagname[NK]          set of tag names, e.g. keywords, rating, ISO, city ...
   tagval[]             returned tag data, NF * NK values
                        caller must zmalloc() pointers before call,
                        and zfree() data and pointers after call.

   return status: 0 = OK, +N = system error (errno)
                          -1 = exiftool failure

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

#define XEXIF 100                      //  max. exiftool threads to use (only NSMP in parallel)

namespace meta_getN_names
{
   ch          **files;                                                                //  caller args
   ch          **tagname;
   ch          **tagval;
   int         Fall;
   int         tagtype[50];                                                            //  up to 50 tags can be processed
   int         NF, NK;
   int         TF1[XEXIF], TF2[XEXIF];                                                 //  file range per exiftool thread
   pthread_t   pid[XEXIF];
   int         busythreads, donethreads;
   int         errstat;
   xxrec_t     *xxrec;
}


//  caller function

int meta_getN(ch **files2, int NF2, ch **tagname2, ch **tagval2, int NK2, int Fall2)   //  26.0
{
   using namespace meta_getN_names;

   void * meta_getN_thread(void *arg);

   int      ii, ff, kk, kk2, tt;
   int      Frange;
   double   secs;
   ch       *pp1, *pp2, *pp3;
   ch       tagdata[metadataXcc];
   int      cc;
   ch       *xmeta;
   
   files = files2;                                                                     //  copy args to namespace
   NF = NF2;                                                                           //  files to process
   tagname = tagname2;                                                                 //  metadata tag names to fetch
   tagval = tagval2;                                                                   //  corresp. tag data
   NK = NK2;                                                                           //  count of tag names and data
   if (NK > 50) zappcrash("meta_getN(), too many tags: %d \n",NK);
   Fall = Fall2;
   
   for (kk = 0; kk < NF * NK; kk++)                                                    //  clear output tag data
      tagval[kk] = 0;

   for (kk = 0; kk < NK; kk++)                                                         //  get tag types from tag names
      tagtype[kk] = metatagtype(tagname[kk]);                                          //  xxrec_tab[], extra indexed metadata,
                                                                                       //    or not indexed
   for (ff = 0; ff < NF; ff++)
   {                                                                                   //  loop all files
      ii = xxrec_index(files[ff]);
      if (ii < 0) continue;                                                            //  non-indexed file, ignore
      xxrec = xxrec_tab[ii];                                                           //  xxrec_tab[] for file

      for (kk = 0; kk < NK; kk++)                                                      //  loop tag names
      {
         kk2 = ff * NK + kk;                                                           //  output slot for file and tag

         if (tagtype[kk] == 0) continue;                                               //  not indexed, must read file metadata
         if (Fall) continue;                                                           //  image index function
         
         if (tagtype[kk] < 100)                                                        //  tag is a member of xxrec_tab[]
         {
            switch (tagtype[kk])
            {
               case xxFNAME:
                  tagval[kk2] = zstrdup(xxrec->file,"meta_getN");
                  continue;
               case xxFDATE:
                  tagval[kk2] = zstrdup(xxrec->fdate,"meta_getN");
                  continue;
               case xxFSIZE: 
                  tagval[kk2] = zstrdup(xxrec->fsize,"meta_getN");
                  continue;
               case xxPDATE:
                  tagval[kk2] = zstrdup(xxrec->pdate,"meta_getN");
                  continue;
               case xxPSIZE: 
                  tagval[kk2] = zstrdup(xxrec->psize,"meta_getN");
                  continue;
               case xxBPC: 
                  tagval[kk2] = zstrdup(xxrec->bpc,"meta_getN");
                  continue;
               case xxRATE:
                  tagval[kk2] = zstrdup(xxrec->rating,"meta_getN");
                  continue;
               case xxKWDS: 
                  tagval[kk2] = zstrdup(xxrec->keywords,"meta_getN");
                  continue;
               case xxTITL: 
                  tagval[kk2] = zstrdup(xxrec->title,"meta_getN");
                  continue;
               case xxDESC: 
                  tagval[kk2] = zstrdup(xxrec->desc,"meta_getN");
                  continue;
               case xxLOC: 
                  tagval[kk2] = zstrdup(xxrec->location,"meta_getN");
                  continue;
               case xxCNTR: 
                  tagval[kk2] = zstrdup(xxrec->country,"meta_getN");
                  continue;
               case xxMAKE: 
                  tagval[kk2] = zstrdup(xxrec->make,"meta_getN");
                  continue;
               case xxMODL: 
                  tagval[kk2] = zstrdup(xxrec->model,"meta_getN");
                  continue;
               case xxLENS: 
                  tagval[kk2] = zstrdup(xxrec->lens,"meta_getN");
                  continue;
               case xxEXP: 
                  tagval[kk2] = zstrdup(xxrec->exp,"meta_getN");
                  continue;
               case xxFNUM: 
                  tagval[kk2] = zstrdup(xxrec->fn,"meta_getN");
                  continue;
               case xxFLNG: 
                  tagval[kk2] = zstrdup(xxrec->fl,"meta_getN");
                  continue;
               case xxISO: 
                  tagval[kk2] = zstrdup(xxrec->iso,"meta_getN");
                  continue;
               default:
                  tagval[kk2] = zstrdup("meta_getN() bug","meta_getN");
                  continue;
            }
         }
         
         else if (tagtype[kk] == 100)                                                  //  tag exists in extra indexed metadata
         {                                                                             //  ( <1 µsec. for 5 GHz CPU and <1 KB)
            *tagdata = 0;
            xmeta = xxrec->xmeta;                                                      //  name1=data1^ name2=data2^ ...
            if (! xmeta) continue;                                                     //               |    |     |
            cc = strlen(tagname[kk]);                                                  //               pp1  pp2   pp3
            pp1 = xmeta;
            while (true) {                                                             //  scan xmeta data
               pp2 = strchr(pp1,'=');
               if (! pp2) break;                                                       //  invalid
               pp3 = strchr(pp2,'^');
               if (! pp3) break;                                                       //  invalid
               if (strmatchcaseN(tagname[kk],pp1,cc) && pp2-pp1 == cc) {
                  if (pp3-pp2 > 1 && pp3 - pp2 < metadataXcc)                          //  meta name and cc match
                     strncpy0(tagdata,pp2+1,pp3-pp2);                                  //  copy metadata
                  break;
               }
               pp1 = pp3 + 1;
               while (*pp1 == ' ') pp1++;
               if (! *pp1) break;
            }
            tagval[kk2] = zstrdup(tagdata,"meta_getN");
         }
      }
   }

   if (! Fall)                                                                         //  not image index function
   {
      for (kk = 0; kk < NK; kk++)                                                      //  loop tag names
         if (tagtype[kk] == 0) break;                                                  //  not indexed, must read file metadata
      if (kk == NK) return 0;                                                          //  no non-indexed tags, done
   }   

   progress_setgoal(NF);                                                               //  start progress counter

   for (tt = 0; tt < XEXIF; tt++)                                                      //  loop exiftool threads
      TF1[tt] = TF2[tt] = -1;                                                          //  file range, initially disabled

   Frange = NF / XEXIF + 1;                                                            //  files per thread

   for (ff = tt = 0; tt < XEXIF; tt++)                                                 //  loop threads
   {
      TF1[tt] = ff;                                                                    //  1st file for thread
      ff += Frange;                                                                    //  + files per thread
      if (ff > NF) ff = NF;                                                            //  limit to last file
      TF2[tt] = ff - 1;                                                                //  last file for thread
      if (ff == NF) break;                                                             //  done
   }

   secs = get_seconds();

   busythreads = donethreads = 0;
   errstat = 0;

   for (tt = 0; tt < XEXIF; tt++)                                                      //  start XEXIF threads
   {
      if (TF1[tt] < 0) break;
      while (busythreads - donethreads >= NSMP) zmainsleep(0.01);                      //  limit to NSMP parallel threads        25.2
      if (errstat) break;
      busythreads++;
      pid[tt] = start_detached_thread(meta_getN_thread,&Nval[tt]);
   }

   while (busythreads > donethreads) zmainsleep(0.01);                                 //  wait for last threads

   if (Fescape) goto cancel;

   secs = get_seconds() - secs;
   printf("meta_getN() files: %d  time: %.1f \n",NF,secs);

   if (errstat) zmessageACK(Mwin,"meta_getN(): %s \n",strerror(errstat));
   progress_setgoal(0);                                                                //  reset progress counter
   return errstat;

cancel:
   printf("*** meta_getN() canceled \n");                                              //  user cancel, terminate threads
   progress_setgoal(0);                                                                //  reset progress counter
   return 1;
}


//  thread function
//  get metadata for files for this thread, file1[T] to fileN[T]

void * meta_getN_thread(void *arg)
{
   using namespace meta_getN_names;

   int      T = *((int *) arg);                                                        //  thread number
   int      F1st, Flast, Fdone;                                                        //  file range for thread
   int      Fonefile;                                                                  //  flag, only 1 file to process
   int      ff, kk, kk2, cc, err;
   int      Fkw = 0, Fcity = 0, Fjxl = 0;                                              //  JXL non-standard tags present         jxl
   ch       *pp1, *pp2;
   ch       linkdir[200], linkname[200];
   ch       exifcommand[1000], buff[xmetaXcc+100];
   ch       *file = 0;
   FILE     *fid = 0;

   F1st = TF1[T];                                                                      //  file range for thread
   Flast = TF2[T];
   
//  create folder containing symlinks to all image files for thread T

   snprintf(linkdir,200,"%s/metalinks_%d",temp_folder,T);                              //  <temp folder>/metalinks_T
   zshell(0,"rm -R -f %s",linkdir);
   err = zshell(0,"mkdir -p -m 0750 %s",linkdir);
   if (err) goto exiferr;

   for (ff = F1st; ff <= Flast; ff++)                                                  //  create symlinks to input files
   {
      snprintf(linkname,200,"%s/%06d",linkdir,ff);                                     //  linkname: <tempfolder>/metalinks_N/nnnnnn
      err = symlink(files[ff],linkname);                                               //  linkname --> filename
      if (err) printf("*** meta_getN(): %s \n %s \n",files[ff],strerror(errno));
   }

//  build exiftool command:
//    exiftool -m -S -n -fast                                                          //  processing options
//             -q is deadly - do not use
//             -c "%.4f"                                                               //  geocoordinate formats
//              2>/dev/null                                                            //  suppress exiftool errors
//             -tagname1 -tagname2 ... -tagnameN                                       //  tag names to extract
//             /home/<user>/.fotocx/temp-nnnnn/metalinks_N/*                           //  folder with input files

   strcpy(exifcommand,"exiftool -m -S -n -fast -c \"%.4f\" 2>/dev/null ");
   cc = strlen(exifcommand);

   for (kk = 0; kk < NK; kk++)                                                         //  loop tag names
   {
      if (! Fall && tagtype[kk] != 0) continue;                                        //  skip indexed tags   

      if (! tagname[kk] || strlen(tagname[kk]) < 1)                                    //  append "-tagname " for each tag name
         tagname[kk] = zstrdup("??","meta_getN");                                      //  stop deadly exiftool input
      exifcommand[cc++] = '-';
      strcpy(exifcommand+cc,tagname[kk]);
      cc += strlen(tagname[kk]);
      exifcommand[cc++] = ' ';
      if (strmatchcase(tagname[kk],"keywords")) Fkw = 1;                               //  flag 'keywords' present               jxl
      if (strmatchcase(tagname[kk],"city")) Fcity = 1;                                 //  flag 'city' present                   jxl
   }

   if (Fkw) {                                                                          //  if 'keywords' present, add 'subject'  jxl
      strcpy(exifcommand+cc,"-subject ");                                              //    (JXL equivalent of 'keywords')
      cc += 9;
   }

   if (Fcity) {                                                                        //  if 'city' present, add 'location'     jxl
      strcpy(exifcommand+cc,"-location ");                                             //    (JXL equivalent of 'city')
      cc += 10;
   }

   strncpy0(exifcommand+cc,linkdir,1000-cc);                                           //  append linkdir
   strcat(exifcommand+cc,"/*");                                                        //  append "/*"

   fid = popen(exifcommand,"r");                                                       //  execute exiftool, read outputs
   if (! fid) goto exiferr;

   Fonefile = 0;
   if (Flast == F1st) Fonefile = 1;                                                    //  flag, only 1 file to process

   Fdone = 0;

   while (true)                                                                        //  loop exiftool outputs
   {
      if (Fonefile) {                                                                  //  one and only file,
         strcpy(buff,"== ");                                                           //    supply filename exiftool omits
         strcpy(buff+3,files[F1st]);
         pp1 = buff;
         Fonefile = 0;                                                                 //  reset flag
      }
      else  pp1 = fgets_trim(buff,xmetaXcc,fid,1);                                     //  next exiftool output record

      if (! pp1) break;                                                                //  EOF
      
      if (*pp1 == '=')                                                                 //  filename record
      {
         ff = F1st + (Fdone++);                                                        //  current file in F1st to Flast
         if (ff > Flast) goto retxx;
         progress_addvalue(1);                                                         //  update progress counter
         if (Fescape) goto retxx;                                                      //  user killed
         file = files[ff];
         zfuncs::zappcrash_context1 = file;                                            //  note file in case zappcrash
         pp2 = strrchr(file,'.');
         Fjxl = 0;
         if (pp2 && strmatchcase(pp2,".jxl")) Fjxl = 1;                                //  note jxl file                         jxl
         continue;                                                                     //  next exiftool output
      }

      pp2 = strchr(pp1,':');                                                           //  this is a tag data record
      if (! pp2) continue;                                                             //  format is: tagname: tagval
      if (strlen(pp2) < 2) continue;                                                   //             |        |
      *pp2 = 0;                                                                        //             pp1       pp2
      pp2 += 2;
      err = utf8_check(pp2);                                                           //  check for utf8 bad data
      if (err) {
         printf("*** bad utf8 detected *** %s \n",file);                               //  25.1
         continue;
      }

      if (Fjxl && Fkw && strmatchcase(pp1,"subject"))                                  //  JXL file uses 'subject'               jxl
         pp1 = "keywords";                                                             //  return data as 'keywords'

      if (Fjxl && Fkw && strmatchcase(pp1,"location"))                                 //  JXL file uses 'location'              jxl
         pp1 = "city";                                                                 //  return data as 'city'

      cc = strlen(pp2);
      if (cc >= metadataXcc) continue;

      for (kk = 0; kk < NK; kk++) {                                                    //  look for tag name match
         kk2 = ff * NK + kk;                                                           //  corresp. tag data
         if (tagval[kk2]) continue;                                                    //  already found, skip
         if (strmatchcase(pp1,tagname[kk])) break;
      }
      if (kk == NK) continue;                                                          //  should not happen

      kk = ff * NK + kk;
      tagval[kk] = zstrdup(pp2,"meta_getN");                                           //  return tag data
   }

   goto OKret;

exiferr:
   errno = -1;
   goto retxx;

OKret:
   errno = 0;

retxx:
   zshell(0,"rm -R -f -d %s",linkdir);                                                 //  remove linkdir
   if (fid) pclose(fid);
   if (errno) {
      errstat = errno;
      printf("*** meta_getN(): %s \n %s \n",file, strerror(errno));
   }
   zadd_locked(donethreads,+1);                                                        //  not busy
   return 0;
}


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

//  create or change metadata for given image file and metadata tags.
//  update both file and corresponding xxrec_tab[] if file is indexed.
//
//  command:
//    exiftool -m -overwrite_original -tagname="tagvalue" ... "file"
//
//  NOTE: exiftool replaces \n (newline) in tag value with . (period).
//  returns: 0 = OK, +N = error (message --> logfile)

int meta_put(ch *file, ch **tagname, ch **tagval, int NK)
{
   int      ccc = 9999;
   ch       exifcommand[ccc];
   ch       *file2;
   ch       *pp;
   ch       tagval2[metadataXcc];
   int      ii, cc, err;
   int      Fjxl = 0;

   if (NK < 1 || NK > 30) zappcrash("meta_put NK: %d",NK);

   err = access(file,W_OK);                                                            //  test file can be written by me
   if (err) {
      printf("*** no write permission: %s \n",file);
      return 1;
   }

   pp = strrchr(file,'.');                                                             //  flag JXL file                         jxl
   if (pp && strmatchcase(pp,".jxl")) Fjxl = 1;

   sprintf(exifcommand,"exiftool -m -n -q -overwrite_original ");                      //  add -n -q                             25.0
   cc = strlen(exifcommand);

   for (ii = 0; ii < NK; ii++)                                                         //  build exiftool inputs
   {
      exifcommand[cc++] = '-';                                                         //  add string -tagname=

      if (Fjxl && strmatchcase(tagname[ii],"keywords")) {                              //  if JXL file, replace 'keywords'       jxl
         strcpy(exifcommand+cc,"subject");                                             //     with 'subject'
         cc += 7;
      }
      else if (Fjxl && strmatchcase(tagname[ii],"city")) {                             //  if JXL file, replace 'city'           jxl
         strcpy(exifcommand+cc,"location");                                            //     with 'location'
         cc += 8;
      }
      else {
         strcpy(exifcommand+cc,tagname[ii]);
         cc += strlen(tagname[ii]);
      }

      exifcommand[cc++] = '=';

      if (! tagval[ii] || ! *tagval[ii]) {                                             //  delete tagname                        25.1
         exifcommand[cc] = ' ';
         cc++;
         continue;
      }

      repl_Nstrs(tagval[ii],tagval2,metadataXcc,"\n",". ","\"","\\\"",null);           //  replace embedded \n with ". "
      if (cc + (int) strlen(tagval2) > ccc-4) {                                        //     and embedded " with \"
         printf("*** meta_put() data too long \n");
         return 1;
      }

      snprintf(exifcommand+cc,ccc-cc,"\"%s\" ",tagval2);                               //  append "tagval" + blank
      cc += strlen(tagval2) + 3;
   }

   file2 = zescape_quotes(file);

   if (cc + (int) strlen(file2) > ccc-3) {
      printf("*** meta_put() data too long \n");
      zfree(file2);
      return 1;
   }

   snprintf(exifcommand+cc,ccc-cc,"\"%s\"",file2);                                     //  append file name
   zfree(file2);

   err = zshell(0,exifcommand);                                                        //  update file metadata
   if (err) printf("*** meta_put() error: %s \n",file);

   file_to_xxrec(file);                                                                //  update xxrec_tab[]                    25.1

   return err;
}


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

//  copy metadata from one image file to new (edited) image file
//  if NK > 0: tagname[] tags get new values from tagval[]
//  return: 0 = OK, +N = error

int meta_copy(ch *file1, ch *file2, ch **tagname, ch **tagval, int NK)
{
   int      ccc = 9999;
   ch       *file1a, *file2a;
   ch       exifcommand[ccc];
   int      err = 0;
   ch       *pp;
   ch       *kwtag[1] = { "keywords" };
   ch       *subtag[1] = { "subject" };
   ch       *kwdata[1] = { "" };

   err = access(file2,W_OK);                                                           //  test file can be written by me
   if (err) {
      printf("*** no write permission: %s \n",file2);
      return 1;
   }

   file1a = zescape_quotes(file1);
   file2a = zescape_quotes(file2);

   snprintf(exifcommand,ccc,"exiftool -m -n -q -tagsfromfile \"%s\" "                  //  simplified                            25.0
                            "\"%s\" -overwrite_original",file1a,file2a);

   zfree(file1a);
   zfree(file2a);

   err = zshell(0,exifcommand);
   if (err) {
      printf("*** meta_copy() error: %s \n",file1);
      return err;
   }

   if (NK) {
      err = meta_put(file2, tagname, tagval, NK);                                      //  add/replace new tags/data
      if (err) {
         printf("*** meta_copy() error: %s \n",file1);
         return err;
      }
   }

   pp = strrchr(file2,'.');                                                            //  test for JXL output file              jxl
   if (pp && strmatchcase(pp,".jxl")) {
      err = meta_get(file1,kwtag,kwdata,1);                                            //  get keywords data from input file
      if (kwdata[0] && *kwdata[0]) {                                                   //  if present, write to output file      25.0
         meta_put(file2,subtag,kwdata,1);
         zfree(kwdata[0]);
      }
   }

   file_to_xxrec(file2);                                                               //  update xxrec_tab[] and thumbnail

   return 0;
}


/**************************************************************************************
   Functions to update metadata in xxrec_tab[] and corresponding image file.
***************************************************************************************/

//  update xxrec_tab[] entry from corresponding file metadata
//  entry will be added if missing, or existing entry updated
//  returns 0 if OK and +N if error

int file_to_xxrec(ch *file)
{
   #define     NK2  NKX + xmetaXtags                                                   //  standard + max. extra metadata tags
   ch          *tagname[NK2], *tagval[NK2];
   ch          xmetarec[xmetaXcc];                                                     //  max. extra metadata cc
   int         kk, nk2, xcc;
   xxrec_t     xxrec, *xxrec2;
   int         ii, iix, nn, cc, err;
   int         Fadd, Freplace;
   float       flati, flongi;
   ch          *RP;
   FILE        *fid;
   STATB       statB;

   if (strstr(file,"-fotocx-temp")) return 0;                                          //  25.1

   RP = f_realpath(file);                                                              //  use real path
   if (! RP) return 1;                                                                 //  FNF

   if (! regfile(RP,&statB)) {                                                         //  not reg. file
      zfree(RP);
      return 1;
   }

   if (Xindexlev < 1) {                                                                //  image index not valid
      zfree(RP);
      return 2;
   }

   for (kk = 0; kk < NKX; kk++)                                                        //  get standard metadata tag names
      tagname[kk] = tagnamex[kk];

   for (kk = 0; kk < xmetaNtags; kk++)                                                 //  add tag names for extra indexed metadata
      tagname[NKX+kk] = xmeta_tags[kk];

   nk2 = NKX + kk;                                                                     //  total tag count

   for (kk = 0; kk < nk2; kk++)                                                        //  insure tagval pointers null
      tagval[kk] = 0;

   err = meta_get(RP,tagname,tagval,nk2);                                              //  get all metadata from image file
   if (err) {
      zfree(RP);
      return err;
   }

   memset(&xxrec,0,sizeof(xxrec));

   xxrec.file = RP;                                                                    //  file name

   if (tagval[0])                                                                      //  file date  yyyy:mm:dd hh:mm:ss+hh:mm
      strncpy0(xxrec.fdate,tagval[0],20);                                              //  (time zone +hh:mm is truncated)
   else *xxrec.fdate = 0;

   if (tagval[1])                                                                      //  file size
      strncpy0(xxrec.fsize,tagval[1],16);
   else *xxrec.fsize = 0;

   if (tagval[2]) {                                                                    //  photo date
      strncpy0(xxrec.pdate,tagval[2],20);
      xxrec.pdate[4] = xxrec.pdate[7] = ':';                                           //  metadata mixed yyyy:mm:dd, yyyy-mm-dd
   }
   else *xxrec.pdate = 0;

   if (tagval[3])                                                                      //  pixel size
      strncpy0(xxrec.psize,tagval[3],16);
   else *xxrec.psize = 0;

   if (tagval[4])                                                                      //  bpc
      strncpy0(xxrec.bpc,tagval[4],4);
   else *xxrec.bpc = 0;

   if (tagval[5])                                                                      //  rating
      strncpy0(xxrec.rating,tagval[5],4);
   else *xxrec.rating = 0;

   if (tagval[6]) {                                                                    //  keywords
      xxrec.keywords = zstrdup(tagval[6],"file_to_xxrec");
      cc = strlen(xxrec.keywords);
      while (cc > 0 && xxrec.keywords[cc-1] == ' ') cc--;                              //  remove trailing ", " (prior fotocx)
      while (cc > 0 && xxrec.keywords[cc-1] == ',') cc--;
      xxrec.keywords[cc] = 0;
   }
   else xxrec.keywords = 0;

   if (tagval[7])                                                                      //  title
      xxrec.title = zstrdup(tagval[7],"file_to_xxrec");
   else xxrec.title = 0;

   if (tagval[8])                                                                      //  description
      xxrec.desc = zstrdup(tagval[8],"file_to_xxrec");
   else xxrec.desc = 0;

   if (tagval[9])                                                                      //  location
      strncpy0(xxrec.location,tagval[9],40);
   else *xxrec.location = 0;

   if (tagval[10])                                                                     //  country
      strncpy0(xxrec.country,tagval[10],40);
   else *xxrec.country = 0;

   if (tagval[11]) {                                                                   //  GPS coordinates
      strncpy0(xxrec.gps_data,tagval[11],24);                                          //  error -> 0.0 0.0
      get_gps_data(xxrec.gps_data,flati,flongi);                                       //  reduce long strings to %.4f
   }
   else *xxrec.gps_data = 0;

   if (tagval[12])                                                                     //  camera make
      strncpy0(xxrec.make,tagval[12],20);
   else *xxrec.make = 0;

   if (tagval[13])                                                                     //  camera model
      strncpy0(xxrec.model,tagval[13],20);
   else *xxrec.model = 0;

   if (tagval[14])                                                                     //  camera lens
      strncpy0(xxrec.lens,tagval[14],20);
   else *xxrec.lens = 0;

   if (tagval[15])                                                                     //  exposure time
      strncpy0(xxrec.exp,tagval[15],12);
   else *xxrec.exp = 0;

   if (tagval[16])                                                                     //  Fnumber
      strncpy0(xxrec.fn,tagval[16],12);
   else *xxrec.fn = 0;

   if (tagval[17])                                                                     //  focal length
      strncpy0(xxrec.fl,tagval[17],12);
   else *xxrec.fl = 0;

   if (tagval[18])                                                                     //  ISO
      strncpy0(xxrec.iso,tagval[18],12);
   else *xxrec.iso = 0;

   xcc = 0;

   for (kk = NKX; kk < nk2; kk++)                                                      //  extra indexed metadata if any
   {
      if (! tagval[kk]) continue;                                                      //  no data
      strcpy(xmetarec+xcc,tagname[kk]);                                                //  construct series
      xcc += strlen(tagname[kk]);                                                      //    "tagname=tagval^ "
      xmetarec[xcc++] = '=';
      strcpy(xmetarec+xcc,tagval[kk]);
      xcc += strlen(tagval[kk]);
      strcpy(xmetarec+xcc,"^ ");
      xcc += 2;
   }

   if (xcc > 0) xxrec.xmeta = zstrdup(xmetarec,"file_to_xxrec");                       //  add to xmeta output record
   else xxrec.xmeta = 0;

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

   nn = -1;
   for (iix = 0; iix < Nxxrec; iix++) {                                                //  find xxrec position in xxrec_tab[]
      nn = strcmp(RP,xxrec_tab[iix]->file);
      if (nn <= 0) break;                                                              //  xxrec goes before or at posn iix
   }                                                                                   //    = posn to add/replace/delete

   Fadd = Freplace = 0;
   if (nn != 0) Fadd = 1;                                                              //  add new xxrec
   if (nn == 0) Freplace = 1;                                                          //  replace existing xxrec

   if (Fadd)
   {
      spinlock(1);                                                                     //  make thread-safe                      25.1

      if (Nxxrec == maximages) {
         zmessageACK(Mwin,TX("exceed %d max files, cannot continue"),maximages);
         quitxx();
      }

      for (ii = Nxxrec; ii > iix; ii--)                                                //  make empty slot
         xxrec_tab[ii] = xxrec_tab[ii-1];                                              //  (move up to Nxxrec pointers)

      xxrec_tab[iix] = (xxrec_t *) zmalloc(sizeof(xxrec_t),"file_to_xxrec");           //  insert new entry
      *xxrec_tab[iix] = xxrec;                                                         //  4ms to move 1m entries using 4 GHz CPU
      Nxxrec++;

      spinlock(0);
   }

   if (Freplace)
   {
      zfree(xxrec_tab[iix]->file);                                                     //  free memory for old xxrec_tab[]
      if (xxrec_tab[iix]->keywords) zfree(xxrec_tab[iix]->keywords);
      if (xxrec_tab[iix]->title) zfree(xxrec_tab[iix]->title);
      if (xxrec_tab[iix]->desc) zfree(xxrec_tab[iix]->desc);
      if (xxrec_tab[iix]->xmeta) zfree(xxrec_tab[iix]->xmeta);

      *xxrec_tab[iix] = xxrec;                                                         //  replace old entry with new
   }

   Findexnew += 1;                                                                     //  count updates since last full index   25.1

   fid = fopen(image_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;

   xxrec2 = xxrec_tab[iix];

   nn = fprintf(fid,"data: %s^ %s^ %s^ %s^ %s^ %s\n",
                  xxrec2->fdate, xxrec2->fsize,                                        //  file date, file size
                  xxrec2->pdate, xxrec2->psize,                                        //  photo date, pixel size
                  xxrec2->bpc, xxrec2->rating);                                        //  bits/color, rating
   if (! nn) goto file_err;

   if (xxrec2->keywords) {
      nn = fprintf(fid,"keywords: %s\n",xxrec2->keywords);                             //  keywords
      if (! nn) goto file_err;
   }

   if (xxrec2->title) {
      nn = fprintf(fid,"title: %s\n",xxrec2->title);                                   //  title
      if (! nn) goto file_err;
   }

   if (xxrec2->desc) {
      nn = fprintf(fid,"desc: %s\n",xxrec2->desc);                                     //  description
      if (! nn) goto file_err;
   }

   nn = fprintf(fid,"loc: %s^ %s^ %s\n",                                               //  location, country, gps_data
         xxrec2->location, xxrec2->country, xxrec2->gps_data);
   if (! nn) goto file_err;

   nn = fprintf(fid,"foto: %s^ %s^ %s^ %s^ %s^ %s^ %s\n",                              //  camera and exposure data
         xxrec2->make, xxrec2->model, xxrec2->lens,
         xxrec2->exp, xxrec2->fn, xxrec2->fl, xxrec2->iso);

   if (xxrec2->xmeta) {                                                                //  26.0
      nn = fprintf(fid,"xmeta: %s\n",xxrec2->xmeta);                                   //  extra metadata record
      if (! nn) goto file_err;
   }

   nn = fprintf(fid,"END\n");                                                          //  EOL
   if (! nn) goto file_err;

   err = fclose(fid);
   if (err) goto file_err;

   update_thumbfile(RP);                                                               //  refresh thumbnail

   return 0;

file_err:
   zmessageACK(Mwin,TX("image index write error: %s"),strerror(errno));
   if (fid) fclose(fid);
   fid = 0;
   return 3;
}


/**************************************************************************************
   Functions to read and write image index file on disk
   and update the image index memory table, xxrec_tab[]
***************************************************************************************/

//  look-up filename in xxrec_tab[] and return its index (0 - Nxxrec)
//  returns -2 if invalid file name or file not found
//  returns -1 if file not in xxrec_tab[]

int xxrec_index(ch *file)
{
   int      ii, jj, kk, rkk, last;
   ch       *RP, fdate[20];
   STATB    statB;

   if (! file || *file != '/') return -2;                                              //  null or no leading '/'

   RP = f_realpath(file);                                                              //  use real path
   if (! RP) return -2;                                                                //  file not found

   if (! regfile(RP,&statB)) {                                                         //  not a regular file
      zfree(RP);
      return 0;
   }

   if (Xindexlev < 1) goto notfound;                                                   //  index not valid
   if (! Nxxrec) goto notfound;                                                        //  index empty

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

      else if (kk < 0) {
         ii += jj;                                                                     //  too low, go forward in table
         if (ii > last) goto notfound;
      }

      else {                                                                           //  xxrec_tab[] found
         pretty_datetime(statB.st_mtime,fdate);                                        //  yyyy:mm:dd hh:mm:ss
         if (! strmatchN(fdate,xxrec_tab[ii]->fdate,19)) goto notfound;                //  check mod time matches
         zfree(RP);                                                                    //   (file modified outside fotocx)
         return ii;                                                                    //  success
      }

      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) goto notfound;                                      //  if direction change, fail
            if (rkk < 0 && kk > 0) goto notfound;
         }
      }
   }

notfound:
   zfree(RP);
   return -1;
}


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

//  Get the image xxrec_tab[] for the given image file.
//  Returns pointer to xxrec in-memory index, or dummy if not found.
//  Returned xxrec fields are NOT subjects for zfree().

xxrec_t * get_xxrec(ch *file)
{
   int         ii;
   STATB       statB;
   ch          *RP;
   static      xxrec_t  xxrec;
   static ch   file2[XFCC];                                                            //  largest accepted file name

   RP = f_realpath(file);                                                              //  use real path
   if (! RP) return 0;                                                                 //  file not found

   if (! regfile(RP,&statB)) {                                                         //  not a regular file
      zfree(RP);
      return 0;
   }

   if (image_file_type(RP) > VIDEO) {                                                  //  not an image or video file
      zfree(RP);
      return 0;
   }

   if (image_file_type(RP) > VIDEO) {                                                  //  not an image or video file
      zfree(RP);
      return 0;
   }

   ii = xxrec_index(file);                                                             //  get xxrec_tab[] index
   if (ii >= 0) {
      zfree(RP);
      return xxrec_tab[ii];                                                            //  found
   }

   memset(&xxrec,0,sizeof(xxrec_t));                                                   //  build dummy xxrec
   strncpy0(file2,RP,XFCC);
   zfree(RP);
   xxrec.file = file2;
   pretty_datetime(statB.st_mtime,xxrec.fdate);
   snprintf(xxrec.fsize,16,"%ld",statB.st_size);
   *xxrec.pdate = 0;
   *xxrec.psize = 0;
   *xxrec.bpc = 0;
   *xxrec.rating = 0;
   xxrec.keywords = 0;
   xxrec.title = 0;
   xxrec.desc = 0;
   *xxrec.location = 0;
   *xxrec.country = 0;
   *xxrec.gps_data = 0;
   *xxrec.make = 0;
   *xxrec.model = 0;
   *xxrec.lens = 0;
   *xxrec.exp = 0;
   *xxrec.fn = 0;
   *xxrec.fl = 0;
   *xxrec.iso = 0;
   xxrec.xmeta = 0;

   return &xxrec;
}


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

//  Read image index files sequentially, return one xxrec_tab[] 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)
{
   int            ii, cc;
   xxrec_t        *xxrec = 0;
   static FILE    *fid = 0;
   static ch      buff[indexrecl];
   ch             *pp;
   float          flati, flongi;
   static ch      *substring[10];

   if (ftf)                                                                            //  initial call
   {
      ftf = 0;
      fid = fopen(image_index_file,"r");
      if (! fid) {                                                                     //  25.1
         printf("read_xxrec_seq: no image index file \n");                             //  26.0
         return 0;
      }

      *buff = 0;                                                                       //  insure no leftover data

      for (ii = 0; ii < 10; ii++)                                                      //  pre-allocate substring buffers
         substring[ii] = (ch *) malloc(100);
   }

   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),"read_xxrec");                          //  allocate xxrec

   xxrec->file = zstrdup(buff+6,"read_xxrec");                                         //  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))                                               //  file date, file size,
      {                                                                                //    photo date, pixel size, rating
         pp += 6;
         get_substrings(pp,'^',6,20,substring);
         strncpy0(xxrec->fdate,substring[0],20);
         strncpy0(xxrec->fsize,substring[1],16);
         strncpy0(xxrec->pdate,substring[2],20);
         strncpy0(xxrec->psize,substring[3],16);
         strncpy0(xxrec->bpc,substring[4],4);
         strncpy0(xxrec->rating,substring[5],4);
      }

      else if (strmatchN(pp,"keywords: ",10)) {                                        //  keywords
         xxrec->keywords = zstrdup(pp+10,"read_xxrec");
         cc = strlen(xxrec->keywords);
         while (cc > 0 && xxrec->keywords[cc-1] == ' ') cc--;                          //  remove trailing ", " (prior fotocx)
         while (cc > 0 && xxrec->keywords[cc-1] == ',') cc--;
         xxrec->keywords[cc] = 0;
      }

      else if (strmatchN(pp,"title: ",7))                                              //  title
         xxrec->title = zstrdup(pp+7,"read_xxrec");

      else if (strmatchN(pp,"desc: ",6))                                               //  description
         xxrec->desc = zstrdup(pp+6,"read_xxrec");

      else if (strmatchN(pp,"loc: ",5))                                                //  location, country, GPS data
      {
         pp += 5;
         get_substrings(pp,'^',3,40,substring);
         strncpy0(xxrec->location,substring[0],40);
         strncpy0(xxrec->country,substring[1],40);
         strncpy0(xxrec->gps_data,substring[2],24);
         get_gps_data(xxrec->gps_data,flati,flongi);                                   //  validate, shorten to %.4f
      }

      else if (strmatchN(pp,"foto: ",6))                                               //  make, model, lens, exp, fn, fl, iso
      {
         pp += 6;
         get_substrings(pp,'^',7,20,substring);
         strncpy0(xxrec->make,substring[0],20);
         strncpy0(xxrec->model,substring[1],20);
         strncpy0(xxrec->lens,substring[2],20);
         strncpy0(xxrec->exp,substring[3],12);
         strncpy0(xxrec->fn,substring[4],12);
         strncpy0(xxrec->fl,substring[5],12);
         strncpy0(xxrec->iso,substring[6],12);
      }

      else if (strmatchN(pp,"xmeta: ",7))                                              //  extra metadata
         xxrec->xmeta = zstrdup(pp+7,"read_xxrec");
   }

   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
{
   static FILE    *fid = 0;
   int            err, nn;

   if (ftf)                                                                            //  first call
   {
      ftf = 0;
      fid = fopen(image_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);                                         //  file: filename
   if (! nn) goto file_err;

   nn = fprintf(fid,"data: %s^ %s^ %s^ %s^ %s^ %s\n",
        xxrec->fdate, xxrec->fsize,                                                    //  file date, file size
        xxrec->pdate, xxrec->psize,                                                    //  photo date, pixel size
        xxrec->bpc, xxrec->rating);                                                    //  bits/color, rating
   if (! nn) goto file_err;

   if (xxrec->keywords) {
      nn = fprintf(fid,"keywords: %s\n",xxrec->keywords);                              //  keywords: aaaaa, bbbbb, ...
      if (! nn) goto file_err;
   }

   if (xxrec->title) {
      nn = fprintf(fid,"title: %s\n",xxrec->title);                                    //  title: text
      if (! nn) goto file_err;
   }

   if (xxrec->desc) {
      nn = fprintf(fid,"desc: %s\n",xxrec->desc);                                      //  desc: text
      if (! nn) goto file_err;
   }

   nn = fprintf(fid,"loc: %s^ %s^ %s\n",                                               //  loc: location, country, gps coordinates
         xxrec->location, xxrec->country, xxrec->gps_data);
   if (! nn) goto file_err;

   nn = fprintf(fid,"foto: %s^ %s^ %s^ %s^ %s^ %s^ %s\n",
         xxrec->make, xxrec->model, xxrec->lens,
         xxrec->exp, xxrec->fn, xxrec->fl, xxrec->iso);
   if (! nn) goto file_err;

   if (xxrec->xmeta)
      nn = fprintf(fid,"xmeta: %s\n",xxrec->xmeta);                                    //  xmeta: tag1=data1^ tag2=data2^ ...
   if (! nn) goto file_err;

   nn = fprintf(fid,"END\n");                                                          //  EOL
   if (! nn) goto file_err;

   return 0;

file_err:
   zmessageACK(Mwin,TX("image index write error: %s"),strerror(errno));
   if (fid) fclose(fid);
   fid = 0;
   quitxx();
   return 2;
}

