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

   Fotocx - edit photos and manage collections

   Copyright 2007-2024 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
   ----------------------
   select_meta_keys           dialog to select metadata items
   m_meta_view_short          metadata short report
   m_meta_view_long           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_tags         define tags (keywords) for image searching
   m_meta_choose_caps         choose metadata keys for image captions
   meta_show_caps             show captions on current image
   m_meta_toggle_caps         toggle display of image captions on/off
   m_batch_tags               batch add and delete tags for selected image files
   m_batch_rename_tags        convert tag names using a from-to list
   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_meta_mover         batch move metadata between keys
   m_batch_geotags            add given geotag to selected set of images

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

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

   add_tag                    add tag to a tag list
   set_meta_wwhh              set image width/height metadata
   del_tag                    remove tag from a tag list
   add_recentag               add tag to recent tags list, remove oldest if needed
   load_deftags               load defined tags list from tags file and image index
   save_deftags               save defined tags list to tags file
   find_deftag                check if given tag is in defined tags list
   add_deftag                 add new tag to defined tags list or change category
   del_deftag                 remove tag from defined tags list
   del_defcatg                remove category from defined tags list (if no tags assigned)
   deftags_stuff              stuff defined tags into dialog text widget
   defcats_stuff              stuff defined categories into dialog combobox widget

   load_filemeta              load image file metadata into memory (indexed data only)
   save_filemeta              save metadata to image file metadata and to image index
   update_image_index         update index data for current image file
   delete_image_index         delete index record for deleted image file

   load_imagelocs             load geolocations table from image files
   load_worldlocs             load geolocations table from world cities file
   find_imagelocs             find geolocations using image data
   find_worldlocs             find geolocations using world cities table
   choose_location            choose a location/country given leading substring(s)                                         24.20
   nerest_loc                 find nearest known location name for given geocoordinates

   put_geolocs                put new location data in geolocations table
   validate_latlong           validate earth coordinates data
   earth_distance             compute km distance between two earth coordinates
   get_gallerymap             get map coordinates for current gallery files

   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_get1                  get image file metadata from list of keys
   meta_getN                  same for multiple files, using multiple threads
   meta_put                   update image metadata from list of keys and data
   meta_copy                  copy metadata from file to file, with revisions
   meta_tagdate               yyyy-mm-dd hh:mm:ss to yyyymmddhhmmss
   tag_metadate               yyyymmddhhmmss to yyyy-mm-dd hh:mm:ss

   Image index functions
   ---------------------
   get_xxrec                  get image index record for image file
   put_xxrec                  add or update index record for an image file
   read_xxrec_seq             read all index records sequentially, one per call
   write_xxrec_seq            write all index records sequentially

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

#define EX extern                                                                //  enable extern declarations

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

using namespace zfuncs;

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

ch  *pdate_metadate(ch *pdate);                                                  //  "yyyy-mm-dd" to "yyyymmdd"
ch  *ptime_metatime(ch *ptime);                                                  //  "hh:mm[:ss]" to "hhmmss"
ch  *pdatetime_metadatetime(ch *pdatetime);                                      //  yyyy-mm-dd hh:mm[:ss] to yyyymmddhhmmss
int   add_tag(ch *tag, ch *taglist, int maxcc);                                  //  add tag if unique and enough space
int   del_tag(ch *tag, ch *taglist);                                             //  remove tag from tag list
int   add_recentag(ch *tag);                                                     //  add tag to recent tags, keep recent
void  load_deftags(int force);                                                   //  load tags_deftags from index data
void  save_deftags();                                                            //  tags_deftags[] >> defined_tags file
int   find_deftag(ch *tag);                                                      //  find tag in tags_deftags[]
int   add_deftag(ch *catg, ch *tag);                                             //  add tag to tags_deftags[]
int   del_deftag(ch *tag);                                                       //  remove tag from tags_deftags[]
int   del_defcatg(ch *catg);                                                     //  remove category from tags_deftags[]
void  deftags_stuff(zdialog *zd, ch *catg);                                      //  tags_deftags[] >> zd widget deftags
void  defcats_stuff(zdialog *zd);                                                //  defined categories >> " widget defcats

int   load_imagelocs();                                                          //  load image geolocations table
int   load_worldlocs();                                                          //  load cities geolocations table
int   glocs_compare(ch *rec1, ch *rec2);                                         //  compare geocoordinate records
int   find_imagelocs(zdialog *zd);                                               //  find geolocations using image data
int   find_worldlocs(zdialog *zd);                                               //  find geolocations using cities table
float nearest_loc(float flati, float flongi, int &iim, int &iic);                //  find nearest location, image or world city

int   put_geolocs(zdialog *zd);                                                  //  Update geolocations table in memory
int   validate_latlong(ch *lati, ch *longi, float &flat, float &flong);          //  convert and validate earth coordinates
float earth_distance(float lat1, float long1, float lat2, float long2);          //  compute distance from earth coordinates
int   get_gallerymap();                                                          //  get map coordinates for gallery files
void  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     meta_pdate[16];                                                        //  image (photo) date, yyyymmddhhmmss
   ch     meta_rating[4];                                                        //  image rating in stars, "0" to "5"
   ch     meta_wwhh[16];                                                         //  image width/height, "2345x1234"
   ch     meta_tags[filetagsXcc];                                                //  tags for current image file
   ch     meta_title[metadataXcc];                                               //  image title
   ch     meta_description[metadataXcc];                                         //  image description
   ch     meta_city[100], meta_country[100];                                     //  geolocs: location, country
   ch     meta_lati[20], meta_longi[20];                                         //  geolocs: earth coordinates (-123.4567)

   ch     p_meta_pdate[16];                                                      //  previous file metadata
   ch     p_meta_rating[4];
   ch     p_meta_tags[filetagsXcc];
   ch     p_meta_title[metadataXcc];
   ch     p_meta_description[metadataXcc];
   ch     p_meta_city[100], p_meta_country[100];                                 //  metadata "City" is Fotocx "Location"
   ch     p_meta_lati[20], p_meta_longi[20];

   ch     *xmeta_data[xmetamaxkeys];                                             //  indexed metadata (xmeta_key[])

   ch     *tags_deftags[maxtagcats];                                             //  defined tags: catg: tag1, ... tagN,
   ch     tags_recentags[recenttagsXcc] = "";                                    //  recently added tags 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;
}


using namespace meta_names;


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

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

namespace select_meta_keys_names
{
   GtkWidget   *mtext1, *mtext2;
   int         Fexclude, Fchange;
   zdialog     *zd;
   ch          *pp;
   ch          *excludes =
                  meta_tags_key "-" meta_rating_key "-" meta_date_key "-"
                  meta_title_key "-" meta_description_key "-"
                  meta_city_key "-" meta_country_key "-"
                  meta_lati_key "-" meta_longi_key ;
}

int select_meta_keys(zlist_t *mlist, int maxout, int exclude)
{
   using namespace select_meta_keys_names;

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

   int         zstat, ii, nn;
   ch          *pp, ppc[80];
   zlist_t     *picklist;

   Fexclude = exclude;
   Fchange = 0;

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

***/

   zd = zdialog_new("Select Metadata Items",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","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","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");
   textwidget_clear(mtext1);

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

   picklist = zlist_from_file(meta_picklist_file);                               //  metadata picklist
   if (! picklist) {
      zmessageACK(Mwin,"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(ppc,pp);                                                    //  exclude items indexed by default
         if (strcasestr(excludes,ppc)) continue;
      }
      textwidget_append(mtext1,0,"%s\n",pp);                                     //  picklist >> left widget
   }

   textwidget_append(mtext1,0,"%s\n","Other Item ...");                          //  append "Other Item ..."

   zlist_free(picklist);                                                         //  free memory

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

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

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

   if (zstat != 1 || ! Fchange) {                                                //  no changes made
      zdialog_free(zd);
      return 0;
   }

   nn = textwidget_linecount(mtext2);

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

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

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

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


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

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

   ch     *pp, ppc[80];

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

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

   if (strmatch(pp,"Other Item ...")) {                                          //  get manually input metadata name
      pp = zdialog_text(zd->dialog,"metadata item name",0);
      if (! pp) return;
      strCompress(pp);                                                           //  remove blanks
      if (Fexclude && strstr(excludes,pp)) {                                     //  exclude items indexed by default
         zmessageACK(Mwin,"%s is already indexed",pp);
         zfree(pp);
         return;
      }
   }

   strCompress(ppc,pp);                                                          //  remove blanks (exiftool requirement)
   zfree(pp);

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

   Fchange = 1;
   return;
}


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

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

   ch     *pp;

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

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

   textwidget_delete(widget,line);                                               //  delete line

   Fchange = 1;
   return;
}


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

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

int   metadata_report_type = 1;


//  called by f_open() if zd_metaview is defined

void meta_view(int type)
{
   if (type) metadata_report_type = type;

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


//  menu function - metadata short report

void m_meta_view_short(GtkWidget *, ch *menu)
{
   void  meta_view_item_report(ch *kname, ch *kval, GtkWidget *widget);
   int   meta_view_dialog_event(zdialog *zd, ch *event);

   #define vNK 21
   ch         *kname[vNK] = {
                  meta_image_size_key, meta_file_size_key,
                  meta_date_key, meta_file_date_key,
                  meta_make_key, meta_model_key, meta_lens_key,
                  meta_fl_35_key, meta_fl_key,
                  meta_exp_key, meta_fn_key, meta_iso_key,
                  meta_city_key, meta_country_key, meta_lati_key, meta_longi_key,
                  meta_tags_key, meta_rating_key, meta_edithist_key,
                  meta_title_key, meta_description_key };

   ch             *kval[vNK];

   ch             *focallength, chsec[12];
   ch             *text1, **text2;
   static ch      *file, *filen;
   float          fsecs;
   int            err, ii, nn, cc;
   ch             *editdelims = "|";
   GtkWidget      *widget;

   FILE           *fid;
   int            nkx = 0;
   ch             *knamex[maxviewkeys];
   ch             *kvalx[maxviewkeys];                                           //  extra metadata items
   ch             buff[metakeyXcc], *pp;

   F1_help_topic = "view main meta";

   Plog(1,"m_meta_view_short \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("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_dialog_event,"save");
   }

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

   err = meta_get1(file,(ch **) kname,kval,vNK);
   if (err) return;

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

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

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

   if (kval[4] || kval[5] || kval[6])
      textwidget_append(widget,0,"Camera      make: %s  model: %s  lens: %s \n",
                                                kval[4], kval[5], kval[6]);

   if (kval[7] || kval[8] || kval[9] || kval[10] || kval[11])                    //  photo exposure data
   {
      if (kval[7]) focallength = kval[7];                                        //  focal length, 35mm equivalent
      else if (kval[8]) focallength = kval[8];                                   //  focal length, actual mm
      else focallength = 0;                                                      //  missing
      strcpy(chsec,"null");
      if (kval[9]) {
         fsecs = atofz(kval[9]);                                                 //  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);
      }
      textwidget_append(widget,0,"Exposure    %s mm  %s sec  F%s  ISO %s \n",
                        focallength,chsec,kval[10],kval[11]);
   }

   if (kval[12] || kval[13] || kval[14] || kval[15]) {                           //  location, country, latitude, longitude
      if (kval[14] && (strlen(kval[14]) > 8)) kval[14][8] = 0;
      if (kval[15] && (strlen(kval[15]) > 8)) kval[15][8] = 0;
      textwidget_append(widget,0,"Location    %s %s  %s %s \n",
                    kval[12],kval[13],kval[14],kval[15]);
   }

   if (kval[16]) {                                                               //  tags
      cc = strlen(kval[16]) - 1;
      if (kval[16][cc] == ',') kval[16][cc] = 0;
      nn = breakup_text(kval[16],text2,editdelims,60,80);                        //  wrap long lines 
      textwidget_append(widget,0,"tags        %s \n",text2[0]);
      for (ii = 1; ii < nn; ii++)
         textwidget_append(widget,0,"            %s \n",text2[ii]);
      for (ii = 0; ii < nn; ii++)
         zfree(text2[ii]);
      zfree(text2);
   }

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

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

   if (kval[19])                                                                 //  title
      meta_view_item_report(kname[19],kval[19],widget);

   if (kval[20])                                                                 //  description
      meta_view_item_report(kname[20],kval[20],widget);

   for (ii = 0; ii < vNK; ii++)                                                  //  free memory - standard items
      if (kval[ii]) zfree(kval[ii]);

   //  append extra report items if any

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

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

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

   err = meta_get1(file,knamex,kvalx,nkx);                                       //  get all extra items at once
   if (err) goto finished;

   for (ii = 0; ii < nkx; ii++)                                                  //  report user extra items
      meta_view_item_report(knamex[ii],kvalx[ii],widget); 

   for (ii = 0; ii < nkx; ii++) {                                                //  free memory
      zfree(knamex[ii]);
      if (kvalx[ii]) zfree(kvalx[ii]);
   }

finished:
   zfree(file);
   return;
}


//  m_meta_view_short helper function
//  write key name and key value to report, breaking up long text where needed.

void meta_view_item_report(ch *kname, ch *kval, GtkWidget *widget)
{
   int      ii, nn;
   ch       **longtext;

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

   if (strlen(kval) < 100) {
      textwidget_append(widget,0,"%-11s %s \n",kname,kval);
      return;
   }

   textwidget_append(widget,0,"\n");                                             //  blank line
   textwidget_append(widget,0,"%s: \n",kname);                                   //  key name
   nn = breakup_text(kval,longtext,0,70,90);                                     //  break up long key value
   for (ii = 0; ii < nn; ii++)
      textwidget_append(widget,0,"   %s \n",longtext[ii]);                       //  output each piece
   for (ii = 0; ii < nn; ii++)
      zfree(longtext[ii]);                                                       //  free memory
   zfree(longtext);

   return;
}


//  menu function - metadata long report

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

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

   F1_help_topic = "view main meta";

   Plog(1,"m_meta_view_long \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("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_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
   textwidget_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,1000,fid))) {
         err = utf8_clean(pp);                                                   //  replace bad utf8 with '?'             24.60
         if (err) Plog(0,"*** bad utf8 detected *** \n");
         textwidget_append(widget,0,"%s\n",pp);                                  //  add to report window
      }
      pclose(fid);
   }

   zfree(file);
   return;
}


//  dialog event and completion callback function

int meta_view_dialog_event(zdialog *zd, 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 (metadata_report_type != 1) return 1;                                      //  not short report
   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_keys(mlist,maxviewkeys,1);                                   //  user edit of extras list
   if (nn) zlist_to_file(mlist,meta_view_extras_file);                           //  update extras file

   zlist_free(mlist);
   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)                                                     //  24.30
{
   zdialog     *zd;
   ch          text2[metadataXcc]; 
   int         zstat;
   
   repl_1str(text,text2,metadataXcc,"\\n","\n");                                 //  replace fake \n with real \n

   zd = zdialog_new("Edit Metadata",Mwin,"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",text2);                                               //  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",text2,metadataXcc);                                   //  get edited text
   repl_1str(text2,text,metadataXcc,"\n","\\n");                                 //  replace real \n with fake \n

   zdialog_free(zd);
   return 1;
}


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

//  edit main metadata menu function

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

   GtkWidget   *widget;
   zdialog     *zd;
   ch          *ppv, pdate2[12], ptime2[12];
   ch          text50[50];
   ch          RN[4] = "R0";
   int         err, ii;

   F1_help_topic = "edit main meta";

   Plog(1,"m_meta_edit_main \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,"no current file");
      if (zd_editmeta) zdialog_free(zd_editmeta);
      zd_editmeta = 0;
      zd_mapgeotags = 0;
      return;
   }

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

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

/***
          ___________________________________________________________
         |                 Edit Main Metadata                        |
         |                                                           |
         |  File: filename.jpg                                       |
         |          ________________________________________         |
         |  Title  |________________________________________| [edit] |           //  24.70
         |          ________________________________________         |
         |  Desc.  |________________________________________| [edit] |
         |                                                           |
         |  Image Date: [__________]  Time: [________]  [prev]       |
         |                                                           |
         |  Rating (stars):  [] 0  [] 1  [] 2  [] 3  [] 4  [] 5      |
         |                                                           |
         |  location [_______________]  country [______________]     |
         |  latitude [__________]  longitude [__________]            |
         |  [Find] [Lookup] [Prev] [Clear]                           |
         |                                                           |
         |  Image Tags [___________________________________________] |
         |  - - - - - - - - - - - - - - - - - - - - - - - - - - - -  |
         |  Recent Tags [______________________________________] [X] |
         |  Enter Tag [______________]  [Add]                        |
         |  Matching Tags [________________________________________] |
         |                                                           |
         |  Defined Tags Category [______________________________|v] |
         |  |                                                      | |
         |  |                                                      | |
         |  |                                                      | |
         |  |                                                      | |
         |  |                                                      | |
         |  |                                                      | |
         |  |                                                      | |
         |  |______________________________________________________| |
         |                                                           |
         |                                        [Prev] [Apply] [X] |
         |___________________________________________________________|

***/

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

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

      //  File: xxxxxxxxx.jpg
      zdialog_add_widget(zd,"hbox","hbf","dialog",0,"space=3");
      zdialog_add_widget(zd,"label","labf","hbf","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]         //  24.70
      
      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","edit","space=3");
      
      zdialog_add_widget(zd,"hsep","sep","dialog",0,"space=3");

      //          ______________________________________________ 
      //  Desc.  |______________________________________________| [edit]         //  24.70
      
      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","description","hbds",0,"expand");
      zdialog_add_widget(zd,"button","editdesc","hbds","edit","space=3");

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

      //  Defined Tags Category
      zdialog_add_widget(zd,"hbox","hbdt1","dialog");
      zdialog_add_widget(zd,"label","labdt","hbdt1","Defined Tags 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","deftags","swdt2",0,"wrap");

      zdialog_add_ttip(zd,"geofind","search known locations");
      zdialog_add_ttip(zd,"geolookup","find via table lookup"); 
      zdialog_add_ttip(zd,"geoprev","use previous input");
      zdialog_add_ttip(zd,"geoclear","clear inputs");

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

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

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

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

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

      zdialog_resize(zd,600,800);                                                //  run dialog                            24.30
      zdialog_run(zd,editmeta_dialog_event,"save");
   }

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

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

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

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

   strncpy0(text50,meta_title,50);                                               //  title >> dialog                       24.70
   if (strlen(meta_title) > 49) strcpy(text50+45," ...");
   zdialog_stuff(zd,"title",text50);

   strncpy0(text50,meta_description,50);                                         //  description >> dialog                 24.70
   if (strlen(meta_description) > 49) strcpy(text50+45," ...");
   zdialog_stuff(zd,"description",text50);

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

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

   return;
}


//  mouse click functions for various text widgets for tags

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

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

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

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

   zfree(txtag);
   return;
}


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

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

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

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

   zfree(txtag);
   return;
}


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

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

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

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

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

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

   zfree(txtag);
   return;
}


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

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

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

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

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

   zfree(txtag);
   return;
}


//  dialog event and completion callback function

int editmeta_dialog_event(zdialog *zd, ch *event)
{
   ch       pdate2[12], ptime2[12];                                              //  yyyy-mm-dd  and  hh:mm:ss
   ch       *metadate, *metatime;                                                //  yyyymmdd  and  hhmmss
   int      ii, jj, nn, nt, cc1, cc2, ff, err;
   ch       *pp1, *pp2;
   ch       catgname[tagXcc];
   ch       newtag[tagXcc], matchtags[20][tagXcc];
   ch       matchtagstext[(tagXcc+2)*20];
   ch       text50[50];
   ch       RN[4] = "R0";
   ch       location[100], country[100], lati[20], longi[20];
   float    flati, flongi;

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

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

   if (strmatch(event,"edittitle")) {                                            //  edit title                            24.70
      meta_edit_text(meta_title);
      strncpy0(text50,meta_title,50);
      if (strlen(meta_title) > 49) strcpy(text50+45," ...");
      zdialog_stuff(zd,"title",text50);
      Fmetamod++;
   }

   if (strmatch(event,"editdesc")) {                                             //  edit description                      24.70
      meta_edit_text(meta_description);
      strncpy0(text50,meta_description,50);
      if (strlen(meta_description) > 49) strcpy(text50+45," ...");
      zdialog_stuff(zd,"description",text50);
      Fmetamod++;
   }


   if (zstrstr("date time title description",event))                             //  note change but process later
      Fmetamod++;

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

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

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

   if (strmatch(event,"geomap")) {                                               //  have geotags data from map click
      Fmetamod++;
      return 1;
   }

   if (strmatch(event,"geofind"))                                                //  [find]
   {
      nn = find_imagelocs(zd);                                                   //  search image location data
      if (nn) Fmetamod++;                                                        //  success
      return 1;
   }

   if (strmatch(event,"geolookup"))                                              //  [lookup]
   {
      nn = find_worldlocs(zd);                                                   //  search cities geolocations table
      if (nn) Fmetamod++;                                                        //  success
      return 1;
   }

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

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

   if (strmatch(event,"defcats")) {                                              //  new tag category selection
      zdialog_fetch(zd,"defcats",catgname,tagXcc);
      deftags_stuff(zd,catgname);
   }
   
   if (strmatch(event,"clear recent")) {                                         //  clear recent tags list                24.60
      zdialog_stuff(zd,"recentags","");
      *tags_recentags = 0;
   }

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

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

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

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

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

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

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

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

      pp1 = matchtagstext;

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

      zdialog_stuff(zd,"matchtags",matchtagstext);                               //  stuff matchtags in dialog
      return 1;
   }

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

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

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

         if (ff) break;
      }

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

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

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

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

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

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

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

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

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

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

      strncpy0(text50,meta_title,50);                                            //  stuff title                           24.70
      if (strlen(meta_title) > 49) strcpy(text50+45," ...");
      zdialog_stuff(zd,"title",text50);

      strncpy0(text50,meta_description,50);                                      //  stuff description                     24.70
      if (strlen(meta_description) > 49) strcpy(text50+45," ...");
      zdialog_stuff(zd,"description",text50);

      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

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

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

   zdialog_fetch(zd,"location",location,100);                                    //  get location from dialog
   zdialog_fetch(zd,"country",country,100);
   strTrim2(location);
   strTrim2(country);

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

   if (*country && ! strmatch(country,"null")) {
      *country = toupper(*country);
      zdialog_stuff(zd,"country",country);
   }

   zdialog_fetch(zd,"lati",lati,20);                                             //  get latitude, longitude
   zdialog_fetch(zd,"longi",longi,20);
   strTrim2(lati);
   strTrim2(longi);

   if (*lati || *longi) {                                                        //  if coordinates present, validate
      err = validate_latlong(lati,longi,flati,flongi);
      if (err) {
         zmessageACK(Mwin,"bad latitude/longitude: %s %s",lati,longi);
         return 1;
      }
   }

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

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

   return 1;
}


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

//  edit any metadata - add or change specified meta/etc. keydata

namespace meta_edit_any_names
{
   ch     kname[metakeyXcc];
   ch     kdata[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);
   void  meta_edit_any_clickfunc(GtkWidget *, int line, int pos, int kbkey);

   GtkWidget      *mtext;
   int            err;
   zdialog        *zd;
   ch             *pp1[1];
   ch             *pp2[1], *pp;

   F1_help_topic = "edit any meta";

   Plog(1,"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","no write permission",curr_file);
      return;
   }

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

