#!/bin/sh

PROGNAME="$0"

usage() {
	cat <<EOF
NAME
	`basename $PROGNAME`- Convert gc.com *printable* web pages into GPX

SYNOPSIS
	`basename $PROGNAME` [options] [gc-com.html]...

DESCRIPTION
	Convert gc.com *printable* web pages into GPX, including
	cache description and all logs.

	The *printable* web pages can be fetched using geo-nearest,
	geo-newest, geo-placed, geo-found, or geo-gid with the -H option.

OPTIONS
	-b		Normalize output by postprocessing with gpsbabel
	-e		Encode hints with rot13 (e.g. NORTH = ABEGU)
	-i		Incremental, no XML and GPX headers
	-l number       Maximum number of log entries to be exported [unlimited]
	-n		No HTML in descriptions (experimental)
	-o FMT		Output FMT instead of GPX by using gpsbabel
	-u username	Indicate found status for username [$USERNAME]
	-w		Do not add "Additional Waypoints" to the GPX output
	-z		Do not output waypoints with "zero" coordinates
        -E var=val      Set environment "var" to "val"
                        i.e. DATEFMT=0|1
	-D lvl		Debug level

DEFAULTS
        Defaults can also be set with variables in file \$HOME/.georc:

            DATEFMT=[0|1];

DATE FORMATS
        Geocaching.com date formats that are compatible:

            GC Format   Example     Compatible
            YYYY-MM-DD  2011-07-13  yes
            YYYY/MM/DD  2011/07/13  yes
            MM/DD/YYYY  07/13/2011  yes
            DD/MM/YYYY  13/07/2011  yes if DATEFMT=1 in \$HOME/.georc
            DD/Mmm/YYYY 13/Jul/2001 no
            Mmm/DD/YYYY Jul/13/2011 no
            DD Mmm YY   13 Jul 11   yes (english only)

	Change them here:

	    http://www.geocaching.com/account/ManagePreferences.aspx

EXAMPLES
	Convert into GPX:

	    geo-found -n9999 -H. > /dev/null
	    geo-html2gpx *.html > found.gpx
EOF

	exit 1
}

#
#       Report an error and exit
#
error() {
	echo "`basename $PROGNAME`: $1" >&2
	exit 1
}

debug() {
	if [ $DEBUG -ge $1 ]; then
	    echo "`basename $PROGNAME`: $2" >&2
	fi
}

if [ `uname` = 'Darwin' ]; then
    awk=gawk
    date=gdate
else
    awk=awk
    date=date
fi

#
#	Read RC file, if there is one
#
USERNAME=
if [ -f $HOME/.georc ]; then
	. $HOME/.georc
	# N.B. must switch to read_rc_file if LAT/LON is ever needed here
fi
#

#       Process the options
#
POSTPROC="cat"
DEBUG=0
INCR=0
NOWPTS=0
NOZERO=0
NOHTML=0
DECODE=1
NUMLOGS=1000000
while getopts "beE:iwzl:no:u:D:h?" opt
do
	case $opt in
	b)	POSTPROC="gpsbabel -igpx -f- -ogpx -F-";;
	e)	DECODE=0;;
        E)      eval "$OPTARG";;
	i)	INCR=1;;
	l)	NUMLOGS="$OPTARG";;
	o)	POSTPROC="gpsbabel -igpx -f- -o$OPTARG -F-";;
	n)	NOHTML=1;;
	u)	USERNAME="$OPTARG";;
	w)	NOWPTS=1;;
	z)	NOZERO=1;;
	D)	DEBUG="$OPTARG";;
	h|\?)	usage;;
	esac
done
shift `expr $OPTIND - 1`

#
#	Main Program
#
YR=`date +"%Y"`

case `$awk --version` in
"GNU Awk"*)	;;
*)		error "awk is not GNU awk!";;
esac

cat "$@" \
| tr -d '\001\002\003\004\005\006\007\015\021\022\023\024\026\030' \
| sed 's/<A /\
<A /g' |
$awk -vDEBUG=$DEBUG -vINCR=$INCR \
    -vNOWPTS=$NOWPTS -vNOZERO=$NOZERO \
    -vNOHTML=$NOHTML \
    -vDECODE=$DECODE \
    -vUSERNAME="$USERNAME" \
    -vDATE="$date" \
    -vDATEFMT="$DATEFMT" \
    -vYR="$YR" -vNUMLOGS=$NUMLOGS \
'
# Copyright (c) 2010 Dan Saar
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.

function prsJSON_hex2num(s,     rv, ii, len, k)
{
   rv = 0
   s = tolower(s)
   len = length(s)
   
   for (ii = 1; ii <= len; ii++)
   {
      k = index("0123456789abcdef", substr(s, ii, 1))
      if (k > 0)
         rv = rv * 16 + (k-1)
      else
         break;
   }

   return rv
}

function prsJSON_EncodeAsUTF8( v,      s, p1, p2, p3, p4, cs )
{
   cs = "\200\201\202\203\204\205\206\207\210\211\212\213\214\215\216\217\220\221\222\223\224\225\226\227\230\231\232\233\234\235\236\237\240\241\242\243\244\245\246\247\250\251\252\253\254\255\256\257\260\261\262\263\264\265\266\267\270\271\272\273\274\275\276\277\300\301\302\303\304\305\306\307\310\311\312\313\314\315\316\317\320\321\322\323\324\325\326\327\330\331\332\333\334\335\336\337\340\341\342\343\344\345\346\347\350\351\352\353\354\355\356\357\360\361\362\363\364\365\366\367\370\371\372\373\374\375\376\377"
      
   if ( v < 128 )
      s = sprintf("%c", v )
      
   else if ( v < 2048 ) # 110xxxxx 10xxxxxx
   {
      p1 = int(v/64) % 32
      p2 = v % 64
      s = substr(cs, 65+p1, 1) substr(cs, p2+1, 1)
   }
   
   else if ( v < 65536 ) # 1110xxxx 10xxxxxx 10xxxxxx
   {
      p1 = int(v/4096) % 16
      p2 = int(v/64) % 64
      p3 = v % 64
      s = substr(cs, 97+p1, 1) substr(cs, p2+1, 1) substr(cs, p3+1, 1)
   }
   
   else if ( v < 1114112 ) # 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
   {
      p1 = int(v/262144) % 8
      p2 = int(v/4096) % 64
      p3 = int(v/64) % 64
      p4 = v % 64
      s = substr(cs, 113+p1, 1) substr(cs, p2+1, 1) substr(cs, p3+1, 1) substr(cs, p4+1, 1)
   }

   else
      s = ""
   
   return s;
}

