<?php declare(strict_types=1);  
  namespace NZTopo; # 
  if (!defined('PmWiki')) exit();
  DEFINE ('NZTOPONAME', 'NZTopo');
  DEFINE ('NZTOPONEW', ''); # empty string for production version
  DEFINE ('NZTOPOID', NZTOPONAME . NZTOPONEW);
  if ($DisableRecipe[NZTOPONAME] === true) return;
/* NZTopo 
Use https://www.topomap.co.nz/ API to display an excerpt from a map in PmWiki
See https://www.pmwiki.org/wiki/Cookbook/NZTopo and http://kiwiwiki.nz/pmwiki/pmwiki.php/Cookbook/NZTopo
+
  Copyright 2013-2022 Simon Davis
  This text is written for PmWiki; 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 pmwiki.php for full details and lack of warranty.
+
# 2022-02-22 use namespace, filename can now be a PmWiki page name, revise quoted string regex
# 2022-01-10 strict_types=1, use type hints, use <figure>, implement caption
# 2021-06-18 Fix incorrect placement of "-" in regex for PHP 7.4
# 2021-04-21 Caption, inline-block
# 2018-07-10 Don't generate zoom parameter when llbs or nzbs
# 2018-05-14 Call topomap with https
# 2018-05-12 PHP 7.2
# 2015-07-01 Attachments corrected (gpx, kml requires extra processing)
# 2014-08-09 Markup_e PHP 5.5 compatible, add mapref parameter
# 2013-07-15 Initial version
*/
# Version date
  $RecipeInfo[NZTOPONAME]['Version'] = '2022-02-22' . NZTOPONEW;
  $FmtPV['$NZTopoVersion'] = "'" . NZTOPONAME . " version {$RecipeInfo[NZTOPONAME]['Version']}'"; // return version as a custom page variable

  \SDV($NZTopoZoom, 13); # set default zoom factor
  \SDV($NZTopoNewwin, 1); # set default new window setting
  \SDV($NZTopoDebug, false); # set default debug setting
  \SDV($HTMLStylesFmt[NZTOPONAME], # set default styles
    '.nztopo figcaption {font-size:smaller;}'
    . '.nztopodebug {font-size:smaller;}');

# declare $NZTopo for (:if enabled NZTopo:) recipe installation check
  $NZTopo = true; # enabled
# set debug flag
  $nztopo_debugon = boolval ($NZTopoDebug); # if on writes input and output to web page
  if ($nztopo_debugon) tpmsg ('<hr>' . __FILE__, $RecipeInfo[NZTOPONAME]['Version']);
// Initialise constants
    DEFINE ('NL', "\n");
    DEFINE ('BR', '<br/>' . NL);
    DEFINE ('SRC_URL',     'https://www.topomap.co.nz/NZTopoMap'); # URL for browser service
    DEFINE ('SRC_URLEMB',  'https://www.topomap.co.nz/NZTopoMapEmbedded'); # URL for embedded service
    DEFINE ('SRC_PROXY',   'https://www.topomap.co.nz/proxy.ashx?'); # URL for gpx proxy
    DEFINE ('APV_VERSION', 'v=2'); # fixed version of nztopomap interface
## Add a PmWiki custom markup 
// This markup ensures the user is syntactically correct when entering the parameters
# (:nztopo ll=-41.293722,174.871482 :)
# (:nztopo topo50=BP33912708,BP33876687 height=300 width=400 pin=1 label='destination' float=right clear=both zoom=12:)
# (:nztopo mapref=BN33991890:)
## the following builds the regex to parse the NZTopo PmWiki directive input parameters
# directive arguments are
#  ll= -- decimal latitude,longitude
#  llbs = -- -- decimal latitude,longitude;latitude,longitude
  $vlat     = '[-+]?\d{1,2}[.]\d+'; # latitude -90 .. 90
  $vlong    = '[-+]?[1]?\d{1,2}[.]\d+'; # longitude -180 .. 180
  $vlatlong = $vlat . '[,]' . $vlong; # latitude,longitude
  $pll      = 'll=' . $vlatlong;
  $pllbs    = 'llbs=' . $vlatlong . '(?:[;]' . $vlatlong . ')+'; # two or more pairs of lat long co-ordinates separated by semicolons