***/

   if (! zd_editanymeta)                                                         //  popup dialog if not already
   {
      zd = zdialog_new("Edit Any Metadata",Mwin,"Short List","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","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","File:","space=3");
      zdialog_add_widget(zd,"label","file","hbf","filename.jpg","space=5");
      zdialog_add_widget(zd,"hbox","hbkey","vb2",0,"space=2");
      zdialog_add_widget(zd,"label","labkey","hbkey","key name","space=5");
      zdialog_add_widget(zd,"zentry","kname","hbkey",0,"size=30");
      zdialog_add_widget(zd,"hbox","hbdata","vb2",0,"space=2");
      zdialog_add_widget(zd,"label","labdata","hbdata","key value","space=5");
      zdialog_add_widget(zd,"zedit","kdata","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","fetch","space=3");
      zdialog_add_widget(zd,"button","update","hbb","update","space=3");
      zdialog_add_widget(zd,"button","delete","hbb","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
      textwidget_set_eventfunc(mtext,meta_edit_any_clickfunc);                   //  set mouse/KB event function

      *kname = 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 key list

   if (*kname)                                                                   //  update current key value
   {
      pp1[0] = kname;                                                            //  look for key data
      meta_get1(curr_file,pp1,pp2,1);
      if (pp2[0]) {
         strncpy0(kdata,pp2[0],metadataXcc);
         zfree(pp2[0]);
      }
      else *kdata = 0;
      zdialog_stuff(zd,"kdata",kdata);                                           //  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          *pp1[1];
   ch          *pp2[1];
   int         err;
   static int  whichlist = 1;                                                    //  1/2 = short/full list

   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,"kname",kname,metakeyXcc);                                //  get key name from dialog
      strCompress(kname);
      pp1[0] = kname;                                                            //  look for key data
      meta_get1(curr_file,pp1,pp2,1);
      if (pp2[0]) {
         strncpy0(kdata,pp2[0],metadataXcc);
         zfree(pp2[0]);
      }
      else *kdata = 0;
      zdialog_stuff(zd,"kdata",kdata);                                           //  stuff into dialog
   }

   if (strmatch(event,"update"))
   {
      zdialog_fetch(zd,"kname",kname,metakeyXcc);                                //  get key name from dialog
      zdialog_fetch(zd,"kdata",kdata,metadataXcc);
      strCompress(kname);
      pp1[0] = kname;
      pp2[0] = kdata;
      err = meta_put(curr_file,pp1,pp2,1);                                       //  change metadata in image file
      if (err) zmessageACK(Mwin,"metadata update error");
      load_filemeta(curr_file);                                                  //  update image index in case
      update_image_index(curr_file);                                             //    searchable metadata item updated
      if (zd_metaview) meta_view(0);                                             //  update metadata view if active
   }

   if (strmatch(event,"delete"))
   {
      zdialog_fetch(zd,"kname",kname,metakeyXcc);                                //  get key name from dialog
      zdialog_stuff(zd,"kdata","");                                              //  clear key data in dialog
      *kdata = 0;                                                                //  and in memory
      strCompress(kname);
      pp1[0] = kname;
      pp2[0] = kdata;
      err = meta_put(curr_file,pp1,pp2,1);                                       //  change metadata in image file
      if (err) zmessageACK(Mwin,"metadata update error"); 
      load_filemeta(curr_file);                                                  //  update image index in case
      update_image_index(curr_file);                                             //    searchable metadata item updated
      if (zd_metaview) meta_view(0);                                             //  update 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
      textwidget_clear(mtext);

      fid = fopen(meta_picklist_file,"r");                                       //  get list of metadata items
      if (fid) {
         while ((pp = fgets_trim(buff,1000,fid)))
            textwidget_append(mtext,0,"%s\n",pp);
         fclose(fid);
      }

      whichlist = 1;
   }

   else if (zd->zstat == 2)                                                      //  full list
   {
      zd->zstat = 0;                                                             //  keep dialog active
      mtext = zdialog_gtkwidget(zd,"mtext");                                     //  make clickable metadata list
      textwidget_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;
            textwidget_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

void meta_edit_any_clickfunc(GtkWidget *widget, int line, int pos, int kbkey)
{
   using namespace meta_edit_any_names;

   ch        *pp, *pp2[1];
   ch        *pp1[1];

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

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

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

   zdialog_stuff(zd_editanymeta,"kname",pp);

   zdialog_fetch(zd_editanymeta,"kname",kname,metakeyXcc);                       //  get key name from dialog
   strCompress(kname);

   pp1[0] = kname;                                                               //  look for key data
   meta_get1(curr_file,pp1,pp2,1);
   if (pp2[0]) {
      strncpy0(kdata,pp2[0],metadataXcc);
      zfree(pp2[0]);
   }
   else *kdata = 0;
   zdialog_stuff(zd_editanymeta,"kdata",kdata);                                  //  stuff into dialog

   return;
}


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

//  delete metadata, specific key 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 = "delete meta";

   Plog(1,"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,"no current file");
      return;
   }

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

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

***/

   if (! zd_deletemeta)
   {
      zd = zdialog_new("Delete Metadata",Mwin,"Apply"," X ",null);
      zd_deletemeta = zd;
      zdialog_add_widget(zd,"hbox","hbf","dialog");
      zdialog_add_widget(zd,"label","labf","hbf","File:","space=3");
      zdialog_add_widget(zd,"label","file","hbf",0,"space=5");
      zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=3");
      zdialog_add_widget(zd,"radio","kall","hb1","All","space=5");
      zdialog_add_widget(zd,"radio","key1","hb1","One Key:");
      zdialog_add_widget(zd,"zentry","kdata","hb1",0,"size=20");
      zdialog_stuff(zd,"key1",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         kall, key1;
   ch          *file2;
   ch          kdata[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,"kall",kall);
   zdialog_fetch(zd,"key1",key1);
   zdialog_fetch(zd,"kdata",kdata,200);
   strCompress(kdata);

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

   file2 = zescape_quotes(curr_file);

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

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

   if (zd_metaview) meta_view(0);                                                //  update 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 = "copy meta";

   Plog(1,"m_meta_copy \n");

   viewmode('G');

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

***/

   zdialog *zd = zdialog_new("Copy Metadata",Mwin,"Apply"," X ",null);
   zdialog_add_widget(zd,"hbox","hbs","dialog",0,"expand|space=3");
   zdialog_add_widget(zd,"label","labs","hbs","source file:","space=3");
   zdialog_add_widget(zd,"zentry","sfile","hbs",0,"expand|space=3");
   zdialog_add_widget(zd,"button","sbrowse","hbs","Browse","space=3");
   zdialog_add_widget(zd,"hbox","hbt","dialog",0,"expand|space=3");
   zdialog_add_widget(zd,"label","labt","hbt","target file:","space=3");
   zdialog_add_widget(zd,"zentry","tfile","hbt",0,"expand|space=3");
   zdialog_add_widget(zd,"button","tbrowse","hbt","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,"file not found: %s",sfile);
      return 1;
   }

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

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

   Plog(1,"copy metadata from %s \n   to %s \n",sfile,tfile);
   err = meta_copy(sfile,tfile,0,0,0);                                           //  copy the data
   if (err) zmessageACK(Mwin,"metadata update error: %s",tfile);

   zdialog_free(zd);                                                             //  done
   return 1;
}


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

//  menu function
//  fix malformed metadata that prevents metadata edit functions from working

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

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

   F1_help_topic = "fix meta";

   Plog(1,"m_meta_fix \n");

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

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

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

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


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

//  manage tags function - auxiliary dialog

zdialog  *zdmanagetags = 0;

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

   GtkWidget   *widget;
   zdialog     *zd;

   F1_help_topic = "manage tags";

   Plog(1,"m_meta_manage_tags \n");

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

***/

   if (zdmanagetags) return;
   zd = zdialog_new("Manage Tags",Mwin,"OK",null);
   zdmanagetags = 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","labtag","hb7","tag","space=5");
   zdialog_add_widget(zd,"zentry","tag","hb7",0,"size=20|expand");
   zdialog_add_widget(zd,"label","space","hb7",0,"space=5");
   zdialog_add_widget(zd,"button","create","hb7","Create");
   zdialog_add_widget(zd,"button","delete","hb7","Delete","space=3");

   zdialog_add_widget(zd,"hbox","hb8","dialog");
   zdialog_add_widget(zd,"label","labdeftags","hb8","Defined Tags:","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","deftags","scrwin8",0,"wrap");

   widget = zdialog_gtkwidget(zd,"deftags");                                     //  deftags widget mouse/KB event func
   textwidget_set_eventfunc(widget,manage_deftags_clickfunc);

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

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

   return;
}


//  mouse click functions for widget having tags

void manage_deftags_clickfunc(GtkWidget *widget, int line, int pos, int kbkey)   //  tag or tag category was clicked
{
   ch     *txtag, end = 0;

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

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

   if (end == ':') zdialog_stuff(zdmanagetags,"catg",txtag);                     //  selected category >> dialog widget
   else zdialog_stuff(zdmanagetags,"tag",txtag);                                 //  selected tag >> dialog widget

   zfree(txtag);
   return;
}


//  dialog event and completion callback function

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

   ch          tag[tagXcc], catg[tagXcc];
   int         err, changed = 0;

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

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

   if (strmatch(event,"delete")) {                                               //  remove tag from defined tags
      zdialog_fetch(zd,"tag",tag,tagXcc);
      zdialog_fetch(zd,"catg",catg,tagXcc);
      if (*tag) {
         del_deftag(tag);
         changed++;
      }
      else if (*catg) {
         del_defcatg(catg);
         changed++;
      }
   }

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

   return 1;
}


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

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

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

   F1_help_topic = "captions";

   Plog(1,"m_meta_choose_caps \n");

   Zcapskeys = zlist_from_file(capskeys_file);                                   //  get current metadata keys

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

   select_meta_keys(Zcapskeys,maxcapkeys,0);                                     //  user edit key list

   zlist_to_file(Zcapskeys,capskeys_file);                                       //  save changes to file

   meta_show_caps(1);
   return;
}


//  show captions text on current image

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

   if (! curr_file) return;

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

   Zcapskeys = zlist_from_file(capskeys_file);                                   //  get current metadata keys

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

   Ncaps = zlist_count(Zcapskeys);                                               //  key count
   if (! Ncaps) return;

   for (ii = 0; ii < Ncaps; ii++) {                                              //  get metadata keys
      pp = zlist_get(Zcapskeys,ii);
      mkeys[ii] = zstrdup(pp,"capskeys");
   }

   meta_get1(curr_file,mkeys,metatext,Ncaps);                                    //  get metadata text for input keys

   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                           24.30

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


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

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

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

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


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

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

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


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

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

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

   F1_help_topic = "batch tags";

   Plog(1,"m_batch_tags \n");
   
   if (Findexvalid == 0) {
      zmessageACK(Mwin,"image index disabled");                                  //  no image index
      return;
   }

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

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

***/

   zd = zdialog_new("Batch Add/Remove Tags",Mwin,"Proceed"," X ",null);
   zd_batchtags = zd;

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

   //  (o) tags to add    [_______________________________________]
   //  (o) tags to remove [_______________________________________]
   zdialog_add_widget(zd,"hbox","hbtags","dialog",0,"space=3");
   zdialog_add_widget(zd,"vbox","vb1","hbtags",0,"space=3|homog");
   zdialog_add_widget(zd,"vbox","vb2","hbtags",0,"space=3|homog|expand");
   zdialog_add_widget(zd,"radio","radadd","vb1","tags to add");
   zdialog_add_widget(zd,"radio","raddel","vb1","tags to remove");
   zdialog_add_widget(zd,"text","addtags","vb2",0,"expand|wrap");
   zdialog_add_widget(zd,"text","deltags","vb2",0,"expand|wrap");

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

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

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

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

   //  Defined Tags Category [__________________________________|v]
   zdialog_add_widget(zd,"hbox","hbdt1","dialog");
   zdialog_add_widget(zd,"label","labdt","hbdt1","Defined Tags 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","deftags","swdt2",0,"wrap");

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

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

   *addtags = *deltags = 0;

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

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

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

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

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

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

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

   zdialog_free(zd);
   zd_batchtags = 0;

   if (zstat != 1) {                                                             //  cancel
      Fblock(0);
      return;
   }
   
   zdpop = popup_report_open("Batch Tags",Mwin,500,200,0,0,0,"X",0);             //  status report popup window

   Funcbusy(+1);                                                                 //  24.10
   
   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;

      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,0,"%s \n","no write permission");
         continue;
      }

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

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

      save_filemeta(file);                                                       //  save tag changes
   }

   Funcbusy(-1);

   if (! zdialog_valid(zdpop)) 
      Plog(0,"*** report canceled \n");

   else {
      popup_report_write2(zdpop,0,"\n *** COMPLETED \n");
      popup_report_bottom(zdpop);
   }

   Fblock(0);
   load_deftags(1);                                                              //  update defined tags list
   return;
}


//  mouse click functions for widgets holding tags

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

   ch     *txtag, end;

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

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

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

   zfree(txtag);
   return;
}


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

   ch     *txtag, end;

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

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

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

   zfree(txtag);
   return;
}


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

   ch     *txtag, end = 0;

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

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

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

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

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

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

   zfree(txtag);
   return;
}


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

   ch       *txtag, end;
   int      radadd;

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

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

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

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

   zfree(txtag);
   return;
}


//  batchTags dialog event function

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

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

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

   if (strmatch(event,"files"))                                                  //  select images to process
   {
      zdialog_show(zd,0);                                                        //  hide parent dialog
      select_files(0,1);                                                         //  get new list                          24.20
      zdialog_show(zd,1);
      snprintf(countmess,80,"%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 tag category selection
      zdialog_fetch(zd,"defcats",catgname,tagXcc);
      deftags_stuff(zd,catgname);
   }

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

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

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

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

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

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

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

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

      pp1 = matchtagstext;

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

      zdialog_stuff(zd,"matchtags",matchtagstext);                               //  stuff matchtags in dialog
      return 1;
   }

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

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

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

         if (ff) break;
      }

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

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

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

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

   return 1;
}


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

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

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


//  menu function

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

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

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

   F1_help_topic = "batch rename tags";

   Plog(1,"m_batch_rename_tags \n");

   if (! Findexvalid) {
      zmessageACK(Mwin,"image index disabled");                                  //  no image index
      return;
   }
   
   if (Fblock("batch rename tags")) return;

   Ntags = Nfiles = 0;
   filelist = 0;

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

***/

   zd = zdialog_new("Batch Rename Tags",Mwin,"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");

   //  tag [_________________]  rename to [___________________]  [-->]
   zdialog_add_widget(zd,"hbox","hbtags","vb1",0,"space=3");
   zdialog_add_widget(zd,"label","lab1","hbtags","Tag","space=3");
   zdialog_add_widget(zd,"frame","frot","hbtags");
   zdialog_add_widget(zd,"label","oldtag","frot","(click defined tag)");
   zdialog_add_widget(zd,"label","space","hbtags",0,"space=5");
   zdialog_add_widget(zd,"label","lab2","hbtags","Rename to","space=3");
   zdialog_add_widget(zd,"zentry","newtag","hbtags",0,"expand");
   zdialog_add_widget(zd,"label","space","hbtags",0,"space=3");
   zdialog_add_widget(zd,"button","addtags","hbtags",">>");

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

   //  Defined Tags Category [_____________________|v]
   zdialog_add_widget(zd,"hbox","hbdt","vb1",0);
   zdialog_add_widget(zd,"label","labdt","hbdt","Defined Tags 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","deftags","swdt",0,"wrap");

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

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

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

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

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

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

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

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

   Funcbusy(+1);                                                                 //  24.10

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

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

      ff = 0;

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

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

   Funcbusy(-1);

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

   Funcbusy(+1);                                                                 //  24.10

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

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

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

      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,0,"%s \n","no write permission");
         continue;
      }

      Nlist = 0;

      for (jj = 1; ; jj++) {                                                     //  loop file tags
         filetag = (ch *) substring(meta_tags,',',jj);
         if (! filetag) break;
         for (kk = 0; kk < Ntags; kk++) {                                        //  loop tag replacement list
            if (strmatchcase(filetag,oldtags[kk])) {                             //  file tag matches tag to replace
               oldtaglist[Nlist] = oldtags[kk];                                  //  save old and new tags
               newtaglist[Nlist] = newtags[kk];
               Nlist++;
               break;                                                            //  next file tag
            }
         }
      }

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

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

      save_filemeta(file);                                                       //  save tag changes
   }

   Funcbusy(-1);

   if (! zdialog_valid(zdpop))
      Plog(0,"*** report canceled \n");
   else
      popup_report_write2(zdpop,0," *** %s \n","COMPLETED");

   load_deftags(1);                                                              //  update tag list

cleanup:                                                                         //  free resources

   for (ii = 0; ii < Ntags; ii++) {
      zfree(oldtags[ii]);
      zfree(newtags[ii]);
   }

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

   Fblock(0);
   return;
}


//  a defined tag was clicked

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

   ch     *txtag, end;
   ch     tagname[tagXcc];

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

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

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

   zfree(txtag);
   return;
}


//  a tag list line was clicked

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

   int      ii;

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

   if (line >= Ntags) return;

   for (ii = line; ii < Ntags-1; ii++) {                                         //  remove tags pair corresponding
      oldtags[ii] = oldtags[ii+1];                                               //    to the line clicked
      newtags[ii] = newtags[ii+1];
   }
   Ntags--;

   widget = zdialog_gtkwidget(zd,"taglist");                                     //  rewrite dialog tag list
   textwidget_clear(widget);
   for (int ii = 0; ii < Ntags; ii++)
      textwidget_append2(widget,0,"%s >> %s\n",oldtags[ii],newtags[ii]);

   return;
}


//  batch rename tags dialog event function

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

   ch          catgname[tagXcc];
   ch          oldtag[tagXcc], newtag[tagXcc];
   GtkWidget   *widget;

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

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

   if (strmatch(event,"addtags")) {                                              //  [ --> ] button pressed
      zdialog_fetch(zd,"oldtag",oldtag,tagXcc);                                  //  save new pair of tag names
      zdialog_fetch(zd,"newtag",newtag,tagXcc);
      strTrim2(oldtag);
      strTrim2(newtag);
      if (*oldtag <= ' ' || *newtag <= ' ') return 1;
      if (Ntags == 100) {
         zmessageACK(Mwin,"max tags exceeded");
         return 1;
      }
      oldtags[Ntags] = zstrdup(oldtag,"batch-rename-tags");
      newtags[Ntags] = zstrdup(newtag,"batch-rename-tags");
      Ntags++;
   }

   widget = zdialog_gtkwidget(zd,"taglist");                                     //  rewrite dialog tag list
   textwidget_clear(widget);
   for (int ii = 0; ii < Ntags; ii++)
      textwidget_append2(widget,0,"%s >> %s\n",oldtags[ii],newtags[ii]);

   return 1;
}


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

//  batch change or shift photo date/time

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

   ch          *kname[1] = { "DateTimeOriginal" };
   ch          *kvalue[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 = "batch photo date";

   Plog(1,"m_batch_photo_date_time \n");
   
   if (Findexvalid == 0) {
      zmessageACK(Mwin,"image index disabled");                                  //  no image index
      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("Batch Photo Date/Time",Mwin,"Proceed"," X ",null);

   zdialog_add_widget(zd,"hbox","hbfiles","dialog",0,"space=3");
   zdialog_add_widget(zd,"button","files","hbfiles","Select Files","space=5");
   zdialog_add_widget(zd,"label","labcount","hbfiles","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","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","shift existing date/time (+/–):","space=3");

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

   zdialog_add_widget(zd,"hbox","hbshift3","dialog");
   zdialog_add_widget(zd,"label","space","hbshift3",0,"space=10");
   zdialog_add_widget(zd,"label","labhours","hbshift3","hours","space=5");
   zdialog_add_widget(zd,"zspin","s_hours","hbshift3","-23|+23|1|0");
   zdialog_add_widget(zd,"label","space","hbshift3",0,"space=5");
   zdialog_add_widget(zd,"label","labmins","hbshift3","minutes","space=5");
   zdialog_add_widget(zd,"zspin","s_mins","hbshift3","-59|+59|1|0");
   zdialog_add_widget(zd,"label","space","hbshift3",0,"space=5");
   zdialog_add_widget(zd,"label","labsecs","hbshift3","seconds","space=5");
   zdialog_add_widget(zd,"zspin","s_secs","hbshift3","-59|+59|1|0");

   zdialog_add_widget(zd,"hsep","sep1","dialog",0,"space=5");
   zdialog_add_widget(zd,"hbox","hbtest","dialog",0,"space=5");
   zdialog_add_widget(zd,"check","Ftest","hbtest","test: show changes, do not update files","space=3");

   zdialog_load_inputs(zd);

   snprintf(text,100,"%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,"please make a choice");
      goto retry;
   }

   if (SFcount == 0) {
      zmessageACK(Mwin,"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,"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,"invalid date/time format");
         goto retry;
      }

      DTnew.tm_mon -= 1;                                                         //  mktime month is 0-11

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

   if (Fshift)
   {
      zdialog_fetch(zd,"s_years",s_years);                                       //  inputs are shifted date/time values
      zdialog_fetch(zd,"s_mons",s_mons);
      zdialog_fetch(zd,"s_mdays",s_mdays);
      zdialog_fetch(zd,"s_hours",s_hours);
      zdialog_fetch(zd,"s_mins",s_mins);
      zdialog_fetch(zd,"s_secs",s_secs);
   }

   zdialog_free(zd);

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

   if (Fshift) {
      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);
   }

   Funcbusy(+1);                                                                 //  24.10

   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 \n","no write permission");
         continue;
      }

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

      if (kvalue[0]) {
         strncpy0(olddatetime,kvalue[0],20);                                     //  yyyy-mm-dd hh:mm:ss
         zfree(kvalue[0]);
      }
      else strcpy(olddatetime,"0000-01-01 00:00:00");                            //  missing old date/time

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

      if (nn < 6) DTold.tm_sec = 0;                                              //  fix truncated date-time               24.40
      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           24.40
         popup_report_write2(zdpop,0,"  *** metadata 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;
      }

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

      DTnew = *localtime(&timep);

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

      olddatetime[4] = olddatetime[7] = newdatetime[4] = newdatetime[7] = '-';   //  format: yyyy-mm-dd
      popup_report_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
      kvalue[0] = (ch *) &newdatetime;
      err = meta_put(curr_file,(ch **) kname,kvalue,1);                          //  yyyy-mm-dd hh:mm:ss >> metadata
      if (err) {
         popup_report_write2(zdpop,0," *** metadata update error \n");
         continue;
      }

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

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

   Funcbusy(-1);

   if (! zdialog_valid(zdpop))
      Plog(0,"*** report canceled \n");
   else
      popup_report_write2(zdpop,0," *** %s \n","COMPLETED");

   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,1);                                                         //  get new file list                     24.20
      zdialog_show(zd,1);

      snprintf(countmess,80,"%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);
   void batch_change_meta_clickfunc(GtkWidget *, int line, int pos, int kbkey);

   int            ii, jj, err, zstat, nkeys;
   ch             knameN[8] = "knameN", kvalN[8] = "kvalN";
   ch             kname[metakeyXcc], kval[metadataXcc];
   ch             *pp1[10], *pp2[10];
   ch             *file, text[100];
   GtkWidget      *mtext;
   static int     nx, ftf = 1;
   static ch      **itemlist;
   zdialog        *zdpop;

   F1_help_topic = "batch change meta";

   Plog(1,"m_batch_change_meta \n");

   if (Findexvalid == 0) {
      zmessageACK(Mwin,"image index disabled");                                  //  no image index
      return;
   }
   
   if (Fblock("batch change meta")) return;

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

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

**/

   zd = zdialog_new("Batch Add/Change Metadata",Mwin,"Full List","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","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","Select Files","space=5");
   zdialog_add_widget(zd,"label","labcount","hbfiles","no files selected","space=10");

   zdialog_add_widget(zd,"hbox","hbkeys","vb2",0,"space=5");
   zdialog_add_widget(zd,"vbox","vbname","hbkeys");
   zdialog_add_widget(zd,"vbox","vbval","hbkeys",0,"expand");
   zdialog_add_widget(zd,"label","labkey","vbname","key name");
   zdialog_add_widget(zd,"label","labdata","vbval","key value");
   zdialog_add_widget(zd,"zentry","kname0","vbname",0,"size=20");
   zdialog_add_widget(zd,"zentry","kname1","vbname",0,"size=20");
   zdialog_add_widget(zd,"zentry","kname2","vbname",0,"size=20");
   zdialog_add_widget(zd,"zentry","kname3","vbname",0,"size=20");
   zdialog_add_widget(zd,"zentry","kname4","vbname",0,"size=20");
   zdialog_add_widget(zd,"zentry","kname5","vbname",0,"size=20");
   zdialog_add_widget(zd,"zentry","kname6","vbname",0,"size=20");
   zdialog_add_widget(zd,"zentry","kname7","vbname",0,"size=20");
   zdialog_add_widget(zd,"zentry","kname8","vbname",0,"size=20");
   zdialog_add_widget(zd,"zentry","kname9","vbname",0,"size=20");
   zdialog_add_widget(zd,"zentry","kval0","vbval",0,"size=20|expand");
   zdialog_add_widget(zd,"zentry","kval1","vbval",0,"size=20|expand");
   zdialog_add_widget(zd,"zentry","kval2","vbval",0,"size=20|expand");
   zdialog_add_widget(zd,"zentry","kval3","vbval",0,"size=20|expand");
   zdialog_add_widget(zd,"zentry","kval4","vbval",0,"size=20|expand");
   zdialog_add_widget(zd,"zentry","kval5","vbval",0,"size=20|expand");
   zdialog_add_widget(zd,"zentry","kval6","vbval",0,"size=20|expand");
   zdialog_add_widget(zd,"zentry","kval7","vbval",0,"size=20|expand");
   zdialog_add_widget(zd,"zentry","kval8","vbval",0,"size=20|expand");
   zdialog_add_widget(zd,"zentry","kval9","vbval",0,"size=20|expand");

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

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

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

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

   nkeys = 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) goto cleanup;                                                 //  not [apply]

   for (ii = jj = 0; ii < 10; ii++)
   {
      knameN[5] = '0' + ii;
      kvalN[4] = '0' + ii;
      zdialog_fetch(zd,knameN,kname,metakeyXcc);
      zdialog_fetch(zd,kvalN,kval,metadataXcc);
      strCompress(kname);
      if (*kname <= ' ') continue;
      pp1[jj] = zstrdup(kname,"batch-metadata");
      pp2[jj] = zstrdup(kval,"batch-metadata");
      jj++;
   }
   nkeys = jj;

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

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

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

   for (ii = 0; ii < nkeys; ii++)
   {
      if (*pp2[ii]) popup_report_write2(zdpop,0,"%s = %s \n",pp1[ii],pp2[ii]);
      else popup_report_write2(zdpop,0,"%s = DELETED \n",pp1[ii]);
   }

   ii = zdialog_choose(Mwin,"parent","Proceed","Proceed"," X ",null);
   if (ii != 1) {
      zd->zstat = 0;                                                             //  cancel
      popup_report_close(zdpop,0);
      goto retry;
   }

   zdialog_free(zd);
   zd = 0;

   Funcbusy(+1);                                                                 //  24.10

   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;

      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," *** no write permission \n");
         continue;
      }

      err = meta_put(curr_file,pp1,pp2,nkeys);                                   //  change metadata in image file
      if (err) {
         popup_report_write2(zdpop,1," *** metadata update error \n");
         continue;
      }


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

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

   Funcbusy(-1);

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

