Coverage Report

Created: 2026-03-12 06:58

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/postgis/liblwgeom/lwprint.c
Line
Count
Source
1
/**********************************************************************
2
 *
3
 * PostGIS - Spatial Types for PostgreSQL
4
 * http://postgis.net
5
 *
6
 * PostGIS is free software: you can redistribute it and/or modify
7
 * it under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation, either version 2 of the License, or
9
 * (at your option) any later version.
10
 *
11
 * PostGIS is distributed in the hope that it will be useful,
12
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
 * GNU General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU General Public License
17
 * along with PostGIS.  If not, see <http://www.gnu.org/licenses/>.
18
 *
19
 **********************************************************************
20
 *
21
 * Copyright (C) 2010-2015 Paul Ramsey <pramsey@cleverelephant.ca>
22
 * Copyright (C) 2011 Sandro Santilli <strk@kbt.io>
23
 *
24
 **********************************************************************/
25
26
#include "liblwgeom_internal.h"
27
28
#include <assert.h>
29
#include <math.h>
30
#include <stdio.h>
31
#include <string.h>
32
33
#include "ryu/ryu.h"
34
35
/* Ensures the given lat and lon are in the "normal" range:
36
 * -90 to +90 for lat, -180 to +180 for lon. */
37
static void lwprint_normalize_latlon(double *lat, double *lon)
38
0
{
39
  /* First remove all the truly excessive trips around the world via up or down. */
40
0
  while (*lat > 270)
41
0
  {
42
0
    *lat -= 360;
43
0
  }
44
0
  while (*lat < -270)
45
0
  {
46
0
    *lat += 360;
47
0
  }
48
49
  /* Now see if latitude is past the top or bottom of the world.
50
   * Past 90  or -90 puts us on the other side of the earth,
51
       * so wrap latitude and add 180 to longitude to reflect that. */
52
0
  if (*lat > 90)
53
0
  {
54
0
    *lat = 180 - *lat;
55
0
    *lon += 180;
56
0
  }
57
0
  if (*lat < -90)
58
0
  {
59
0
    *lat = -180 - *lat;
60
0
    *lon += 180;
61
0
  }
62
  /* Now make sure lon is in the normal range.  Wrapping longitude
63
   * has no effect on latitude. */
64
0
  while (*lon > 180)
65
0
  {
66
0
    *lon -= 360;
67
0
  }
68
0
  while (*lon < -180)
69
0
  {
70
0
    *lon += 360;
71
0
  }
72
0
}
73
74
/* Converts a single double to DMS given the specified DMS format string.
75
 * Symbols are specified since N/S or E/W are the only differences when printing
76
 * lat vs. lon.  They are only used if the "C" (compass dir) token appears in the
77
 * format string.
78
 * NOTE: Format string and symbols are required to be in UTF-8. */
