From 43b568076ce0cb4bc77b0c3382fc5cd58611df5b Mon Sep 17 00:00:00 2001 From: Joaquim Date: Tue, 25 Aug 2026 19:57:50 +0100 Subject: [PATCH] Fix oblique frame annotations: placement and default (#8418) Two independent defects produce the reported symptoms. Placement: with MAP_ANNOT_OBLIQUE's tick_extend the annotation is put at the tip of the tick extended along the gridline, so gmtplot_map_symbol divides its offset by the sine or cosine of the angle at which the gridline meets the border. gmtplot_map_tick refuses to draw such a tick once that angle drops below MAP_ANNOT_MIN_ANGLE, but gmtplot_map_symbol divided anyway: for a shallow crossing the offset blows up and the label lands far along the border, detached from its crossing, with no tick to connect them. On case I of the report the 175E annotation was drawn at 2.0 cm instead of 3.14 cm, and 55N at -0.3 cm, outside a 6 cm wide border. Below the minimum angle the annotation now steps straight out from the border and stays at the crossing. Default: GMT 6.1.0 defaulted MAP_ANNOT_OBLIQUE to anywhere, but the anywhere bit was dropped when the setting became a keyword list, leaving the effective default at separate - longitudes only on S/N, latitudes only on W/E. That is fine while the graticule roughly follows the frame, which is exactly what an oblique frame does not do: in case II the parallels cross the top and bottom borders and the meridians the left and right ones, so nearly every annotation was thrown away. The documentation has always said the default is anywhere. Note: psxy/rotrectangle, psxyz/rotrectangle and windbarbs/grdbarb_02 gain annotations and ticks from the default change and need new baselines; the other 37 PS files from tests touching -JO, +r regions or MAP_ANNOT_OBLIQUE are byte-identical. test/psbasemap/oblique_annot.sh reads the annotation positions out of the PostScript and compares them with crossings computed via mapproject, so it needs no PS baseline. Co-Authored-By: Claude Opus 5 --- src/gmt_init.c | 6 +++++- src/gmt_plot.c | 25 ++++++++++++++++++++---- test/psbasemap/oblique_annot.sh | 34 +++++++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 5 deletions(-) create mode 100644 test/psbasemap/oblique_annot.sh diff --git a/src/gmt_init.c b/src/gmt_init.c index 9c79e6f079c..1962c29b12e 100644 --- a/src/gmt_init.c +++ b/src/gmt_init.c @@ -6858,7 +6858,11 @@ EXTERN_MSC void gmtinit_conf_classic (struct GMT_CTRL *GMT) { GMT->current.setting.given_unit[GMTCASE_MAP_ANNOT_OFFSET_PRIMARY] = 'p'; GMT->current.setting.given_unit[GMTCASE_MAP_ANNOT_OFFSET_SECONDARY] = 'p'; /* MAP_ANNOT_OBLIQUE */ - GMT->current.setting.map_annot_oblique = GMT_OBL_ANNOT_LON_HORIZONTAL | GMT_OBL_ANNOT_LAT_HORIZONTAL | GMT_OBL_ANNOT_EXTEND_TICKS; + /* The anywhere bit was lost when this setting went from a bit-sum to a keyword list; without it an oblique frame + * only annotates longitudes on the S/N borders and latitudes on the W/E borders, which throws away most of the + * annotations as soon as the graticule is rotated far from the frame axes. The documentation has said "default + * is anywhere" all along, and that is what GMT 6.1.0 did [issue #8418] */ + GMT->current.setting.map_annot_oblique = GMT_OBL_ANNOT_ANYWHERE | GMT_OBL_ANNOT_LON_HORIZONTAL | GMT_OBL_ANNOT_LAT_HORIZONTAL | GMT_OBL_ANNOT_EXTEND_TICKS; /* MAP_ANNOT_MIN_ANGLE */ GMT->current.setting.map_annot_min_angle = 20; /* MAP_ANNOT_MIN_SPACING */ diff --git a/src/gmt_plot.c b/src/gmt_plot.c index a5c7624e86e..9bf5a43f59b 100644 --- a/src/gmt_plot.c +++ b/src/gmt_plot.c @@ -1837,11 +1837,12 @@ GMT_LOCAL void gmtplot_map_symbol (struct GMT_CTRL *GMT, struct PSL_CTRL *PSL, s double line_angle, text_angle, div, tick_length, o_len, len, ca, sa, boost; double *xx = xings->xx, *yy = xings->yy, *line_angles = xings->angle; unsigned int i, annot_type, justify, *sides = xings->sides, nx = xings->nx; - bool flip; + bool flip, no_tick; len = gmtplot_get_annot_offset (GMT, &flip, level); /* Get annotation offset, and flip justification if "inside" */ annot_type = 2 << type; /* 2 = NS, 4 = EW */ for (i = 0; i < nx; i++) { + no_tick = false; if (!(GMT->current.setting.map_annot_oblique & GMT_OBL_ANNOT_ANYWHERE) && ((type == 0 && (sides[i] % 2)) || (type == 1 && !(sides[i] % 2)))) continue; if (gmtlib_prepare_label (GMT, line_angles[i], sides[i], xx[i], yy[i], type, &line_angle, &text_angle, &justify)) continue; @@ -1853,12 +1854,28 @@ GMT_LOCAL void gmtplot_map_symbol (struct GMT_CTRL *GMT, struct PSL_CTRL *PSL, s if (!flip && GMT->current.setting.map_annot_oblique & annot_type) o_len = tick_length; if (GMT->current.setting.map_annot_oblique & GMT_OBL_ANNOT_EXTEND_TICKS) { div = ((sides[i] % 2) ? fabs (ca) : fabs (sa)); - o_len /= div; + /* We only ride out to the tip of the extended tick if there actually is one: gmtplot_map_tick skips + * ticks whose gridline meets the border at less than MAP_ANNOT_MIN_ANGLE, and dividing by that same + * tiny div here would fling the annotation far along the border, away from the crossing it belongs + * to and with no tick to connect the two. Below that angle we step straight out from the border + * instead, so the annotation stays where its gridline meets the frame [issue #8418] */ + if (div > sind (GMT->current.setting.map_annot_min_angle)) + o_len /= div; + else + no_tick = true; } else o_len += copysign (boost, o_len); - xx[i] += o_len * ca; - yy[i] += o_len * sa; + if (no_tick) { /* Offset normal to the border since there is no oblique tick to follow */ + if (sides[i] % 2) + xx[i] += (sides[i] == 1) ? o_len : -o_len; + else + yy[i] += (sides[i] == 2) ? o_len : -o_len; + } + else { + xx[i] += o_len * ca; + yy[i] += o_len * sa; + } if (!flip && (GMT->current.setting.map_annot_oblique & annot_type) && GMT->current.setting.map_annot_offset[level] > 0.0) { if (sides[i] % 2) xx[i] += (sides[i] == 1) ? GMT->current.setting.map_annot_offset[level] : -GMT->current.setting.map_annot_offset[level]; diff --git a/test/psbasemap/oblique_annot.sh b/test/psbasemap/oblique_annot.sh new file mode 100644 index 00000000000..9a302877d68 --- /dev/null +++ b/test/psbasemap/oblique_annot.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# +# Oblique frames must annotate a gridline where it actually meets the border. +# +# With MAP_ANNOT_OBLIQUE's tick_extend the annotation goes at the tip of the tick that is extended +# along the gridline. gmtplot_map_tick draws no such tick once the gridline meets the border at +# less than MAP_ANNOT_MIN_ANGLE, but the annotation code used to extend regardless, dividing its +# offset by a vanishing sine and flinging the label far along the border, detached from the crossing +# it belongs to. Here the 175E meridian meets the N border at about 6 degrees: its annotation +# belongs at x = 3.1 cm and used to be drawn at x = 2.0 cm on a map only 6 cm wide. See issue #8418. +# +# The positions below were checked against the crossings computed independently with gmt mapproject. + +# Pin everything the extracted table depends on, so that it is the annotation placement being tested +gmt set FORMAT_GEO_MAP ddd:mm:ssF MAP_ANNOT_MIN_SPACING 0 \ + MAP_ANNOT_OBLIQUE anywhere,lon_horizontal,lat_horizontal,tick_extend + +gmt psbasemap -JOb168/51/172/32.5/6c -R-500/2300/-400/400+uk -BeNsW -Bxa5f1 -Bya5f1 -P > map.ps + +# Pull the annotations out of the PostScript as "label border position_along_that_border_in_cm". +# PSL units are 1/1200 inch measured from the lower left map corner, hence the 2.54/1200 scaling. +tr '\r' '\n' < map.ps | awk ' + { for (i = 3; i <= NF; i++) if ($i == "M") { x = $(i-2); y = $(i-1) } } # Remember latest moveto + /\([0-9]+.[EN]\) [a-z][a-z] Z/ { + for (i = 1; i <= NF; i++) if ($i ~ /^\(/) { lab = $i; just = $(i+1) } + if (just == "bc") printf "%s N %.1f\n", lab, x*2.54/1200 + else if (just == "tc") printf "%s S %.1f\n", lab, x*2.54/1200 + else if (just == "mr") printf "%s W %.1f\n", lab, y*2.54/1200 + else if (just == "ml") printf "%s E %.1f\n", lab, y*2.54/1200 + }' | sort > result.txt + +printf '%b' '(165\260E) W 0.7\n(170\260E) W 1.4\n(175\260E) N 3.1\n(35\260N) N 5.1\n(40\260N) N 3.9\n(45\260N) N 2.7\n(50\260N) N 1.5\n(55\260N) N 0.3\n(55\260N) W 0.4\n' > answer.txt + +diff -q --strip-trailing-cr answer.txt result.txt