#   nztm
  $vnztm    = '\d{7}(?:[.]\d{0,3})?'; # single NZTM co-ordinate e.g. 1234567(.123)
  $vnztm2   = $vnztm . '[,]' . $vnztm; # pair of NZTM co-ordinates: easting; northing, separated by comma
#   mapref
  $mapref   ='[ABC][A-Z][0-4]\d[ ]?\d{6}'; # topo50 map reference e.g. BN33991890, BN33 991890
## parameters
  $pnzne    = 'nzne=' . $vnztm2;
  $pnzbs    = 'nzbs=' . $vnztm2 . '(?:[;]' . $vnztm2 . ')+'; # two or more pairs of NZTM co-ordinates separated by semicolons
#   topo50= --  topo50 grid coordinate, or two grid coordinates (not currently implemented in nztopomap)
  $ptopo50  = 'topo50=' . $mapref . '(?:[,]' . $mapref .')?'; # separated by comma
## -- only one of topo50, ll, llbs, nzne, nzbs,, kml or gpx can be supplied
# Use PmWiki PageNameChars and UploadNameChars sets to define group, name, and filename syntax
  # see https://regex101.com/r/dc2mho/1
  $vnamepagefile = "(?:[$PageNameChars]+(?:\.|\/)){0,2}" # optional group/page name and separator (/ or .) repeated
                 . "[$UploadNameChars]+\."; # file name and dot, extension is added later
  //if ($nztopo_debugon) tpmsg ('vpagenum', qt ($vnamepagefile . 'gpx', true) . ' PN:"' . $PageNameChars . '" Up:"' . $UploadNameChars . '"');
  $vscheme  = 'https?:\/\/'; # 
  # see https://regex101.com/r/Nrtqlt/1
  $vurl     = $vscheme . '.*'; # 
  $vurlunqt = $vscheme . '[^\s]*'; # 
#   kml= -- URL or wiki page to kml file
  $pkml     = 'kml=(?:' . qt ($vurl) . '|' . qt ($vnamepagefile . 'kml', true) . '|' . $vurlunqt . ')';

  if ($nztopo_debugon) { # add extra test parameters
    $pkml2  = 'kml2=(?:' . qt ($vurl) . '|' . $vurlunqt . ')'; # added for debug and testing
    $pkml3  = 'kml3=(?:' . qt ($vurl) . '|' . $vurlunqt . ')'; # added for debug and testing
    $pkml4  = 'kml4=(?:' . qt ($vurl) . '|' . $vurlunqt . ')'; # added for debug and testing
  } # debugon

#   gpx= -- URL or wiki page to gpx file, see https://regex101.com/r/dc2mho/7
    $pgpx     = 'gpx=(?:' . qt ($vnamepagefile . 'gpx', true) . '|' . qt ($vurl) . '|' . $vurlunqt . ')';
    //if ($nztopo_debugon) tpmsg ('pgpx', $pgpx);
#   map reference
  $pmapref  = 'mapref=' . qt ($mapref, true);
  DEFINE ('LENUNITS', '\d{1,5}(?:px)?'); # units can only be pixels
#   height= -- image height in pixels
  $pheight  = 'height=' . LENUNITS;
#   width= -- image width in pixels
  $pwidth   = 'width=' . LENUNITS;
#   pin= -- show pin
  $ppin     = 'pin=[01]';
#   label -- tool tip label for pin
  $qtstr = '.+?';
  $plabel   = 'label=' . qt($qtstr); # provide for single and double quoted strings
