PostGIS  3.4.0dev-r@@SVN_REVISION@@
lwprint.c
Go to the documentation of this file.
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 {
39  /* First remove all the truly excessive trips around the world via up or down. */
40  while (*lat > 270)
41  {
42  *lat -= 360;
43  }
44  while (*lat < -270)
45  {
46  *lat += 360;
47  }
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  if (*lat > 90)
53  {
54  *lat = 180 - *lat;
55  *lon += 180;
56  }
57  if (*lat < -90)
58  {
59  *lat = -180 - *lat;
60  *lon += 180;
61  }
62  /* Now make sure lon is in the normal range. Wrapping longitude
63  * has no effect on latitude. */
64  while (*lon > 180)
65  {
66  *lon -= 360;
67  }
68  while (*lon < -180)
69  {
70  *lon += 360;
71  }
72 }
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 {
81  /* 3 numbers, 1 sign or compass dir, and 5 possible strings (degree signs, spaces, misc text, etc) between or around them.*/
82 # define NUM_PIECES 9
83 # define WORK_SIZE 1024
84  char pieces[NUM_PIECES][WORK_SIZE];
85  int current_piece = 0;
86  int is_negative = 0;
87 
88  double degrees = 0.0;
89  double minutes = 0.0;
90  double seconds = 0.0;
91 
92  int compass_dir_piece = -1;
93 
94  int reading_deg = 0;
95  int deg_digits = 0;
96  int deg_has_decpoint = 0;
97  int deg_dec_digits = 0;
98  int deg_piece = -1;
99 
100  int reading_min = 0;
101  int min_digits = 0;
102  int min_has_decpoint = 0;
103  int min_dec_digits = 0;
104  int min_piece = -1;
105 
106  int reading_sec = 0;
107  int sec_digits = 0;
108  int sec_has_decpoint = 0;
109  int sec_dec_digits = 0;
110  int sec_piece = -1;
111 
112  int round_pow = 0;
113 
114  int format_length = ((NULL == format) ? 0 : strlen(format));
115 
116  char * result;
117 
118  int index, following_byte_index;
119  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  for (index = 0; index < NUM_PIECES; index++)
125  {
126  pieces[index][0] = '\0';
127  }
128 
129  /* If no format is provided, use a default. */
130  if (0 == format_length)
131  {
132  /* C2B0 is UTF-8 for the degree symbol. */
133  format = "D\xC2\xB0""M'S.SSS\"C";
134  format_length = strlen(format);
135  }
136  else if (format_length > WORK_SIZE)
137  {
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  lwerror("Bad format, exceeds maximum length (%d).", WORK_SIZE);
141  }
142 
143  for (index = 0; index < format_length; index++)
144  {
145  char next_char = format[index];
146  switch (next_char)
147  {
148  case 'D':
149  if (reading_deg)
150  {
151  /* If we're reading degrees, add another digit. */
152  deg_has_decpoint ? deg_dec_digits++ : deg_digits++;
153  }
154  else
155  {
156  /* If we're not reading degrees, we are now. */
157  current_piece++;
158  deg_piece = current_piece;
159  if (deg_digits > 0)
160  {
161  lwerror("Bad format, cannot include degrees (DD.DDD) more than once.");
162  }
163  reading_deg = 1;
164  reading_min = 0;
165  reading_sec = 0;
166  deg_digits++;
167  }
168  break;
169  case 'M':
170  if (reading_min)
171  {
172  /* If we're reading minutes, add another digit. */
173  min_has_decpoint ? min_dec_digits++ : min_digits++;
174  }
175  else
176  {
177  /* If we're not reading minutes, we are now. */
178  current_piece++;
179  min_piece = current_piece;
180  if (min_digits > 0)
181  {
182  lwerror("Bad format, cannot include minutes (MM.MMM) more than once.");
183  }
184  reading_deg = 0;
185  reading_min = 1;
186  reading_sec = 0;
187  min_digits++;
188  }
189  break;
190  case 'S':
191  if (reading_sec)
192  {
193  /* If we're reading seconds, add another digit. */
194  sec_has_decpoint ? sec_dec_digits++ : sec_digits++;
195  }
196  else
197  {
198  /* If we're not reading seconds, we are now. */
199  current_piece++;
200  sec_piece = current_piece;
201  if (sec_digits > 0)
202  {
203  lwerror("Bad format, cannot include seconds (SS.SSS) more than once.");
204  }
205  reading_deg = 0;
206  reading_min = 0;
207  reading_sec = 1;
208  sec_digits++;
209  }
210  break;
211  case 'C':
212  /* We're done reading anything else we might have been reading. */
213  if (reading_deg || reading_min || reading_sec)
214  {
215  /* We were reading something, that means this is the next piece. */
216  reading_deg = 0;
217  reading_min = 0;
218  reading_sec = 0;
219  }
220  current_piece++;
221 
222  if (compass_dir_piece >= 0)
223  {
224  lwerror("Bad format, cannot include compass dir (C) more than once.");
225  }
226  /* The compass dir is a piece all by itself. */
227  compass_dir_piece = current_piece;
228  current_piece++;
229  break;
230  case '.':
231  /* If we're reading deg, min, or sec, we want a decimal point for it. */
232  if (reading_deg)
233  {
234  deg_has_decpoint = 1;
235  }
236  else if (reading_min)
237  {
238  min_has_decpoint = 1;
239  }
240  else if (reading_sec)
241  {
242  sec_has_decpoint = 1;
243  }
244  else
245  {
246  /* Not reading anything, just pass through the '.' */
247  strncat(pieces[current_piece], &next_char, 1);
248  }
249  break;
250  default:
251  /* Any other char is just passed through unchanged. But it does mean we are done reading D, M, or S.*/
252  if (reading_deg || reading_min || reading_sec)
253  {
254  /* We were reading something, that means this is the next piece. */
255  current_piece++;
256  reading_deg = 0;
257  reading_min = 0;
258  reading_sec = 0;
259  }
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  multibyte_char_width = 1;
263  if (next_char & 0x80)
264  {
265  if ((next_char & 0xF8) == 0xF0)
266  {
267  multibyte_char_width += 3;
268  }
269  else if ((next_char & 0xF0) == 0xE0)
270  {
271  multibyte_char_width += 2;
272  }
273  else if ((next_char & 0xE0) == 0xC0)
274  {
275  multibyte_char_width += 1;
276  }
277  else
278  {
279  lwerror("Bad format, invalid high-order byte found first, format string may not be UTF-8.");
280  }
281  }
282  if (multibyte_char_width > 1)
283  {
284  if (index + multibyte_char_width >= format_length)
285  {
286  lwerror("Bad format, UTF-8 character first byte found with insufficient following bytes, format string may not be UTF-8.");
287  }
288  for (following_byte_index = (index + 1); following_byte_index < (index + multibyte_char_width); following_byte_index++)
289  {
290  if ((format[following_byte_index] & 0xC0) != 0x80)
291  {
292  lwerror("Bad format, invalid byte found following leading byte of multibyte character, format string may not be UTF-8.");
293  }
294  }
295  }
296  /* Copy all the character's bytes into the current piece. */
297  strncat(pieces[current_piece], &(format[index]), multibyte_char_width);
298  /* Now increment index past the rest of those bytes. */
299  index += multibyte_char_width - 1;
300  break;
301  }
302  if (current_piece >= NUM_PIECES)
303  {
304  lwerror("Internal error, somehow needed more pieces than it should.");
305  }
306  }
307  if (deg_piece < 0)
308  {
309  lwerror("Bad format, degrees (DD.DDD) must be included.");
310  }
311 
312  /* Divvy the number up into D, DM, or DMS */
313  if (val < 0)
314  {
315  val *= -1;
316  is_negative = 1;
317  }
318  degrees = val;
319  if (min_digits > 0)
320  {
321  /* Break degrees to integer and use fraction for minutes */
322  minutes = modf(val, &degrees) * 60;
323  }
324  if (sec_digits > 0)
325  {
326  if (0 == min_digits)
327  {
328  lwerror("Bad format, cannot include seconds (SS.SSS) without including minutes (MM.MMM).");
329  }
330  seconds = modf(minutes, &minutes) * 60;
331  if (sec_piece >= 0)
332  {
333  /* See if the formatted seconds round up to 60. If so, increment minutes and reset seconds. */
334  round_pow = pow(10, sec_dec_digits);
335  if (floorf(seconds * round_pow) / round_pow >= 60)
336  {
337  minutes += 1;
338  seconds = 0;
339  }
340  }
341  }
342 
343  /* Handle the compass direction. If not using compass dir, display degrees as a positive/negative number. */
344  if (compass_dir_piece >= 0)
345  {
346  strcpy(pieces[compass_dir_piece], is_negative ? neg_dir_symbol : pos_dir_symbol);
347  }
348  else if (is_negative)
349  {
350  degrees *= -1;
351  }
352 
353  /* Format the degrees into their string piece. */
354  if (deg_digits + deg_dec_digits + 2 > WORK_SIZE)
355  {
356  lwerror("Bad format, degrees (DD.DDD) number of digits was greater than our working limit.");
357  }
358  if(deg_piece >= 0)
359  {
360  snprintf(pieces[deg_piece], WORK_SIZE, "%*.*f", deg_digits, deg_dec_digits, degrees);
361  }
362 
363  if (min_piece >= 0)
364  {
365  /* Format the minutes into their string piece. */
366  if (min_digits + min_dec_digits + 2 > WORK_SIZE)
367  {
368  lwerror("Bad format, minutes (MM.MMM) number of digits was greater than our working limit.");
369  }
370  snprintf(pieces[min_piece], WORK_SIZE, "%*.*f", min_digits, min_dec_digits, minutes);
371  }
372  if (sec_piece >= 0)
373  {
374  /* Format the seconds into their string piece. */
375  if (sec_digits + sec_dec_digits + 2 > WORK_SIZE)
376  {
377  lwerror("Bad format, seconds (SS.SSS) number of digits was greater than our working limit.");
378  }
379  snprintf(pieces[sec_piece], WORK_SIZE, "%*.*f", sec_digits, sec_dec_digits, seconds);
380  }
381 
382  /* Allocate space for the result. Leave plenty of room for excess digits, negative sign, etc.*/
383  result = (char*)lwalloc(format_length + WORK_SIZE);
384  memset(result, 0, format_length + WORK_SIZE);
385 
386  /* Append all the pieces together. There may be less than 9, but in that case the rest will be blank. */
387  strcpy(result, pieces[0]);
388  for (index = 1; index < NUM_PIECES; index++)
389  {
390  strcat(result, pieces[index]);
391  }
392 
393  return result;
394 }
395 
396 /* Print two doubles (lat and lon) in DMS form using the specified format.
397  * First normalizes them so they will display as -90 to 90 and -180 to 180.
398  * Format string may be null or 0-length, in which case a default format will be used.
399  * NOTE: Format string is required to be in UTF-8.
400  * NOTE2: returned string is lwalloc'ed, caller is responsible to lwfree it up
401  */
402 static char * lwdoubles_to_latlon(double lat, double lon, const char * format)
403 {
404  char * lat_text;
405  char * lon_text;
406  char * result;
407  size_t sz;
408 
409  /* Normalize lat/lon to the normal (-90 to 90, -180 to 180) range. */
410  lwprint_normalize_latlon(&lat, &lon);
411  /* This is somewhat inefficient as the format is parsed twice. */
412  lat_text = lwdouble_to_dms(lat, "N", "S", format);
413  lon_text = lwdouble_to_dms(lon, "E", "W", format);
414 
415  /* lat + lon + a space between + the null terminator. */
416  sz = strlen(lat_text) + strlen(lon_text) + 2;
417  result = (char*)lwalloc(sz);
418  snprintf(result, sz, "%s %s", lat_text, lon_text);
419  lwfree(lat_text);
420  lwfree(lon_text);
421  return result;
422 }
423 
424 /* Print the X (lon) and Y (lat) of the given point in DMS form using
425  * the specified format.
426  * First normalizes the values so they will display as -90 to 90 and -180 to 180.
427  * Format string may be null or 0-length, in which case a default format will be used.
428  * NOTE: Format string is required to be in UTF-8.
429  * NOTE2: returned string is lwalloc'ed, caller is responsible to lwfree it up
430  */
431 char* lwpoint_to_latlon(const LWPOINT * pt, const char *format)
432 {
433  const POINT2D *p;
434  if (NULL == pt)
435  {
436  lwerror("Cannot convert a null point into formatted text.");
437  }
438  if (lwgeom_is_empty((LWGEOM *)pt))
439  {
440  lwerror("Cannot convert an empty point into formatted text.");
441  }
442  p = getPoint2d_cp(pt->point, 0);
443  return lwdoubles_to_latlon(p->y, p->x, format);
444 }
445 
446 /*
447  * Print an ordinate value using at most **maxdd** number of decimal digits
448  * The actual number of printed decimal digits may be less than the
449  * requested ones if out of significant digits.
450  *
451  * The function will write at most OUT_DOUBLE_BUFFER_SIZE bytes, including the
452  * terminating NULL.
453  * It returns the number of bytes written (exluding the final NULL)
454  *
455  */
456 int
457 lwprint_double(double d, int maxdd, char *buf)
458 {
459  int length;
460  double ad = fabs(d);
461  int precision = FP_MAX(0, maxdd);
462 
463  if (ad <= OUT_MIN_DOUBLE || ad >= OUT_MAX_DOUBLE)
464  {
465  length = d2sexp_buffered_n(d, precision, buf);
466  }
467  else
468  {
469  length = d2sfixed_buffered_n(d, precision, buf);
470  }
471  buf[length] = '\0';
472 
473  return length;
474 }
static uint8_t precision
Definition: cu_in_twkb.c:25
char result[OUT_DOUBLE_BUFFER_SIZE]
Definition: cu_print.c:262
void lwfree(void *mem)
Definition: lwutil.c:242
void * lwalloc(size_t size)
Definition: lwutil.c:227
#define OUT_MAX_DOUBLE
#define FP_MAX(A, B)
void lwerror(const char *fmt,...)
Write a notice out to the error handler.
Definition: lwutil.c:190
static const POINT2D * getPoint2d_cp(const POINTARRAY *pa, uint32_t n)
Returns a POINT2D pointer into the POINTARRAY serialized_ptlist, suitable for reading from.
Definition: lwinline.h:101
static int lwgeom_is_empty(const LWGEOM *geom)
Return true or false depending on whether a geometry is an "empty" geometry (no vertices members)
Definition: lwinline.h:203
static char * lwdouble_to_dms(double val, const char *pos_dir_symbol, const char *neg_dir_symbol, const char *format)
Definition: lwprint.c:79
#define WORK_SIZE
#define NUM_PIECES
static char * lwdoubles_to_latlon(double lat, double lon, const char *format)
Definition: lwprint.c:402
char * lwpoint_to_latlon(const LWPOINT *pt, const char *format)
Definition: lwprint.c:431
static void lwprint_normalize_latlon(double *lat, double *lon)
Definition: lwprint.c:37
int lwprint_double(double d, int maxdd, char *buf)
Definition: lwprint.c:457
POINTARRAY * point
Definition: liblwgeom.h:471
double y
Definition: liblwgeom.h:390
double x
Definition: liblwgeom.h:390