79
static char * lwdouble_to_dms(double val, const char *pos_dir_symbol, const char *neg_dir_symbol, const char * format)
80
0
{
81
  /* 3 numbers, 1 sign or compass dir, and 5 possible strings (degree signs, spaces, misc text, etc) between or around them.*/
82
0
# define NUM_PIECES 9
83
0
# define WORK_SIZE 1024
84
0
  char pieces[NUM_PIECES][WORK_SIZE];
85
0
  int current_piece = 0;
86
0
  int is_negative = 0;
87
88
0
  double degrees = 0.0;
89
0
  double minutes = 0.0;
90
0
  double seconds = 0.0;
91
92
0
  int compass_dir_piece = -1;
93
94
0
  int reading_deg = 0;
95
0
  int deg_digits = 0;
96
0
  int deg_has_decpoint = 0;
97
0
  int deg_dec_digits = 0;
98
0
  int deg_piece = -1;
99
100
0
  int reading_min = 0;
101
0
  int min_digits = 0;
102
0
  int min_has_decpoint = 0;
103
0
  int min_dec_digits = 0;
104
0
  int min_piece = -1;
105
106
0
  int reading_sec = 0;
107
0
  int sec_digits = 0;
108
0
  int sec_has_decpoint = 0;
109
0
  int sec_dec_digits = 0;
110
0
  int sec_piece = -1;
111
112
0
  int round_pow = 0;
113
114
0
  int format_length = ((NULL == format) ? 0 : strlen(format));
115
116
0
  char * result;
117
118
0
  int index, following_byte_index;
119
0
  int multibyte_char_width = 1;
120
121
  /* Initialize the working strs to blank.  We may not populate all of them, and
122
   * this allows us to concat them all at the end without worrying about how many
123
   * we actually needed. */
124
0
  for (index = 0; index < NUM_PIECES; index++)
125
0
  {
126
0
    pieces[index][0] = '\0';
127
0
  }
128
129
  /* If no format is provided, use a default. */
130
0
  if (0 == format_length)
131
0
  {
132
    /* C2B0 is UTF-8 for the degree symbol. */
133
0
    format = "D\xC2\xB0""M'S.SSS\"C";
134
0
    format_length = strlen(format);
135
0
  }
136
0
  else if (format_length > WORK_SIZE)
137
0
  {
138
    /* Sanity check, we don't want to overwrite an entire piece of work and no one should need a 1K-sized
139
    * format string anyway. */
140
0
    lwerror("Bad format, exceeds maximum length (%d).", WORK_SIZE);
141
0
  }
142
143
0
  for (index = 0; index < format_length; index++)
144
0
  {
145
0
    char next_char = format[index];
146
0
    switch (next_char)
147
0
    {
148
0
    case 'D':
149
0
      if (reading_deg)
150
0
      {
151
        /* If we're reading degrees, add another digit. */
152
0
        deg_has_decpoint ? deg_dec_digits++ : deg_digits++;
153
0
      }
154
0
      else
155
0
      {
156
        /* If we're not reading degrees, we are now. */
157
0
        current_piece++;
158
0
        deg_piece = current_piece;
159
0
        if (deg_digits > 0)
160
0
        {
161
0
          lwerror("Bad format, cannot include degrees (DD.DDD) more than once.");
162
0
        }
163
0
        reading_deg = 1;
164
0
        reading_min = 0;
165
0
        reading_sec = 0;
166
0
        deg_digits++;
167
0
      }
168
0
      break;
169
0
    case 'M':
170
0
      if (reading_min)
171
0
      {
172
        /* If we're reading minutes, add another digit. */
173
0
        min_has_decpoint ? min_dec_digits++ : min_digits++;
174
0
      }
175
0
      else
176
0
      {
177
        /* If we're not reading minutes, we are now. */
178
0
        current_piece++;
179
0
        min_piece = current_piece;
180
0
        if (min_digits > 0)
181
0
        {
182
0
          lwerror("Bad format, cannot include minutes (MM.MMM) more than once.");
183
0
        }
184
0
        reading_deg = 0;
185
0
        reading_min = 1;
186
0
        reading_sec = 0;
187
0
        min_digits++;
188
0
      }
189
0
      break;
190
0
    case 'S':
191
0
      if (reading_sec)
192
0
      {
193
        /* If we're reading seconds, add another digit. */
194
0
        sec_has_decpoint ? sec_dec_digits++ : sec_digits++;
195
0
      }
196
0
      else
197
0
      {
198
        /* If we're not reading seconds, we are now. */
199
0
        current_piece++;
200
0
        sec_piece = current_piece;
201
0
        if (sec_digits > 0)
202
0
        {
203
0
          lwerror("Bad format, cannot include seconds (SS.SSS) more than once.");
204
0
        }
205
0
        reading_deg = 0;
206
0
        reading_min = 0;
207
0
        reading_sec = 1;
208
0
        sec_digits++;
209
0
      }
210
0
      break;
211
0
    case 'C':
212
      /* We're done reading anything else we might have been reading. */
213
0
      if (reading_deg || reading_min || reading_sec)
214
0
      {
215
        /* We were reading something, that means this is the next piece. */
216
0
        reading_deg = 0;
217
0
        reading_min = 0;
218
0
        reading_sec = 0;
219
0
      }
220
0
      current_piece++;
221
222
0
      if (compass_dir_piece >= 0)
223
0
      {
224
0
        lwerror("Bad format, cannot include compass dir (C) more than once.");
225
0
      }
226
      /* The compass dir is a piece all by itself.  */
227
0
      compass_dir_piece = current_piece;
228
0
      current_piece++;
229
0
      break;
230
0
    case '.':
231
      /* If we're reading deg, min, or sec, we want a decimal point for it. */
232
0
      if (reading_deg)
233
0
      {
234
0
        deg_has_decpoint = 1;
235
0
      }
236
0
      else if (reading_min)
237
0
      {
238
0
        min_has_decpoint = 1;
239
0
      }
240
0
      else if (reading_sec)
241
0
      {
242
0
        sec_has_decpoint = 1;
243
0
      }
244
0
      else
245
0
      {
246
        /* Not reading anything, just pass through the '.' */
247
0
        strncat(pieces[current_piece], &next_char, 1);
248
0
      }
249
0
      break;
250
0
    default:
251
      /* Any other char is just passed through unchanged.  But it does mean we are done reading D, M, or S.*/
252
0
      if (reading_deg || reading_min || reading_sec)
253
0
      {
254
        /* We were reading something, that means this is the next piece. */
255
0
        current_piece++;
256
0
        reading_deg = 0;
257
0
        reading_min = 0;
258
0
        reading_sec = 0;
259
0
      }
260
261
      /* Check if this is a multi-byte UTF-8 character.  If so go ahead and read the rest of the bytes as well. */
262
0
      multibyte_char_width = 1;
263
0
      if (next_char & 0x80)
264
0
      {
265
0
        if ((next_char & 0xF8) == 0xF0)
266
0
        {
267
0
          multibyte_char_width += 3;
268
0
        }
269
0
        else if ((next_char & 0xF0) == 0xE0)
270
0
        {
271
0
          multibyte_char_width += 2;
272
0
        }
273
0
        else if ((next_char & 0xE0) == 0xC0)
274
0
        {
275
0
          multibyte_char_width += 1;
276
0
        }
277
0
        else
278
0
        {
279
0
          lwerror("Bad format, invalid high-order byte found first, format string may not be UTF-8.");
280
0
        }
281
0
      }
282
0
      if (multibyte_char_width > 1)
283
0
      {
284
0
        if (index + multibyte_char_width >= format_length)
285
0
        {
286
0
          lwerror("Bad format, UTF-8 character first byte found with insufficient following bytes, format string may not be UTF-8.");
287
0
        }
288
0
        for (following_byte_index = (index + 1); following_byte_index < (index + multibyte_char_width); following_byte_index++)
289
0
        {
290
0
          if ((format[following_byte_index] & 0xC0) != 0x80)
291
0
          {
292
0
            lwerror("Bad format, invalid byte found following leading byte of multibyte character, format string may not be UTF-8.");
293
0
          }
294
0
        }
295
0
      }
296
      /* Copy all the character's bytes into the current piece. */
297
0
      strncat(pieces[current_piece], &(format[index]), multibyte_char_width);
298
      /* Now increment index past the rest of those bytes. */
299
0
      index += multibyte_char_width - 1;
300
0
      break;
301
0
    }
302
0
    if (current_piece >= NUM_PIECES)
303
0
    {
304
0
      lwerror("Internal error, somehow needed more pieces than it should.");
305
0
    }
306
0
  }
307
0
  if (deg_piece < 0)
308
0
  {
309
0
    lwerror("Bad format, degrees (DD.DDD) must be included.");
310
0
  }
311
312
  /* Divvy the number up into D, DM, or DMS */
313
0
  if (val < 0)
314
0
  {
315
0
    val *= -1;
316
0
    is_negative = 1;
317
0
  }
318
0
  degrees = val;
319
0
  if (min_digits > 0)
320
0
  {
321
    /* Break degrees to integer and use fraction for minutes */
322
0
    minutes = modf(val, &degrees) * 60;
323
0
  }
324
0
  if (sec_digits > 0)
325
0
  {
326
0
    if (0 == min_digits)
327
0
    {
328
0
      lwerror("Bad format, cannot include seconds (SS.SSS) without including minutes (MM.MMM).");
329
0
    }
330
0
    seconds = modf(minutes, &minutes) * 60;
331
0
    if (sec_piece >= 0)
332
0
    {
333
      /* See if the formatted seconds round up to 60. If so, increment minutes and reset seconds. */
334
0
      round_pow = pow(10, sec_dec_digits);
335
0
      if (lround(seconds * round_pow) >= 60 * round_pow)
336
0
      {
337
0
        minutes += 1;
338
0
        seconds = 0;
339
        /* See if the formatted minutes round up to 60. If so, increment degrees and reset seconds. */
340
0
        if (lround(minutes * round_pow) >= 60 * round_pow)
341
0
        {
342
0
          degrees += 1;
343
0
          minutes = 0;
344
0
        }
345
0
      }
346
0
    }
347
0
  }
348
349
  /* Handle the compass direction.  If not using compass dir, display degrees as a positive/negative number. */
350
0
  if (compass_dir_piece >= 0)
351
0
  {
352
0
    strcpy(pieces[compass_dir_piece], is_negative ? neg_dir_symbol : pos_dir_symbol);
353
0
  }
354
0
  else if (is_negative)
355
0
  {
356
0
    degrees *= -1;
357
0
  }
358
359
  /* Format the degrees into their string piece. */
360
0
  if (deg_digits + deg_dec_digits + 2 > WORK_SIZE)
361
0
  {
362
0
    lwerror("Bad format, degrees (DD.DDD) number of digits was greater than our working limit.");
363
0
  }
364
0
  if(deg_piece >= 0)
365
0
  {
366
0
    snprintf(pieces[deg_piece], WORK_SIZE, "%*.*f", deg_digits, deg_dec_digits, degrees);
367
0
  }
368
369
0
  if (min_piece >= 0)
370
0
  {
371
    /* Format the minutes into their string piece. */
372
0
    if (min_digits + min_dec_digits + 2 > WORK_SIZE)
373
0
    {
374
0
      lwerror("Bad format, minutes (MM.MMM) number of digits was greater than our working limit.");
375
0
    }
376
0
    snprintf(pieces[min_piece], WORK_SIZE, "%*.*f", min_digits, min_dec_digits, minutes);
377
0
  }
378
0
  if (sec_piece >= 0)
379
0
  {
380
    /* Format the seconds into their string piece. */
381
0
    if (sec_digits + sec_dec_digits + 2 > WORK_SIZE)
382
0
    {
383
0
      lwerror("Bad format, seconds (SS.SSS) number of digits was greater than our working limit.");
384
0
    }
385
0
    snprintf(pieces[sec_piece], WORK_SIZE, "%*.*f", sec_digits, sec_dec_digits, seconds);
386
0
  }
387
388
  /* Allocate space for the result.  Leave plenty of room for excess digits, negative sign, etc.*/
389
0
  result = (char*)lwalloc(format_length + WORK_SIZE);
390
0
  memset(result, 0, format_length + WORK_SIZE);
391
392
  /* Append all the pieces together. There may be less than 9, but in that case the rest will be blank. */
393
0
  strcpy(result, pieces[0]);
394
0
  for (index = 1; index < NUM_PIECES; index++)
395
0
  {
396
0
    strcat(result, pieces[index]);
397
0
  }
398
399
0
  return result;
400
0
}
401
402
/* Print two doubles (lat and lon) in DMS form using the specified format.
403
 * First normalizes them so they will display as -90 to 90 and -180 to 180.
404
 * Format string may be null or 0-length, in which case a default format will be used.
405
 * NOTE: Format string is required to be in UTF-8.
406
 * NOTE2: returned string is lwalloc'ed, caller is responsible to lwfree it up
407
 */