#   zoom= -- scale factor for map
  $pzoom    = 'zoom=\d{1,2}';
#   float= -- left or right
  $pfloat   = 'float=(?:left|right)';
#   clear= -- left, right, both
  $pclear   = 'clear=(?:left|right|both)';
#   caption = string
  $pcaption = 'caption=' . qt($qtstr); # provide for single and double quoted strings
## only one location can be supplied
  $qlocale  = '(?:' . $pll . ')|(?:' . $pllbs . ')|(?:' . $pkml . ')|(?:' . $pgpx . ')|(?:' . $pnzne . ')|(?:' . $pnzbs . ')|(?:' . $pmapref . ')';
  if ($nztopo_debugon) { # add extra test parameters
	$qlocale .= '|(?:' . $pkml2 . ')|(?:' . $pkml3 . ')|(?:' . $pkml4 . ')';
  } # debugon
# display modifications
  $qmods    = "(" . $pheight . ")\s*|(" . $pwidth . ")\s*|(" . $ppin . ")\s*|(" . $plabel . ")\s*|(" . $pzoom . ")\s*|(" . $pfloat . ")\s*|(" . $pclear .  ")\s*";
# captions
  $qdesc    = "(" . $pcaption . ")\s*";
##
  $MarkupParam = "/\\(:" . mb_strtolower(NZTOPONAME) . '\s+?(' . $qlocale . ")\s*?(?:" . $qmods . "){0,7}\s*?(?:" . $qdesc . "){0,1}\s*?:\\)/i"; # 
  # see https://regex101.com/r/hUTcCN/3
  # s = dot matches all chars including newline
  # i = case insensitive
  if ($nztopo_debugon) tpmsg ('markup', $MarkupParam);
  \Markup(NZTOPOID, # an internal PmWiki function that defines the custom markup for the wiki (see https://www.pmwiki.org/wiki/PmWiki/CustomMarkup)
    'directives',
    $MarkupParam,
    __NAMESPACE__ . '\NZTopo_Parse' );
  #
  return; # NZTopo processing complete

# Keep prevents PmWiki markup being applied
#
/** Main NZTopo parser
 *   /param   arguments as documented above
 *   /return  The HTML-formatted NZTopo markup wrapped in a <figure> of class "nztopo", see https://www.topomap.co.nz/.
 */
