source: git/src/commands.c

Last change on this file was 98b705d, checked in by Olly Betts <olly@…>, 2 months ago

cavern: Track column as well as file:line for pfx

This allows us to report columns diagnostics which get the position
from a station or survey name.

  • Property mode set to 100644
File size: 87.5 KB
Line 
1/* commands.c
2 * Code for directives
3 * Copyright (C) 1991-2025 Olly Betts
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program; if not, write to the Free Software
17 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
18 */
19
20#include <config.h>
21
22#include <limits.h>
23#include <stddef.h> /* for offsetof */
24#include <string.h>
25
26#include <proj.h>
27#if PROJ_VERSION_MAJOR < 8
28# define proj_context_errno_string(CTX, ERR) proj_errno_string(ERR)
29#endif
30
31#include "cavern.h"
32#include "commands.h"
33#include "datain.h"
34#include "date.h"
35#include "debug.h"
36#include "filename.h"
37#include "message.h"
38#include "netbits.h"
39#include "netskel.h"
40#include "osalloc.h"
41#include "out.h"
42#include "readval.h"
43#include "str.h"
44
45static void
46move_to_fixedlist(node *stn, int ignore_dirn)
47{
48    remove_stn_from_list(&stnlist, stn);
49    add_stn_to_list(&fixedlist, stn);
50    pos *p = stn->name->pos;
51    for (int d = 0; d < 3; d++) {
52        if (d == ignore_dirn) continue;
53        linkfor *leg = stn->leg[d];
54        if (!leg) break;
55        node *to = leg->l.to;
56        if (to->name->pos == p) {
57            move_to_fixedlist(to, reverse_leg_dirn(leg));
58        }
59    }
60}
61
62int fix_station(prefix *fix_name, const double* coords, long offset) {
63    bool new_stn = (fix_name->stn == NULL &&
64                    !TSTBIT(fix_name->sflags, SFLAGS_SOLVED));
65    fix_name->sflags |= BIT(SFLAGS_FIXED);
66    if (new_stn) fix_name->sflags |= BIT(SFLAGS_UNUSED_FIXED_POINT);
67    node *stn = StnFromPfx(fix_name);
68    if (fixed(stn)) {
69        if (coords[0] != POS(stn, 0) ||
70            coords[1] != POS(stn, 1) ||
71            coords[2] != POS(stn, 2)) {
72            return -1;
73        }
74        return 1;
75    }
76
77    POS(stn, 0) = coords[0];
78    POS(stn, 1) = coords[1];
79    POS(stn, 2) = coords[2];
80
81    if (new_stn) {
82        remove_stn_from_list(&stnlist, stn);
83        add_stn_to_list(&fixedlist, stn);
84    } else {
85        move_to_fixedlist(stn, -1);
86    }
87
88    // Make the station's file:line location reflect where it was fixed.
89    fix_name->filename = file.filename;
90    fix_name->line = file.line;
91    fix_name->column = offset >= 0 ? offset - file.lpos : 0;
92    return 0;
93}
94
95void fix_station_with_variance(prefix *fix_name, const double* coords,
96                               real var_x, real var_y, real var_z,
97#ifndef NO_COVARIANCES
98                               real cxy, real cyz, real czx
99#endif
100                              )
101{
102    bool new_stn = (fix_name->stn == NULL &&
103                    !TSTBIT(fix_name->sflags, SFLAGS_SOLVED));
104    if (new_stn) fix_name->sflags |= BIT(SFLAGS_UNUSED_FIXED_POINT);
105
106    node *stn = StnFromPfx(fix_name);
107    if (!fixed(stn)) {
108        node *fixpt = osnew(node);
109        prefix *name;
110        name = osnew(prefix);
111        name->pos = osnew(pos);
112        name->ident.p = NULL;
113        fixpt->name = name;
114        name->stn = fixpt;
115        name->up = NULL;
116        if (TSTBIT(pcs->infer, INFER_EXPORTS)) {
117            name->min_export = USHRT_MAX;
118        } else {
119            name->min_export = 0;
120        }
121        name->max_export = 0;
122        name->sflags = 0;
123        add_stn_to_list(&fixedlist, fixpt);
124        POS(fixpt, 0) = coords[0];
125        POS(fixpt, 1) = coords[1];
126        POS(fixpt, 2) = coords[2];
127        fixpt->leg[0] = fixpt->leg[1] = fixpt->leg[2] = NULL;
128        addfakeleg(fixpt, stn, 0, 0, 0,
129                   var_x, var_y, var_z
130#ifndef NO_COVARIANCES
131                   , cxy, cyz, czx
132#endif
133                  );
134    }
135}
136
137static void
138default_grade(settings *s)
139{
140   /* Values correspond to those in bcra5.svx */
141   s->Var[Q_POS] = (real)sqrd(0.05);
142   s->Var[Q_LENGTH] = (real)sqrd(0.05);
143   s->Var[Q_BACKLENGTH] = (real)sqrd(0.05);
144   s->Var[Q_COUNT] = (real)sqrd(0.05);
145   s->Var[Q_DX] = s->Var[Q_DY] = s->Var[Q_DZ] = (real)sqrd(0.05);
146   s->Var[Q_BEARING] = (real)sqrd(rad(0.5));
147   s->Var[Q_GRADIENT] = (real)sqrd(rad(0.5));
148   s->Var[Q_BACKBEARING] = (real)sqrd(rad(0.5));
149   s->Var[Q_BACKGRADIENT] = (real)sqrd(rad(0.5));
150   /* SD of plumbed legs (0.25 degrees?) */
151   s->Var[Q_PLUMB] = (real)sqrd(rad(0.25));
152   /* SD of level legs (0.25 degrees?) */
153   s->Var[Q_LEVEL] = (real)sqrd(rad(0.25));
154   s->Var[Q_DEPTH] = (real)sqrd(0.05);
155}
156
157static void
158default_truncate(settings *s)
159{
160   s->Truncate = INT_MAX;
161}
162
163static void
164default_case(settings *s)
165{
166   s->Case = LOWER;
167}
168
169static reading default_order[] = { Fr, To, Tape, Comp, Clino, End };
170
171static void
172default_style(settings *s)
173{
174   s->recorded_style = s->style = STYLE_NORMAL;
175   s->ordering = default_order;
176   s->dash_for_anon_wall_station = false;
177}
178
179static void
180default_prefix(settings *s)
181{
182   s->Prefix = root;
183}
184
185static void
186init_default_translate_map(short * t)
187{
188   int i;
189   for (i = '0'; i <= '9'; i++) t[i] |= SPECIAL_NAMES;
190   for (i = 'A'; i <= 'Z'; i++) t[i] |= SPECIAL_NAMES;
191   for (i = 'a'; i <= 'z'; i++) t[i] |= SPECIAL_NAMES;
192
193   t['\t'] |= SPECIAL_BLANK;
194   t[' '] |= SPECIAL_BLANK;
195   t[','] |= SPECIAL_BLANK;
196   t[';'] |= SPECIAL_COMMENT;
197   t['\032'] |= SPECIAL_EOL; /* Ctrl-Z, so olde DOS text files are handled ok */
198   t['\n'] |= SPECIAL_EOL;
199   t['\r'] |= SPECIAL_EOL;
200   t['*'] |= SPECIAL_KEYWORD;
201   t['-'] |= SPECIAL_OMIT;
202   t['\\'] |= SPECIAL_ROOT;
203   t['.'] |= SPECIAL_SEPARATOR;
204   t['_'] |= SPECIAL_NAMES;
205   t['-'] |= SPECIAL_NAMES; /* Added in 0.97 prerelease 4 */
206   t['.'] |= SPECIAL_DECIMAL;
207   t['-'] |= SPECIAL_MINUS;
208   t['+'] |= SPECIAL_PLUS;
209#if 0 /* FIXME */
210   t['{'] |= SPECIAL_OPEN;
211   t['}'] |= SPECIAL_CLOSE;
212#endif
213}
214
215static void
216default_translate(settings *s)
217{
218   if (s->next && s->next->Translate == s->Translate) {
219      /* We're currently using the same character translation map as our parent
220       * scope so allocate a new one before we modify it.
221       */
222      s->Translate = ((short*)osmalloc(sizeof(short) * 257)) + 1;
223   } else {
224/*  SVX_ASSERT(EOF==-1);*/ /* important, since we rely on this */
225   }
226   s->Translate[EOF] = SPECIAL_EOL;
227   memset(s->Translate, 0, sizeof(short) * 256);
228   init_default_translate_map(s->Translate);
229}
230
231/* Flag anything used in SPECIAL_* cumulatively to help us pick a suitable
232 * separator to use in the .3d file. */
233static short separator_map[256];
234
235void
236scan_compass_station_name(prefix *stn)
237{
238    /* We only need to scan the leaf station name - any survey hierarchy above
239     * that must have been set up in .svx files for which we update
240     * separator_map via cmd_set() plus adding the defaults in
241     * find_output_separator().
242     */
243    for (const char *p = prefix_ident(stn); *p; ++p) {
244        separator_map[(unsigned char)*p] |= SPECIAL_NAMES;
245    }
246}
247
248static char
249find_output_separator(void)
250{
251    // Fast path to handle most common cases where we'd pick '.'.
252    if ((separator_map['.'] & SPECIAL_NAMES) == 0) {
253        return '.';
254    }
255
256    static bool added_defaults = false;
257    if (!added_defaults) {
258        /* Add the default settings to separator_map. */
259        init_default_translate_map(separator_map);
260        added_defaults = true;
261    }
262
263    /* 30 punctuation characters plus space to try arranged in a sensible order
264     * of decreasing preference (these are all the ASCII punctuation characters
265     * excluding '_' and '-' since those are allowed in names by default so are
266     * poor choices for the separator).
267     */
268    int best = -1;
269    for (const char *p = "./:;,!|\\ ~+*^='`\"#$%&?@<>()[]{}"; *p; ++p) {
270        unsigned char candidate = *p;
271        int mask = separator_map[candidate];
272        switch (mask & (SPECIAL_SEPARATOR|SPECIAL_NAMES)) {
273            case SPECIAL_SEPARATOR:
274                /* A character which is set as a separator character at some
275                 * point but never set as a name character is perfect.
276                 */
277                return candidate;
278            case 0:
279                /* A character which is never set as either a separator
280                 * character or a name character is a reasonable option.
281                 */
282                if (best < 0) best = candidate;
283                break;
284        }
285    }
286    if (best < 0) {
287        /* Argh, no plausible choice!  Just return the default for now. */
288        return '.';
289    }
290    return best;
291}
292
293void
294default_units(settings *s)
295{
296   int quantity;
297   for (quantity = 0; quantity < Q_MAC; quantity++) {
298      if (TSTBIT(ANG_QMASK, quantity))
299         s->units[quantity] = (real)(M_PI / 180.0); /* degrees */
300      else
301         s->units[quantity] = (real)1.0; /* metres */
302   }
303   s->f_clino_percent = s->f_backclino_percent = false;
304   s->f_bearing_quadrants = s->f_backbearing_quadrants = false;
305}
306
307void
308default_calib(settings *s)
309{
310   int quantity;
311   for (quantity = 0; quantity < Q_MAC; quantity++) {
312      s->z[quantity] = (real)0.0;
313      s->sc[quantity] = (real)1.0;
314   }
315}
316
317static void
318default_flags(settings *s)
319{
320   s->flags = 0;
321}
322
323extern void
324default_all(settings *s)
325{
326   default_truncate(s);
327   s->infer = 0;
328   default_case(s);
329   default_style(s);
330   default_prefix(s);
331   default_translate(s);
332   default_grade(s);
333   default_units(s);
334   default_calib(s);
335   default_flags(s);
336}
337
338string token = S_INIT;
339
340string uctoken = S_INIT;
341
342extern void
343get_token_legacy(void)
344{
345   skipblanks();
346   get_token_legacy_no_blanks();
347}
348
349extern void
350get_token_legacy_no_blanks(void)
351{
352   s_clear(&token);
353   s_clear(&uctoken);
354   while (isalpha(ch)) {
355      s_appendch(&token, ch);
356      s_appendch(&uctoken, toupper(ch));
357      nextch();
358   }
359
360#if 0
361   printf("get_token_legacy_no_blanks() got “%s”\n", s_str(&token));
362#endif
363}
364
365void
366do_legacy_token_warning(void)
367{
368    if (!s_empty(&token)) {
369        if (!isBlank(ch) && !isComm(ch) && !isEol(ch)) {
370            compile_diagnostic(DIAG_WARN|DIAG_COL, /*No blank after token*/74);
371        }
372    }
373}
374
375extern void
376get_token(void)
377{
378    skipblanks();
379    get_token_no_blanks();
380}
381
382extern void
383get_token_no_blanks(void)
384{
385    s_clear(&token);
386    s_clear(&uctoken);
387    if (isalpha(ch)) {
388        do {
389            s_appendch(&token, ch);
390            s_appendch(&uctoken, toupper(ch));
391            nextch();
392        } while (isalnum(ch));
393    }
394}
395
396/* read word */
397void
398get_word(void)
399{
400   s_clear(&token);
401   skipblanks();
402   while (!isBlank(ch) && !isComm(ch) && !isEol(ch)) {
403      s_appendch(&token, ch);
404      nextch();
405   }
406#if 0
407   printf("get_word() got “%s”\n", s_str(&token));
408#endif
409}
410
411/* match_tok() now uses binary chop
412 * tab argument should be alphabetically sorted (ascending)
413 */
414extern int
415match_tok(const sztok *tab, int tab_size)
416{
417   int a = 0, b = tab_size - 1, c;
418   int r;
419   const char* tok = s_str(&uctoken);
420   SVX_ASSERT(tab_size > 0); /* catch empty table */
421/*  printf("[%d,%d]",a,b); */
422   while (a <= b) {
423      c = (unsigned)(a + b) / 2;
424/*     printf(" %d",c); */
425      r = strcmp(tab[c].sz, tok);
426      if (r == 0) return tab[c].tok; /* match */
427      if (r < 0)
428         a = c + 1;
429      else
430         b = c - 1;
431   }
432   return tab[tab_size].tok; /* no match */
433}
434
435typedef enum {
436   CMD_NULL = -1, CMD_ALIAS, CMD_BEGIN, CMD_CALIBRATE, CMD_CARTESIAN, CMD_CASE,
437   CMD_COPYRIGHT, CMD_CS, CMD_DATA, CMD_DATE, CMD_DECLINATION, CMD_DEFAULT,
438   CMD_END, CMD_ENTRANCE, CMD_EQUATE, CMD_EXPORT, CMD_FIX, CMD_FLAGS,
439   CMD_INCLUDE, CMD_INFER, CMD_INSTRUMENT, CMD_PREFIX, CMD_REF, CMD_REQUIRE,
440   CMD_SD, CMD_SET, CMD_SOLVE, CMD_TEAM, CMD_TITLE, CMD_TRUNCATE, CMD_UNITS
441} cmds;
442
443static const sztok cmd_tab[] = {
444     {"ALIAS",     CMD_ALIAS},
445     {"BEGIN",     CMD_BEGIN},
446     {"CALIBRATE", CMD_CALIBRATE},
447     {"CARTESIAN", CMD_CARTESIAN},
448     {"CASE",      CMD_CASE},
449     {"COPYRIGHT", CMD_COPYRIGHT},
450     {"CS",        CMD_CS},
451     {"DATA",      CMD_DATA},
452     {"DATE",      CMD_DATE},
453     {"DECLINATION", CMD_DECLINATION},
454#ifndef NO_DEPRECATED
455     {"DEFAULT",   CMD_DEFAULT},
456#endif
457     {"END",       CMD_END},
458     {"ENTRANCE",  CMD_ENTRANCE},
459     {"EQUATE",    CMD_EQUATE},
460     {"EXPORT",    CMD_EXPORT},
461     {"FIX",       CMD_FIX},
462     {"FLAGS",     CMD_FLAGS},
463     {"INCLUDE",   CMD_INCLUDE},
464     {"INFER",     CMD_INFER},
465     {"INSTRUMENT",CMD_INSTRUMENT},
466#ifndef NO_DEPRECATED
467     {"PREFIX",    CMD_PREFIX},
468#endif
469     {"REF",       CMD_REF},
470     {"REQUIRE",   CMD_REQUIRE},
471     {"SD",        CMD_SD},
472     {"SET",       CMD_SET},
473     {"SOLVE",     CMD_SOLVE},
474     {"TEAM",      CMD_TEAM},
475     {"TITLE",     CMD_TITLE},
476     {"TRUNCATE",  CMD_TRUNCATE},
477     {"UNITS",     CMD_UNITS},
478     {NULL,        CMD_NULL}
479};
480
481/* masks for units which are length and angles respectively */
482#define LEN_UMASK (BIT(UNITS_METRES) | BIT(UNITS_FEET) | BIT(UNITS_YARDS))
483#define ANG_UMASK (BIT(UNITS_DEGS) | BIT(UNITS_GRADS) | BIT(UNITS_MINUTES))
484
485/* ordering must be the same as the units enum */
486const real factor_tab[] = {
487   1.0, METRES_PER_FOOT, (METRES_PER_FOOT*3.0),
488   (M_PI/180.0), (M_PI/180.0), (M_PI/200.0), 0.01, (M_PI/180.0/60.0)
489};
490
491const int units_to_msgno[] = {
492    /*m*/424,
493    /*′*/428,
494    -1, /* yards */
495    /*°*/344, /* quadrants */
496    /*°*/344,
497    /*ᵍ*/345,
498    /*%*/96,
499    -1 /* minutes */
500};
501
502int get_length_units(int quantity) {
503    double factor = pcs->units[quantity];
504    if (fabs(factor - METRES_PER_FOOT) <= REAL_EPSILON ||
505        fabs(factor - METRES_PER_FOOT * 3.0) <= REAL_EPSILON) {
506        return UNITS_FEET;
507    }
508    return UNITS_METRES;
509}
510
511int get_angle_units(int quantity) {
512    double factor = pcs->units[quantity];
513    if (fabs(factor - M_PI / 200.0) <= REAL_EPSILON) {
514        return UNITS_GRADS;
515    }
516    return UNITS_DEGS;
517}
518
519static int
520get_units(unsigned long qmask, bool percent_ok)
521{
522   static const sztok utab[] = {
523        {"DEGREES",       UNITS_DEGS },
524        {"DEGS",          UNITS_DEGS },
525        {"FEET",          UNITS_FEET },
526        {"GRADS",         UNITS_GRADS },
527        {"METERS",        UNITS_METRES },
528        {"METRES",        UNITS_METRES },
529        {"METRIC",        UNITS_METRES },
530        {"MILS",          UNITS_DEPRECATED_ALIAS_FOR_GRADS },
531        {"MINUTES",       UNITS_MINUTES },
532        {"PERCENT",       UNITS_PERCENT },
533        {"PERCENTAGE",    UNITS_PERCENT },
534        {"QUADRANTS",     UNITS_QUADRANTS },
535        {"QUADS",         UNITS_QUADRANTS },
536        {"YARDS",         UNITS_YARDS },
537        {NULL,            UNITS_NULL }
538   };
539   get_token();
540   int units = match_tok(utab, TABSIZE(utab));
541   if (units == UNITS_NULL) {
542      compile_diagnostic(DIAG_ERR|DIAG_TOKEN|DIAG_SKIP, /*Unknown units “%s”*/35,
543                         s_str(&token));
544      return UNITS_NULL;
545   }
546   /* Survex has long misdefined "mils" as an alias for "grads", of which
547    * there are 400 in a circle.  There are several definitions of "mils"
548    * with a circle containing 2000π SI milliradians, 6400 NATO mils, 6000
549    * Warsaw Pact mils, and 6300 Swedish streck, and they aren't in common
550    * use by cave surveyors, so we now just warn if mils are used.
551    */
552   if (units == UNITS_DEPRECATED_ALIAS_FOR_GRADS) {
553      compile_diagnostic(DIAG_WARN|DIAG_TOKEN|DIAG_SKIP,
554                         /*Units “%s” are deprecated, assuming “grads” - see manual for details*/479,
555                         s_str(&token));
556      units = UNITS_GRADS;
557   }
558   if (units == UNITS_PERCENT && percent_ok &&
559       !(qmask & ~(BIT(Q_GRADIENT)|BIT(Q_BACKGRADIENT)))) {
560      return units;
561   }
562   if (units == UNITS_QUADRANTS &&
563       !(qmask & ~(BIT(Q_BEARING)|BIT(Q_BACKBEARING)))) {
564      return units;
565   }
566   if (((qmask & LEN_QMASK) && !TSTBIT(LEN_UMASK, units)) ||
567       ((qmask & ANG_QMASK) && !TSTBIT(ANG_UMASK, units))) {
568      /* TRANSLATORS: Note: In English you talk about the *units* of a single
569       * measurement, but the correct term in other languages may be singular.
570       */
571      compile_diagnostic(DIAG_ERR|DIAG_TOKEN|DIAG_SKIP, /*Invalid units “%s” for quantity*/37, s_str(&token));
572      return UNITS_NULL;
573   }
574   return units;
575}
576
577/* returns mask with bit x set to indicate quantity x specified */
578static unsigned long
579get_qlist(unsigned long mask_bad)
580{
581   static const sztok qtab[] = {
582        {"ALTITUDE",     Q_DZ },
583        {"BACKBEARING",  Q_BACKBEARING },
584        {"BACKCLINO",    Q_BACKGRADIENT },    /* alternative name */
585        {"BACKCOMPASS",  Q_BACKBEARING },     /* alternative name */
586        {"BACKGRADIENT", Q_BACKGRADIENT },
587        {"BACKLENGTH",   Q_BACKLENGTH },
588        {"BACKTAPE",     Q_BACKLENGTH },    /* alternative name */
589        {"BEARING",      Q_BEARING },
590        {"CEILING",      Q_UP },          /* alternative name */
591        {"CLINO",        Q_GRADIENT },    /* alternative name */
592        {"COMPASS",      Q_BEARING },     /* alternative name */
593        {"COUNT",        Q_COUNT },
594        {"COUNTER",      Q_COUNT },       /* alternative name */
595        {"DECLINATION",  Q_DECLINATION },
596        {"DEFAULT",      Q_DEFAULT }, /* not a real quantity... */
597        {"DEPTH",        Q_DEPTH },
598        {"DOWN",         Q_DOWN },
599        {"DX",           Q_DX },          /* alternative name */
600        {"DY",           Q_DY },          /* alternative name */
601        {"DZ",           Q_DZ },          /* alternative name */
602        {"EASTING",      Q_DX },
603        {"FLOOR",        Q_DOWN },        /* alternative name */
604        {"GRADIENT",     Q_GRADIENT },
605        {"LEFT",         Q_LEFT },
606        {"LENGTH",       Q_LENGTH },
607        {"LEVEL",        Q_LEVEL},
608        {"NORTHING",     Q_DY },
609        {"PLUMB",        Q_PLUMB},
610        {"POSITION",     Q_POS },
611        {"RIGHT",        Q_RIGHT },
612        {"TAPE",         Q_LENGTH },      /* alternative name */
613        {"UP",           Q_UP },
614        {NULL,           Q_NULL }
615   };
616   unsigned long qmask = 0;
617   int tok;
618   filepos fp;
619
620   while (1) {
621      get_pos(&fp);
622      get_token_legacy();
623      tok = match_tok(qtab, TABSIZE(qtab));
624      if (tok == Q_DEFAULT && !(mask_bad & BIT(Q_DEFAULT))) {
625          /* Only recognise DEFAULT if it is the first quantity, and then don't
626           * look for any more. */
627          if (qmask == 0)
628              return BIT(Q_DEFAULT);
629          break;
630      }
631      /* bail out if we reach the table end with no match */
632      if (tok == Q_NULL) break;
633      qmask |= BIT(tok);
634      if (qmask & mask_bad) {
635         compile_diagnostic(DIAG_ERR|DIAG_TOKEN|DIAG_SKIP, /*Unknown instrument “%s”*/39, s_str(&token));
636         return 0;
637      }
638   }
639
640   if (qmask == 0) {
641      /* TRANSLATORS: A "quantity" is something measured like "LENGTH",
642       * "BEARING", "ALTITUDE", etc. */
643      compile_diagnostic(DIAG_ERR|DIAG_TOKEN|DIAG_SKIP, /*Unknown quantity “%s”*/34, s_str(&token));
644   } else {
645      set_pos(&fp);
646   }
647
648   return qmask;
649}
650
651#define SPECIAL_UNKNOWN 0
652static void
653cmd_set(void)
654{
655   static const sztok chartab[] = {
656        {"BLANK",     SPECIAL_BLANK },
657/*FIXME {"CLOSE",     SPECIAL_CLOSE }, */
658        {"COMMENT",   SPECIAL_COMMENT },
659        {"DECIMAL",   SPECIAL_DECIMAL },
660        {"EOL",       SPECIAL_EOL }, /* EOL won't work well */
661        {"KEYWORD",   SPECIAL_KEYWORD },
662        {"MINUS",     SPECIAL_MINUS },
663        {"NAMES",     SPECIAL_NAMES },
664        {"OMIT",      SPECIAL_OMIT },
665/*FIXME {"OPEN",      SPECIAL_OPEN }, */
666        {"PLUS",      SPECIAL_PLUS },
667#ifndef NO_DEPRECATED
668        {"ROOT",      SPECIAL_ROOT },
669#endif
670        {"SEPARATOR", SPECIAL_SEPARATOR },
671        {NULL,        SPECIAL_UNKNOWN }
672   };
673   int i;
674
675   get_token();
676   int mask = match_tok(chartab, TABSIZE(chartab));
677
678   if (mask == SPECIAL_UNKNOWN) {
679      compile_diagnostic(DIAG_ERR|DIAG_TOKEN|DIAG_SKIP, /*Unknown character class “%s”*/42,
680                         s_str(&token));
681      return;
682   }
683
684#ifndef NO_DEPRECATED
685   if (mask == SPECIAL_ROOT) {
686      if (root_depr_count < 5) {
687         /* TRANSLATORS: Use of the ROOT character (which is "\" by default) is
688          * deprecated, so this error would be generated by:
689          *
690          * *equate \foo.7 1
691          *
692          * If you're unsure what "deprecated" means, see:
693          * https://en.wikipedia.org/wiki/Deprecation */
694         compile_diagnostic(DIAG_WARN|DIAG_TOKEN, /*ROOT is deprecated*/25);
695         if (++root_depr_count == 5)
696             /* TRANSLATORS: If you're unsure what "deprecated" means, see:
697              * https://en.wikipedia.org/wiki/Deprecation */
698            compile_diagnostic(DIAG_INFO, /*Further uses of this deprecated feature will not be reported*/95);
699      }
700   }
701#endif
702
703   /* if we're currently using an inherited translation table, allocate a new
704    * table, and copy old one into it */
705   if (pcs->next && pcs->next->Translate == pcs->Translate) {
706      short *p;
707      p = ((short*)osmalloc(sizeof(short) * 257)) + 1;
708      memcpy(p - 1, pcs->Translate - 1, sizeof(short) * 257);
709      pcs->Translate = p;
710   }
711
712   skipblanks();
713
714   /* clear this flag for all non-alphanums */
715   for (i = 0; i < 256; i++)
716      if (!isalnum(i)) pcs->Translate[i] &= ~mask;
717
718   /* now set this flag for all specified chars */
719   while (!isEol(ch)) {
720      int char_to_set;
721      if (!isalnum(ch)) {
722         char_to_set = ch;
723      } else if (tolower(ch) == 'x') {
724         int hex;
725         filepos fp;
726         get_pos(&fp);
727         nextch();
728         if (!isxdigit(ch)) {
729            set_pos(&fp);
730            break;
731         }
732         hex = isdigit(ch) ? ch - '0' : tolower(ch) - 'a';
733         nextch();
734         if (!isxdigit(ch)) {
735            set_pos(&fp);
736            break;
737         }
738         hex = hex << 4 | (isdigit(ch) ? ch - '0' : tolower(ch) - 'a');
739         char_to_set = hex;
740      } else {
741         break;
742      }
743      pcs->Translate[char_to_set] |= mask;
744      separator_map[char_to_set] |= mask;
745      nextch();
746   }
747
748   output_separator = find_output_separator();
749}
750
751void
752update_output_separator(void)
753{
754   output_separator = find_output_separator();
755}
756
757static void
758check_reentry(prefix *survey, const filepos* fpos_ptr)
759{
760   /* Don't try to check "*prefix \" or "*begin \" */
761   if (!survey->up) return;
762   if (TSTBIT(survey->sflags, SFLAGS_PREFIX_ENTERED)) {
763      static int reenter_depr_count = 0;
764      filepos fp_tmp;
765
766      if (reenter_depr_count >= 5)
767         return;
768
769      get_pos(&fp_tmp);
770      set_pos(fpos_ptr);
771      /* TRANSLATORS: The first of two warnings given when a survey which has
772       * already been completed is reentered.  This example file crawl.svx:
773       *
774       * *begin crawl     ; <- second warning here
775       * 1 2 9.45 234 -01
776       * *end crawl
777       * *begin crawl     ; <- first warning here
778       * 2 3 7.67 223 -03
779       * *end crawl
780       *
781       * Would lead to:
782       *
783       * crawl.svx:4:8: warning: Reentering an existing survey is deprecated
784       * crawl.svx:1: info: Originally entered here
785       *
786       * If you're unsure what "deprecated" means, see:
787       * https://en.wikipedia.org/wiki/Deprecation */
788      compile_diagnostic(DIAG_WARN|DIAG_WORD, /*Reentering an existing survey is deprecated*/29);
789      set_pos(&fp_tmp);
790      /* TRANSLATORS: The second of two warnings given when a survey which has
791       * already been completed is reentered.  This example file crawl.svx:
792       *
793       * *begin crawl     ; <- second warning here
794       * 1 2 9.45 234 -01
795       * *end crawl
796       * *begin crawl     ; <- first warning here
797       * 2 3 7.67 223 -03
798       * *end crawl
799       *
800       * Would lead to:
801       *
802       * crawl.svx:4:8: warning: Reentering an existing survey is deprecated
803       * crawl.svx:1: info: Originally entered here
804       *
805       * If you're unsure what "deprecated" means, see:
806       * https://en.wikipedia.org/wiki/Deprecation */
807      compile_diagnostic_pfx(DIAG_INFO, survey, /*Originally entered here*/30);
808      if (++reenter_depr_count == 5) {
809         /* After we've warned about 5 uses of the same deprecated feature, we
810          * give up for the rest of the current processing run.
811          *
812          * If you're unsure what "deprecated" means, see:
813          * https://en.wikipedia.org/wiki/Deprecation */
814         compile_diagnostic(DIAG_INFO, /*Further uses of this deprecated feature will not be reported*/95);
815      }
816   } else {
817      survey->sflags |= BIT(SFLAGS_PREFIX_ENTERED);
818      survey->filename = file.filename;
819      survey->line = file.line;
820      survey->column = fpos_ptr->offset - file.lpos;
821   }
822}
823
824#ifndef NO_DEPRECATED
825static void
826cmd_prefix(void)
827{
828   filepos fp;
829   get_pos(&fp);
830   prefix *survey = read_prefix(PFX_SURVEY|PFX_ALLOW_ROOT);
831   pcs->Prefix = survey;
832   check_reentry(survey, &fp);
833}
834#endif
835
836static void
837cmd_alias(void)
838{
839   /* Currently only two forms are supported:
840    * *alias station - ..
841    * *alias station -
842    */
843   get_token();
844   if (!S_EQ(&uctoken, "STATION")) {
845       compile_diagnostic(DIAG_ERR|DIAG_SKIP|DIAG_TOKEN, /*Bad *alias command*/397);
846       return;
847   }
848   get_word();
849   if (!S_EQ(&token, "-"))
850      goto bad_word;
851   get_word();
852   if (!s_empty(&token) && !S_EQ(&token, ".."))
853      goto bad_word;
854   pcs->dash_for_anon_wall_station = !s_empty(&token);
855   return;
856bad_word:
857   compile_diagnostic(DIAG_ERR|DIAG_SKIP|DIAG_TOKEN, /*Bad *alias command*/397);
858}
859
860static void
861cmd_begin(void)
862{
863   settings *pcsNew;
864
865   pcsNew = osnew(settings);
866   *pcsNew = *pcs; /* copy contents */
867   pcsNew->begin_lineno = file.line;
868   pcsNew->begin_lpos = file.lpos;
869   pcsNew->next = pcs;
870   pcs = pcsNew;
871
872   pcs->begin_survey = NULL;
873   pcs->begin_col = 0;
874   if (!isEol(ch) && !isComm(ch)) {
875      filepos fp;
876      prefix *survey;
877      get_pos(&fp);
878      int begin_col = fp.offset - file.lpos;
879      survey = read_prefix(PFX_SURVEY|PFX_ALLOW_ROOT|PFX_WARN_SEPARATOR);
880      // read_prefix() might fail and longjmp() so only set begin_col if
881      // it succeeds.
882      pcs->begin_col = begin_col;
883      pcs->begin_survey = survey;
884      pcs->Prefix = survey;
885      check_reentry(survey, &fp);
886      f_export_ok = true;
887   }
888}
889
890void
891invalidate_pj_cached(void)
892{
893    /* Invalidate the cached PJ. */
894    if (pj_cached) {
895        proj_destroy(pj_cached);
896        pj_cached = NULL;
897    }
898}
899
900void
901report_declination(settings *p)
902{
903    if (p->min_declination <= p->max_declination) {
904        int y, m, d;
905        char range[128];
906        const char* deg_sign = msg(/*°*/344);
907        ymd_from_days_since_1900(p->min_declination_days, &y, &m, &d);
908        snprintf(range, sizeof(range),
909                 "%.1f%s @ %04d-%02d-%02d",
910                 deg(p->min_declination), deg_sign, y, m, d);
911        if (p->min_declination_days != p->max_declination_days) {
912            size_t len = strlen(range);
913            ymd_from_days_since_1900(p->max_declination_days, &y, &m, &d);
914            snprintf(range + len, sizeof(range) - len,
915                     " / %.1f%s @ %04d-%02d-%02d",
916                     deg(p->max_declination), deg_sign, y, m, d);
917        }
918        /* TRANSLATORS: This message gives information about the range of
919         * declination values and the grid convergence value calculated for
920         * each "*declination auto ..." command.
921         *
922         * The first %s will be replaced by the declination range (or single
923         * value), and %.1f%s by the grid convergence angle.
924         */
925        compile_diagnostic_at(DIAG_INFO, p->dec_filename, p->dec_line,
926                              /*Declination: %s, grid convergence: %.1f%s*/484,
927                              range,
928                              deg(p->convergence), deg_sign);
929        PUTC(' ', STDERR);
930        fputs(p->dec_context, STDERR);
931        fputnl(STDERR);
932        if (p->next && p->dec_context != p->next->dec_context)
933            free(p->dec_context);
934        p->dec_context = NULL;
935        p->min_declination = HUGE_VAL;
936        p->max_declination = -HUGE_VAL;
937    }
938}
939
940void
941pop_settings(void)
942{
943    settings * p = pcs;
944    pcs = pcs->next;
945
946    SVX_ASSERT(pcs);
947
948    if (pcs->dec_lat != p->dec_lat ||
949        pcs->dec_lon != p->dec_lon ||
950        pcs->dec_alt != p->dec_alt) {
951        report_declination(p);
952    } else {
953        pcs->min_declination_days = p->min_declination_days;
954        pcs->max_declination_days = p->max_declination_days;
955        pcs->min_declination = p->min_declination;
956        pcs->max_declination = p->max_declination;
957        pcs->convergence = p->convergence;
958    }
959
960    if (p->proj_str != pcs->proj_str) {
961        if (!p->proj_str || !pcs->proj_str ||
962            strcmp(p->proj_str, pcs->proj_str) != 0) {
963            invalidate_pj_cached();
964        }
965        /* free proj_str if not used by parent */
966        free(p->proj_str);
967    }
968
969    /* don't free default ordering or ordering used by parent */
970    if (p->ordering != default_order && p->ordering != pcs->ordering)
971        free((reading*)p->ordering);
972
973    /* free Translate if not used by parent */
974    if (p->Translate != pcs->Translate)
975        free(p->Translate - 1);
976
977    /* free meta if not used by parent, or in this block */
978    if (p->meta && p->meta != pcs->meta && p->meta->ref_count == 0)
979        free(p->meta);
980
981    free(p);
982}
983
984static void
985cmd_end(void)
986{
987   filepos fp;
988
989   int begin_lineno = pcs->begin_lineno;
990   if (begin_lineno == 0) {
991      if (pcs->next == NULL) {
992         /* more ENDs than BEGINs */
993         /* TRANSLATORS: %s is replaced with e.g. BEGIN or .BOOK or #[ */
994         compile_diagnostic(DIAG_ERR|DIAG_SKIP, /*No matching %s*/192, "BEGIN");
995      } else {
996         /* TRANSLATORS: %s and %s are replaced with e.g. BEGIN and END
997          * or END and BEGIN or #[ and #] */
998         compile_diagnostic(DIAG_ERR|DIAG_SKIP,
999                            /*%s with no matching %s in this file*/23, "END", "BEGIN");
1000      }
1001      return;
1002   }
1003
1004   prefix *begin_survey = pcs->begin_survey;
1005   long begin_lpos = pcs->begin_lpos;
1006   int begin_col = pcs->begin_col;
1007
1008   pop_settings();
1009
1010   /* note need to read using root *before* BEGIN */
1011   prefix *survey = NULL;
1012   if (!isEol(ch) && !isComm(ch)) {
1013      get_pos(&fp);
1014      survey = read_prefix(PFX_SURVEY|PFX_ALLOW_ROOT);
1015   }
1016
1017   if (survey != begin_survey) {
1018      filepos fp_save;
1019      get_pos(&fp_save);
1020      if (survey) {
1021         set_pos(&fp);
1022         if (!begin_survey) {
1023            /* TRANSLATORS: Used when a BEGIN command has no survey, but the
1024             * END command does, e.g.:
1025             *
1026             * *begin
1027             * 1 2 10.00 178 -01
1028             * *end entrance      <--[Message given here] */
1029            compile_diagnostic(DIAG_ERR|DIAG_WORD, /*Matching BEGIN command has no survey name*/36);
1030         } else {
1031            /* TRANSLATORS: *BEGIN <survey> and *END <survey> should have the
1032             * same <survey> if it’s given at all */
1033            compile_diagnostic(DIAG_ERR|DIAG_WORD, /*Survey name doesn’t match BEGIN*/193);
1034         }
1035      } else {
1036         /* TRANSLATORS: Used when a BEGIN command has a survey name, but the
1037          * END command omits it, e.g.:
1038          *
1039          * *begin entrance
1040          * 1 2 10.00 178 -01
1041          * *end     <--[Message given here] */
1042         compile_diagnostic(DIAG_WARN|DIAG_COL, /*Survey name omitted from END*/194);
1043      }
1044      parse file_save = file;
1045      file.line = begin_lineno;
1046      file.lpos = begin_lpos;
1047      int word_flag = 0;
1048      if (begin_col) {
1049          word_flag = DIAG_WORD;
1050          fseek(file.fh, begin_lpos + begin_col - 1, SEEK_SET);
1051          nextch();
1052      }
1053      compile_diagnostic(DIAG_INFO|word_flag, /*Corresponding %s was here*/22, "BEGIN");
1054      file = file_save;
1055      set_pos(&fp_save);
1056   }
1057}
1058
1059static void
1060cmd_entrance(void)
1061{
1062   prefix *pfx = read_prefix(PFX_STATION);
1063   pfx->sflags |= BIT(SFLAGS_ENTRANCE);
1064   pfx->sflags &= ~BIT(SFLAGS_UNUSED_FIXED_POINT);
1065}
1066
1067static const prefix * first_fix_name = NULL;
1068static const char * first_fix_filename;
1069static unsigned first_fix_line;
1070
1071static void
1072cmd_fix(void)
1073{
1074   static prefix *name_omit_already = NULL;
1075   static const char * name_omit_already_filename = NULL;
1076   static unsigned int name_omit_already_line;
1077   PJ_COORD coord;
1078   filepos fp_stn, fp;
1079
1080   get_pos(&fp_stn);
1081   prefix *fix_name = read_prefix(PFX_STATION|PFX_ALLOW_ROOT);
1082
1083   get_pos(&fp);
1084   get_token_legacy();
1085   bool reference = S_EQ(&uctoken, "REFERENCE");
1086   if (reference) {
1087      do_legacy_token_warning();
1088   } else {
1089      if (!s_empty(&uctoken)) set_pos(&fp);
1090   }
1091
1092   skipblanks();
1093   get_pos(&fp);
1094
1095   // If `REFERENCE` is specified the coordinates can't be omitted.
1096   coord.v[0] = read_numeric(!reference);
1097   if (coord.v[0] == HUGE_REAL) {
1098      /* If the end of the line isn't blank, read a number after all to
1099       * get a more helpful error message */
1100      if (!isEol(ch) && !isComm(ch)) coord.v[0] = read_numeric(false);
1101   }
1102   if (coord.v[0] == HUGE_REAL) {
1103      if (pcs->proj_str || proj_str_out) {
1104         compile_diagnostic(DIAG_ERR|DIAG_COL|DIAG_SKIP, /*Coordinates can't be omitted when coordinate system has been specified*/439);
1105         return;
1106      }
1107
1108      if (fix_name == name_omit_already) {
1109         compile_diagnostic(DIAG_WARN|DIAG_COL, /*Same station fixed twice with no coordinates*/61);
1110         return;
1111      }
1112
1113      if (name_omit_already) {
1114         /* TRANSLATORS: Emitted after second and subsequent "FIX" command
1115          * with no coordinates.
1116          */
1117         compile_diagnostic_at(DIAG_ERR,
1118                               name_omit_already_filename,
1119                               name_omit_already_line,
1120                               /*Already had FIX command with no coordinates for station “%s”*/441,
1121                               sprint_prefix(name_omit_already));
1122      } else {
1123         /* TRANSLATORS: " *fix a " gives this message: */
1124         compile_diagnostic(DIAG_INFO|DIAG_COL, /*FIX command with no coordinates - fixing at (0,0,0)*/54);
1125
1126         name_omit_already = fix_name;
1127         name_omit_already_filename = file.filename;
1128         name_omit_already_line = file.line;
1129      }
1130
1131      coord.v[0] = coord.v[1] = coord.v[2] = (real)0.0;
1132   } else {
1133      coord.v[1] = read_numeric(false);
1134      coord.v[2] = read_numeric(false);
1135
1136      if (pcs->proj_str && proj_str_out) {
1137         PJ *transform = pj_cached;
1138         if (!transform) {
1139             transform = proj_create_crs_to_crs(PJ_DEFAULT_CTX,
1140                                                pcs->proj_str,
1141                                                proj_str_out,
1142                                                NULL);
1143             if (transform) {
1144                /* Normalise the output order so x is longitude and y latitude - by
1145                 * default new PROJ has them switched for EPSG:4326 which just seems
1146                 * confusing.
1147                 */
1148                PJ* pj_norm = proj_normalize_for_visualization(PJ_DEFAULT_CTX,
1149                                                               transform);
1150                proj_destroy(transform);
1151                transform = pj_norm;
1152             }
1153
1154             pj_cached = transform;
1155         }
1156
1157         if (proj_angular_input(transform, PJ_FWD)) {
1158            /* Input coordinate system expects radians. */
1159            coord.v[0] = rad(coord.v[0]);
1160            coord.v[1] = rad(coord.v[1]);
1161         }
1162
1163         coord.v[3] = HUGE_VAL;
1164         coord = proj_trans(transform, PJ_FWD, coord);
1165
1166         if (coord.v[0] == HUGE_VAL ||
1167             coord.v[1] == HUGE_VAL ||
1168             coord.v[2] == HUGE_VAL) {
1169            compile_diagnostic(DIAG_ERR|DIAG_FROM(fp),
1170                               /*Failed to convert coordinates: %s*/436,
1171                               proj_context_errno_string(PJ_DEFAULT_CTX,
1172                                                         proj_errno(transform)));
1173            /* Set dummy values which are finite. */
1174            coord.v[0] = coord.v[1] = coord.v[2] = 0;
1175         }
1176      } else if (pcs->proj_str) {
1177         compile_diagnostic(DIAG_ERR, /*The input projection is set but the output projection isn't*/437);
1178      } else if (proj_str_out) {
1179         compile_diagnostic(DIAG_ERR, /*The output projection is set but the input projection isn't*/438);
1180      }
1181
1182      get_pos(&fp);
1183      real sdx = read_numeric(true);
1184      if (sdx <= 0) {
1185          set_pos(&fp);
1186          compile_diagnostic(DIAG_ERR|DIAG_SKIP|DIAG_NUM, /*Standard deviation must be positive*/48);
1187          return;
1188      }
1189      if (sdx != HUGE_REAL) {
1190         real sdy, sdz;
1191         real cxy = 0, cyz = 0, czx = 0;
1192         get_pos(&fp);
1193         sdy = read_numeric(true);
1194         if (sdy == HUGE_REAL) {
1195            /* only one variance given */
1196            sdy = sdz = sdx;
1197         } else {
1198            if (sdy <= 0) {
1199               set_pos(&fp);
1200               compile_diagnostic(DIAG_ERR|DIAG_SKIP|DIAG_NUM, /*Standard deviation must be positive*/48);
1201               return;
1202            }
1203            get_pos(&fp);
1204            sdz = read_numeric(true);
1205            if (sdz == HUGE_REAL) {
1206               /* two variances given - horizontal & vertical */
1207               sdz = sdy;
1208               sdy = sdx;
1209            } else {
1210               if (sdz <= 0) {
1211                  set_pos(&fp);
1212                  compile_diagnostic(DIAG_ERR|DIAG_SKIP|DIAG_NUM, /*Standard deviation must be positive*/48);
1213                  return;
1214               }
1215               cxy = read_numeric(true);
1216               if (cxy != HUGE_REAL) {
1217                  /* covariances given */
1218                  cyz = read_numeric(false);
1219                  czx = read_numeric(false);
1220               } else {
1221                  cxy = 0;
1222               }
1223            }
1224         }
1225         fix_station_with_variance(fix_name, coord.v,
1226                                   sdx * sdx, sdy * sdy, sdz * sdz
1227#ifndef NO_COVARIANCES
1228                                   , cxy, cyz, czx
1229#endif
1230                                  );
1231
1232         if (reference) {
1233             // `*fix reference` so suppress "unused fixed point" warning.
1234             fix_name->sflags &= ~BIT(SFLAGS_UNUSED_FIXED_POINT);
1235         }
1236         if (!first_fix_name) {
1237            /* We track if we've fixed a station yet, and if so what the name
1238             * of the first fix was, so that we can issue an error if the
1239             * output coordinate system is set after fixing a station. */
1240            first_fix_name = fix_name;
1241            first_fix_filename = file.filename;
1242            first_fix_line = file.line;
1243         }
1244
1245         return;
1246      }
1247   }
1248
1249   if (!first_fix_name) {
1250      /* We track if we've fixed a station yet, and if so what the name of the
1251       * first fix was, so that we can issue an error if the output coordinate
1252       * system is set after fixing a station. */
1253      first_fix_name = fix_name;
1254      first_fix_filename = file.filename;
1255      first_fix_line = file.line;
1256   }
1257
1258   int fix_result = fix_station(fix_name, coord.v, fp_stn.offset);
1259   if (reference) {
1260       // `*fix reference` so suppress "unused fixed point" warning.
1261       fix_name->sflags &= ~BIT(SFLAGS_UNUSED_FIXED_POINT);
1262   }
1263   if (fix_result == 0) {
1264      return;
1265   }
1266
1267   get_pos(&fp);
1268   set_pos(&fp_stn);
1269   if (fix_result < 0) {
1270       compile_diagnostic(DIAG_ERR|DIAG_WORD, /*Station already fixed or equated to a fixed point*/46);
1271   } else {
1272       /* TRANSLATORS: *fix a 1 2 3 / *fix a 1 2 3 */
1273       compile_diagnostic(DIAG_WARN|DIAG_WORD, /*Station already fixed at the same coordinates*/55);
1274   }
1275   compile_diagnostic_pfx(DIAG_INFO, fix_name, /*Previously fixed or equated here*/493);
1276   set_pos(&fp);
1277}
1278
1279static void
1280cmd_flags(void)
1281{
1282   static const sztok flagtab[] = {
1283        {"DUPLICATE", FLAGS_DUPLICATE },
1284        {"NOT",       FLAGS_NOT },
1285        {"SPLAY",     FLAGS_SPLAY },
1286        {"SURFACE",   FLAGS_SURFACE },
1287        {NULL,        FLAGS_UNKNOWN }
1288   };
1289   bool fNot = false;
1290   bool fEmpty = true;
1291   while (1) {
1292      int flag;
1293      get_token();
1294      /* If token is empty, it could mean end of line, or maybe
1295       * some non-alphanumeric junk which is better reported later */
1296      if (s_empty(&token)) break;
1297
1298      fEmpty = false;
1299      flag = match_tok(flagtab, TABSIZE(flagtab));
1300      /* treat the second NOT in "NOT NOT" as an unknown flag */
1301      if (flag == FLAGS_UNKNOWN || (fNot && flag == FLAGS_NOT)) {
1302         compile_diagnostic(DIAG_ERR|DIAG_TOKEN, /*FLAG “%s” unknown*/68,
1303                            s_str(&token));
1304         /* Recover from “*FLAGS NOT BOGUS SURFACE” by ignoring "NOT BOGUS" */
1305         fNot = false;
1306      } else if (flag == FLAGS_NOT) {
1307         fNot = true;
1308      } else if (fNot) {
1309         pcs->flags &= ~BIT(flag);
1310         fNot = false;
1311      } else {
1312         pcs->flags |= BIT(flag);
1313      }
1314   }
1315
1316   if (fNot) {
1317      compile_diagnostic(DIAG_ERR|DIAG_TOKEN,
1318                         /*Expecting “%s”, “%s”, or “%s”*/188,
1319                         "DUPLICATE", "SPLAY", "SURFACE");
1320   } else if (fEmpty) {
1321      compile_diagnostic(DIAG_ERR|DIAG_TOKEN,
1322                         /*Expecting “%s”, “%s”, “%s”, or “%s”*/189,
1323                         "NOT", "DUPLICATE", "SPLAY", "SURFACE");
1324   }
1325}
1326
1327static void
1328cmd_equate(void)
1329{
1330   filepos fp;
1331   get_pos(&fp);
1332   prefix *prev_name = NULL;
1333   prefix *name = read_prefix(PFX_STATION|PFX_ALLOW_ROOT|PFX_SUSPECT_TYPO);
1334   while (true) {
1335      if (!name->stn || !fixed(name->stn)) {
1336          // If the station isn't already fixed, make its file:line location
1337          // reflect this *equate.
1338          name->filename = file.filename;
1339          name->line = file.line;
1340          name->column = fp.offset - file.lpos;
1341      }
1342      skipblanks();
1343      if (isEol(ch) || isComm(ch)) {
1344         if (prev_name == NULL) {
1345            /* E.g. *equate entrance.6 */
1346            set_pos(&fp);
1347            /* TRANSLATORS: EQUATE is a command name, so shouldn’t be
1348             * translated.
1349             *
1350             * Here "station" is a survey station, not a train station.
1351             */
1352            compile_diagnostic(DIAG_ERR|DIAG_SKIP|DIAG_WORD, /*Only one station in EQUATE command*/33);
1353         }
1354         return;
1355      }
1356
1357      prev_name = name;
1358      name = read_prefix(PFX_STATION|PFX_ALLOW_ROOT|PFX_SUSPECT_TYPO);
1359      process_equate(prev_name, name);
1360   }
1361}
1362
1363static void
1364report_missing_export(prefix *pfx, int depth)
1365{
1366   char *s;
1367   const char *p;
1368   prefix *survey = pfx;
1369   int i;
1370   for (i = depth + 1; i; i--) {
1371      survey = survey->up;
1372      SVX_ASSERT(survey);
1373   }
1374   s = osstrdup(sprint_prefix(survey));
1375   p = sprint_prefix(pfx);
1376   if (survey->filename) {
1377      /* TRANSLATORS: A station must be exported out of each level it is in, so
1378       * this would give "Station “\outer.inner.1” not exported from survey
1379       * “\outer”)":
1380       *
1381       * *equate entrance outer.inner.1
1382       * *begin outer
1383       * *begin inner
1384       * *export 1
1385       * 1 2 1.23 045 -6
1386       * *end inner
1387       * *end outer
1388       *
1389       * Here "survey" is a "cave map" rather than list of questions - it should be
1390       * translated to the terminology that cavers using the language would use.
1391       */
1392      compile_diagnostic_pfx(DIAG_ERR, survey,
1393                             /*Station “%s” not exported from survey “%s”*/26, p, s);
1394   } else {
1395      compile_diagnostic(DIAG_ERR, /*Station “%s” not exported from survey “%s”*/26, p, s);
1396   }
1397   free(s);
1398}
1399
1400static void
1401cmd_export(void)
1402{
1403   prefix *pfx;
1404
1405   fExportUsed = true;
1406   do {
1407      int depth = 0;
1408      pfx = read_prefix(PFX_STATION);
1409      {
1410         prefix *p = pfx;
1411         while (p != NULL && p != pcs->Prefix) {
1412            depth++;
1413            p = p->up;
1414         }
1415         /* Something like: *export \foo, but we've excluded use of root */
1416         SVX_ASSERT(p);
1417      }
1418      /* *export \ or similar bogus stuff */
1419      SVX_ASSERT(depth);
1420#if 0
1421      printf("C min %d max %d depth %d pfx %s\n",
1422             pfx->min_export, pfx->max_export, depth, sprint_prefix(pfx));
1423#endif
1424      if (pfx->min_export == 0) {
1425         /* not encountered *export for this name before */
1426         if (pfx->max_export > depth) report_missing_export(pfx, depth);
1427         pfx->min_export = pfx->max_export = depth;
1428      } else if (pfx->min_export != USHRT_MAX) {
1429         /* FIXME: what to do if a station is marked for inferred exports
1430          * but is then explicitly exported?  Currently we just ignore the
1431          * explicit export... */
1432         if (pfx->min_export - 1 > depth) {
1433            report_missing_export(pfx, depth);
1434         } else if (pfx->min_export - 1 < depth) {
1435            /* TRANSLATORS: Here "station" is a survey station, not a train station.
1436             *
1437             * Exporting a station twice gives this error:
1438             *
1439             * *begin example
1440             * *export 1
1441             * *export 1
1442             * 1 2 1.24 045 -6
1443             * *end example */
1444            compile_diagnostic(DIAG_ERR, /*Station “%s” already exported*/66,
1445                               sprint_prefix(pfx));
1446         }
1447         pfx->min_export = depth;
1448      }
1449      skipblanks();
1450   } while (!isEol(ch) && !isComm(ch));
1451}
1452
1453static void
1454cmd_data(void)
1455{
1456   static const sztok dtab[] = {
1457        {"ALTITUDE",     Dz },
1458        {"BACKBEARING",  BackComp },
1459        {"BACKCLINO",    BackClino }, /* alternative name */
1460        {"BACKCOMPASS",  BackComp }, /* alternative name */
1461        {"BACKGRADIENT", BackClino },
1462        {"BACKLENGTH",   BackTape },
1463        {"BACKTAPE",     BackTape }, /* alternative name */
1464        {"BEARING",      Comp },
1465        {"CEILING",      Up }, /* alternative name */
1466        {"CLINO",        Clino }, /* alternative name */
1467        {"COMPASS",      Comp }, /* alternative name */
1468        {"COUNT",        Count }, /* FrCount&ToCount in multiline */
1469        {"DEPTH",        Depth }, /* FrDepth&ToDepth in multiline */
1470        {"DEPTHCHANGE",  DepthChange },
1471        {"DIRECTION",    Dir },
1472        {"DOWN",         Down },
1473        {"DX",           Dx },
1474        {"DY",           Dy },
1475        {"DZ",           Dz },
1476        {"EASTING",      Dx },
1477        {"FLOOR",        Down }, /* alternative name */
1478        {"FROM",         Fr },
1479        {"FROMCOUNT",    FrCount },
1480        {"FROMDEPTH",    FrDepth },
1481        {"GRADIENT",     Clino },
1482        {"IGNORE",       Ignore },
1483        {"IGNOREALL",    IgnoreAll },
1484        {"LEFT",         Left },
1485        {"LENGTH",       Tape },
1486        {"NEWLINE",      Newline },
1487        {"NORTHING",     Dy },
1488        {"RIGHT",        Right },
1489        {"STATION",      Station }, /* Fr&To in multiline */
1490        {"TAPE",         Tape }, /* alternative name */
1491        {"TO",           To },
1492        {"TOCOUNT",      ToCount },
1493        {"TODEPTH",      ToDepth },
1494        {"UP",           Up },
1495        {NULL,           End }
1496   };
1497
1498#define MASK_stns BIT(Fr) | BIT(To) | BIT(Station)
1499#define MASK_tape BIT(Tape) | BIT(BackTape) | BIT(FrCount) | BIT(ToCount) | BIT(Count)
1500#define MASK_dpth BIT(FrDepth) | BIT(ToDepth) | BIT(Depth) | BIT(DepthChange)
1501#define MASK_comp BIT(Comp) | BIT(BackComp)
1502#define MASK_clin BIT(Clino) | BIT(BackClino)
1503
1504#define MASK_NORMAL MASK_stns | BIT(Dir) | MASK_tape | MASK_comp | MASK_clin
1505#define MASK_DIVING MASK_NORMAL | MASK_dpth
1506#define MASK_CARTESIAN MASK_stns | BIT(Dx) | BIT(Dy) | BIT(Dz)
1507#define MASK_CYLPOLAR  MASK_stns | BIT(Dir) | MASK_tape | MASK_comp | MASK_dpth
1508#define MASK_NOSURVEY MASK_stns
1509#define MASK_PASSAGE BIT(Station) | BIT(Left) | BIT(Right) | BIT(Up) | BIT(Down)
1510#define MASK_IGNORE 0 // No readings in this style.
1511
1512   // readings which may be given for each style (index is STYLE_*)
1513   static const unsigned long mask[] = {
1514      MASK_NORMAL, MASK_DIVING, MASK_CARTESIAN, MASK_CYLPOLAR, MASK_NOSURVEY,
1515      MASK_PASSAGE, MASK_IGNORE
1516   };
1517
1518   // readings which may be omitted for each style (index is STYLE_*)
1519   static const unsigned long mask_optional[] = {
1520      BIT(Dir) | BIT(Clino) | BIT(BackClino),
1521      BIT(Dir) | BIT(Clino) | BIT(BackClino),
1522      0,
1523      BIT(Dir),
1524      0,
1525      0, /* BIT(Left) | BIT(Right) | BIT(Up) | BIT(Down), */
1526      0
1527   };
1528
1529   /* all valid readings */
1530   static const unsigned long mask_all[] = {
1531      MASK_NORMAL | BIT(Newline) | BIT(Ignore) | BIT(IgnoreAll) | BIT(End),
1532      MASK_DIVING | BIT(Newline) | BIT(Ignore) | BIT(IgnoreAll) | BIT(End),
1533      MASK_CARTESIAN | BIT(Newline) | BIT(Ignore) | BIT(IgnoreAll) | BIT(End),
1534      MASK_CYLPOLAR | BIT(Newline) | BIT(Ignore) | BIT(IgnoreAll) | BIT(End),
1535      MASK_NOSURVEY | BIT(Ignore) | BIT(IgnoreAll) | BIT(End),
1536      MASK_PASSAGE | BIT(Ignore) | BIT(IgnoreAll) | BIT(End),
1537      MASK_IGNORE
1538   };
1539#define STYLE_DEFAULT   -2
1540#define STYLE_UNKNOWN   -1
1541
1542   static const sztok styletab[] = {
1543        {"CARTESIAN",    STYLE_CARTESIAN },
1544        {"CYLPOLAR",     STYLE_CYLPOLAR },
1545        {"DEFAULT",      STYLE_DEFAULT },
1546        {"DIVING",       STYLE_DIVING },
1547        {"IGNORE",       STYLE_IGNORE },
1548        {"NORMAL",       STYLE_NORMAL },
1549        {"NOSURVEY",     STYLE_NOSURVEY },
1550        {"PASSAGE",      STYLE_PASSAGE },
1551        {"TOPOFIL",      STYLE_NORMAL },
1552        {NULL,           STYLE_UNKNOWN }
1553   };
1554
1555#define m_multi (BIT(Station) | BIT(Count) | BIT(Depth))
1556
1557   int style, k = 0;
1558   reading d;
1559   unsigned long mUsed = 0;
1560   int old_style = pcs->style;
1561
1562   /* after a bad *data command ignore survey data until the next
1563    * *data command to avoid an avalanche of errors */
1564   pcs->recorded_style = pcs->style = STYLE_IGNORE;
1565
1566   get_token();
1567   style = match_tok(styletab, TABSIZE(styletab));
1568
1569   if (style == STYLE_DEFAULT) {
1570      default_style(pcs);
1571      return;
1572   }
1573
1574   if (style == STYLE_IGNORE) {
1575      return;
1576   }
1577
1578   if (style == STYLE_UNKNOWN) {
1579      if (s_empty(&token)) {
1580         /* "*data" without arguments reinitialises the current style - useful
1581          * when using *data passage as it provides a way to break the passage
1582          * tube without having to repeat the full *data passage command.
1583          */
1584         pcs->recorded_style = pcs->style = style = old_style;
1585         goto reinit_style;
1586      }
1587      /* TRANSLATORS: e.g. trying to refer to an invalid FNORD data style */
1588      compile_diagnostic(DIAG_ERR|DIAG_TOKEN|DIAG_SKIP, /*Data style “%s” unknown*/65, s_str(&token));
1589      return;
1590   }
1591
1592   skipblanks();
1593#ifndef NO_DEPRECATED
1594   /* Olde syntax had optional field for survey grade, so allow an omit
1595    * but issue a warning about it */
1596   if (isOmit(ch)) {
1597      static int data_depr_count = 0;
1598      if (data_depr_count < 5) {
1599         compile_diagnostic(DIAG_WARN|DIAG_TOKEN, /*“*data %s %c …” is deprecated - use “*data %s …” instead*/104,
1600                            s_str(&token), ch, s_str(&token));
1601         if (++data_depr_count == 5)
1602            compile_diagnostic(DIAG_INFO, /*Further uses of this deprecated feature will not be reported*/95);
1603      }
1604      nextch();
1605   }
1606#endif
1607
1608   int kMac = 6; /* minimum for NORMAL style */
1609   reading *new_order = osmalloc(kMac * sizeof(reading));
1610   char *style_name = s_steal(&token);
1611   do {
1612      filepos fp;
1613      get_pos(&fp);
1614      get_token();
1615      d = match_tok(dtab, TABSIZE(dtab));
1616      if (d == End && !s_empty(&token)) {
1617         compile_diagnostic(DIAG_ERR|DIAG_TOKEN|DIAG_SKIP,
1618                            /*Reading “%s” not allowed in data style “%s”*/63,
1619                            s_str(&token), style_name);
1620         free(style_name);
1621         free(new_order);
1622         return;
1623      }
1624
1625      /* only token allowed after IGNOREALL is NEWLINE */
1626      if (k && new_order[k - 1] == IgnoreAll && d != Newline) {
1627         set_pos(&fp);
1628         break;
1629      }
1630      /* Note: an unknown token is reported as trailing garbage */
1631      if (!TSTBIT(mask_all[style], d)) {
1632         /* TRANSLATORS: a data "style" is something like NORMAL, DIVING, etc.
1633          * a "reading" is one of FROM, TO, TAPE, COMPASS, CLINO for NORMAL
1634          * style.  Neither "style" nor "reading" is a keyword in the program.
1635          *
1636          * This error complains about a "DEPTH" gauge reading in "NORMAL"
1637          * style, for example.
1638          */
1639         compile_diagnostic(DIAG_ERR|DIAG_TOKEN|DIAG_SKIP,
1640                            /*Reading “%s” not allowed in data style “%s”*/63,
1641                            s_str(&token), style_name);
1642         free(style_name);
1643         free(new_order);
1644         return;
1645      }
1646      if (TSTBIT(mUsed, Newline) && TSTBIT(m_multi, d)) {
1647         /* TRANSLATORS: caused by e.g.
1648          *
1649          * *data diving station newline depth tape compass
1650          *
1651          * ("depth" needs to occur before "newline"). */
1652         compile_diagnostic(DIAG_ERR|DIAG_TOKEN|DIAG_SKIP,
1653                            /*Reading “%s” must occur before NEWLINE*/225, s_str(&token));
1654         free(style_name);
1655         free(new_order);
1656         return;
1657      }
1658      /* Check for duplicates unless it's a special reading:
1659       *   IGNOREALL,IGNORE (duplicates allowed) ; END (not possible)
1660       */
1661      if (!((BIT(Ignore) | BIT(End) | BIT(IgnoreAll)) & BIT(d))) {
1662         if (TSTBIT(mUsed, d)) {
1663            /* TRANSLATORS: complains about a situation like trying to define
1664             * two from stations per leg */
1665            compile_diagnostic(DIAG_ERR|DIAG_TOKEN|DIAG_SKIP, /*Duplicate reading “%s”*/67, s_str(&token));
1666            free(style_name);
1667            free(new_order);
1668            return;
1669         } else {
1670            /* Check for previously listed readings which are incompatible
1671             * with this one - e.g. Count vs FrCount */
1672            bool fBad = false;
1673            switch (d) {
1674             case Station:
1675               if (mUsed & (BIT(Fr) | BIT(To))) fBad = true;
1676               break;
1677             case Fr: case To:
1678               if (TSTBIT(mUsed, Station)) fBad = true;
1679               break;
1680             case Count:
1681               if (mUsed & (BIT(FrCount) | BIT(ToCount) | BIT(Tape)))
1682                  fBad = true;
1683               break;
1684             case FrCount: case ToCount:
1685               if (mUsed & (BIT(Count) | BIT(Tape)))
1686                  fBad = true;
1687               break;
1688             case Depth:
1689               if (mUsed & (BIT(FrDepth) | BIT(ToDepth) | BIT(DepthChange)))
1690                  fBad = true;
1691               break;
1692             case FrDepth: case ToDepth:
1693               if (mUsed & (BIT(Depth) | BIT(DepthChange))) fBad = true;
1694               break;
1695             case DepthChange:
1696               if (mUsed & (BIT(FrDepth) | BIT(ToDepth) | BIT(Depth)))
1697                  fBad = true;
1698               break;
1699             case Newline:
1700               if (mUsed & ~m_multi) {
1701                  /* TRANSLATORS: e.g.
1702                   *
1703                   * *data normal from to tape newline compass clino */
1704                  compile_diagnostic(DIAG_ERR|DIAG_TOKEN|DIAG_SKIP, /*NEWLINE can only be preceded by STATION, DEPTH, and COUNT*/226);
1705                  free(style_name);
1706                  free(new_order);
1707                  return;
1708               }
1709               if (k == 0) {
1710                  /* TRANSLATORS: error from:
1711                   *
1712                   * *data normal newline from to tape compass clino */
1713                  compile_diagnostic(DIAG_ERR|DIAG_TOKEN|DIAG_SKIP, /*NEWLINE can’t be the first reading*/222);
1714                  free(style_name);
1715                  free(new_order);
1716                  return;
1717               }
1718               break;
1719             default: /* avoid compiler warnings about unhandled enums */
1720               break;
1721            }
1722            if (fBad) {
1723               /* Not entirely happy with phrasing this... */
1724               /* TRANSLATORS: This is an error from the *DATA command.  It
1725                * means that a reading (which will appear where %s is isn't
1726                * valid as the list of readings has already included the same
1727                * reading, or an equivalent one (e.g. you can't have both
1728                * DEPTH and DEPTHCHANGE together). */
1729               compile_diagnostic(DIAG_ERR|DIAG_TOKEN|DIAG_SKIP, /*Reading “%s” duplicates previous reading(s)*/77,
1730                                  s_str(&token));
1731               free(style_name);
1732               free(new_order);
1733               return;
1734            }
1735            mUsed |= BIT(d); /* used to catch duplicates */
1736         }
1737      }
1738      if (k && new_order[k - 1] == IgnoreAll) {
1739         SVX_ASSERT(d == Newline);
1740         k--;
1741         d = IgnoreAllAndNewLine;
1742      }
1743      if (k >= kMac) {
1744         kMac = kMac * 2;
1745         new_order = osrealloc(new_order, kMac * sizeof(reading));
1746      }
1747      new_order[k++] = d;
1748   } while (d != End);
1749
1750   if (k >= 2 && new_order[k - 2] == Newline) {
1751      /* TRANSLATORS: error from:
1752       *
1753       * *data normal from to tape compass clino newline */
1754      compile_diagnostic(DIAG_ERR|DIAG_TOKEN|DIAG_SKIP, /*NEWLINE can’t be the last reading*/223);
1755      free(style_name);
1756      free(new_order);
1757      return;
1758   }
1759
1760   if (style == STYLE_NOSURVEY) {
1761      if (TSTBIT(mUsed, Station)) {
1762         if (k >= kMac) {
1763            kMac = kMac * 2;
1764            new_order = osrealloc(new_order, kMac * sizeof(reading));
1765         }
1766         new_order[k - 1] = Newline;
1767         new_order[k++] = End;
1768      }
1769   } else if (style == STYLE_PASSAGE) {
1770      /* Station doesn't mean "multiline" for STYLE_PASSAGE. */
1771   } else if (!TSTBIT(mUsed, Newline) && (m_multi & mUsed)) {
1772      /* TRANSLATORS: Error given by something like:
1773       *
1774       * *data normal station tape compass clino
1775       *
1776       * ("station" signifies interleaved data). */
1777      compile_diagnostic(DIAG_ERR|DIAG_SKIP, /*Interleaved readings, but no NEWLINE*/224);
1778      free(style_name);
1779      free(new_order);
1780      return;
1781   }
1782
1783#if 0
1784   printf("mUsed = 0x%x\n", mUsed);
1785#endif
1786
1787   /* Check the supplied readings form a sufficient set. */
1788   if (style != STYLE_PASSAGE) {
1789       if ((mUsed & (BIT(Fr) | BIT(To))) == (BIT(Fr) | BIT(To)))
1790           mUsed |= BIT(Station);
1791       else if (TSTBIT(mUsed, Station))
1792           mUsed |= BIT(Fr) | BIT(To);
1793   }
1794
1795   if (mUsed & (BIT(Comp) | BIT(BackComp)))
1796      mUsed |= BIT(Comp) | BIT(BackComp);
1797
1798   if (mUsed & (BIT(Clino) | BIT(BackClino)))
1799      mUsed |= BIT(Clino) | BIT(BackClino);
1800
1801   if ((mUsed & (BIT(FrDepth) | BIT(ToDepth))) == (BIT(FrDepth) | BIT(ToDepth)))
1802      mUsed |= BIT(Depth) | BIT(DepthChange);
1803   else if (mUsed & (BIT(Depth) | BIT(DepthChange)))
1804      mUsed |= BIT(FrDepth) | BIT(ToDepth) | BIT(Depth) | BIT(DepthChange);
1805
1806   if ((mUsed & (BIT(FrCount) | BIT(ToCount))) == (BIT(FrCount) | BIT(ToCount)))
1807      mUsed |= BIT(Count) | BIT(Tape) | BIT(BackTape);
1808   else if (mUsed & (BIT(Count) | BIT(Tape) | BIT(BackTape)))
1809      mUsed |= BIT(FrCount) | BIT(ToCount) | BIT(Count) | BIT(Tape) | BIT(BackTape);
1810
1811#if 0
1812   printf("mUsed = 0x%x, opt = 0x%x, mask = 0x%x\n", mUsed,
1813          mask_optional[style], mask[style]);
1814#endif
1815
1816   if (((mUsed &~ BIT(Newline)) | mask_optional[style]) != mask[style]) {
1817      /* Test should only fail with too few bits set, not too many */
1818      SVX_ASSERT((((mUsed &~ BIT(Newline)) | mask_optional[style])
1819              &~ mask[style]) == 0);
1820      /* TRANSLATORS: i.e. not enough readings for the style. */
1821      compile_diagnostic(DIAG_ERR|DIAG_SKIP, /*Too few readings for data style “%s”*/64, style_name);
1822      free(style_name);
1823      free(new_order);
1824      return;
1825   }
1826
1827   /* don't free default ordering or ordering used by parent */
1828   if (pcs->ordering != default_order &&
1829       !(pcs->next && pcs->next->ordering == pcs->ordering))
1830      free((reading*)pcs->ordering);
1831
1832   pcs->recorded_style = pcs->style = style;
1833   pcs->ordering = new_order;
1834
1835   free(style_name);
1836
1837reinit_style:
1838   if (style == STYLE_PASSAGE) {
1839      lrudlist * new_psg = osnew(lrudlist);
1840      new_psg->tube = NULL;
1841      new_psg->next = model;
1842      model = new_psg;
1843      next_lrud = &(new_psg->tube);
1844   }
1845}
1846
1847static void
1848cmd_units(void)
1849{
1850   int units, quantity;
1851   unsigned long qmask;
1852   unsigned long m; /* mask with bit x set to indicate quantity x specified */
1853   real factor;
1854   filepos fp;
1855
1856   qmask = get_qlist(BIT(Q_POS)|BIT(Q_PLUMB)|BIT(Q_LEVEL));
1857
1858   if (!qmask) return;
1859   if (qmask == BIT(Q_DEFAULT)) {
1860      default_units(pcs);
1861      return;
1862   }
1863
1864   get_pos(&fp);
1865   factor = read_numeric(true);
1866   if (factor == 0.0) {
1867      set_pos(&fp);
1868      /* TRANSLATORS: error message given by "*units tape 0 feet" - it’s
1869       * meaningless to say your tape is marked in "0 feet" (but you might
1870       * measure distance by counting knots on a diving line, and tie them
1871       * every "2 feet"). */
1872      compile_diagnostic(DIAG_ERR|DIAG_WORD, /**UNITS factor must be non-zero*/200);
1873      skipline();
1874      return;
1875   }
1876
1877   units = get_units(qmask, true);
1878   if (units == UNITS_NULL) return;
1879   if (TSTBIT(qmask, Q_GRADIENT))
1880      pcs->f_clino_percent = (units == UNITS_PERCENT);
1881   if (TSTBIT(qmask, Q_BACKGRADIENT))
1882      pcs->f_backclino_percent = (units == UNITS_PERCENT);
1883
1884   if (TSTBIT(qmask, Q_BEARING)) {
1885      pcs->f_bearing_quadrants = (units == UNITS_QUADRANTS);
1886   }
1887   if (TSTBIT(qmask, Q_BACKBEARING)) {
1888      pcs->f_backbearing_quadrants = (units == UNITS_QUADRANTS);
1889   }
1890
1891   if (factor == HUGE_REAL) {
1892      factor = factor_tab[units];
1893   } else {
1894      factor *= factor_tab[units];
1895   }
1896
1897   for (quantity = 0, m = BIT(quantity); m <= qmask; quantity++, m <<= 1)
1898      if (qmask & m) pcs->units[quantity] = factor;
1899}
1900
1901static void
1902cmd_calibrate(void)
1903{
1904   real sc, z;
1905   unsigned long qmask, m;
1906   int quantity;
1907   filepos fp;
1908
1909   qmask = get_qlist(BIT(Q_POS)|BIT(Q_PLUMB)|BIT(Q_LEVEL));
1910   if (!qmask) return; /* error already reported */
1911
1912   if (qmask == BIT(Q_DEFAULT)) {
1913      default_calib(pcs);
1914      return;
1915   }
1916
1917   if (((qmask & LEN_QMASK)) && ((qmask & ANG_QMASK))) {
1918      /* TRANSLATORS: e.g.
1919       *
1920       * *calibrate tape compass 1 1
1921       */
1922      compile_diagnostic(DIAG_ERR|DIAG_SKIP, /*Can’t calibrate angular and length quantities together*/227);
1923      return;
1924   }
1925
1926   z = read_numeric(false);
1927   get_pos(&fp);
1928   sc = read_numeric(true);
1929   if (sc == HUGE_REAL) {
1930      if (isalpha(ch)) {
1931         int units = get_units(qmask, false);
1932         if (units == UNITS_NULL) {
1933            return;
1934         }
1935         z *= factor_tab[units];
1936         sc = read_numeric(true);
1937         if (sc == HUGE_REAL) {
1938            sc = (real)1.0;
1939         } else {
1940            /* Adjustment applied is: (reading - z) * sc
1941             * We want: reading * sc - z
1942             * So divide z by sc so the applied adjustment does what we want.
1943             */
1944            z /= sc;
1945         }
1946      } else {
1947         sc = (real)1.0;
1948      }
1949   }
1950
1951   if (sc == HUGE_REAL) sc = (real)1.0;
1952   /* check for declination scale */
1953   if (TSTBIT(qmask, Q_DECLINATION) && sc != 1.0) {
1954      set_pos(&fp);
1955      /* TRANSLATORS: DECLINATION is a built-in keyword, so best not to
1956       * translate */
1957      compile_diagnostic(DIAG_ERR|DIAG_WORD, /*Scale factor must be 1.0 for DECLINATION*/40);
1958      skipline();
1959      return;
1960   }
1961   if (sc == 0.0) {
1962      set_pos(&fp);
1963      /* TRANSLATORS: If the scale factor for an instrument is zero, then any
1964       * reading would be mapped to zero, which doesn't make sense. */
1965      compile_diagnostic(DIAG_ERR|DIAG_WORD, /*Scale factor must be non-zero*/391);
1966      skipline();
1967      return;
1968   }
1969   for (quantity = 0, m = BIT(quantity); m <= qmask; quantity++, m <<= 1) {
1970      if (qmask & m) {
1971         pcs->z[quantity] = pcs->units[quantity] * z;
1972         pcs->sc[quantity] = sc;
1973      }
1974   }
1975}
1976
1977static const sztok north_tab[] = {
1978     { "GRID",          GRID_NORTH },
1979     { "MAGNETIC",      MAGNETIC_NORTH },
1980     { "TRUE",          TRUE_NORTH },
1981     { NULL,            -1 }
1982};
1983
1984static void
1985cmd_cartesian(void)
1986{
1987    get_token();
1988    int north = match_tok(north_tab, TABSIZE(north_tab));
1989    if (north < 0) {
1990        compile_diagnostic(DIAG_ERR|DIAG_TOKEN|DIAG_SKIP,
1991                           /*Expecting “%s”, “%s”, or “%s”*/188,
1992                           "GRID", "MAGNETIC", "TRUE");
1993        return;
1994    }
1995    pcs->cartesian_north = north;
1996    pcs->cartesian_rotation = 0.0;
1997
1998    skipblanks();
1999    if (!isEol(ch) && !isComm(ch)) {
2000        real rotation = read_numeric(false);
2001        // Accept the same units as *declination does.
2002        int units = get_units(BIT(Q_DECLINATION), false);
2003        if (units == UNITS_NULL) {
2004            return;
2005        }
2006        pcs->cartesian_rotation = rotation * factor_tab[units];
2007    }
2008}
2009
2010static void
2011cmd_declination(void)
2012{
2013    real v = read_numeric(true);
2014    if (v == HUGE_REAL) {
2015        get_token_legacy_no_blanks();
2016        if (!S_EQ(&uctoken, "AUTO")) {
2017            compile_diagnostic(DIAG_ERR|DIAG_SKIP|DIAG_COL, /*Expected number or “AUTO”*/309);
2018            return;
2019        }
2020        do_legacy_token_warning();
2021        if (!pcs->proj_str) {
2022            // TRANSLATORS: %s is replaced by the command that requires it, e.g.
2023            // *DECLINATION AUTO
2024            compile_diagnostic(DIAG_ERR|DIAG_SKIP|DIAG_TOKEN,
2025                               /*Input coordinate system must be specified for “%s”*/301,
2026                               "*DECLINATION AUTO");
2027            return;
2028        }
2029
2030        /* *declination auto X Y Z */
2031        filepos fp;
2032        get_pos(&fp);
2033        real x = read_numeric(false);
2034        real y = read_numeric(false);
2035        real z = read_numeric(false);
2036        set_declination_location(x, y, z, pcs->proj_str, &fp);
2037    } else {
2038        /* *declination D UNITS */
2039        int units = get_units(BIT(Q_DECLINATION), false);
2040        if (units == UNITS_NULL) {
2041            return;
2042        }
2043        pcs->z[Q_DECLINATION] = -v * factor_tab[units];
2044        pcs->convergence = 0;
2045    }
2046}
2047
2048#ifndef NO_DEPRECATED
2049static void
2050cmd_default(void)
2051{
2052   static const sztok defaulttab[] = {
2053      { "CALIBRATE", CMD_CALIBRATE },
2054      { "DATA",      CMD_DATA },
2055      { "UNITS",     CMD_UNITS },
2056      { NULL,        CMD_NULL }
2057   };
2058
2059   get_token();
2060   switch (match_tok(defaulttab, TABSIZE(defaulttab))) {
2061    case CMD_CALIBRATE:
2062      default_calib(pcs);
2063      break;
2064    case CMD_DATA:
2065      default_style(pcs);
2066      default_grade(pcs);
2067      pcs->cartesian_north = TRUE_NORTH;
2068      pcs->cartesian_rotation = 0.0;
2069      break;
2070    case CMD_UNITS:
2071      default_units(pcs);
2072      break;
2073    default:
2074      compile_diagnostic(DIAG_ERR|DIAG_TOKEN|DIAG_SKIP, /*Unknown setting “%s”*/41,
2075                         s_str(&token));
2076   }
2077}
2078#endif
2079
2080static void
2081cmd_include(void)
2082{
2083   char *pth = NULL;
2084   string fnm = S_INIT;
2085#ifndef NO_DEPRECATED
2086   prefix *root_store;
2087#endif
2088   int ch_store;
2089
2090   pth = path_from_fnm(file.filename);
2091
2092   read_string(&fnm);
2093
2094#ifndef NO_DEPRECATED
2095   /* Since *begin / *end nesting cannot cross file boundaries we only
2096    * need to preserve the prefix if the deprecated *prefix command
2097    * can be used */
2098   root_store = root;
2099   root = pcs->Prefix; /* Root for include file is current prefix */
2100#endif
2101   ch_store = ch;
2102
2103   data_file(pth, s_str(&fnm));
2104
2105#ifndef NO_DEPRECATED
2106   root = root_store; /* and restore root */
2107#endif
2108   ch = ch_store;
2109
2110   s_free(&fnm);
2111   free(pth);
2112}
2113
2114static void
2115cmd_sd(void)
2116{
2117   real sd, variance;
2118   int units;
2119   unsigned long qmask, m;
2120   int quantity;
2121   qmask = get_qlist(BIT(Q_DECLINATION));
2122   if (!qmask) return; /* no quantities found - error already reported */
2123
2124   if (qmask == BIT(Q_DEFAULT)) {
2125      default_grade(pcs);
2126      return;
2127   }
2128   sd = read_numeric(false);
2129   if (sd <= (real)0.0) {
2130      compile_diagnostic(DIAG_ERR|DIAG_SKIP|DIAG_COL, /*Standard deviation must be positive*/48);
2131      return;
2132   }
2133   units = get_units(qmask, false);
2134   if (units == UNITS_NULL) return;
2135
2136   sd *= factor_tab[units];
2137   variance = sqrd(sd);
2138
2139   for (quantity = 0, m = BIT(quantity); m <= qmask; quantity++, m <<= 1)
2140      if (qmask & m) pcs->Var[quantity] = variance;
2141}
2142
2143enum {
2144    ROLE_BACKTAPE,
2145    ROLE_BACKCOMPASS,
2146    ROLE_BACKCLINO,
2147    ROLE_TAPE,
2148    ROLE_COMPASS,
2149    ROLE_CLINO,
2150    ROLE_COUNTER,
2151    ROLE_DEPTH,
2152    ROLE_STATION,
2153    ROLE_POSITION,
2154    ROLE_NOTES,
2155    ROLE_PICTURES,
2156    ROLE_INSTRUMENTS,
2157    ROLE_ASSISTANT,
2158    ROLE_ALTITUDE,
2159    ROLE_DIMENSIONS,
2160    ROLE_LEFT,
2161    ROLE_RIGHT,
2162    ROLE_UP,
2163    ROLE_DOWN,
2164    ROLE_EXPLORER
2165};
2166
2167static const sztok role_tab[] = {
2168    {"ALTITUDE",        ROLE_ALTITUDE},
2169    {"ASSISTANT",       ROLE_ASSISTANT},
2170    {"BACKBEARING",     ROLE_BACKCOMPASS},
2171    {"BACKCLINO",       ROLE_BACKCLINO},
2172    {"BACKCOMPASS",     ROLE_BACKCOMPASS},
2173    {"BACKGRADIENT",    ROLE_BACKCLINO},
2174    {"BACKLENGTH",      ROLE_BACKTAPE},
2175    {"BACKTAPE",        ROLE_BACKTAPE},
2176    {"BEARING",         ROLE_COMPASS},
2177    {"CEILING",         ROLE_UP},
2178    {"CLINO",           ROLE_CLINO},
2179    {"COMPASS",         ROLE_COMPASS},
2180    {"COUNT",           ROLE_COUNTER},
2181    {"COUNTER",         ROLE_COUNTER},
2182    {"DEPTH",           ROLE_DEPTH},
2183    {"DIMENSIONS",      ROLE_DIMENSIONS},
2184    {"DOG",             ROLE_ASSISTANT},
2185    {"DOWN",            ROLE_DOWN},
2186    {"DZ",              ROLE_ALTITUDE},
2187    {"EXPLORER",        ROLE_EXPLORER},
2188    {"FLOOR",           ROLE_DOWN},
2189    {"GRADIENT",        ROLE_CLINO},
2190    {"INSTRUMENTS",     ROLE_INSTRUMENTS},
2191    {"INSTS",           ROLE_INSTRUMENTS},
2192    {"LEFT",            ROLE_LEFT},
2193    {"LENGTH",          ROLE_TAPE},
2194    {"NOTEBOOK",        ROLE_NOTES},
2195    {"NOTES",           ROLE_NOTES},
2196    {"PICS",            ROLE_PICTURES},
2197    {"PICTURES",        ROLE_PICTURES},
2198    {"POSITION",        ROLE_POSITION},
2199    {"RIGHT",           ROLE_RIGHT},
2200    {"STATION",         ROLE_STATION},
2201    {"TAPE",            ROLE_TAPE},
2202    {"UP",              ROLE_UP},
2203    {NULL,              -1}
2204};
2205
2206static void
2207cmd_team(void)
2208{
2209    string name = S_INIT;
2210    if (!read_string_warning(&name)) {
2211        skipline();
2212        return;
2213    }
2214    s_free(&name);
2215
2216    while (true) {
2217        skipblanks();
2218        if (isComm(ch) || isEol(ch)) return;
2219        get_token();
2220        int role = match_tok(role_tab, TABSIZE(role_tab));
2221        if (role < 0) {
2222            // Skip after a bad role to avoid triggering multiple warnings for
2223            // one *team command in existing data from before this check was
2224            // implemented.
2225            compile_diagnostic(DIAG_WARN|DIAG_TOKEN|DIAG_SKIP, /*Unknown team role “%s”*/532,
2226                               s_str(&token));
2227            return;
2228        }
2229    }
2230}
2231
2232static void
2233cmd_title(void)
2234{
2235   if (!fExplicitTitle && pcs->Prefix == root) {
2236       /* If we don't have an explicit title yet, and we're currently in the
2237        * root prefix, use this title explicitly. */
2238      fExplicitTitle = true;
2239      read_string(&survey_title);
2240   } else {
2241      /* parse and throw away this title (but still check rest of line) */
2242      string s = S_INIT;
2243      read_string(&s);
2244      s_free(&s);
2245   }
2246}
2247
2248static const sztok case_tab[] = {
2249     {"PRESERVE", OFF},
2250     {"TOLOWER",  LOWER},
2251     {"TOUPPER",  UPPER},
2252     {NULL,       -1}
2253};
2254
2255static void
2256cmd_case(void)
2257{
2258   int setting;
2259   get_token();
2260   setting = match_tok(case_tab, TABSIZE(case_tab));
2261   if (setting != -1) {
2262      pcs->Case = setting;
2263   } else {
2264      compile_diagnostic(DIAG_ERR|DIAG_TOKEN|DIAG_SKIP, /*Found “%s”, expecting “PRESERVE”, “TOUPPER”, or “TOLOWER”*/10, s_str(&token));
2265   }
2266}
2267
2268static void
2269cmd_copyright(void)
2270{
2271    filepos fp;
2272    get_pos(&fp);
2273    unsigned y1 = read_uint_raw(DIAG_WARN|DIAG_UINT, /*Invalid year*/534, NULL);
2274    if (y1 < 1000) {
2275        set_pos(&fp);
2276        compile_diagnostic(DIAG_WARN|DIAG_UINT, /*Invalid year*/534, y1);
2277    } else if (y1 > current_year) {
2278        set_pos(&fp);
2279        compile_diagnostic(DIAG_WARN|DIAG_UINT, /*Date is in the future!*/80);
2280    }
2281    if (ch == '-') {
2282        nextch();
2283        get_pos(&fp);
2284        unsigned y2 = read_uint_raw(DIAG_WARN|DIAG_UINT, /*Invalid year*/534, NULL);
2285        if (y2 < 1000) {
2286            set_pos(&fp);
2287            compile_diagnostic(DIAG_WARN|DIAG_UINT, /*Invalid year*/534, y2);
2288        } else if (y2 < y1) {
2289            set_pos(&fp);
2290            compile_diagnostic(DIAG_WARN|DIAG_UINT, /*End of date range is before the start*/81);
2291        } else if (y2 > current_year) {
2292            set_pos(&fp);
2293            compile_diagnostic(DIAG_WARN|DIAG_UINT, /*Date is in the future!*/80);
2294        }
2295    }
2296
2297    string text = S_INIT;
2298    if (!read_string_warning(&text)) {
2299        skipline();
2300        return;
2301    }
2302    s_free(&text);
2303
2304    skipblanks();
2305    if (!isComm(ch) && !isEol(ch))
2306        compile_diagnostic(DIAG_WARN|DIAG_TAIL, /*End of line not blank*/15);
2307}
2308
2309typedef enum {
2310    CS_NONE = -1,
2311    CS_CUSTOM,
2312    CS_EPSG,
2313    CS_ESRI,
2314    CS_EUR79Z30,
2315    CS_IJTSK,
2316    CS_IJTSK03,
2317    CS_JTSK,
2318    CS_JTSK03,
2319    CS_LAT,
2320    CS_LOCAL,
2321    CS_LONG,
2322    CS_OSGB,
2323    CS_S_MERC,
2324    CS_UTM
2325} cs_class;
2326
2327static const sztok cs_tab[] = {
2328     {"CUSTOM",   CS_CUSTOM},
2329     {"EPSG",     CS_EPSG},     /* EPSG:<number> */
2330     {"ESRI",     CS_ESRI},     /* ESRI:<number> */
2331     {"EUR79Z30", CS_EUR79Z30},
2332     {"IJTSK",    CS_IJTSK},
2333     {"IJTSK03",  CS_IJTSK03},
2334     {"JTSK",     CS_JTSK},
2335     {"JTSK03",   CS_JTSK03},
2336     {"LAT",      CS_LAT},      /* LAT-LONG */
2337     {"LOCAL",    CS_LOCAL},
2338     {"LONG",     CS_LONG},     /* LONG-LAT */
2339     {"OSGB",     CS_OSGB},     /* OSGB:<H, N, O, S or T><A-Z except I> */
2340     {"S",        CS_S_MERC},   /* S-MERC */
2341     // UTM<zone><N or S or nothing> is handled separately to avoid needing 180
2342     // entries in this lookup table.
2343     {NULL,       CS_NONE}
2344};
2345
2346static void
2347cmd_cs(void)
2348{
2349   char *proj_str = NULL;
2350   cs_class cs;
2351   int cs_sub = INT_MIN;
2352   filepos fp;
2353   bool output = false;
2354   enum { YES, NO, MAYBE } ok_for_output = YES;
2355   static bool had_cs = false;
2356
2357   if (!had_cs) {
2358      had_cs = true;
2359      if (first_fix_name) {
2360         compile_diagnostic_at(DIAG_ERR,
2361                               first_fix_filename, first_fix_line,
2362                               /*Station “%s” fixed before CS command first used*/442,
2363                               sprint_prefix(first_fix_name));
2364      }
2365   }
2366
2367   skipblanks();
2368   get_pos(&fp);
2369   get_token_no_blanks();
2370   if (S_EQ(&uctoken, "OUT")) {
2371      output = true;
2372      skipblanks();
2373      get_pos(&fp);
2374      get_token_no_blanks();
2375   }
2376
2377   if (s_len(&uctoken) > 3 &&
2378       memcmp(s_str(&uctoken), "UTM", 3) == 0 &&
2379       isdigit((unsigned char)s_str(&uctoken)[3])) {
2380       // The token starts "UTM" followed by a digit so handle that separately
2381       // to avoid needing 180 entries for UTM zones in the cs_tab lookup
2382       // table.
2383       cs = CS_UTM;
2384       // Reposition on the digit after "UTM".
2385       set_pos(&fp);
2386       nextch();
2387       nextch();
2388       nextch();
2389       unsigned n = read_uint();
2390       if (n >= 1 && n <= 60) {
2391           int uch = toupper(ch);
2392           cs_sub = (int)n;
2393           if (uch == 'S') {
2394               nextch();
2395               cs_sub = -cs_sub;
2396           } else if (uch == 'N') {
2397               nextch();
2398           }
2399       }
2400   } else {
2401       cs = match_tok(cs_tab, TABSIZE(cs_tab));
2402       switch (cs) {
2403         case CS_NONE:
2404           break;
2405         case CS_CUSTOM:
2406           ok_for_output = MAYBE;
2407           get_pos(&fp);
2408           string str = S_INIT;
2409           read_string(&str);
2410           proj_str = s_steal(&str);
2411           cs_sub = 0;
2412           break;
2413         case CS_EPSG: case CS_ESRI:
2414           ok_for_output = MAYBE;
2415           if (ch == ':' && isdigit(nextch())) {
2416               unsigned n = read_uint();
2417               if (n < 1000000) {
2418                   cs_sub = (int)n;
2419               }
2420           }
2421           break;
2422         case CS_EUR79Z30:
2423           cs_sub = 0;
2424           break;
2425         case CS_JTSK:
2426         case CS_JTSK03:
2427           ok_for_output = NO;
2428           cs_sub = 0;
2429           break;
2430         case CS_IJTSK:
2431         case CS_IJTSK03:
2432           cs_sub = 0;
2433           break;
2434         case CS_LAT: case CS_LONG:
2435           ok_for_output = NO;
2436           if (ch == '-') {
2437               nextch();
2438               get_token_no_blanks();
2439               cs_class cs2 = match_tok(cs_tab, TABSIZE(cs_tab));
2440               if ((cs ^ cs2) == (CS_LAT ^ CS_LONG)) {
2441                   cs_sub = 0;
2442               }
2443           }
2444           break;
2445         case CS_LOCAL:
2446           cs_sub = 0;
2447           break;
2448         case CS_OSGB:
2449           if (ch == ':') {
2450               int uch1 = toupper(nextch());
2451               if (strchr("HNOST", uch1)) {
2452                   int uch2 = toupper(nextch());
2453                   if (uch2 >= 'A' && uch2 <= 'Z' && uch2 != 'I') {
2454                       int x, y;
2455                       nextch();
2456                       if (uch1 > 'I') --uch1;
2457                       uch1 -= 'A';
2458                       if (uch2 > 'I') --uch2;
2459                       uch2 -= 'A';
2460                       x = uch1 % 5;
2461                       y = uch1 / 5;
2462                       x = (x * 5) + uch2 % 5;
2463                       y = (y * 5) + uch2 / 5;
2464                       cs_sub = y * 25 + x;
2465                   }
2466               }
2467           }
2468           break;
2469         case CS_S_MERC:
2470           if (ch == '-') {
2471               nextch();
2472               get_token_no_blanks();
2473               if (S_EQ(&uctoken, "MERC")) {
2474                   cs_sub = 0;
2475               }
2476           }
2477           break;
2478         case CS_UTM:
2479           // Handled outside of this switch, but avoid compiler warning about
2480           // unhandled enumeration value.
2481           break;
2482       }
2483   }
2484   if (cs_sub == INT_MIN || isalnum(ch)) {
2485      set_pos(&fp);
2486      compile_diagnostic(DIAG_ERR|DIAG_WORD, /*Unknown coordinate system*/434);
2487      skipline();
2488      return;
2489   }
2490   /* Actually handle the cs */
2491   switch (cs) {
2492      case CS_NONE:
2493         break;
2494      case CS_CUSTOM:
2495         /* proj_str already set */
2496         break;
2497      case CS_EPSG:
2498         proj_str = osmalloc(32);
2499         snprintf(proj_str, 32, "EPSG:%d", cs_sub);
2500         break;
2501      case CS_ESRI:
2502         proj_str = osmalloc(32);
2503         snprintf(proj_str, 32, "ESRI:%d", cs_sub);
2504         break;
2505      case CS_EUR79Z30:
2506         proj_str = osstrdup("+proj=utm +zone=30 +ellps=intl +towgs84=-86,-98,-119,0,0,0,0 +no_defs");
2507         break;
2508      case CS_IJTSK:
2509         proj_str = osstrdup("+proj=krovak +ellps=bessel +towgs84=570.8285,85.6769,462.842,4.9984,1.5867,5.2611,3.5623 +no_defs");
2510         break;
2511      case CS_IJTSK03:
2512         proj_str = osstrdup("+proj=krovak +ellps=bessel +towgs84=485.021,169.465,483.839,7.786342,4.397554,4.102655,0 +no_defs");
2513         break;
2514      case CS_JTSK:
2515         proj_str = osstrdup("+proj=krovak +czech +ellps=bessel +towgs84=570.8285,85.6769,462.842,4.9984,1.5867,5.2611,3.5623 +no_defs");
2516         break;
2517      case CS_JTSK03:
2518         proj_str = osstrdup("+proj=krovak +czech +ellps=bessel +towgs84=485.021,169.465,483.839,7.786342,4.397554,4.102655,0 +no_defs");
2519         break;
2520      case CS_LAT:
2521         /* FIXME: Requires PROJ >= 4.8.0 for +axis, and the SDs will be
2522          * misapplied, so we may want to swap ourselves.  Also, while
2523          * therion supports lat-long, I'm not totally convinced that it is
2524          * sensible to do so - people often say "lat-long", but probably
2525          * don't think that that's actually "Northing, Easting".  This
2526          * seems like it'll result in people accidentally getting X and Y
2527          * swapped in their fixed points...
2528          */
2529#if 0
2530         proj_str = osstrdup("+proj=longlat +ellps=WGS84 +datum=WGS84 +axis=neu +no_defs");
2531#endif
2532         break;
2533      case CS_LOCAL:
2534         /* FIXME: Is it useful to be able to explicitly specify this? */
2535         break;
2536      case CS_LONG:
2537         proj_str = osstrdup("EPSG:4326");
2538         break;
2539      case CS_OSGB: {
2540         int x = 14 - (cs_sub % 25);
2541         int y = (cs_sub / 25) - 20;
2542         proj_str = osmalloc(160);
2543         snprintf(proj_str, 160,
2544                  "+proj=tmerc +lat_0=49 +lon_0=-2 +k=0.9996012717 "
2545                  "+x_0=%d +y_0=%d +ellps=airy +datum=OSGB36 +units=m +no_defs",
2546                  x * 100000, y * 100000);
2547         break;
2548      }
2549      case CS_S_MERC:
2550         proj_str = osstrdup("EPSG:3857");
2551         break;
2552      case CS_UTM:
2553         proj_str = osmalloc(32);
2554         if (cs_sub > 0) {
2555            snprintf(proj_str, 32, "EPSG:%d", 32600 + cs_sub);
2556         } else {
2557            snprintf(proj_str, 32, "EPSG:%d", 32700 - cs_sub);
2558         }
2559         break;
2560   }
2561
2562   if (!proj_str) {
2563      /* printf("CS %d:%d\n", (int)cs, cs_sub); */
2564      set_pos(&fp);
2565      compile_diagnostic(DIAG_ERR|DIAG_STRING, /*Unknown coordinate system*/434);
2566      skipline();
2567      return;
2568   }
2569
2570   if (output) {
2571      if (ok_for_output == NO) {
2572         set_pos(&fp);
2573         compile_diagnostic(DIAG_ERR|DIAG_STRING, /*Coordinate system unsuitable for output*/435);
2574         skipline();
2575         return;
2576      }
2577
2578      if (proj_str_out && strcmp(proj_str, proj_str_out) == 0) {
2579          /* Same as the output cs that's already set, so nothing to do. */
2580          free(proj_str);
2581          return;
2582      }
2583
2584      if (ok_for_output == MAYBE) {
2585          /* We only actually create the transformation from input to output when
2586           * we need it, but for a custom proj string or EPSG/ESRI code we need
2587           * to check that the specified coordinate system is valid and also if
2588           * it's suitable for output so we need to test creating it here.
2589           */
2590          proj_errno_reset(NULL);
2591          PJ* pj = proj_create(PJ_DEFAULT_CTX, proj_str);
2592          if (!pj) {
2593              set_pos(&fp);
2594              compile_diagnostic(DIAG_ERR|DIAG_STRING, /*Invalid coordinate system: %s*/443,
2595                                 proj_context_errno_string(PJ_DEFAULT_CTX,
2596                                                           proj_context_errno(PJ_DEFAULT_CTX)));
2597              skipline();
2598              free(proj_str);
2599              return;
2600          }
2601          int type = proj_get_type(pj);
2602          if (type == PJ_TYPE_GEOGRAPHIC_2D_CRS ||
2603              type == PJ_TYPE_GEOGRAPHIC_3D_CRS) {
2604              set_pos(&fp);
2605              compile_diagnostic(DIAG_ERR|DIAG_STRING, /*Coordinate system unsuitable for output*/435);
2606              skipline();
2607              free(proj_str);
2608              return;
2609          }
2610      }
2611
2612      if (proj_str_out) {
2613          /* If the output cs is already set, subsequent attempts to set it
2614           * are silently ignored (so you can combine two datasets and set
2615           * the output cs to use before you include either).
2616           */
2617          free(proj_str);
2618      } else {
2619          proj_str_out = proj_str;
2620      }
2621   } else {
2622      if (proj_str_out && strcmp(proj_str, proj_str_out) == 0) {
2623         /* Same as the current output projection, so valid for input. */
2624      } else if (pcs->proj_str && strcmp(proj_str, pcs->proj_str) == 0) {
2625         /* Same as the current input projection, so nothing to do! */
2626         return;
2627      } else if (ok_for_output == MAYBE) {
2628         /* (ok_for_output == MAYBE) also happens to indicate whether we need
2629          * to check that the coordinate system is valid for input.
2630          */
2631         proj_errno_reset(NULL);
2632         PJ* pj = proj_create(PJ_DEFAULT_CTX, proj_str);
2633         if (!pj) {
2634            set_pos(&fp);
2635            compile_diagnostic(DIAG_ERR|DIAG_STRING, /*Invalid coordinate system: %s*/443,
2636                               proj_context_errno_string(PJ_DEFAULT_CTX,
2637                                                         proj_context_errno(PJ_DEFAULT_CTX)));
2638            skipline();
2639            return;
2640         }
2641         proj_destroy(pj);
2642      }
2643
2644      /* Free current input proj_str if not used by parent. */
2645      settings * p = pcs;
2646      if (!p->next || p->proj_str != p->next->proj_str)
2647         free(p->proj_str);
2648      p->proj_str = proj_str;
2649      p->input_convergence = HUGE_REAL;
2650      invalidate_pj_cached();
2651   }
2652}
2653
2654static const sztok infer_tab[] = {
2655     { "EQUATES",       INFER_EQUATES },
2656     { "EXPORTS",       INFER_EXPORTS },
2657     { "PLUMBS",        INFER_PLUMBS },
2658#if 0 /* FIXME */
2659     { "SUBSURVEYS",    INFER_SUBSURVEYS },
2660#endif
2661     { NULL,            INFER_NULL }
2662};
2663
2664static const sztok onoff_tab[] = {
2665     { "OFF", 0 },
2666     { "ON",  1 },
2667     { NULL, -1 }
2668};
2669
2670static void
2671cmd_infer(void)
2672{
2673   infer_what setting;
2674   int on;
2675   get_token();
2676   setting = match_tok(infer_tab, TABSIZE(infer_tab));
2677   if (setting == INFER_NULL) {
2678      compile_diagnostic(DIAG_ERR|DIAG_TOKEN|DIAG_SKIP, /*Found “%s”, expecting “EQUATES”, “EXPORTS”, or “PLUMBS”*/31, s_str(&token));
2679      return;
2680   }
2681   get_token();
2682   on = match_tok(onoff_tab, TABSIZE(onoff_tab));
2683   if (on == -1) {
2684      compile_diagnostic(DIAG_ERR|DIAG_TOKEN|DIAG_SKIP, /*Found “%s”, expecting “ON” or “OFF”*/32, s_str(&token));
2685      return;
2686   }
2687
2688   if (on) {
2689      pcs->infer |= BIT(setting);
2690      if (setting == INFER_EXPORTS) fExportUsed = true;
2691   } else {
2692      pcs->infer &= ~BIT(setting);
2693   }
2694}
2695
2696static void
2697cmd_truncate(void)
2698{
2699   unsigned int truncate_at = 0; /* default is no truncation */
2700   filepos fp;
2701
2702   get_pos(&fp);
2703
2704   get_token();
2705   if (!S_EQ(&uctoken, "OFF")) {
2706      if (!s_empty(&uctoken)) set_pos(&fp);
2707      truncate_at = read_uint();
2708   }
2709   /* for backward compatibility, "*truncate 0" means "*truncate off" */
2710   pcs->Truncate = (truncate_at == 0) ? INT_MAX : truncate_at;
2711}
2712
2713static void
2714cmd_ref(void)
2715{
2716   /* Just syntax check for now. */
2717   string ref = S_INIT;
2718   read_string(&ref);
2719   s_free(&ref);
2720}
2721
2722static void
2723cmd_require(void)
2724{
2725    // Add extra 0 so `*require 1.4.10.1` fails with cavern version 1.4.10.
2726    const unsigned version[] = {COMMAVERSION, 0};
2727
2728    filepos fp;
2729    get_pos(&fp);
2730
2731    // Parse the required version number, storing its components in
2732    // required_version.  We only store at most one more component than
2733    // COMMAVERSION has since more than that can't affect the comparison.
2734    size_t i = 0;
2735    int diff = 0;
2736    while (1) {
2737        unsigned component = read_uint();
2738        if (diff == 0 && i < sizeof(version) / sizeof(version[0])) {
2739            if (diff == 0) {
2740                diff = (int)version[i++] - (int)component;
2741            }
2742        }
2743        if (ch != '.' || isBlank(nextch()) || isComm(ch) || isEol(ch))
2744            break;
2745    }
2746
2747    if (diff < 0) {
2748        // Requirement not satisfied
2749        size_t len = (size_t)(ftell(file.fh) - fp.offset);
2750        char *v = osmalloc(len + 1);
2751        set_pos(&fp);
2752        for (size_t j = 0; j < len; j++) {
2753            v[j] = ch;
2754            nextch();
2755        }
2756        v[len] = '\0';
2757        /* TRANSLATORS: Feel free to translate as "or newer" instead of "or
2758         * greater" if that gives a more natural translation.  It's
2759         * technically not quite right when there are parallel active release
2760         * series (e.g. Survex 1.0.40 was released *after* 1.2.0), but this
2761         * seems unlikely to confuse users.  "Survex" is the name of the
2762         * software, so should not be translated.
2763         *
2764         * Here "survey" is a "cave map" rather than list of questions - it should be
2765         * translated to the terminology that cavers using the language would use.
2766         */
2767        compile_diagnostic(DIAG_FATAL|DIAG_FROM(fp), /*Survex version %s or greater required to process this survey data.*/38, v);
2768        // Does not return so no point freeing v here.
2769    }
2770}
2771
2772/* allocate new meta_data if need be */
2773void
2774copy_on_write_meta(settings *s)
2775{
2776   if (!s->meta || s->meta->ref_count != 0) {
2777       meta_data * meta_new = osnew(meta_data);
2778       if (!s->meta) {
2779           meta_new->days1 = meta_new->days2 = -1;
2780       } else {
2781           *meta_new = *(s->meta);
2782       }
2783       meta_new->ref_count = 0;
2784       s->meta = meta_new;
2785   }
2786}
2787
2788static int
2789read_year(filepos *fp_date_ptr)
2790{
2791    int y = read_uint_raw(DIAG_ERR, /*Expecting date, found “%s”*/198, fp_date_ptr);
2792    if (y < 100) {
2793        /* Two digit year is 19xx. */
2794        y += 1900;
2795        filepos fp_save;
2796        get_pos(&fp_save);
2797        set_pos(fp_date_ptr);
2798        /* TRANSLATORS: %d will be replaced by the assumed year, e.g. 1918 */
2799        compile_diagnostic(DIAG_WARN|DIAG_UINT, /*Assuming 2 digit year is %d*/76, y);
2800        set_pos(&fp_save);
2801    } else if (y < 1900 || y > 2078) {
2802        set_pos(fp_date_ptr);
2803        compile_diagnostic(DIAG_WARN|DIAG_UINT, /*Invalid year (< 1900 or > 2078)*/58);
2804        longjmp(jbSkipLine, 1);
2805    }
2806    return y;
2807}
2808
2809static void
2810cmd_date(void)
2811{
2812    enum { DATE_SURVEYED = 1, DATE_EXPLORED = 2 };
2813    unsigned date_flags = 0;
2814    while (get_token(), !s_empty(&uctoken)) {
2815        unsigned new_flag = 0;
2816        if (S_EQ(&uctoken, "SURVEYED")) {
2817            new_flag = DATE_SURVEYED;
2818        } else if (S_EQ(&uctoken, "EXPLORED")) {
2819            new_flag = DATE_EXPLORED;
2820        } else {
2821            compile_diagnostic(DIAG_ERR|DIAG_TOKEN,
2822                               /*Expecting “%s” or “%s”*/103, "SURVEYED", "EXPLORED");
2823            continue;
2824        }
2825
2826        if ((date_flags & new_flag)) {
2827            compile_diagnostic(DIAG_ERR|DIAG_TOKEN,
2828                               /*Duplicate date type “%s”*/416, s_str(&token));
2829        }
2830        date_flags |= new_flag;
2831    }
2832
2833    int date_sep = '-';
2834    if (date_flags == 0) {
2835        // `*date` without qualification sets the surveyed date.
2836        date_flags = DATE_SURVEYED;
2837        // Also allow '.' for compatibility.
2838        date_sep = 0;
2839    }
2840
2841    filepos fp_date1, fp_date2, fp;
2842    get_pos(&fp_date1);
2843    int year = read_year(&fp_date1);
2844    int month = 0, day = 0, year2 = 0, month2 = 0, day2 = 0;
2845
2846    if (ch == '-') {
2847        // Could be ISO-format date (e.g. 2024-10 or 2024-10-21 or 1912-11), or
2848        // a range of old-format dates (e.g. 1973-1975 or 1911-12.02.03) or an
2849        // ambiguous case like 1911-12 which we need to warn about in a `*date`
2850        // command without `surveyed` or `explored` qualifiers.
2851        nextch();
2852        get_pos(&fp_date2);
2853        int v = read_uint_raw(DIAG_ERR, /*Expecting date, found “%s”*/198, &fp_date1);
2854        if (date_sep == '-') {
2855            // We're only accepting ISO dates.
2856        } else if (ch == '-') {
2857            // Two `-` so must be an ISO date, e.g. `2024-10-21`.
2858        } else if (v >= 1 && v <= 12) {
2859            // Valid month number so assume it's an ISO date.
2860            if (year < 1900 + v) {
2861                // Warn about ambiguous cases.
2862                compile_diagnostic(DIAG_WARN|DIAG_FROM(fp_date1),
2863                                   /*Interpreting as an ISO-format date - use “*date surveyed %d-%02d” to suppress this warning, or “*date %d %d” if you wanted a date range*/158,
2864                                   year, v, year, 1900 + v);
2865            }
2866        } else {
2867            date_sep = '.';
2868            year2 = v;
2869            if (year2 < 100) {
2870                /* Two digit year is 19xx. */
2871                year2 += 1900;
2872                /* TRANSLATORS: %d will be replaced by the assumed year, e.g. 1918 */
2873                compile_diagnostic(DIAG_WARN|DIAG_FROM(fp_date2),
2874                                   /*Assuming 2 digit year is %d*/76, year2);
2875            } else if (year2 < 1900 || year2 > 2078) {
2876                compile_diagnostic(DIAG_WARN|DIAG_FROM(fp_date2),
2877                                   /*Invalid year (< 1900 or > 2078)*/58);
2878                longjmp(jbSkipLine, 1);
2879            }
2880            goto process_dates;
2881        }
2882
2883        date_sep = '-';
2884        month = v;
2885        fp = fp_date2;
2886    } else if (ch == '.') {
2887        if (date_sep == '-') {
2888            char date_sep_string[2] = { date_sep, '\0' };
2889            compile_diagnostic(DIAG_ERR|DIAG_COL|DIAG_SKIP,
2890                               /*Expecting “%s”*/497, date_sep_string);
2891            return;
2892        }
2893        date_sep = ch;
2894        nextch();
2895        get_pos(&fp);
2896        month = read_uint_raw(DIAG_ERR, /*Expecting date, found “%s”*/198, &fp_date1);
2897    } else {
2898        // Just a year - might be a ISO date range though.
2899        date_sep = '-';
2900        goto try_date2;
2901    }
2902
2903    if (month < 1 || month > 12) {
2904        set_pos(&fp);
2905        compile_diagnostic(DIAG_WARN|DIAG_UINT, /*Invalid month*/86);
2906        longjmp(jbSkipLine, 1);
2907    }
2908
2909    if (ch == date_sep) {
2910        nextch();
2911        get_pos(&fp);
2912        day = read_uint_raw(DIAG_ERR, /*Expecting date, found “%s”*/198, &fp_date1);
2913        if (day < 1 || day > last_day(year, month)) {
2914            set_pos(&fp);
2915            /* TRANSLATORS: e.g. 31st of April, or 32nd of any month */
2916            compile_diagnostic(DIAG_WARN|DIAG_UINT,
2917                               /*Invalid day of the month*/87);
2918            longjmp(jbSkipLine, 1);
2919        }
2920    }
2921
2922try_date2:
2923    if (date_sep == '-') {
2924        skipblanks();
2925        if (!isdigit(ch)) goto process_dates;
2926    } else if (ch == '-') {
2927        nextch();
2928    } else {
2929        goto process_dates;
2930    }
2931    {
2932        get_pos(&fp_date2);
2933        year2 = read_year(&fp_date2);
2934        if (ch == date_sep) {
2935            nextch();
2936            get_pos(&fp);
2937            month2 = read_uint_raw(DIAG_ERR, /*Expecting date, found “%s”*/198,
2938                                   &fp_date2);
2939            if (month2 < 1 || month2 > 12) {
2940                set_pos(&fp);
2941                compile_diagnostic(DIAG_WARN|DIAG_UINT, /*Invalid month*/86);
2942                longjmp(jbSkipLine, 1);
2943            }
2944
2945            if (ch == date_sep) {
2946                nextch();
2947                get_pos(&fp);
2948                day2 = read_uint_raw(DIAG_ERR, /*Expecting date, found “%s”*/198,
2949                                     &fp_date2);
2950                if (day2 < 1 || day2 > last_day(year2, month2)) {
2951                    set_pos(&fp);
2952                    /* TRANSLATORS: e.g. 31st of April, or 32nd of any month */
2953                    compile_diagnostic(DIAG_WARN|DIAG_UINT,
2954                                       /*Invalid day of the month*/87);
2955                    longjmp(jbSkipLine, 1);
2956                }
2957            }
2958        }
2959    }
2960
2961process_dates:;
2962    bool date_range = (year2 != 0);
2963    if (!date_range) {
2964        year2 = year;
2965        month2 = month;
2966        day2 = day;
2967    }
2968
2969    int days1 = days_since_1900(year, month ? month : 1, day ? day : 1);
2970
2971    if (days1 > current_days_since_1900) {
2972        filepos fp_save;
2973        get_pos(&fp_save);
2974        set_pos(&fp_date1);
2975        compile_diagnostic(DIAG_WARN|DIAG_DATE, /*Date is in the future!*/80);
2976        set_pos(&fp_save);
2977    }
2978
2979    if (month2 == 0) {
2980        month2 = 12;
2981        day2 = 31;
2982    } else if (day2 == 0) {
2983        day2 = last_day(year2, month2);
2984    }
2985    int days2 = days_since_1900(year2, month2, day2);
2986    if (date_range && days2 > current_days_since_1900) {
2987        // If !date_range, either we already emitted this warning when
2988        // processing the start date, or the date is partial and for the
2989        // current year or current month, in which case it's not helpful
2990        // to warn that the end is on the future, but it makes sense to
2991        // process for the end date so the result doesn't change as time
2992        // passes.
2993        filepos fp_save;
2994        get_pos(&fp_save);
2995        set_pos(&fp_date2);
2996        compile_diagnostic(DIAG_WARN|DIAG_DATE, /*Date is in the future!*/80);
2997        set_pos(&fp_save);
2998    }
2999
3000    if (days2 < days1) {
3001        filepos fp_save;
3002        get_pos(&fp_save);
3003        set_pos(&fp_date1);
3004        compile_diagnostic(DIAG_ERR|DIAG_WORD, /*End of date range is before the start*/81);
3005        set_pos(&fp_save);
3006        // Swap range ends to put us in a consistent state.
3007        int tmp = days1;
3008        days1 = days2;
3009        days2 = tmp;
3010    }
3011
3012    if ((date_flags & DATE_SURVEYED)) {
3013        if (!pcs->meta || pcs->meta->days1 != days1 || pcs->meta->days2 != days2) {
3014            copy_on_write_meta(pcs);
3015            pcs->meta->days1 = days1;
3016            pcs->meta->days2 = days2;
3017            /* Invalidate cached declination. */
3018            pcs->declination = HUGE_REAL;
3019        }
3020    }
3021
3022    if ((date_flags & DATE_EXPLORED)) {
3023        // FIXME: Need to revise 3d format to allow storing EXPLORED date too.
3024    }
3025}
3026
3027typedef void (*cmd_fn)(void);
3028
3029static const cmd_fn cmd_funcs[] = {
3030   cmd_alias,
3031   cmd_begin,
3032   cmd_calibrate,
3033   cmd_cartesian,
3034   cmd_case,
3035   cmd_copyright,
3036   cmd_cs,
3037   cmd_data,
3038   cmd_date,
3039   cmd_declination,
3040#ifndef NO_DEPRECATED
3041   cmd_default,
3042#endif
3043   cmd_end,
3044   cmd_entrance,
3045   cmd_equate,
3046   cmd_export,
3047   cmd_fix,
3048   cmd_flags,
3049   cmd_include,
3050   cmd_infer,
3051   skipline, /*cmd_instrument,*/
3052#ifndef NO_DEPRECATED
3053   cmd_prefix,
3054#endif
3055   cmd_ref,
3056   cmd_require,
3057   cmd_sd,
3058   cmd_set,
3059   solve_network,
3060   cmd_team,
3061   cmd_title,
3062   cmd_truncate,
3063   cmd_units
3064};
3065
3066extern void
3067handle_command(void)
3068{
3069   filepos fp;
3070   get_pos(&fp);
3071   get_token_legacy();
3072   int cmdtok = match_tok(cmd_tab, TABSIZE(cmd_tab));
3073   if (cmdtok < 0 || cmdtok >= (int)(sizeof(cmd_funcs) / sizeof(cmd_fn))) {
3074      set_pos(&fp);
3075      get_token();
3076      compile_diagnostic(DIAG_ERR|DIAG_TOKEN|DIAG_SKIP, /*Unknown command “%s”*/12, s_str(&token));
3077      return;
3078   }
3079
3080   do_legacy_token_warning();
3081
3082   switch (cmdtok) {
3083    case CMD_EXPORT:
3084      if (!f_export_ok)
3085         /* TRANSLATORS: The *EXPORT command is only valid just after *BEGIN
3086          * <SURVEY>, so this would generate this error:
3087          *
3088          * *begin fred
3089          * 1 2 1.23 045 -6
3090          * *export 2
3091          * *end fred */
3092         compile_diagnostic(DIAG_ERR, /**EXPORT must immediately follow “*BEGIN <SURVEY>”*/57);
3093      break;
3094    case CMD_ALIAS:
3095    case CMD_CALIBRATE:
3096    case CMD_CARTESIAN:
3097    case CMD_CASE:
3098    case CMD_COPYRIGHT:
3099    case CMD_CS:
3100    case CMD_DATA:
3101    case CMD_DATE:
3102    case CMD_DECLINATION:
3103    case CMD_FLAGS:
3104    case CMD_INFER:
3105    case CMD_INSTRUMENT:
3106    case CMD_REF:
3107    case CMD_REQUIRE:
3108    case CMD_SD:
3109    case CMD_SET:
3110    case CMD_TEAM:
3111    case CMD_TITLE:
3112    case CMD_TRUNCATE:
3113    case CMD_UNITS:
3114      /* These can all occur between *begin and *export */
3115      break;
3116    case CMD_DEFAULT: {
3117      // Issue warning here before we skipblanks().
3118      static int default_depr_count = 0;
3119      if (default_depr_count < 5) {
3120          /* TRANSLATORS: If you're unsure what "deprecated" means, see:
3121           * https://en.wikipedia.org/wiki/Deprecation */
3122          compile_diagnostic(DIAG_WARN|DIAG_TOKEN, /**DEFAULT is deprecated - use *CALIBRATE/DATA/SD/UNITS with argument DEFAULT instead*/20);
3123          if (++default_depr_count == 5)
3124              compile_diagnostic(DIAG_INFO, /*Further uses of this deprecated feature will not be reported*/95);
3125      }
3126      /* Can occur between *begin and *export */
3127      break;
3128    }
3129    case CMD_PREFIX: {
3130      // Issue warning here before we skipblanks().
3131      static int prefix_depr_count = 0;
3132      if (prefix_depr_count < 5) {
3133          /* TRANSLATORS: If you're unsure what "deprecated" means, see:
3134           * https://en.wikipedia.org/wiki/Deprecation */
3135          compile_diagnostic(DIAG_WARN|DIAG_TOKEN, /**prefix is deprecated - use *begin and *end instead*/109);
3136          if (++prefix_depr_count == 5)
3137              compile_diagnostic(DIAG_INFO, /*Further uses of this deprecated feature will not be reported*/95);
3138      }
3139      f_export_ok = false;
3140      break;
3141    }
3142    default:
3143      /* NB: additional handling for "*begin <survey>" in cmd_begin */
3144      f_export_ok = false;
3145      break;
3146   }
3147
3148   skipblanks();
3149
3150   cmd_funcs[cmdtok]();
3151}
Note: See TracBrowser for help on using the repository browser.