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

   Fotocx - edit photos and manage collections

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

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

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
   See the GNU General Public License for more details.

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

   image geotag mapping (internet maps)                                                //  all functions    26.7
   ------------------------------------
   m_load_netmap           display net map (OpenFreeMap web app) 
   map_marker_event        find images within clicked marker, show gallery of images
   map_mouse_event         respond to mouse movement and clicks on map
   find_geocoordinates     convert location/country into latitude/longitude

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

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

using namespace zfuncs;

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

//  OpenFreeMap and libwebkit functions generated by Claude/Anthropic                  //  26.7


#include <webkit2/webkit2.h>

/* ------------------------------------------------------------------------
 * HTML/JS page that actually renders the map.
 * Loads MapLibre GL JS from a CDN.
 * Points the map style at OpenFreeMap's public "liberty" style
 *      https://tiles.openfreemap.org/styles/liberty
 * which is free to use with no API key.
 * ------------------------------------------------------------------------ */

static const char *MAP_HTML =
"<!DOCTYPE html>"
"<html><head>"
"<meta charset='utf-8'/>"
"<style>"
"  html, body, #map { height: 100%; margin: 0; padding: 0; }"
"</style>"
"<script src='https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.js'></script>"
"<link href='https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.css' rel='stylesheet'/>"
"</head><body>"
"<div id='map'></div>"
"<script>"
"  const map = new maplibregl.Map({"
"    container: 'map',"
"    style: 'https://tiles.openfreemap.org/styles/liberty',"
"    center: [13.388, 52.517]," /* Berlin, as a default view */                        //  initial map center
"    zoom: 5"
"  });"
"  map.addControl(new maplibregl.NavigationControl());"
"  window._markers = {};"
""
"  /* Called from C (via webkit_web_view_evaluate_javascript) to add a marker. */"
"  window.addMarker = function(id, lat, lon, label) {"
"    const el = document.createElement('div');"
"    el.style.width = '8px';"                                                          //  marker size
"    el.style.height = '8px';"
"    el.style.borderRadius = '50%';"
"    el.style.background = '#e63946';"
"    el.style.border = '2px solid white';"
"    el.style.boxShadow = '0 0 4px rgba(0,0,0,0.5)';"
"    el.style.cursor = 'pointer';"
""
"    const marker = new maplibregl.Marker({ element: el })"
"      .setLngLat([lon, lat])"
"      .setPopup(new maplibregl.Popup({ offset: 12 }).setText(label))"
"      .addTo(map);"
""
"    el.addEventListener('click', function(e) {"
"      e.stopPropagation();"
"      window.webkit.messageHandlers.markerClicked.postMessage("
"        JSON.stringify({ id: id, lat: lat, lon: lon, label: label })"
"      );"
"    });"
""
"    window._markers[id] = marker;"
"  };"
""
"  /* Called from C (via webkit_web_view_evaluate_javascript) to remove"
"   * every marker currently on the map. */"
"  window.clearMarkers = function() {"
"    for (const id in window._markers) {"
"      window._markers[id].remove();"
"    }"
"    window._markers = {};"
"  };"
""
"  /* Called from C (via webkit_web_view_evaluate_javascript) to recenter"
"   * the map at (lat, lon) and rescale it to the given zoom level. Uses"
"   * an animated flyTo() for a smoother feel; swap for map.jumpTo(...)"
"   * with the same arguments if you'd rather have an instant cut. */"
"  window.setMapView = function(lat, lon, zoom) {"
"    map.flyTo({ center: [lon, lat], zoom: zoom });"
"  };"
""
"  /* Called from C (via webkit_web_view_evaluate_javascript) to report"
"   * scale information for the map's current view. Returns a plain"
"   * comma-separated string (not JSON) so the C side can parse it with"
"   * a single sscanf() call:"
"   *   zoom,centerLat,centerLon,metersPerPixel,pxPerDegLat,pxPerDegLon"
"   */"
"  window.getScaleInfo = function() {"
"    const zoom = map.getZoom();"
"    const center = map.getCenter();"
"    const lat = center.lat;"
"    const lon = center.lng;"
""
"    /* Standard Web Mercator ground resolution formula (meters per pixel"
"     * at a given latitude and zoom level, assuming 256px tiles, which is"
"     * the convention MapLibre's zoom levels follow regardless of the"
"     * tile pixel size actually used for rendering). */"
"    const metersPerPixel ="
"      156543.03392 * Math.cos(lat * Math.PI / 180) / Math.pow(2, zoom);"
""
"    /* ~111320 m is the (near-constant) length of one degree of latitude."
"     * One degree of longitude shrinks toward the poles by cos(latitude). */"
"    const metersPerDegreeLat = 111320;"
"    const metersPerDegreeLon = 111320 * Math.cos(lat * Math.PI / 180);"
""
"    const pixelsPerDegreeLat = metersPerDegreeLat / metersPerPixel;"
"    const pixelsPerDegreeLon = metersPerDegreeLon / metersPerPixel;"
""
"    return [zoom, lat, lon, metersPerPixel, pixelsPerDegreeLat,"
"            pixelsPerDegreeLon].join(',');"
"  };"
""
"  /* Report general mouse events back to native code:"
"   *   - clicks on the map that are NOT on a marker (marker clicks are"
"   *     handled by the click listener inside addMarker() above, which"
"   *     calls e.stopPropagation() so they don't also arrive here)"
"   *   - mouse movement anywhere over the map, including over a marker"
"   * We listen on the map's outer container (not just the canvas) so"
"   * movement over marker elements -- which sit visually on top of the"
"   * canvas as separate DOM elements -- is still reported."
"   *"
"   * NOTE: the message is sent as a JSON STRING (like markerClicked"
"   * above), not as a raw JS object -- some WebKitGTK builds only"
"   * support string payloads for postMessage() on script message"
"   * handlers, and silently throw for object payloads. */"
"  const mapContainer = map.getContainer();"
""
"  function lngLatFromMouseEvent(e) {"
"    const rect = map.getCanvasContainer().getBoundingClientRect();"
"    const x = e.clientX - rect.left;"
"    const y = e.clientY - rect.top;"
"    return map.unproject([x, y]);"
"  }"
""
"  function reportMouseEvent(type, e) {"
"    try {"
"      const lngLat = lngLatFromMouseEvent(e);"
"      window.webkit.messageHandlers.mapMouseEvent.postMessage("
"        JSON.stringify({ type: type, lat: lngLat.lat, lon: lngLat.lng })"
"      );"
"    } catch (err) {"
"      console.error('reportMouseEvent failed:', err);"
"    }"
"  }"
""
"  mapContainer.addEventListener('click', function(e) {"
"    /* Ignore clicks on MapLibre's own UI controls (zoom buttons etc). */"
"    if (e.target.closest && e.target.closest('.maplibregl-ctrl')) return;"
"    reportMouseEvent('click', e);"
"  });"
""
"  mapContainer.addEventListener('mousemove', function(e) {"
"    reportMouseEvent('move', e);"
"  });"
"</script>"
"</body></html>";


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