function NZTopo_Parse(array $m):string {
#
global $NZTopoZoom, $NZTopoNewwin; # configuration variables from configuration file, e.g. config.php
global $vnamepagefile;
global $nztopo_debugon; # for debug
// Initialise variables
  $retval    = ''; # return value
  $opt       = \ParseArgs(implode (' ', array_slice ($m, 1))); # see pmWiki documentation https://www.pmwiki.org/wiki/Cookbook/ParseArgs
  $newwin    = $NZTopoNewwin; # open map in new window
  //
  ## need dpi to calculate size of map to ask for, have not been told what topomap generates
  $ppcm      = 80; # 80 pixels per centimetre to provide base scale for maps (guess)
  //
  $width     = intval($opt['width']); # iframe parameter
  $height    = intval($opt['height']); # iframe parameter
  $float     = array_key_exists ('float', $opt)  ? 'float:' . $opt['float'] . ';' : ''; # layout parameter
  $clear     = array_key_exists ('clear', $opt)  ? 'clear:' . $opt['clear'] . ';' : ''; # layout parameter
  $position   = (bool) $clear . $float ? ' style="' . $clear . $float . '"' : ''; # layout positioning
  
## query string parameters
  $bllbs = array_key_exists ('llbs', $opt);
  if( $bllbs ) { # get size of map in km
    list ($widthkm, $heightkm) = llbskm($opt['llbs']);
    $width  = max ($widthkm * $ppcm, $width); # override height
    $height = max ($heightkm * $ppcm, $width); # override height
  }
  $bnzbs = array_key_exists ('nzbs', $opt);
  if( $bnzbs ) { # get size of map in km
    list ($widthkm, $heightkm) = nzbskm($opt['nzbs']); 
    $width  = max ($widthkm * $ppcm, $width); # override height
    $height = max ($heightkm * $ppcm, $width); # override height
  }
## build query string (i.e. topomap API)
  $query  = ''; # initialise
  $query .= array_key_exists ('ll', $opt)      ? '&ll='     . $opt['ll']     : '';
  $query .= array_key_exists ('llbs', $opt)    ? '&llbs='   . $opt['llbs']   : '';
  $query .= array_key_exists ('topo50', $opt)  ? '&topo50=' . $opt['topo50'] : '';
  $query .= array_key_exists ('nzbs', $opt)    ? '&nzbs='   . $opt['nzbs']   : '';
  $query .= array_key_exists ('nzne', $opt)    ? '&nzne='   . $opt['nzne']   : '';
  // 
# if kml or gpx parameter see if wiki address used for file
  switch (true) {
      case array_key_exists ('kml', $opt):           
          if (1 == preg_match ('/^' . $vnamepagefile . '/i', $opt['kml'])) {
              list ($fileexists, $kmlopt) = CheckWikiFile ($opt['kml']);
              //if ($nztopo_debugon) tpmsg ('kmlfile', '"' . $opt['kml'] . '", "' . $kmlopt . '"');
              if (! $fileexists) return $kmlopt; # return file upload code to PmWiki (don't use Keep)
          } else {
              $kmlopt = strval ($opt['kml']);
          }
          $query .= '&kml='    . encode_kml($kmlopt); # call function to comply with topomap API
          break;
      case array_key_exists ('gpx', $opt):          
          if (1 == preg_match ('/^' . $vnamepagefile . '/i', $opt['gpx'])) {
              # I'm not clear why it doesn't work when I specify $/i
              list ($fileexists, $gpxopt) = CheckWikiFile ($opt['gpx']);
              //if ($nztopo_debugon) tpmsg ('gpxfile', '"' . $opt['gpx'] . '", "' . $gpxopt . '"');
              if (! $fileexists) return $gpxopt; # return file upload code to PmWiki (don't use Keep)
          } else {
              $gpxopt = strval ($opt['gpx']);
          }
          $query .= '&gpx='    . rawurlencode(SRC_PROXY . htmlentities(rawurldecode($gpxopt)));
          break;
  } # end switch
  # trim URLs for spaces and quotes; rawurldecode URLs before rawurlencode them for nztopomap
  # rawurldecode resets any pasted url to a 'known' state
//$query .= array_key_exists ('kml', $opt) ? '&kml='    . rawurlencode(htmlentities(rawurldecode(trim($kmlopt, ' \'\"')))) : ''; # does not comply with topomap API

if ($nztopo_debugon) { # apply different combinations of encoding for match with topomap API
  # kml filename double rawurlencode (for debug and testing only)
  $query .= array_key_exists ('kml2', $opt) ? '&kml='    . rawurlencode(rawurlencode(htmlentities(rawurldecode(trim($opt['kml2'], ' \'\"'))))) : '';
  # kml filename single rawurlencode with no rawurldecode (for debug and testing only)
  $query .= array_key_exists ('kml3', $opt) ? '&kml='    . rawurlencode(htmlentities(trim($opt['kml3'], ' \'\"'))) : '';
  # kml filename double rawurlencode with no rawurldecode (for debug and testing only)
  $query .= array_key_exists ('kml4', $opt) ? '&kml='    . rawurlencode(rawurlencode(htmlentities(trim($opt['kml4'], ' \'\"')))) : '';
} # end debugon

  //
  $query .= array_key_exists ('mapref', $opt) ? '&mapref=' . $opt['mapref'] : '';
  $query .= array_key_exists ('pin', $opt)    ? '&pin='    . $opt['pin']    : '';
  $query .= array_key_exists ('label', $opt)  ? '&lbl='    . rawurlencode(trim($opt['label'])) : ''; # trim spaces, encode special chars for them to work with the topo map
  if(! ($bnzbs OR $bllbs)) { # zoom cannot be a parameter for these options
    $query .= array_key_exists ('zoom', $opt) ? '&z='      . $opt['zoom']   : '&z=' . $NZTopoZoom;
  }
  $caption = array_key_exists ('caption', $opt) ? htmlentities(trim($opt['caption'])) : ''; # trim spaces
  $query .= '&new=' . $newwin; # set open in new window option
  $awidth  = (bool) $width  ? ' width="'  . $width  . '"' : ''; # iframe parameter
  $aheight = (bool) $height ? ' height="' . $height . '"' : ''; # iframe parameter
  $src    = SRC_URLEMB . '?' . APV_VERSION . $query;

  $retval .= '<:block><figure class="nztopo"' . $position . '>' . NL;
  $retval .= '<iframe class="nztopoframe" title="Display NZ Topo map" loading="lazy" frameborder="0" scrolling="no" marginheight="0" marginwidth="0"' . $awidth . $aheight . ' src="' . $src . '">';
  $retval .= NL . '</iframe>' . NL;
  if (!empty ($caption)) $retval .= '<figcaption>' . $caption . '</figcaption>' . NL;
  $retval .= '</figure>' . NL;
/* example query string for nztopomap https://www.topomap.co.nz/NZTopoMap or https://www.topomap.co.nz/NZTopoMapEmbedded
?v=2&ll=-41.293722,174.871482&z=15&pin=1
?v=2&kml=https%3A%2F%2Fkiwiwiki.co.nz%2Fpmwiki%2Fuploads%2FTest%2FNZTopo-Attach%2F29%2520May%25202015%252020_01_35.kml
?v=2&gpx=https%3A%2F%2Fwww.topomap.co.nz%2Fproxy.ashx%3Fhttps%3A%2F%2Fkiwiwiki.co.nz%2Fpmwiki%2Fuploads%2FTest%2FNZTopo-Attach%2F20150531.gpx
*/
    if( $nztopo_debugon ) { # display inputs and outputs to wiki page
        //tpmsg ('m', $m);
        tpmsg ('opt', $opt);
        tpmsg ('output', $retval);
    } # debugon
  return \Keep($retval); # Keep prevents PmWiki markup being applied
} # end NZTopo_Parse