408
static char * lwdoubles_to_latlon(double lat, double lon, const char * format)
409
0
{
410
0
  char * lat_text;
411
0
  char * lon_text;
412
0
  char * result;
413
0
  size_t sz;
414
415
  /* Normalize lat/lon to the normal (-90 to 90, -180 to 180) range. */
416
0
  lwprint_normalize_latlon(&lat, &lon);
417
  /* This is somewhat inefficient as the format is parsed twice. */
418
0
  lat_text = lwdouble_to_dms(lat, "N", "S", format);
419
0
  lon_text = lwdouble_to_dms(lon, "E", "W", format);
420
421
  /* lat + lon + a space between + the null terminator. */
422
0
  sz = strlen(lat_text) + strlen(lon_text) + 2;
423
0
  result = (char*)lwalloc(sz);
424
0
  snprintf(result, sz, "%s %s", lat_text, lon_text);
425
0
  lwfree(lat_text);
426
0
  lwfree(lon_text);
427
0
  return result;
428
0
}
429
430
/* Print the X (lon) and Y (lat) of the given point in DMS form using
431
 * the specified format.
432
 * First normalizes the values so they will display as -90 to 90 and -180 to 180.
433
 * Format string may be null or 0-length, in which case a default format will be used.
434
 * NOTE: Format string is required to be in UTF-8.
435
 * NOTE2: returned string is lwalloc'ed, caller is responsible to lwfree it up
436
 */