cleanup:

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

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

   nkeys = 0;
   
   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/key names");
      return 1;
   }

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

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

   return 1;
}


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

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

   int      ii;
   ch       *pp;
   ch       knameX[8] = "knameX";
   ch       kname[metakeyXcc];

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

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

   for (ii = 0; ii < 10; ii++) {                                                 //  find 1st empty dialog key name
      knameX[5] = '0' + ii;
      zdialog_fetch(zd,knameX,kname,metakeyXcc);
      if (*kname <= ' ') break;
   }

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


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

//  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          **itemlist, *knamex[brmx], *kvalx[brmx];

   F1_help_topic = "batch report meta";

   Plog(1,"m_batch_report_meta \n");
   
   if (Findexvalid == 0) {
      zmessageACK(Mwin,"image index disabled");                                  //  no image index
      return;
   }

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

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

***/

   zd = zdialog_new("Batch Report Metadata",Mwin,"Proceed"," X ",null);
   zdialog_add_widget(zd,"hbox","hbfiles","dialog",0,"space=3");
   zdialog_add_widget(zd,"button","files","hbfiles","Select Files","space=5");
   zdialog_add_widget(zd,"label","labcount","hbfiles","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","list of reported metadata items","space=10");

   snprintf(text,100,"%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,"no files selected");
      Fblock(0);
      return;
   }

   brm = zreadfile(meta_report_items_file,itemlist);
   if (brm > brmx) brm = brmx;
   for (ii = 0; ii < brm; ii++)
      knamex[ii] = itemlist[ii];

   if (itemlist) zfree(itemlist);

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

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

   Funcbusy(+1);                                                                 //  24.10

   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,0,"*** invalid file \n");
         continue;
      }

      err = meta_get1(file,knamex,kvalx,brm);                                    //  get all report items
      if (err) continue;

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

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

   Funcbusy(-1);

   if (! zdialog_valid(zdpop)) 
      Plog(0,"*** report canceled \n");
   else
      popup_report_write2(zdpop,0," *** %s \n","COMPLETED");

   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,1);                                                         //  get new list                          24.20
      zdialog_show(zd,1);

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

   if (strmatch(event,"edit"))                                                   //  select metadata items to report
   {
      mlist = zlist_from_file(meta_report_items_file);                           //  load metadata report list
      if (! mlist) mlist = zlist_new(0);
      nn = select_meta_keys(mlist,maxbatchkeys,0);                               //  user edit of metadata list
      if (nn) zlist_to_file(mlist,meta_report_items_file);                       //  replace file
      zlist_free(mlist);
   }

   return 1;
}


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

//  move metadata from selected input keys to a selected output key.
//  purpose: consolidate multiple overlapping/redundant keys into a chosen key.
//           (compensate metadata standards chaos)


namespace batch_meta_mover_names
{
   int      Nfiles, Fallfiles, Freportonly;
   ch       inputkeys[200], outputkey[50];
   ch       *inkeys[10], *outkey[1], *outkdata[1];
   ch       *inkdata[10];
   zdialog  *zd, *zdpop;
}


//  menu function

void m_batch_meta_mover(GtkWidget *, ch *menu)
{
   using namespace batch_meta_mover_names;

   int  batch_meta_mover_dialog_event(zdialog *zd, ch *event);

   ch       *title = "Metadata Mover";
   ch       *text1 = "Move input keys to output key \n"
                     "Input keys may include output key \n"
                     "Output data order is input key order.";
   ch       *text2 = "Report only - make no data changes.";
   ch       outputkdata[2000];
   ch       *file, *pp;
   int      zstat, yn, Fvalid, err;
   int      Nkeys, cc, ii, iimax, jj;

   F1_help_topic = "batch move meta";

   Plog(1,"m_batch_meta_mover");
   
   if (Findexvalid == 0) {
      zmessageACK(Mwin,"image index disabled");                                  //  no image index
      return;
   }

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

/***
          ____________________________________________
         |             Metadata Mover                 |
         |                                            |
         | Press F1 for help                          |
         |                                            |
         | [Select Files]  NN files selected          |        selectfiles    Nfiles
         | [x] Select all Fotocx files                |        Fallfiles
         |                                            |
         | Input keys (comma separated list):         |
         | [________________________________________] |        inputkeys[200]
         |                                            |
         | Output key: [_____________________]        |        outputkey[50]  outputkdata[2000]
         |                                            |
         | Move input keys to output key.             |
         | Input keys may include output key.         |
         | Output data order is input key order.      |
         |                                            |
         | [x] Report only - make no data changes     |        Freportonly
         |                                            |
         |                              [Proceed] [X] |
         |____________________________________________|

***/

   zd = zdialog_new(title,Mwin,"Proceed"," X ",null);
   zdialog_add_widget(zd,"label","labhelp1","dialog","Press F1 for help","space=5");

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

   zdialog_add_widget(zd,"hbox","hballfiles","dialog");
   zdialog_add_widget(zd,"check","Fallfiles","hballfiles","Select all Fotocx files");

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

   zdialog_add_widget(zd,"hbox","hbin1","dialog");
   zdialog_add_widget(zd,"label","labin","hbin1","Input keys (comma separated list)","space=3");
   zdialog_add_widget(zd,"hbox","hbin2","dialog");
   zdialog_add_widget(zd,"zentry","inputkeys","hbin2",0,"space=3|expand");

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

   zdialog_add_widget(zd,"hbox","hbout","dialog");
   zdialog_add_widget(zd,"label","labout","hbout","Output key:","space=3");
   zdialog_add_widget(zd,"zentry","outputkey","hbout",0,"space=3|expand");

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

   zdialog_add_widget(zd,"hbox","hbact1","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","space","hbact1",0,"space=10");
   zdialog_add_widget(zd,"label","text1","hbact1",text1);

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

   zdialog_add_widget(zd,"hbox","hbact2","dialog");
   zdialog_add_widget(zd,"check","Freportonly","hbact2",text2,"space=3");

   zdialog_load_inputs(zd);
   zdialog_stuff(zd,"Freportonly",1);

   zstat = zdialog_run(zd,batch_meta_mover_dialog_event,"save");

retry:

   zd->zstat = 0;

   zstat = zdialog_wait(zd);
   if (zstat != 1) {
      zdialog_free(zd);
      goto report_end;
   }

   if (zdpop) popup_report_close(zdpop,0);
   zmainsleep(0.5);

   zdpop = popup_report_open("batch metadata move",Mwin,800,600,0,0,0,"X",0);    //  log report

   Fvalid = 1;

   if (Fallfiles)
      popup_report_write(zdpop,0,"all files will be processed \n");
   else if (Nfiles)
      popup_report_write(zdpop,0,"%d files will be processed \n",Nfiles);
   else {
      popup_report_write(zdpop,1,"*** no files are selected \n");
      Fvalid = 0;
   }

   popup_report_write(zdpop,0,"input keys: %s \n",inputkeys);
   popup_report_write(zdpop,0,"output key: %s \n",outputkey);

   for (ii = 0; ii < 10; ii++) {                                                 //  parse input key list
      pp = substring(inputkeys,",",ii+1);
      if (! pp) break;
      inkeys[ii] = zstrdup(pp,"meta-mover");
      if (strlen(pp) < 2) {
         popup_report_write(zdpop,1,"*** input key %s is invalid \n",pp);
         Fvalid = 0;
      }
   }

   Nkeys = ii;
   if (Nkeys < 1) {
      popup_report_write(zdpop,1,"*** input keys are missing \n");
      Fvalid = 0;
   }

   if (strlen(outputkey) < 2) {
      popup_report_write(zdpop,1,"*** output key is missing \n");
      Fvalid = 0;
   }

   if (! Fvalid) goto retry;

   yn = zmessageYN(Mwin,"continue?");
   if (! yn) goto retry;

   zdialog_free(zd);

   for (ii = 0; ii < Nkeys; ii++)                                                //  initz. no input key metadata
      inkdata[ii] = 0;

   iimax = 0;                                                                    //  set up file loop
   if (Nfiles) iimax = Nfiles;
   if (Fallfiles) iimax = Nxxrec;

   Funcbusy(+1);                                                                 //  24.10

   for (ii = 0; ii < iimax; ii++)                                                //  loop all files
   {
      zmainloop();

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

      file = 0;
      if (Nfiles) file = SelFiles[ii];                                           //  selected files loop
      if (Fallfiles) file = xxrec_tab[ii]->file;                                 //  all files loop

      popup_report_write(zdpop,0,"------------------------- \n");
      popup_report_write(zdpop,0,"file: %s \n",file);                            //  log file name

      err = meta_get1(file,inkeys,inkdata,Nkeys);                                //  get input keys metadata
      if (err) continue;

      for (jj = 0; jj < Nkeys; jj++)                                             //  log input keys having data
         if (inkdata[jj])
            popup_report_write(zdpop,0,"input key: %s  data: %s \n",
                                           inkeys[jj], inkdata[jj]);
      *outputkdata = 0;

      for (jj = 0; jj < Nkeys; jj++) {
         if (inkdata[jj])                                                        //  catenate input data with "\n" delims
            strncatv(outputkdata,2000,inkdata[jj],"\\n",0);                      //  (real EOL not allowed in metadata)
      }
      cc = strlen(outputkdata);                                                  //  catenated input keys data
      if (cc) {
         cc -= 2;
         outputkdata[cc] = 0;                                                    //  remove last "\n"
      }

      outkey[0] = outputkey;                                                     //  set output key metadata from inputs
      outkdata[0] = outputkdata;
      if (cc == 0) outkdata[0] = 0;                                              //  no inputs --> erase output

      popup_report_write2(zdpop,0,"output key: %s  data: %s \n",                 //  log output key and data
                                   outputkey, outputkdata);
      if (! Freportonly) {                                                       //  set output key data
         err = meta_put(file,outkey,outkdata,1);
         if (err) popup_report_write2(zdpop,1," *** metadata update error \n");
      }

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

   Funcbusy(-1);

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

report_end:

   Fblock(0);
   return;
}


//  dialog event and completion callback function

int  batch_meta_mover_dialog_event(zdialog *zd, ch *event)
{
   using namespace batch_meta_mover_names;

   ch     countmess[80];

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

      Nfiles = SFcount;
      snprintf(countmess,80,"%d image files selected",Nfiles);                   //  show selected file count in dialog
      zdialog_stuff(zd,"labcount",countmess);
      zdialog_stuff(zd,"Fallfiles",0);                                           //  clear 'select all files'
   }

   if (strmatch(event,"Fallfiles")) {                                            //  select all files
      zdialog_fetch(zd,"Fallfiles",Fallfiles);
      if (Fallfiles) {
         zdialog_stuff(zd,"labcount","no files selected");                       //  show 'no files selected' in dialog
         Nfiles = 0;
      }
   }

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

   if (zd->zstat == 1)                                                           //  [proceed]
   {
      zdialog_fetch(zd,"Fallfiles",Fallfiles);                                   //  save all dialog data
      zdialog_fetch(zd,"inputkeys",inputkeys,200);
      zdialog_fetch(zd,"outputkey",outputkey,50);
      zdialog_fetch(zd,"Freportonly",Freportonly);
   }

   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[100], country[100];
   ch          lati[20], longi[20], text[100];
   zdialog     *zd, *zdpop;

   F1_help_topic = "batch geotags";

   Plog(1,"m_batch_geotags \n");

   if (! Findexvalid) {
      zmessageACK(Mwin,"image index disabled");                                  //  no image index
      return;
   }
   
   if (Fblock("batch geotags")) return;

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

/***
       _____________________________________________________
      |                Batch Set Geotags                    |
      |                                                     |
      | [select files]  NN files selected                   |
      | location [______________]  country [______________] |
      | latitude [_______] longitude [_______]              |
      |                                                     |
      |               [Find] [Lookup] [Clear] [Proceed] [X] |
      |_____________________________________________________|

***/

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

   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=3");
   zdialog_add_widget(zd,"button","files","hb1","Select Files","space=10");
   zdialog_add_widget(zd,"label","labcount","hb1","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","lablat","hb3","Latitude","space=3");
   zdialog_add_widget(zd,"zentry","lati","hb3",0,"size=10");
   zdialog_add_widget(zd,"label","space","hb3",0,"space=5");
   zdialog_add_widget(zd,"label","lablong","hb3","Longitude","space=3");
   zdialog_add_widget(zd,"zentry","longi","hb3",0,"size=10");
   zdialog_add_widget(zd,"hbox","hbmq","dialog",0,"space=3");

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

   snprintf(text,100,"%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_geolocs(zd);                                                              //  update geolocs table

   zdialog_fetch(zd,"location",location,100);                                    //  get location from dialog
   zdialog_fetch(zd,"country",country,100);
   zdialog_fetch(zd,"lati",lati,20);                                             //  and latitude, longitude
   zdialog_fetch(zd,"longi",longi,20);

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

   if (SFcount == 0) goto cleanup;

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

   Funcbusy(+1);                                                                 //  24.10

   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,0,"%s: %s \n","no write permission",file);
         continue;
      }

      *meta_city = *meta_country = *meta_lati = *meta_longi = 0;

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

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

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

   Funcbusy(-1);

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

