Skip to content

Commit 9c25c6b

Browse files
authored
Add a 'pagebg' option that lets users paint the background paper with an image. (#1587)
* Add tests * Export flatfv * Let 'opt' in scan_opt() have more than 2 chars. * Move a developing function to elsewhere. * Add a maskregion() function. * Add a flatfv() function (renamed from imgfv) * Add a 'pagebg' option that lets users paint the background paper with an image.
1 parent 72b3f89 commit 9c25c6b

File tree

8 files changed

+257
-107
lines changed

8 files changed

+257
-107
lines changed

src/GMT.jl

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ struct CTRLstruct
1515
pocket_call::Vector{Any} # To temporarily store data needed by modules sub-calls. Put in [3] for pre-calls
1616
pocket_B::Vector{String} # To temporarily store opt_B grid and fill color to be reworked in psclip
1717
pocket_J::Vector{String} # To temporarily store opt_J and fig size to eventualy flip directions (y + down, etc)
18+
# = [opt_J width opt_Jz codes-to-tell-which-axis-to-reverse]
1819
pocket_R::Vector{String} # To temporarily store opt_R
1920
XYlabels::Vector{String} # To temporarily store the x,y col names to let x|y labels know what to plot (if "auto")
2021
IamInPaperMode::Vector{Bool} # A 2 elem vec to know if we are in under-the-hood paper mode. 2nd traces if first call
@@ -185,8 +186,8 @@ export
185186

186187
lazinfo, lazread, lazwrite, lasread, laswrite,
187188

188-
cube, cylinder, circlepts, dodecahedron, ellipse3D, eulermat, icosahedron, loft, sphere, spinmat, octahedron,
189-
tetrahedron, torus, replicant, revolve, rotate, rotate!, translate, translate!,
189+
cube, cylinder, circlepts, dodecahedron, ellipse3D, eulermat, flatfv, icosahedron, loft, sphere, spinmat,
190+
octahedron, tetrahedron, torus, replicant, revolve, rotate, rotate!, translate, translate!,
190191

191192
df2ds, ds2df, extrude, fv2fv, isclockwise, surf2fv, ODE2ds,
192193
@?, @dir

src/common_options.jl

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4972,16 +4972,22 @@ end
49724972
"""
49734973
str = scan_opt(cmd::AbstractString, opt::String, keepX=false)
49744974
4975-
Scans the CMD string for the OPT option. Note, OPT must be a 2 chars -X GMT option.
4975+
Scans the CMD string for the OPT option. Note, OPT must be at least a 2 chars -X GMT option.
49764976
'keepX' retains the OPT 2 chars -X GMT option in output.
49774977
49784978
### Example
4979+
```julia
49794980
scan_opt(" -Baf", "-B", true)
49804981
" -Baf"
4982+
4983+
scan_opt(" -R -JX -JZ4", "-JZ")
4984+
"4"
4985+
```
49814986
"""
49824987
function scan_opt(cmd::AbstractString, opt::String, keepX::Bool=false)::String
4983-
out::String = ((ind = findfirst(opt, cmd)) !== nothing) ? (ind[end] == length(cmd)) ? "" : strtok(cmd[ind[1]+2:end])[1] : ""
4984-
(out != "" && cmd[ind[1]+2] == ' ') && (out = "") # Because seeking -R in a " -R -JX" would ret "-JX"
4988+
len = length(opt)
4989+
out::String = ((ind = findfirst(opt, cmd)) !== nothing) ? (ind[end] == length(cmd)) ? "" : strtok(cmd[ind[1]+len:end])[1] : ""
4990+
(out != "" && cmd[ind[1]+len] == ' ') && (out = "") # Because seeking -R in a " -R -JX" would ret "-JX"
49854991
(keepX && out != "") && (out = string(' ', opt, out)) # Keep the option flag in output
49864992
return out
49874993
end

src/psxy.jl

Lines changed: 104 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -158,8 +158,8 @@ function common_plot_xyz(cmd0::String, arg1, caller::String, first::Bool, is3D::
158158
cmd = replace(cmd, opt_J => " -JX" * split(DEF_FIG_SIZE, '/')[1] * "/0") # If projected, it's a axis equal for sure
159159
end
160160
if (is3D && isempty(opt_JZ) && length(collect(eachmatch(r"/", opt_R))) == 5)
161-
if O opt_JZ = (CTRL.pocket_J[3] != "") ? CTRL.pocket_J[3][1:4] : " -JZ"
162-
else opt_JZ = CTRL.pocket_J[3] = (is_gridtri) ? " -JZ5c" : " -JZ6c" # Arbitrary and not satisfactory for all cases.
161+
if (O) opt_JZ = (CTRL.pocket_J[3] != "") ? CTRL.pocket_J[3][1:4] : " -JZ"
162+
else opt_JZ = CTRL.pocket_J[3] = (is_gridtri) ? " -JZ5c" : " -JZ6c" # Arbitrary and not satisfactory for all cases.
163163
end
164164
cmd *= opt_JZ # Default -JZ
165165
end
@@ -339,6 +339,7 @@ function common_plot_xyz(cmd0::String, arg1, caller::String, first::Bool, is3D::
339339
end
340340

341341
_cmd = fish_bg(d, _cmd) # See if we have a "pre-command"
342+
_cmd = fish_pagebg(d, _cmd, autoJZ=(is3D && axis_equal)) # Last arg tells if JZ was computed automatically
342343

343344
isa(arg1, GDtype) && plt_txt_attrib!(arg1, d, _cmd) # Function barrier to plot TEXT attributed labels (in case)
344345

@@ -584,23 +585,111 @@ function with_xyvar(d::Dict, arg1::GMTdataset, no_x::Bool=false)::Union{GMTdatas
584585
end
585586

586587
# ---------------------------------------------------------------------------------------------------
588+
"""
589+
cmd = fish_pagebg(d::Dict, cmd::Vector{String}) -> Vector{String}
590+
591+
Check if using a background image to replace the page color.
592+
593+
This function checks for the presence of a `pagebg` option that sets the page background image.
594+
Note that this different from the `background` or `bg` option that sets the plotting canvas background color.
595+
596+
- `pagebg`: a NamedTuple with the following members
597+
- `image`: the image name or a GMTimage/GMTgrid object
598+
- `width`: the width of the background image in percentage of the page width (default: 0.8)
599+
- `offset`: the offset of the background image in percentage of the page width (default: (0.0,0.0))
600+
If only one value is provided it is used for the X offset only.
601+
602+
OR
603+
604+
- `pagebg`: an image file name or a GMTimage/GMTgrid object
605+
In this case the above defaults for the _width_ and _offset_ parameters are used
606+
"""
607+
function fish_pagebg(d::Dict, cmd::Vector{String}; autoJZ::Bool=true)::Vector{String}
608+
((val = find_in_dict(d, [:pagebg])[1]) === nothing) && return cmd
609+
width::Float64 = 0.8; off_X::Float64 = 0.0; off_Y::Float64 = 0.0 # The off's are offsets from the center
610+
if isa(val, NamedTuple)
611+
!haskey(val, :image) && error("pagebg: NamedTuple must contain the member 'image'")
612+
fname = helper_fish_bgs(val[:image]) # Get the image name or set it under the hood if input is a GMTimage
613+
haskey(val, :width) && (width = val[:width])
614+
(width <= 0 || width > 1) && error("pagebg: width is a normalized value, must be between 0 and 1")
615+
if (haskey(val, :offset) || haskey(val, :off))
616+
off = (haskey(val, :offset)) ? val[:offset] : val[:off]
617+
isa(off, Real) ? (off_X = off) : length(off) == 2 ? (off_X = off[1]; off_Y = off[2]) :
618+
error("pagebg: offset must be a Real or a two elements Array/Tuple")
619+
end
620+
else # Here, val is just the file name or a GMTimage
621+
fname = helper_fish_bgs(val) # Get the image name or set it under the hood if input is a GMTimage
622+
end
623+
624+
if contains(CTRL.pocket_J[2], "/") Wt, Ht = split(CTRL.pocket_J[2], '/')
625+
else Wt = CTRL.pocket_J[2]; Ht = "/0"
626+
end
627+
isletter(Wt[end]) ? (cw=Wt[end]; Wt = Wt[1:end-1]) : (cw = 'c')
628+
isletter(Ht[end]) ? (ch=Ht[end]; Ht = Ht[1:end-1]) : (ch = 'c'); (Ht == "0") && (Ht = "/0")
629+
W = parse(Float64, Wt)
630+
if (Ht != "" && Ht != "/0") # User gave an explicit height
631+
H = parse(Float64, Ht) * width # r = H / W; H = r * width * W; = H * width
632+
Ht = @sprintf("/%.4g%c", H, ch)
633+
end
634+
635+
off_XY = @sprintf(" -X%.4g%c", off_X + (1-width)/2 * W, cw)
636+
(off_Y != 0.0) && (off_XY *= @sprintf(" -Y%.4g%c", off_Y, cw))
637+
opt_J = scan_opt(cmd[1], "-J", true)
638+
new_J = string(opt_J[1:4], width * W, cw, Ht)
639+
640+
# If the 'bg' option is also set it sits in cmd[1] and then we want to modify cmd[2].
641+
ind_cmd = startswith(cmd[1], "grdimage") ? 2 : 1 # The presence of 'grdimage' says that 'bg' was used.
642+
643+
cmd[ind_cmd] = replace(cmd[ind_cmd], opt_J => new_J * off_XY)
644+
if (autoJZ && (opt_JZ = scan_opt(cmd[1], "-JZ", true)) != "") # Only do this for JZ that was set automatically
645+
z = parse(Float64, isletter(opt_JZ[end]) ? opt_JZ[5:end-1] : opt_JZ[5:end]) * width
646+
CTRL.pocket_J[3] = @sprintf(" -JZ%.4g%c", z, cw)
647+
cmd[ind_cmd] = replace(cmd[ind_cmd], opt_JZ => CTRL.pocket_J[3])
648+
end
649+
proggy = IamModern[1] ? "image " : "psimage "
650+
[proggy * fname * CTRL.pocket_J[1] * CTRL.pocket_R[1] * " -Dx0/0+w"*Wt*cw, cmd...]
651+
end
652+
653+
# ---------------------------------------------------------------------------------------------------
654+
"""
655+
cmd = fish_bg(d::Dict, cmd::Vector{String}) -> Vector{String}
656+
657+
Check if a background image in a plot area is requested.
658+
659+
Check if the background image is used and if yes insert a first command that calls grdimage to fill
660+
the canvas with that bg image. The BG image can be a file name, the name of one of the pre-defined
661+
functions, or a GMTgrid/GMTimage object. By default we use a trimmed gray scale (between ~64 & 240)
662+
but if user wants to control the colormap then the option's argument can be a tuple where the second
663+
element is cpt name or a GMTcpt obj.
664+
665+
### Example
666+
667+
```julia
668+
plot(rand(8,2), bg=(:somb, :turbo), show=1)
669+
670+
# To revert the sense of the color progression prefix the cpt name or of the pre-def function with a '-'
671+
672+
plot(rand(8,2), bg="-circ", show=1)
673+
```
674+
"""
587675
function fish_bg(d::Dict, cmd::Vector{String})::Vector{String}
588-
# Check if the background image is used and if yes insert a first command that calls grdimage to fill
589-
# the canvas with that bg image. The BG image can be a file name, the name of one of the pre-defined
590-
# functions, or a GMTgrid/GMTimage object.
591-
# By default we use a trimmed gray scale (between ~64 & 240) but if user wants to control the colormap
592-
# then the option's argument can be a tuple where the second element is cpt name or a GMTcpt obj.
593-
# Ex: plot(rand(8,2), bg=(:somb, :turbo), show=1)
594-
# To revert the sense of the color progression prefix the cpt name or of the pre-def function with a '-'
595-
# Ex: plot(rand(8,2), bg="-circ", show=1)
596676
((val = find_in_dict(d, [:bg :background])[1]) === nothing) && return cmd
677+
fname = helper_fish_bgs(val)
678+
679+
opt_p = scan_opt(cmd[1], "-p", true); opt_c = scan_opt(cmd[1], "-c", true)
680+
opt_D = (IamModern[1]) ? " -Dr " : " -D " # Found this difference by experience. It might break in future GMTs
681+
["grdimage" * opt_D * fname * CTRL.pocket_J[1] * opt_p * opt_c, cmd...]
682+
end
683+
684+
function helper_fish_bgs(val)::String
597685
arg1, arg2 = isa(val, Tuple) ? val[:] : (val, nothing)
598-
(arg2 !== nothing && (!isa(arg2, GMTcpt) && !isa(arg2, StrSymb))) &&error("When a Tuple is used in argument of the background image option, the second element must be a string or a GMTcpt object.")
686+
(arg2 !== nothing && (!isa(arg2, GMTcpt) && !isa(arg2, StrSymb))) &&
687+
error("When a Tuple is used in argument of the background image option, the second element must be a string or a GMTcpt object.")
599688
gotfname, fname::String, opt_I::String = false, "", ""
600689
if (isa(arg1, StrSymb))
601-
if (splitext(string(arg1)::String)[2] != "") # Assumed to be an image file name
690+
if (splitext(string(arg1)::String)[2] != "") # Assumed to be an image file name
602691
fname, gotfname = arg1, true
603-
else # A pre-set fun name
692+
else # A pre-set fun name
604693
fun::String = string(arg1)
605694
(fun[1] == '-') && (fun = fun[2:end]; opt_I = " -I")
606695
I::GMTimage = imagesc(mat2grid(fun))
@@ -617,12 +706,8 @@ function fish_bg(d::Dict, cmd::Vector{String})::Vector{String}
617706
image_cpt!(I, C)
618707
CTRL.pocket_call[3] = I # This signals finish_PS_module() to run _cmd first
619708
end
620-
621-
opt_p = scan_opt(cmd[1], "-p"); (opt_p != "") && (opt_p = " -p" * opt_p)
622-
opt_c = scan_opt(cmd[1], "-c"); (opt_c != "") && (opt_c = " -c" * opt_c)
623-
#(opt_c == "" && contains(cmd[1], " -c ")) && (opt_c = " -c") # Because of a scan_opt() desing error (but causes error)
624-
opt_D = (IamModern[1]) ? " -Dr " : " -D " # Found this difference by experience. It might break in future GMTs
625-
["grdimage" * opt_D * fname * CTRL.pocket_J[1] * opt_p * opt_c, cmd...]
709+
FIG_MARGIN[1] = 0
710+
return fname
626711
end
627712

628713
# ---------------------------------------------------------------------------------------------------

src/rasterpolygonfuns.jl

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,3 +293,23 @@ function meansqrt(x)
293293
end
294294
sqrt(sum/length(x))
295295
end
296+
297+
# ---------------------------------------------------------------------------------------------------
298+
"""
299+
mask = maskregion(X::Matrix{T}, Y::Matrix{T}, shape) where {T <: AbstractFloat}
300+
"""
301+
function maskregion(X::Matrix{T}, Y::Matrix{T}, shape) where {T <: AbstractFloat}
302+
# Need to add more methods and documentation
303+
@assert length(X) == length(Y) "X and Y matrices have different sizes"
304+
masca = BitArray(undef,size(X).-1)
305+
n_rows, n_cols = size(masca)
306+
# A a bug in 'pip'. pip(-0.5011, -3.469446951953614e-17, shape) = -1 but pip(-0.5011, 0, shape) = 1
307+
dx = (X[1,2]-X[1,1]) / 2.0
308+
dy = (Y[2,1]-Y[1,1]) / 2.0
309+
Threads.@threads for col = 1:n_cols
310+
for row = 1:n_rows
311+
@inbounds masca[row,col] = pip(X[row,col]+dx, Y[row,col]+dy, shape) >= 0
312+
end
313+
end
314+
return masca
315+
end

src/solids.jl

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -677,3 +677,94 @@ function loft(C1, C2; n_steps::Int=0, closed=true, type=:quad)
677677

678678
surf2fv(X, Y, Z; type=type, bfculling=(closed != 1), top=top, bottom=bot)
679679
end
680+
681+
682+
# ---------------------------------------------------------------------------------------------------
683+
"""
684+
FV = flatfv(I::Union{GMTimage, AbstractString}; shape=nothing, level=0.0) ->GMTfv
685+
686+
Create a flat 3D surface from an image and a set of xyz or just xy coordinates.
687+
688+
This function creates face for each pixel in the image that is inside the `shape` and assigns the
689+
face's color from that of the image. So be careful that the image is not too large.
690+
691+
### Args
692+
- `I`: A `GMTimage` object or a file name of an image.
693+
694+
### Kwargs
695+
- `shape`: A xyz or xy polygon defining a flat surface. When it is a 3D polygon, it must lie in the xz or yz planes.
696+
But it can also be a can also be a Symbol; one of `:circle`, `:circ`, `:ellipse`. In this later case, we
697+
compute a normalized circle or ellipse with dimensions taken from number of rows and columns in `I`.
698+
The ellipse (with a horizontal major) eccentricity is computed from the ratio of the number of rows and columns.
699+
- `level`: In case that `shape` is a polygon in the xy plane, this is the level or height of that flat surface.
700+
For other plane orientations, this level is extracted from the column of constant values in `shape`.
701+
702+
Returns:
703+
- A `GMTfv` object representing the flat 3D surface.
704+
705+
### Example
706+
```julia
707+
FV = flatfv("image.png", shape=:circle, level=1.0);
708+
viz(FV)
709+
```
710+
"""
711+
function flatfv(I::Union{GMTimage, AbstractString}; shape=:n, level=0.0)::GMTfv
712+
713+
function forceRGB(I)::GMTimage{UInt8, 3}
714+
I_ = isa(I, GMTimage) ? I : gmtread(I)::GMTimage
715+
size(I_, 3) == 1 && (I_ = ind2rgb(I_))
716+
return I_
717+
end
718+
719+
_I = forceRGB(I)
720+
n_rows::Int, n_cols::Int = size(_I.image)
721+
722+
if (shape == :circle || shape == :circ || shape == :ellipse)
723+
X,Y = meshgrid(linspace(0, 1, n_cols+1), linspace(1, 0, n_rows+1))
724+
Z = fill(Float64(level), n_rows+1, n_cols+1)
725+
if (shape == :ellipse)
726+
e = sqrt(1 - ((n_rows+1) / (n_cols+1))^2)
727+
masca = maskregion(X, Y, ellipse3D(0.5; center=(0.5, 0.5), e=e)) # An horizontal ellipse
728+
else
729+
masca = maskregion(X, Y, circlepts(0.5; center=(0.5, 0.5))) # A normalized circle
730+
end
731+
elseif (isa(shape, Array{<:AbstractFloat}))
732+
if (size(shape, 2) == 2)
733+
xc = extrema(view(shape, :, 1)) # Start and end coordinates
734+
yc = extrema(view(shape, :, 2))
735+
X,Y = meshgrid(linspace(xc[1], xc[2], n_cols+1), linspace(yc[2], yc[1], n_rows+1))
736+
Z = fill(Float64(level), n_rows+1, n_cols+1)
737+
masca = maskregion(X, Y, shape)
738+
elseif (size(shape, 2) == 3) # A cicle but not necessarily in the xy plane
739+
c0 = std(diff(view(shape, :, 1), dims=1)) 0 ? 1 : std(diff(view(shape, :, 2), dims=1)) 0 ? 2 :
740+
std(diff(view(shape, :, 3), dims=1)) 0 ? 3 : error("'shape' is not a circle in the horizontal or vertical planes.")
741+
two_col = (c0 == 1) ? (2,3) : (c0 == 2) ? (1,3) : (1,2) # The indices of the non-zero columns
742+
xc = extrema(view(shape, :, two_col[1]))
743+
yc = extrema(view(shape, :, two_col[2]))
744+
_X,_Y = meshgrid(linspace(xc[1], xc[2], n_cols+1), linspace(yc[2], yc[1], n_rows+1))
745+
masca = maskregion(_X, _Y, shape[:, [two_col[1], two_col[2]]])
746+
level = (c0 == 1) ? shape[1] : (c0 == 2) ? shape[1,2] : shape[1,3]
747+
_Z = fill(Float64(level), n_rows+1, n_cols+1)
748+
X, Y, Z = (c0 == 1) ? (_Z, _X, _Y) : (c0 == 2) ? (_X, _Z, _Y) : (_X, _Y, _Z)
749+
end
750+
else
751+
X,Y = meshgrid(_I.x, reverse(_I.y)) # MUST CONFIRM that these coords are pixel-reg
752+
Z = fill(Float64(level), n_rows+1, n_cols+1)
753+
masca = BitArray(undef,0,0)
754+
end
755+
doMask = !isempty(masca)
756+
757+
FV = surf2fv(X, Y, Z, type=:quad, mask=masca)
758+
n_colors = doMask ? sum(masca) : (n_rows * n_cols)
759+
cor = Vector{String}(undef, n_colors)
760+
761+
kk = 0
762+
@inbounds for n = 1:n_cols, m = 1:n_rows
763+
doMask && !masca[m,n] && continue
764+
k = (m-1) * n_cols * 3 + (n-1) * 3
765+
cor[kk+=1] = @sprintf("-G#%.2x%.2x%.2x", _I[k+=1], _I[k+=1], _I[k+=1])
766+
end
767+
768+
FV.color, FV.isflat = [cor], true
769+
return FV
770+
end

0 commit comments

Comments
 (0)