437
char* lwpoint_to_latlon(const LWPOINT * pt, const char *format)
438
0
{
439
0
  const POINT2D *p;
440
0
  if (NULL == pt)
441
0
  {
442
0
    lwerror("Cannot convert a null point into formatted text.");
443
0
  }
444
0
  if (lwgeom_is_empty((LWGEOM *)pt))
445
0
  {
446
0
    lwerror("Cannot convert an empty point into formatted text.");
447
0
  }
448
0
  p = getPoint2d_cp(pt->point, 0);
449
0
  return lwdoubles_to_latlon(p->y, p->x, format);
450
0
}
451
452
/*
453
 * Print an ordinate value using at most **maxdd** number of decimal digits
454
 * The actual number of printed decimal digits may be less than the
455
 * requested ones if out of significant digits.
456
 *
457
 * The function will write at most OUT_DOUBLE_BUFFER_SIZE bytes, including the
458
 * terminating NULL.
459
 * It returns the number of bytes written (excluding the final NULL)
460
 *
461
 */
462
int
463
lwprint_double(double d, int maxdd, char *buf)
464
0
{
465
0
  int length;
466
0
  double ad = fabs(d);
467
0
  int precision = FP_MAX(0, maxdd);
468
469
0
  if (ad <= OUT_MIN_DOUBLE || ad >= OUT_MAX_DOUBLE)
470
0
  {
471
0
    length = d2sexp_buffered_n(d, precision, buf);
472
0
  }
473
0
  else
474
0
  {
475
0
    length = d2sfixed_buffered_n(d, precision, buf);
476
0
  }
477
0
  buf[length] = '\0';
478
479
0
  return length;
480
0
}