function prsJSON_UnescapeString(jsonString,     matchedString, matchedValue)
{
   if (jsonString == "\"\"")
      return ""

   if (jsonString ~ /^".+"$/) 
      jsonString = substr(jsonString,2,length(jsonString)-2)

   gsub(/\\\\/, "\\u005C", jsonString)
   gsub(/\\"/, "\"", jsonString)
   gsub(/\\\//, "/", jsonString)
   gsub(/\\b/, "\b", jsonString)
   gsub(/\\f/, "\f", jsonString)
   gsub(/\\n/, "\n", jsonString)
   gsub(/\\r/, "\r", jsonString)
   gsub(/\\t/, "\t", jsonString)

   if (match(jsonString, /\\[^u]/))
      return "ParseJSON Error: Invalid String at " jsonString
   
   # handle encoded UTF-16 surrogates
   while (match(jsonString, /\\uD[89AaBb][0123456789AaBbCcDdEeFf][0123456789AaBbCcDdEeFf]\\uD[CcDdEeFf][0123456789AaBbCcDdEeFf][0123456789AaBbCcDdEeFf]/))
   {
      matchedValue = (prsJSON_hex2num(substr(jsonString, RSTART+2, 4)) % 1024) * 1024 + prsJSON_hex2num(substr(jsonString, RSTART+8, 4)) % 1024 + 65536
      #print matchedValue, substr(jsonString, RSTART+2, 4), substr(jsonString, RSTART+8, 4)
      matchedString = prsJSON_EncodeAsUTF8( matchedValue )
      sub(/\\uD[89AaBb][0123456789AaBbCcDdEeFf][0123456789AaBbCcDdEeFf]\\uD[CcDdEeFf][0123456789AaBbCcDdEeFf][0123456789AaBbCcDdEeFf]/, matchedString, jsonString)
   }
   
   while (match(jsonString, /\\u[0123456789AaBbCcDdEeFf][0123456789AaBbCcDdEeFf][0123456789AaBbCcDdEeFf][0123456789AaBbCcDdEeFf]/))
   {
      matchedValue = prsJSON_hex2num(substr(jsonString, RSTART+2, 4))
      matchedString = prsJSON_EncodeAsUTF8( matchedValue )
      sub(/\\u[0123456789AaBbCcDdEeFf][0123456789AaBbCcDdEeFf][0123456789AaBbCcDdEeFf][0123456789AaBbCcDdEeFf]/, matchedString, jsonString)
   }
   
   return jsonString;
}

function prsJSON_ValidString(jsonString)
{
   return jsonString !~ /^ParseJSON Error: Invalid String at /
}

function prsJSON_SetDataValue(jsonData, prefix, value)
{
   jsonData[prefix] = value
}

function prsJSON_Error(jsonStringArr, cnt, idx, jsonData, message)
{
   split("", jsonData)
   prsJSON_SetDataValue(jsonData, "1", sprintf("ParseJSON Error: %s at ", message) (idx <= cnt ? jsonStringArr[idx] : ""))
   split("", jsonStringArr)
   return cnt + 1
}

function prsJSON_CopyError(jsonData, tv)
{
   split("", jsonData)
   prsJSON_SetDataValue(jsonData, "1", tv[1])
}

function prsJSON_ParseNumber(jsonStringArr, cnt, idx, jsonData, prefix)
{
   if (idx <= cnt)
   {
      if (match(jsonStringArr[idx], /^(\-?)(0|[123456789][0123456789]*)(\.[0123456789]+)?([eE][+-]?[0123456789]+)?/)) 
      {
         prsJSON_SetDataValue(jsonData, prefix, substr(jsonStringArr[idx], 1, RLENGTH))
         jsonStringArr[idx] = length(jsonStringArr[idx]) >= RLENGTH+1 ? substr(jsonStringArr[idx], RLENGTH+1) : ""
      }
      else
         idx = prsJSON_Error(jsonStringArr, cnt, idx, jsonData, "Number not found") # starts like a number, but doesnt match the REGEX
   }

   return idx
}

function prsJSON_ParseString(jsonStringArr, cnt, idx, jsonData, prefix,      jsonString, idxn, idxs, idxq, t)
{
   if (idx <= cnt && length(jsonStringArr[idx]) > 0 && substr(jsonStringArr[idx], 1, 1) == "\"")
   {
      idxn = 2
      jsonString = jsonStringArr[idx]

      do
      {
         t = length(jsonString) >= idxn ? substr(jsonString, idxn) : ""
         idxs = index(t, "\\")
         idxq = index(t, "\"")

         # no valid close quote found
         if (idxq == 0)
         {
            if (idx == cnt)
               break;

            idx++
            jsonString = jsonString "," jsonStringArr[idx]
         }

         # a valid close quote was found - not before a slash
         if (idxq != 0 && (idxs == 0 || (idxs != 0 && idxq < idxs)))
            break;

         if (idxs != 0 && idxq == idxs + 1) # slash quote
            idxn = idxn + idxq

         else
            idxn = idxn + idxs + 1

      } while (1)

      if (idxq > 0)
      {
         t = substr(jsonString, 1, idxn+idxq-1)
         if (match(t, /[\001\002\003\004\005\006\007\010\011\012\013\014\015\016\017\020\021\022\023\024\025\026\027\030\031\032\033\034\035\036\037]/) == 0)
         {
            t = prsJSON_UnescapeString(t)
            if ( prsJSON_ValidString(t) )
            {
               prsJSON_SetDataValue(jsonData, prefix, t)
               jsonStringArr[idx] = length(jsonString) >= idxn+idxq ? substr(jsonString,idxn+idxq) : ""
            }
            else
               idx = prsJSON_Error(jsonStringArr, cnt, idx, jsonData, "Invalid string") 
         }
         else
            idx = prsJSON_Error(jsonStringArr, cnt, idx, jsonData, "Invalid character in string") 
      }
      else
         idx = prsJSON_Error(jsonStringArr, cnt, idx, jsonData, "Unterminated string") 
   }
   else
      idx = prsJSON_Error(jsonStringArr, cnt, idx, jsonData, "String expected")
      
   return idx
}

function prsJSON_ParseObject(jsonStringArr, cnt, idx, jsonData, prefix,     tv )
{
   if (idx <= cnt)
   {
      sub(/^\{[ \t\r\n\f]*/, "", jsonStringArr[idx]) #skip open { and skipwhite

      while (idx <= cnt && length(jsonStringArr[idx]) > 0 && substr(jsonStringArr[idx], 1, 1) != "}")
      {
         idx = prsJSON_ParseString(jsonStringArr, cnt, idx, tv, "1")
         
         if (idx <= cnt && length(tv[1]) == 0)
             idx = prsJSON_Error(jsonStringArr, cnt, idx, tv, "Empty string used for property name")

         if (idx <= cnt)
         {
            sub(/^[ \t\r\n\f]+/, "", jsonStringArr[idx]) #skipwhite
      
            if ( length(jsonStringArr[idx]) > 0 && substr(jsonStringArr[idx], 1, 1) == ":" )
            {
               sub(/^:[ \t\r\n\f]*/, "", jsonStringArr[idx]) #skip colon and skipwhite
          
               if ( length(jsonStringArr[idx]) > 0 )
               {
                  idx = prsJSON_ParseJSONInt(jsonStringArr, cnt, idx, jsonData, prefix != "" ? prefix SUBSEP tv[1] : tv[1])
                  if (idx <= cnt)
                  {
                     sub(/^[ \t\r\n\f]+/, "", jsonStringArr[idx]) #skipwhite
       
                     if (length(jsonStringArr[idx]) == 0 && idx < cnt)
                     {
                        idx++
                        sub(/^[ \t\r\n\f]+/, "", jsonStringArr[idx]) #skipwhite
                        if (length(jsonStringArr[idx]) == 0 || substr(jsonStringArr[idx], 1, 1) == "}")
                           idx = prsJSON_Error(jsonStringArr, cnt, idx, jsonData, "Expected object property")
                     }
         
                     else if (length(jsonStringArr[idx]) == 0 || substr(jsonStringArr[idx], 1, 1) != "}")
                        idx = prsJSON_Error(jsonStringArr, cnt, idx, jsonData, "Expected object property or closing brace")
                  }
               }
               else
                  idx = prsJSON_Error(jsonStringArr, cnt, idx, jsonData, "Expected JSON value (1)")
            }
            else
               idx = prsJSON_Error(jsonStringArr, cnt, idx, jsonData, "Expected colon")
         }
         else
            prsJSON_CopyError(jsonData, tv)
      }
   
      if (idx <= cnt && (length(jsonStringArr[idx]) == 0 || substr(jsonStringArr[idx], 1, 1) != "}"))
         idx = prsJSON_Error(jsonStringArr, cnt, idx, jsonData, "Expected closing brace")
   
      if (idx <= cnt && length(jsonStringArr[idx]) > 0 && substr(jsonStringArr[idx], 1, 1) == "}")
         sub(/^\}[ \t\r\n\f]*/, "", jsonStringArr[idx]) #skip close } and skipwhite
   }  

   return idx
}

function prsJSON_ParseArray(jsonStringArr, cnt, idx, jsonData, prefix,     ii)
{
   if (idx <= cnt)
   {
      sub(/^\[[ \t\r\n\f]*/, "", jsonStringArr[idx]) #skip open bracket and skipwhite
      ii = 1

      while (idx <= cnt && length(jsonStringArr[idx]) > 0 && substr(jsonStringArr[idx], 1, 1) != "]")
      {
         idx = prsJSON_ParseJSONInt(jsonStringArr, cnt, idx, jsonData, prefix != "" ? prefix SUBSEP ii : ii )
         ii++
    
         if (idx <= cnt)  
         {
            sub(/^[ \t\r\n\f]+/, "", jsonStringArr[idx]) #skipwhite
      
            if (length(jsonStringArr[idx]) == 0 && idx < cnt)
            {
               idx++;
               sub(/^[ \t\r\n\f]+/, "", jsonStringArr[idx]) #skipwhite
               if (length(jsonStringArr[idx]) == 0 || substr(jsonStringArr[idx], 1, 1) == "]")
                  idx = prsJSON_Error(jsonStringArr, cnt, idx, jsonData, "Expected array value")
            }
         
            else if (length(jsonStringArr[idx]) == 0 || substr(jsonStringArr[idx], 1, 1) != "]")
               idx = prsJSON_Error(jsonStringArr, cnt, idx, jsonData, "Expected array value or closing bracket")
         }
      }
      
      if (idx <= cnt && (length(jsonStringArr[idx]) == 0 || substr(jsonStringArr[idx], 1, 1) != "]"))
         idx = prsJSON_Error(jsonStringArr, cnt, idx, jsonData, "Expected closing bracket")
   
      if (idx <= cnt && length(jsonStringArr[idx]) > 0 && substr(jsonStringArr[idx], 1, 1) == "]")
         sub(/^\][ \t\r\n\f]*/, "", jsonStringArr[idx]) #skip close bracket and skipwhite
   }
      
   return idx
}

function prsJSON_ParseJSONInt(jsonStringArr, cnt, idx, jsonData, prefix,     tk)
{
   if (idx <= cnt)
   {
      sub(/^[ \t\r\n\f]+/, "", jsonStringArr[idx]) #skipwhite

      if (length(jsonStringArr[idx]) > 0)
      {
         tk = substr(jsonStringArr[idx], 1, 1)
         if (tk == "\"" && prefix != "")
            idx = prsJSON_ParseString(jsonStringArr, cnt, idx, jsonData, prefix)
         else if (tk ~ /^[0123456789-]/ && prefix != "") 
            idx = prsJSON_ParseNumber(jsonStringArr, cnt, idx, jsonData, prefix)
         else if (jsonStringArr[idx] ~ /^true/ && prefix != "") 
         {
            prsJSON_SetDataValue(jsonData, prefix, "<<true>>")
            jsonStringArr[idx] = length(jsonStringArr[idx]) <= 4 ? "" : substr(jsonStringArr[idx],5)
         }
         else if (jsonStringArr[idx] ~ /^false/ && prefix != "") 
         {
            prsJSON_SetDataValue(jsonData, prefix, "<<false>>")
            jsonStringArr[idx] = length(jsonStringArr[idx]) <= 5 ? "" : substr(jsonStringArr[idx],6)
         }
         else if (jsonStringArr[idx] ~ /^null/ && prefix != "") 
         {
            prsJSON_SetDataValue(jsonData, prefix, "<<null>>")
            jsonStringArr[idx] = length(jsonStringArr[idx]) <= 4 ? "" : substr(jsonStringArr[idx],5)
         }
         else if (tk == "{") 
            idx = prsJSON_ParseObject(jsonStringArr, cnt, idx, jsonData, prefix)
         else if (tk == "[") 
            idx = prsJSON_ParseArray(jsonStringArr, cnt, idx, jsonData, prefix)
         else
            idx = prsJSON_Error(jsonStringArr, cnt, idx, jsonData, "Expected JSON value (2)")

         if (idx <= cnt)
            sub(/^[ \t\r\n\f]+/, "", jsonStringArr[idx]) #skipwhite
      }
   
      if (prefix == "" && idx <= cnt && length(jsonStringArr[idx]) != 0)
         idx = prsJSON_Error(jsonStringArr, cnt, idx, jsonData, "Expected end of JSON text")
      else if (prefix == "" && idx+1 <= cnt)
      {
         idx++
         idx = prsJSON_Error(jsonStringArr, cnt, idx, jsonData, "Expected end of JSON text (2)")
      }
         
   }

   return idx
}

#
# JSON Formatting Routines
#

function useJSON_ArrayCount( possibleArray,     a, min, max, cnt, rv)
{
   cnt = 0
   
   for ( a in possibleArray )
   {
      if (possibleArray[a] "" !~ /^[0123456789][0123456789]*$/)
         return -1
         
      if ( cnt == 0 )
      {
         min = possibleArray[a]
         max = possibleArray[a]
      }
      else
      {
         if (min == possibleArray[a] || max == possibleArray[a])
            return -1
            
         if (possibleArray[a] < min)
            min = possibleArray[a]
            
         if (max < possibleArray[a])
            max = possibleArray[a]
      }
      
      cnt++
   }
   
   if (min == 1 && max == cnt)
      return cnt
      
   return -1
}

function useJSON_GetObjectMembers(jsonSchema, prefix)
{
   if (prefix == "") prefix = "<<novalue>>"
   return prefix in jsonSchema ? jsonSchema[prefix] : ""
}

# quick sort array arr
function utlJSON_qsortArray(arr, left, right,   i, last, t)
{
   if (left >= right)   # do nothing if array has less than 2 elements
      return
   i = left + int((right-left+1)*rand())
   t = arr[left]; 
   arr[left] = arr[i]; 
   arr[i] = t
   last = left  # arr[left] is now partition element
   for (i = left+1; i <= right; i++)
   {
      if (arr[i] < arr[left])
      {
         last++
         t = arr[last]; 
         arr[last] = arr[i]; 
         arr[i] = t
      }
   }
   t = arr[left]; 
   arr[left] = arr[last]; 
   arr[last] = t
   utlJSON_qsortArray(arr, left, last-1)
   utlJSON_qsortArray(arr, last+1, right)
}
    
function useJSON_GetSchema(jsonData, jsonSchema,    a, tidx, tv, sv, idx)
{
   split("", jsonSchema)
   for (a in jsonData)
   {
      while (match(a, SUBSEP "[^" SUBSEP "]+$"))
      {
         tidx = substr(a,1,RSTART-1)
         tv = substr(a,RSTART+1)
         sv = (tidx in jsonSchema) ? jsonSchema[tidx] : ""
         # if ( sv != tv && sv !~ "^" tv SUBSEP && sv !~ SUBSEP tv "$" && sv !~ SUBSEP tv SUBSEP )
         # Rephrase this using index so object member names with regex characters work
         if ( sv != tv && index(sv, tv SUBSEP) != 1 && (length(sv) <= length(tv)+1 || substr(sv, length(sv)-length(tv)) != SUBSEP tv) && index(sv, SUBSEP tv SUBSEP) == 0 )
            jsonSchema[tidx] = sv (sv == "" ? "" : SUBSEP)  tv
         a = tidx
      }
      
      tidx = "<<novalue>>"
      tv = a
      sv = (tidx in jsonSchema) ? jsonSchema[tidx] : ""
      if ( sv != tv && sv !~ "^" tv SUBSEP && sv !~ SUBSEP tv "$" && sv !~ SUBSEP tv SUBSEP )
         jsonSchema[tidx] = sv (sv == "" ? "" : SUBSEP)  tv
   }
}

function useJSON_EscapeString(s,     ii, c, t, t2, t3, t4, cs)
{
   cs = "\200\201\202\203\204\205\206\207\210\211\212\213\214\215\216\217\220\221\222\223\224\225\226\227\230\231\232\233\234\235\236\237\240\241\242\243\244\245\246\247\250\251\252\253\254\255\256\257\260\261\262\263\264\265\266\267\270\271\272\273\274\275\276\277\300\301\302\303\304\305\306\307\310\311\312\313\314\315\316\317\320\321\322\323\324\325\326\327\330\331\332\333\334\335\336\337\340\341\342\343\344\345\346\347\350\351\352\353\354\355\356\357\360\361\362\363\364\365\366\367\370\371\372\373\374\375\376\377"
   gsub(/\\/, "\\u005C", s)
   gsub(/"/, "\\\"", s)
   #gsub(/\//, "\\/", s) # required to decode, but not to encode
   gsub(/\b/, "\\b", s)
   gsub(/\f/, "\\f", s)
   gsub(/\n/, "\\n", s)
   gsub(/\r/, "\\r", s)
   gsub(/\t/, "\\t", s)
   
   for ( ii = 1 ; ii <= length(s) ; ii++ )
   {
      t = substr(s,ii,1)
      
      if (t == "\000") # having \000 in list below doesnt work in all awks
      {
         c = 0
         s = (ii > 1 ? substr(s, 1, ii-1) : "") sprintf("\\u%04X", c) (ii==length(s) ? "" : substr(s, ii+1))
         ii += 5
      }
      else
      {
         c = index("\001\002\003\004\005\006\007\010\011\012\013\014\015\016\017\020\021\022\023\024\025\026\027\030\031\032\033\034\035\036\037", t)
         c = c == 0 ? -1 : c
         
         if ( c >= 0 )
         {
            s = (ii > 1 ? substr(s, 1, ii-1) : "") sprintf("\\u%04X", c) (ii==length(s) ? "" : substr(s, ii+1))
            ii += 5
         }
      }

      t = index(cs, t)
      t2 = ii+1 <= length(s) ? index(cs, substr(s,ii+1,1)) : 0
      t3 = ii+2 <= length(s) ? index(cs, substr(s,ii+2,1)) : 0
      t4 = ii+3 <= length(s) ? index(cs, substr(s,ii+3,1)) : 0
        
      if ( c < 0 && t > 64 && t <= 96 && ii+1 <= length(s) && t2 > 0 && t2 <= 64) # two character UTF-8 sequence
      {
         c = (t - 65)*64 + (t2-1)
         s = (ii > 1 ? substr(s, 1, ii-1) : "") sprintf("\\u%04X", c) (ii+1==length(s) ? "" : substr(s, ii+2))
         ii += 5
      }
            
      else if ( c < 0 && t > 96 && t <= 112 && ii+2 <= length(s) && t2 > 0 && t2 <= 64 && t3 > 0 && t3 <= 64) # three character UTF-8 sequence
      {
         c = (t - 97)*4096 + (t2-1)*64 + (t3-1)
         if ( c < 65536 )
         {
            s = (ii > 1 ? substr(s, 1, ii-1) : "") sprintf("\\u%04X", c) (ii+2==length(s) ? "" : substr(s, ii+3))
            ii += 5
         }
         else
         {
            # encode in JSON-style with two \u#### UTF-16 surrogates
            # printf("1: %08X\n", c)
            s = (ii > 1 ? substr(s, 1, ii-1) : "") sprintf("\\u%04X\\u%04X", (c/1024)%1024 + 55296, c%1024 + 56320) (ii+3==length(s) ? "" : substr(s, ii+4))
            ii += 11
         }
      }
            
      # four character UTF-8 sequence, encode in JSON-style with two \u#### UTF-16 surrogates
      else if ( c < 0 && t > 112 && t <= 120 && ii+3 <= length(s) && t2 > 0 && t2 <= 64 && t3 > 0 && t3 <= 64 && t4 > 0 && t4 <=  64) 
      {
         c = (t - 113)*262144 + (t2-1)*4096 + (t3-1)*64 + (t4-1)
         # printf("2: %08X, %d, %d, %d, %d\n", c, t, t2, t3, t4)
         # printf("\\u%04X\\u%04X\n", (c/1024)%1024 + 55296, c%1024 + 56320)
         c -= 65536
         s = (ii > 1 ? substr(s, 1, ii-1) : "") sprintf("\\u%04X\\u%04X", (c/1024)%1024 + 55296, c%1024 + 56320) (ii+3==length(s) ? "" : substr(s, ii+4))
         ii += 11
      }
   }
         
   return "\"" s "\""
}

function useJSON_GetDataValue(jsonData, prefix)
{
   return prefix in jsonData ? jsonData[prefix] : "<<novalue>>"
}

function useJSON_PrettyFormat(s, pretty)
{
   if (s == "" || pretty <= 0) return s

   # dont sprintf the whole thing, some awks have short buffers for sprintf
   return sprintf("%*.*s", (pretty-1)*3, (pretty-1)*3, "") s (s == "}" || s == "]" ? "" : "\n")
}

function useJSON_FormatInt(jsonData, jsonSchema, prefix, pretty,     allLines, member, memberArr, memberList, arrCount, a, ii)
{
   memberList = useJSON_GetObjectMembers(jsonSchema, prefix)
   
   if ( memberList == "" )
   {
      a = useJSON_GetDataValue(jsonData, prefix)
      if ( a == "<<true>>" ) return "true"
      if ( a == "<<false>>" ) return "false"
      if ( a == "<<null>>" ) return "null"
      if ( a == "<<novalue>>" ) return "" # <<novalue>> is a help for dealing with empty arrays and objects

      # if it looks like a number, encode it as such.  Cant tell a string from a number.
      if (a "" ~ /^(\-?)(0|[123456789][0123456789]*)(\.[0123456789]+)?([eE][+-]?[0123456789]+)?$/)
         return a
         
      return useJSON_EscapeString(a)
   }
   
   split(memberList, memberArr, SUBSEP)
   arrCount = useJSON_ArrayCount( memberArr )

   if ( arrCount >= 0 )
   {
      allLines = "[" (pretty == 0 ? "" : "\n")
      
      for ( ii = 1 ; ii <= arrCount ; ii++ )
         allLines = allLines useJSON_PrettyFormat(useJSON_FormatInt( jsonData, jsonSchema, prefix (prefix == "" ? "" : SUBSEP) ii, (pretty != 0 ? pretty+1 : 0)) (ii < arrCount ? "," : ""), pretty != 0 ? pretty+1 : 0)
      allLines = allLines useJSON_PrettyFormat("]", pretty)
      
      return allLines
   }
   
   allLines = "{" (pretty == 0 ? "" : "\n")
      
   ii = 0

   arrCount = 0
   for (a in memberArr)
      arrCount++
      
   utlJSON_qsortArray(memberArr, 1, arrCount)
   
   for ( ii = 1 ; ii <= arrCount ; ii++ )
      allLines = allLines useJSON_PrettyFormat(useJSON_EscapeString(memberArr[ii]) (pretty == 0 ? ":" : " : ") useJSON_FormatInt(jsonData, jsonSchema, prefix (prefix == "" ? "" : SUBSEP) memberArr[ii], (pretty != 0 ? pretty+1 : 0)) (ii < arrCount ? "," : ""), pretty != 0 ? pretty+1 : 0)
      
   allLines = allLines useJSON_PrettyFormat("}", pretty)
   
   return allLines
}

#
# Entry Points
#

#
# ParseJSON : Parse JSON text into an awk array
#
#    jsonString : JSON text
#    jsonData : array of parsed JSON data
#
#    returns : N/A
#
function ParseJSON(jsonString, jsonData,      jsonStringArr, cnt)
{
   # newlines split differently in some awks, replace them with formfeeds (also white space)
   # if (split("1\n2\n3", jsonData, ",") == 3) # is this an awk that splits newlines differently?
   gsub(/\n/, "\f", jsonString) # always replace literal newlines - allows compatibility when testing

   split("", jsonData) # clear the array jsonData
   cnt = split(jsonString, jsonStringArr, ",")
   prsJSON_ParseJSONInt(jsonStringArr, cnt, 1, jsonData, "")
}

#
# FormatJSON : Format parsed JSON data back into JSON text
#
#    jsonData : array of parsed JSON data
#    pretty : 0 = compact format, non-zero = pretty format
#
#    returns : string with JSON text
#
function FormatJSON(jsonData, pretty,    jsonSchema)
{
   useJSON_GetSchema(jsonData, jsonSchema)
   return useJSON_FormatInt(jsonData, jsonSchema, "", pretty ? 1 : 0)
}

#
# JSONArrayLength : Find number of members in a JSON array
#
#    jsonData : array of parsed JSON data
#    prefix : array name
#
#    returns : number of entries in the array
#
function JSONArrayLength(jsonData, prefix,     a, cnt, tv)
{
   cnt = -1
   
   for (a in jsonData)
   {
      if (prefix == "" || index(a, prefix) == 1)
      {
         tv = substr(a, prefix == "" ? 1 : (1+length(prefix)+1))
         if ( index(tv, SUBSEP) )
            tv = substr(tv, 1, index(tv, SUBSEP)-1)
         tv = tv + 0
         if ( tv > cnt )
            cnt = tv
      }
   }

   return cnt
}

#
# JSONUnescapeString : turn a JSON-escaped string into UTF-8
#
#    jsonString : the escaped JSON string to convert
#
#    returns : the string in UTF-8
#
function JSONUnescapeString(jsonString)
{
   return prsJSON_UnescapeString(jsonString)
}

#
# JSONIsTrue : return non-zero if the value is the true value
#
#    jsonValue : the value to test
#
#    returns : true or false
#
function JSONIsTrue(jsonValue)
{
   return jsonValue == "<<true>>";
}

#
# JSONIsFalse : return non-zero if the value is the false value
#
#    jsonValue : the value to test
#
#    returns : true or false
#
function JSONIsFalse(jsonValue)
{
   return jsonValue == "<<false>>";
}

#
# JSONIsNull : return non-zero if the value is the null value
#
#    jsonValue : the value to test
#
#    returns : true or false
#
function JSONIsNull(jsonValue)
{
   return jsonValue == "<<null>>";
}

#
# JSONObjectMembers : get the set of members of an object
#
#    jsonData : array of parsed JSON data
#    prefix : object name
#    memberArr : [out] an array of the names of the object members, if the target was an object or an array
#
#    returns : If the target was actually an array rather than an object, the number of elements in the array
#              Else, zero if the target was an object or a value
#
function JSONObjectMembers(jsonData, prefix, memberArr,     jsonSchema, memberList, rv, a)
{
   useJSON_GetSchema(jsonData, jsonSchema)
   memberList = useJSON_GetObjectMembers(jsonSchema, prefix)

   if ( memberList == "" )
   {
      split("", memberArr)
      return 0
   }
      
   split(memberList, memberArr, SUBSEP)
   rv = useJSON_ArrayCount( memberArr )
   if ( rv == -1 ) # not an array, sort the object member names
   {
      rv = 0
      for (a in memberArr)
         rv++
      
      utlJSON_qsortArray(memberArr, 1, rv)
      rv = 0
   }
   return rv
}
# End of Copyright (c) 2010 Dan Saar

function debug(lvl, text) {
    if (lvl <= DEBUG)
        print text > "/dev/stderr"
}

function wpt_init() {
    available = "True"
    archived = "False"
    sym = "Geocache"
    json_log_bool = 0
    logs = ""
    logs_section = 0
    hints = ""
    lat = ""
    yy = 0
    wplist = ""
    nattr_yes = 0
    nattr_no = 0
    gs_type = ""
}

function dec2utf(dec) {
    if (dec <= 0x7f)
	return sprintf("%c", dec)
    else if (dec <= 0x07ff)
	return sprintf("%c%c", 0xC0 + rshift(dec, 6), 0x80 + and(dec, 0x3F) )
}

function asc2xml(txt,	o, ent, dec) {
    o = ""
    while (match(txt, "&#x[0123456789abcdefABCDEF]*;"))
    {
	o = o substr(txt, 1, RSTART-1)
	ent = toupper(substr(txt, RSTART+3, RLENGTH-4))
	txt = substr(txt, RSTART+RLENGTH)
	dec = hex2dec(ent)
	o = o dec2utf(dec)
    }
    txt = o txt
    o = ""
    while (match(txt, "&#[0123456789]*;"))
    {
	o = o substr(txt, 1, RSTART-1)
	ent = toupper(substr(txt, RSTART+2, RLENGTH-3))
	txt = substr(txt, RSTART+RLENGTH)
	dec = ent + 0
	o = o dec2utf(dec)
    }
    o = o txt
    return o
}

function umlauts(text) {
    text = asc2xml(text)
    if(0)
    {
	# Somewhat minimal translation of HTML entities in titles
	gsub("&#228;", "\xc3\xa4", text)
	gsub("&#xE4;", "\xc3\xa4", text)
	gsub("&#246;", "\xc3\xb6", text)
	gsub("&#xF6;", "\xc3\xb6", text)
	gsub("&#252;", "\xc3\xbc", text)
	gsub("&#xFC;", "\xc3\xbc", text)
	gsub("&#196;", "\xc3\x84", text)
	gsub("&#xC4;", "\xc3\x84", text)
	gsub("&#214;", "\xc3\x96", text)
	gsub("&#xD6;", "\xc3\x96", text)
	gsub("&#220;", "\xc3\x9c", text)
	gsub("&#xDC;", "\xc3\x9c", text)
	gsub("&#223;", "\xc3\x9f", text)
	gsub("&#xDF;", "\xc3\x9f", text)
	gsub("&#176;", "\xc2\xb0", text)
	gsub("&#xB0;", "\xc2\xb0", text)
	gsub("&#180;", "\xc2\xb4", text)
	gsub("&#xB4;", "\xc2\xb4", text)
    }
    gsub("&amp;", "\\&", text)
    return text
}

function htmlclean(text) {
    gsub("&nbsp;", " ", text)
    gsub("</?[pP][^>]*>", "\n", text)
    gsub("<[bB][rR][^>]*>", "\n", text)
    gsub("<[^>]*>", "", text)
    # compress whitespace
    gsub("\n\n\n*", "\n\n", text)
    gsub("[ \t][ \t]*", " ", text)
    return text
}

function tableclean(text) {
    gsub("\n", "", text)
    gsub("&nbsp;", " ", text)
    # translate/remove HTML tags
    gsub("</?[pP][^>]*>", "\n", text)
    gsub("</[bB][rR][^>]*>", "", text)
    gsub("</?font[^>]*>", "", text)
    gsub("</?table[^>]*>", "", text)
    gsub("<t[rdh]>", "", text)
    gsub("</tr>", "\n", text)
    gsub("</t[dh][^>]*>", " | ", text)
    gsub("<[^>]*>", "", text)
    # compress whitespace
    gsub("[ \t][ \t]*", " ", text)
    return text
}

function remdiv(text, tag) {
    if (tag != "")
	pat = ".*<div id=." tag ".[^>]*>[ \t\n]*"
    else
	pat = ".*<div[^>]*>[ \t\n]*"
    sub(pat, "", text)
    while (text !~ "/?div")
    {
	if (getline more <= 0)
            break
	text = text "\n" more
    }
    sub("[ \t\n]*</div>.*", "", text)
    debug(3, "Div:\n" text)
    return text
}

function remspan(text, tag) {
    if (tag != "")
	pat = ".*<span id=." tag ".[^>]*>[ \t\n]*"
    else
	pat = ".*<span[^>]*>[ \t\n]*"
    sub(pat, "", text)
    while (text !~ "/?span")
    {
	if (getline more <= 0)
            break
	text = text "\n" more
    }
    sub("[ \t\n]*</span>.*", "", text)
    debug(3, "Span:\n" text)
    return text
}

function remspanlong(text, tag) {
    if (tag != "")
	pat = ".*<span id=." tag ".[^>]*>[ \t\n]*"
    else
	pat = ".*<span[^>]*>[ \t\n]*"
    sub(pat, "", text)
    # i = "span level"
    i = 1; j = 0
    debug(2, length(text) "\t" i "   " j++ "   " text)
    # input is in text
    while (i != 0)
    {
	# emergency exit
	if (length(text) > 500000)
	{
	    debug(0, "Warning: logs exceeded 500,000 bytes!")
	    break
	}
	# cleanup: remove </*span...>, adjust "span level"
	while (text ~ "</*span.*>")
	{
	    if (text ~ "</span>")
	    {
		--i; sub("</span>", "", text)
	    }
	    if (text ~ "<span.*>")
	    {
		++i; sub("<span[^>]*>", "", text)
	    }
	}
	debug(2, "=" length(text) "\t" i "   " j++ "   " text)
	# if "span level" down to zero, closing tag reached
	if (i == 0) break
	# get more input
	if (getline more <= 0)
	    break
	text = text "\n" more
	debug(2, "+" length(more) "\t" i "   " j++ "   " more)
    }
    debug(1, length(text) "\t" i "   " j++)
    sub("[ \t\n]*</span>.*", "", text)
    gsub("&nbsp;", " ", text)
    if (tag == "CacheLogs")
	gsub("</?table[^>]*>", "", text)
    debug(3, "SpanLong:\n" text)
    return text
}

function remwaypoints() {
    text = ""
    while (text !~ "</table>" && text !~ "No additional waypoints to display")
    {
	if (getline more <= 0)
            break
	text = text " " more
    }
    gsub("&nbsp;", " ", text)
    gsub("\n[ \t]*", "", text)
    debug(3, "Waypoints:\n" text "\nEnd Waypoints")
    return text
    # will return complete table contents! split by </tr> instead of
    # <STRONG><img...>
}

function splitwaypoints(waypoints,
	    line, fld, prefix, lookup, wpname, x, y, lat, lon) {
    text=""
    # separate lines
    split(waypoints, wps, "</tr>")
    i = 0
    for (wp in wps)
	++i
    wp = 1 # skip header line
    while (wp < i)
    {
	++wp
	# get URL from full table line
	url = wps[wp]
	gsub(".*href=.", "", url)
	gsub("\".*", "", url)
	if (url !~ "^http:")
	{
	    url = ""
	}
	else
	{
	    debug(1, "url: " url)
	}
	# individual fields without leading/trailing blanks, remove HTML tags
	split(wps[wp], line, "</td>")
	j = 0
	for (fld in line)
	{
	    ++j
	    debug(2, "Before Line[" fld "]: " line[fld])
	    gsub("[ \t]*<[^>]*>", "", line[fld])
	    gsub("^[ \t]*", "", line[fld])
	    gsub("[ \t]*$", "", line[fld])
	    debug(2, "after Line[" fld "]: " line[fld])
	}
	# 8 fields: 1st line old style
	# 9 fields: 1st line new style
	# 4 fields, [1]~"Note:": 2nd line old style
	# 4 fields, [2]~"Note:": 2nd line new style
	# else: drop
	if (j == 8)
	{
	    # main information line, old style (pre-2010/07)
	    if (!line[3]) continue
	    prefix = substr(line[3] "00", 1, 2)
	    lookup = line[4]
	    wpname = line[5]
	    lat = toupper(line[6])
	    gsub(" *[EW].*", "", lat)
	    split(lat, y)
	    lat = y[2] + y[3]/60.0
	    if (y[1] == "S")
		lat = -lat
	    lon = toupper(line[6])
	    gsub("[NS] *[0-9]*.. *[0-9.]* ", "", lon)
	    gsub("[^ 0-9.NESW-]", "", lon)
	    split(lon, x)
	    lon = x[2] + x[3]/60.0
	    if (x[1] == "W")
		lon = -lon
	    text = text sprintf("\nlat=\"%.6f\" lon=\"%.6f\"|%s|%s|%s|%s",
				lat, lon, prefix, lookup, wpname, url)
	}
	else if (j == 9)
	{
	    # main information line, new style (2010/07)
	    if (!line[4]) continue
	    prefix = substr(line[4] "00", 1, 2)
	    lookup = line[5]
	    wpname = line[6]
	    lat = toupper(line[7])
	    gsub(" *[EW].*", "", lat)
	    split(lat, y)
	    lat = y[2] + y[3]/60.0
	    if (y[1] == "S")
		lat = -lat
	    lon = toupper(line[7])
	    gsub("[NS] *[0-9]*.. *[0-9.]* ", "", lon)
	    gsub("[^ 0-9.NESW-]", "", lon)
	    split(lon, x)
	    lon = x[2] + x[3]/60.0
	    if (x[1] == "W")
		lon = -lon
	    text = text sprintf("\nlat=\"%.6f\" lon=\"%.6f\"|%s|%s|%s|%s",
				lat, lon, prefix, lookup, wpname, url)
	}
	else if (j == 4)
	{
	    if (line[1] ~ "Note:")
	    {
		# continuation line, old style
		text = text sprintf("|%s", line[2])
	    }
	    else if (line[2] ~ "Note:")
	    {
		# continuation line, new style
		text = text sprintf("|%s", line[3])
	    }
	}
    }
    debug(3, "Split WPs\n" text)
    return text
}

function wpclean(waypoints,     line, fld, prefix, lookup, wpname, coords) {
    # simplify Additional Waypoints table:
    # prefixedname - name<br>coordfield<br>note
    text = ""
    split(waypoints, wps, "</tr>")
    i = 0
    for (wp in wps)
	++i
    wp = 1
    while (wp < i)
    {
	++wp
	split(wps[wp], line, "</td>")
	j = 0
	for (fld in line)
	{
	    ++j
	    gsub("[ \t]*<[^>]*>", "", line[fld])
	    gsub("^[ \t]*", "", line[fld])
	    gsub("[ \t]*$", "", line[fld])
	}
	# 8 fields: 1st line old style
	# 9 fields: 1st line new style
	# 4 fields, [1]~"Note:": 2nd line old style
	# 4 fields, [2]~"Note:": 2nd line new style
	# else: drop
	if (j == 8)
	{
	    # main information line, old style (pre-2010/07)
	    if (!line[3]) continue
	    prefix = substr(line[3] "00", 1, 2) substr(gcid, 3)
	    lookup = line[4]
	    wpname = line[5]
	    gsub(" \\(.*\\).*", "", wpname)
	    coords = toupper(line[6])
	    text = text sprintf("%s - %s<br />%s<br />", prefix, wpname, coords)
	}
	else if (j == 9)
	{
	    # main information line, new style (2010/07)
	    if (!line[4]) continue
	    prefix = substr(line[4] "00", 1, 2) substr(gcid, 3)
	    lookup = line[5]
	    wpname = line[6]
	    gsub(" \\(.*\\).*", "", wpname)
	    coords = toupper(line[7])
	    text = text sprintf("%s - %s<br />%s<br />", prefix, wpname, coords)
	}
	else if(j == 4)
	{
	    if (line[1] ~ "Note:")
	    {
		# continuation line, old style
		text = text sprintf("%s<br />", line[2])
	    }
	    else if (line[2] ~ "Note:")
	    {
		# continuation line, new style
		text = text sprintf("%s<br />", line[3])
	    }
	}
    }
    debug(3, "Clean WPs\n" text)
    return text
}

function hex2dec(x,   val) {
    for (val = 0; length(x); x = substr(x, 2))
        val = 16*val + index("0123456789ABCDEF", substr(x, 1, 1)) - 1
    return val
}

# Convert GC0000 to 58913
function wp2id(wp,    val) {
    sub("^GC", "", wp)
    debug(5, "wp2id: " wp " ...")
    if ((length(wp) <= 4) && (wp < "G000"))
    {
	# old hex style
        val = hex2dec(wp)
        debug(5, "wp2id hex: " val " ...")
        return val
    }
    # new style, base-31, can have 4 or more places!
    set = "0123456789ABCDEFGHJKMNPQRTVWXYZ"
    val = 0
    for (pos = 1; pos <= length(wp); ++pos)
    {
        val *= 31
        val += index(set, substr(wp, pos, 1)) - 1
    }
    val = val - 411120
    debug(5, "wp2id id: " val " ...")
    return val
}

# to decode hints: rot13 http://lorance.freeshell.org/rot13/
function rot13 (string) {
    ROTFROM = "nopqrstuvwxyzabcdefghijklmNOPQRSTUVWXYZABCDEFGHIJKLM"
    ROTTO   = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
    retstr = ""
    for (pos = 0; pos < length(string); pos++)
    {
	char = substr(string,pos + 1,1)
	rotpos = index(ROTFROM,char)
	if (rotpos > 0)
	    char = substr(ROTTO,rotpos,1)
	retstr = retstr char
    }
    return retstr
}

function tagstart(lvl, tag, parms) {
    printf "%*s", lvl*2, ""
    if (parms == "")
	printf "<%s>\n", tag
    else
	printf "<%s %s>\n", tag, parms
}

function tagend(lvl, tag) {
    printf "%*s", lvl*2, ""
    printf "</%s>\n", tag
}

function ee(text) {
    gsub(/&/, "\\&amp;", text)
    gsub(/</, "\\&lt;", text)
    gsub(/>/, "\\&gt;", text)
    return text
}

function tagtext(lvl, tag, text) {
    text = ee(text)
    printf "%*s", lvl*2, ""
    printf "<%s>%s</%s>\n", tag, text, tag
}

function tagptext(lvl, tag, parms, text) {
    text = ee(text)
    printf "%*s", lvl*2, ""
    printf "<%s %s>%s</%s>\n", tag, parms, text, tag
}

function attr_begin1(gif, id, text) {
    debug(1, "attr_begin1: " gif " " id " \"" text "\"")
    attr_id[gif] = id; attr_text[gif] = text
    debug(1, "attr_id: " attr_id["slealth"])
    debug(1, "attr_id: " attr_id[gif])
}
function attr_begin() {
    # attr_begin1("slealth", 40, "Stealth required")  Dont work!!!
    attr_id["dog"] = 1; attr_text["dog"] = "Dogs"
    attr_id["dogs"] = 1; attr_text["dogs"] = "Dogs"
    attr_id["fee"] = 2; attr_text["fee"] = "Access or parking fee"
    attr_id["rappelling"] = 3; attr_text["rappelling"] = "Climbing gear"
    attr_id["boat"] = 4; attr_text["boat"] = "Boat"
    attr_id["scuba"] = 5; attr_text["scuba"] = "Scuba gear"
    attr_id["kids"] = 6; attr_text["kids"] = "Recommended for kids"
    attr_id["onehour"] = 7; attr_text["onehour"] = "Takes less than an hour"
    attr_id["scenic"] = 8; attr_text["scenic"] = "Scenic view"
    attr_id["hiking"] = 9; attr_text["hiking"] = "Significant hike"

    attr_id["climbing"] = 10; attr_text["climbing"] = "Difficult climbing"
    attr_id["wading"] = 11; attr_text["wading"] = "May require wading"
    attr_id["swimming"] = 12; attr_text["swimming"] = "May require swimming"
    attr_id["available"] = 13; attr_text["available"] = "Available at all times"
    attr_id["night"] = 14; attr_text["night"] = "Recommended at night"
    attr_id["winter"] = 15; attr_text["winter"] = "Available during winter"
    attr_id["poisonoak"] = 16; attr_text["poisonoak"] = "Poison plants"
    attr_id["dangerousanimals"] = 17; attr_text["dangerousanimals"] = "Dangerous Animals"
    attr_id["ticks"] = 18; attr_text["ticks"] = "Ticks"

    attr_id["mines"] = 19; attr_text["mines"] = "Abandoned mines"
    attr_id["cliff"] = 20; attr_text["cliff"] = "Cliff / falling rocks"
    attr_id["hunting"] = 21; attr_text["hunting"] = "Hunting"
    attr_id["danger"] = 22; attr_text["danger"] = "Dangerous area"
    attr_id["wheelchair"] = 23; attr_text["wheelchair"] ="Wheelchair accessible"
    attr_id["parking"] = 24; attr_text["parking"] = "Parking available"
    attr_id["public"] = 25; attr_text["public"] = "Public transportation"
    attr_id["water"] = 26; attr_text["water"] = "Drinking water nearby"
    attr_id["restrooms"] = 27; attr_text["restrooms"] ="Public restrooms nearby"
    attr_id["phone"] = 28; attr_text["phone"] = "Telephone nearby"

    attr_id["picnic"] = 29; attr_text["picnic"] = "Picnic tables nearby"
    attr_id["camping"] = 30; attr_text["camping"] = "Camping available"
    attr_id["bicycles"] = 31; attr_text["bicycles"] = "Bicycles"
    attr_id["motorcycles"] = 32; attr_text["motorcycles"] = "Motorcycles"
    attr_id["quads"] = 33; attr_text["quads"] = "Quads"
    attr_id["jeeps"] = 34; attr_text["jeeps"] = "Off-road vehicles"
    attr_id["snowmobiles"] = 35; attr_text["snowmobiles"] = "Snowmobiles"
    attr_id["horses"] = 36; attr_text["horses"] = "Horses"
    attr_id["campfires"] = 37; attr_text["campfires"] = "Campfires"
    attr_id["thorns"] = 38; attr_text["thorns"] = "Thorns"

    attr_id["stealth"] = 39; attr_text["stealth"] = "Stealth required"
    attr_id["stroller"] = 40; attr_text["stroller"] = "Stroller accessible"
    attr_id["firstaid"] = 41; attr_text["firstaid"] = "Needs maintenance"
    attr_id["cow"] = 42; attr_text["cow"] = "Watch for livestock"
    attr_id["flashlight"] = 43; attr_text["flashlight"] = "Flashlight required"
    attr_id["landf"] = 44; attr_text["landf"] = "Lost And Found Tour"
    attr_id["rv"] = 45; attr_text["rv"] = "Recreational Vehicle"
    attr_id["field"] = 46; attr_text["field"] = "Field Puzzle"
    attr_id["UV"] = 47; attr_text["UV"] = "UV Light Required"
    attr_id["snowshoes"] = 48; attr_text["snowshoes"] = "Snowshoes"

    attr_id["skiis"] = 49; attr_text["skiis"] = "Cross Country Skis"
    attr_id["s-tool"] = 50; attr_text["s-tool"] = "Special Tool Required"
    attr_id["nightcache"] = 51; attr_text["nightcache"] = "Night Cache"
    attr_id["parkngrab"] = 52; attr_text["parkngrab"] = "Park and Grab"
    attr_id["AbandonedBuilding"] = 53; attr_text["AbandonedBuilding"] = "Abandoned Structure"
    attr_id["hike_short"] = 54; attr_text["hike_short"] = "Short hike (less than 1km)"
    attr_id["hike_med"] = 55; attr_text["hike_med"] = "Medium hike (1km-10km)"
    attr_id["hike_long"] = 56; attr_text["hike_long"] = "Long hike (+10km)"
    attr_id["fuel"] = 57; attr_text["fuel"] = "Fuel Nearby"
    attr_id["food"] = 58; attr_text["food"] = "Food Nearby"

    attr_id["wirelessbeacon"] = 59; attr_text["wirelessbeacon"] = "Wireless Beacon"
    attr_id["partnership"] = 60; attr_text["partnership"] = "Partnership"
    attr_id["seasonal"] = 61; attr_text["seasonal"] = "Seasonal Access"
    attr_id["tourist"] = 62; attr_text["tourist"] = "Tourist Friendly"
    attr_id["treeclimbing"] = 63; attr_text["treeclimbing"] = "Tree Climbing"
    attr_id["frontyard"] = 64; attr_text["frontyard"] = "Front Yard (Private Residence)"
    attr_id["teamwork"] = 65; attr_text["teamwork"] = "Teamwork Required"
}

function tagattr(lvl, kind, yesno) {
    kind = kind ""
    #debug(1, "kind: \"" kind "\"")
    if (attr_id[kind] == 0)
	return
    printf "%*s", lvl*2, ""
    printf "<groundspeak:attribute id=\"%d\" inc=\"%d\">", attr_id[kind], yesno
    printf "%s", attr_text[kind]
    printf "</groundspeak:attribute>\n"
}

/cache_types.aspx/ {	# gc 02/01/11
    gs_type = $0
    sub(/.* alt=./, "", gs_type)
    sub(/. width=.*/, "", gs_type)
    sub(/. title=.*/, "", gs_type)
    debug(1, "gs_type: " gs_type)
}
/<span id="ctl00_ContentBody_CacheName">/ {
    if (gs_type)
    {
	gs_name = remspan($0, "ctl00_ContentBody_CacheName")
	next
    }
    gs_type = $0
    sub(/.* alt=./, "", gs_type)
    sub(/. width=.*/, "", gs_type)
    debug(1, "type: " gs_type)
}
/<span id="CacheName">/		{ gs_name = remspan($0, "CacheName") }
/<span id="ctl00_ContentBody_CacheName">/ {
    gs_name = remspan($0, "ctl00_ContentBody_CacheName")
}
/<span id=".*WaypointName".*>/	{ gcid = remspan($0) }
/;wp=GC.*" /	{
    # new way, yech!
    gcid = $0; sub(/.*wp=/, "", gcid); sub(/".*/, "", gcid)
}
/<span id=".*ShortDescription">/	{
    gs_short_description = remspan($0)
}
/<span id="LongDescription">/	{
    gs_long_description = remspanlong($0, "LongDescription")
    waypoints = ""
}
/<span id="ctl00_ContentBody_LongDescription">/	{
    gs_long_description = remspanlong($0, "ctl00_ContentBody_LongDescription")
    waypoints = ""
}
/<div id="div_hint"/		{
    hints = remdiv($0)
    gsub("\n", " ", hints)
    gsub("^ *", "", hints)
    gsub("<br>", "\n", hints)
    if (DECODE)
	hints=rot13(hints)
}
/<span id="Hints"/		{
    hints = remspan($0)
    hints = htmlclean(hints)
    if (DECODE)
	hints=rot13(hints)
    gsub("\n", " ", hints)
}
/<span id="ctl00_ContentBody_Hints"/		{
    hints = $0
    sub(".*displayMe.>", "", hints)
    sub("</span>.*", "", hints)
    gsub("<br>", "\n", hints)
    # debug(1, "Hints: " hints)
    if (DECODE)
	hints=rot13(hints)
}
/<b>Additional Waypoints/	{
    waypoints = remwaypoints()
    wplist = splitwaypoints(waypoints)
}
/<strong>Additional Waypoints/	{
    waypoints = remwaypoints()
    wplist = splitwaypoints(waypoints)
}
# 03/01/2011
/ContentBody_WaypointsInfo/	{
    waypoints = remwaypoints()
    wplist = splitwaypoints(waypoints)
}
/class="LogsTable Table"/ {	# old
    logs_section = 1
}
/class="LogsTable"/ {		# new 06/28/11
    logs_section = 1
}
(logs_section > 0) {
    logs = logs $0
}
(logs_section > 0) && /<table/ {
    logs_section += 1
}
(logs_section > 0) && /<\/table>/ {
    logs_section -= 1
}

/<span id="CacheLogs">/		{
    logs = remspanlong($0, "CacheLogs")
    # remove header which does not exist >2010-01-12
    sub(".*td class=.containerHeader.>Cache Logs</td></tr>", "", logs)
}
/<span id="ctl00_ContentBody_CacheLogs">/		{
    logs = remspanlong($0, "ctl00_ContentBody_CacheLogs")
}
/<span id=".*CacheStats">/	{ stats = remspan($0) }
/<span id=".*NumVisits">/	{
    numvisits = remspan($0)
    debug(1, numvisits)
}

/lnkPrintFriendly/ {
    gid = $0
    if (gid ~ /ID=/)
    {
	# Printable page has ID number
	sub(/^.*ID=/, "", gid)
	sub(/&.*/, "", gid)
    }
    else
    {
	# Non-printable page has guid number
	sub(/^.*guid=/, "", gid)
	sub(/&.*/, "", gid)
    }
}
# Add optional "A cache ". 08/21/2012
/^ *(A cache )*by <a href/ {
    gs_owner = $0
    sub(/.*ds=2.>/, "", gs_owner)
    sub(/<.*/, "", gs_owner)
    debug(1, "owner: " gs_owner)
    gs_guid = $0
    sub(/.*guid=/, "", gs_guid)
    sub(/&.*/, "", gs_guid)
}
# Fake gs_guid is user 03/01/2011
/userInfo = {ID:/ {
    gs_guid = $0
    sub(/.*: /, "", gs_guid)
    sub(/}.*/, "", gs_guid)
    debug(1, "guid " gs_guid)
}
/.* alt=.Size/ {
    gs_size = $0
    sub(/.*Size: /, "", gs_size); sub(". />.*", "", gs_size)
}
/<span id="CacheOwner"/ {
    text = remspan($0)
    debug(1, "Owner text " text)
    gs_type = text; sub(/<.*/, "", gs_type)
    gs_owner = text
	debug(1, gs_owner)
	sub(/.*<br>by /, "", gs_owner); sub(/ [[].*/, "", gs_owner)
	debug(1, gs_owner)
	sub(/<a[^>]*>/, "", gs_owner)
	sub(/<.a[^>]*>/, "", gs_owner)
	sub(/.*<br .>/, "", gs_owner)
	sub(/^by /, "", gs_owner)
	debug(1, "owner " gs_owner)
    gs_size = text; sub(/.*Size: /, "", gs_size); sub(/<.*/, "", gs_size)
    gs_guid = text; sub(/.*guid=/, "", gs_guid)
    sub(/&.*/, "", gs_guid)
    debug(1, "guid " gs_guid)
}
/<span id="ctl00_ContentBody_CacheOwner"/ {
    text = $0
    debug(2, "Owner text: " text)
    gs_type = text
    sub(/<br .*/, "", gs_type)
    sub(/.*>/, "", gs_type)
    debug(1, "gs_type: " gs_type)

    gs_owner = text
    sub(/.*ds=2.>/, "", gs_owner); sub(/<.*/, "", gs_owner)
    debug(1, "gs_owner: " gs_owner)

    gs_size = text; sub(/.*Size: /, "", gs_size); sub(/<.*/, "", gs_size)
    gs_guid = text; sub(/.*guid=/, "", gs_guid)
    sub(/&.*/, "", gs_guid)
    sub(/. title=.*/, "", gs_guid)
    debug(1, "guid: " gs_guid)
}
/<span id="ErrorText"/ {
    if ($0 ~ "unavailable")
	available = "False"
    if ($0 ~ "been archived")
	archived = "True"
}
/<span id="ctl00_ContentBody_ErrorText"/ {
    errortext = remspan($0, "ctl00_ContentBody_ErrorText")
    if (errortext ~ "unavailable")
	available = "False"
    if (errortext ~ "been archived")
	archived = "True"
    debug(1, "available: " available "; archived: " archived)
}
/<span id="LargeMapPrint"/ {
    text = remspan($0)
    lat = text; sub(/.*latitude=/, "", lat); sub(/&.*/, "", lat)
    lon = text; sub(/.*longitude=/, "", lon); sub(/\".*/, "", lon)
    sub(/&.*/, "", lon)
}
/var lat=[-0-9]/ {
    if (lat == "")
    {
	lat = $0; sub(/.*lat=/, "", lat); sub(/;.*/, "", lat)
	lon = $0; sub(/.*lng=/, "", lon); sub(/;.*/, "", lon)
    }
}
/<span id=".*Location"/ {
    text = remspan($0)
    gs_state = text
    sub(/In */, "", gs_state)
    sub(/,.*/, "", gs_state)

    gs_country = text;
    sub(/.*, /, "", gs_country)
    sub(/ <.*/, "", gs_country)
    sub(/^In /, "", gs_country)
}
/lat=.*; lng=.*; guid=/ {
    if (lat == "")
    {
	lat = $0; sub(/.*lat=/, "", lat); sub(/;.*/, "", lat)
	lon = $0; sub(/.*lng=/, "", lon); sub(/;.*/, "", lon)
    }
}
/<span class="minorCacheDetails">Hidden/ {	# gc 2/1/11
    getline time
    getline time
    sub(/^ */, "", time)
    sub(/<.*/, "", time)
    split(time, fld, "/")
    time = sprintf("%d-%02d-%02d", fld[3], fld[1], fld[2])
    debug(1, "time: " time)
}
/> <span class="minorCacheDetails">/ {	# gc 6/28/11
    getline time
    getline time
    getline time
    sub(/^ */, "", time)
    sub(/<.*/, "", time)
    gsub(/-/, "/", time)
    rc = split(time, fld, "/")
    if (rc == 1)
	rc = split(time, fld, "-")
    debug(1, "timerc: " rc)
    if (DATEFMT == 1)
	time = sprintf("%d-%02d-%02d", fld[3], fld[2], fld[1])
    else if (fld[1] >= 1000)
        time = sprintf("%d-%02d-%02d", fld[1], fld[2], fld[3])
    else
	time = sprintf("%d-%02d-%02d", fld[3], fld[1], fld[2])
    debug(1, "time: " time)
}
/<span id="DateHidden">/ {
    getline text
    time = remspan($0)
    split(time, fld, "/")
    time = sprintf("%d-%02d-%02d", fld[3], fld[1], fld[2])
}
/<span id="ctl00_ContentBody_DateHidden">/ {
    time = remspan($0, "ctl00_ContentBody_DateHidden")
    rc = split(time, fld, "/")
    if (rc == 3)
    {
	time = sprintf("%d-%02d-%02d", fld[3], fld[1], fld[2])
	debug(1, "time: " time)
	next
    }
    rc = split(time, fld, ",")
    if (rc == 3)
    {
	yyyy = fld[3];
	split(fld[2], fld, " ")
	mm = Month[ fld[1] ]
	dd = fld[2]
	time = sprintf("%d-%02d-%02d", yyyy, mm, dd)
	debug(1, "time: " time)
	next
    }
    time = ""
}
/ctl00_ContentBody_uxLegendScale/ {
    text = $0
    sub(/.*alt=./, "", text); sub(/ .*/, "", text)
    gs_diff = text
    debug(1 , "gs_diff: " gs_diff)
}
/ctl00_ContentBody_Localize/ {
    text = $0
    sub(/.*alt=./, "", text); sub(/ .*/, "", text)
    gs_terr = text
    debug(1 , "gs_terr: " gs_terr)
}
/^ *Difficulty:<.strong>/ {
    getline text
    sub(/.*alt=./, "", text); sub(/ .*/, "", text)
    gs_diff = text
    debug(1 , "gs_diff: " gs_diff)
}
/^ *Difficulty:/ {	# gc 2/1/11
    getline text
    getline text
    getline text
    sub(/.*alt=./, "", text); sub(/ .*/, "", text)
    gs_diff = text
    debug(1 , "gs_diff: " gs_diff)
}
/<span id="Difficulty">/ {
    text = remspan($0)
    sub(/.*alt=./, "", text); sub(/ .*/, "", text)
    gs_diff = text
}
/<span id="ctl00_ContentBody_Difficulty">/ {
    text = remspan($0, "ctl00_ContentBody_Difficulty")
    sub(/.*alt=./, "", text); sub(/ .*/, "", text)
    debug(1, "difficulty " text)
    gs_diff = text
}
/^ *Terrain:<.strong>/ {
    getline text
    sub(/.*alt=./, "", text); sub(/ .*/, "", text)
    gs_terr = text
    debug(1 , "gs_terr: " gs_terr)
}
/^ *Terrain:/ {		# gc 2/1/11
    getline text
    getline text
    getline text
    sub(/.*alt=./, "", text); sub(/ .*/, "", text)
    gs_terr = text
    debug(1 , "gs_terr: " gs_terr)
}
/<span id="Terrain">/ {
    text = remspan($0)
    sub(/.*alt=./, "", text); sub(/ .*/, "", text)
    gs_terr = text
}
/<span id="ctl00_ContentBody_Terrain">/ {
    text = remspan($0, "ctl00_ContentBody_Terrain")
    sub(/.*alt=./, "", text); sub(/ .*/, "", text)
    debug(1, "terrain " text)
    gs_terr = text
}
/title=.What are Attributes?/ {
    text = $0
    debug(5, "Attr " text)
    gsub("<img src=./images/attributes/", "", text)
    # before 06/03/10
	gsub(/alt="[^"]*" width="30" height="30" .>/, "", text)
    # after 06/03/10
	gsub(/alt="[^"]*" title="[^"]*" width="30" height="30" .>/, "", text)
    gsub("<p class=.NoSpacing.*", "", text)
    gsub(/^ */, "", text)
    gsub(/\.gif../, "", text)
    gsub(/attribute-blank/, "", text)

    attrs_yes = text
    gsub(/[a-z0-9A-Z]*-no/, "", attrs_yes)
    gsub(/-yes/, "", attrs_yes)

    attrs_no = text
    gsub(/[a-z0-9A-Z]*-yes/, "", attrs_no)
    gsub(/-no/, "", attrs_no)

    debug(1, "attrs_yes: " attrs_yes)
    debug(1, "attrs_no: " attrs_no)
    nattr_yes = split(attrs_yes, attr_yes, " ")
    nattr_no = split(attrs_no, attr_no, " ")
    debug(1, "nattr_yes: " nattr_yes)
    debug(1, "nattr_no: " nattr_no)
}
/^{.status.:.success/ {
    ParseJSON($0, json_logs)
    json_log_bool = 1
}

BEGIN {
    Month["January"] = 1
    Month["February"] = 2
    Month["March"] = 3
    Month["April"] = 4
    Month["May"] = 5
    Month["June"] = 6
    Month["July"] = 7
    Month["August"] = 8
    Month["September"] = 9
    Month["October"] = 10
    Month["November"] = 11
    Month["December"] = 12
    BaseURL = "http://www.geocaching.com/seek/cache_details.aspx"
    attr_begin()

    first = 1

    wpt_init()
}
/<\/html>/ {
    if ((lat == "") || (lon == ""))
    {
	debug(0, "Waypoint coordinates not found for " gcid ", no output!")
	#next
    }

    # too long a block to be indented
    if (!INCR && first)
    {
	print "<?xml version=\"1.0\" encoding=\"utf-8\"?>"
	tagstart(0, "gpx")
	tagtext(1, "desc", "Geocache file generated by geo-html2gpx")
	tagtext(1, "author", "geo-html2gpx")
	"date +%Y-%m-%dT%H:%M:%S" | getline date
	tagtext(1, "time", date)
	first = 0
    }

    gs_name = umlauts(gs_name)
    gs_owner = umlauts(gs_owner)

    tagstart(1, "wpt", "lat=\"" lat "\" lon=\"" lon "\"")
    if (time != "")
	tagtext(2, "time", time "T07:00:00Z")
    tagtext(2, "name", gcid)
    tagtext(2, "desc", gs_name " by " gs_owner ", " \
		    gs_type " (" gs_diff "/" gs_terr ")")

    # alternate URL... tagtext(2, "url", BaseURL "?wp=" gcid)
    # alternate URL... tagtext(2, "url", BaseURL "?id=" gid)
    tagtext(2, "url", BaseURL "?wp=" gcid)
    tagtext(2, "urlname", gs_name)

    # we do this last... tagtext(2, "sym", sym)

    tagtext(2, "type", "Geocache|" gs_type)

    # FIXME? GC-written GPX files contain numeric, non-UUID,
    # cache/owner/finder ids
    # Oregon needs numeric cache id, or behaves erratically!
    gid = wp2id(gcid)
    tagstart(2, "groundspeak:cache",
	    "id=\"" gid "\" available=\"" available \
	    "\" archived=\"" archived "\"" \
	    " xmlns:groundspeak=\"http://www.groundspeak.com/cache/1/0/1\"")
    tagtext(3, "groundspeak:name", gs_name)
    tagtext(3, "groundspeak:placed_by", gs_owner)
    tagptext(3,"groundspeak:owner", "id=\"" gs_guid "\"", gs_owner)
    tagtext(3, "groundspeak:type", gs_type)
    tagtext(3, "groundspeak:container", gs_size)

    if (nattr_yes != 0 || nattr_no != 0)
    {
	tagstart(3, "groundspeak:attributes")
	for (i = 1; i <= nattr_yes; ++i)
	    tagattr(4, attr_yes[i], 1)
	for (i = 1; i <= nattr_no; ++i)
	    tagattr(4, attr_no[i], 0)
	tagend(3, "groundspeak:attributes")
    }

    tagtext(3, "groundspeak:difficulty", gs_diff)
    tagtext(3, "groundspeak:terrain", gs_terr)
    tagtext(3, "groundspeak:country", gs_country)
    tagtext(3, "groundspeak:state", gs_state)
    if (!NOHTML)
    {
	tagptext(3, "groundspeak:short_description", "html=\"True\"",
							gs_short_description)
	if (!NOWPTS && waypoints)
	{
	    # reproduce "simplified table" by GC PQ
	    # prefixed_gcid - wpname<br />original_style_coord<br />note<br />
	    waypoints = wpclean(waypoints)
	    # include "zero" waypoints here!
	    gs_long_description = gs_long_description \
				"<p>Additional Waypoints</p>" waypoints
	}
	tagptext(3, "groundspeak:long_description", "html=\"True\"",
							gs_long_description)
    }
    else
    {
	gs_short_description = htmlclean(gs_short_description)
	tagptext(3, "groundspeak:short_description", "html=\"False\"",
							gs_short_description)
	gs_long_description = htmlclean(gs_long_description)
	if (waypoints)
	    gs_long_description = gs_long_description \
		    "\n\nAdditional Waypoints\n" tableclean(waypoints)
	tagptext(3, "groundspeak:long_description", "html=\"False\"",
							gs_long_description)
    }
    tagtext(3, "groundspeak:encoded_hints", hints)

    if (json_log_bool)
    {
	nlogs = JSONArrayLength(json_logs, "data")
	if (nlogs > NUMLOGS+1)
	    nlogs = NUMLOGS+1
	debug(1, "New Logs: " nlogs)
	if (nlogs > 1)
	    tagstart(3, "groundspeak:logs")
	else
	    tagstart(3, "groundspeak:logs", "/")

	for (i = 1; i < nlogs; ++i)
	{
	    ltype = json_logs["data" SUBSEP i SUBSEP "LogTypeImage"]
	    if (ltype ~ /smile/) ltype = "Found it"
	    else if (ltype ~ /happy/) ltype = "Found it"
	    else if (ltype ~ /note/) ltype = "Write note"
	    else if (ltype ~ /sad/) ltype = "Didn'"'"'t Find it"
	    else if (ltype ~ /attended/) ltype = "Attended"
	    else if (ltype ~ /rsvp/) ltype = "Will Attend"
	    else if (ltype ~ /greenlight/) ltype = "Green"
	    else if (ltype ~ /traffic_cone/) ltype = "Archive"
	    else if (ltype ~ /disabled/) ltype = "Temporarily Disable Listing"
	    else if (ltype ~ /coord_update/) ltype = "Update Coordinates"
	    else ltype = "Unknown"

	    ldate = json_logs["data" SUBSEP i SUBSEP "Visited"]
	    n = split(ldate, fld, "/")
	    if (n == 3)
	    {
		#new format: 08/18/2011
		if (DATEFMT == 1)
		    ldate = sprintf("%d-%02d-%02dT20:00:00Z",
			fld[3], fld[2], fld[1])
		else
		    ldate = sprintf("%d-%02d-%02dT20:00:00Z",
			fld[3], fld[1], fld[2])
		debug(2, "logdate: " ldate)
	    }
	    lfinder = json_logs["data" SUBSEP i SUBSEP "UserName"]
	    lfinder = umlauts(lfinder)
	    logid = json_logs["data" SUBSEP i SUBSEP "LogID"]
	    guid = json_logs["data" SUBSEP i SUBSEP "AccountID"]
	    ltext = json_logs["data" SUBSEP i SUBSEP "LogText"]
	    ltext = htmlclean(ltext)
	    ltext = umlauts(ltext)

	    if (lfinder == USERNAME && ltype == "Found it")
		sym = "Geocache Found"
	    if (lfinder == USERNAME && ltype == "Attended")
		sym = "Geocache Found"
	    tagstart(4, "groundspeak:log", "id=\"" logid "\"")
	    tagtext(5, "groundspeak:date", ldate)
	    tagtext(5, "groundspeak:type", ltype)
	    tagptext(5, "groundspeak:finder", "id=\"" guid "\"", lfinder)
	    tagptext(5, "groundspeak:text", "encoded=\"" "False" "\"", ltext)
	    tagend(4, "groundspeak:log")
	}

	if (nlogs > 1)
	    tagend(3, "groundspeak:logs")
    }
    else
    {
	# nlogs = split(logs, entry, "</tr>")
	nlogs = split(logs, entry, "</tr><tr>")
	if (nlogs > NUMLOGS+1)
	    nlogs = NUMLOGS+1

	if (nlogs > 1)
	    tagstart(3, "groundspeak:logs")
	else
	    tagstart(3, "groundspeak:logs", "/")

	for (i = 1; i < nlogs; ++i)
	{
	    sub("<tr><td[^>]*>", "", entry[i])
	    sub("</td>", "", entry[i])
	    if (!entry[i]) continue
	    # old split location
	    sub(/.*<[Ss][Tt][Rr][Oo][Nn][Gg]><img src=./, "", entry[i])

	    ltype = entry[i]
	    #debug(1, "log: " ltype)
	    sub(/>.*/, "", ltype) # leaves the URL of the smiley
	    if (ltype ~ /smile/) ltype = "Found it"
	    else if (ltype ~ /happy/) ltype = "Found it"
	    else if (ltype ~ /note/) ltype = "Write note"
	    else if (ltype ~ /sad/) ltype = "Didn'"'"'t Find it"
	    else if (ltype ~ /attended/) ltype = "Attended"
	    else if (ltype ~ /rsvp/) ltype = "Will Attend"
	    else if (ltype ~ /greenlight/) ltype = "Green"
	    else if (ltype ~ /traffic_cone/) ltype = "Archive"
	    else if (ltype ~ /disabled/) ltype = "Temporarily Disable Listing"
	    else if (ltype ~ /coord_update/) ltype = "Update Coordinates"
	    else ltype = "Unknown"

	    ldate = entry[i]
	    # split off &nbsp;/blank
	    sub(/^[^>]*>[^ ;]*[ ;]/, "", ldate)
	    sub(/ by <.*/, "", ldate)
	    sub(/ by /, "", ldate)
	    sub(/.*LogDate.>about /, "", ldate)
	    sub(/.*LogDate.>/, "", ldate)
	    sub(/<.*/, "", ldate)
	    gsub(/-/, "/", ldate)
	    debug(1, "logdate: " ldate)
	    if (ldate ~ /ago/)
	    {
		cmd = sprintf("%s -d \"12am %s\" +%%Y-%%m-%%dT07:00:00Z",
		    DATE, ldate)
		cmd | getline ldate; close(cmd)
	    }
	    else
	    {
		n = split(ldate, fld, " ")
		if (n >= 2)
		{
		    #old format: August 18
		    mm = Month[fld[1]]
		    dd = fld[2] + 0
		    if (n >= 3)
			yy = fld[3]
		    if (yy+0 == 0)
			yy = YR
		    ldate = sprintf("%d-%02d-%02dT07:00:00", yy, mm, dd)
		}
		n = split(ldate, fld, "/")
		if (n == 3)
		{
		    #new format: 08/18/2011
		    if (DATEFMT == 1)
			ldate = sprintf("%d-%02d-%02dT07:00:00",
			    fld[3], fld[2], fld[1])
		    else
			ldate = sprintf("%d-%02d-%02dT07:00:00",
			    fld[3], fld[1], fld[2])
		    debug(1, "logdate: " ldate)
		}
	    }

	    lfinder = entry[i]
	    sub(/[^<]*</, "", lfinder)	# Delete all before <A NAME...

	    logid = lfinder
	    sub(/[^"]*"/, "", logid)
	    sub(/.* id="/, "", logid)
	    sub(/.*LUID=/, "", logid)
	    sub(/\".*/, "", logid)
	    debug(1, "logid: " logid)

	    guid = lfinder
	    debug(1, "guid: " guid)
	    #sub(/[^>]*>/, "", guid)		# Delete all before <A HREF...
	    #sub(/>.*/, "", guid)		# Delete all after <A HREF...
	    sub(/.*guid=/, "", guid)
	    sub(/\".*/, "", guid)
	    sub(/\&.*/, "", guid)
	    sub(/. id=.*/, "", guid)
	    debug(1, "guid: " guid)

	    #debug(1, "lfinder: " lfinder)
	    sub(/[^>]*>/, "", lfinder)		# Delete all before <A HREF...
	    #debug(1, "lfinder: " lfinder)
	    #sub(/[^>]*>/, "", lfinder)	# Delete all before name
	    sub(/<.*/, "", lfinder)		# Delete all after name
	    lfinder = umlauts(lfinder)
	    debug(1, "lfinder: " lfinder)

	    ltext = entry[i]
	    sub(/.*found\)<br .>/, "", ltext)
	    sub("</font>.*", "", ltext)
	    sub("<a href=.log.aspx[^>]*>[^<]*</a>", "", ltext)
	    sub("<a href=.upload.aspx[^>]*>[^<]*</a>", "", ltext)
	    # remove remaining HTML tags from log text. Seems to be a good
	    # idea in any case, independent of NOHTML setting!
	    ltext = htmlclean(ltext)
	    ltext = umlauts(ltext)

	    if (lfinder == USERNAME && ltype == "Found it")
		sym = "Geocache Found"
	    if (lfinder == USERNAME && ltype == "Attended")
		sym = "Geocache Found"
	    tagstart(4, "groundspeak:log", "id=\"" logid "\"")
	    tagtext(5, "groundspeak:date", ldate)
	    tagtext(5, "groundspeak:type", ltype)
	    tagptext(5, "groundspeak:finder", "id=\"" guid "\"", lfinder)
	    tagptext(5, "groundspeak:text", "encoded=\"" "False" "\"", ltext)
	    tagend(4, "groundspeak:log")
	}
	if (nlogs > 1)
	    tagend(3, "groundspeak:logs")
    }

    tagstart(3, "groundspeak:travelbugs", "/")

    tagend(2, "groundspeak:cache")
    tagtext(2, "sym", sym)
    tagend(1, "wpt")

    # add Additional Waypoints in wpt form
    if (!NOWPTS && wplist)
    {
	split(wplist, wps, "\n")
	i = 0
	for (wp in wps)
	    ++i
	wp = 0
	while (wp < i)
	{
	    ++wp
	    # lat lon|prefix|lookup|wpname|url|note
	    # i.e.: lat="44.888267" lon="-93.159233"|PC|PARK|http://...
	    #		|GCPMG6-Parking (Parking Area)|.31 miles from cache.
	    debug(1, "wps: " wps[wp])
	    split(wps[wp], line, "|")
	    if (line[1] && 
		(!NOZERO || (line[1] !~ "lat=\"0.000000\" lon=\"0.000000\"") ) )
	    {
		# line format: coords|prefix|lookup|wpname|note
		tagstart(1, "wpt", line[1])
		#tagtext(2, "time", "...")
		tagtext(2, "name", line[2] substr(gcid,3))
		tagtext(2, "cmt", line[6] ? line[6] : "")
		statname = line[4]
		gsub(" \\(.*\\).*", "", statname)

		desc = line[4]
		sub(" \\(.*", "", desc)
		tagtext(2, "desc", desc)

		tagtext(2, "url", line[5])

		urlname = desc
		tagtext(2, "urlname", urlname)

		stattype = line[4]
		gsub(".*\\(", "", stattype)
		gsub("\\).*", "", stattype)
		tagtext(2, "sym", stattype)
		tagtext(2, "type", "Waypoint|" stattype)
		tagend(1, "wpt")
	    }
	}
    }
    wpt_init()
}
END {
    if (!INCR && !first)
	tagend(0, "gpx")
}
' | $POSTPROC
