From 485d6a988b8ec6acf08a764a79e607ed6a3f8ead Mon Sep 17 00:00:00 2001 From: Esteban82 Date: Tue, 13 Oct 2026 11:42:50 -0300 Subject: [PATCH 1/5] text: Honor vector heads on the -D+v leader line pen The pen given to -D...+v accepted the +v line-end modifiers but silently ignored them. Now +vb places a vector head at the data point, shortening the leader so the tip lands on the point. The text end is clipped in PostScript so its end point is unknown in C; +ve (or plain +v) is therefore rejected with an error. The head is skipped when longer than the leader. psxy's end-vector helper moves to gmt_plot.c as gmt_plot_end_vectors so both modules share it. Also stop the paragraph (-M) box from inheriting a stale fill/outline when only a leader line is requested, and do not share (then free) the default pen's vector heads in the -D pen. Co-Authored-By: Claude Opus 5.5 --- doc/rst/source/text.rst | 5 ++++- src/gmt_plot.c | 47 ++++++++++++++++++++++++++++++++++++++++ src/gmt_prototypes.h | 1 + src/pstext.c | 36 ++++++++++++++++++++----------- src/psxy.c | 48 +---------------------------------------- 5 files changed, 77 insertions(+), 60 deletions(-) diff --git a/doc/rst/source/text.rst b/doc/rst/source/text.rst index df2b4798af4..a3ccda470f9 100644 --- a/doc/rst/source/text.rst +++ b/doc/rst/source/text.rst @@ -138,7 +138,10 @@ Optional Arguments columns, one offset pair per text record, in the same order as the input data. Values are in the current length unit [:term:`PROJ_LENGTH_UNIT`]. - **+v** - Draw a line from the original point to the shifted point; - append a *pen* to change the attributes for this line. **Note**: + append a *pen* to change the attributes for this line. To place an arrow + head at the original point, add the pen modifier **+vb**\ *vspecs* (e.g., + **-D**\ 1c+v1p,red+vb0.3c+gred); a head at the text end is not supported. + See :ref:`Technical reference - Drawing of Vectors`. **Note**: The **-Dj**\|\ **J** selection cannot be used with paragraph mode (|-M|). .. _-F: diff --git a/src/gmt_plot.c b/src/gmt_plot.c index 3929e540c5b..fb87807418d 100644 --- a/src/gmt_plot.c +++ b/src/gmt_plot.c @@ -58,6 +58,7 @@ * gmt_map_text * gmt_map_title * gmt_plane_perspective + * gmt_plot_end_vectors * gmt_plot_geo_ellipse * gmt_plot_grid_graticules * gmt_plot_line @@ -6186,6 +6187,52 @@ GMT_LOCAL bool gmtplot_these_are_duplicates (double x0, double y0, double x1, do return true; } +void gmt_plot_end_vectors (struct GMT_CTRL *GMT, double *x, double *y, uint64_t n, struct GMT_PEN *P) { + /* Maybe add vector heads. Here, x,y are in inches on the plot */ + unsigned int k, current[2] = {0,0}, next[2] = {1,0}; + double dim[PSL_MAX_DIMS], angle, s, c, L; + char *end[2] = {"start", "end"}; + if (n < 2) return; /* No line of mine */ + if (P->end[0].V == NULL && P->end[1].V == NULL) return; /* No arrow heads requested */ + current[1] = (unsigned int)n-1; next[1] = (unsigned int)n-2; + PSL_command (GMT->PSL, "V\n"); + GMT->PSL->current.linewidth = -1.0; /* Will be changed by next PSL_setlinewidth */ + gmt_M_memset (dim, PSL_MAX_DIMS, double); + for (k = 0; k < 2; k++) { + if (P->end[k].V == NULL) continue; + P->end[k].V->v.status |= PSL_VEC_LINE; /* Flag so head is not skipped due to "short" vector */ + /* Add vector heads to this end */ + PSL_comment (GMT->PSL, "Add vector head to %s of line\n", end[k]); + angle = d_atan2d (y[current[k]] - y[next[k]], x[current[k]] - x[next[k]]); + sincosd (angle, &s, &c); + L = (P->end[k].V->v.v_kind[1] == PSL_VEC_TERMINAL) ? 1e-3 : P->end[k].length; + P->end[k].V->v.v_width = (float)(P->end[k].V->v.pen.width * GMT->session.u2u[GMT_PT][GMT_INCH]); /* Set symbol pen width */ + dim[PSL_VEC_XTIP] = x[current[k]] + c * L; + dim[PSL_VEC_YTIP] = y[current[k]] + s * L; + dim[PSL_VEC_TAIL_WIDTH] = P->end[k].V->v.v_width; + dim[PSL_VEC_HEAD_LENGTH] = P->end[k].V->v.h_length; + dim[PSL_VEC_HEAD_WIDTH] = P->end[k].V->v.h_width; + dim[PSL_VEC_HEAD_SHAPE] = P->end[k].V->v.v_shape; + dim[PSL_VEC_STATUS] = (double)P->end[k].V->v.status; + dim[PSL_VEC_HEAD_TYPE_BEGIN] = (double)P->end[k].V->v.v_kind[0]; + dim[PSL_VEC_HEAD_TYPE_END] = (double)P->end[k].V->v.v_kind[1]; + dim[PSL_VEC_TRIM_BEGIN] = (double)P->end[k].V->v.v_trim[0]; + dim[PSL_VEC_TRIM_END] = (double)P->end[k].V->v.v_trim[1]; + if (P->end[k].V->v.status & PSL_VEC_OUTLINE2) { /* Gave specific head outline pen */ + PSL_defpen (GMT->PSL, "PSL_vecheadpen", P->end[k].V->v.pen.width, P->end[k].V->v.pen.style, P->end[k].V->v.pen.offset, P->end[k].V->v.pen.rgb); + dim[PSL_VEC_HEAD_PENWIDTH] = P->end[k].V->v.pen.width; + } + else { /* Set default based on line pen */ + PSL_defpen (GMT->PSL, "PSL_vecheadpen", 0.5 * P->width, P->style, P->offset, P->rgb); + dim[PSL_VEC_HEAD_PENWIDTH] = 0.5 * P->width; + } + gmt_setfill (GMT, &P->end[k].V->v.fill, (P->end[k].V->v.status & PSL_VEC_OUTLINE2) == PSL_VEC_OUTLINE2); + PSL_plotsymbol (GMT->PSL, x[current[k]], y[current[k]], dim, PSL_VECTOR); + } + GMT->PSL->current.linewidth = -1.0; /* Will be changed by next PSL_setlinewidth */ + PSL_command (GMT->PSL, "U\n"); +} + void gmt_plot_line (struct GMT_CTRL *GMT, double *x, double *y, unsigned int *pen, uint64_t n, unsigned int mode) { /* Mode = PSL_LINEAR [0] or PSL_BEZIER [1] */ /* Drawing lines of finite thickness that cross map boundaries is tricky because the exit or entry point diff --git a/src/gmt_prototypes.h b/src/gmt_prototypes.h index e8139237fe2..d485f218fb4 100644 --- a/src/gmt_prototypes.h +++ b/src/gmt_prototypes.h @@ -305,6 +305,7 @@ EXTERN_MSC void gmt_set_basemap_orders (struct GMT_CTRL *GMT, unsigned int frame EXTERN_MSC void gmt_map_clip_off (struct GMT_CTRL *GMT); EXTERN_MSC void gmt_map_clip_on (struct GMT_CTRL *GMT, double rgb[], unsigned int flag); EXTERN_MSC void gmt_BB_clip_on (struct GMT_CTRL *GMT, double rgb[], unsigned int flag); +EXTERN_MSC void gmt_plot_end_vectors (struct GMT_CTRL *GMT, double *x, double *y, uint64_t n, struct GMT_PEN *P); EXTERN_MSC void gmt_plot_line (struct GMT_CTRL *GMT, double *x, double *y, unsigned int *pen, uint64_t n, unsigned int mode); EXTERN_MSC void gmt_setpen (struct GMT_CTRL *GMT, struct GMT_PEN *pen); EXTERN_MSC void gmt_setfill (struct GMT_CTRL *GMT, struct GMT_FILL *fill, int outline); diff --git a/src/pstext.c b/src/pstext.c index df595215956..d46144ff73b 100644 --- a/src/pstext.c +++ b/src/pstext.c @@ -145,6 +145,7 @@ static void *New_Ctrl (struct GMT_CTRL *GMT) { /* Allocate and initialize a new /* Initialize values whose defaults are not 0/false/NULL */ C->D.pen = C->W.pen = GMT->current.setting.map_default_pen; + C->D.pen.end[0].V = C->D.pen.end[1].V = NULL; /* Do not share (and later free) any vector heads of the default pen */ C->C.dx = C->C.dy = GMT_TEXT_CLEARANCE; /* 15% of font size is default clearance */ C->C.percent = true; C->C.mode = 'o'; /* Rectangular box shape */ @@ -163,10 +164,24 @@ static void *New_Ctrl (struct GMT_CTRL *GMT) { /* Allocate and initialize a new static void Free_Ctrl (struct GMT_CTRL *GMT, struct PSTEXT_CTRL *C) { /* Deallocate control structure */ if (!C) return; gmt_M_str_free (C->D.file); + gmt_freepen (GMT, &C->D.pen); gmt_M_str_free (C->F.text); gmt_M_free (GMT, C); } +GMT_LOCAL void pstext_draw_leader (struct GMT_CTRL *GMT, struct PSTEXT_INFO *T, double *xx, double *yy, char *text, double offset[]) { + /* Draw leader line from the data point (xx[0],yy[0]) to the text at (xx[1],yy[1]), with optional vector head at the data point */ + double x[2] = {xx[0], xx[1]}, y[2] = {yy[0], yy[1]}, s, c; + bool head = (T->vecpen.end[0].V && hypot (x[1] - x[0], y[1] - y[0]) > T->vecpen.end[0].length); /* Skip head if longer than the line */ + if (head && T->vecpen.end[0].length > 0.0) { /* Shorten the line so it stops at the base of the vector head */ + sincosd (d_atan2d (y[0] - y[1], x[0] - x[1]), &s, &c); + x[0] -= c * T->vecpen.end[0].length; y[0] -= s * T->vecpen.end[0].length; + } + gmt_setpen (GMT, &T->vecpen); + PSL_plotline_clipped_by_textbox (GMT->PSL, x[0], y[0], x[1], y[1], T->font.size, text, T->paragraph_angle, T->block_justify, offset); + if (head) gmt_plot_end_vectors (GMT, x, y, 2, &T->vecpen); +} + GMT_LOCAL void pstext_output_words (struct GMT_CTRL *GMT, struct PSL_CTRL *PSL, double x, double y, char *text, struct PSTEXT_INFO *T, struct PSTEXT_CTRL *Ctrl) { double offset[2]; @@ -186,8 +201,8 @@ GMT_LOCAL void pstext_output_words (struct GMT_CTRL *GMT, struct PSL_CTRL *PSL, PSL_setfont (PSL, T->font.id); if (T->boxflag & 32) { /* Need to draw a vector from (x,y) to the offset text */ - gmt_setpen (GMT, &(T->vecpen)); - PSL_plotsegment (PSL, x, y, x + T->x_offset, y + T->y_offset); + double xx[2] = {x, x + T->x_offset}, yy[2] = {y, y + T->y_offset}; + pstext_draw_leader (GMT, T, xx, yy, NULL, offset); } if (Ctrl->D.justify) /* Smart offset according to justification (from Dave Huang) */ gmt_smart_justify (GMT, T->block_justify, T->paragraph_angle, T->x_offset, T->y_offset, &x, &y, Ctrl->D.justify); @@ -207,7 +222,7 @@ GMT_LOCAL void pstext_output_words (struct GMT_CTRL *GMT, struct PSL_CTRL *PSL, } if (T->boxflag & 1) gmt_setpen (GMT, &(T->boxpen)); /* Change current pen */ if (T->boxflag & 2) fill = &(T->boxfill); /* Determine if fill or not */ - if (T->boxflag & 3) gmt_setfill (GMT, fill, T->boxflag & 1); /* Change current fill and/or outline */ + gmt_setfill (GMT, fill, T->boxflag & 1); /* Change current fill and/or outline (none if only a leader line) */ /* Compute text box, draw/fill it, and in the process store the text in the PS file for next command */ PSL_plotparagraphbox (PSL, x, y, T->font.size, text, T->paragraph_angle, T->block_justify, offset, mode); /* Passing NULL means we typeset using the last stored paragraph info */ @@ -357,7 +372,7 @@ static int usage (struct GMTAPI_CTRL *API, int level) { "Use -Dj to move text origin away from point (direction determined by text's justification). " "Upper case -DJ will shorten diagonal shifts at corners by sqrt(2). Cannot be used with -M. Optional modifiers:"); GMT_Usage (API, 3, "+f: Read per-record offsets (dx dy) from (two columns in current distance units); overrides fixed /."); - GMT_Usage (API, 3, "+v: Draw line from text to original point; optionally append a [%s].", gmt_putpen (API->GMT, &API->GMT->current.setting.map_default_pen)); + GMT_Usage (API, 3, "+v: Draw line from text to original point; optionally append a [%s]. Add pen modifier +vb for a vector head at the point.", gmt_putpen (API->GMT, &API->GMT->current.setting.map_default_pen)); GMT_Usage (API, 1, "\n-F[+a[]][+c[]][+f[]][+h|l|r[]|+t|+z[]][+j[]]"); GMT_Usage (API, -2, "Specify values for text attributes that apply to all text records:"); GMT_Usage (API, 3, "+a Specify baseline for all text [0]."); @@ -482,6 +497,7 @@ static int parse (struct GMT_CTRL *GMT, struct PSTEXT_CTRL *Ctrl, struct GMT_OPT if (opt->arg[j] == 'v') { /* Want to draw a line from point to offset point */ Ctrl->D.line = true; n_errors += gmt_M_check_condition(GMT, opt->arg[j+1] && gmt_getpen (GMT, &opt->arg[j+1], &Ctrl->D.pen), "Option -D: Give pen after +v\n"); + n_errors += gmt_M_check_condition(GMT, Ctrl->D.pen.end[1].V != NULL, "Option -D: A vector head can only be placed at the data point end of the line; use pen modifier +vb\n"); if (opt->arg[j-1] == '+') /* New-style syntax */ opt->arg[j-1] = 0; else @@ -1006,10 +1022,8 @@ EXTERN_MSC int GMT_pstext (void *V_API, int mode, void *args) { offset[0] = T.x_space; offset[1] = T.y_space; } - if (T.boxflag & 32) { /* Draw line from original point to shifted location, clipped at textbox edge */ - gmt_setpen(GMT, &T.vecpen); - PSL_plotline_clipped_by_textbox(PSL, xx[0], yy[0], xx[1], yy[1], T.font.size, use_text, T.paragraph_angle, T.block_justify, offset); - } + if (T.boxflag & 32) /* Draw line from original point to shifted location, clipped at textbox edge */ + pstext_draw_leader (GMT, &T, xx, yy, use_text, offset); if (!Ctrl->G.mode && T.boxflag & 3) { /* Plot the box beneath the text */ if (Ctrl->S.active) { /* Lay down shaded box first */ PSL_setfill (PSL, Ctrl->S.fill.rgb, 0); /* shade color */ @@ -1485,10 +1499,8 @@ EXTERN_MSC int GMT_pstext (void *V_API, int mode, void *args) { offset[0] = T.x_space; offset[1] = T.y_space; } - if (T.boxflag & 32) { /* Draw line from original point to shifted location, clipped at textbox edge */ - gmt_setpen (GMT, &T.vecpen); - PSL_plotline_clipped_by_textbox(PSL, xx[0], yy[0], xx[1], yy[1], T.font.size, use_text, T.paragraph_angle, T.block_justify, offset); - } + if (T.boxflag & 32) /* Draw line from original point to shifted location, clipped at textbox edge */ + pstext_draw_leader (GMT, &T, xx, yy, use_text, offset); if (!Ctrl->G.mode && T.boxflag & 3) { /* Plot the box beneath the text */ if (Ctrl->S.active) { /* Lay down shaded box first */ PSL_setfill (PSL, Ctrl->S.fill.rgb, 0); /* shade color */ diff --git a/src/psxy.c b/src/psxy.c index 9da764bda2e..058b30eed1c 100644 --- a/src/psxy.c +++ b/src/psxy.c @@ -470,52 +470,6 @@ GMT_LOCAL int psxy_plot_decorations (struct GMT_CTRL *GMT, struct GMT_DATASET *D return GMT_NOERROR; } -GMT_LOCAL void psxy_plot_end_vectors (struct GMT_CTRL *GMT, double *x, double *y, uint64_t n, struct GMT_PEN *P) { - /* Maybe add vector heads. Here, x,y are in inches on the plot */ - unsigned int k, current[2] = {0,0}, next[2] = {1,0}; - double dim[PSL_MAX_DIMS], angle, s, c, L; - char *end[2] = {"start", "end"}; - if (n < 2) return; /* No line of mine */ - if (P->end[0].V == NULL && P->end[1].V == NULL) return; /* No arrow heads requested */ - current[1] = (unsigned int)n-1; next[1] = (unsigned int)n-2; - PSL_command (GMT->PSL, "V\n"); - GMT->PSL->current.linewidth = -1.0; /* Will be changed by next PSL_setlinewidth */ - gmt_M_memset (dim, PSL_MAX_DIMS, double); - for (k = 0; k < 2; k++) { - if (P->end[k].V == NULL) continue; - P->end[k].V->v.status |= PSL_VEC_LINE; /* Flag so head is not skipped due to "short" vector */ - /* Add vector heads to this end */ - PSL_comment (GMT->PSL, "Add vector head to %s of line\n", end[k]); - angle = d_atan2d (y[current[k]] - y[next[k]], x[current[k]] - x[next[k]]); - sincosd (angle, &s, &c); - L = (P->end[k].V->v.v_kind[1] == PSL_VEC_TERMINAL) ? 1e-3 : P->end[k].length; - P->end[k].V->v.v_width = (float)(P->end[k].V->v.pen.width * GMT->session.u2u[GMT_PT][GMT_INCH]); /* Set symbol pen width */ - dim[PSL_VEC_XTIP] = x[current[k]] + c * L; - dim[PSL_VEC_YTIP] = y[current[k]] + s * L; - dim[PSL_VEC_TAIL_WIDTH] = P->end[k].V->v.v_width; - dim[PSL_VEC_HEAD_LENGTH] = P->end[k].V->v.h_length; - dim[PSL_VEC_HEAD_WIDTH] = P->end[k].V->v.h_width; - dim[PSL_VEC_HEAD_SHAPE] = P->end[k].V->v.v_shape; - dim[PSL_VEC_STATUS] = (double)P->end[k].V->v.status; - dim[PSL_VEC_HEAD_TYPE_BEGIN] = (double)P->end[k].V->v.v_kind[0]; - dim[PSL_VEC_HEAD_TYPE_END] = (double)P->end[k].V->v.v_kind[1]; - dim[PSL_VEC_TRIM_BEGIN] = (double)P->end[k].V->v.v_trim[0]; - dim[PSL_VEC_TRIM_END] = (double)P->end[k].V->v.v_trim[1]; - if (P->end[k].V->v.status & PSL_VEC_OUTLINE2) { /* Gave specific head outline pen */ - PSL_defpen (GMT->PSL, "PSL_vecheadpen", P->end[k].V->v.pen.width, P->end[k].V->v.pen.style, P->end[k].V->v.pen.offset, P->end[k].V->v.pen.rgb); - dim[PSL_VEC_HEAD_PENWIDTH] = P->end[k].V->v.pen.width; - } - else { /* Set default based on line pen */ - PSL_defpen (GMT->PSL, "PSL_vecheadpen", 0.5 * P->width, P->style, P->offset, P->rgb); - dim[PSL_VEC_HEAD_PENWIDTH] = 0.5 * P->width; - } - gmt_setfill (GMT, &P->end[k].V->v.fill, (P->end[k].V->v.status & PSL_VEC_OUTLINE2) == PSL_VEC_OUTLINE2); - PSL_plotsymbol (GMT->PSL, x[current[k]], y[current[k]], dim, PSL_VECTOR); - } - GMT->PSL->current.linewidth = -1.0; /* Will be changed by next PSL_setlinewidth */ - PSL_command (GMT->PSL, "U\n"); -} - GMT_LOCAL bool psxy_is_stroke_symbol (int symbol) { /* Return true if cross, x, y, - symbols */ if (symbol == PSL_CROSS) return true; @@ -3260,7 +3214,7 @@ EXTERN_MSC int GMT_psxy (void *V_API, int mode, void *args) { else if (draw_line) { if ((GMT->current.plot.n = gmt_geo_to_xy_line (GMT, L->data[GMT_X], L->data[GMT_Y], L->n_rows)) == 0) continue; if (S.symbol != GMT_SYMBOL_FRONT || !S.f.invisible) gmt_plot_line (GMT, GMT->current.plot.x, GMT->current.plot.y, GMT->current.plot.pen, GMT->current.plot.n, current_pen.mode); - psxy_plot_end_vectors (GMT, GMT->current.plot.x, GMT->current.plot.y, GMT->current.plot.n, ¤t_pen); /* Maybe add vector heads */ + gmt_plot_end_vectors (GMT, GMT->current.plot.x, GMT->current.plot.y, GMT->current.plot.n, ¤t_pen); /* Maybe add vector heads */ } } if (S.symbol == GMT_SYMBOL_FRONT) { /* Must also draw fault crossbars */ From 81b7f04941f64ba7f4f8bae8ce9c6621a19c6754 Mon Sep 17 00:00:00 2001 From: Esteban82 Date: Wed, 14 Oct 2026 17:10:44 -0300 Subject: [PATCH 2/5] text: Support the +o pen modifier on the -D+v leader at the data point +o (c|i|p) stops the leader, and its +vb head, that distance short of the data point, e.g. to clear a symbol plotted there, as plot -W does. Only the data-point end is affected (a single value also sets the text end, which is ignored), other units and negative offsets are rejected, and nothing is drawn if the offset reaches the text. Without +o the output is unchanged. Co-Authored-By: Claude Sonnet 5.5 --- doc/rst/source/text.rst | 3 +++ src/pstext.c | 17 +++++++++++------ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/doc/rst/source/text.rst b/doc/rst/source/text.rst index a3ccda470f9..829fe80b688 100644 --- a/doc/rst/source/text.rst +++ b/doc/rst/source/text.rst @@ -141,6 +141,9 @@ Optional Arguments append a *pen* to change the attributes for this line. To place an arrow head at the original point, add the pen modifier **+vb**\ *vspecs* (e.g., **-D**\ 1c+v1p,red+vb0.3c+gred); a head at the text end is not supported. + Append the pen modifier **+o**\ *offset* (in **c**\|\ **i**\|\ **p**) to stop the line, and + its head, that distance short of the point, e.g., to clear a symbol drawn there; + only the data-point end is affected. See :ref:`Technical reference - Drawing of Vectors`. **Note**: The **-Dj**\|\ **J** selection cannot be used with paragraph mode (|-M|). diff --git a/src/pstext.c b/src/pstext.c index d46144ff73b..13000b3e2ef 100644 --- a/src/pstext.c +++ b/src/pstext.c @@ -170,12 +170,15 @@ static void Free_Ctrl (struct GMT_CTRL *GMT, struct PSTEXT_CTRL *C) { /* Dealloc } GMT_LOCAL void pstext_draw_leader (struct GMT_CTRL *GMT, struct PSTEXT_INFO *T, double *xx, double *yy, char *text, double offset[]) { - /* Draw leader line from the data point (xx[0],yy[0]) to the text at (xx[1],yy[1]), with optional vector head at the data point */ - double x[2] = {xx[0], xx[1]}, y[2] = {yy[0], yy[1]}, s, c; - bool head = (T->vecpen.end[0].V && hypot (x[1] - x[0], y[1] - y[0]) > T->vecpen.end[0].length); /* Skip head if longer than the line */ - if (head && T->vecpen.end[0].length > 0.0) { /* Shorten the line so it stops at the base of the vector head */ + /* Draw leader line from the data point (xx[0],yy[0]) to the text at (xx[1],yy[1]), with optional trim (+o) and vector head at the data point */ + double x[2] = {xx[0], xx[1]}, y[2] = {yy[0], yy[1]}, s, c, shift; + double trim = T->vecpen.end[0].offset, L = (T->vecpen.end[0].V) ? T->vecpen.end[0].length : 0.0, d = hypot (x[1] - x[0], y[1] - y[0]); + bool head = (T->vecpen.end[0].V && d > trim + L); /* Skip head if it does not fit */ + if (d <= trim) return; /* Trimmed away the entire leader */ + shift = trim + ((head) ? L : 0.0); /* The line starts after the offset and, with a head, after the head */ + if (shift > 0.0) { sincosd (d_atan2d (y[0] - y[1], x[0] - x[1]), &s, &c); - x[0] -= c * T->vecpen.end[0].length; y[0] -= s * T->vecpen.end[0].length; + x[0] -= c * shift; y[0] -= s * shift; } gmt_setpen (GMT, &T->vecpen); PSL_plotline_clipped_by_textbox (GMT->PSL, x[0], y[0], x[1], y[1], T->font.size, text, T->paragraph_angle, T->block_justify, offset); @@ -372,7 +375,7 @@ static int usage (struct GMTAPI_CTRL *API, int level) { "Use -Dj to move text origin away from point (direction determined by text's justification). " "Upper case -DJ will shorten diagonal shifts at corners by sqrt(2). Cannot be used with -M. Optional modifiers:"); GMT_Usage (API, 3, "+f: Read per-record offsets (dx dy) from (two columns in current distance units); overrides fixed /."); - GMT_Usage (API, 3, "+v: Draw line from text to original point; optionally append a [%s]. Add pen modifier +vb for a vector head at the point.", gmt_putpen (API->GMT, &API->GMT->current.setting.map_default_pen)); + GMT_Usage (API, 3, "+v: Draw line from text to original point; optionally append a [%s]. Add pen modifier +vb for a vector head at the point, and +o (c|i|p) to stop the line short of it.", gmt_putpen (API->GMT, &API->GMT->current.setting.map_default_pen)); GMT_Usage (API, 1, "\n-F[+a[]][+c[]][+f[]][+h|l|r[]|+t|+z[]][+j[]]"); GMT_Usage (API, -2, "Specify values for text attributes that apply to all text records:"); GMT_Usage (API, 3, "+a Specify baseline for all text [0]."); @@ -497,6 +500,8 @@ static int parse (struct GMT_CTRL *GMT, struct PSTEXT_CTRL *Ctrl, struct GMT_OPT if (opt->arg[j] == 'v') { /* Want to draw a line from point to offset point */ Ctrl->D.line = true; n_errors += gmt_M_check_condition(GMT, opt->arg[j+1] && gmt_getpen (GMT, &opt->arg[j+1], &Ctrl->D.pen), "Option -D: Give pen after +v\n"); + n_errors += gmt_M_check_condition(GMT, Ctrl->D.pen.end[0].offset != 0.0 && Ctrl->D.pen.end[0].unit != 'C', "Option -D: Pen modifier +o must be given in plot units (c|i|p)\n"); + n_errors += gmt_M_check_condition(GMT, Ctrl->D.pen.end[0].offset < 0.0, "Option -D: Pen modifier +o cannot be negative\n"); n_errors += gmt_M_check_condition(GMT, Ctrl->D.pen.end[1].V != NULL, "Option -D: A vector head can only be placed at the data point end of the line; use pen modifier +vb\n"); if (opt->arg[j-1] == '+') /* New-style syntax */ opt->arg[j-1] = 0; From 2bc76aab5874de7f18212b8bb495917f1879b5bd Mon Sep 17 00:00:00 2001 From: Esteban82 Date: Wed, 14 Oct 2026 17:16:44 -0300 Subject: [PATCH 3/5] text: Do not inherit MAP_DEFAULT_PEN line-end settings in the -D leader pen The -D pen starts as a copy of MAP_DEFAULT_PEN, so a +o or +v in that default leaked into the leader and could even trigger the new -D errors (e.g. MAP_DEFAULT_PEN=1p+o100k). Clear all line-end settings instead of only the head pointers; master never applied them to the leader. Co-Authored-By: Claude Opus 5.5 --- src/pstext.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pstext.c b/src/pstext.c index 13000b3e2ef..79a04acae49 100644 --- a/src/pstext.c +++ b/src/pstext.c @@ -145,7 +145,7 @@ static void *New_Ctrl (struct GMT_CTRL *GMT) { /* Allocate and initialize a new /* Initialize values whose defaults are not 0/false/NULL */ C->D.pen = C->W.pen = GMT->current.setting.map_default_pen; - C->D.pen.end[0].V = C->D.pen.end[1].V = NULL; /* Do not share (and later free) any vector heads of the default pen */ + gmt_M_memset (C->D.pen.end, 2, struct GMT_LINE_END); /* Leader ignores the default pen trims and heads (and must not free its heads) */ C->C.dx = C->C.dy = GMT_TEXT_CLEARANCE; /* 15% of font size is default clearance */ C->C.percent = true; C->C.mode = 'o'; /* Rectangular box shape */ From e3238de1c900972e1308f1eb9c366f70cc5dbf59 Mon Sep 17 00:00:00 2001 From: Esteban82 Date: Wed, 14 Oct 2026 17:29:08 -0300 Subject: [PATCH 4/5] text: Trim the -D+v leader with gmt_trim_line, as plot does for -W+o/+v Replace the hand-made +o handling with the existing gmt_trim_line, so +o accepts the same offsets as plot -W (plot units and map distances) and the head-length trim follows the same rules. The leader is sampled at 101 vertices because gmt_trim_line treats reaching the last vertex as trimming away the whole line. The text end is never trimmed since the box clipping is relative to it. A leader shorter than the offset plus head is not drawn, as in plot. Co-Authored-By: Claude Sonnet 5.5 --- doc/rst/source/text.rst | 6 +++--- src/pstext.c | 45 ++++++++++++++++++++++++++++------------- 2 files changed, 34 insertions(+), 17 deletions(-) diff --git a/doc/rst/source/text.rst b/doc/rst/source/text.rst index 829fe80b688..286b7b8a389 100644 --- a/doc/rst/source/text.rst +++ b/doc/rst/source/text.rst @@ -141,9 +141,9 @@ Optional Arguments append a *pen* to change the attributes for this line. To place an arrow head at the original point, add the pen modifier **+vb**\ *vspecs* (e.g., **-D**\ 1c+v1p,red+vb0.3c+gred); a head at the text end is not supported. - Append the pen modifier **+o**\ *offset* (in **c**\|\ **i**\|\ **p**) to stop the line, and - its head, that distance short of the point, e.g., to clear a symbol drawn there; - only the data-point end is affected. + Append the pen modifier **+o**\ *offset* (as for |-W| in :doc:`plot`) to stop the line, and + its head, that distance short of the point, e.g., to clear a symbol drawn there. + Only the data-point end is affected. See :ref:`Technical reference - Drawing of Vectors`. **Note**: The **-Dj**\|\ **J** selection cannot be used with paragraph mode (|-M|). diff --git a/src/pstext.c b/src/pstext.c index 79a04acae49..fd0055cc787 100644 --- a/src/pstext.c +++ b/src/pstext.c @@ -169,20 +169,39 @@ static void Free_Ctrl (struct GMT_CTRL *GMT, struct PSTEXT_CTRL *C) { /* Dealloc gmt_M_free (GMT, C); } -GMT_LOCAL void pstext_draw_leader (struct GMT_CTRL *GMT, struct PSTEXT_INFO *T, double *xx, double *yy, char *text, double offset[]) { - /* Draw leader line from the data point (xx[0],yy[0]) to the text at (xx[1],yy[1]), with optional trim (+o) and vector head at the data point */ - double x[2] = {xx[0], xx[1]}, y[2] = {yy[0], yy[1]}, s, c, shift; - double trim = T->vecpen.end[0].offset, L = (T->vecpen.end[0].V) ? T->vecpen.end[0].length : 0.0, d = hypot (x[1] - x[0], y[1] - y[0]); - bool head = (T->vecpen.end[0].V && d > trim + L); /* Skip head if it does not fit */ - if (d <= trim) return; /* Trimmed away the entire leader */ - shift = trim + ((head) ? L : 0.0); /* The line starts after the offset and, with a head, after the head */ - if (shift > 0.0) { - sincosd (d_atan2d (y[0] - y[1], x[0] - x[1]), &s, &c); - x[0] -= c * shift; y[0] -= s * shift; +#define PSTEXT_LEADER_NP 101 /* Number of vertices used to trim the leader */ + +GMT_LOCAL bool pstext_trim_leader (struct GMT_CTRL *GMT, double *x, double *y, struct GMT_PEN *pen) { + /* Trim the data-point end of the leader (x[0],y[0]) -> (x[1],y[1]), in inches, with the pen's +o and +v via gmt_trim_line as plot does. + * The text end is never trimmed since the box clipping is relative to it. Returns true if nothing is left to draw. */ + uint64_t n = PSTEXT_LEADER_NP; + double *xx = gmt_M_memory (GMT, NULL, n, double), *yy = gmt_M_memory (GMT, NULL, n, double), xe, ye; + struct GMT_PEN P = *pen; /* Shallow copy so we can skip the text end */ + + P.end[1].offset = 0.0; + for (uint64_t k = 0; k < n; k++) { /* gmt_trim_line needs vertices to trim along, and treats reaching the last one as trimmed away */ + double f = (double)k / (double)(n - 1); + xx[k] = x[0] + f * (x[1] - x[0]); yy[k] = y[0] + f * (y[1] - y[0]); + } + for (uint64_t k = 0; k < n; k++) /* gmt_trim_line works in input coordinates */ + gmt_xy_to_geo (GMT, &xx[k], &yy[k], xx[k], yy[k]); + if (gmt_trim_line (GMT, &xx, &yy, &n, &P)) { /* Trimmed away completely */ + gmt_M_free (GMT, xx); gmt_M_free (GMT, yy); + return true; } + gmt_geo_to_xy (GMT, xx[0], yy[0], &xe, &ye); /* New start of the leader */ + x[0] = xe; y[0] = ye; + gmt_M_free (GMT, xx); gmt_M_free (GMT, yy); + return false; +} + +GMT_LOCAL void pstext_draw_leader (struct GMT_CTRL *GMT, struct PSTEXT_INFO *T, double *xx, double *yy, char *text, double offset[]) { + /* Draw leader line from the data point (xx[0],yy[0]) to the text at (xx[1],yy[1]), with optional +o trim and vector head at the data point */ + double x[2] = {xx[0], xx[1]}, y[2] = {yy[0], yy[1]}; + if (gmt_trim_requested (GMT, &T->vecpen) && pstext_trim_leader (GMT, x, y, &T->vecpen)) return; gmt_setpen (GMT, &T->vecpen); PSL_plotline_clipped_by_textbox (GMT->PSL, x[0], y[0], x[1], y[1], T->font.size, text, T->paragraph_angle, T->block_justify, offset); - if (head) gmt_plot_end_vectors (GMT, x, y, 2, &T->vecpen); + gmt_plot_end_vectors (GMT, x, y, 2, &T->vecpen); } GMT_LOCAL void pstext_output_words (struct GMT_CTRL *GMT, struct PSL_CTRL *PSL, double x, double y, char *text, struct PSTEXT_INFO *T, struct PSTEXT_CTRL *Ctrl) { @@ -375,7 +394,7 @@ static int usage (struct GMTAPI_CTRL *API, int level) { "Use -Dj to move text origin away from point (direction determined by text's justification). " "Upper case -DJ will shorten diagonal shifts at corners by sqrt(2). Cannot be used with -M. Optional modifiers:"); GMT_Usage (API, 3, "+f: Read per-record offsets (dx dy) from (two columns in current distance units); overrides fixed /."); - GMT_Usage (API, 3, "+v: Draw line from text to original point; optionally append a [%s]. Add pen modifier +vb for a vector head at the point, and +o (c|i|p) to stop the line short of it.", gmt_putpen (API->GMT, &API->GMT->current.setting.map_default_pen)); + GMT_Usage (API, 3, "+v: Draw line from text to original point; optionally append a [%s]. Add pen modifier +vb for a vector head at the point, and +o (as for plot -W) to stop the line short of it.", gmt_putpen (API->GMT, &API->GMT->current.setting.map_default_pen)); GMT_Usage (API, 1, "\n-F[+a[]][+c[]][+f[]][+h|l|r[]|+t|+z[]][+j[]]"); GMT_Usage (API, -2, "Specify values for text attributes that apply to all text records:"); GMT_Usage (API, 3, "+a Specify baseline for all text [0]."); @@ -500,8 +519,6 @@ static int parse (struct GMT_CTRL *GMT, struct PSTEXT_CTRL *Ctrl, struct GMT_OPT if (opt->arg[j] == 'v') { /* Want to draw a line from point to offset point */ Ctrl->D.line = true; n_errors += gmt_M_check_condition(GMT, opt->arg[j+1] && gmt_getpen (GMT, &opt->arg[j+1], &Ctrl->D.pen), "Option -D: Give pen after +v\n"); - n_errors += gmt_M_check_condition(GMT, Ctrl->D.pen.end[0].offset != 0.0 && Ctrl->D.pen.end[0].unit != 'C', "Option -D: Pen modifier +o must be given in plot units (c|i|p)\n"); - n_errors += gmt_M_check_condition(GMT, Ctrl->D.pen.end[0].offset < 0.0, "Option -D: Pen modifier +o cannot be negative\n"); n_errors += gmt_M_check_condition(GMT, Ctrl->D.pen.end[1].V != NULL, "Option -D: A vector head can only be placed at the data point end of the line; use pen modifier +vb\n"); if (opt->arg[j-1] == '+') /* New-style syntax */ opt->arg[j-1] = 0; From 091afe82e8242716ad8b877621e4a8c8a3ca1ba2 Mon Sep 17 00:00:00 2001 From: Esteban82 Date: Wed, 14 Oct 2026 17:34:47 -0300 Subject: [PATCH 5/5] text: Draw the leader untrimmed when its end cannot be inverse projected With -F+c corner anchoring on maps whose bounding-box corners fall outside the projection domain (e.g. -JG), the NaN coordinates made gmt_trim_line collapse the leader to nothing. Skip the trim in that case. Also document that +o goes before +vb and which line attributes it follows. Co-Authored-By: Claude Sonnet 5.5 --- doc/rst/source/text.rst | 7 ++++--- src/pstext.c | 9 +++++++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/doc/rst/source/text.rst b/doc/rst/source/text.rst index 286b7b8a389..a8b14b4007a 100644 --- a/doc/rst/source/text.rst +++ b/doc/rst/source/text.rst @@ -141,9 +141,10 @@ Optional Arguments append a *pen* to change the attributes for this line. To place an arrow head at the original point, add the pen modifier **+vb**\ *vspecs* (e.g., **-D**\ 1c+v1p,red+vb0.3c+gred); a head at the text end is not supported. - Append the pen modifier **+o**\ *offset* (as for |-W| in :doc:`plot`) to stop the line, and - its head, that distance short of the point, e.g., to clear a symbol drawn there. - Only the data-point end is affected. + Add the pen modifier **+o**\ *offset*, given *before* **+vb**\ (e.g., **-D**\ 1c+v1p,red+o0.1c+vb0.3c+gred), + to stop the line, and its head, that distance short of the point, e.g., to clear a symbol drawn there; + the offset is as for :ref:`line attributes ` + in :doc:`plot`. Only the data-point end is affected. See :ref:`Technical reference - Drawing of Vectors`. **Note**: The **-Dj**\|\ **J** selection cannot be used with paragraph mode (|-M|). diff --git a/src/pstext.c b/src/pstext.c index fd0055cc787..9a0efc2ef54 100644 --- a/src/pstext.c +++ b/src/pstext.c @@ -183,8 +183,13 @@ GMT_LOCAL bool pstext_trim_leader (struct GMT_CTRL *GMT, double *x, double *y, s double f = (double)k / (double)(n - 1); xx[k] = x[0] + f * (x[1] - x[0]); yy[k] = y[0] + f * (y[1] - y[0]); } - for (uint64_t k = 0; k < n; k++) /* gmt_trim_line works in input coordinates */ + for (uint64_t k = 0; k < n; k++) { /* gmt_trim_line works in input coordinates */ gmt_xy_to_geo (GMT, &xx[k], &yy[k], xx[k], yy[k]); + if (gmt_M_is_dnan (xx[k]) || gmt_M_is_dnan (yy[k])) { /* Not invertible (e.g., corner outside the map domain): draw untrimmed */ + gmt_M_free (GMT, xx); gmt_M_free (GMT, yy); + return false; + } + } if (gmt_trim_line (GMT, &xx, &yy, &n, &P)) { /* Trimmed away completely */ gmt_M_free (GMT, xx); gmt_M_free (GMT, yy); return true; @@ -394,7 +399,7 @@ static int usage (struct GMTAPI_CTRL *API, int level) { "Use -Dj to move text origin away from point (direction determined by text's justification). " "Upper case -DJ will shorten diagonal shifts at corners by sqrt(2). Cannot be used with -M. Optional modifiers:"); GMT_Usage (API, 3, "+f: Read per-record offsets (dx dy) from (two columns in current distance units); overrides fixed /."); - GMT_Usage (API, 3, "+v: Draw line from text to original point; optionally append a [%s]. Add pen modifier +vb for a vector head at the point, and +o (as for plot -W) to stop the line short of it.", gmt_putpen (API->GMT, &API->GMT->current.setting.map_default_pen)); + GMT_Usage (API, 3, "+v: Draw line from text to original point; optionally append a [%s]. Add pen modifier +vb for a vector head at the point, and, before +vb, +o (as for plot -W) to stop the line short of it.", gmt_putpen (API->GMT, &API->GMT->current.setting.map_default_pen)); GMT_Usage (API, 1, "\n-F[+a[]][+c[]][+f[]][+h|l|r[]|+t|+z[]][+j[]]"); GMT_Usage (API, -2, "Specify values for text attributes that apply to all text records:"); GMT_Usage (API, 3, "+a Specify baseline for all text [0].");