/* Shared completion callback for our "fire and forget" evaluate_javascript()
 * calls (add_marker, clear_markers). We don't need the JS return value, but
 * we still must call the _finish() function to release the GAsyncResult,
 * and it's the right place to report any JavaScript errors. */

static void on_js_call_finished(GObject *source, GAsyncResult *result, gpointer user_data)
{
    (void)user_data;
    GError *error = NULL;
    JSCValue *value = webkit_web_view_evaluate_javascript_finish(
        WEBKIT_WEB_VIEW(source), result, &error);

    if (!value) {
        g_warning("add_marker: JavaScript error: %s", error->message);
        g_error_free(error);
        return;
    }
    /* value is owned by us; we don't need it, so just release it. */
    g_object_unref(value);
}


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

/* ------------------------------------------------------------------------
 * add_marker()
 *
 * Adds a marker to the currently displayed map by injecting a call to the
 * page's window.addMarker() JavaScript function.
 *
 * id      - a caller-chosen unique string identifying the marker
 * lat/lon - marker position in degrees
 * label   - text shown in the marker's popup and reported back on click
 * ------------------------------------------------------------------------ */

void add_marker(WebKitWebView *webview, const char *id, double lat, double lon, const char *label)
{
    if (webview == NULL) {
        g_warning("add_marker: no webview available yet");
        return;
    }

    gchar *id_json = g_strdup_printf("\"%s\"", id);
    gchar *label_json = g_strdup_printf("\"%s\"", label);
    gchar *script = g_strdup_printf("window.addMarker(%s, %f, %f, %s);",
                                     id_json, lat, lon, label_json);
    g_free(id_json);
    g_free(label_json);

    webkit_web_view_evaluate_javascript(webview, script, -1, NULL, NULL,
                                         NULL, on_js_call_finished, NULL);
    g_free(script);
}


