PostGIS 3.7.0dev-r@@SVN_REVISION@@
Loading...
Searching...
No Matches
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. */
37static 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. */
79static 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 (lround(seconds * round_pow) >= 60 * round_pow)
336 {
337 minutes += 1;
338 seconds = 0;
339 /* See if the formatted minutes round up to 60. If so, increment degrees and reset seconds. */
340 if (lround(minutes * round_pow) >= 60 * round_pow)
341 {
342 degrees += 1;
343 minutes = 0;
344 }
345 }
346 }
347 }
348
349 /* Handle the compass direction. If not using compass dir, display degrees as a positive/negative number. */
350 if (compass_dir_piece >= 0)
351 {
352 strcpy(pieces[compass_dir_piece], is_negative ? neg_dir_symbol : pos_dir_symbol);
353 }
354 else if (is_negative)
355 {
356 degrees *= -1;
357 }
358
359 /* Format the degrees into their string piece. */
360 if (deg_digits + deg_dec_digits + 2 > WORK_SIZE)
361 {
362 lwerror("Bad format, degrees (DD.DDD) number of digits was greater than our working limit.");
363 }
364 if(deg_piece >= 0)
365 {
366 snprintf(pieces[deg_piece], WORK_SIZE, "%*.*f", deg_digits, deg_dec_digits, degrees);
367 }
368
369 if (min_piece >= 0)
370 {
371 /* Format the minutes into their string piece. */
372 if (min_digits + min_dec_digits + 2 > WORK_SIZE)
373 {
374 lwerror("Bad format, minutes (MM.MMM) number of digits was greater than our working limit.");
375 }
376 snprintf(pieces[min_piece], WORK_SIZE, "%*.*f", min_digits, min_dec_digits, minutes);
377 }
378 if (sec_piece >= 0)
379 {
380 /* Format the seconds into their string piece. */
381 if (sec_digits + sec_dec_digits + 2 > WORK_SIZE)
382 {
383 lwerror("Bad format, seconds (SS.SSS) number of digits was greater than our working limit.");
384 }
385 snprintf(pieces[sec_piece], WORK_SIZE, "%*.*f", sec_digits, sec_dec_digits, seconds);
386 }
387
388 /* Allocate space for the result. Leave plenty of room for excess digits, negative sign, etc.*/
389 result = (char*)lwalloc(format_length + WORK_SIZE);
390 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 strcpy(result, pieces[0]);
394 for (index = 1; index < NUM_PIECES; index++)
395 {
396 strcat(result, pieces[index]);
397 }
398
399 return result;
400}
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 */
408static char * lwdoubles_to_latlon(double lat, double lon, const char * format)
409{
410 char * lat_text;
411 char * lon_text;
412 char * result;
413 size_t sz;
414
415 /* Normalize lat/lon to the normal (-90 to 90, -180 to 180) range. */
416 lwprint_normalize_latlon(&lat, &lon);
417 /* This is somewhat inefficient as the format is parsed twice. */
418 lat_text = lwdouble_to_dms(lat, "N", "S", format);
419 lon_text = lwdouble_to_dms(lon, "E", "W", format);
420
421 /* lat + lon + a space between + the null terminator. */
422 sz = strlen(lat_text) + strlen(lon_text) + 2;
423 result = (char*)lwalloc(sz);
424 snprintf(result, sz, "%s %s", lat_text, lon_text);
425 lwfree(lat_text);
426 lwfree(lon_text);
427 return result;
428}
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 */
437char* lwpoint_to_latlon(const LWPOINT * pt, const char *format)
438{
439 const POINT2D *p;
440 if (NULL == pt)
441 {
442 lwerror("Cannot convert a null point into formatted text.");
443 }
444 if (lwgeom_is_empty((LWGEOM *)pt))
445 {
446 lwerror("Cannot convert an empty point into formatted text.");
447 }
448 p = getPoint2d_cp(pt->point, 0);
449 return lwdoubles_to_latlon(p->y, p->x, format);
450}
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 */
462int
463lwprint_double(double d, int maxdd, char *buf)
464{
465 int length;
466 double ad = fabs(d);
467 int precision = FP_MAX(0, maxdd);
468
469 if (ad <= OUT_MIN_DOUBLE || ad >= OUT_MAX_DOUBLE)
470 {
471 length = d2sexp_buffered_n(d, precision, buf);
472 }
473 else
474 {
475 length = d2sfixed_buffered_n(d, precision, buf);
476 }
477 buf[length] = '\0';
478
479 return length;
480}
static uint8_t precision
Definition cu_in_twkb.c:25
char result[OUT_DOUBLE_BUFFER_SIZE]
Definition cu_print.c:267
void * lwalloc(size_t size)
Definition lwutil.c:227
void lwfree(void *mem)
Definition lwutil.c:248
#define OUT_MAX_DOUBLE
#define FP_MAX(A, B)
void void lwerror(const char *fmt,...) __attribute__((format(printf
Write a notice out to the error handler.
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:199
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:97
static char * lwdoubles_to_latlon(double lat, double lon, const char *format)
Definition lwprint.c:408
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
char * lwpoint_to_latlon(const LWPOINT *pt, const char *format)
Definition lwprint.c:437
#define NUM_PIECES
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:463
POINTARRAY * point
Definition liblwgeom.h:471
double y
Definition liblwgeom.h:390
double x
Definition liblwgeom.h:390