// generate regex for quoted parameters
function qt (string $parm, bool $unqt = false):string {
    # use a named regex capture group to allow a parameter to be single or double quoted, 
    static $namenr = 0; # to generate unique names
    # named capture group is (?<q1>\'|\"|),  named back reference is \k<q1>)
    $retval = '(?<q' . ++$namenr . '>\\\'|\"';
    if ($unqt) $retval .= '|'; # allow unquoted strings
    return $retval . ')(?:' . $parm . ')\k<q' . $namenr . '>';
    # the only drawback in the the quote (or null) is in a captured group and is returned by the regex
}
// check wiki file exists when passed groupname.pagename/filename.ext
function CheckWikiFile (string $wikifilename):array {
    global $UploadDir, $FmtV, $nztopo_debugon, $pagename;
    # check if wiki file exists
    # if it does exist return FQDN
    # if it doesn't exist return markup to allow file to be uploaded
    $wikifilefqdn = \DownloadUrl ($pagename, $wikifilename); #PmWiki function, returns the public URL of an attached file or false if it doesn't exist
    //if ($nztopo_debugon) tpmsg ('wikifile', '"' . $wikifilename . '", "' . strval ($wikifilefqdn) . '", "' . $FmtV['$LinkUpload'] . '"');
    if (! $wikifilefqdn === false) { # file exists
        return [true, $wikifilefqdn];
    }
    $wikimarkup = 'Upload: [[Attach:' . $wikifilename . ' | ' . $wikifilename . ']]' . BR; # 
    # note that the markup [[>>]] already seems to have been processed by the time we return this
    return [false, $wikimarkup];
}

