Ticket #150: 0001-cavern-Allow-cs-custom-to-read-the-CRS-from-a-file.patch

File 0001-cavern-Allow-cs-custom-to-read-the-CRS-from-a-file.patch, 19.1 KB (added by Philip Schuchardt, 3 days ago)
  • doc/3dformat.htm

    From 1e000e26e383837035b56e65219c7221ab3c29c9 Mon Sep 17 00:00:00 2001
    From: Philip Schuchardt <vpicaver@gmail.com>
    Date: Tue, 11 Aug 2026 19:55:12 -0500
    Subject: [PATCH] cavern: Allow *cs custom to read the CRS from a file
    
    `*cs custom @FILENAME` reads the coordinate system description from a
    separate file.  This makes it possible to specify a coordinate system
    as WKT or PROJJSON, which the .svx file itself can't carry because a
    quoted string has no way to represent the double quotes those formats
    are made of, and because they're usually written over several lines.
    ---
     doc/3dformat.htm   |   9 +++-
     doc/datafile.rst   |  35 +++++++++++++-
     src/commands.c     | 115 ++++++++++++++++++++++++++++++++++++++++++++-
     src/filelist.h     |   1 +
     tests/Makefile.am  |   3 ++
     tests/cavern.tst   |   3 +-
     tests/csprj.out    |  24 ++++++++++
     tests/csprj.pos    |   4 ++
     tests/csprj.prj    |  33 +++++++++++++
     tests/csprj.svx    |  11 +++++
     tests/csprj2.out   |  24 ++++++++++
     tests/csprj2.pos   |   4 ++
     tests/csprj2.prj   |  33 +++++++++++++
     tests/csprj2.svx   |  11 +++++
     tests/csprjbad.out |  30 ++++++++++++
     tests/csprjbad.svx |   8 ++++
     16 files changed, 343 insertions(+), 5 deletions(-)
     create mode 100644 tests/csprj.out
     create mode 100644 tests/csprj.pos
     create mode 100644 tests/csprj.prj
     create mode 100644 tests/csprj.svx
     create mode 100644 tests/csprj2.out
     create mode 100644 tests/csprj2.pos
     create mode 100644 tests/csprj2.prj
     create mode 100644 tests/csprj2.svx
     create mode 100644 tests/csprjbad.out
     create mode 100644 tests/csprjbad.svx
    
    diff --git a/doc/3dformat.htm b/doc/3dformat.htm
    index b6e8ab42..17546c91 100644
    a b There's no length limit on this string.  
    6060which can be passed to PROJ.  For a coordinate system with an EPSG code
    6161<code>EPSG:</code> followed by the code number can be used (we recommend
    6262using this if an EPSG code exists).  Similarly, an ESRI code can be specified
    63 with <code>ESRI:</code> followed by the code number.
     63with <code>ESRI:</code> followed by the code number.  Any other description
     64PROJ accepts may be used, such as a PROJ string, WKT or PROJJSON - this is how
     65a coordinate system based on a datum PROJ has no <code>+datum=</code> keyword
     66for gets recorded.  Since the metadata ends at a linefeed, the string must be
     67written on a single line - WKT and PROJJSON are usually pretty printed over
     68several lines, so a writer needs to join those lines (a single space in place
     69of each line break works, as neither format allows a newline inside a quoted
     70name).
    6471  <li>Survey hierarchy separator character.  Survey station names form a
    6572hierarchy, and this character separates levels in the hierarchy.  E.g.
    6673<code>161.entrance.6</code>.  Defaults to <code>.</code> if this item is not
  • doc/datafile.rst

    diff --git a/doc/datafile.rst b/doc/datafile.rst
    index 57952747..45b6b06e 100644
    a b Example  
    501501       ; Output in the coordinate system used in the Totes Gebirge in Austria
    502502       *cs out custom "+proj=tmerc +lat_0=0 +lon_0=13d20 +k=1 +x_0=0 +y_0=-5200000 +ellps=bessel +towgs84=577.326,90.129,463.919,5.137,1.474,5.297,2.4232"
    503503
     504   ::
     505
     506       ; Output in a low distortion projection described by mammoth.prj
     507       *cs out custom @mammoth.prj
     508
    504509Description
    505510   ``*cs`` allows the coordinate systems used for fixed points and for
    506511   processed survey data to be specified.
    Description  
    529534     resource for finding the EPSG code you want.  For example, ``EPSG:4167``
    530535     is NZGD2000.  Supported since Survex 1.2.15.
    531536
    532    * ``CUSTOM`` followed by a PROJ string (like in the example above).
     537   * ``CUSTOM`` followed by a PROJ string, or by ``@`` and the name of a file
     538     containing a description of the coordinate system - the last two examples
     539     above show each of these in turn.
     540
     541     Such a file may contain a PROJ string, but the point of it is that it can
     542     also hold WKT or PROJJSON, which are made up of double quoted strings
     543     written over several lines and so are awkward to write in a ``.svx``
     544     file.  This means a ``.prj`` file such as those which accompany ESRI
     545     shapefiles can be used directly, so you can georeference a survey to match
     546     GIS data you already have.
     547
     548     It's worth using WKT when the datum you need is one PROJ has no
     549     ``+datum=`` keyword for, which is the case for most datums in current use
     550     (such as NAD83(2011), ETRS89 and GDA2020).  A PROJ string can only express
     551     a datum as an ellipsoid plus a static shift, so PROJ quietly drops such a
     552     datum when it converts a coordinate system to a PROJ string.
     553
     554     The filename is relative to the directory containing the file the ``*cs``
     555     command is in, and an extension of ``.prj`` is assumed if the name as
     556     given doesn't exist.  Use double quotes around the name if it contains
     557     spaces - the ``@`` may go either side of the opening quote, so both
     558     ``@"My Cave.prj"`` and ``"@My Cave.prj"`` work.
     559
     560     The lines of the file are joined together with a single space, since the
     561     coordinate system is stored in the ``.3d`` file as a single line.  This
     562     only affects whitespace which the formats treat as insignificant, because
     563     neither WKT nor PROJJSON allows a newline inside a quoted name.
     564
     565     Reading the coordinate system from a file was added in Survex 1.4.23.
    533566
    534567   * ``ESRI:`` followed by a positive integer code.  ESRI codes are used by
    535568     ArcGIS to specify coordinate systems (in a similar way to EPSG codes)
  • src/commands.c

    diff --git a/src/commands.c b/src/commands.c
    index 5834a3d2..f826f0bf 100644
    a b  
    3333#include "datain.h"
    3434#include "date.h"
    3535#include "debug.h"
     36#include "filelist.h"
    3637#include "filename.h"
    3738#include "message.h"
    3839#include "netbits.h"
    static const sztok cs_tab[] = {  
    24862487     {NULL,       CS_NONE}
    24872488};
    24882489
     2490/* Read a coordinate system from the file FNM, as specified by
     2491 * `*cs custom @FILENAME`.  FP is the position of FILENAME in the current file,
     2492 * which any diagnostic is reported against.
     2493 *
     2494 * WKT and PROJJSON are made up of double quoted strings and are usually
     2495 * written over several lines, which makes them awkward to write in a .svx
     2496 * file.  Keeping such a description in its own file also means a .prj file
     2497 * such as those which accompany ESRI shapefiles can be used directly.
     2498 *
     2499 * Returns the coordinate system description, or NULL if the file couldn't be
     2500 * opened (in which case a diagnostic has been reported).
     2501 */
     2502static char *
     2503read_cs_from_file(const char *fnm, const filepos *fp)
     2504{
     2505   char *pth = path_from_fnm(file.filename);
     2506   char *fnm_used = NULL;
     2507   FILE *fh = fopen_portable(pth, fnm, EXT_PRJ, "rb", &fnm_used);
     2508   free(pth);
     2509   if (fh == NULL) {
     2510      set_pos(fp);
     2511      compile_diagnostic(DIAG_ERR|DIAG_STRING, /*Couldn’t open file “%s”*/1,
     2512                         fnm);
     2513      return NULL;
     2514   }
     2515
     2516   int c = GETC(fh);
     2517   if (c == 0xef) {
     2518      /* Skip a UTF-8 "BOM" if there is one - PROJ rejects a description which
     2519       * starts with one. */
     2520      if (GETC(fh) == 0xbb && GETC(fh) == 0xbf) {
     2521         c = GETC(fh);
     2522      } else {
     2523         rewind(fh);
     2524         c = GETC(fh);
     2525      }
     2526   }
     2527
     2528   /* We store the coordinate system in the .3d file as part of a
     2529    * newline-terminated line, so it can't contain a newline.  Join the lines
     2530    * with a single space, dropping blanks at the start and end of each line.
     2531    * Neither WKT nor PROJJSON allows a newline inside a quoted name, so only
     2532    * insignificant whitespace is affected.
     2533    */
     2534   /* The s_clear() calls give each string a buffer, which for an empty file it
     2535    * would otherwise still lack by the time we use it - s_steal() writes the
     2536    * terminating zero byte to one, and s_appends() reads from one.
     2537    */
     2538   string cs = S_INIT;
     2539   s_clear(&cs);
     2540   string blanks = S_INIT;
     2541   s_clear(&blanks);
     2542   bool line_break = false;
     2543   for ( ; c != EOF; c = GETC(fh)) {
     2544      if (c == '\n' || c == '\r') {
     2545         line_break = true;
     2546         s_clear(&blanks);
     2547         continue;
     2548      }
     2549      if (c == ' ' || c == '\t') {
     2550         /* Only keep blanks which turn out to be between two non-blanks on
     2551          * the same line. */
     2552         if (cs.len) s_appendch(&blanks, c);
     2553         continue;
     2554      }
     2555      if (cs.len) {
     2556         if (line_break) {
     2557            s_appendch(&cs, ' ');
     2558         } else {
     2559            s_appends(&cs, &blanks);
     2560         }
     2561      }
     2562      s_clear(&blanks);
     2563      line_break = false;
     2564      s_appendch(&cs, c);
     2565   }
     2566   s_free(&blanks);
     2567
     2568   if (FERROR(fh))
     2569      fatalerror_in_file(fnm_used, 0, /*Error reading file*/18);
     2570   fclose(fh);
     2571   free(fnm_used);
     2572
     2573   return s_steal(&cs);
     2574}
     2575
    24892576static void
    24902577cmd_cs(void)
    24912578{
    cmd_cs(void)  
    25452632       switch (cs) {
    25462633         case CS_NONE:
    25472634           break;
    2548          case CS_CUSTOM:
     2635         case CS_CUSTOM: {
    25492636           ok_for_output = MAYBE;
     2637           skipblanks();
     2638           /* `@FILENAME` reads the coordinate system from a file.  If FILENAME
     2639            * is quoted then the `@` may be written either side of the opening
     2640            * quote.
     2641            */
     2642           bool from_file = (ch == '@');
     2643           if (from_file) nextch();
    25502644           get_pos(&fp);
    25512645           string str = S_INIT;
    25522646           read_string(&str);
    2553            proj_str = s_steal(&str);
     2647           const char *p = s_str(&str);
     2648           if (!from_file && *p == '@') {
     2649              from_file = true;
     2650              ++p;
     2651           }
     2652           if (from_file) {
     2653              proj_str = read_cs_from_file(p, &fp);
     2654              s_free(&str);
     2655              if (!proj_str) {
     2656                 skipline();
     2657                 return;
     2658              }
     2659           } else {
     2660              proj_str = s_steal(&str);
     2661           }
    25542662           cs_sub = 0;
    25552663           break;
     2664         }
    25562665         case CS_EPSG: case CS_ESRI:
    25572666           ok_for_output = MAYBE;
    25582667           if (ch == ':' && isdigit(nextch())) {
    cmd_cs(void)  
    27662875         /* Same as the current output projection, so valid for input. */
    27672876      } else if (pcs->proj_str && strcmp(proj_str, pcs->proj_str) == 0) {
    27682877         /* Same as the current input projection, so nothing to do! */
     2878         free(proj_str);
    27692879         return;
    27702880      } else if (ok_for_output == MAYBE) {
    27712881         /* (ok_for_output == MAYBE) also happens to indicate whether we need
    cmd_cs(void)  
    27792889                               proj_context_errno_string(PJ_DEFAULT_CTX,
    27802890                                                         proj_context_errno(PJ_DEFAULT_CTX)));
    27812891            skipline();
     2892            free(proj_str);
    27822893            return;
    27832894         }
    27842895         proj_destroy(pj);
  • src/filelist.h

    diff --git a/src/filelist.h b/src/filelist.h
    index 215cbbff..694d5d78 100644
    a b  
    2424#define EXT_SVX_MSG  "msg"
    2525#define EXT_INI      "ini"
    2626#define EXT_LOG      "log"
     27#define EXT_PRJ      "prj"
  • tests/Makefile.am

    diff --git a/tests/Makefile.am b/tests/Makefile.am
    index 0fef3790..2f9aeb92 100644
    a b csbad.altout csbad.out csbad.svx\  
    177177csbadsdfix.altout csbadsdfix.out csbadsdfix.svx\
    178178csfeet.out csfeet.pos csfeet.svx\
    179179cslonglat.out cslonglat.svx\
     180csprj.out csprj.pos csprj.prj csprj.svx\
     181csprj2.out csprj2.pos csprj2.prj csprj2.svx\
     182csprjbad.out csprjbad.svx\
    180183omitfixaroundsolve.out omitfixaroundsolve.svx\
    181184repeatreading.svx repeatreading.out repeatreading.pos\
    182185mixedeols.out mixedeols.svx\
  • tests/cavern.tst

    diff --git a/tests/cavern.tst b/tests/cavern.tst
    index 4b1d228b..5d2882bb 100755
    a b TESTS_=  
    120120 passage hanging_lrud equatenosuchstn surveytypo\
    121121 skipafterbadomit passagebad badreadingdotplus badcalibrate calibrate_clino\
    122122 badunits badbegin anonstn anonstnbad anonstnrev doubleinc reenterlots\
    123  cs csbad csbadsdfix csfeet cslonglat omitfixaroundsolve repeatreading\
     123 cs csbad csbadsdfix csfeet cslonglat csprj csprj2 csprjbad\
     124 omitfixaroundsolve repeatreading\
    124125 mixedeols utf8bom nonewlineateof suspectreadings cmd_data_default\
    125126 cmd_data_ignore\
    126127 quadrant_bearing bad_quadrant_bearing\
  • new file tests/csprj.out

    diff --git a/tests/csprj.out b/tests/csprj.out
    new file mode 100644
    index 00000000..c4cb8d94
    - +  
     1
     2Removing trailing traverses...
     3
     4Concatenating traverses...
     5
     6Simplifying network...
     7
     8Calculating network...
     9
     10Calculating traverses...
     11
     12Calculating trailing traverses...
     13
     14Calculating statistics...
     15
     16Approximate full range of grid convergence: 0.0dg at entrance to 0.0dg at b
     17Survey contains 3 survey stations, joined by 2 legs.
     18There are 0 loops.
     19Total length of survey legs =   55.00m (  55.00m adjusted)
     20Total plan length of survey legs =   54.87m
     21Total vertical length of survey legs =    3.49m
     22Vertical range = 2.61m (from entrance at 200.00m to a at 197.39m)
     23North-South range = 17.67m (from a at 1109.86m to b at 1092.19m)
     24East-West range = 47.55m (from b at 935.37m to entrance at 887.82m)
  • new file tests/csprj.pos

    diff --git a/tests/csprj.pos b/tests/csprj.pos
    new file mode 100644
    index 00000000..b2af0f79
    - +  
     1( Easting, Northing, Altitude )
     2(  917.71,  1109.86,   197.39 ) a
     3(  935.37,  1092.19,   198.26 ) b
     4(  887.82,  1109.86,   200.00 ) entrance
  • new file tests/csprj.prj

    diff --git a/tests/csprj.prj b/tests/csprj.prj
    new file mode 100644
    index 00000000..bb40c2e4
    - +  
     1PROJCRS["Mammoth Cave LDP",
     2    BASEGEOGCRS["NAD83(2011)",
     3        DATUM["NAD83 (National Spatial Reference System 2011)",
     4            ELLIPSOID["GRS 1980",6378137,298.257222101,
     5                LENGTHUNIT["metre",1]]],
     6        PRIMEM["Greenwich",0,
     7            ANGLEUNIT["degree",0.0174532925199433]],
     8        ID["EPSG",6318]],
     9    CONVERSION["Transverse Mercator",
     10        METHOD["Transverse Mercator",
     11            ID["EPSG",9807]],
     12        PARAMETER["Latitude of natural origin",37.1866,
     13            ANGLEUNIT["degree",0.0174532925199433],
     14            ID["EPSG",8801]],
     15        PARAMETER["Longitude of natural origin",-86.1005,
     16            ANGLEUNIT["degree",0.0174532925199433],
     17            ID["EPSG",8802]],
     18        PARAMETER["Scale factor at natural origin",1,
     19            SCALEUNIT["unity",1],
     20            ID["EPSG",8805]],
     21        PARAMETER["False easting",0,
     22            LENGTHUNIT["metre",1],
     23            ID["EPSG",8806]],
     24        PARAMETER["False northing",0,
     25            LENGTHUNIT["metre",1],
     26            ID["EPSG",8807]]],
     27    CS[Cartesian,2],
     28    AXIS["(E)",east,
     29        ORDER[1],
     30        LENGTHUNIT["metre",1]],
     31    AXIS["(N)",north,
     32        ORDER[2],
     33        LENGTHUNIT["metre",1]]]
  • new file tests/csprj.svx

    diff --git a/tests/csprj.svx b/tests/csprj.svx
    new file mode 100644
    index 00000000..a7cccb4b
    - +  
     1; pos=yes warn=0
     2; Test *cs custom @FILENAME, which reads the coordinate system from a separate
     3; file.  Here that's a low distortion projection for a cave, written as WKT
     4; over several lines - a PROJ string can't express the datum it's based on.
     5*cs EPSG:6318
     6*cs out CUSTOM @csprj.prj
     7
     8*fix entrance -86.0905 37.1966 200
     9
     10entrance a 30.0 090 -05
     11a        b 25.0 135 +02
  • new file tests/csprj2.out

    diff --git a/tests/csprj2.out b/tests/csprj2.out
    new file mode 100644
    index 00000000..cac0d37f
    - +  
     1
     2Removing trailing traverses...
     3
     4Concatenating traverses...
     5
     6Simplifying network...
     7
     8Calculating network...
     9
     10Calculating traverses...
     11
     12Calculating trailing traverses...
     13
     14Calculating statistics...
     15
     16Approximate full range of grid convergence: 0.0dg at entrance to 0.0dg at b
     17Survey contains 3 survey stations, joined by 2 legs.
     18There are 0 loops.
     19Total length of survey legs =   55.00m (  55.00m adjusted)
     20Total plan length of survey legs =   54.87m
     21Total vertical length of survey legs =    3.49m
     22Vertical range = 2.61m (from entrance at 200.00m to a at 197.39m)
     23North-South range = 17.67m (from a at 41819.44m to b at 41801.78m)
     24East-West range = 47.55m (from b at 10891.01m to entrance at 10843.46m)
  • new file tests/csprj2.pos

    diff --git a/tests/csprj2.pos b/tests/csprj2.pos
    new file mode 100644
    index 00000000..7fb569af
    - +  
     1( Easting, Northing, Altitude )
     2( 10873.35, 41819.44,   197.39 ) a
     3( 10891.01, 41801.78,   198.26 ) b
     4( 10843.46, 41819.44,   200.00 ) entrance
  • new file tests/csprj2.prj

    diff --git a/tests/csprj2.prj b/tests/csprj2.prj
    new file mode 100644
    index 00000000..df9d4f2a
    - +  
     1PROJCRS["Mammoth Cave LDP",
     2    BASEGEOGCRS["NAD83(2011)",
     3        DATUM["NAD83 (National Spatial Reference System 2011)",
     4            ELLIPSOID["GRS 1980",6378137,298.257222101,
     5                LENGTHUNIT["metre",1]]],
     6        PRIMEM["Greenwich",0,
     7            ANGLEUNIT["degree",0.0174532925199433]],
     8        ID["EPSG",6318]],
     9    CONVERSION["Mammoth Cave LDP TM",
     10        METHOD["Transverse Mercator",
     11            ID["EPSG",9807]],
     12        PARAMETER["Latitude of natural origin",37,
     13            ANGLEUNIT["degree",0.0174532925199433],
     14            ID["EPSG",8801]],
     15        PARAMETER["Longitude of natural origin",-86.1,
     16            ANGLEUNIT["degree",0.0174532925199433],
     17            ID["EPSG",8802]],
     18        PARAMETER["Scale factor at natural origin",1.0000383,
     19            SCALEUNIT["unity",1],
     20            ID["EPSG",8805]],
     21        PARAMETER["False easting",10000,
     22            LENGTHUNIT["metre",1],
     23            ID["EPSG",8806]],
     24        PARAMETER["False northing",20000,
     25            LENGTHUNIT["metre",1],
     26            ID["EPSG",8807]]],
     27    CS[Cartesian,2],
     28        AXIS["easting (E)",east,
     29            ORDER[1],
     30            LENGTHUNIT["metre",1]],
     31        AXIS["northing (N)",north,
     32            ORDER[2],
     33            LENGTHUNIT["metre",1]]]
  • new file tests/csprj2.svx

    diff --git a/tests/csprj2.svx b/tests/csprj2.svx
    new file mode 100644
    index 00000000..9ae0d094
    - +  
     1; pos=yes warn=0
     2; Test that *cs custom @FILENAME assumes a .prj extension if the name as given
     3; doesn't exist, and that a UTF-8 "BOM" at the start of the file is skipped.
     4; The coordinate system is quoted here too, with the `@` inside the quotes.
     5*cs EPSG:6318
     6*cs out CUSTOM "@csprj2"
     7
     8*fix entrance -86.0905 37.1966 200
     9
     10entrance a 30.0 090 -05
     11a        b 25.0 135 +02
  • new file tests/csprjbad.out

    diff --git a/tests/csprjbad.out b/tests/csprjbad.out
    new file mode 100644
    index 00000000..e3aa2292
    - +  
     1./csprjbad.svx:4:17: error: Couldn't open file "nosuchfile.prj"
     2 *cs out CUSTOM @nosuchfile.prj
     3                 ^~~~~~~~~~~~~~
     4./csprjbad.svx:6: error: The input projection is set but the output projection isn't
     5 *fix entrance -86.0905 37.1966 200
     6
     7Removing trailing traverses...
     8
     9Concatenating traverses...
     10
     11Simplifying network...
     12
     13Calculating network...
     14
     15Calculating traverses...
     16
     17Calculating trailing traverses...
     18
     19Calculating statistics...
     20
     21Survey contains 2 survey stations, joined by 1 leg.
     22There are 0 loops.
     23Total length of survey legs =   30.00m (  30.00m adjusted)
     24Total plan length of survey legs =   29.89m
     25Total vertical length of survey legs =    2.61m
     26Vertical range = 2.61m (from entrance at 200.00m to a at 197.39m)
     27North-South range = 0.00m (from a at 37.20m to a at 37.20m)
     28East-West range = 29.89m (from a at -56.20m to entrance at -86.09m)
     29
     30There were 0 warning(s) and 2 error(s) - no output files produced.
  • new file tests/csprjbad.svx

    diff --git a/tests/csprjbad.svx b/tests/csprjbad.svx
    new file mode 100644
    index 00000000..8491a7fa
    - +  
     1; pos=fail error=2
     2; Test the error from *cs custom @FILENAME naming a file which isn't there.
     3*cs EPSG:6318
     4*cs out CUSTOM @nosuchfile.prj
     5
     6*fix entrance -86.0905 37.1966 200
     7
     8entrance a 30.0 090 -05