, 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 ('
' . __FILE__, $RecipeInfo[NZTOPONAME]['Version']); // Initialise constants DEFINE ('NL', "\n"); DEFINE ('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
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>
' . NL; $retval .= '' . NL; if (!empty ($caption)) $retval .= '
' . $caption . '
' . NL; $retval .= '
' . 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 (?\'|\"|), named back reference is \k) $retval = '(?\\\'|\"'; if ($unqt) $retval .= '|'; # allow unquoted strings return $retval . ')(?:' . $parm . ')\k'; # 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 = '' . $tpmsgprefix . ': '; switch (true) { case (is_null($tpmsgdata)) : $MessageFmt .= 'null'; break; case (is_bool($tpmsgdata)) : $MessageFmt .= var_export($tpmsgdata,true); break; case (is_array($tpmsgdata)) : $MessageFmt .= '
' . var_export($tpmsgdata, true) . '
' . NL; break; case (is_string ($tpmsgdata)): $MessageFmt .= "'" . htmlspecialchars ($tpmsgdata) . "'"; break; default: $MessageFmt .= '"' . htmlspecialchars (strval ($tpmsgdata)) . '"'; break; } $MessageFmt .= '
' . BR; $MessagesFmt [NZTOPONAME] .= $MessageFmt; return; } # end tpmsg