function encode_kml(string $kml_url):string {
  global $nztopo_debugon; # for debug
# for the kml parameter the topomap API is inconsistent. The kml path is urlencoded separately to the full url string
  // specifically rawurlencode(SRC_PROXY . rawurldecode(trim(filename.gpx, ' \'\"'))) works,
  // but          rawurlencode(            rawurldecode(trim(filename.kml, ' \'\"'))) does not
	$temp_url = # decode for parse_url and to start with known state (user may for example provide spaces in the URL string)
	    htmlentities(         # protect against injection attacks
		rawurldecode(trim($kml_url, ' \'\"'))); # trim single and double quotes and spaces
	$parsed_url = parse_url($temp_url); # extract url components
	if( $nztopo_debugon ) tpmsg ('encode_kml path', $parsed_url['path']); # debug output
	$parsed_url['path'] = rawurlencode(substr($parsed_url['path'], 1)); # encode path separately again,  dropping leading '/'
	if( $nztopo_debugon ) tpmsg ('encode_kml enc_path', $parsed_url['path']); # debug output
	$enc_url = ''; # reconstruct URL
	$enc_url .= array_key_exists ('scheme', $parsed_url) ? $parsed_url['scheme'] . '://' : ''; # aka http or https
	$enc_url .= array_key_exists ('host', $parsed_url)   ? $parsed_url['host']           : ''; # domain name
	$enc_url .= array_key_exists ('port', $parsed_url)   ? ':' . $parsed_url['port']     : ''; # port
	$enc_url .= array_key_exists ('path', $parsed_url)   ? '/' . $parsed_url['path']     : ''; # add '/' dropped before as topomap considers it part of the host
	# query and fragment omitted
	return rawurlencode ($enc_url); # finally encode url and return it
} # end encode_kml

function llbskm(string $llbs):array {
  global $nztopo_debugon; # for debug
  $latlngs = preg_split ('/[,;]/', $llbs);
  $lats = array(); # eastings
  $lngs = array(); # northings
  foreach ($latlngs as $k => $v) {
    $k % 2 == 0 ? $lats [] = $v : $lngs [] = $v;
  }
  $maxlat = max ($lats);
  $maxlng = max ($lngs);
  $minlat = min ($lats);
  $minlng = min ($lngs);
/* rectangle coordinates are (lat/long)
       left:      right:
  top: max/min    max/max
  bot: min/min    min/max
*/
  $width1  = distance ($maxlat, $minlng, $maxlat, $maxlng);
  $width2  = distance ($minlat, $minlng, $minlat, $maxlng);
  $height1 = distance ($maxlat, $minlng, $minlat, $minlng);
  $height2 = distance ($maxlat, $maxlng, $minlat, $maxlng);
  $widthkm  = ceil (max ($width1, $width2) * 10) / 10; # take largest width and round up to km
  $heightkm = ceil (max ($height1, $height2) * 10) / 10; # take largest height and round up to km
  if( $nztopo_debugon ) {
    $dbgval = 'lats="' . var_export ($lats, TRUE) . BR;
    $dbgval .= 'lngs="' . implode ('", "', $lngs) . BR;
    $dbgval .= 'minlat=' . $minlat . ', maxlat=' . $maxlat . ', minlng=' . $minlng . ', maxlng=' . $maxlng . BR;
    $dbgval .= 'w1=' . $width1 . ', w2=' . $width2 . ', h1=' . $height1 . ', h2=' . $height2 . ', w=' . $widthkm . 'km, h=' . $heightkm . 'km';
    tpmsg ('llbskm', $dbgval);
  } # debugon
  return array ($widthkm, $heightkm);
} # end llbskm