/* ------------------------------------------------------------------------
 * on_marker_clicked()
 * ------------------------------------------------------------------------ */

static void on_marker_clicked(WebKitUserContentManager *manager,
                           WebKitJavascriptResult *js_result, gpointer user_data)
{
    (void) manager;
    (void) user_data;

    JSCValue *value = webkit_javascript_result_get_js_value(js_result);
    gchar *payload = jsc_value_to_string(value); /* JSON string from JS */
    map_marker_event(payload);
    g_free(payload);
}


/* ------------------------------------------------------------------------
 * clear_markers()
 *
 * Removes every marker currently shown on the map by calling the page's
 * window.clearMarkers() JavaScript function.
 * ------------------------------------------------------------------------ */

void clear_markers(WebKitWebView *webview)
{
    if (webview == NULL) {
        g_warning("clear_markers: no webview available yet");
        return;
    }

    /* Reuse the same completion handler as add_marker(); we don't care
     * about the (undefined) return value either way, just error reporting. */
    webkit_web_view_evaluate_javascript(webview, "window.clearMarkers();", -1,
                                     NULL, NULL, NULL, on_js_call_finished, NULL);
}


/* ------------------------------------------------------------------------
 * function6: set_map_view
 *
 * Recenters the map at (lat, lon) and rescales it to the given zoom level,
 * by calling the page's window.setMapView() JavaScript function.
 *
 * lat/lon - new map center, in degrees
 * zoom    - new zoom level (MapLibre's usual range is roughly 0-22,
 *           depending on the style's available data)
 * ------------------------------------------------------------------------ */

void set_map_view(WebKitWebView *webview, double lat, double lon, double zoom)
{
    if (webview == NULL) {
        g_warning("set_map_view: no webview available yet");
        return;
    }

    /* Format the numbers locale-independently (always using '.' as the
     * decimal separator): g_strdup_printf()'s %f is locale-sensitive, and
     * on systems where the locale uses ',' as the decimal separator that
     * would produce invalid JavaScript (e.g. "48,8566" instead of
     * "48.8566"). */
    gchar lat_buf[G_ASCII_DTOSTR_BUF_SIZE];
    gchar lon_buf[G_ASCII_DTOSTR_BUF_SIZE];
    gchar zoom_buf[G_ASCII_DTOSTR_BUF_SIZE];
    g_ascii_formatd(lat_buf, sizeof(lat_buf), "%f", lat);
    g_ascii_formatd(lon_buf, sizeof(lon_buf), "%f", lon);
    g_ascii_formatd(zoom_buf, sizeof(zoom_buf), "%f", zoom);

    gchar *script = g_strdup_printf("window.setMapView(%s, %s, %s);",
                                     lat_buf, lon_buf, zoom_buf);

    webkit_web_view_evaluate_javascript(webview, script, -1, NULL, NULL,
                                         NULL, on_js_call_finished, NULL);
    g_free(script);
}


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

/* Scale information for the map's current view, as produced by
 * get_scale_info() below. */

typedef struct {
    double zoom;
    double center_lat;
    double center_lon;
    double meters_per_pixel;        /* ground distance per screen pixel */
    double pixels_per_degree_lat;   /* screen pixels per degree of latitude */
    double pixels_per_degree_lon;   /* screen pixels per degree of longitude */
} ScaleInfo;


/* Called with the result once get_scale_info()'s async JS call completes.
 * On failure (e.g. the page hasn't loaded yet), info is NULL. */

typedef void (*ScaleInfoCallback)(const ScaleInfo *info, gpointer user_data);


/* Bundles a caller's ScaleInfoCallback + user_data so it can travel through
 * as the gpointer passed to the GAsyncReadyCallback below. */

typedef struct {
    ScaleInfoCallback callback;
    gpointer user_data;
} ScaleInfoRequest;

double meters_per_pixel;                                                               //  returned data