cleanup:

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

   Fblock(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[100], country[100];
   ch       lati[20], longi[20];
   float    flati, flongi;
   
   if (strmatch(event,"files"))                                                  //  select images to add tags
   {
      zdialog_show(zd,0);                                                        //  hide parent dialog
      select_files(0,1);                                                         //  get file list from user               24.20
      zdialog_show(zd,1);

      snprintf(countmess,80,"%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_imagelocs(zd);                                                        //  search image location data
      return 1;
   }

   else if (zstat == 2)                                                          //  [lookup]
   {
      find_worldlocs(zd);                                                        //  search cities geolocations table
      return 1;
   }

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

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

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

      if (*country && ! strmatch(country,"null")) {
         *country = toupper(*country);
         zdialog_stuff(zd,"country",country);
      }

      zdialog_fetch(zd,"lati",lati,20);                                          //  get latitude, longitude
      zdialog_fetch(zd,"longi",longi,20);
      strTrim2(lati);
      strTrim2(longi);

      if (*lati || *longi) {                                                     //  if coordinates present, validate
         err = validate_latlong(lati,longi,flati,flongi);
         if (err) goto badcoord;
      }

      if (! SFcount) goto nofiles;

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

      zd->zstat = 4;
      return 1;
   }

   zdialog_free(zd);                                                             //  canceled
   return 1;

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

nofiles:
   zmessageACK(Mwin,"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 locs_names
{
   struct grec_t  {                                                              //  image geotags data
      ch          *location, *country;                                           //  group location
      ch          pdate[12];                                                     //  nominal group date, yyyymmdd
      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      locs_groupby, locs_daterange;
   int      Fusesearch, Nsearch;
   int      pline;

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


//  menu function

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

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

   F1_help_topic = "places/dates";

   Plog(1,"m_meta_places_dates \n");

   if (! Findexvalid) {
      zmessageACK(Mwin,"image index disabled");                                  //  no image index
      return;
   }

   if (Findexvalid == 1) zmessage_post(Mwin,"20/10",2,"image index not updated");

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

***/

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

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

   zdialog_stuff(zd,"country",0);
   zdialog_stuff(zd,"location",1);                                               //  default by location
   zdialog_stuff(zd,"date",0);
   zdialog_stuff(zd,"date2",0);

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

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

   zdialog_fetch(zd,"country",iix);
   if (iix) locs_groupby = 1;                                                    //  group country
   zdialog_fetch(zd,"location",iix);
   if (iix) locs_groupby = 2;                                                    //  group country/location
   zdialog_fetch(zd,"date",iix);
   if (iix) locs_groupby = 3;                                                    //  group country/location/date-range
   zdialog_fetch(zd,"date2",iix);
   if (iix) locs_groupby = 4;                                                    //  group date-range/country/location

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

   zdialog_free(zd);

   if (Ngrec) {                                                                  //  free prior memory
      for (iix = 0; iix < Ngrec; iix++) {
         if (grec[iix].location) zfree(grec[iix].location);
         if (grec[iix].country) zfree(grec[iix].country);
      }
      zfree(grec);
      Ngrec = 0;
   }

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

   if (! Nxxrec) {
      zmessageACK(Mwin,"no geotags data 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,"no search results 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;

         grec[jj].location = zstrdup(xxrec->location,"meta-places");             //  get location and country
         grec[jj].country = zstrdup(xxrec->country,"meta-places");
         strncpy0(grec[jj].pdate,xxrec->pdate,9);                                //  photo date, truncate to yyyymmdd
         if (grec[jj].pdate[0] == 0) strcpy(grec[jj].pdate,"null");
         grec[jj].lodate = locs_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 = zstrdup(xxrec->location,"meta-places");             //  get location and country
         grec[ii].country = zstrdup(xxrec->country,"meta-places");
         strncpy0(grec[ii].pdate,xxrec->pdate,9);                                //  photo date, truncate to yyyymmdd
         if (grec[ii].pdate[0] == 0) strcpy(grec[ii].pdate,"null");
         grec[ii].lodate = locs_getdays(xxrec->pdate);                           //  days since 0 CE
         grec[ii].hidate = grec[ii].lodate;
      }

      Ngrec = Nxxrec;
   }

   if (Ngrec > 1)                                                                //  sort grecs by country/location/date
      HeapSort((ch *) grec, sizeof(grec_t), Ngrec, locs_comp);

   iig = 0;                                                                      //  1st group from grec[0]
   grec[iig].count = 1;                                                          //  group count = 1

   for (iix = 1; iix < Ngrec; iix++)                                             //  scan following grecs
   {
      newgroup = 0;

      if (! strmatch(grec[iix].country,grec[iig].country))
         newgroup = 1;                                                           //  new country >> new group

      if (locs_groupby >= 2)                                                     //  new location >> new group
         if (! strmatch(grec[iix].location,grec[iig].location)) newgroup = 1;    //    if group by location

      if (locs_groupby >= 3)
         if (grec[iix].lodate - grec[iig].hidate > locs_daterange)               //  new date >> new group if group by date
            newgroup = 1;                                                        //    and date out of range

      if (newgroup)
      {
         iig++;                                                                  //  new group
         if (iix > iig) {
            grec[iig] = grec[iix];                                               //  copy and pack down
            grec[iix].location = grec[iix].country = 0;                          //  no zfree()
         }
         grec[iig].count = 1;                                                    //  group count = 1
      }
      else
      {
         zfree(grec[iix].location);                                              //  same group
         zfree(grec[iix].country);                                               //  free memory
         grec[iix].location = grec[iix].country = 0;
         grec[iig].hidate = grec[iix].lodate;                                    //  expand group date-range
         grec[iig].count++;                                                      //  increment group count
      }
   }

   Ngrec = iig + 1;                                                              //  unique groups count

   if (locs_groupby == 1) ww = 350;                                              //  group country
   if (locs_groupby == 2) ww = 600;                                              //  group country/location
   if (locs_groupby == 3) ww = 650;                                              //  group country/location/date-range
   if (locs_groupby == 4) ww = 650;                                              //  group date-range/country/location

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

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

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

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

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

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

      for (iig = 0; iig < Ngrec; iig++)
      {
         utf8substring(country,grec[iig].country,0,26);                          //  get graphic cc for UTF-8 names
         cc1 = 26 + strlen(country) - utf8len(country);
         utf8substring(location,grec[iig].location,0,26);
         cc2 = 26 + strlen(location) - utf8len(location);

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

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

   if (locs_groupby == 4)                                                        //  group by date-range/country/location
   {
      if (Ngrec > 1)                                                             //  re-sort by date/country/location
         HeapSort((ch *) grec, sizeof(grec_t), Ngrec, locs_comp2);

      popup_report_header(zdpop,1,"%-10s  %-26s  %-26s   %5s","Date","Country","Location","Count");

      for (iig = 0; iig < Ngrec; iig++)
      {
         utf8substring(country,grec[iig].country,0,26);                          //  get graphic cc for UTF-8 names
         cc1 = 26 + strlen(country) - utf8len(country);
         utf8substring(location,grec[iig].location,0,26);
         cc2 = 26 + strlen(location) - utf8len(location);

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

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

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


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

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

   int      ii;

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

   ch   * loc1 = ((grec_t *) rec1)->location;                                    //  compare locations
   ch   * loc2 = ((grec_t *) rec2)->location;
   ii = strcmp(loc1,loc2);
   if (ii) return ii;

   int date1 = ((grec_t *) rec1)->lodate;                                        //  compare dates
   int date2 = ((grec_t *) rec2)->lodate;
   ii = date1 - date2;
   return ii;
}


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

int locs_names::locs_comp2(ch *rec1, ch *rec2)
{
   using namespace locs_names;

   int      ii;

   int date1 = ((grec_t *) rec1)->lodate;                                        //  compare dates
   int date2 = ((grec_t *) rec2)->lodate;
   ii = date1 - date2;
   if (ii) return ii;

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

   ch   * loc1 = ((grec_t *) rec1)->location;                                    //  compare locations
   ch   * loc2 = ((grec_t *) rec2)->location;
   ii = strcmp(loc1,loc2);
   return ii;
}


//  convert yyyymmdd date into days from 0001 C.E.

int locs_names::locs_getdays(ch *pdate)
{
   using namespace locs_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 (strmatch(pdate,"null")) return 0;

   year = month = day = 0;

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

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

   strncpy0(temp,pdate+6,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

void locs_names::locs_clickfunc(GtkWidget *widget, int line, int pos, int kbkey)
{
   using namespace locs_names;

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

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

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

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

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

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

   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;

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

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

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

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

   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 (locs_groupby >= 2)
            if (! strmatch(xxrec->location,location)) continue;                  //  no location match

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

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

   fclose(fid);

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

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


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

//  Produce a report of image counts by year and month.
//  Click on a report line to get a thumbnail gallery of images.

namespace timeline_names
{
   int      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 };
}


//  menu function

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

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

   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;
   zdialog     *zdpop;

   F1_help_topic = "timeline";

   Plog(1,"m_meta_timeline \n");

   if (! Findexvalid) {
      zmessageACK(Mwin,"image index disabled");                                  //  no image index
      return;
   }

   if (Findexvalid == 1) zmessage_post(Mwin,"20/10",2,"image index not updated");

   if (Nsearch) zlist_free(filelist);                                            //  free prior memory
   Nsearch = 0;

   ii = zdialog_choose(Mwin,"mouse","Include:",                                  //  query user
          "all images", "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,"no search results found");
         return;
      }

      for (ii = jj = 0; ii < Nsearch; ii++)
      {
         xxrec = get_xxrec(zlist_get(filelist,ii));
         if (! xxrec) continue;

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

         if (strmatch(pdate,"")) {                                               //  if missing, count
            ++Mcount;
            continue;
         }

         jj = atoi(pdate);                                                       //  photo date, 0 to 209912
         yy = jj / 100;                                                          //  year, 0000 to 2099
         mm = jj - yy * 100;                                                     //  month, 1 to 12
         if (yy < 0 || yy >= Nyears || mm < 1 || mm > 12) {
            ++Ecount;                                                            //  invalid, add to error count
            continue;
         }

         ++Ycount[yy];                                                           //  add to year totals
         pp = yy * 12 + mm - 1;                                                  //  add to period totals
         ++Pcount[pp];
      }
   }

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

         xxrec = xxrec_tab[ii];

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

         if (strmatch(pdate,"")) {                                               //  if missing, count
            ++Mcount;
            continue;
         }

         jj = atoi(pdate);                                                       //  photo date, 0 to 209912
         yy = jj / 100;                                                          //  year, 0000 to 2099
         mm = jj - yy * 100;                                                     //  month, 1 to 12
         if (yy < 0 || yy >= Nyears || mm < 1 || mm > 12) {
            ++Ecount;                                                            //  invalid, add to error count
            continue;
         }

         ++Ycount[yy];                                                           //  add to year totals
         pp = yy * 12 + mm - 1;                                                  //  add to period totals
         ++Pcount[pp];
      }
   }

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

   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

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

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

   if (busy) return;                                                             //  stop re-entry
   busy++;

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

   if (line == -1)                                                               //  arrow key navigation
   {
      for (ii = 0; ii < 14; ii++)                                                //  current report column
         if (ppos == colpos[ii]) break;

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

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

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

      line = pline;
      pos = ppos;
   }

   if (line < 0) line = 0;
   if (line > Nyears2) line = Nyears2;
   if (pos < 0) pos = 0;

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

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

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

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

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

   cc = 0;

   if (strmatchN(txline,"null",4)) Fnull = 1;                                    //  find images with null date

   else if (strmatchN(txline,"invalid",7)) Finvalid = 1;                         //  find images with invalid date

   else if (pos < 13) {                                                          //  clicked on year or year count
      strncpy0(pdate,txline,5);                                                  //  have "yyyy"
      cc = 4;
   }

   else                                                                          //  month was clicked
   {
      mm = (pos - 13) / 6 + 1;                                                   //  month, 1-12
      if (mm < 1 || mm > 12) goto retx;
      strncpy(pdate,txline,4);                                                   //  "yyyy"
      pdate[4] = '0' + mm/10;
      pdate[5] = '0' + mm % 10;                                                  //  have "yyyymm"
      pdate[6] = 0;
      cc = 6;
   }

   snprintf(albumfile,200,"%s/timeline",albums_folder);
   fid = fopen(albumfile,"w");                                                   //  open output file
   if (! fid) {
      zmessageACK(Mwin,"file error: %s",strerror(errno));
      goto retx;
   }

   if (Fusesearch)                                                               //  include prior search results
   {
      for (ii = 0; ii < Nsearch; ii++)
      {
         zmainloop();                                                            //  keep GTK alive

         xxrec = get_xxrec(zlist_get(filelist,ii));
         if (! xxrec) continue;

         if (Fnull) {                                                            //  search for missing dates
            if (strmatch(xxrec->pdate,"")) {
               fprintf(fid,"%s\n",xxrec->file);
               continue;
            }
         }

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

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

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

         xxrec = xxrec_tab[ii];

         if (Fnull) {                                                            //  search for missing dates
            if (strmatch(xxrec->pdate,"")) {
               fprintf(fid,"%s\n",xxrec->file);
               continue;
            }
         }

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

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

   fclose(fid);

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

retx:
   busy = 0;
   return;
}


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

//  Search image tags, 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       searchDateFrom[20] = "";                                             //  search images
   ch       searchDateTo[20] = "";                                               //  format is "yyyy-mm-dd hh:mm"
   ch       searchdatelo[16], searchdatehi[16];                                  //  format is yyyymmddhhmmss

   ch       searchRatingFrom[4] = "";
   ch       searchRatingTo[4] = "";

   ch       searchtags[searchtagsXcc] = "";                                      //  search tags list
   ch       searchtext[searchtagsXcc] = "";                                      //  search title and description text list
   ch       searchfiles[searchtagsXcc] = "";                                     //  search files list

   ch       searchLocations[200] = "";                                           //  search locations

   int      Fscanall, Fscancurr, Fnewset, Faddset, Fremset;
   int      Ftext, Ffiles, Ftags, Frating, Flocs;
   int      Fdates, Fphotodate, Ffiledate, Fnulldate;
   int      Flastver, Forglast, Fallvers, Fnochange;
   int      Falltags, Falltext, Fallfiles, Falllocs;
   int      Frepgallery, Frepmeta, Fautosearch = 0;

   #define  maxNkeys 3                                                           //  max. Nkeys (search dialog entries)
   int      Nkeys = 0;                                                           //  search keys in use (user selections)
   ch       *srchkeys[maxNkeys];                                                 //  metadata keys to search
   ch       *machvals[maxNkeys];                                                 //  data values to search for
   ch       machtyp[maxNkeys];                                                   //  match type: string or number < = >
   int      keyindexed[maxNkeys];                                                //  key included in extra indexed metadata

   FILE     *srfid;                                                              //  search results file
   ch       **scanfiles = 0;                                                     //  files to scan
   ch       **passfiles = 0;                                                     //  files passing extract criteria
   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;

   Plog(1,"m_autosearch \n");
   Plog(1,"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,null,null,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);
   Fblock(0);

   zexit(0,"autosearch exit");
}


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

//  menu function                                                                //  reduced dialog height 

void m_search_images(GtkWidget *, ch *)
{
   using namespace search_images;

   void  search_searchtags_clickfunc(GtkWidget *widget, int line, int pos, int kbkey);
   void  search_matchtags_clickfunc(GtkWidget *widget, int line, int pos, int kbkey);
   void  search_deftags_clickfunc(GtkWidget *widget, int line, int pos, int kbkey);
   int   search_dialog_event(zdialog*, ch *event);

   zdialog     *zd;
   GtkWidget   *widget;

   int         ii, nk;
   static ch   **mlist = 0;
   ch          matchx[8] = "matchx";

   F1_help_topic = "search images";

   Plog(1,"m_search_images \n");

   if (! Findexvalid) {
      zmessageACK(Mwin,"image index disabled");                                  //  no image index
      return;
   }

   if (Findexvalid == 1) 
      zmessage_post(Mwin,"20/10",2,"image index not updated");
   
   if (Fblock("search images")) return;

   if (! srchkeys[0])                                                            //  first call initialization
   {
      for (ii = 0; ii < maxNkeys; ii++) {
         srchkeys[ii] = (ch *) zmalloc(metakeyXcc,"search");
         machvals[ii] = (ch *) zmalloc(metadataXcc,"search");
      }
   }

/***
          _______________________________________________________________________
         |                      Search Images                                    |
         |                                                                       |
         |  images to search: (o) all  (o) current set only                      |
         |  matching images: (o) make new set  (o) add to set  (o) remove        |
         |   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
         |  report: (o) last ver.  (o) org.+last  (o) org.+all  (o) no change    |
         |   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
         |  report type: (o) gallery  (o) metadata                               |
         |   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
         |  date range [_________] [_________]  (o) photo  (o) file (yyyy-mm-dd) |
         |   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
         |  rating range (stars) [__] [__]                                       |
         |   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
         |                                                               all/any |
         |  search tags  [_____________________________________________] (o) (o) |
         |  search text  [_____________________________________________] (o) (o) |
         |  search files [_____________________________________________] (o) (o) |
         |  search locations [_________________________________________] (o) (o) |
         |   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
         |      Keyname            Condition          Match Values            X  |
         |  [________________|v] [ report    ] [___________________________] [x] |
         |  [________________|v] [ matches   ] [___________________________] [x] |
         |  [________________|v] [ number =  ] [___________________________] [x] |
         |   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
         |  Enter Tag [______________]  Matches [______________________________] |
         |   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
         |  Defined Tags Category [__________________________________________|v] |
         |  |                                                                  | |
         |  |                                                                  | |
         |  |                                                                  | |
         |  |                                                                  | |
         |  |                                                                  | |
         |  |                                                                  | |
         |  |__________________________________________________________________| |
         |                                                                       |
         |                                   [Load] [Save] [Clear] [Proceed] [X] |
         |_______________________________________________________________________|

***/

   zd = zdialog_new("Search Images",Mwin,"Load","Save","Clear","Proceed"," X ",null);
   zd_search = zd;

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

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

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

   zdialog_add_widget(zd,"hbox","hbsell","dialog");
   zdialog_add_widget(zd,"label","labrep","hbsell","report:","space=3");
   zdialog_add_widget(zd,"radio","lastver","hbsell","last ver.","space=3");
   zdialog_add_widget(zd,"radio","org+last","hbsell","org.+last","space=3");
   zdialog_add_widget(zd,"radio","allvers","hbsell","org.+all","space=3");
   zdialog_add_widget(zd,"radio","nochange","hbsell","no change","space=3");

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

   zdialog_add_widget(zd,"hbox","hbrt","dialog");
   zdialog_add_widget(zd,"label","labrt","hbrt","report type:","space=3");
   zdialog_add_widget(zd,"radio","repgallery","hbrt","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","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","photo","space=5");
   zdialog_add_widget(zd,"radio","filedate","hbdt","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","labS","hbrating","rating range (stars)","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,"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","hbtags","dialog","space=3");
   zdialog_add_widget(zd,"label","labtags","hbtags","search tags","space=3");
   zdialog_add_widget(zd,"text","searchtags","hbtags",0,"expand|wrap|space=3");
   zdialog_add_widget(zd,"radio","alltags","hbtags",0,"space=5");
   zdialog_add_widget(zd,"radio","anytags","hbtags",0,"space=5");

   zdialog_add_widget(zd,"hbox","hbtext","dialog","space=3");
   zdialog_add_widget(zd,"label","labtext","hbtext","search text","space=3");
   zdialog_add_widget(zd,"zentry","searchtext","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","search files","space=3");
   zdialog_add_widget(zd,"zentry","searchfiles","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","search 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","enter cities, countries");

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

   zdialog_add_widget(zd,"hbox","hbmeta","dialog");
   zdialog_add_widget(zd,"vbox","vbkey","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","vbkey","keyname");
   zdialog_add_widget(zd,"label","lab2","vbmatch","condition");
   zdialog_add_widget(zd,"label","lab3","vbvalue","match values");
   zdialog_add_widget(zd,"label","lab0","vbclear","X");

   zdialog_add_widget(zd,"combo","key0","vbkey",0,"size=15");                    //  must match maxNkeys (now 3)
   zdialog_add_widget(zd,"combo","key1","vbkey",0,"size=15");
   zdialog_add_widget(zd,"combo","key2","vbkey",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","enter tag","space=3");
   zdialog_add_widget(zd,"zentry","entertag","hbnt",0,"size=12");
   zdialog_add_widget(zd,"label","labnt","hbnt","  matches","space=3");
   zdialog_add_widget(zd,"text","matchtags","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","defined tags category","space=3");
   zdialog_add_widget(zd,"combo","defcats","hbdt1",0,"space=10|size=20");

   zdialog_add_widget(zd,"hbox","hbdeftags","dialog",0,"expand");                //  scroll window for defined tags
   zdialog_add_widget(zd,"scrwin","scrwdeftags","hbdeftags",0,"expand");
   zdialog_add_widget(zd,"text","deftags","scrwdeftags",0,"wrap");               //  defined tags window

   if (Fautosearch) {                                                            //  autosearch caller
      zdialog_run(zd,search_dialog_event,"save");                                //  bypass interactive stuff
      return;
   }

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

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

   widget = zdialog_gtkwidget(zd,"deftags");
   textwidget_set_eventfunc(widget,search_deftags_clickfunc);

   zdialog_stuff(zd,"allimages",1);                                              //  defaults
   zdialog_stuff(zd,"currset",0);
   zdialog_stuff(zd,"newset",1);
   zdialog_stuff(zd,"addset",0);
   zdialog_stuff(zd,"remset",0);
   zdialog_stuff(zd,"repgallery",1);
   zdialog_stuff(zd,"repmeta",0);
   zdialog_stuff(zd,"photodate",1);
   zdialog_stuff(zd,"filedate",0);
   zdialog_stuff(zd,"lastver",0);
   zdialog_stuff(zd,"org+last",0);
   zdialog_stuff(zd,"allvers",0);
   zdialog_stuff(zd,"nochange",1);
   zdialog_stuff(zd,"alltags",0);
   zdialog_stuff(zd,"anytags",1);
   zdialog_stuff(zd,"alltext",0);
   zdialog_stuff(zd,"anytext",1);
   zdialog_stuff(zd,"allfiles",0);
   zdialog_stuff(zd,"anyfiles",1);
   zdialog_stuff(zd,"alllocs",0);
   zdialog_stuff(zd,"anylocs",1);

   nk = zreadfile(meta_picklist_file,mlist);                                     //  get metadata picklist

   for (ii = 0; ii < nk; ii++) {
      zdialog_stuff(zd,"key0",mlist[ii]);                                        //  metadata picklist > key picklist
      zdialog_stuff(zd,"key1",mlist[ii]);
      zdialog_stuff(zd,"key2",mlist[ii]);
   }

   zreadfile_free(mlist);

   zdialog_stuff(zd,"key0","(other)");                                           //  add "other" choice
   zdialog_stuff(zd,"key1","(other)");
   zdialog_stuff(zd,"key2","(other)");

   zdialog_stuff(zd,"key0","");                                                  //  clear picklist choices
   zdialog_stuff(zd,"key1","");
   zdialog_stuff(zd,"key2","");

   for (ii = 0; ii < maxNkeys; 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,"searchtags",searchtags,searchtagsXcc);
   strcat(searchtags," ");                                                       //  trailing blank after "tagname,"

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

   zdialog_resize(zd,0,800);                                                     //  start dialog
   zdialog_run(zd,search_dialog_event,"save");
   zdialog_wait(zd);                                                             //  wait for dialog completion
   zdialog_free(zd);
   Fblock(0);
   return;
}


//  search tag was clicked

void search_searchtags_clickfunc(GtkWidget *widget, int line, int pos, int kbkey)
{
   using namespace search_images;

   ch     *txtag, end = 0;

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

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

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

   zfree(txtag);
   return;
}


//  matching tag was clicked

void search_matchtags_clickfunc(GtkWidget *widget, int line, int pos, int kbkey)
{
   using namespace search_images;

   ch     *txtag, end = 0;

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

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

   add_tag(txtag,searchtags,searchtagsXcc);                                      //  add to search tag list

   zdialog_stuff(zd_search,"entertag","");                                       //  update dialog widgets
   zdialog_stuff(zd_search,"matchtags","");
   zdialog_stuff(zd_search,"searchtags",searchtags);

   zdialog_goto(zd_search,"entertag");                                           //  focus back to entertag widget

   zfree(txtag);
   return;
}


//  defined tag was clicked

void search_deftags_clickfunc(GtkWidget *widget, int line, int pos, int kbkey)
{
   using namespace search_images;

   ch     *txtag, end = 0;

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

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

   add_tag(txtag,searchtags,searchtagsXcc);                                      //  add to search tag list
   zdialog_stuff(zd_search,"searchtags",searchtags);

   zfree(txtag);
   return;
}


//  search images dialog event and completion callback function

int search_dialog_event(zdialog *zd, ch *event)
{
   using namespace search_images;

   int   datetimeOK(ch *datetime);
   int   search_metadata_dialog(zdialog *zd);
   void  search_main();
   void  search_xmeta();
   void  search_nxmeta();
   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       entertag[tagXcc], matchtags[20][tagXcc];
   ch       matchtagstext[(tagXcc+2)*20];
   ch       catgname[tagXcc];
   ch       albumfile[200];
   ch       keyx[8] = "keyx", valuex[8] = "valuex", matchx[8] = "matchx";
   ch       wname[8], temp[100];

   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,null,"saved_searches",null);                       //  folder name under get_zhomedir() 
      zdialog_fetch(zd,"searchtags",searchtags,searchtagsXcc);
      strcat(searchtags," ");                                                    //  trailing blank after "tagname,"
      return 1;
   }

   if (zd->zstat == 2) {                                                         //  [save] settings to file
      zd->zstat = 0;
      zdialog_save_widgets(zd,null,"saved_searches",null);
      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,"lastver",0);
      zdialog_stuff(zd,"org+last",0);
      zdialog_stuff(zd,"allvers",0);
      zdialog_stuff(zd,"nochange",1);
      zdialog_stuff(zd,"alltags",0);
      zdialog_stuff(zd,"anytags",1);
      zdialog_stuff(zd,"alltext",0);
      zdialog_stuff(zd,"anytext",1);
      zdialog_stuff(zd,"allfiles",0);
      zdialog_stuff(zd,"anyfiles",1);
      zdialog_stuff(zd,"datefrom","");
      zdialog_stuff(zd,"dateto","");
      zdialog_stuff(zd,"photodate",1);
      zdialog_stuff(zd,"filedate",0);
      zdialog_stuff(zd,"ratingfrom","");
      zdialog_stuff(zd,"ratingto","");
      zdialog_stuff(zd,"searchtags","");
      zdialog_stuff(zd,"searchtext","");
      zdialog_stuff(zd,"searchfiles","");
      zdialog_stuff(zd,"searchlocs","");

      *searchtags = 0;
      Flocs = 0;
      *searchLocations = 0;
      Nkeys = 0;

      for (ii = 0; ii < maxNkeys; ii++) {                                        //  erase metadata entries
         keyx[3] = '0' + ii;
         valuex[5] = '0' + ii;
         matchx[5] = '0' + ii;
         zdialog_stuff(zd,keyx,"");
         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,"entertag"))                                               //  new tag is being typed in
   {
      zdialog_stuff(zd,"matchtags","");                                          //  clear matchtags in dialog

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

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

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

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

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

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

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

      pp1 = matchtagstext;

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

      zdialog_stuff(zd,"matchtags",matchtagstext);                               //  stuff matchtags in dialog
      return 1;
   }

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

   if (strstr(event,"key"))                                                      //  metadata keyN selected
   {
      snprintf(wname,8,"match%c",event[3]);                                      //  widget keyN >> widget matchN
      zdialog_stuff(zd,wname,"report");                                          //  set "report" default operator
   }

   if (strstr(event,"clear"))                                                    //  metadata clearN selected 
   {
      ii = event[5];                                                             //  character N
      keyx[3] = ii;
      valuex[5] = ii;
      matchx[5] = ii;
      zdialog_stuff(zd,keyx,"");
      zdialog_stuff(zd,matchx,"");
      zdialog_stuff(zd,valuex,"");
   }

   for (ii = 0; ii < maxNkeys; ii++)                                             //  if key name "(other)" get key name
   {
      keyx[3] = '0' + ii;
      valuex[5] = '0' + ii;
      zdialog_fetch(zd,keyx,temp,100);
      if (strmatch(temp,"(other)")) {
         pp = zdialog_text1(zd->parent,"enter key name",0);
         if (pp) {
            zdialog_stuff(zd,keyx,pp);
            zfree(pp);
         }
         else zdialog_stuff(zd,keyx,"");
      }
   }

   for (ii = 0; ii < maxNkeys; ii++)                                             //  if match type "report" or "reportx"
   {                                                                             //    clear irrelevant match values
      valuex[5] = '0' + ii;
      matchx[5] = '0' + ii;
      zdialog_fetch(zd,matchx,temp,20);
      if (strstr("report reportx",temp))
         zdialog_stuff(zd,valuex,"");
   }

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

   if (Faddset && Fscancurr) {
      zmessageACK(Mwin,"to add images to current set, \n"
                       "search all images");
      zd->zstat = 0;                                                             //  keep dialog active
      return 1;
   }

   zdialog_fetch(zd,"repgallery",Frepgallery);                                   //  gallery report
   zdialog_fetch(zd,"repmeta",Frepmeta);                                         //  metadata report
   zdialog_fetch(zd,"lastver",Flastver);                                         //  get last versions only
   zdialog_fetch(zd,"org+last",Forglast);                                        //  get original + last version
   zdialog_fetch(zd,"allvers",Fallvers);                                         //  get all vers. of matching image

   zdialog_fetch(zd,"datefrom",searchDateFrom,20);                               //  get search date range
   zdialog_fetch(zd,"dateto",searchDateTo,20);
   zdialog_fetch(zd,"photodate",Fphotodate);                                     //  photo or file date
   zdialog_fetch(zd,"filedate",Ffiledate);
   zdialog_fetch(zd,"ratingfrom",searchRatingFrom,2);                            //  get search rating range
   zdialog_fetch(zd,"ratingto",searchRatingTo,2);
   zdialog_fetch(zd,"searchtags",searchtags,searchtagsXcc);                      //  get search tags
   zdialog_fetch(zd,"searchtext",searchtext,searchtagsXcc);                      //  get search text*
   zdialog_fetch(zd,"searchfiles",searchfiles,searchtagsXcc);                    //  get search /path*/file*
   zdialog_fetch(zd,"searchlocs",searchLocations,200);                           //  get search locations

   zdialog_fetch(zd,"alltags",Falltags);                                         //  get match all/any options
   zdialog_fetch(zd,"alltext",Falltext);
   zdialog_fetch(zd,"allfiles",Fallfiles);
   zdialog_fetch(zd,"alllocs",Falllocs);

   Fdates = 0;
   if (*searchDateFrom) Fdates++;                                                //  search date from was given
   else strcpy(searchDateFrom,dateLoDefault);                                    //  else search from start of time

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

   Fnulldate = 0;
   if (strmatchcase(searchDateFrom,"null")) {                                    //  search for missing photo date
      Fnulldate = 1;                                                             //  (user input "null")
      Fdates = Ffiledate = Fphotodate = 0;
      zdialog_stuff(zd,"photodate",1);
      zdialog_stuff(zd,"filedate",0);
   }

   if (Fdates)                                                                   //  complete partial date/time data
   {
      cc = strlen(searchDateFrom);
      for (ii = cc; ii < 20; ii++)                                               //  default date from:
         searchDateFrom[ii] = dateLoDefault[ii];                                 //    0000-01-01 00:00:00

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

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

      ff = 0;                                                                    //  check search dates reasonable
      if (! datetimeOK(searchDateFrom)) ff = 1;                                  //  invalid year/mon/day (e.g. mon 13)
      if (! datetimeOK(searchDateTo)) ff = 1;                                    //    or hour/min/sec (e.g. hour 33)
      if (strcmp(searchDateFrom,searchDateTo) > 0) ff = 1;                       //  search-from date > search-to date
      if (ff) {
         zmessageACK(Mwin,"search dates not reasonable \n %s  %s",
                                    searchDateFrom,searchDateTo);
         zd->zstat = 0;
         return 1;
      }

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

   Frating = 0;
   if (*searchRatingFrom || *searchRatingTo) {
      Frating = 1;                                                               //  rating was given
      ii = *searchRatingFrom;
      if (! ii) ii = '0';
      if (ii < '0' || ii > '5') Frating = 0;                                     //  validate inputs
      jj = *searchRatingTo;
      if (! jj) jj = '5';
      if (jj < '0' || jj > '5') Frating = 0;
      if (jj < ii) Frating = 0;
      if (! Frating) {
         zmessageACK(Mwin,"rating range not reasonable");
         zd->zstat = 0;
         return 1;
      }
   }

   Ffiles = 0;
   if (! blank_null(searchfiles)) Ffiles = 1;                                    //  search path / file (fragment) was given

   Ftext = 0;
   if (! blank_null(searchtext)) Ftext = 1;                                      //  search text was given

   Ftags = 0;
   if (! blank_null(searchtags)) Ftags = 1;                                      //  search tags was given

   Flocs = 0;
   if (*searchLocations) Flocs = 1;                                              //  search locations was given

   Nkeys = 0;                                                                    //  validate search metadata keys

   for (ii = jj = 0; ii < maxNkeys; ii++)
   {
      keyx[3] = '0' + ii;
      matchx[5] = '0' + ii;
      valuex[5] = '0' + ii;

      zdialog_fetch(zd,keyx,srchkeys[ii],metakeyXcc);                            //  get search key
      strCompress(srchkeys[ii]);                                                 //  remove all blanks from key names
      if (*srchkeys[ii] <= ' ') {
         zdialog_stuff(zd,matchx,"");                                            //  empty search key position
         zdialog_stuff(zd,valuex,"");
         continue;
      }

      memmove(srchkeys[jj],srchkeys[ii],metakeyXcc);                             //  repack blank keys

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

      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,"need numeric match value: %s",srchkeys[jj]);
            zmessageACK(Mwin,temp);
            zd->zstat = 0;
            return 1;
         }
      }

      if (ii > jj) *srchkeys[ii] = *machvals[ii] = 0;
      jj++;
   }

   Nkeys = jj;                                                                   //  search keys count

   for (ii = 0; ii < Nkeys; ii++) {                                              //  loop search keys
      keyindexed[ii] = 0;
      for (jj = 0; jj < xmetamaxkeys; jj++) {                                    //  search extra indexed metadata keys
         if (! xmeta_keys[jj]) break;
         if (strmatchcase(srchkeys[ii],xmeta_keys[jj]))
            keyindexed[ii] = 1;                                                  //  found, search key is indexed
      }
   }

   //  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;
      if (Fscancurr) file = gallery(0,"getR",ii);
      scanfiles[ii] = file;
   }

   viewmode('F');

   search_main();                                                                //  test main select criteria
   search_xmeta();                                                               //  test indexed metadata select criteria
   search_nxmeta();                                                              //  test non-indexed metadata select criteria

   if (Fescape) goto usercancel;                                                 //  user killed search

   Plog(1,"matching files: %d \n",Npass);

   if (Fnewset) { /* do nothing */ }                                             //  new set: no change in passfiles[]

   if (Faddset)                                                                  //  add to set
   {
      cc = (navi::Gfiles + Npass) * sizeof(ch *);                                //  passfiles = gallery + passfiles
      ch **passfiles2 = (ch **) zmalloc(cc+1,"search");
      for (ii = jj = 0; ii < navi::Gfiles; ii++)
         passfiles2[jj++] = gallery(0,"getR",ii);
      for (ii = 0; ii < Npass; ii++)
         passfiles2[jj++] = passfiles[ii];
      Npass = jj;
      zfree(passfiles);
      passfiles = passfiles2;

      HeapSort(passfiles,Npass,zstrcmp2);                                        //  sort passfiles

      for (ii = jj = 0; ii < Npass; ii++) {                                      //  eliminate duplicates
         if (strmatch(passfiles[ii],passfiles[jj])) continue;
         passfiles[++jj] = passfiles[ii];
      }
      Npass = jj + 1;
   }

   if (Fremset)                                                                  //  remove from set
   {
      cc = (navi::Gfiles + Npass) * sizeof(ch *);                                //  passfiles = gallery - matching passfiles
      ch **passfiles2 = (ch **) zmalloc(cc+1,"search");
      for (ii = jj = 0; ii < navi::Gfiles; ii++)
         passfiles2[jj++] = gallery(0,"getR",ii);
      for (ii = 0; ii < Npass; ii++)
         passfiles2[jj++] = passfiles[ii];
      Npass = jj;
      zfree(passfiles);
      passfiles = passfiles2;

      HeapSort(passfiles,Npass,zstrcmp2);                                        //  sort passfiles

      for (ii = 0; ii < Npass-1; ii++) {                                         //  duplicate pairs = null
         if (strmatch(passfiles[ii],passfiles[ii+1])) {
            passfiles[ii] = passfiles[ii+1] = 0;
            ii++;
         }
      }

      for (ii = jj = 0; ii < Npass; ii++) {                                      //  remove null pairs
         if (passfiles[ii])
            passfiles[jj++] = passfiles[ii];
      }

      Npass = jj;
   }

   if (Npass)
   {
      srfid = fopen(searchresults_file,"w");                                     //  open new output file
      if (! srfid) goto filerror;

      for (ii = 0; ii < Npass; ii++)                                             //  passfiles[] --> search results file
      {
         file = passfiles[ii];
         cc = fprintf(srfid,"%s\n",file);
         if (! cc) break;
      }

      fclose(srfid);
      if (! cc) goto filerror;
   }

   Ncurrset = Npass;                                                             //  current set, including last results

//  add related files if wanted (original, last version only ...) ---------------

   if (Ncurrset) search_add_related_files();

//  search complete -------------------------------------------------------------

search_complete:

   Fescape = 0;

   if (scanfiles) zfree(scanfiles);
   if (passfiles) zfree(passfiles);
   scanfiles = passfiles = 0;

   Plog(1,"search count: %d \n", Ncurrset);
   if (Ncurrset == 0) {
      if (Fnewset || Faddset) zmessageACK(Mwin,"nothing found");
      if (Fremset) zmessageACK(Mwin,"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
      search_metadata_report();                                                  //  get metadata from image files
      navi::gallerytype = META;                                                  //  report
   }

   gallery(0,"paint",0);                                                         //  position at top
   viewmode('G');
   return 1;

usercancel:                                                                      //  cancel via escape key
   zmessage_post(Mwin,"parent",1,"function canceled");
   Fescape = 0;

   if (scanfiles) zfree(scanfiles);
   if (passfiles) zfree(passfiles);
   scanfiles = passfiles = 0;
   return 1;

filerror:
   zmessageACK(Mwin,"file error: %s",strerror(errno));
   Fescape = 0;

   if (scanfiles) zfree(scanfiles);
   if (passfiles) zfree(passfiles);
   scanfiles = 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, cc, 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) return;

      file = scanfiles[ff];

      xxrec = get_xxrec(file);
      if (! xxrec) goto nomatch;

      if (Ffiles)                                                                //  file name match is wanted
      {
         Nmatch = Nnomatch = 0;

         for (ii = 1; ; ii++)
         {
            pps = substringR(searchfiles," ,",ii);                               //  step thru search file names
            if (! pps) break;

            if (strchr(pps,'*')) {
               if (MatchWildCase(pps,file) == 0) Nmatch++;                       //  use wildcard matching
               else Nnomatch++;
            }

            else {
               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 && ! strmatch(xxrec->pdate,"")) goto nomatch;                //  missing photo date wanted

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

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

      if (Ftags)                                                                 //  tags match is wanted
      {
         if (! xxrec->tags) goto nomatch;                                        //  should not happen

         Nmatch = Nnomatch = 0;

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

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

            zfree(pps);
         }

         if (Nmatch == 0) goto nomatch;                                          //  no match to any tag
         if (Falltags && Nnomatch) goto nomatch;                                 //  no match to all tags
      }

      if (Frating)                                                               //  rating match is wanted
      {
         if (*searchRatingFrom && xxrec->rating[0] < *searchRatingFrom) goto nomatch;
         if (*searchRatingTo && xxrec->rating[0] > *searchRatingTo) goto nomatch;
      }

      if (Ftext)                                                                 //  text match is wanted
      {                                                                          //  opt. wildcard '*' match
         Nmatch = Nnomatch = 0;

         for (iis = 1; ; iis++)                                                  //  step through search words
         {
            match1 = 0;

            pps = substringR(searchtext,' ',iis);
            if (! pps) break;

            for (iit = 1; ; iit++)                                               //  step through title words
            {
               ppt = substringR(xxrec->title," ,.;:?/'\"",iit);                  //  delimiters: blank , . ; : ? / ' "
               if (! ppt) break;
               if (MatchWildCase(pps,ppt) == 0) 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 (MatchWildCase(pps,ppt) == 0) 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(searchLocations,", ",iis);                          //  comma or blank delimiter
            if (! pps) break;
            if (strcasestr(xxrec->location,pps)) Nmatch++;                       //  no special treatment for "null"
            else if (strcasestr(xxrec->country,pps)) Nmatch++;
            else Nnomatch++;
            zfree(pps);
         }

         if (! Nmatch) goto nomatch;                                             //  no match found
         if (Falllocs && Nnomatch) goto nomatch;
      }

//  match:
      continue;                                                                  //  file passed main select criteria

    nomatch:                                                                     //  file does not match
      scanfiles[ff] = 0;                                                         //  remove from scanfiles list
      continue;
   }

   cc = Nscan * sizeof(ch *);
   passfiles = (ch **) zmalloc(cc,"search");

   for (ii = jj = 0; ii < Nscan; ii++)                                           //  passfiles[] = remaining scanfiles[]
      if (scanfiles[ii])
         passfiles[jj++] = scanfiles[ii];

   Npass = jj;                                                                   //  count of passed files

   return;
}


//  test indexed metadata against select criteria

void search_xmeta()
{
   using namespace search_images;

   int  searchmeta_test1(ch *keydata, ch machtyp, ch *machvals);

   ch       *kname[maxNkeys], *kvals[maxNkeys], kmach[maxNkeys];
   ch       *xkey[xmetamaxkeys], *xval[xmetamaxkeys];
   int      ii, jj, ff, cc, NK, NX, pass;
   ch       *xmeta, *pps, *ppf;
   ch       *file;
   xxrec_t  *xxrec;

   if (! Npass) return;

   for (ii = jj = 0; ii < Nkeys; ii++)                                           //  indexed metadata select criteria
   {
      if (keyindexed[ii] == 1) {
         kname[jj] = srchkeys[ii];
         kvals[jj] = machvals[ii];
         kmach[jj] = machtyp[ii];
         jj++;
      }
   }

   NK = jj;
   if (NK == 0) return;                                                          //  no indexed search keys

   for (ii = 0; ii < xmetamaxkeys; ii++)
   {
      xkey[ii] = (ch *) zmalloc(metakeyXcc,"searchX");
      xval[ii] = (ch *) zmalloc(metadataXcc,"searchX");
   }

   for (ff = 0; ff < Npass; ff++)                                                //  loop through files to scan
   {
      zmainloop();
      if (Fescape) return;

      file = passfiles[ff];                                                      //  get indexed metadata for file
      xxrec = get_xxrec(file);
      xmeta = xxrec->xmeta;

      if (! xmeta) {                                                             //  no metadata
         for (ii = 0; ii < NK; ii++) {                                           //  loop search keys
            pass = searchmeta_test1(0,kmach[ii],kvals[ii]);                      //  test for "null" select criteria
            if (! pass) goto nomatch;
         }
         goto match;
      }

      pps = xmeta;

      for (ii = 0; ii < xmetamaxkeys; ii++)                                      //  unpack indexed metadata
      {                                                                          //    to xkey[], xval[]
         ppf = strchr(pps,'=');
         if (! ppf) break;
         cc = ppf-pps;
         if (cc > 79) break;
         strncpy0(xkey[ii],pps,cc+1);
         pps = ppf + 1;
         ppf = strchr(pps,'^');
         if (! ppf) break;
         cc = ppf - pps;
         if (cc > 99) break;
         strncpy0(xval[ii],pps,cc+1);
         pps = ppf + 1;
         while (*pps == ' ') pps++;
      }

      NX = ii;                                                                   //  xmeta keys found

      pass = 1;                                                                  //  assume metadata match

      for (ii = 0; ii < NK; ii++)                                                //  loop search keys
      {
         for (jj = 0; jj < NX; jj++)                                             //  find matching file xmeta key
         {
            if (strmatchcase(kname[ii],xkey[jj])) {                              //  if found, test metadata
               pass = searchmeta_test1(xval[jj],kmach[ii],kvals[ii]);            //    against select criteria
               if (! pass) goto nomatch;                                         //  fail, no more testing needed
               break;
            }
         }

         if (jj == NX) {                                                         //  search key not present in file
            pass = searchmeta_test1(0,kmach[ii],kvals[ii]);                      //  test for "null" select criteria
            if (! pass) goto nomatch;
         }
      }

    match:                                                                       //  file metadata matches
      continue;                                                                  //    select criteria

    nomatch:                                                                     //  metadata does not match
      passfiles[ff] = 0;                                                         //  remove file from list
      continue;
   }

   for (ii = jj = 0; ii < Npass; ii++)                                           //  remove null files from list
      if (passfiles[ii])
         passfiles[jj++] = passfiles[ii];

   Npass = jj;                                                                   //  count of passed files

   for (ii = 0; ii < xmetamaxkeys; ii++)
   {
      zfree(xkey[ii]);
      zfree(xval[ii]);
   }

   return;
}


//  test non-indexed metadata against select criteria

void search_nxmeta()
{
   using namespace search_images;

   int  searchmeta_test1(ch *keydata, ch  machtyp, ch *machvals);

   ch       *kname[maxNkeys], *kvals[maxNkeys], kmach[maxNkeys];
   ch       **kdata;
   ch       *xkey[xmetamaxkeys], *xval[xmetamaxkeys];
   int      ii, jj, jj1, jj2;
   int      ff, cc, NK, pass;

   if (! Npass) return;

   for (ii = jj = 0; ii < Nkeys; ii++)                                           //  get non-indexed metadata
   {                                                                             //    select criteria
      if (keyindexed[ii] == 0) {
         kname[jj] = srchkeys[ii];                                               //  search keys
         kvals[jj] = machvals[ii];                                               //  match values
         kmach[jj] = machtyp[ii];                                                //  match type
         jj++;
      }
   }

   NK = jj;
   if (NK == 0) return;                                                          //  no non-indexed search keys

   cc = Npass * NK * sizeof(ch *);                                               //  allocate space for returned data
   kdata = (ch **) zmalloc(cc,"search");

   meta_getN(passfiles, Npass, kname, kdata, NK);                                //  get non-indexed metadata for all files

   for (ff = 0; ff < Npass; ff++)                                                //  loop through files to scan
   {
      zmainloop();
      if (Fescape) return;

      jj1 = ff * NK;                                                             //  kdata[] range for file[ff]
      jj2 = jj1 + NK;

      for (ii = 0, jj = jj1; jj < jj2; ii++, jj++) {                             //  get key names and values for file[ff]
         xkey[ii] = kname[ii];
         xval[ii] = kdata[jj];
      }

      for (ii = 0; ii < NK; ii++)                                                //  loop search keys
      {
         for (jj = 0; jj < NK; jj++)                                             //  find matching file key data
         {
            if (strmatchcase(kname[ii],xkey[jj])) {                              //  if found, test metadata
               pass = searchmeta_test1(xval[jj],kmach[ii],kvals[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] = 0;                                                         //  remove file from pass list
      continue;                                                                  //  next file
   }

   for (ii = 0; ii < Npass * NK; ii++)                                           //  free memory from meta_getN()
      if (kdata[ii]) zfree(kdata[ii]);
   zfree(kdata);

   for (ii = jj = 0; ii < Npass; ii++)                                           //  remove unmatching files from list
      if (passfiles[ii])
         passfiles[jj++] = passfiles[ii];

   Npass = jj;                                                                   //  count of passed files

   return;
}


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

int searchmeta_test1(ch *keydata, ch  machtyp, ch *machvals)
{
   using namespace search_images;

   int         nth, n1, n2, n3, mm;
   ch          *pps, *ppm;
   float       Fkeydata = 0, Fsearchval;

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

   if (machtyp == 'x') {                                                         //  exists, reported, not tested
      if (keydata) return 1;                                                     //  kay value present, pass
      else return 0;                                                             //  no data, fail
   }

   if (*machvals <= ' ') return 1;                                               //  no match values, pass

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

   if (strchr("= > <",machtyp)) {                                                //  real value, look for N/N format
      Fkeydata = atofz(keydata);
      n1 = sscanf(keydata,"%d/%d",&n2,&n3);
      if (n1 == 2) Fkeydata = 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') {                                                      //  key matches any [wild] value
         mm = MatchWildCase(pps,keydata);                                        //  match not case sensitive
         zfree(pps);
         if (mm == 0) return 1;
      }

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

      else if (machtyp == '=') {                                                 //  numeric key equals any value
         Fsearchval = atofz(pps);
         zfree(pps);
         if (Fkeydata == Fsearchval) return 1;                                   //  found match
      }

      else if (machtyp == '>') {                                                 //  numeric key >= one value
         Fsearchval = atofz(pps);
         zfree(pps);
         if (Fkeydata >= Fsearchval) return 1;                                   //  found match
      }

      else if (machtyp == '<') {                                                 //  numeric key <= one value
         Fsearchval = atofz(pps);
         zfree(pps);
         if (Fkeydata <= Fsearchval) return 1;                                   //  found match
      }

      else {
         Plog(0,"searchmeta invalid machtyp %c \n",machtyp);
         zfree(pps);
         return 0;
      }
   }
}


//  add related files to search results if wanted

void search_add_related_files()
{
   using namespace search_images;

   int      cc, ii, kk, nf, jj, Nmatch;
   ch       **flist = 0;
   ch       *file, *file2;
   ch       **vlist;
   ch       buffer[XFCC];
   ch       *pp;

   //  Include only last version of matching images -----------------------------

   if (Flastver && Ncurrset)
   {
      cc = Ncurrset * sizeof(ch *);
      flist = (ch **) zmalloc(cc,"search");

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

      for (ii = 0; ii < Ncurrset; ii++)                                          //  build file list in memory
      {
         file = fgets_trim(buffer,XFCC,srfid);
         if (! file) break;
         flist[ii] = zstrdup(file,"search");
      }

      fclose(srfid);

      for (ii = 1; ii < Ncurrset; ii++)                                          //  scan file list in memory
      {
         pp = strrchr(flist[ii],'/');                                            //  /folder.../filename.v...
         if (! pp) continue;                                                     //  |                  |
         pp = strstr(pp,".v");                                                   //  flist[ii]          pp
         if (! pp) continue;
         cc = pp - flist[ii] + 1;
         if (strmatchN(flist[ii],flist[ii-1],cc)) {                              //  compare each filespec with prior
            zfree(flist[ii-1]);                                                  //  match: remove prior from list
            flist[ii-1] = 0;
         }
      }

      srfid = fopen(searchresults_file,"w");                                     //  write remaining file list
      if (! srfid) goto filerror;                                                //    to search results file

      Nmatch = 0;

      for (ii = 0; ii < Ncurrset; ii++)
      {
         file = flist[ii];
         if (! file) continue;
         file2 = file_newest_version(file);                                      //  insure last version
         if (! file2) continue;                                                  //  should not happen
         fprintf(srfid,"%s\n",file2);
         zfree(file);
         zfree(file2);
         Nmatch++;
      }

      fclose(srfid);
      zfree(flist);
      flist = 0;
      Ncurrset = Nmatch;                                                         //  new current set count
   }

   //  Include original and last version of matching images ---------------------

   if (Forglast && Ncurrset)
   {
      cc = Ncurrset * sizeof(ch *);                                              //  first, reduce selected images
      flist = (ch **) zmalloc(cc,"search");                                      //    to one version each

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

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

      fclose(srfid);

      for (ii = 1; ii < Ncurrset; ii++)                                          //  scan file list in memory
      {
         pp = strrchr(flist[ii],'/');
         if (! pp) continue;
         pp = strrchr(pp,'.');
         if (! pp) continue;                                                     //  /folder.../filename.ext
         if (strmatchN(pp-4,".v",2)) pp -= 4;                                    //  (or)   .../filename.vNN.ext
         cc = pp - flist[ii] + 1;
         if (strmatchN(flist[ii],flist[ii-1],cc)) {                              //  compare each filespec with prior
            zfree(flist[ii-1]);                                                  //  match: remove prior from list
            flist[ii-1] = 0;
         }
      }

      srfid = fopen(searchresults_file,"w");                                     //  write remaining file list
      if (! srfid) goto filerror;                                                //    to search results file

      for (ii = 0; ii < Ncurrset; ii++)
      {
         if (flist[ii]) {
            fprintf(srfid,"%s\n",flist[ii]);
            zfree(flist[ii]);
         }
      }

      fclose(srfid);
      zfree(flist);
      flist = 0;

      flist = (ch **) zmalloc(maxgallery * sizeof(ch *),"search");               //  new list for original + last ver.

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

      for (ii = Nmatch = 0; Nmatch < maxgallery-2; ii++)                         //  get all versions for selected files
      {
         file = fgets_trim(buffer,XFCC,srfid);
         if (! file) break;
         vlist = file_all_versions(file,nf);
         if (! vlist) continue;
         if (nf > 0) flist[Nmatch++] = zstrdup(vlist[0],"search");               //  original
         if (nf > 1) flist[Nmatch++] = zstrdup(vlist[nf-1],"search");            //  last version
         for (kk = 0; kk < nf; kk++)
            zfree(vlist[kk]);
         zfree(vlist);
      }

      fclose(srfid);

      if (Nmatch > maxgallery-2)
         zmessageACK(Mwin,"gallery truncated to %d images",maxgallery);

      srfid = fopen(searchresults_file,"w");                                     //  new search results file
      if (! srfid) goto filerror;

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

      fclose(srfid);
      zfree(flist);
      flist = 0;
      Ncurrset = Nmatch;                                                         //  new current set count
   }

   //  Include original and all versions of matching images ---------------------

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

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

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

      fclose(srfid);

      for (ii = 1; ii < Ncurrset; ii++)                                          //  scan file list in memory
      {
         pp = strrchr(flist[ii],'/');
         if (! pp) continue;
         pp = strrchr(pp,'.');
         if (! pp) continue;                                                     //  /folder.../filename.ext
         if (strmatchN(pp-4,".v",2)) pp -= 4;                                    //  (or)   .../filename.vNN.ext
         cc = pp - flist[ii] + 1;
         if (strmatchN(flist[ii],flist[ii-1],cc)) {                              //  compare each filespec with prior
            zfree(flist[ii-1]);                                                  //  match: remove prior from list
            flist[ii-1] = 0;
         }
      }

      srfid = fopen(searchresults_file,"w");                                     //  write remaining file list
      if (! srfid) goto filerror;                                                //    to search results file

      for (ii = 0; ii < Ncurrset; ii++)
      {
         if (flist[ii]) {
            fprintf(srfid,"%s\n",flist[ii]);
            zfree(flist[ii]);
         }
      }

      fclose(srfid);
      zfree(flist);
      flist = 0;

      flist = (ch **) zmalloc(maxgallery * sizeof(ch *),"search");               //  new list for all versions

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

      for (ii = Nmatch = 0; Nmatch < maxgallery-100; ii++)                       //  get all versions for selected files
      {
         file = fgets_trim(buffer,XFCC,srfid);
         if (! file) break;
         vlist = file_all_versions(file,nf);
         if (! vlist) continue;
         for (jj = 0; jj < nf; jj++)
            flist[Nmatch++] = vlist[jj];
         zfree(vlist);
      }

      fclose(srfid);

      if (Nmatch > maxgallery-100)
         zmessageACK(Mwin,"gallery truncated to %d images",maxgallery);

      srfid = fopen(searchresults_file,"w");                                     //  new search results file
      if (! srfid) goto filerror;

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

      fclose(srfid);
      zfree(flist);
      flist = 0;
      Ncurrset = Nmatch;                                                         //  new current set count
   }

   return;

filerror:
   zmessageACK(Mwin,"file error: %s",strerror(errno));
   fclose(srfid);
   if (flist) zfree(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, **keyvals = 0;
   ch       pdate[12], ptime[12];
   ch       wwhh[16], fsize[16];
   ch       text1[2000], text2[200];                                             //  note text1 limit
   xxrec_t  *xxrec;

   if (! Gfiles) {                                                               //  curr. gallery files
      Plog(1,"metadata report, 0 files \n");
      return 0;
   }

   if (Nkeys)
   {
      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 * Nkeys * sizeof(ch **);                                       //  allocate pointers for returned metadata
      keyvals = (ch **) zmalloc(cc,"search");

      meta_getN(repfiles,Gfiles,srchkeys,keyvals,Nkeys);                         //  get Nkeys keyvals 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;

      metadate_pdate(xxrec->pdate,pdate,ptime);
      snprintf(text2,200,"photo date: %s \n",pdate);
      strcpy(text1,text2);
      cc = strlen(text1);

      snprintf(wwhh,16,"%dx%d",xxrec->ww,xxrec->hh);
      snprintf(fsize,16,"%.2fmb",xxrec->fsize/MEGA);
      snprintf(text2,200,"Rating: %s   Size: %s  %s \n",
                              xxrec->rating, wwhh, fsize);
      strcpy(text1+cc,text2);
      cc += strlen(text2);

      snprintf(text2,200,"tags: %s\n",xxrec->tags);
      strcpy(text1+cc,text2);
      cc += strlen(text2);

      snprintf(text2,200,"Location: %s %s\n",xxrec->location,xxrec->country);    //  24.70
      strcpy(text1+cc,text2);
      cc += strlen(text2);

      snprintf(text2,200,"title: %s\n",xxrec->title);
      strcpy(text1+cc,text2);
      cc += strlen(text2);

      snprintf(text2,200,"description: %s\n",xxrec->desc);
      strcpy(text1+cc,text2);
      cc += strlen(text2);

      if (Gindex[ff].mdata1) zfree(Gindex[ff].mdata1);                           //  standard metadata >> gallery index
      Gindex[ff].mdata1 = zstrdup(text1,"search");

      if (Gindex[ff].mdata2) zfree(Gindex[ff].mdata2);                           //  clear user selected metadata
      Gindex[ff].mdata2 = 0;

      if (Nkeys)                                                                 //  other metadata to report
      {
         ii = ff * Nkeys;                                                        //  metadata values for this file

         for (cc = jj = 0; jj < Nkeys; jj++, ii++)
         {
            snprintf(text2,200,"%s:  %s \n",srchkeys[jj], keyvals[ii]);
            if (cc + strlen(text2) > 1999) break;
            strcpy(text1+cc,text2);
            cc += strlen(text2);
         }

         Gindex[ff].mdata2 = zstrdup(text1,"search");                            //  user metadata >> gallery index
      }
   }

   Gmdrows = 6 + Nkeys;                                                          //  report rows

   if (Nkeys)
   {
      zfree(repfiles);

      for (ii = 0; ii < Gfiles * Nkeys; ii++)                                    //  free keyvals memory
         if (keyvals[ii]) zfree(keyvals[ii]);
      zfree(keyvals);
   }

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


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

//  Convert date format from "yyyy-mm-dd" to "yyyymmdd".
//  Missing month or day ("yyyy" or "yyyy-mm") is replaced with "01".
//  Output user message and return null if input is not valid.

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

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

   strcpy(pdate2,pdate);

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

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

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

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

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

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

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


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

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

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

   strcpy(ptime2,ptime);

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

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

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

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

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

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


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

ch * pdatetime_metadatetime(ch *pdatetime)
{
   ch          pdate[12], ptime[12];
   static ch   metadatetime[20];
   ch          *pp;
   int         cc;

   strncpy0(pdate,pdatetime,11);                                                 //  yyyy-mm-dd
   strncpy0(ptime,pdatetime+11,9);                                               //  hh:mm[:ss]

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

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

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

   return metadatetime;
}


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

void metadate_pdate(ch *metadate, ch *pdate, ch *ptime)
{
   if (*metadate)
   {
      if (strmatch(metadate,"null")) {
         strcpy(pdate,"null");
         *ptime = 0;
      }
      else {
         memcpy(pdate,metadate,4);                                               //  yyyymmdd to yyyy-mm-dd
         memcpy(pdate+5,metadate+4,2);
         memcpy(pdate+8,metadate+6,2);
         pdate[4] = pdate[7] = '-';
         pdate[10] = 0;
         memcpy(ptime,metadate+8,2);                                             //  hhmmss to hh:mm:ss
         memcpy(ptime+3,metadate+10,2);
         ptime[2] = ':';
         ptime[5] = 0;
         memcpy(ptime+6,metadate+12,2);
         ptime[5] = ':';
         ptime[8] = 0;
      }
   }
   else *pdate = *ptime = 0;                                                     //  missing
   return;
}


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

int datetimeOK(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 != 16 && cc != 19) return 0;

   if (datetime[4] != '-') return 0;
   if (datetime[7] != '-') return 0;
   if (datetime[13] != ':') return 0;
   if (cc == 19 && datetime[16] != ':') return 0;

   year = atoi(datetime);
   mon = atoi(datetime+5);
   day = atoi(datetime+8);
   hour = atoi(datetime+11);
   min = atoi(datetime+14);
   if (cc == 19) sec = atoi(datetime+17);
   else sec = 0;

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

   return 1;
}


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

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

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

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

   pp1 = taglist;
   cc1 = strlen(tag);

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

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

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

   return 0;
}


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

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


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

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

   temptags = zstrdup(taglist,"delete-tag");

   *taglist = 0;
   ftcc = found = 0;

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

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

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


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

int add_recentag(ch *tag)
{
   int         err;
   ch          *pp, temptags[recenttagsXcc];

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

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

   return 0;
}


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

//  Load tags_defined file into tags_deftags[ii] => category: tag1, tag2, ...
//  Read image_index recs. and add unmatched tags: => nocatg: tag1, tag2, ...
//  force: read image index and build deftags list

void load_deftags(int force)
{
   static int  Floaded = 0;
   FILE *      fid;
   xxrec_t     *xxrec;
   int         ii, jj, ntags, err, cc, tcc;
   int         ncats, catoverflow;
   int         nocat, nocatcc;
   ch          tag[tagXcc], catg[tagXcc];
   ch          tagsbuff[catgXcc];
   ch          *pp1, *pp2;
   ch          ptags[maxtags][tagXcc];                                           //  10000 * 50 = 0.5 MB

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

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

   ncats = catoverflow = 0;

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

//  sort the categories in ascending order

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

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

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

//  if not already there, add category "nocatg" to the end of the list

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

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

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

      xxrec = xxrec_tab[ii];

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

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

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

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

         pp1 = pp2;
      }
   }

   if (catoverflow) goto cattoobig;

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

   for (ii = 0; ii < ncats; ii++)
   {
      pp1 = tags_deftags[ii];
      pp2 = strchr(pp1,':');
      if (! pp2) {
         Plog(0,"defined tags file format error: %s \n",pp1);
         continue;
      }
      cc = pp2 - pp1 + 1;
      strncpy0(catg,pp1,cc);
      pp1 = pp2 + 1;
      while (*pp1 == ' ') pp1++;
      tcc = 0;

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

      ntags = jj;
      if (ntags == maxtags) goto toomanytags;
      HeapSort((ch *) ptags,tagXcc,ntags,zstrcasecmp);

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

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

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

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

      *pp2 = 0;
   }

   return;

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

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

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

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


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

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

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

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

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

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


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

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

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

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

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

   return 1;
}


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

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

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

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

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

   cc1 = strlen(catg1);

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

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

oldcatg:                                                                         //  logic simplified
   pp2 = pp1 + cc1 + 2;                                                          //  ch following "catname: "
   cc1 = strlen(tag1);
   cc2 = strlen(pp1);                                                            //  add new tag to old record
   if (cc1 + cc2 + 4 > catgXcc) goto cattoobig;
   pp2 = zstrdup(pp1,"add-deftag",cc1+cc2+4);                                    //  expand string
   zfree(pp1);
   tags_deftags[ii] = pp2;
   strcpy(pp2+cc2,tag1);                                                         //  old record + tag + delim + blank
   strcpy(pp2+cc2+cc1,", ");
   save_deftags();
   return 0;

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

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

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

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


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

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

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

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

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

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

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

   return 0;
}


//  delete category from defined tags list, tags_deftags[]
//  return: 0 = found and deleted, 1 = not found
//          2 = not deleted because category has tags assigned

int del_defcatg(ch *catg)
{
   int      ii, jj, cc;
   ch       catg2[tagXcc+2];
   ch       *pp;

   if (! catg || ! *catg || *catg == ' ') return 1;                              //  bad catg (utf8 can be < ' ')

   strncpy0(catg2,catg,tagXcc);                                                  //  construct "catgname:"
   cc = strlen(catg2);
   strcpy(catg2+cc,":");
   cc += 1;

   for (ii = 0; ii < maxtagcats; ii++)
   {
      pp = tags_deftags[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 < maxtagcats-1; jj++)                                        //  close hole in table
      tags_deftags[jj] = tags_deftags[jj+1];

   return 0;                                                                     //  found and deleted
}


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

void deftags_stuff(zdialog *zd, ch *acatg)
{
   GtkWidget      *widget;
   int            ii, ff, cc;
   ch             catgname[tagXcc+4];
   ch             *pp1, *pp2;

   widget = zdialog_gtkwidget(zd,"deftags");
   textwidget_clear(widget);

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

      if (! strmatch(acatg,"ALL")) {
         ff = strmatch(catgname,acatg);
         if (! ff) continue;
      }

      strcat(catgname,": ");
      textwidget_append(widget,1,catgname);                                      //  "category: " in bold text

      pp2++;
      if (*pp2 == ' ') pp2++;
      if (*pp2) textwidget_append(widget,0,pp2);                                 //  "cat1, cat2, ... catN,"
      textwidget_append(widget,0,"\n");
   }

   return;
}


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

void defcats_stuff(zdialog *zd)
{
   ch       catgname[tagXcc+2];
   int      ii, cc;
   ch       *pp1, *pp2;

   zdialog_combo_clear(zd,"defcats");
   zdialog_stuff(zd,"defcats","ALL");

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

   zdialog_stuff(zd,"defcats","ALL");                                            //  default selection

   return;
}


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

//  image file metadata >> memory metadata:
//    meta_pdate, meta_rating, meta_tags, meta_title, meta_description,
//    meta_location, meta_country, meta_lati, meta_longi

void load_filemeta(ch *file)
{
   int      ii, jj, cc;
   int      ww, hh;
   ch       *pp;
   ch       *mkeys[100] = { meta_date_key, meta_tags_key,
                            meta_rating_key, meta_ww_key, meta_hh_key,           //  replace meta_wwhh_key
                            meta_title_key, meta_description_key,
                            meta_city_key, meta_country_key,
                            meta_lati_key, meta_longi_key };
   int      nkey = 11;                                                           //  11 items

   ch       *ppv[100], *imagedate, *imagetags, *imagerating;
   ch       *imageww, *imagehh, *imagetitle, *imagedesc;
   ch       *imageloc, *imagecountry, *imagelati, *imagelongi;

   strncpy0(p_meta_pdate,meta_pdate,15);                                         //  save prior metadata for use by
   strncpy0(p_meta_rating,meta_rating,4);                                        //    edit_metadata [Prev] button
   strncpy0(p_meta_tags,meta_tags,filetagsXcc);
   strncpy0(p_meta_title,meta_title,metadataXcc);
   strncpy0(p_meta_description,meta_description,metadataXcc);
   strncpy0(p_meta_city,meta_city,100);
   strncpy0(p_meta_country,meta_country,100);
   strncpy0(p_meta_lati,meta_lati,20);
   strncpy0(p_meta_longi,meta_longi,20);

   *meta_tags = *meta_pdate = *meta_description = *meta_title = 0;
   strcpy(meta_rating,"0");
   *meta_city = *meta_country = *meta_lati = *meta_longi = 0;

   for (ii = 0; ii < xmetamaxkeys; ii++) {                                       //  add keys for extra indexed metadata
      if (! xmeta_keys[ii]) break;
      mkeys[nkey] = xmeta_keys[ii];
      nkey++;
   }

   meta_get1(file,mkeys,ppv,nkey);                                               //  get metadata from image file

   imagedate = ppv[0];
   imagetags = ppv[1];
   imagerating = ppv[2];
   imageww = ppv[3];
   imagehh = ppv[4];
   imagetitle = ppv[5];
   imagedesc = ppv[6];
   imageloc = ppv[7];
   imagecountry = ppv[8];
   imagelati = ppv[9];
   imagelongi = ppv[10];

   if (imagedate)
      meta_tagdate(imagedate,meta_pdate);                                        //  metadata date/time >> yyyymmddhhmmss

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

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

   strcpy(meta_wwhh,"no data");

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

   if (imagetitle)
      strncpy0(meta_title,imagetitle,metadataXcc);

   if (imagedesc)
      strncpy0(meta_description,imagedesc,metadataXcc);

   if (imageloc)                                                                 //  geotags
      strncpy0(meta_city,imageloc,100);
   else strcpy(meta_city,"");                                                    //  replace missing data with ""

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

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

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

   for (ii = 0; ii < xmetamaxkeys; ii++) {                                       //  get extra indexed metadata if any
      if (! xmeta_keys[ii]) break;
      if (xmeta_data[ii]) zfree(xmeta_data[ii]);
      if (ppv[ii+11]) xmeta_data[ii] = zstrdup(ppv[ii+11],"load-filemeta");
      else xmeta_data[ii] = zstrdup("null","load-filemeta");                     //  use "null" for missing data
   }

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

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


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

void save_filemeta(ch *file)
{
   ch       *mkeys[100] = { meta_date_key, meta_tags_key, meta_rating_key,
                            meta_title_key, meta_description_key,
                            meta_city_key, meta_country_key,
                            meta_lati_key, meta_longi_key };
   int      nkey = 9;                                                            //  9 items
   int      ii, err;
   ch       *metadata[100];
   ch       imagedate[24];

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

   *imagedate = 0;
   if (*meta_pdate) tag_metadate(meta_pdate,imagedate);                          //  yyyymmddhhmmss >> metadata date/time

   metadata[0] = imagedate;                                                      //  update file metadata
   metadata[1] = meta_tags;
   metadata[2] = meta_rating;
   metadata[3] = meta_title;
   metadata[4] = meta_description;
   metadata[5] = meta_city;
   metadata[6] = meta_country;

   if (strmatch(meta_lati,"") || strmatch(meta_longi,""))
      metadata[7] = metadata[8] = " ";
   else {
      metadata[7] = meta_lati;
      metadata[8] = meta_longi;
   }

   for (ii = 0; ii < xmetamaxkeys; ii++) {                                       //  add keys for extra indexed metadata
      if (! xmeta_keys[ii]) break;
      mkeys[nkey] = xmeta_keys[ii];
      metadata[nkey] = xmeta_data[ii];                                           //  missing data ("") will be erased
      nkey++;
   }

   err = meta_put(file,(ch **) mkeys,metadata,nkey);                             //  write metadata
   if (err) zmessageACK(Mwin,"metadata update error: %s",file);

   update_image_index(file);                                                     //  update image index file

   if (zd_metaview) meta_view(0);                                                //  live meta update

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


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

void update_image_index(ch *file)
{
   int      ii, xcc;
   int      nn, ww, hh;
   ch       xmetarec[xmetaXcc];
   xxrec_t  xxrec;
   STATB    statB;

   if (! Findexvalid) {                                                          //  should not happen
      Plog(0,"*** update_image_index() no index \n");
      return;
   }

   if (! regfile(file,&statB)) {
      zmessageACK(Mwin,"file not found");
      return;
   }

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

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

   xxrec.title = meta_title;
   xxrec.desc = meta_description;

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

   xxrec.rating[0] = meta_rating[0];                                             //  rating, 0-5 stars
   xxrec.rating[1] = 0;                                                          //  make string "0" to "5"

   nn = sscanf(meta_wwhh,"%dx%d",&ww,&hh);
   if (nn == 2) {
      xxrec.ww = ww;                                                             //  pixel dimensions
      xxrec.hh = hh;
   }

   xxrec.fsize = statB.st_size;                                                  //  file size, bytes
   xxrec.tags = meta_tags;

   xxrec.location = meta_city;
   xxrec.country = meta_country;

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

   xcc = 0;

   for (ii = 0; ii < xmetamaxkeys; ii++) {                                       //  add extra indexed metadata if any
      if (! xmeta_keys[ii]) break;
      if (strmatch(xmeta_data[ii],"null")) continue;                             //  skip missing data
      if (strlen(xmeta_data[ii]) > 100) xmeta_data[ii][100] = 0;                 //  practical for image search
      strcpy(xmetarec+xcc,xmeta_keys[ii]);                                       //  construct series
      xcc += strlen(xmeta_keys[ii]);                                             //    "keyname=keydata^ "
      xmetarec[xcc++] = '=';
      strcpy(xmetarec+xcc,xmeta_data[ii]);
      xcc += strlen(xmeta_data[ii]);
      strcpy(xmetarec+xcc,"^ ");
      xcc += 2;
      if (xcc > xmetaXcc-102) {
         Plog(0,"file metadata exceeds record size: %s \n",file);
         break;
      }
   }

   if (xcc > 0) xxrec.xmeta = xmetarec;
   else xxrec.xmeta = "null";                                                    //  put_xxrec() uses zstrdup()

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

   return;
}


//  delete given image file from image index recs.

void delete_image_index(ch *file)
{
   if (! Findexvalid) {                                                          //  should not happen
      Plog(0,"*** delete_image_index() no index \n");
      return;
   }

   put_xxrec(null,file);
   return;
}


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

//  Load image geolocations data into memory from image index table.
//  Returns no. geolocations loaded.

int load_imagelocs()
{
   ch       location[100], country[100];
   float    flati, flongi;
   int      cc, ii, jj;
   xxrec_t  *xxrec;

   if (! Findexvalid) {                                                          //  should not happen
      Plog(0,"*** load_imagelocs() no 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,100);
      strncpy0(country,xxrec->country,100);
      flati = xxrec->flati;
      flongi = xxrec->flongi;

      if (! *location) strcpy(location,"null");                                  //  geocoordinates but no location
      if (! *country) strcpy(country,"null");

      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
   Plog(0,"total image geolocations: %d \n",Nimagelocs);

/***

   for (ii = 0; ii < Nimagelocs; ii++)                                           //  dump table
      Plog(0,"%-30s %-30s %10.5f %10.5f \n",
               imagelocs[ii]->country, imagelocs[ii]->location,
               imagelocs[ii]->flati, imagelocs[ii]->flongi);
***/

   return Nimagelocs;
}


//  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 cities geolocations table into memory
//  worldcities file must be sorted by location, country

int load_worldlocs() 
{
   ch       worldcitiesfile[200], wcfbuff[200];
   ch       location[100], country[100];
   ch       plocation[100], pcountry[100];
   float    flati, flongi;
   ch       *pp;
   int      ii, cc;
   FILE     *fid;

   if (! Findexvalid) {                                                          //  should not happen
      Plog(0,"*** load_worldlocs() no 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) {
      Plog(0,"worldcities.txt file missing \n");
      goto retx;
   }

   ii = 0;
   *plocation = *pcountry = '?';

   while (true)
   {
      pp = fgets(wcfbuff,200,fid);
      if (! pp) break;

      pp = substring(wcfbuff,',',1);
      if (! pp) continue;
      strncpy0(location,pp,100);

      pp = substring(wcfbuff,',',2);
      if (! pp) continue;
      strncpy0(country,pp,100);

      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[ii] = (glocs_t *) zmalloc(sizeof(glocs_t),"load_worldlocs");
      worldlocs[ii]->location = zstrdup(location,"load_worldlocs");
      worldlocs[ii]->country = zstrdup(country,"load_worldlocs");
      worldlocs[ii]->flati = flati;
      worldlocs[ii]->flongi = flongi;

      ii++;
      if (ii == maxworldcities) break;
   }

   fclose(fid);
   Nworldlocs = ii;

retx:
   Plog(0,"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 all image files
//  zdialog widgets: location, country, lati, longi
//  location and country are inputs (may be partial leading strings)
//  all four widgets are outputs (found location and geocoordinates)
//  return: 1 = zdialog updated, 0 = no updates made

int find_imagelocs(zdialog *zd)
{
   ch          *pp;
   GtkWidget   *parent;
   int         cc, ii, jj, kk, Nmatch, zoomlev;
   int         flocation = 0, fcountry = 0;
   ch          location[100], country[100], text[200];
   ch          lati[20], longi[20], *matches[20][2];
   ch          *picklist[20];
   ch          *choice;
   float       flati1 = 999, flati2 = -999;
   float       flongi1 = 999, flongi2 = -999;
   float       flatic, flongic, kmrange, fmpp;

   ii = load_imagelocs();                                                        //  if not already
   if (! ii) return 0;

   zdialog_fetch(zd,"location",location,100);                                    //  get dialog inputs
   zdialog_fetch(zd,"country",country,100);
   strTrim2(location);
   strTrim2(country);

   if (*location) flocation = 1;                                                 //  one of these must be present
   if (*country) fcountry = 1;
   if (! flocation && ! fcountry) return 0;

   for (ii = kk = Nmatch = 0; ii < Nimagelocs; ii++)                             //  search for partial location match
   {
      if (flocation) {
         cc = strlen(location);
         if (! strmatchcaseN(location,imagelocs[ii]->location,cc)) continue;
      }
      if (fcountry) {
         cc = strlen(country);
         if (! strmatchcaseN(country,imagelocs[ii]->country,cc)) continue;
      }

      for (jj = 0; jj < Nmatch; jj++)                                            //  reject duplicate match
      {
         if (strmatch(imagelocs[ii]->location,matches[jj][0]) &&
            (strmatch(imagelocs[ii]->country,matches[jj][1]))) break;
      }
      if (jj < Nmatch) continue;

      matches[Nmatch][0] = imagelocs[ii]->location;                              //  save match
      matches[Nmatch][1] = imagelocs[ii]->country;
      if (Nmatch == 20) {
         zmessageACK(Mwin,"more than 20 matches");
         return 0;
      }
      Nmatch++;                                                                  //  count matches
      if (Nmatch == 1) kk = ii;                                                  //  note first match
   }

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

   if (Nmatch == 1) {                                                            //  one match
      strncpy0(location,imagelocs[kk]->location,100);                            //  save matching location
      strncpy0(country,imagelocs[kk]->country,100);
      goto found_location;
   }

   for (ii = 0; ii < Nmatch; ii++) {                                             //  build picklist of locations
      snprintf(text,200,"%s | %s",matches[ii][0],matches[ii][1]);
      picklist[ii] = zstrdup(text,"find_imagelocs");
   }
   parent = zd->widget[0].widget;
   choice = popup_picklist(parent,(ch **) picklist,0,Nmatch);                    //  show picklist, choose
   pp = substring(choice,'|',1);
   if (pp) strncpy0(location,pp,100);                                            //  user choice, location and country
   pp = substring(choice,'|',2);
   if (pp) strncpy0(country,pp,100);
   strTrim2(location);
   strTrim2(country);
   for (ii = 0; ii < Nmatch; ii++) zfree(picklist[ii]);

found_location:

   zdialog_stuff(zd,"location",location);                                        //  return location data to zdialog
   zdialog_stuff(zd,"country",country);

   for (ii = 0; ii < Nimagelocs; ii++)                                           //  search for location & 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;
      }
   }

   if (flati1 == 999) {                                                          //  no match, return null geocoordinates
      zdialog_stuff(zd,"lati","");
      zdialog_stuff(zd,"longi","");
      return 1;
   }

   if (flati1 == flati2 && flongi1 == flongi2) {                                 //  one match, return geocoordinates
      snprintf(lati,20,"%.4f",flati1);                                           //  reformat with std. precision
      snprintf(longi,20,"%.4f",flongi1);
      zdialog_stuff(zd,"lati",lati);
      zdialog_stuff(zd,"longi",longi);
      return 1;
   }

   flatic = 0.5 * (flati1 + flati2);                                             //  multiple matches
   flongic = 0.5 * (flongi1 + flongi2);                                          //  center of enclosing rectangle
   kmrange = earth_distance(flati1,flongi1,flati2,flongi2);                      //  length of diagonal
   if (kmrange > 100) kmrange = 100;

   viewmode('M');

   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 --> stuff zdialog lat/long
   return 1;
}


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

//  find a geolocation from partial zdialog inputs and user choice of options
//  uses world cities table of locations and geocoordinates
//  zdialog widgets: location, country, lati, longi
//  location and country are inputs (may be partial leading strings)
//  all four widgets are outputs (found location and geocoordinates)
//  return: 1 = zdialog updated, 0 = no updates made

int find_worldlocs(zdialog *zd) 
{
   ch          *pp;
   GtkWidget   *parent;
   int         cc, ii, jj, kk, Nmatch, zoomlev;
   int         flocation = 0, fcountry = 0;
   ch          location[100], country[100], text[200];
   ch          lati[20], longi[20], *matches[20][2];
   ch          *picklist[20];
   ch          *choice;
   float       flati1 = 999, flati2 = -999;
   float       flongi1 = 999, flongi2 = -999;
   float       flatic, flongic, kmrange, fmpp;

   ii = load_imagelocs();                                                        //  load images geolocations table
   ii += load_worldlocs();                                                       //  load cities geolocations table
   if (! ii) return 0;

   zdialog_fetch(zd,"location",location,100);                                    //  get dialog inputs
   zdialog_fetch(zd,"country",country,100);
   strTrim2(location);
   strTrim2(country);

   if (*location) flocation = 1;                                                 //  one of these must be present
   if (*country) fcountry = 1;
   if (! flocation && ! fcountry) return 0;

   for (ii = kk = Nmatch = 0; ii < Nworldlocs; ii++)                             //  search for partial location match
   {
      if (flocation) {
         cc = strlen(location);
         if (! strmatchcaseN(location,worldlocs[ii]->location,cc)) continue;
      }
      if (fcountry) {
         cc = strlen(country);
         if (! strmatchcaseN(country,worldlocs[ii]->country,cc)) continue;
      }

      for (jj = 0; jj < Nmatch; jj++)                                            //  reject duplicate match
      {
         if (strmatch(worldlocs[ii]->location,matches[jj][0]) &&
            (strmatch(worldlocs[ii]->country,matches[jj][1]))) break;
      }
      if (jj < Nmatch) continue;

      matches[Nmatch][0] = worldlocs[ii]->location;                              //  save match
      matches[Nmatch][1] = worldlocs[ii]->country;

      if (Nmatch == 19) {
         zmessageACK(Mwin,"more than 20 matches");
         return 0;
      }
      Nmatch++;                                                                  //  count matches
      if (Nmatch == 1) kk = ii;                                                  //  note first match
   }

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

   if (Nmatch == 1) {                                                            //  one match
      strncpy0(location,worldlocs[kk]->location,100);                            //  save matching location
      strncpy0(country,worldlocs[kk]->country,100);
      goto found_location;
   }

   for (ii = 0; ii < Nmatch; ii++) {                                             //  build picklist of locations
      snprintf(text,200,"%s | %s",matches[ii][0],matches[ii][1]);
      picklist[ii] = zstrdup(text,"find_imagelocs");
   }
   parent = zd->widget[0].widget;
   choice = popup_picklist(parent,(ch **) picklist,0,Nmatch);                    //  show picklist, choose
   pp = substring(choice,'|',1);
   if (pp) strncpy0(location,pp,100);                                            //  user choice, location and country
   pp = substring(choice,'|',2);
   if (pp) strncpy0(country,pp,100);
   strTrim2(location);
   strTrim2(country);
   for (ii = 0; ii < Nmatch; ii++) zfree(picklist[ii]);

found_location:

   zdialog_stuff(zd,"location",location);                                        //  return location data to zdialog
   zdialog_stuff(zd,"country",country);

   for (ii = 0; ii < Nworldlocs; ii++)                                           //  search for location & country
   {
      if (strmatchcase(location,worldlocs[ii]->location) &&
          strmatchcase(country,worldlocs[ii]->country))
      {
         if (worldlocs[ii]->flati == 0 && worldlocs[ii]->flongi == 0) continue;  //  ignore missing values
         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;
      }
   }

   if (flati1 == 999) {                                                          //  no match, return null geocoordinates
      zdialog_stuff(zd,"lati","");
      zdialog_stuff(zd,"longi","");
      return 1;
   }

   if (flati1 == flati2 && flongi1 == flongi2) {                                 //  one match, return geocoordinates
      snprintf(lati,20,"%.4f",flati1);                                           //  reformat with std. precision
      snprintf(longi,20,"%.4f",flongi1);
      zdialog_stuff(zd,"lati",lati);
      zdialog_stuff(zd,"longi",longi);
      return 1;
   }

   flatic = 0.5 * (flati1 + flati2);                                             //  multiple matches
   flongic = 0.5 * (flongi1 + flongi2);                                          //  center of enclosing rectangle
   kmrange = earth_distance(flati1,flongi1,flati2,flongi2);                      //  length of diagonal
   if (kmrange > 100) kmrange = 100;

   viewmode('M');

   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 --> stuff zdialog lat/long
   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)      //  24.20
{
   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       *picklist[20], *choice, text[200];
   ch       *pp;
   
   nn = load_imagelocs();
   nn += load_worldlocs();
   if (! nn) {
      Plog(0,"no location data is available \n");
      return 1;
   }
   
   if (! *location && ! *country) {
      Plog(0,"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,"no matching locations");
      return 1;
   }

   for (ii = 0; ii < Nmatch; ii++)                                               //  build picklist of locations
   {
      snprintf(text,200,"%s / %s | %.5f / %.5f  %c",
                  mloc[ii], mcon[ii], mflati[ii], mflongi[ii], Fimage[ii]);
      picklist[ii] = zstrdup(text,"choose_location");
   }
   
   choice = popup_picklist(Mwin,(ch **) picklist,0,Nmatch);                      //  show picklist, choose one
   
   if (choice) strncpy0(text,choice,200);                                        //  preserve from zfree()

   for (ii = 0; ii < Nmatch; ii++) zfree(picklist[ii]);                          //  free memory
   
   if (! choice) return 1;
   
   pp = substring(text,"/|*",1);                                                 //  parse returned text
   if (! pp) return 1;                                                           //  location / country | NN.NNN / NN.NNN
   strncpy0(location,pp,100);

   pp = substring(text,"/|*",2);
   if (! pp) return 1;
   strncpy0(country,pp,100);
   
   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 cities

float nearest_loc(float lati, float longi, int &iim, int &iic)                   //  24.20
{
   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
   {
      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 geolocations 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_geolocs(zdialog *zd)
{
   ch          location[100], country[100];
   ch          lati[20], longi[20];
   float       flati, flongi;
   int         ii, err, cc, nn, found = 0;

   if (! Findexvalid) {                                                          //  should not happen
      Plog(0,"*** put_geolocs() no index \n");
      return 0;
   }

   zdialog_fetch(zd,"location",location,100);                                    //  get location and geocoordinates
   zdialog_fetch(zd,"country",country,100);
   strTrim2(location);
   strTrim2(country);
   if (! *location || ! *country) return 0;                                      //  location not complete
   if (strmatch(location,"null")) return 0;                                      //  or null
   if (strmatch(country,"null")) return 0;

   *location = toupper(*location);                                               //  capitalize location names
   *country = toupper(*country);
   zdialog_stuff(zd,"location",location);
   zdialog_stuff(zd,"country",country);

   zdialog_fetch(zd,"lati",lati,20);
   zdialog_fetch(zd,"longi",longi,20);
   strTrim2(lati);
   strTrim2(longi);

   if (! *lati && ! *longi) return 0;                                            //  geocoordinates not complete

   err = validate_latlong(lati,longi,flati,flongi);
   if (err) {                                                                    //  1 = missing, 2 = bad
      zmessageACK(Mwin,"bad latitude/longitude: %s %s",lati,longi);
      return -1;
   }
   else {
      snprintf(lati,20,"%.4f",flati);                                            //  reformat with std. precision
      snprintf(longi,20,"%.4f",flongi);
   }

   for (ii = 0; ii < 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];

   zfree(imagelocs);                                                             //  geolocs --> new table
   imagelocs = glocsB;
   Nimagelocs += 1;

   return 2;
}


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

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

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

   if (! *lati && ! *longi) goto status1;                                        //  both missing
   if (! *lati || ! *longi) goto status2;                                        //  one missing

   pp = strchr(lati,',');                                                        //  replace comma decimal point
   if (pp) *pp = '.';                                                            //    with period
   pp = strchr(longi,',');
   if (pp) *pp = '.';

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

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

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


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

//  compute the km distance between two earth coordinates

float earth_distance(float lat1, float long1, float lat2, float long2)
{
   float    dlat, dlong, mlat, dist;

   dlat = fabsf(lat2 - lat1);                                                    //  latitude distance
   dlong = fabsf(long2 - long1);                                                 //  longitude distance
   mlat = 0.5 * (lat1 + lat2);                                                   //  mean latitude
   mlat *= 0.01745;                                                              //  radians
   dlong = dlong * cosf(mlat);                                                   //  longitude distance * cos(latitude)
   dist = sqrtf(dlat * dlat + dlong * dlong);                                    //  distance in degrees
   dist *= 111.0;                                                                //  distance in km
   return dist;
}


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

//  generate a list of files and geocoordinates from the current gallery file list

int get_gallerymap()
{
   int         ii, jj, cc;
   xxrec_t     *xxrec;

   if (! Findexvalid) {                                                          //  should not happen
      Plog(0,"*** get_gallerymap() no index \n");
      return 0;
   }

   if (! navi::Gfiles) {
      zmessageACK(Mwin,"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;
      gallerymap[jj].flati = xxrec->flati;
      gallerymap[jj].flongi = xxrec->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 = "world map";                                        //  24.20

   Plog(1,"m_load_map \n");

   if (! Findexvalid) {
      zmessageACK(Mwin,"image index disabled");                                  //  no image index
      return;
   }

   if (Findexvalid == 1)
      zmessage_post(Mwin,"20/10",2,"image index not updated");

   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,null);                                           //  these have no effect      FIXME
   gdk_window_set_cursor(gdkwin,arrowcursor);
   gdk_window_set_device_cursor(gdkwin,zfuncs::mouse,arrowcursor);
// ------------------------------------------------------------------------------

   mapview = gtk_champlain_embed_get_view(GTK_CHAMPLAIN_EMBED(mapwidget));
   if (! mapview) goto fail;

   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,"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);
   void  map_regions_clickfunc(GtkWidget *, int line, int pos, int kbkey);

   zdialog     *zd;
   GtkWidget   *mtext;
   ch          *pp;
   FILE        *fid;

   F1_help_topic = "map regions";

   Plog(1,"m_map_regions \n");

   if (! Findexvalid) {
      zmessageACK(Mwin,"image index disabled");                                  //  no image index
      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("Map regions",Mwin,"Add","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","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
   textwidget_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;
         textwidget_append(mtext,0,"%s \n",pp);                                  //  write into dialog list
      }
      fclose(fid);
   }

   textwidget_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,"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
   textwidget_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;
      textwidget_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

void map_regions_clickfunc(GtkWidget *widget, int line, int pos, int kbkey)
{
   using namespace map_regions_names;

   ch       *pp1, *pp2;
   FILE     *fidr;
   zdialog  *zd = zdmapreg;

   if (! zd) return;

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

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

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

   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;

notfound:
   Plog(0,"map region not found: %s \n",regname);
   return;

baddata:
   Plog(0,"map region invalid: %s %.4f %.4f %d \n",
                  regname,reglati,reglongi,regzoom);
   return;
}


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

//  Input a (partial) location name, choose full name from picklist, 
//  goto corresponding map location

void m_map_location(GtkWidget *, ch *)                                           //  24.20
{
   zdialog  *zd;
   int      zstat, err;
   ch       location[100], country[100];
   float    flati = 0, flongi = 0;
   
   F1_help_topic = "map location";

/***
          _____________________________
         |   Go to map location        |
         |                             |
         | location [________________] |
         | country  [________________] |
         |                             |
         |                    [OK] [X] |
         |_____________________________|

***/

   zd = zdialog_new("Go to map location",Mwin,"OK","X",null);
   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,100);
   zdialog_fetch(zd,"country",country,100);
   zdialog_free(zd);
   
   err = choose_location(location,country,flati,flongi);                         //  present picklist, choose location
   if (err) return;

   map_zoomto(flati,flongi,7);                                                   //  goto map location
   Plog(2,"location: %s %s  %.5f %.5f \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 = "map markers";

   Plog(1,"m_map_markers \n");

   if (! Findexvalid) {
      zmessageACK(Mwin,"image index disabled");                                  //  no image index
      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,"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,null,"mouse");

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

   zdialog_fetch(zd,"all",showall);                                              //  show all images
   zdialog_free(zd);

   if (showall) {
      if (gallerymap) {                                                          //  free gallerymap
         for (int ii = 0; ii < Ngallerymap; ii++)
            zfree(gallerymap[ii].file);
         zfree(gallerymap);
         gallerymap = 0;
      }
   }

   else get_gallerymap();                                                        //  show gallery images only

   if (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;

   Plog(1,"m_map_zoomin \n");

   F1_help_topic = "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;

   flati = xxrec->flati;
   flongi = xxrec->flongi;
   if (flati == 0 && flongi == 0) return;

   viewmode('M');                                                                //  24.60
   map_zoomto(flati,flongi,11);
   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];
   int         mapww, maphh;
   zdialog     *zd = zd_mapgeotags;

   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           24.20
   
   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 > 10) location = 0;                                                    //  nothing within 10 km

   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);
            zdialog_stuff(zd,"lati",glati,"%.5f");                               //  5 decimal places
            zdialog_stuff(zd,"longi",glongi,"%.5f");
         }
         
         else {
            zdialog_stuff(zd,"lati",flati,"%.5f");                               //  5 decimal places
            zdialog_stuff(zd,"longi",flongi,"%.5f");
         
            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,"%.5f %.5f",flati,flongi);                          //  shift key - show coordinates
            poptext_mouse(text,20,-20,0.1,3);
         }

         else if (location)                                                      //  on marker - show corresp. images
            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 %.5f %.5f",location,flati,flongi);                //  show location, coordinates
      poptext_mouse(text,20,-20,0.1,3);
   }

   else {
      snprintf(text,20,"%.5f %.5f",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 (! Findexvalid) {
      zmessageACK(Mwin,"image index disabled");                                  //  no image index
      return;
   }

   x1 = champlain_view_longitude_to_x(mapview,flongi);                           //  target map pixel location
   y1 = champlain_view_latitude_to_y(mapview,flati);

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

   if (gallerymap)                                                               //  show gallery images at location
   {
      for (ii = 0; ii < Ngallerymap; ii++)                                       //  loop all gallery files
      {
         zmainloop();                                                            //  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];

         glati = xxrec->flati;
         glongi = xxrec->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",xxrec->file);                                     //  output matching file
            nn++;
         }
      }
   }

   fclose(fid);

   if (! nn) {
      poptext_mouse("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 key names

   file                 image file to retrieve metadata
   kname[NK]            set of key names, e.g. tags, rating, ISO, city ...
   kdata[NK]            returned key data
                        (caller must zmalloc() before and zfree() after)

   return status: 0 = OK, +N = system error (errno)
                          -1 = exiftool failure

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

int meta_get1(ch *file, ch **kname, ch **kdata, int NK)
{
   int      ii, kk, cc, err;
   ch       *file2;
   ch       *pp1, *pp2, *pp3, *pp4;
   ch       exifcommand[XFCC+500], buff[XFCC+100];
   FILE     *fid = 0;

   zfuncs::zappcrash_context1 = file;

   for (ii = 0; ii < NK; ii++)                                                   //  clear outputs
      kdata[ii] = 0;

//  build exiftool command:
//    exiftool -m -S -n -fast                                                    //  processing options
//             -d "%Y-%m-%d %H:%M:%S"   -c "%.5f"                                //  date/time and geocoordinate format
//             -keyname1 -keyname2 ... -keynameN                                 //  key names to extract
//             "/.../filename.jpg"                                               //  input file

   strcpy(exifcommand,"exiftool -m -S -n -fast -d \"%Y-%m-%d %H:%M:%S\" -c \"%.5f\" ");
   cc = strlen(exifcommand);

   for (kk = 0; kk < NK; kk++)
   {                                                                             //  append "-keyname " for each key name
      if (! kname[kk] || *kname[kk] <= ' ') 
         kname[kk] = zstrdup("??","meta_get1");                                  //  stop deadly exiftool input            24.30
      exifcommand[cc++] = '-';
      strcpy(exifcommand+cc,kname[kk]);
      cc += strlen(kname[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 key values
   
   fid = popen(exifcommand,"r");
   if (! fid) goto exiferr;

   while (true)                                                                  //  loop exiftool outputs
   {
      pp1 = fgets_trim(buff,XFCC+100,fid,1);                                     //  next exiftool output record
      if (! pp1) break;                                                          //  EOF

      pp2 = strchr(pp1,':');                                                     //  this is a key data record
      if (! pp2) continue;                                                       //  format is: keyname: keyvalue
      if (strlen(pp2) < 2) continue;                                             //             |        |                 24.60
      *pp2 = 0;                                                                  //             pp1       pp2
      pp2 += 2;
      
      for (kk = 0; kk < NK; kk++)                                                //  look for key name match
         if (strmatchcase(pp1,kname[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) continue;

      pp3 = zstrdup(pp2,"meta_get1");                                            //  get key data
      
      err = utf8_clean(pp3);                                                     //  replace bad utf8 with '?'             24.60
      if (err) Plog(0,"*** bad utf8 detected *** \n");

      if (strstr(pp1,"Date")) {                                                  //  if date/time value
         pp4 = strchr(pp3,':');                                                  //     fix yyyy:mm:dd --> yyyy-mm-dd
         if (pp4 && strlen(pp4) > 3 && *(pp4+3) == ':')                          //      ...Date...  yyyy:mm:dd
            *pp4 = *(pp4+3) = '-';                                               //                      |  |
      }                                                                          //                    pp4  +3

      kdata[kk] = pp3;                                                           //  return key data
   }

   goto OKret;

exiferr:
   errno = -1;
   goto retxx;

OKret:
   errno = 0;

retxx:
   if (fid) pclose(fid);
   if (errno) Plog(0,"meta_get1(): %s \n %s \n",file, strerror(errno));
   return 0;
}


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

   get metadata for set of image files and set of metadata key names

   files[NF]            set of image files to retrieve metadata
   kname[NK]            set of key names, e.g. tags, rating, ISO, city ...
   kdata[]              returned key 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

   bug: unknown file count limit in exiftool (about 20K)
   kludge: use up to Xsmp (64) threads

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

namespace meta_getN_names
{
   ch          **files;                                 //  caller args
   ch          **kname;
   ch          **kdata;
   int         NF, NK;
   int         TF1[Xsmp], TF2[Xsmp];                    //  file range per thread
   pthread_t   pid[Xsmp];
   int         threadbusy[Xsmp];                        //  thread busy status
   int         threaderror[Xsmp];                       //  thread error status
}


//  caller function

int meta_getN(ch **files2, int NF2, ch **kname2, ch **kdata2, int NK2)
{
   using namespace meta_getN_names;

   void * meta_getN_thread(void *arg);

   int      ff, kk, tt, errstat = 0;
   int      NTU, Frange;
   double   secs;

   files = files2;                                                               //  copy args to namespace
   NF = NF2;
   kname = kname2;
   kdata = kdata2;
   NK = NK2;

   index_updates = 0;                                                            //  files done counter (index function)

   progress_setgoal(NF);                                                         //  start progress counter

   for (kk = 0; kk < NF * NK; kk++)                                              //  clear output key data
      kdata[kk] = 0;

   for (tt = 0; tt < Xsmp; tt++)                                                 //  loop max. threads
      TF1[tt] = TF2[tt] = -1;                                                    //  file range, initially disabled

   Frange = NF / Xsmp + 1;                                                       //  files per thread

   for (ff = tt = 0; tt < Xsmp; 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
   }

   NTU = tt + 1;                                                                 //  threads used, <= Xsmp

   secs = get_seconds();

   for (tt = 0; tt < NTU; tt++) {                                                //  start NTU threads
      threadbusy[tt] = 1;
      pid[tt] = start_detached_thread(meta_getN_thread,&Nval[tt]);
   }

   for (tt = 0; tt < NTU; tt++) {                                                //  wait for threads
      while (threadbusy[tt]) zmainsleep(0.01);
      if (threaderror[tt]) errstat = threaderror[tt];
   }

   if (Fescape) goto cancel;                                                     //  24.60

   secs = get_seconds() - secs;
   Plog(2,"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:

   Plog(0,"*** 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;
   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 = <temp folder>/metalinks_N/nnnnnn
      err = symlink(files[ff],linkname);                                         //  linkname --> filename
      if (err) Plog(0,"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
//             -d "%Y-%m-%d %H:%M:%S"  -c "%.5f"                                 //  date/time and geocoordinate formats
//              2>/dev/null                                                      //  suppress exiftool warnings
//             -keyname1 -keyname2 ... -keynameN                                 //  key names to extract
//             /home/<user>/.fotocx/temp-nnnnn/metalinks_N/*                     //  folder with input files

   strcpy(exifcommand,"exiftool -m -S -n -fast -d \"%Y-%m-%d %H:%M:%S\" -c \"%.5f\" 2>/dev/null ");
   cc = strlen(exifcommand);

   for (kk = 0; kk < NK; kk++)
   {                                                                             //  append "-keyname " for each key name
      if (! kname[kk] || strlen(kname[kk]) < 1) 
         kname[kk] = zstrdup("??","meta_getN");                                  //  stop deadly exiftool input            24.20
      exifcommand[cc++] = '-';
      strcpy(exifcommand+cc,kname[kk]);
      cc += strlen(kname[kk]);
      exifcommand[cc++] = ' ';
   }

   strncpy0(exifcommand+cc,linkdir,1000-cc);                                     //  append linkdir
   strcat(exifcommand+cc,"/*");                                                  //  append "/*"

//  execute exiftool command and read outputs, filenames followed by key values

   fid = popen(exifcommand,"r");
   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;
         index_updates++;                                                        //  count index updates
         progress_addvalue(T,1);                                                 //  update progress counter
         if (Fescape) goto retxx;                                                //  user killed
         zfuncs::zappcrash_context1 = file;                                      //  note file in case zappcrash
         continue;                                                               //  next exiftool output
      }

      pp2 = strchr(pp1,':');                                                     //  this is a key data record
      if (! pp2) continue;                                                       //  format is: keyname: keyvalue
      if (strlen(pp2) < 2) continue;                                             //             |        |
      *pp2 = 0;                                                                  //             pp1       pp2
      pp2 += 2;
      err = utf8_check(pp2);                                                     //  check for utf8 bad data               24.60
      if (err) {
         Plog(0,"*** bad utf8 detected *** \n");
         continue;
      }

      cc = strlen(pp2);
      if (cc == 1 && *pp2 == '\n') continue;                                     //  ignore this shit from Sony camera
      if (cc >= metadataXcc) continue;

      for (kk = 0; kk < NK; kk++) {                                              //  look for key name match
         kk2 = ff * NK + kk;                                                     //  corresp. key data
         if (kdata[kk2]) continue;                                               //  already found, skip
         if (strmatchcase(pp1,kname[kk])) break;
      }
      if (kk == NK) continue;                                                    //  should not happen

      kk = ff * NK + kk;
      kdata[kk] = zstrdup(pp2,"meta_getN");                                      //  return key data
   }

   goto OKret;

exiferr:
   errno = -1;
   goto retxx;

OKret:
   errno = 0;

retxx:
   if (errno) Plog(0,"meta_getN(): %s \n %s \n",file, strerror(errno));
   zshell(0,"rm -R -f -d %s",linkdir);                                           //  remove linkdir
   if (fid) pclose(fid);
   threaderror[T] = errno;                                                       //  status
   threadbusy[T] = 0;                                                            //  not busy
   return 0;
}


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

//  create or change metadata for given image file and key(s)
//
//  command:
//    exiftool -m -overwrite_original -keyname="keyvalue" ... "file"
//
//  NOTE: exiftool replaces \n (newline) in keyvalue with . (period).
//  returns: 0 = OK, +N = error

int meta_put(ch *file, ch **kname, ch **kdata, int nkeys)
{
   int      ccc = 2 * metadataXcc + 500;
   ch       *file2;
   ch       exifcommand[ccc];
   ch       kdata2[metadataXcc];
   int      ii, cc, err;

   if (nkeys < 1 || nkeys > 30) zappcrash("meta_put nkeys: %d",nkeys);

   err = access(file,W_OK);                                                      //  test file can be written by me
   if (err) {
      Plog(0,"no write permission: %s \n",file);
      return 1;
   }

   sprintf(exifcommand,"exiftool -m -n -overwrite_original ");                   //  add -n 
   cc = strlen(exifcommand);

   for (ii = 0; ii < nkeys; ii++)                                                //  build exiftool inputs
   {
      if (! kdata[ii]) continue;                                                 //  skip missing data

      if (strmatchcase(kname[ii],"Aperture")) continue;                          //  skip known unwritable data
      if (strmatchcase(kname[ii],"Fnumber")) continue;

      exifcommand[cc++] = '-';                                                   //  add string -kname=
      strcpy(exifcommand+cc,kname[ii]);
      cc += strlen(kname[ii]);
      exifcommand[cc++] = '=';

      if (! *kdata[ii] || strmatch(kdata[ii]," ") || strmatch(kdata[ii],"null")) 
         *kdata2 = 0;

      else {
         repl_Nstrs(kdata[ii],kdata2,metadataXcc,"\n",". ","\"","\\\"",0);       //  replace embedded \n with ". "
         if (cc + (int) strlen(kdata2) > ccc-4) {                                //     and embedded " with \" 
            Plog(0,"meta_put() data too long \n");
            return 1;
         }
      }

      snprintf(exifcommand+cc,ccc-cc,"\"%s\" ",kdata2);                          //  append "kdata" + blank
      cc += strlen(kdata2) + 3;

      if (strmatchcase(kname[ii],"GPSLatitude")) {
         if (*kdata[ii] <= ' ')
            strcpy(exifcommand+cc,"-GPSLatitudeRef=\"\"");
         else if (*kdata[ii] == '-')                                             //  signed values not stored in metadata
            strcpy(exifcommand+cc,"-GPSLatitudeRef=S ");
         else
            strcpy(exifcommand+cc,"-GPSLatitudeRef=N ");
         cc += 18;
         exifcommand[cc++] = ' ';
      }

      if (strmatchcase(kname[ii],"GPSLongitude")) {
         if (*kdata[ii] <= ' ')
            strcpy(exifcommand+cc,"-GPSLongitudeRef=\"\"");
         else if (*kdata[ii] == '-')
            strcpy(exifcommand+cc,"-GPSLongitudeRef=W ");
         else
            strcpy(exifcommand+cc,"-GPSLongitudeRef=E ");
         cc += 19;
         exifcommand[cc++] = ' ';
      }
   }

   if (cc + (int) strlen(file) > ccc-3) {
      Plog(0,"meta_put() data too long \n");
      return 1;
   }

   file2 = zescape_quotes(file);
   snprintf(exifcommand+cc,ccc-cc,"\"%s\"",file2);                               //  append file name

   err = zshell("log",exifcommand);
   if (err) Plog(0,"meta_put() error: %s \n",file2);

   zfree(file2);

   thumbfile_set_mod_time(file);                                                 //  thumb file mod time = image file

   return err;
}


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

//  copy metadata from one image file to new (edited) image file
//  if nkeys > 0: kname[] keys get new values from kdata[]
//  Replace one call (copy and add data) with two separate calls.
//  The one call method (exiftool) did not always work.

int meta_copy(ch *file1, ch *file2, ch **kname, ch **kdata, int nkeys)
{
   int      ccc = XFCC + XFCC + 100;
   ch       *file1a, *file2a;
   ch       exifcommand[ccc];
   int      err = 0;

   err = access(file2,W_OK);                                                     //  test file can be written by me
   if (err) {
      Plog(0,"no write permission: %s \n",file2);
      return 1;
   }

   file1a = zescape_quotes(file1);
   file2a = zescape_quotes(file2);

   snprintf(exifcommand,ccc,"exiftool -m -n -tagsfromfile \"%s\" "               //  add -n
                            "-all -xmp -iptc -icc_profile "
                            "\"%s\" -overwrite_original",file1a,file2a);
   zfree(file1a);
   zfree(file2a);

   err = zshell(0,exifcommand);
   if (err) {
      Plog(0,"meta_copy() error: %s \n",file1);
      return err;
   }

   if (nkeys) {
      err = meta_put(file2, kname, kdata, nkeys);                                //  add/replace new keys/data
      if (err) {
         Plog(0,"meta_copy() error: %s \n",file1);
         return err;
      }
   }

   return 0;
}


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

//  convert between metadata and fotocx tag date formats
//  metadata date: yyyy-mm-dd hh:mm:ss         20 chars.
//       tag date: yyyymmddhhmmss              16 chars.
//

void meta_tagdate(ch *metadate, ch *tagdate)
{
   int      cc;

   memset(tagdate,0,15);
   cc = strlen(metadate);

   if (cc > 3) strncpy(tagdate+0,metadate+0,4);
   if (cc > 6) strncpy(tagdate+4,metadate+5,2);
   if (cc > 9) strncpy(tagdate+6,metadate+8,2);
   if (cc > 12) strncpy(tagdate+8,metadate+11,2);
   if (cc > 15) strncpy(tagdate+10,metadate+14,2);
   if (cc > 18) strncpy(tagdate+12,metadate+17,2);
   tagdate[14] = 0;
   return;
}

void tag_metadate(ch *tagdate, ch *metadate)
{
   int      cc;

   memset(metadate,0,20);
   cc = strlen(tagdate);

   strcpy(metadate,"1900-01-01 00:00:00");
   if (cc > 3) strncpy(metadate+0,tagdate+0,4);
   if (cc > 5) strncpy(metadate+5,tagdate+4,2);
   if (cc > 7) strncpy(metadate+8,tagdate+6,2);
   if (cc > 9) strncpy(metadate+11,tagdate+8,2);
   if (cc > 11) strncpy(metadate+14,tagdate+10,2);
   if (cc > 13) strncpy(metadate+17,tagdate+12,2);
   metadate[19] = 0;
   return;
}


/********************************************************************************
   Functions to read and write image index file on disk
   and update the image index memory table (xxrec table).
*********************************************************************************/

//  Get the image index record for the given image file.
//  Returns pointer to xxrec in memory index, or null if not found.
//  Returned xxrec fields are NOT subjects for zfree()
//  If file is not indexed or modified since index,
//    a dummy xxrec in static memory is returned.
//      (only good until next call to get_xxrec()).

xxrec_t * get_xxrec(ch *file)
{
   int      ii, jj, kk, rkk, last;
   STATB    statB;
   ch       fdate[16], *RP;

   static xxrec_t  xxrec;

   if (! file || *file != '/') return 0;

   RP = f_realpath(file);                                                        //  use real path
   if (! RP) return 0;

   if (! regfile(RP,&statB)) goto ret0;

   if (image_file_type(RP) > VIDEO) goto ret0;                                   //  not indexable

   if (! Findexvalid) goto noxxrec;                                              //  index not valid
   if (! Nxxrec) goto noxxrec;                                                   //  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) break;
      }

      else if (kk < 0) {
         ii += jj;                                                               //  too low, go forward in table
         if (ii > last) break;
      }

      else {
         compact_time(statB.st_mtime,fdate);                                     //  index record found
         if (! strmatch(fdate,xxrec_tab[ii]->fdate)) break;                      //  check mod time matches
         zfree(RP);
         return xxrec_tab[ii];                                                   //  found OK
      }

      jj = jj / 2;                                                               //  reduce increment

      if (jj == 0) {
         jj = 1;                                                                 //  step by 1 element
         if (! rkk) rkk = kk;                                                    //  save last direction
         else {
            if (rkk > 0 && kk < 0) break;                                        //  if direction change, fail
            if (rkk < 0 && kk > 0) break;
         }
      }
   }

noxxrec:                                                                         //  file not in index or index is stale
   memset(&xxrec,0,sizeof(xxrec_t));                                             //  build dummy xxrec
   xxrec.file = RP;
   xxrec.title = zstrdup("null","get_xxrec");
   xxrec.desc = zstrdup("null","get_xxrec");
   compact_time(statB.st_mtime,xxrec.fdate);
   strcpy(xxrec.pdate,"null");
   xxrec.fsize = statB.st_size;
   xxrec.tags = zstrdup("null","get_xxrec");
   xxrec.location = zstrdup("null","get_xxrec");
   xxrec.country = zstrdup("null","get_xxrec");
   xxrec.xmeta = zstrdup("null","get_xxrec");
   return &xxrec;

ret0:
   zfree(RP);
   return 0;
}


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

//  Add or update the image index record for the given image file.
//  If xxrec is null, delete the table entry for the file.
//  Append new index record data to the image index file on disk.
//  (previous record, if any, is now superseded but still present)
//  Return 0 if success, +N if error (diagnosed).

int put_xxrec(xxrec_t *xxrec, ch *file)
{
   int         ii, iix, nn, err;
   int         Fadd, Freplace, Fdelete;
   ch          *RP;
   xxrec_t     *xxrec_new = 0;
   FILE        *fid;
   STATB       statB;

   if (! file || *file != '/') {
      zmessageACK(Mwin,"put_xxrec() file: %s",file);
      return 0;
   }

   if (! Findexvalid) return 1;                                                  //  image index not valid

   RP = f_realpath(file);                                                        //  use real path
   if (! RP) xxrec = 0;
   else if (! regfile(RP,&statB)) xxrec = 0;                                     //  if not reg. file, set xxrec = null

   if (xxrec)                                                                    //  caller xxrec with valid file
   {
      xxrec_new = (xxrec_t *) zmalloc(sizeof(xxrec_t),"put_xxrec");              //  make new xxrec with data from caller

      xxrec_new->file = RP;                                                      //  note: 2 pointers to same memory

      if (xxrec->title)
         xxrec_new->title = zstrdup(xxrec->title,"put_xxrec");
      else xxrec_new->title = zstrdup("null","put_xxrec");

      if (xxrec->desc)
         xxrec_new->desc = zstrdup(xxrec->desc,"put_xxrec");
      else xxrec_new->desc = zstrdup("null","put_xxrec");

      compact_time(statB.st_mtime,xxrec_new->fdate);                             //  refresh file mod date/time
      xxrec_new->fsize = statB.st_size;                                          //  refresh file size

      if (xxrec->pdate[0]) strcpy(xxrec_new->pdate,xxrec->pdate);
      else strcpy(xxrec_new->pdate,"null");

      xxrec_new->ww = xxrec->ww;                                                 //  image pixel dimensions
      xxrec_new->hh = xxrec->hh;

      if (xxrec->rating[0]) strcpy(xxrec_new->rating,xxrec->rating);
      else strcpy(xxrec_new->rating,"0");

      if (xxrec->tags)
         xxrec_new->tags = zstrdup(xxrec->tags,"put_xxrec");
      else xxrec_new->tags = zstrdup("null","put_xxrec");

      if (xxrec->location)
         xxrec_new->location = zstrdup(xxrec->location,"put_xxrec");
      else xxrec_new->location = zstrdup("null","put_xxrec");

      if (xxrec->country)
         xxrec_new->country = zstrdup(xxrec->country,"put_xxrec");
      else xxrec_new->country = zstrdup("null","put_xxrec");

      xxrec_new->flati = xxrec->flati;
      xxrec_new->flongi = xxrec->flongi;

      if (xxrec->xmeta)
         xxrec_new->xmeta = zstrdup(xxrec->xmeta,"put_xxrec");
      else xxrec_new->xmeta = zstrdup("null","put_xxrec");
   }

   nn = -1;                                                                      //  empty xxrec_tab[] >> not found

   if (RP) {
      for (iix = 0; iix < Nxxrec; iix++) {                                       //  find file in xxrec_tab[]
         nn = strcmp(RP,xxrec_tab[iix]->file);
         if (nn <= 0) break;                                                     //  file goes before or at posn iix
      }                                                                          //    = posn to add/replace/delete
   }

   Fadd = Freplace = Fdelete = 0;

   if (nn != 0 && ! xxrec) {                                                     //  no file, no xxrec - nothing to do
      if (RP) zfree(RP);
      return 0;
   }

   if (nn != 0 && xxrec) Fadd = 1;                                               //  add new xxrec
   if (nn == 0 && ! xxrec) Fdelete = 1;                                          //  delete existing xxrec
   if (nn == 0 && xxrec) Freplace = 1;                                           //  replace existing xxrec

   if (Fdelete)
   {
      if (RP) zfree(RP);
      zfree(xxrec_tab[iix]->file);                                               //  delete existing entry
      zfree(xxrec_tab[iix]->tags);
      zfree(xxrec_tab[iix]->title);
      zfree(xxrec_tab[iix]->desc);
      zfree(xxrec_tab[iix]->location);
      zfree(xxrec_tab[iix]->country);
      zfree(xxrec_tab[iix]->xmeta);
      zfree(xxrec_tab[iix]);
      Nxxrec--;
      for (ii = iix; ii < Nxxrec; ii++)                                          //  pack down
         xxrec_tab[ii] = xxrec_tab[ii+1];
      return 0;
   }

   if (Fadd)
   {
      if (Nxxrec == maximages) {
         zmessageACK(Mwin,"exceed %d max files, cannot continue",maximages);
         quitxx();
      }

      for (ii = Nxxrec; ii > iix; ii--)                                          //  make empty slot
         xxrec_tab[ii] = xxrec_tab[ii-1];                                        //  (move up to Nxxrec pointers)
      xxrec_tab[iix] = xxrec_new;                                                //  insert new entry
      Nxxrec++;
   }

   if (Freplace)
   {
      zfree(xxrec_tab[iix]->file);                                               //  replace old entry
      zfree(xxrec_tab[iix]->tags);
      zfree(xxrec_tab[iix]->title);
      zfree(xxrec_tab[iix]->desc);
      zfree(xxrec_tab[iix]->location);
      zfree(xxrec_tab[iix]->country);
      zfree(xxrec_tab[iix]->xmeta);
      zfree(xxrec_tab[iix]);
      xxrec_tab[iix] = xxrec_new;                                                //  replace with new entry
   }

   fid = fopen(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;

   nn = fprintf(fid,"title: %s\n",xxrec_new->title);                             //  title record
   if (! nn) goto file_err;

   nn = fprintf(fid,"desc: %s\n",xxrec_new->desc);                               //  description record
   if (! nn) goto file_err;

   nn = fprintf(fid,"data: %s %s %s %d %d %d\n",                                 //  file date, photo date, rating,
                  xxrec_new->fdate, xxrec_new->pdate, xxrec_new->rating,         //    width, height, file size
                  xxrec_new->ww, xxrec_new->hh, xxrec_new->fsize);
   if (! nn) goto file_err;

   nn = fprintf(fid,"tags: %s\n",xxrec_new->tags);                               //  tags record
   if (! nn) goto file_err;

   nn = fprintf(fid,"gtags: %s^ %s^ %.4f^ %.4f\n",                               //  geotags record
         xxrec_new->location, xxrec_new->country,
         xxrec_new->flati, xxrec_new->flongi);
   if (! nn) goto file_err;

   nn = fprintf(fid,"xmeta: %s\n",xxrec_new->xmeta);                             //  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;

   return 0;

file_err:
   zmessageACK(Mwin,"image index write error \n %s",strerror(errno));
   if (fid) fclose(fid);
   fid = 0;
   return 3;
}


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

//  Read image index files sequentially, return one index record per call.
//  Set ftf = 1 for first read, will be reset to 0.
//  Returns xxrec or null for EOF or error.
//  Returned xxrec_t and its allocated pointers are subject to zfree().
//  Used by index_rebuild() function.

xxrec_t * read_xxrec_seq(int &ftf)
{
   xxrec_t        *xxrec = 0;
   static FILE    *fid = 0;
   static ch      buff[indexrecl];
   ch             *pp, *pp2;
   float          flati, flongi;
   ch             fdate[16], pdate[16], rating[4];
   int            nn, ww, hh, fsize;

   if (ftf)                                                                      //  initial call
   {
      ftf = 0;
      fid = fopen(image_index_file,"r");
      if (! fid) return 0;                                                       //  no index file ?
      *buff = 0;                                                                 //  insure no leftover data
   }

   while (true)                                                                  //  read to next "file: " record
   {
      pp = fgets_trim(buff,indexrecl,fid);
      if (! pp) {
         fclose(fid);                                                            //  EOF
         return 0;
      }
      if (strmatchN(pp,"file: ",6)) break;
   }

   xxrec = (xxrec_t *) zmalloc(sizeof(xxrec_t),"read_xxrec");                    //  allocate returned 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,"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,"data: ",6)) {
         nn = sscanf(pp+6,"%15s %15s %1s %d %d %d",
                           fdate, pdate, rating, &ww, &hh, &fsize);              //  file date, photo date, rating,
         if (nn == 6) {                                                          //    width, height, file size
            strncpy0(xxrec->fdate,fdate,16);
            if (strmatch(pdate,"undated")) strcpy(xxrec->pdate,"");
            else strncpy0(xxrec->pdate,pdate,16);
            strncpy0(xxrec->rating,rating,2);
            xxrec->ww = ww;
            xxrec->hh = hh;
            xxrec->fsize = fsize;
         }
      }

      else if (strmatchN(pp,"tags: ",6))                                         //  tags
         xxrec->tags = zstrdup(pp+6,"read_xxrec");

      else if (strmatchN(pp,"gtags: ",7)) {                                      //  geotags
         pp += 7;
         pp2 = substring(pp,"^",1);
         if (pp2 && *pp2 > ' ')
            xxrec->location = zstrdup(pp2,"read_xxrec");
         else xxrec->location = zstrdup("null","read_xxrec");
         pp2 = substring(pp,"^",2);
         if (pp2 && *pp2 > ' ')
            xxrec->country = zstrdup(pp2,"read_xxrec");
         else xxrec->country = zstrdup("null","read_xxrec");
         flati = flongi = 999;
         pp2 = substring(pp,"^",3);
         if (pp2) flati = atofz(pp2);
         pp2 = substring(pp,"^",4);
         if (pp2) flongi = atofz(pp2);
         if (flati < -90.0 || flati > 90.0) flati = flongi = 0;
         if (flongi < -180.0 || flongi > 180.0) flati = flongi = 0;
         xxrec->flati = flati;
         xxrec->flongi = flongi;
      }

      else if (strmatchN(pp,"xmeta: ",7))                                        //  extra metadata
         xxrec->xmeta = zstrdup(pp+7,"read_xxrec");
   }

   if (! xxrec->title)
      xxrec->title = zstrdup("null","read_xxrec");

   if (! xxrec->desc)
      xxrec->desc = zstrdup("null","read_xxrec");

   if (! xxrec->fdate[0])                                                        //  supply defaults for missing items
      strcpy(xxrec->fdate,"");

   if (! xxrec->pdate[0])
      strcpy(xxrec->pdate,"");

   if (! xxrec->rating[0])
      strcpy(xxrec->rating,"0");

   if (! xxrec->tags)
      xxrec->tags = zstrdup("null","read_xxrec");

   if (! xxrec->location)
      xxrec->location = zstrdup("null","read_xxrec");

   if (! xxrec->country)
      xxrec->country = zstrdup("null","read_xxrec");

   if (! xxrec->xmeta)
      xxrec->xmeta = zstrdup("null","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;
   ch             pdate[16];

   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);                                   //  output: filename record
   if (! nn) goto file_err;

   if (xxrec->title)
      nn = fprintf(fid,"title: %s\n",xxrec->title);                              //  title: text
   else nn = fprintf(fid,"title: \n");
   if (! nn) goto file_err;

   if (xxrec->desc)
      nn = fprintf(fid,"desc: %s\n",xxrec->desc);                                //  desc: text
   else nn = fprintf(fid,"desc: \n");
   if (! nn) goto file_err;

   strncpy0(pdate,xxrec->pdate,16);
   if (! *pdate) strcpy(pdate,"undated");

   nn = fprintf(fid,"data: %s %s %s %d %d %d\n",                                 //  file date, photo date, rating,
                  xxrec->fdate, pdate, xxrec->rating,                            //    width, height, file size
                  xxrec->ww, xxrec->hh, xxrec->fsize);
   if (! nn) goto file_err;

   if (xxrec->tags)
      nn = fprintf(fid,"tags: %s\n",xxrec->tags);                                //  tags: aaaaa, bbbbb, ...
   else nn = fprintf(fid,"tags: \n");
   if (! nn) goto file_err;

   nn = fprintf(fid,"gtags: %s^ %s^ %.4f^ %.4f\n",                               //  gtags: location country NN.NNNN NN.NNNN
         xxrec->location, xxrec->country, xxrec->flati, xxrec->flongi);
   if (! nn) goto file_err;

   if (xxrec->xmeta)
      nn = fprintf(fid,"xmeta: %s\n",xxrec->xmeta);                              //  xmeta: name1^ name2^ ...
   else nn = fprintf(fid,"xmeta: \n");
   if (! nn) goto file_err;

   nn = fprintf(fid,"END\n");                                                    //  EOL
   if (! nn) goto file_err;

   return 0;

file_err:
   zmessageACK(Mwin,"image index write error \n %s",strerror(errno));
   if (fid) fclose(fid);
   fid = 0;
   quitxx();
   return 2;
}