function nzbskm(string $nzbs):array {
  global $nztopo_debugon; # for debug
  $nthsests = preg_split ('/[,;]/', $nzbs); # https://www.linz.govt.nz/land/maps/linz-topographic-maps/topo50-maps/features-topo50-map/topo50-prototype-map-reading
  $nths = array(); # northings
  $ests = array(); # eastings
  foreach ($nthsests as $ky => $val) {
    $ky % 2 == 0 ? $nths [] = $val : $ests [] = $val;
  }
  $maxnth = max ($nths);
  $maxest = max ($ests);
  $minnth = min ($nths);
  $minest = min ($ests);
/* rectangle coordinates are (nth/est)
       left:      right:
  top: max/min    max/max
  bot: min/min    min/max
*/
  $width1  = ($maxest - $minest) / 1000; # metres to km
  $height1 = ($maxnth - $minnth) / 1000; # metres to km
  $widthkm  = ceil ($width1 * 10) / 10; # round up to 0.1 km
  $heightkm = ceil ($height1 * 10) / 10; # round up to 0.1 km
  if( $nztopo_debugon ) {
    $dbgval = 'nthsests="' . implode ('", "', $nthsests) . BR;
    $dbgval .= 'ests="' . implode ('", "', $ests) . BR;
    $dbgval .= 'nths="' . implode ('", "', $nths) . BR;
    $dbgval .= 'minest=' . $minest . ', maxest=' . $maxest . ', minnth=' . $minnth . ', maxnth=' . $maxnth . BR;
    $dbgval .= 'w1=' . $width1 . ', h1=' . $height1 . ', w=' . $widthkm . 'km, h=' . $heightkm . 'km';
    tpmsg ('nzbskm', $dbgval);
  } # debugon
  return array ($widthkm, $heightkm);
} # end nzbskm

# https://web.archive.org/web/20170604082154/http://snipplr.com/view/2531, https://inkplant.com/code/calculate-the-distance-between-two-points
function distance ($lat1, $lng1, $lat2, $lng2):float
# convert distance in km between two lat longs
{
	$pi80 = M_PI / 180;
	$lat1 *= $pi80;
	$lng1 *= $pi80;
	$lat2 *= $pi80;
	$lng2 *= $pi80;
 
	$r = 6372.797; // mean radius of Earth in km
	$dlat = $lat2 - $lat1;
	$dlng = $lng2 - $lng1;
	$a = sin($dlat / 2) * sin($dlat / 2) + cos($lat1) * cos($lat2) * sin($dlng / 2) * sin($dlng / 2);
	$c = 2 * atan2(sqrt($a), sqrt(1 - $a));
	$km = $r * $c;
 
	return $km;
} # end distance
##
// Debug function
function tpmsg(string $tpmsgprefix, $tpmsgdata) { 
# see https://www.pmwiki.org/wiki/Cookbook/DebuggingForCookbookAuthors
# add debug text to message array
    global $MessagesFmt;
    $MessageFmt  = '<span class="nztopodebug"><i>' . $tpmsgprefix . '</i>: ';
    switch (true) {
        case (is_null($tpmsgdata)) : 
            $MessageFmt .= 'null'; 
            break;
        case (is_bool($tpmsgdata)) : 
            $MessageFmt .= var_export($tpmsgdata,true); 
            break;
        case (is_array($tpmsgdata)) :
 		    $MessageFmt .= '<pre>' . var_export($tpmsgdata, true) . '</pre>' . NL; 
            break;
        case (is_string ($tpmsgdata)):
            $MessageFmt .= "'" . htmlspecialchars ($tpmsgdata) . "'";
            break;            
	    default: 
            $MessageFmt .= '"' . htmlspecialchars (strval ($tpmsgdata)) . '"'; 
            break;
    }
    $MessageFmt .= '</span>' . BR;
	$MessagesFmt [NZTOPONAME] .= $MessageFmt;
    return;
} # end tpmsg