static void on_get_scale_info_finished(GObject *source, GAsyncResult *result,
                                        gpointer user_data)
{
    ScaleInfoRequest *req = (ScaleInfoRequest *) user_data;
    GError *error = NULL;
    JSCValue *value = webkit_web_view_evaluate_javascript_finish(
        WEBKIT_WEB_VIEW(source), result, &error);

    if (!value) {
        g_warning("get_scale_info: JavaScript error: %s", error->message);
        g_error_free(error);
        if (req->callback)
            req->callback(NULL, req->user_data);
        g_free(req);
        return;
    }

    /* window.getScaleInfo() returns a comma-separated string:
     *   zoom,centerLat,centerLon,metersPerPixel,pxPerDegLat,pxPerDegLon */
    gchar *str = jsc_value_to_string(value);
    g_object_unref(value);
    
    ScaleInfo info = { 0 };
    sscanf(str, "%lf,%lf,%lf,%lf,%lf,%lf",
                    &info.zoom, &info.center_lat, &info.center_lon,
                    &info.meters_per_pixel, &info.pixels_per_degree_lat,
                    &info.pixels_per_degree_lon);
    g_free(str);
    meters_per_pixel = info.meters_per_pixel;                                          //  save result 
    g_free(req);
}

/* ------------------------------------------------------------------------
 * get_scale_info()
 *
 * Asynchronously fetches scale information for the map's current view —
 * the zoom level, the map center, and derived measurements (meters per
 * screen pixel, and how many screen pixels correspond to one degree of
 * latitude/longitude at that position and zoom).
 *
 * This is inherently asynchronous: WebKit's JS evaluation always runs on
 * the page's own event loop and reports back later, so the result is
 * delivered via `callback` instead of a return value.
 * ------------------------------------------------------------------------ */

void get_scale_info(WebKitWebView *webview, ScaleInfoCallback callback, gpointer user_data)
{
    meters_per_pixel = -1;                                                             //  initz. invalid result

    if (webview == NULL) {
        g_warning("get_scale_info: no webview available yet");
        if (callback)
            callback(NULL, user_data);
        return;
    }

    ScaleInfoRequest *req = g_new0(ScaleInfoRequest, 1);
    req->callback = callback;
    req->user_data = user_data;

    webkit_web_view_evaluate_javascript(webview, "window.getScaleInfo();", -1,
                                         NULL, NULL, NULL,
                                         on_get_scale_info_finished, req);
    while (meters_per_pixel < 0)                                                       //  wait for result
      zmainsleep(0.01);
    
    return;
}


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

//  respond to mouse movement and clicks other than marker clicks

/* Kind of mouse event reported by on_map_mouse_event() below. */

typedef enum {
    MAP_MOUSE_EVENT_CLICK,   /* click on the map, NOT on a marker */
    MAP_MOUSE_EVENT_MOVE     /* mouse moved to a new map position, which may
                               * be over a marker */
} MapMouseEventType;

/* Info passed to on_map_mouse_event() for every general mouse event. */

typedef struct {
    MapMouseEventType type;
    double lat;
    double lon;
} MapMouseEvent;

/* Application-specific reaction to a general mouse event. Mouse movement
 * fires continuously, so this updates a status label rather than popping a
 * dialog; a click on empty map area also logs to the console. Swap this out
 * for whatever your app actually needs (e.g. updating a crosshair overlay,
 * starting a measurement tool, looking up what's at that location, etc). */

static void handle_map_mouse_event(const MapMouseEvent *event)
{
    map_mouse_event(event->type,event->lat, event->lon); 
    return;
}


/* ------------------------------------------------------------------------
 * function5: on_map_mouse_event
 *
 * Invoked whenever JavaScript posts a message to the "mapMouseEvent"
 * handler (see the click/mousemove listeners on the map container inside
 * MAP_HTML). Unlike on_marker_clicked(), this fires for:
 *   - clicks on the map that are NOT on a marker
 *   - mouse movement to any map position (including over a marker)
 *
 * The posted message is a JSON string (the same approach as
 * on_marker_clicked() above), parsed here with simple substring/strtod
 * extraction. For anything more complex, use a JSON library instead.
 * ------------------------------------------------------------------------ */

