8 `basename $PROGNAME`- Convert gc.com *printable* web pages into GPX
11 `basename $PROGNAME` [options] [gc-com.html]...
14 Convert gc.com *printable* web pages into GPX, including
15 cache description and all logs.
17 The *printable* web pages can be fetched using geo-nearest,
18 geo-newest, geo-placed, geo-found, or geo-gid with the -H option.
21 -b Normalize output by postprocessing with gpsbabel
22 -e Encode hints with rot13 (e.g. NORTH = ABEGU)
23 -i Incremental, no XML and GPX headers
24 -l number Maximum number of log entries to be exported [unlimited]
25 -n No HTML in descriptions (experimental)
26 -o FMT Output FMT instead of GPX by using gpsbabel
27 -u username Indicate found status for username [$USERNAME]
28 -w Do not add "Additional Waypoints" to the GPX output
29 -z Do not output waypoints with "zero" coordinates
30 -E var=val Set environment "var" to "val"
35 Defaults can also be set with variables in file \$HOME/.georc:
40 Geocaching.com date formats that are compatible:
42 GC Format Example Compatible
43 YYYY-MM-DD 2011-07-13 yes
44 YYYY/MM/DD 2011/07/13 yes
45 MM/DD/YYYY 07/13/2011 yes
46 DD/MM/YYYY 13/07/2011 yes if DATEFMT=1 in \$HOME/.georc
47 DD/Mmm/YYYY 13/Jul/2001 no
48 Mmm/DD/YYYY Jul/13/2011 no
49 DD Mmm YY 13 Jul 11 yes (english only)
53 http://www.geocaching.com/account/ManagePreferences.aspx
58 geo-found -n9999 -H. > /dev/null
59 geo-html2gpx *.html > found.gpx
66 # Report an error and exit
69 echo "`basename $PROGNAME`: $1" >&2
74 if [ $DEBUG -ge $1 ]; then
75 echo "`basename $PROGNAME`: $2" >&2
79 if [ `uname` = 'Darwin' ]; then
88 # Read RC file, if there is one
91 if [ -f $HOME/.georc ]; then
93 # N.B. must switch to read_rc_file if LAT/LON is ever needed here
107 while getopts "beE:iwzl:no:u:D:h?" opt
110 b) POSTPROC="gpsbabel -igpx -f- -ogpx -F-";;
114 l) NUMLOGS="$OPTARG";;
115 o) POSTPROC="gpsbabel -igpx -f- -o$OPTARG -F-";;
117 u) USERNAME="$OPTARG";;
124 shift `expr $OPTIND - 1`
131 case `$awk --version` in
133 *) error "awk is not GNU awk!";;
137 | tr -d '\001\002\003\004\005\006\007\015\021\022\023\024\026\030' \
140 $awk -vDEBUG=$DEBUG -vINCR=$INCR \
141 -vNOWPTS=$NOWPTS -vNOZERO=$NOZERO \
144 -vUSERNAME="$USERNAME" \
146 -vDATEFMT="$DATEFMT" \
147 -vYR="$YR" -vNUMLOGS=$NUMLOGS \
149 # Copyright (c) 2010 Dan Saar
151 # Permission is hereby granted, free of charge, to any person obtaining a copy
152 # of this software and associated documentation files (the "Software"), to deal
153 # in the Software without restriction, including without limitation the rights
154 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
155 # copies of the Software, and to permit persons to whom the Software is
156 # furnished to do so, subject to the following conditions:
158 # The above copyright notice and this permission notice shall be included in
159 # all copies or substantial portions of the Software.
161 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
162 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
163 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
164 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
165 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
166 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
169 function prsJSON_hex2num(s, rv, ii, len, k)
175 for (ii = 1; ii <= len; ii++)
177 k = index("0123456789abcdef", substr(s, ii, 1))
187 function prsJSON_EncodeAsUTF8( v, s, p1, p2, p3, p4, cs )
189 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"
192 s = sprintf("%c", v )
194 else if ( v < 2048 ) # 110xxxxx 10xxxxxx
198 s = substr(cs, 65+p1, 1) substr(cs, p2+1, 1)
201 else if ( v < 65536 ) # 1110xxxx 10xxxxxx 10xxxxxx
203 p1 = int(v/4096) % 16
206 s = substr(cs, 97+p1, 1) substr(cs, p2+1, 1) substr(cs, p3+1, 1)
209 else if ( v < 1114112 ) # 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
211 p1 = int(v/262144) % 8
212 p2 = int(v/4096) % 64
215 s = substr(cs, 113+p1, 1) substr(cs, p2+1, 1) substr(cs, p3+1, 1) substr(cs, p4+1, 1)
224 function prsJSON_UnescapeString(jsonString, matchedString, matchedValue)
226 if (jsonString == "\"\"")
229 if (jsonString ~ /^".+"$/)
230 jsonString = substr(jsonString,2,length(jsonString)-2)
232 gsub(/\\\\/, "\\u005C", jsonString)
233 gsub(/\\"/, "\"", jsonString)
234 gsub(/\\\//, "/", jsonString)
235 gsub(/\\b/, "\b", jsonString)
236 gsub(/\\f/, "\f", jsonString)
237 gsub(/\\n/, "\n", jsonString)
238 gsub(/\\r/, "\r", jsonString)
239 gsub(/\\t/, "\t", jsonString)
241 if (match(jsonString, /\\[^u]/))
242 return "ParseJSON Error: Invalid String at " jsonString
244 # handle encoded UTF-16 surrogates
245 while (match(jsonString, /\\uD[89AaBb][0123456789AaBbCcDdEeFf][0123456789AaBbCcDdEeFf]\\uD[CcDdEeFf][0123456789AaBbCcDdEeFf][0123456789AaBbCcDdEeFf]/))
247 matchedValue = (prsJSON_hex2num(substr(jsonString, RSTART+2, 4)) % 1024) * 1024 + prsJSON_hex2num(substr(jsonString, RSTART+8, 4)) % 1024 + 65536
248 #print matchedValue, substr(jsonString, RSTART+2, 4), substr(jsonString, RSTART+8, 4)
249 matchedString = prsJSON_EncodeAsUTF8( matchedValue )
250 sub(/\\uD[89AaBb][0123456789AaBbCcDdEeFf][0123456789AaBbCcDdEeFf]\\uD[CcDdEeFf][0123456789AaBbCcDdEeFf][0123456789AaBbCcDdEeFf]/, matchedString, jsonString)
253 while (match(jsonString, /\\u[0123456789AaBbCcDdEeFf][0123456789AaBbCcDdEeFf][0123456789AaBbCcDdEeFf][0123456789AaBbCcDdEeFf]/))
255 matchedValue = prsJSON_hex2num(substr(jsonString, RSTART+2, 4))
256 matchedString = prsJSON_EncodeAsUTF8( matchedValue )
257 sub(/\\u[0123456789AaBbCcDdEeFf][0123456789AaBbCcDdEeFf][0123456789AaBbCcDdEeFf][0123456789AaBbCcDdEeFf]/, matchedString, jsonString)
263 function prsJSON_ValidString(jsonString)
265 return jsonString !~ /^ParseJSON Error: Invalid String at /
268 function prsJSON_SetDataValue(jsonData, prefix, value)
270 jsonData[prefix] = value
273 function prsJSON_Error(jsonStringArr, cnt, idx, jsonData, message)
276 prsJSON_SetDataValue(jsonData, "1", sprintf("ParseJSON Error: %s at ", message) (idx <= cnt ? jsonStringArr[idx] : ""))
277 split("", jsonStringArr)
281 function prsJSON_CopyError(jsonData, tv)
284 prsJSON_SetDataValue(jsonData, "1", tv[1])
287 function prsJSON_ParseNumber(jsonStringArr, cnt, idx, jsonData, prefix)
291 if (match(jsonStringArr[idx], /^(\-?)(0|[123456789][0123456789]*)(\.[0123456789]+)?([eE][+-]?[0123456789]+)?/))
293 prsJSON_SetDataValue(jsonData, prefix, substr(jsonStringArr[idx], 1, RLENGTH))
294 jsonStringArr[idx] = length(jsonStringArr[idx]) >= RLENGTH+1 ? substr(jsonStringArr[idx], RLENGTH+1) : ""
297 idx = prsJSON_Error(jsonStringArr, cnt, idx, jsonData, "Number not found") # starts like a number, but doesnt match the REGEX
303 function prsJSON_ParseString(jsonStringArr, cnt, idx, jsonData, prefix, jsonString, idxn, idxs, idxq, t)
305 if (idx <= cnt && length(jsonStringArr[idx]) > 0 && substr(jsonStringArr[idx], 1, 1) == "\"")
308 jsonString = jsonStringArr[idx]
312 t = length(jsonString) >= idxn ? substr(jsonString, idxn) : ""
313 idxs = index(t, "\\")
314 idxq = index(t, "\"")
316 # no valid close quote found
323 jsonString = jsonString "," jsonStringArr[idx]
326 # a valid close quote was found - not before a slash
327 if (idxq != 0 && (idxs == 0 || (idxs != 0 && idxq < idxs)))
330 if (idxs != 0 && idxq == idxs + 1) # slash quote
334 idxn = idxn + idxs + 1
340 t = substr(jsonString, 1, idxn+idxq-1)
341 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)
343 t = prsJSON_UnescapeString(t)
344 if ( prsJSON_ValidString(t) )
346 prsJSON_SetDataValue(jsonData, prefix, t)
347 jsonStringArr[idx] = length(jsonString) >= idxn+idxq ? substr(jsonString,idxn+idxq) : ""
350 idx = prsJSON_Error(jsonStringArr, cnt, idx, jsonData, "Invalid string")
353 idx = prsJSON_Error(jsonStringArr, cnt, idx, jsonData, "Invalid character in string")
356 idx = prsJSON_Error(jsonStringArr, cnt, idx, jsonData, "Unterminated string")
359 idx = prsJSON_Error(jsonStringArr, cnt, idx, jsonData, "String expected")
364 function prsJSON_ParseObject(jsonStringArr, cnt, idx, jsonData, prefix, tv )
368 sub(/^\{[ \t\r\n\f]*/, "", jsonStringArr[idx]) #skip open { and skipwhite
370 while (idx <= cnt && length(jsonStringArr[idx]) > 0 && substr(jsonStringArr[idx], 1, 1) != "}")
372 idx = prsJSON_ParseString(jsonStringArr, cnt, idx, tv, "1")
374 if (idx <= cnt && length(tv[1]) == 0)
375 idx = prsJSON_Error(jsonStringArr, cnt, idx, tv, "Empty string used for property name")
379 sub(/^[ \t\r\n\f]+/, "", jsonStringArr[idx]) #skipwhite
381 if ( length(jsonStringArr[idx]) > 0 && substr(jsonStringArr[idx], 1, 1) == ":" )
383 sub(/^:[ \t\r\n\f]*/, "", jsonStringArr[idx]) #skip colon and skipwhite
385 if ( length(jsonStringArr[idx]) > 0 )
387 idx = prsJSON_ParseJSONInt(jsonStringArr, cnt, idx, jsonData, prefix != "" ? prefix SUBSEP tv[1] : tv[1])
390 sub(/^[ \t\r\n\f]+/, "", jsonStringArr[idx]) #skipwhite
392 if (length(jsonStringArr[idx]) == 0 && idx < cnt)
395 sub(/^[ \t\r\n\f]+/, "", jsonStringArr[idx]) #skipwhite
396 if (length(jsonStringArr[idx]) == 0 || substr(jsonStringArr[idx], 1, 1) == "}")
397 idx = prsJSON_Error(jsonStringArr, cnt, idx, jsonData, "Expected object property")
400 else if (length(jsonStringArr[idx]) == 0 || substr(jsonStringArr[idx], 1, 1) != "}")
401 idx = prsJSON_Error(jsonStringArr, cnt, idx, jsonData, "Expected object property or closing brace")
405 idx = prsJSON_Error(jsonStringArr, cnt, idx, jsonData, "Expected JSON value (1)")
408 idx = prsJSON_Error(jsonStringArr, cnt, idx, jsonData, "Expected colon")
411 prsJSON_CopyError(jsonData, tv)
414 if (idx <= cnt && (length(jsonStringArr[idx]) == 0 || substr(jsonStringArr[idx], 1, 1) != "}"))
415 idx = prsJSON_Error(jsonStringArr, cnt, idx, jsonData, "Expected closing brace")
417 if (idx <= cnt && length(jsonStringArr[idx]) > 0 && substr(jsonStringArr[idx], 1, 1) == "}")
418 sub(/^\}[ \t\r\n\f]*/, "", jsonStringArr[idx]) #skip close } and skipwhite
424 function prsJSON_ParseArray(jsonStringArr, cnt, idx, jsonData, prefix, ii)
428 sub(/^\[[ \t\r\n\f]*/, "", jsonStringArr[idx]) #skip open bracket and skipwhite
431 while (idx <= cnt && length(jsonStringArr[idx]) > 0 && substr(jsonStringArr[idx], 1, 1) != "]")
433 idx = prsJSON_ParseJSONInt(jsonStringArr, cnt, idx, jsonData, prefix != "" ? prefix SUBSEP ii : ii )
438 sub(/^[ \t\r\n\f]+/, "", jsonStringArr[idx]) #skipwhite
440 if (length(jsonStringArr[idx]) == 0 && idx < cnt)
443 sub(/^[ \t\r\n\f]+/, "", jsonStringArr[idx]) #skipwhite
444 if (length(jsonStringArr[idx]) == 0 || substr(jsonStringArr[idx], 1, 1) == "]")
445 idx = prsJSON_Error(jsonStringArr, cnt, idx, jsonData, "Expected array value")
448 else if (length(jsonStringArr[idx]) == 0 || substr(jsonStringArr[idx], 1, 1) != "]")
449 idx = prsJSON_Error(jsonStringArr, cnt, idx, jsonData, "Expected array value or closing bracket")
453 if (idx <= cnt && (length(jsonStringArr[idx]) == 0 || substr(jsonStringArr[idx], 1, 1) != "]"))
454 idx = prsJSON_Error(jsonStringArr, cnt, idx, jsonData, "Expected closing bracket")
456 if (idx <= cnt && length(jsonStringArr[idx]) > 0 && substr(jsonStringArr[idx], 1, 1) == "]")
457 sub(/^\][ \t\r\n\f]*/, "", jsonStringArr[idx]) #skip close bracket and skipwhite
463 function prsJSON_ParseJSONInt(jsonStringArr, cnt, idx, jsonData, prefix, tk)
467 sub(/^[ \t\r\n\f]+/, "", jsonStringArr[idx]) #skipwhite
469 if (length(jsonStringArr[idx]) > 0)
471 tk = substr(jsonStringArr[idx], 1, 1)
472 if (tk == "\"" && prefix != "")
473 idx = prsJSON_ParseString(jsonStringArr, cnt, idx, jsonData, prefix)
474 else if (tk ~ /^[0123456789-]/ && prefix != "")
475 idx = prsJSON_ParseNumber(jsonStringArr, cnt, idx, jsonData, prefix)
476 else if (jsonStringArr[idx] ~ /^true/ && prefix != "")
478 prsJSON_SetDataValue(jsonData, prefix, "<<true>>")
479 jsonStringArr[idx] = length(jsonStringArr[idx]) <= 4 ? "" : substr(jsonStringArr[idx],5)
481 else if (jsonStringArr[idx] ~ /^false/ && prefix != "")
483 prsJSON_SetDataValue(jsonData, prefix, "<<false>>")
484 jsonStringArr[idx] = length(jsonStringArr[idx]) <= 5 ? "" : substr(jsonStringArr[idx],6)
486 else if (jsonStringArr[idx] ~ /^null/ && prefix != "")
488 prsJSON_SetDataValue(jsonData, prefix, "<<null>>")
489 jsonStringArr[idx] = length(jsonStringArr[idx]) <= 4 ? "" : substr(jsonStringArr[idx],5)
492 idx = prsJSON_ParseObject(jsonStringArr, cnt, idx, jsonData, prefix)
494 idx = prsJSON_ParseArray(jsonStringArr, cnt, idx, jsonData, prefix)
496 idx = prsJSON_Error(jsonStringArr, cnt, idx, jsonData, "Expected JSON value (2)")
499 sub(/^[ \t\r\n\f]+/, "", jsonStringArr[idx]) #skipwhite
502 if (prefix == "" && idx <= cnt && length(jsonStringArr[idx]) != 0)
503 idx = prsJSON_Error(jsonStringArr, cnt, idx, jsonData, "Expected end of JSON text")
504 else if (prefix == "" && idx+1 <= cnt)
507 idx = prsJSON_Error(jsonStringArr, cnt, idx, jsonData, "Expected end of JSON text (2)")
516 # JSON Formatting Routines
519 function useJSON_ArrayCount( possibleArray, a, min, max, cnt, rv)
523 for ( a in possibleArray )
525 if (possibleArray[a] "" !~ /^[0123456789][0123456789]*$/)
530 min = possibleArray[a]
531 max = possibleArray[a]
535 if (min == possibleArray[a] || max == possibleArray[a])
538 if (possibleArray[a] < min)
539 min = possibleArray[a]
541 if (max < possibleArray[a])
542 max = possibleArray[a]
548 if (min == 1 && max == cnt)
554 function useJSON_GetObjectMembers(jsonSchema, prefix)
556 if (prefix == "") prefix = "<<novalue>>"
557 return prefix in jsonSchema ? jsonSchema[prefix] : ""
560 # quick sort array arr
561 function utlJSON_qsortArray(arr, left, right, i, last, t)
563 if (left >= right) # do nothing if array has less than 2 elements
565 i = left + int((right-left+1)*rand())
569 last = left # arr[left] is now partition element
570 for (i = left+1; i <= right; i++)
572 if (arr[i] < arr[left])
581 arr[left] = arr[last];
583 utlJSON_qsortArray(arr, left, last-1)
584 utlJSON_qsortArray(arr, last+1, right)
587 function useJSON_GetSchema(jsonData, jsonSchema, a, tidx, tv, sv, idx)
589 split("", jsonSchema)
592 while (match(a, SUBSEP "[^" SUBSEP "]+$"))
594 tidx = substr(a,1,RSTART-1)
595 tv = substr(a,RSTART+1)
596 sv = (tidx in jsonSchema) ? jsonSchema[tidx] : ""
597 # if ( sv != tv && sv !~ "^" tv SUBSEP && sv !~ SUBSEP tv "$" && sv !~ SUBSEP tv SUBSEP )
598 # Rephrase this using index so object member names with regex characters work
599 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 )
600 jsonSchema[tidx] = sv (sv == "" ? "" : SUBSEP) tv
606 sv = (tidx in jsonSchema) ? jsonSchema[tidx] : ""
607 if ( sv != tv && sv !~ "^" tv SUBSEP && sv !~ SUBSEP tv "$" && sv !~ SUBSEP tv SUBSEP )
608 jsonSchema[tidx] = sv (sv == "" ? "" : SUBSEP) tv
612 function useJSON_EscapeString(s, ii, c, t, t2, t3, t4, cs)
614 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"
615 gsub(/\\/, "\\u005C", s)
617 #gsub(/\//, "\\/", s) # required to decode, but not to encode
624 for ( ii = 1 ; ii <= length(s) ; ii++ )
628 if (t == "\000") # having \000 in list below doesnt work in all awks
631 s = (ii > 1 ? substr(s, 1, ii-1) : "") sprintf("\\u%04X", c) (ii==length(s) ? "" : substr(s, ii+1))
636 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)
641 s = (ii > 1 ? substr(s, 1, ii-1) : "") sprintf("\\u%04X", c) (ii==length(s) ? "" : substr(s, ii+1))
647 t2 = ii+1 <= length(s) ? index(cs, substr(s,ii+1,1)) : 0
648 t3 = ii+2 <= length(s) ? index(cs, substr(s,ii+2,1)) : 0
649 t4 = ii+3 <= length(s) ? index(cs, substr(s,ii+3,1)) : 0
651 if ( c < 0 && t > 64 && t <= 96 && ii+1 <= length(s) && t2 > 0 && t2 <= 64) # two character UTF-8 sequence
653 c = (t - 65)*64 + (t2-1)
654 s = (ii > 1 ? substr(s, 1, ii-1) : "") sprintf("\\u%04X", c) (ii+1==length(s) ? "" : substr(s, ii+2))
658 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
660 c = (t - 97)*4096 + (t2-1)*64 + (t3-1)
663 s = (ii > 1 ? substr(s, 1, ii-1) : "") sprintf("\\u%04X", c) (ii+2==length(s) ? "" : substr(s, ii+3))
668 # encode in JSON-style with two \u#### UTF-16 surrogates
669 # printf("1: %08X\n", c)
670 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))
675 # four character UTF-8 sequence, encode in JSON-style with two \u#### UTF-16 surrogates
676 else if ( c < 0 && t > 112 && t <= 120 && ii+3 <= length(s) && t2 > 0 && t2 <= 64 && t3 > 0 && t3 <= 64 && t4 > 0 && t4 <= 64)
678 c = (t - 113)*262144 + (t2-1)*4096 + (t3-1)*64 + (t4-1)
679 # printf("2: %08X, %d, %d, %d, %d\n", c, t, t2, t3, t4)
680 # printf("\\u%04X\\u%04X\n", (c/1024)%1024 + 55296, c%1024 + 56320)
682 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))
690 function useJSON_GetDataValue(jsonData, prefix)
692 return prefix in jsonData ? jsonData[prefix] : "<<novalue>>"
695 function useJSON_PrettyFormat(s, pretty)
697 if (s == "" || pretty <= 0) return s
699 # dont sprintf the whole thing, some awks have short buffers for sprintf
700 return sprintf("%*.*s", (pretty-1)*3, (pretty-1)*3, "") s (s == "}" || s == "]" ? "" : "\n")
703 function useJSON_FormatInt(jsonData, jsonSchema, prefix, pretty, allLines, member, memberArr, memberList, arrCount, a, ii)
705 memberList = useJSON_GetObjectMembers(jsonSchema, prefix)
707 if ( memberList == "" )
709 a = useJSON_GetDataValue(jsonData, prefix)
710 if ( a == "<<true>>" ) return "true"
711 if ( a == "<<false>>" ) return "false"
712 if ( a == "<<null>>" ) return "null"
713 if ( a == "<<novalue>>" ) return "" # <<novalue>> is a help for dealing with empty arrays and objects
715 # if it looks like a number, encode it as such. Cant tell a string from a number.
716 if (a "" ~ /^(\-?)(0|[123456789][0123456789]*)(\.[0123456789]+)?([eE][+-]?[0123456789]+)?$/)
719 return useJSON_EscapeString(a)
722 split(memberList, memberArr, SUBSEP)
723 arrCount = useJSON_ArrayCount( memberArr )
727 allLines = "[" (pretty == 0 ? "" : "\n")
729 for ( ii = 1 ; ii <= arrCount ; ii++ )
730 allLines = allLines useJSON_PrettyFormat(useJSON_FormatInt( jsonData, jsonSchema, prefix (prefix == "" ? "" : SUBSEP) ii, (pretty != 0 ? pretty+1 : 0)) (ii < arrCount ? "," : ""), pretty != 0 ? pretty+1 : 0)
731 allLines = allLines useJSON_PrettyFormat("]", pretty)
736 allLines = "{" (pretty == 0 ? "" : "\n")
744 utlJSON_qsortArray(memberArr, 1, arrCount)
746 for ( ii = 1 ; ii <= arrCount ; ii++ )
747 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)
749 allLines = allLines useJSON_PrettyFormat("}", pretty)
759 # ParseJSON : Parse JSON text into an awk array
761 # jsonString : JSON text
762 # jsonData : array of parsed JSON data
766 function ParseJSON(jsonString, jsonData, jsonStringArr, cnt)
768 # newlines split differently in some awks, replace them with formfeeds (also white space)
769 # if (split("1\n2\n3", jsonData, ",") == 3) # is this an awk that splits newlines differently?
770 gsub(/\n/, "\f", jsonString) # always replace literal newlines - allows compatibility when testing
772 split("", jsonData) # clear the array jsonData
773 cnt = split(jsonString, jsonStringArr, ",")
774 prsJSON_ParseJSONInt(jsonStringArr, cnt, 1, jsonData, "")
778 # FormatJSON : Format parsed JSON data back into JSON text
780 # jsonData : array of parsed JSON data
781 # pretty : 0 = compact format, non-zero = pretty format
783 # returns : string with JSON text
785 function FormatJSON(jsonData, pretty, jsonSchema)
787 useJSON_GetSchema(jsonData, jsonSchema)
788 return useJSON_FormatInt(jsonData, jsonSchema, "", pretty ? 1 : 0)
792 # JSONArrayLength : Find number of members in a JSON array
794 # jsonData : array of parsed JSON data
795 # prefix : array name
797 # returns : number of entries in the array
799 function JSONArrayLength(jsonData, prefix, a, cnt, tv)
805 if (prefix == "" || index(a, prefix) == 1)
807 tv = substr(a, prefix == "" ? 1 : (1+length(prefix)+1))
808 if ( index(tv, SUBSEP) )
809 tv = substr(tv, 1, index(tv, SUBSEP)-1)
820 # JSONUnescapeString : turn a JSON-escaped string into UTF-8
822 # jsonString : the escaped JSON string to convert
824 # returns : the string in UTF-8
826 function JSONUnescapeString(jsonString)
828 return prsJSON_UnescapeString(jsonString)
832 # JSONIsTrue : return non-zero if the value is the true value
834 # jsonValue : the value to test
836 # returns : true or false
838 function JSONIsTrue(jsonValue)
840 return jsonValue == "<<true>>";
844 # JSONIsFalse : return non-zero if the value is the false value
846 # jsonValue : the value to test
848 # returns : true or false
850 function JSONIsFalse(jsonValue)
852 return jsonValue == "<<false>>";
856 # JSONIsNull : return non-zero if the value is the null value
858 # jsonValue : the value to test
860 # returns : true or false
862 function JSONIsNull(jsonValue)
864 return jsonValue == "<<null>>";
868 # JSONObjectMembers : get the set of members of an object
870 # jsonData : array of parsed JSON data
871 # prefix : object name
872 # memberArr : [out] an array of the names of the object members, if the target was an object or an array
874 # returns : If the target was actually an array rather than an object, the number of elements in the array
875 # Else, zero if the target was an object or a value
877 function JSONObjectMembers(jsonData, prefix, memberArr, jsonSchema, memberList, rv, a)
879 useJSON_GetSchema(jsonData, jsonSchema)
880 memberList = useJSON_GetObjectMembers(jsonSchema, prefix)
882 if ( memberList == "" )
888 split(memberList, memberArr, SUBSEP)
889 rv = useJSON_ArrayCount( memberArr )
890 if ( rv == -1 ) # not an array, sort the object member names
896 utlJSON_qsortArray(memberArr, 1, rv)
901 # End of Copyright (c) 2010 Dan Saar
903 function debug(lvl, text) {
905 print text > "/dev/stderr"
908 function wpt_init() {
924 function dec2utf(dec) {
926 return sprintf("%c", dec)
927 else if (dec <= 0x07ff)
928 return sprintf("%c%c", 0xC0 + rshift(dec, 6), 0x80 + and(dec, 0x3F) )
931 function asc2xml(txt, o, ent, dec) {
933 while (match(txt, "&#x[0123456789abcdefABCDEF]*;"))
935 o = o substr(txt, 1, RSTART-1)
936 ent = toupper(substr(txt, RSTART+3, RLENGTH-4))
937 txt = substr(txt, RSTART+RLENGTH)
943 while (match(txt, "&#[0123456789]*;"))
945 o = o substr(txt, 1, RSTART-1)
946 ent = toupper(substr(txt, RSTART+2, RLENGTH-3))
947 txt = substr(txt, RSTART+RLENGTH)
955 function umlauts(text) {
959 # Somewhat minimal translation of HTML entities in titles
960 gsub("ä", "\xc3\xa4", text)
961 gsub("ä", "\xc3\xa4", text)
962 gsub("ö", "\xc3\xb6", text)
963 gsub("ö", "\xc3\xb6", text)
964 gsub("ü", "\xc3\xbc", text)
965 gsub("ü", "\xc3\xbc", text)
966 gsub("Ä", "\xc3\x84", text)
967 gsub("Ä", "\xc3\x84", text)
968 gsub("Ö", "\xc3\x96", text)
969 gsub("Ö", "\xc3\x96", text)
970 gsub("Ü", "\xc3\x9c", text)
971 gsub("Ü", "\xc3\x9c", text)
972 gsub("ß", "\xc3\x9f", text)
973 gsub("ß", "\xc3\x9f", text)
974 gsub("°", "\xc2\xb0", text)
975 gsub("°", "\xc2\xb0", text)
976 gsub("´", "\xc2\xb4", text)
977 gsub("´", "\xc2\xb4", text)
979 gsub("&", "\\&", text)
983 function htmlclean(text) {
984 gsub(" ", " ", text)
985 gsub("</?[pP][^>]*>", "\n", text)
986 gsub("<[bB][rR][^>]*>", "\n", text)
987 gsub("<[^>]*>", "", text)
988 # compress whitespace
989 gsub("\n\n\n*", "\n\n", text)
990 gsub("[ \t][ \t]*", " ", text)
994 function tableclean(text) {
996 gsub(" ", " ", text)
997 # translate/remove HTML tags
998 gsub("</?[pP][^>]*>", "\n", text)
999 gsub("</[bB][rR][^>]*>", "", text)
1000 gsub("</?font[^>]*>", "", text)
1001 gsub("</?table[^>]*>", "", text)
1002 gsub("<t[rdh]>", "", text)
1003 gsub("</tr>", "\n", text)
1004 gsub("</t[dh][^>]*>", " | ", text)
1005 gsub("<[^>]*>", "", text)
1006 # compress whitespace
1007 gsub("[ \t][ \t]*", " ", text)
1011 function remdiv(text, tag) {
1013 pat = ".*<div id=." tag ".[^>]*>[ \t\n]*"
1015 pat = ".*<div[^>]*>[ \t\n]*"
1017 while (text !~ "/?div")
1019 if (getline more <= 0)
1021 text = text "\n" more
1023 sub("[ \t\n]*</div>.*", "", text)
1024 debug(3, "Div:\n" text)
1028 function remspan(text, tag) {
1030 pat = ".*<span id=." tag ".[^>]*>[ \t\n]*"
1032 pat = ".*<span[^>]*>[ \t\n]*"
1034 while (text !~ "/?span")
1036 if (getline more <= 0)
1038 text = text "\n" more
1040 sub("[ \t\n]*</span>.*", "", text)
1041 debug(3, "Span:\n" text)
1045 function remspanlong(text, tag) {
1047 pat = ".*<span id=." tag ".[^>]*>[ \t\n]*"
1049 pat = ".*<span[^>]*>[ \t\n]*"
1053 debug(2, length(text) "\t" i " " j++ " " text)
1058 if (length(text) > 500000)
1060 debug(0, "Warning: logs exceeded 500,000 bytes!")
1063 # cleanup: remove </*span...>, adjust "span level"
1064 while (text ~ "</*span.*>")
1066 if (text ~ "</span>")
1068 --i; sub("</span>", "", text)
1070 if (text ~ "<span.*>")
1072 ++i; sub("<span[^>]*>", "", text)
1075 debug(2, "=" length(text) "\t" i " " j++ " " text)
1076 # if "span level" down to zero, closing tag reached
1079 if (getline more <= 0)
1081 text = text "\n" more
1082 debug(2, "+" length(more) "\t" i " " j++ " " more)
1084 debug(1, length(text) "\t" i " " j++)
1085 sub("[ \t\n]*</span>.*", "", text)
1086 gsub(" ", " ", text)
1087 if (tag == "CacheLogs")
1088 gsub("</?table[^>]*>", "", text)
1089 debug(3, "SpanLong:\n" text)
1093 function remwaypoints() {
1095 while (text !~ "</table>" && text !~ "No additional waypoints to display")
1097 if (getline more <= 0)
1099 text = text " " more
1101 gsub(" ", " ", text)
1102 gsub("\n[ \t]*", "", text)
1103 debug(3, "Waypoints:\n" text "\nEnd Waypoints")
1105 # will return complete table contents! split by </tr> instead of
1109 function splitwaypoints(waypoints,
1110 line, fld, prefix, lookup, wpname, x, y, lat, lon) {
1113 split(waypoints, wps, "</tr>")
1117 wp = 1 # skip header line
1121 # get URL from full table line
1123 gsub(".*href=.", "", url)
1124 gsub("\".*", "", url)
1125 if (url !~ "^http:")
1131 debug(1, "url: " url)
1133 # individual fields without leading/trailing blanks, remove HTML tags
1134 split(wps[wp], line, "</td>")
1139 debug(2, "Before Line[" fld "]: " line[fld])
1140 gsub("[ \t]*<[^>]*>", "", line[fld])
1141 gsub("^[ \t]*", "", line[fld])
1142 gsub("[ \t]*$", "", line[fld])
1143 debug(2, "after Line[" fld "]: " line[fld])
1145 # 8 fields: 1st line old style
1146 # 9 fields: 1st line new style
1147 # 4 fields, [1]~"Note:": 2nd line old style
1148 # 4 fields, [2]~"Note:": 2nd line new style
1152 # main information line, old style (pre-2010/07)
1153 if (!line[3]) continue
1154 prefix = substr(line[3] "00", 1, 2)
1157 lat = toupper(line[6])
1158 gsub(" *[EW].*", "", lat)
1160 lat = y[2] + y[3]/60.0
1163 lon = toupper(line[6])
1164 gsub("[NS] *[0-9]*.. *[0-9.]* ", "", lon)
1165 gsub("[^ 0-9.NESW-]", "", lon)
1167 lon = x[2] + x[3]/60.0
1170 text = text sprintf("\nlat=\"%.6f\" lon=\"%.6f\"|%s|%s|%s|%s",
1171 lat, lon, prefix, lookup, wpname, url)
1175 # main information line, new style (2010/07)
1176 if (!line[4]) continue
1177 prefix = substr(line[4] "00", 1, 2)
1180 lat = toupper(line[7])
1181 gsub(" *[EW].*", "", lat)
1183 lat = y[2] + y[3]/60.0
1186 lon = toupper(line[7])
1187 gsub("[NS] *[0-9]*.. *[0-9.]* ", "", lon)
1188 gsub("[^ 0-9.NESW-]", "", lon)
1190 lon = x[2] + x[3]/60.0
1193 text = text sprintf("\nlat=\"%.6f\" lon=\"%.6f\"|%s|%s|%s|%s",
1194 lat, lon, prefix, lookup, wpname, url)
1198 if (line[1] ~ "Note:")
1200 # continuation line, old style
1201 text = text sprintf("|%s", line[2])
1203 else if (line[2] ~ "Note:")
1205 # continuation line, new style
1206 text = text sprintf("|%s", line[3])
1210 debug(3, "Split WPs\n" text)
1214 function wpclean(waypoints, line, fld, prefix, lookup, wpname, coords) {
1215 # simplify Additional Waypoints table:
1216 # prefixedname - name<br>coordfield<br>note
1218 split(waypoints, wps, "</tr>")
1226 split(wps[wp], line, "</td>")
1231 gsub("[ \t]*<[^>]*>", "", line[fld])
1232 gsub("^[ \t]*", "", line[fld])
1233 gsub("[ \t]*$", "", line[fld])
1235 # 8 fields: 1st line old style
1236 # 9 fields: 1st line new style
1237 # 4 fields, [1]~"Note:": 2nd line old style
1238 # 4 fields, [2]~"Note:": 2nd line new style
1242 # main information line, old style (pre-2010/07)
1243 if (!line[3]) continue
1244 prefix = substr(line[3] "00", 1, 2) substr(gcid, 3)
1247 gsub(" \\(.*\\).*", "", wpname)
1248 coords = toupper(line[6])
1249 text = text sprintf("%s - %s<br />%s<br />", prefix, wpname, coords)
1253 # main information line, new style (2010/07)
1254 if (!line[4]) continue
1255 prefix = substr(line[4] "00", 1, 2) substr(gcid, 3)
1258 gsub(" \\(.*\\).*", "", wpname)
1259 coords = toupper(line[7])
1260 text = text sprintf("%s - %s<br />%s<br />", prefix, wpname, coords)
1264 if (line[1] ~ "Note:")
1266 # continuation line, old style
1267 text = text sprintf("%s<br />", line[2])
1269 else if (line[2] ~ "Note:")
1271 # continuation line, new style
1272 text = text sprintf("%s<br />", line[3])
1276 debug(3, "Clean WPs\n" text)
1280 function hex2dec(x, val) {
1281 for (val = 0; length(x); x = substr(x, 2))
1282 val = 16*val + index("0123456789ABCDEF", substr(x, 1, 1)) - 1
1286 # Convert GC0000 to 58913
1287 function wp2id(wp, val) {
1289 debug(5, "wp2id: " wp " ...")
1290 if ((length(wp) <= 4) && (wp < "G000"))
1294 debug(5, "wp2id hex: " val " ...")
1297 # new style, base-31, can have 4 or more places!
1298 set = "0123456789ABCDEFGHJKMNPQRTVWXYZ"
1300 for (pos = 1; pos <= length(wp); ++pos)
1303 val += index(set, substr(wp, pos, 1)) - 1
1306 debug(5, "wp2id id: " val " ...")
1310 # to decode hints: rot13 http://lorance.freeshell.org/rot13/
1311 function rot13 (string) {
1312 ROTFROM = "nopqrstuvwxyzabcdefghijklmNOPQRSTUVWXYZABCDEFGHIJKLM"
1313 ROTTO = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
1315 for (pos = 0; pos < length(string); pos++)
1317 char = substr(string,pos + 1,1)
1318 rotpos = index(ROTFROM,char)
1320 char = substr(ROTTO,rotpos,1)
1321 retstr = retstr char
1326 function tagstart(lvl, tag, parms) {
1327 printf "%*s", lvl*2, ""
1329 printf "<%s>\n", tag
1331 printf "<%s %s>\n", tag, parms
1334 function tagend(lvl, tag) {
1335 printf "%*s", lvl*2, ""
1336 printf "</%s>\n", tag
1340 gsub(/&/, "\\&", text)
1341 gsub(/</, "\\<", text)
1342 gsub(/>/, "\\>", text)
1346 function tagtext(lvl, tag, text) {
1348 printf "%*s", lvl*2, ""
1349 printf "<%s>%s</%s>\n", tag, text, tag
1352 function tagptext(lvl, tag, parms, text) {
1354 printf "%*s", lvl*2, ""
1355 printf "<%s %s>%s</%s>\n", tag, parms, text, tag
1358 function attr_begin1(gif, id, text) {
1359 debug(1, "attr_begin1: " gif " " id " \"" text "\"")
1360 attr_id[gif] = id; attr_text[gif] = text
1361 debug(1, "attr_id: " attr_id["slealth"])
1362 debug(1, "attr_id: " attr_id[gif])
1364 function attr_begin() {
1365 # attr_begin1("slealth", 40, "Stealth required") Dont work!!!
1366 attr_id["dog"] = 1; attr_text["dog"] = "Dogs"
1367 attr_id["dogs"] = 1; attr_text["dogs"] = "Dogs"
1368 attr_id["fee"] = 2; attr_text["fee"] = "Access or parking fee"
1369 attr_id["rappelling"] = 3; attr_text["rappelling"] = "Climbing gear"
1370 attr_id["boat"] = 4; attr_text["boat"] = "Boat"
1371 attr_id["scuba"] = 5; attr_text["scuba"] = "Scuba gear"
1372 attr_id["kids"] = 6; attr_text["kids"] = "Recommended for kids"
1373 attr_id["onehour"] = 7; attr_text["onehour"] = "Takes less than an hour"
1374 attr_id["scenic"] = 8; attr_text["scenic"] = "Scenic view"
1375 attr_id["hiking"] = 9; attr_text["hiking"] = "Significant hike"
1377 attr_id["climbing"] = 10; attr_text["climbing"] = "Difficult climbing"
1378 attr_id["wading"] = 11; attr_text["wading"] = "May require wading"
1379 attr_id["swimming"] = 12; attr_text["swimming"] = "May require swimming"
1380 attr_id["available"] = 13; attr_text["available"] = "Available at all times"
1381 attr_id["night"] = 14; attr_text["night"] = "Recommended at night"
1382 attr_id["winter"] = 15; attr_text["winter"] = "Available during winter"
1383 attr_id["poisonoak"] = 16; attr_text["poisonoak"] = "Poison plants"
1384 attr_id["dangerousanimals"] = 17; attr_text["dangerousanimals"] = "Dangerous Animals"
1385 attr_id["ticks"] = 18; attr_text["ticks"] = "Ticks"
1387 attr_id["mines"] = 19; attr_text["mines"] = "Abandoned mines"
1388 attr_id["cliff"] = 20; attr_text["cliff"] = "Cliff / falling rocks"
1389 attr_id["hunting"] = 21; attr_text["hunting"] = "Hunting"
1390 attr_id["danger"] = 22; attr_text["danger"] = "Dangerous area"
1391 attr_id["wheelchair"] = 23; attr_text["wheelchair"] ="Wheelchair accessible"
1392 attr_id["parking"] = 24; attr_text["parking"] = "Parking available"
1393 attr_id["public"] = 25; attr_text["public"] = "Public transportation"
1394 attr_id["water"] = 26; attr_text["water"] = "Drinking water nearby"
1395 attr_id["restrooms"] = 27; attr_text["restrooms"] ="Public restrooms nearby"
1396 attr_id["phone"] = 28; attr_text["phone"] = "Telephone nearby"
1398 attr_id["picnic"] = 29; attr_text["picnic"] = "Picnic tables nearby"
1399 attr_id["camping"] = 30; attr_text["camping"] = "Camping available"
1400 attr_id["bicycles"] = 31; attr_text["bicycles"] = "Bicycles"
1401 attr_id["motorcycles"] = 32; attr_text["motorcycles"] = "Motorcycles"
1402 attr_id["quads"] = 33; attr_text["quads"] = "Quads"
1403 attr_id["jeeps"] = 34; attr_text["jeeps"] = "Off-road vehicles"
1404 attr_id["snowmobiles"] = 35; attr_text["snowmobiles"] = "Snowmobiles"
1405 attr_id["horses"] = 36; attr_text["horses"] = "Horses"
1406 attr_id["campfires"] = 37; attr_text["campfires"] = "Campfires"
1407 attr_id["thorns"] = 38; attr_text["thorns"] = "Thorns"
1409 attr_id["stealth"] = 39; attr_text["stealth"] = "Stealth required"
1410 attr_id["stroller"] = 40; attr_text["stroller"] = "Stroller accessible"
1411 attr_id["firstaid"] = 41; attr_text["firstaid"] = "Needs maintenance"
1412 attr_id["cow"] = 42; attr_text["cow"] = "Watch for livestock"
1413 attr_id["flashlight"] = 43; attr_text["flashlight"] = "Flashlight required"
1414 attr_id["landf"] = 44; attr_text["landf"] = "Lost And Found Tour"
1415 attr_id["rv"] = 45; attr_text["rv"] = "Recreational Vehicle"
1416 attr_id["field"] = 46; attr_text["field"] = "Field Puzzle"
1417 attr_id["UV"] = 47; attr_text["UV"] = "UV Light Required"
1418 attr_id["snowshoes"] = 48; attr_text["snowshoes"] = "Snowshoes"
1420 attr_id["skiis"] = 49; attr_text["skiis"] = "Cross Country Skis"
1421 attr_id["s-tool"] = 50; attr_text["s-tool"] = "Special Tool Required"
1422 attr_id["nightcache"] = 51; attr_text["nightcache"] = "Night Cache"
1423 attr_id["parkngrab"] = 52; attr_text["parkngrab"] = "Park and Grab"
1424 attr_id["AbandonedBuilding"] = 53; attr_text["AbandonedBuilding"] = "Abandoned Structure"
1425 attr_id["hike_short"] = 54; attr_text["hike_short"] = "Short hike (less than 1km)"
1426 attr_id["hike_med"] = 55; attr_text["hike_med"] = "Medium hike (1km-10km)"
1427 attr_id["hike_long"] = 56; attr_text["hike_long"] = "Long hike (+10km)"
1428 attr_id["fuel"] = 57; attr_text["fuel"] = "Fuel Nearby"
1429 attr_id["food"] = 58; attr_text["food"] = "Food Nearby"
1431 attr_id["wirelessbeacon"] = 59; attr_text["wirelessbeacon"] = "Wireless Beacon"
1432 attr_id["partnership"] = 60; attr_text["partnership"] = "Partnership"
1433 attr_id["seasonal"] = 61; attr_text["seasonal"] = "Seasonal Access"
1434 attr_id["tourist"] = 62; attr_text["tourist"] = "Tourist Friendly"
1435 attr_id["treeclimbing"] = 63; attr_text["treeclimbing"] = "Tree Climbing"
1436 attr_id["frontyard"] = 64; attr_text["frontyard"] = "Front Yard (Private Residence)"
1437 attr_id["teamwork"] = 65; attr_text["teamwork"] = "Teamwork Required"
1440 function tagattr(lvl, kind, yesno) {
1442 #debug(1, "kind: \"" kind "\"")
1443 if (attr_id[kind] == 0)
1445 printf "%*s", lvl*2, ""
1446 printf "<groundspeak:attribute id=\"%d\" inc=\"%d\">", attr_id[kind], yesno
1447 printf "%s", attr_text[kind]
1448 printf "</groundspeak:attribute>\n"
1451 /cache_types.aspx/ { # gc 02/01/11
1453 sub(/.* alt=./, "", gs_type)
1454 sub(/. width=.*/, "", gs_type)
1455 sub(/. title=.*/, "", gs_type)
1456 debug(1, "gs_type: " gs_type)
1458 /<span id="ctl00_ContentBody_CacheName">/ {
1461 gs_name = remspan($0, "ctl00_ContentBody_CacheName")
1465 sub(/.* alt=./, "", gs_type)
1466 sub(/. width=.*/, "", gs_type)
1467 debug(1, "type: " gs_type)
1469 /<span id="CacheName">/ { gs_name = remspan($0, "CacheName") }
1470 /<span id="ctl00_ContentBody_CacheName">/ {
1471 gs_name = remspan($0, "ctl00_ContentBody_CacheName")
1473 /<span id=".*WaypointName".*>/ { gcid = remspan($0) }
1476 gcid = $0; sub(/.*wp=/, "", gcid); sub(/".*/, "", gcid)
1478 /<span id=".*ShortDescription">/ {
1479 gs_short_description = remspan($0)
1481 /<span id="LongDescription">/ {
1482 gs_long_description = remspanlong($0, "LongDescription")
1485 /<span id="ctl00_ContentBody_LongDescription">/ {
1486 gs_long_description = remspanlong($0, "ctl00_ContentBody_LongDescription")
1489 /<div id="div_hint"/ {
1491 gsub("\n", " ", hints)
1492 gsub("^ *", "", hints)
1493 gsub("<br>", "\n", hints)
1497 /<span id="Hints"/ {
1499 hints = htmlclean(hints)
1502 gsub("\n", " ", hints)
1504 /<span id="ctl00_ContentBody_Hints"/ {
1506 sub(".*displayMe.>", "", hints)
1507 sub("</span>.*", "", hints)
1508 gsub("<br>", "\n", hints)
1509 # debug(1, "Hints: " hints)
1513 /<b>Additional Waypoints/ {
1514 waypoints = remwaypoints()
1515 wplist = splitwaypoints(waypoints)
1517 /<strong>Additional Waypoints/ {
1518 waypoints = remwaypoints()
1519 wplist = splitwaypoints(waypoints)
1522 /ContentBody_WaypointsInfo/ {
1523 waypoints = remwaypoints()
1524 wplist = splitwaypoints(waypoints)
1526 /class="LogsTable Table"/ { # old
1529 /class="LogsTable"/ { # new 06/28/11
1532 (logs_section > 0) {
1535 (logs_section > 0) && /<table/ {
1538 (logs_section > 0) && /<\/table>/ {
1542 /<span id="CacheLogs">/ {
1543 logs = remspanlong($0, "CacheLogs")
1544 # remove header which does not exist >2010-01-12
1545 sub(".*td class=.containerHeader.>Cache Logs</td></tr>", "", logs)
1547 /<span id="ctl00_ContentBody_CacheLogs">/ {
1548 logs = remspanlong($0, "ctl00_ContentBody_CacheLogs")
1550 /<span id=".*CacheStats">/ { stats = remspan($0) }
1551 /<span id=".*NumVisits">/ {
1552 numvisits = remspan($0)
1556 /lnkPrintFriendly/ {
1560 # Printable page has ID number
1561 sub(/^.*ID=/, "", gid)
1566 # Non-printable page has guid number
1567 sub(/^.*guid=/, "", gid)
1571 # Add optional "A cache ". 08/21/2012
1572 /^ *(A cache )*by <a href/ {
1574 sub(/.*ds=2.>/, "", gs_owner)
1575 sub(/<.*/, "", gs_owner)
1576 debug(1, "owner: " gs_owner)
1578 sub(/.*guid=/, "", gs_guid)
1579 sub(/&.*/, "", gs_guid)
1581 # Fake gs_guid is user 03/01/2011
1584 sub(/.*: /, "", gs_guid)
1585 sub(/}.*/, "", gs_guid)
1586 debug(1, "guid " gs_guid)
1590 sub(/.*Size: /, "", gs_size); sub(". />.*", "", gs_size)
1592 /<span id="CacheOwner"/ {
1594 debug(1, "Owner text " text)
1595 gs_type = text; sub(/<.*/, "", gs_type)
1598 sub(/.*<br>by /, "", gs_owner); sub(/ [[].*/, "", gs_owner)
1600 sub(/<a[^>]*>/, "", gs_owner)
1601 sub(/<.a[^>]*>/, "", gs_owner)
1602 sub(/.*<br .>/, "", gs_owner)
1603 sub(/^by /, "", gs_owner)
1604 debug(1, "owner " gs_owner)
1605 gs_size = text; sub(/.*Size: /, "", gs_size); sub(/<.*/, "", gs_size)
1606 gs_guid = text; sub(/.*guid=/, "", gs_guid)
1607 sub(/&.*/, "", gs_guid)
1608 debug(1, "guid " gs_guid)
1610 /<span id="ctl00_ContentBody_CacheOwner"/ {
1612 debug(2, "Owner text: " text)
1614 sub(/<br .*/, "", gs_type)
1615 sub(/.*>/, "", gs_type)
1616 debug(1, "gs_type: " gs_type)
1619 sub(/.*ds=2.>/, "", gs_owner); sub(/<.*/, "", gs_owner)
1620 debug(1, "gs_owner: " gs_owner)
1622 gs_size = text; sub(/.*Size: /, "", gs_size); sub(/<.*/, "", gs_size)
1623 gs_guid = text; sub(/.*guid=/, "", gs_guid)
1624 sub(/&.*/, "", gs_guid)
1625 sub(/. title=.*/, "", gs_guid)
1626 debug(1, "guid: " gs_guid)
1628 /<span id="ErrorText"/ {
1629 if ($0 ~ "unavailable")
1631 if ($0 ~ "been archived")
1634 /<span id="ctl00_ContentBody_ErrorText"/ {
1635 errortext = remspan($0, "ctl00_ContentBody_ErrorText")
1636 if (errortext ~ "unavailable")
1638 if (errortext ~ "been archived")
1640 debug(1, "available: " available "; archived: " archived)
1642 /<span id="LargeMapPrint"/ {
1644 lat = text; sub(/.*latitude=/, "", lat); sub(/&.*/, "", lat)
1645 lon = text; sub(/.*longitude=/, "", lon); sub(/\".*/, "", lon)
1651 lat = $0; sub(/.*lat=/, "", lat); sub(/;.*/, "", lat)
1652 lon = $0; sub(/.*lng=/, "", lon); sub(/;.*/, "", lon)
1655 /<span id=".*Location"/ {
1658 sub(/In */, "", gs_state)
1659 sub(/,.*/, "", gs_state)
1662 sub(/.*, /, "", gs_country)
1663 sub(/ <.*/, "", gs_country)
1664 sub(/^In /, "", gs_country)
1666 /lat=.*; lng=.*; guid=/ {
1669 lat = $0; sub(/.*lat=/, "", lat); sub(/;.*/, "", lat)
1670 lon = $0; sub(/.*lng=/, "", lon); sub(/;.*/, "", lon)
1673 /<span class="minorCacheDetails">Hidden/ { # gc 2/1/11
1676 sub(/^ */, "", time)
1677 sub(/<.*/, "", time)
1678 split(time, fld, "/")
1679 time = sprintf("%d-%02d-%02d", fld[3], fld[1], fld[2])
1680 debug(1, "time: " time)
1682 /> <span class="minorCacheDetails">/ { # gc 6/28/11
1686 sub(/^ */, "", time)
1687 sub(/<.*/, "", time)
1688 gsub(/-/, "/", time)
1689 rc = split(time, fld, "/")
1691 rc = split(time, fld, "-")
1692 debug(1, "timerc: " rc)
1694 time = sprintf("%d-%02d-%02d", fld[3], fld[2], fld[1])
1695 else if (fld[1] >= 1000)
1696 time = sprintf("%d-%02d-%02d", fld[1], fld[2], fld[3])
1698 time = sprintf("%d-%02d-%02d", fld[3], fld[1], fld[2])
1699 debug(1, "time: " time)
1701 /<span id="DateHidden">/ {
1704 split(time, fld, "/")
1705 time = sprintf("%d-%02d-%02d", fld[3], fld[1], fld[2])
1707 /<span id="ctl00_ContentBody_DateHidden">/ {
1708 time = remspan($0, "ctl00_ContentBody_DateHidden")
1709 rc = split(time, fld, "/")
1712 time = sprintf("%d-%02d-%02d", fld[3], fld[1], fld[2])
1713 debug(1, "time: " time)
1716 rc = split(time, fld, ",")
1720 split(fld[2], fld, " ")
1721 mm = Month[ fld[1] ]
1723 time = sprintf("%d-%02d-%02d", yyyy, mm, dd)
1724 debug(1, "time: " time)
1729 /ctl00_ContentBody_uxLegendScale/ {
1731 sub(/.*alt=./, "", text); sub(/ .*/, "", text)
1733 debug(1 , "gs_diff: " gs_diff)
1735 /ctl00_ContentBody_Localize/ {
1737 sub(/.*alt=./, "", text); sub(/ .*/, "", text)
1739 debug(1 , "gs_terr: " gs_terr)
1741 /^ *Difficulty:<.strong>/ {
1743 sub(/.*alt=./, "", text); sub(/ .*/, "", text)
1745 debug(1 , "gs_diff: " gs_diff)
1747 /^ *Difficulty:/ { # gc 2/1/11
1751 sub(/.*alt=./, "", text); sub(/ .*/, "", text)
1753 debug(1 , "gs_diff: " gs_diff)
1755 /<span id="Difficulty">/ {
1757 sub(/.*alt=./, "", text); sub(/ .*/, "", text)
1760 /<span id="ctl00_ContentBody_Difficulty">/ {
1761 text = remspan($0, "ctl00_ContentBody_Difficulty")
1762 sub(/.*alt=./, "", text); sub(/ .*/, "", text)
1763 debug(1, "difficulty " text)
1766 /^ *Terrain:<.strong>/ {
1768 sub(/.*alt=./, "", text); sub(/ .*/, "", text)
1770 debug(1 , "gs_terr: " gs_terr)
1772 /^ *Terrain:/ { # gc 2/1/11
1776 sub(/.*alt=./, "", text); sub(/ .*/, "", text)
1778 debug(1 , "gs_terr: " gs_terr)
1780 /<span id="Terrain">/ {
1782 sub(/.*alt=./, "", text); sub(/ .*/, "", text)
1785 /<span id="ctl00_ContentBody_Terrain">/ {
1786 text = remspan($0, "ctl00_ContentBody_Terrain")
1787 sub(/.*alt=./, "", text); sub(/ .*/, "", text)
1788 debug(1, "terrain " text)
1791 /title=.What are Attributes?/ {
1793 debug(5, "Attr " text)
1794 gsub("<img src=./images/attributes/", "", text)
1796 gsub(/alt="[^"]*" width="30" height="30" .>/, "", text)
1798 gsub(/alt="[^"]*" title="[^"]*" width="30" height="30" .>/, "", text)
1799 gsub("<p class=.NoSpacing.*", "", text)
1800 gsub(/^ */, "", text)
1801 gsub(/\.gif../, "", text)
1802 gsub(/attribute-blank/, "", text)
1805 gsub(/[a-z0-9A-Z]*-no/, "", attrs_yes)
1806 gsub(/-yes/, "", attrs_yes)
1809 gsub(/[a-z0-9A-Z]*-yes/, "", attrs_no)
1810 gsub(/-no/, "", attrs_no)
1812 debug(1, "attrs_yes: " attrs_yes)
1813 debug(1, "attrs_no: " attrs_no)
1814 nattr_yes = split(attrs_yes, attr_yes, " ")
1815 nattr_no = split(attrs_no, attr_no, " ")
1816 debug(1, "nattr_yes: " nattr_yes)
1817 debug(1, "nattr_no: " nattr_no)
1819 /^{.status.:.success/ {
1820 ParseJSON($0, json_logs)
1825 Month["January"] = 1
1826 Month["February"] = 2
1833 Month["September"] = 9
1834 Month["October"] = 10
1835 Month["November"] = 11
1836 Month["December"] = 12
1837 BaseURL = "http://www.geocaching.com/seek/cache_details.aspx"
1845 if ((lat == "") || (lon == ""))
1847 debug(0, "Waypoint coordinates not found for " gcid ", no output!")
1851 # too long a block to be indented
1854 print "<?xml version=\"1.0\" encoding=\"utf-8\"?>"
1856 tagtext(1, "desc", "Geocache file generated by geo-html2gpx")
1857 tagtext(1, "author", "geo-html2gpx")
1858 "date +%Y-%m-%dT%H:%M:%S" | getline date
1859 tagtext(1, "time", date)
1863 gs_name = umlauts(gs_name)
1864 gs_owner = umlauts(gs_owner)
1866 tagstart(1, "wpt", "lat=\"" lat "\" lon=\"" lon "\"")
1868 tagtext(2, "time", time "T07:00:00Z")
1869 tagtext(2, "name", gcid)
1870 tagtext(2, "desc", gs_name " by " gs_owner ", " \
1871 gs_type " (" gs_diff "/" gs_terr ")")
1873 # alternate URL... tagtext(2, "url", BaseURL "?wp=" gcid)
1874 # alternate URL... tagtext(2, "url", BaseURL "?id=" gid)
1875 tagtext(2, "url", BaseURL "?wp=" gcid)
1876 tagtext(2, "urlname", gs_name)
1878 # we do this last... tagtext(2, "sym", sym)
1880 tagtext(2, "type", "Geocache|" gs_type)
1882 # FIXME? GC-written GPX files contain numeric, non-UUID,
1883 # cache/owner/finder ids
1884 # Oregon needs numeric cache id, or behaves erratically!
1886 tagstart(2, "groundspeak:cache",
1887 "id=\"" gid "\" available=\"" available \
1888 "\" archived=\"" archived "\"" \
1889 " xmlns:groundspeak=\"http://www.groundspeak.com/cache/1/0/1\"")
1890 tagtext(3, "groundspeak:name", gs_name)
1891 tagtext(3, "groundspeak:placed_by", gs_owner)
1892 tagptext(3,"groundspeak:owner", "id=\"" gs_guid "\"", gs_owner)
1893 tagtext(3, "groundspeak:type", gs_type)
1894 tagtext(3, "groundspeak:container", gs_size)
1896 if (nattr_yes != 0 || nattr_no != 0)
1898 tagstart(3, "groundspeak:attributes")
1899 for (i = 1; i <= nattr_yes; ++i)
1900 tagattr(4, attr_yes[i], 1)
1901 for (i = 1; i <= nattr_no; ++i)
1902 tagattr(4, attr_no[i], 0)
1903 tagend(3, "groundspeak:attributes")
1906 tagtext(3, "groundspeak:difficulty", gs_diff)
1907 tagtext(3, "groundspeak:terrain", gs_terr)
1908 tagtext(3, "groundspeak:country", gs_country)
1909 tagtext(3, "groundspeak:state", gs_state)
1912 tagptext(3, "groundspeak:short_description", "html=\"True\"",
1913 gs_short_description)
1914 if (!NOWPTS && waypoints)
1916 # reproduce "simplified table" by GC PQ
1917 # prefixed_gcid - wpname<br />original_style_coord<br />note<br />
1918 waypoints = wpclean(waypoints)
1919 # include "zero" waypoints here!
1920 gs_long_description = gs_long_description \
1921 "<p>Additional Waypoints</p>" waypoints
1923 tagptext(3, "groundspeak:long_description", "html=\"True\"",
1924 gs_long_description)
1928 gs_short_description = htmlclean(gs_short_description)
1929 tagptext(3, "groundspeak:short_description", "html=\"False\"",
1930 gs_short_description)
1931 gs_long_description = htmlclean(gs_long_description)
1933 gs_long_description = gs_long_description \
1934 "\n\nAdditional Waypoints\n" tableclean(waypoints)
1935 tagptext(3, "groundspeak:long_description", "html=\"False\"",
1936 gs_long_description)
1938 tagtext(3, "groundspeak:encoded_hints", hints)
1942 nlogs = JSONArrayLength(json_logs, "data")
1943 if (nlogs > NUMLOGS+1)
1945 debug(1, "New Logs: " nlogs)
1947 tagstart(3, "groundspeak:logs")
1949 tagstart(3, "groundspeak:logs", "/")
1951 for (i = 1; i < nlogs; ++i)
1953 ltype = json_logs["data" SUBSEP i SUBSEP "LogTypeImage"]
1954 if (ltype ~ /smile/) ltype = "Found it"
1955 else if (ltype ~ /happy/) ltype = "Found it"
1956 else if (ltype ~ /note/) ltype = "Write note"
1957 else if (ltype ~ /sad/) ltype = "Didn'"'"'t Find it"
1958 else if (ltype ~ /attended/) ltype = "Attended"
1959 else if (ltype ~ /rsvp/) ltype = "Will Attend"
1960 else if (ltype ~ /greenlight/) ltype = "Green"
1961 else if (ltype ~ /traffic_cone/) ltype = "Archive"
1962 else if (ltype ~ /disabled/) ltype = "Temporarily Disable Listing"
1963 else if (ltype ~ /coord_update/) ltype = "Update Coordinates"
1964 else ltype = "Unknown"
1966 ldate = json_logs["data" SUBSEP i SUBSEP "Visited"]
1967 n = split(ldate, fld, "/")
1970 #new format: 08/18/2011
1972 ldate = sprintf("%d-%02d-%02dT20:00:00Z",
1973 fld[3], fld[2], fld[1])
1975 ldate = sprintf("%d-%02d-%02dT20:00:00Z",
1976 fld[3], fld[1], fld[2])
1977 debug(2, "logdate: " ldate)
1979 lfinder = json_logs["data" SUBSEP i SUBSEP "UserName"]
1980 lfinder = umlauts(lfinder)
1981 logid = json_logs["data" SUBSEP i SUBSEP "LogID"]
1982 guid = json_logs["data" SUBSEP i SUBSEP "AccountID"]
1983 ltext = json_logs["data" SUBSEP i SUBSEP "LogText"]
1984 ltext = htmlclean(ltext)
1985 ltext = umlauts(ltext)
1987 if (lfinder == USERNAME && ltype == "Found it")
1988 sym = "Geocache Found"
1989 if (lfinder == USERNAME && ltype == "Attended")
1990 sym = "Geocache Found"
1991 tagstart(4, "groundspeak:log", "id=\"" logid "\"")
1992 tagtext(5, "groundspeak:date", ldate)
1993 tagtext(5, "groundspeak:type", ltype)
1994 tagptext(5, "groundspeak:finder", "id=\"" guid "\"", lfinder)
1995 tagptext(5, "groundspeak:text", "encoded=\"" "False" "\"", ltext)
1996 tagend(4, "groundspeak:log")
2000 tagend(3, "groundspeak:logs")
2004 # nlogs = split(logs, entry, "</tr>")
2005 nlogs = split(logs, entry, "</tr><tr>")
2006 if (nlogs > NUMLOGS+1)
2010 tagstart(3, "groundspeak:logs")
2012 tagstart(3, "groundspeak:logs", "/")
2014 for (i = 1; i < nlogs; ++i)
2016 sub("<tr><td[^>]*>", "", entry[i])
2017 sub("</td>", "", entry[i])
2018 if (!entry[i]) continue
2019 # old split location
2020 sub(/.*<[Ss][Tt][Rr][Oo][Nn][Gg]><img src=./, "", entry[i])
2023 #debug(1, "log: " ltype)
2024 sub(/>.*/, "", ltype) # leaves the URL of the smiley
2025 if (ltype ~ /smile/) ltype = "Found it"
2026 else if (ltype ~ /happy/) ltype = "Found it"
2027 else if (ltype ~ /note/) ltype = "Write note"
2028 else if (ltype ~ /sad/) ltype = "Didn'"'"'t Find it"
2029 else if (ltype ~ /attended/) ltype = "Attended"
2030 else if (ltype ~ /rsvp/) ltype = "Will Attend"
2031 else if (ltype ~ /greenlight/) ltype = "Green"
2032 else if (ltype ~ /traffic_cone/) ltype = "Archive"
2033 else if (ltype ~ /disabled/) ltype = "Temporarily Disable Listing"
2034 else if (ltype ~ /coord_update/) ltype = "Update Coordinates"
2035 else ltype = "Unknown"
2038 # split off /blank
2039 sub(/^[^>]*>[^ ;]*[ ;]/, "", ldate)
2040 sub(/ by <.*/, "", ldate)
2041 sub(/ by /, "", ldate)
2042 sub(/.*LogDate.>about /, "", ldate)
2043 sub(/.*LogDate.>/, "", ldate)
2044 sub(/<.*/, "", ldate)
2045 gsub(/-/, "/", ldate)
2046 debug(1, "logdate: " ldate)
2049 cmd = sprintf("%s -d \"12am %s\" +%%Y-%%m-%%dT07:00:00Z",
2051 cmd | getline ldate; close(cmd)
2055 n = split(ldate, fld, " ")
2058 #old format: August 18
2065 ldate = sprintf("%d-%02d-%02dT07:00:00", yy, mm, dd)
2067 n = split(ldate, fld, "/")
2070 #new format: 08/18/2011
2072 ldate = sprintf("%d-%02d-%02dT07:00:00",
2073 fld[3], fld[2], fld[1])
2075 ldate = sprintf("%d-%02d-%02dT07:00:00",
2076 fld[3], fld[1], fld[2])
2077 debug(1, "logdate: " ldate)
2082 sub(/[^<]*</, "", lfinder) # Delete all before <A NAME...
2085 sub(/[^"]*"/, "", logid)
2086 sub(/.* id="/, "", logid)
2087 sub(/.*LUID=/, "", logid)
2088 sub(/\".*/, "", logid)
2089 debug(1, "logid: " logid)
2092 debug(1, "guid: " guid)
2093 #sub(/[^>]*>/, "", guid) # Delete all before <A HREF...
2094 #sub(/>.*/, "", guid) # Delete all after <A HREF...
2095 sub(/.*guid=/, "", guid)
2096 sub(/\".*/, "", guid)
2097 sub(/\&.*/, "", guid)
2098 sub(/. id=.*/, "", guid)
2099 debug(1, "guid: " guid)
2101 #debug(1, "lfinder: " lfinder)
2102 sub(/[^>]*>/, "", lfinder) # Delete all before <A HREF...
2103 #debug(1, "lfinder: " lfinder)
2104 #sub(/[^>]*>/, "", lfinder) # Delete all before name
2105 sub(/<.*/, "", lfinder) # Delete all after name
2106 lfinder = umlauts(lfinder)
2107 debug(1, "lfinder: " lfinder)
2110 sub(/.*found\)<br .>/, "", ltext)
2111 sub("</font>.*", "", ltext)
2112 sub("<a href=.log.aspx[^>]*>[^<]*</a>", "", ltext)
2113 sub("<a href=.upload.aspx[^>]*>[^<]*</a>", "", ltext)
2114 # remove remaining HTML tags from log text. Seems to be a good
2115 # idea in any case, independent of NOHTML setting!
2116 ltext = htmlclean(ltext)
2117 ltext = umlauts(ltext)
2119 if (lfinder == USERNAME && ltype == "Found it")
2120 sym = "Geocache Found"
2121 if (lfinder == USERNAME && ltype == "Attended")
2122 sym = "Geocache Found"
2123 tagstart(4, "groundspeak:log", "id=\"" logid "\"")
2124 tagtext(5, "groundspeak:date", ldate)
2125 tagtext(5, "groundspeak:type", ltype)
2126 tagptext(5, "groundspeak:finder", "id=\"" guid "\"", lfinder)
2127 tagptext(5, "groundspeak:text", "encoded=\"" "False" "\"", ltext)
2128 tagend(4, "groundspeak:log")
2131 tagend(3, "groundspeak:logs")
2134 tagstart(3, "groundspeak:travelbugs", "/")
2136 tagend(2, "groundspeak:cache")
2137 tagtext(2, "sym", sym)
2140 # add Additional Waypoints in wpt form
2141 if (!NOWPTS && wplist)
2143 split(wplist, wps, "\n")
2151 # lat lon|prefix|lookup|wpname|url|note
2152 # i.e.: lat="44.888267" lon="-93.159233"|PC|PARK|http://...
2153 # |GCPMG6-Parking (Parking Area)|.31 miles from cache.
2154 debug(1, "wps: " wps[wp])
2155 split(wps[wp], line, "|")
2157 (!NOZERO || (line[1] !~ "lat=\"0.000000\" lon=\"0.000000\"") ) )
2159 # line format: coords|prefix|lookup|wpname|note
2160 tagstart(1, "wpt", line[1])
2161 #tagtext(2, "time", "...")
2162 tagtext(2, "name", line[2] substr(gcid,3))
2163 tagtext(2, "cmt", line[6] ? line[6] : "")
2165 gsub(" \\(.*\\).*", "", statname)
2168 sub(" \\(.*", "", desc)
2169 tagtext(2, "desc", desc)
2171 tagtext(2, "url", line[5])
2174 tagtext(2, "urlname", urlname)
2177 gsub(".*\\(", "", stattype)
2178 gsub("\\).*", "", stattype)
2179 tagtext(2, "sym", stattype)
2180 tagtext(2, "type", "Waypoint|" stattype)
2188 if (!INCR && !first)