static void on_map_mouse_event(WebKitUserContentManager *manager,
                                WebKitJavascriptResult *js_result,
                                gpointer user_data)
{
    (void)manager;
    (void)user_data;

    JSCValue *value = webkit_javascript_result_get_js_value(js_result);
    gchar *payload = jsc_value_to_string(value); /* JSON string from JS */

    MapMouseEvent mouse_event = { MAP_MOUSE_EVENT_MOVE, 0.0, 0.0 };
    gboolean ok = TRUE;

    /* type: "click" or "move" */
    const char *type_key = "\"type\":\"";
    char *p = strstr(payload, type_key);
    if (p) {
        p += strlen(type_key);
        mouse_event.type = (strncmp(p, "click", 5) == 0)
                                ? MAP_MOUSE_EVENT_CLICK
                                : MAP_MOUSE_EVENT_MOVE;
    } 
    else {
        ok = FALSE;
    }

    /* lat: <number> */
    const char *lat_key = "\"lat\":";
    p = strstr(payload, lat_key);
    if (p) {
        mouse_event.lat = g_ascii_strtod(p + strlen(lat_key), NULL);
    } else {
        ok = FALSE;
    }

    /* lon: <number> */
    const char *lon_key = "\"lon\":";
    p = strstr(payload, lon_key);
    if (p) {
        mouse_event.lon = g_ascii_strtod(p + strlen(lon_key), NULL);
    } else {
        ok = FALSE;
    }

    if (ok) {
        handle_map_mouse_event(&mouse_event);
    } else {
        g_warning("on_map_mouse_event: unexpected payload: %s", payload);
    }

    g_free(payload);
}


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

/* set indicator when map load is finished */

static int load_finished = 0;

static void on_load_changed(WebKitWebView *webview, WebKitLoadEvent event, gpointer user_data)
{
    (void) user_data;
    if (event == WEBKIT_LOAD_FINISHED) load_finished = 1;
}


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

//  internet map initialization - load map and show markers for all images

WebKitWebView *g_webview = NULL;
char  *map_html = "https://tiles.openfreemap.org/";

void m_load_netmap(GtkWidget *, ch *)
{
   static int  markers_loaded = 0;
   int         ii;
   float       flati, flongi;

   printf("m_load_netmap \n");
   
   if (! g_webview)                                                                    //  initialize internet map
   {
      load_finished = 0;

      WebKitUserContentManager *content_manager = webkit_user_content_manager_new();   //  setup webkit content manager
      webkit_user_content_manager_register_script_message_handler                      //  setup marker click response function
                                    (content_manager, "markerClicked");
      g_signal_connect(content_manager, "script-message-received::markerClicked",
                                        G_CALLBACK(on_marker_clicked), NULL);

      webkit_user_content_manager_register_script_message_handler(
           content_manager, "mapMouseEvent");
      g_signal_connect(content_manager, "script-message-received::mapMouseEvent",
                      G_CALLBACK(on_map_mouse_event), NULL);

      g_webview = WEBKIT_WEB_VIEW(webkit_web_view_new_with_user_content_manager        //  connect function to content manager
                                                       (content_manager));
      g_signal_connect(g_webview,"load-changed",G_CALLBACK(on_load_changed), NULL);

      gtk_container_add(GTK_CONTAINER(Mhbox),GTK_WIDGET(g_webview));                   //  add webview to main window container
      webkit_web_view_load_html(g_webview, MAP_HTML, map_html);                        //  connect to web server
      while (! load_finished) zmainsleep(0.1);                                         //  delay until map load finished
   }

   gtk_widget_show_all(GTK_WIDGET(g_webview));                                         //  show map
   
   if (markers_loaded) return;

   load_imagelocs();                                                                   //  refresh image geolocations table

   for (ii = 0; ii < Nimagelocs; ii++)                                                 //  show markers for all images
   {
      flati = imagelocs[ii]->flati;
      flongi = imagelocs[ii]->flongi;
      add_marker(g_webview,null,flati,flongi,null);
   }

   markers_loaded = 1;
   return;
}

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

   Code from here on is from Fotocx author                                             //  26.7

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

//  respond to marker click: find images marker, show gallery of images

void map_marker_event(ch *payload)                                                     //  26.7
{
   float       flati, flongi, mlati, mlongi;
   float       hdist, hdist2, vdist, dist, mrad;
   int         ii, nn;
   ch          *pp;
   xxrec_t     *xxrec;
   FILE        *fid;
   zdialog     *zd = zd_mapgeotags;
   
   if (Xindexlev < 1) {
      index_rebuild(1,0);
      if (Nxxrec == 0) {
         zmessageACK(Mwin,TX("image index required"));
         return;
      }
   }

   get_scale_info(g_webview,null,null);                                                //  get current map scale
   if (meters_per_pixel <= 0) return;

   mrad = 4 * meters_per_pixel;                                                        //  marker radius in map meters

   pp = strstr(payload,"lat");                                                         //  get marker latitude
   if (! pp) return;
   if (pp[4] != ':') return;
   mlati = atof(pp+5);
   if (mlati < -90 || mlati > 90) return;
   
   pp = strstr(payload,"lon");                                                         //  get marker longitude
   if (! pp) return;
   if (pp[4] != ':') return;
   mlongi = atof(pp+5);
   if (mlongi < -180 || mlongi > 180) return;
   
   if (zd) {                                                                           //  if active zdialog,
      map_mouse_event(MAP_MOUSE_EVENT_CLICK,mlati,mlongi);                             //    stuff clicked location information
      return;
   }

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

   for (ii = nn = 0; ii < Nxxrec; ii++)                                                //  show all images at location
   {
      zmainloop(100);                                                                  //  keep GTK alive
      xxrec = xxrec_tab[ii];
      get_gps_data(xxrec->gps_data,flati,flongi);                                      //  file geocoordinates
      if (! flati || ! flongi) continue;
      vdist = mlati - flati;                                                           //  vert distance, degrees latitude
      hdist = mlongi - flongi;                                                         //  horz distance, degrees longitude
      hdist2 = hdist * cosf(0.0175 * mlati);                                           //  horz distance, degrees 
      dist = sqrtf((vdist * vdist) + (hdist2 * hdist2));                               //  distance in degrees
      dist = dist * 111000;                                                            //  distance in meters
      if (dist > mrad) continue;                                                       //  outside marker
      fprintf(fid,"%s\n",xxrec->file);                                                 //  output matching file
      nn++;
   }

   fclose(fid);

   if (! nn) {
      poptext_mouse(TX("No 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;
}


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

//  respond to mouse movement and clicks on map (not on marker) 
//  clicks: look for pending dialog and supply location information if available
//  movements: show known location name close to mouse pointer location

void map_mouse_event(int type, float flati, float flongi)                              //  26.7
{
   int         iim, iic, mrad, Fuseloc = 0;
   ch          *location = 0, *country = 0;
   float       glati, glongi, km;
   ch          text[100], gps_data[24];
   zdialog     *zd = zd_mapgeotags;
   
   glati = glongi = 0;

   km = nearest_loc(flati,flongi,iim,iic);                                             //  find nearest known location

   if (iim > -1) {
      glati = imagelocs[iim]->flati;                                                   //  nearest image location
      glongi = imagelocs[iim]->flongi;
      location = imagelocs[iim]->location;
      country = imagelocs[iim]->country;
   }

   if (iic > -1) {                                                                     //  nearest world cities location
      glati = worldlocs[iic]->flati;
      glongi = worldlocs[iic]->flongi;
      location = worldlocs[iic]->location;
      country = worldlocs[iic]->country;
   }
   
   if (! location) return;                                                             //  should not happen

   get_scale_info(g_webview,null,null);                                                //  get current map scale
   mrad = 4 * meters_per_pixel;                                                        //  marker radius in map meters
   if (mrad * 0.001 > km) Fuseloc = 1;                                                 //  location < marker size from mouse

   if (zd && type == MAP_MOUSE_EVENT_CLICK)                                            //  stuff active dialog
   {
      if (Fuseloc) {
         zdialog_stuff(zd,"location",location);                                        //  known location clicked
         zdialog_stuff(zd,"country",country);                                          //  stuff location and geocoordinates
         snprintf(gps_data,24,"%.4f %.4f",glati,glongi);
         zdialog_stuff(zd,"gps_data",gps_data);
      }
      else {
         snprintf(gps_data,24,"%.4f %.4f",flati,flongi);                               //  new location clicked
         zdialog_stuff(zd,"gps_data",gps_data);                                        //  stuff geocoordinates only
         add_marker(g_webview,null,flati,flongi,null);                                 //  add new marker
      }

      zdialog_send_event(zd,"geomap-click");                                           //  activate calling dialog
      return;
   }

   if (Fuseloc) {
      snprintf(text,100,"%s \n %.4f %.4f",location,flati,flongi);                      //  show location and coordinates
      poptext_mouse(text,20,-20,0.1,3);                                                //    close to mouse
   }
   else {
      snprintf(text,20,"%.4f %.4f",flati,flongi);                                      //  show coordinates only
      poptext_mouse(text,20,-20,0.1,3);
   }

   return;
}


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

//  go to the map position of the selected image file or current image file

void m_show_on_map(GtkWidget *, ch *)                                                  //  26.7
{
   ch             location[40], country[40], gps_data[24];
   static ch      *file = 0;
   float          flati, flongi;
   xxrec_t        *xxrec;
   zdialog        *zd = zd_mapgeotags;

   F1_help_topic = TX("SHOW ON MAP");
   
   if (zd)                                                                             //  active dialog
   {
      zdialog_fetch(zd,"location",location,40);                                        //  get location data from dialog
      zdialog_fetch(zd,"country",country,40);
      zdialog_fetch(zd,"gps_data",gps_data,24);
      get_gps_data(gps_data,flati,flongi);
   }
   
   else                                                                                //  else look for file
   {
      if (file) zfree(file);
      file = 0;

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

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

      strncpy0(location,xxrec->location,40);                                           //  get location data from file
      strncpy0(country,xxrec->country,40);
      get_gps_data(xxrec->gps_data,flati,flongi);
   }

   if (flati == 0 && flongi == 0) {                                                    //  geocoordinates not found
      find_geocoordinates(location,country,flati,flongi);                              //  try to get from location 
      if (! flati || ! flongi) {
         printf("no geocoordinates found");
         return;
      }
   }

   viewmode('M');                                                                      //  show location on map
   set_map_view(g_webview,flati,flongi,8); 
   return;
}


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

//  Find geocoordinates for a given location and country.
//  Searches both known locations and world cities locations
//  Return: 0 = not found, 1 = found one match, 2 = found multiple close matches
//  If return 2, returned geocoordinates = center of box enclosing matching locations

int find_geocoordinates(ch *location, ch *country, float &glati, float &glongi)        //  26.7
{
   int      nn, ii, cc;
   float    flati, flongi;
   float    minflati, maxflati, minflongi, maxflongi;
   
   glati = glongi = 0;

   nn = load_imagelocs();                                                              //  load image and world locations tables
   nn += load_worldlocs();                                                             //  (if not already)
   if (! nn) return 0;

   minflati = 999;
   maxflati = -999;
   minflongi = 999;
   maxflongi = -999;

   for (ii = 0; ii < Nimagelocs; ii++)                                                 //  search all image files 
   {                                                                                   //    for location and country
      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;
      }
      
      flati = imagelocs[ii]->flati;
      flongi = imagelocs[ii]->flongi;
      if (! flati || ! flongi) continue;
      
      if (flati < minflati) minflati = flati;
      if (flati > maxflati) maxflati = flati;
      if (flongi < minflongi) minflongi = flongi;
      if (flongi > maxflongi) maxflongi = flongi;
   }
   
   for (ii = 0; ii < Nworldlocs; ii++)                                                 //  search world locations table
   {                                                                                   //    for location and country
      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;
      }
      
      flati = worldlocs[ii]->flati;
      flongi = worldlocs[ii]->flongi;
      if (! flati || ! flongi) continue;
      
      if (flati < minflati) minflati = flati;
      if (flati > maxflati) maxflati = flati;
      if (flongi < minflongi) minflongi = flongi;
      if (flongi > maxflongi) maxflongi = flongi;
   }

   if (minflati == 999) {                                                              //  no match found
      glati = glongi = 0;
      return 0;
   }

   if (minflati == maxflati && minflongi == maxflongi) {                               //  one matching location
      glati = minflati;
      glongi = minflongi;
      return 1;
   }
   
   if (maxflati - minflati < 1 &&                                                      //  multiple matching locations
       (maxflongi - minflongi) * cosf(maxflati) < 1)                                   //    within 1 degree (111 km)
   {
      glati = 0.5 * (minflati + maxflati);                                             //  return middle of enclosing box
      glongi = 0.5 * (minflongi + maxflongi);
      return 2;
   }
   
   else {
      printf("*** multiple matches, too far apart \n");
      glati = glongi = 0;                                                              //  return no match
      return 0;
   }
}


