Skip to content

Commit 5be0b89

Browse files
joa-quimclaude
andauthored
Add standalone compass() function for directional and magnetic roses (#1917)
Convenience wrapper around basemap that auto-selects directional (-Td) vs magnetic (-Tm) rose based on whether `dec` is provided. When called standalone without region/proj, creates a paper-coordinates canvas sized to fit the rose automatically. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 53991bb commit 5be0b89

3 files changed

Lines changed: 160 additions & 1 deletion

File tree

src/GMT.jl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ export
121121
gmt_GMTgrid, libgdal, arrows, arrows!, bar, bar!, bar3, bar3!, band, band!, bubblechart, bubblechart!, crop,
122122
feather, feather!, hband, hband!, hlines, hlines!, lines, lines!, legend, legend!, quiver, quiver!, radar,
123123
radar!, stairs, stairs!, stem, stem!, vlines, vlines!, vband, vband!, hspan, hspan!, vspan, vspan!,
124-
basemap, basemap!, blockmean, blockmedian, blockmode, clip, clip!,
124+
basemap, basemap!, blockmean, blockmedian, blockmode, clip, clip!, compass, compass!,
125125
coast, coast!, colorbar, colorbar!, colorscale, colorscale!, contour, contour!, contourf, contourf!, events,
126126
filter1d, fitcircle, gmt2kml, gmtbinstats, binstats, gmtconnect, gmtconvert,
127127
gmtinfo, gmtlogo, gmtlogo!, gmtmath, gmtregress, gmtread, gmtselect, gmtset, gmtsimplify, gmtspatial,
@@ -284,6 +284,7 @@ include("orbits.jl")
284284
include("plot.jl")
285285
include("project.jl")
286286
include("psbasemap.jl")
287+
include("compass.jl")
287288
include("psclip.jl")
288289
include("pscoast.jl")
289290
include("pscontour.jl")

src/compass.jl

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
"""
2+
compass(; kwargs...)
3+
4+
Draw a map directional rose or magnetic compass on the map.
5+
6+
If `dec` (magnetic declination) is provided, draws a magnetic compass rose (`-Tm`).
7+
Otherwise, draws a directional rose (`-Td`).
8+
9+
Can be called standalone (creates its own plot) or as an overlay (`compass!`) on an existing plot.
10+
When called standalone without `region`/`proj`, a default canvas sized to fit the rose is created
11+
automatically.
12+
13+
### Compass-specific options
14+
15+
- **anchor** :: [Type => Tuple | Str] — Reference point on the map for the rose.
16+
- **width** :: [Type => Number | Str] — Width of the rose in cm.
17+
- **justify** :: [Type => Str] — Justification of the rose relative to anchor.
18+
- **labels** or **label** :: [Type => Str] — Comma-separated labels for the cardinal points.
19+
- **offset** :: [Type => Tuple | Str] — Offset from the anchor point.
20+
- **map** | **inside** | **outside** | **norm** | **paper** :: [Type => Str] — Coordinate system for the anchor.
21+
22+
### Directional rose only (`-Td`)
23+
24+
- **fancy** :: [Type => Bool | Int] — Draw a fancy rose. 1-3 for different levels.
25+
26+
### Magnetic compass only (`-Tm`)
27+
28+
- **dec** :: [Type => Number | Str] — Magnetic declination.
29+
- **rose_primary** :: [Type => Tuple | Str] — Pen for the primary rose circle.
30+
- **rose_secondary** :: [Type => Tuple | Str] — Pen for the secondary rose circle.
31+
- **annot** :: [Type => Tuple | Str] — Annotation info for the magnetic compass.
32+
33+
All other keyword arguments (e.g., `region`, `proj`, `par`, `show`, `savefig`, `Vd`, etc.)
34+
are passed through to `basemap`.
35+
36+
### Examples
37+
38+
Draw a standalone directional rose:
39+
40+
```julia
41+
compass(width=2.5, fancy=true, labels=",,,N", show=true)
42+
```
43+
44+
Draw a directional rose as overlay:
45+
46+
```julia
47+
coast(region=(-10,10,-10,10), proj=:Mercator, frame=:auto, land=:lightgray)
48+
compass!(width=2.5, anchor=(0,0), justify=:CM, fancy=true, labels=",,,N", show=true)
49+
```
50+
51+
Draw a magnetic compass:
52+
53+
```julia
54+
basemap(region=(-8,8,-6,6), proj=:Mercator, frame=:auto)
55+
compass!(anchor=(0,0), width=6, dec=-14.5, annot=(45,10,5,30,10,2),
56+
rose_primary=(0.25,:blue), rose_secondary=0.5, labels="", show=true)
57+
```
58+
"""
59+
compass!(; kw...) = compass(; first=false, kw...)
60+
function compass(; first=true, kw...)
61+
d = init_module(false, kw...)[1] # Also checks if the user wants ONLY the HELP mode
62+
compass(first, d)
63+
end
64+
function compass(first::Bool, d::Dict{Symbol, Any})
65+
66+
compass_keys = (:anchor, :width, :justify, :labels, :label, :offset,
67+
:map, :inside, :outside, :norm, :paper,
68+
:fancy, :dec, :rose_primary, :rose_secondary, :annot)
69+
70+
nt_pairs = Pair{Symbol,Any}[]
71+
for k in compass_keys
72+
haskey(d, k) && push!(nt_pairs, k => pop!(d, k))
73+
end
74+
75+
is_magnetic = any(p -> p.first === :dec, nt_pairs)
76+
77+
# When standalone (first=true) and no region/proj given, create a paper-coordinates canvas.
78+
has_RJ = any(k -> haskey(d, k), (:R, :region, :limits, :J, :proj, :projection))
79+
if (first && !has_RJ)
80+
# Get the width to size the canvas (default 5 cm)
81+
w = 5.0
82+
for p in nt_pairs
83+
if p.first === :width
84+
w = isa(p.second, Real) ? Float64(p.second) : 5.0
85+
break
86+
end
87+
end
88+
sz = ceil(w * 1.6; digits=1) # Canvas slightly larger than the rose
89+
d[:region] = (0, sz, 0, sz)
90+
d[:proj] = "X$(sz)c"
91+
# Default anchor to center of canvas in paper coordinates if not provided
92+
has_anchor = any(p -> p.first === :anchor, nt_pairs)
93+
has_coord = any(p -> p.first in (:map, :inside, :outside, :norm, :paper), nt_pairs)
94+
if !has_anchor && !has_coord
95+
# No positioning at all — center the rose on the canvas
96+
push!(nt_pairs, :paper => "$(sz/2)/$(sz/2)")
97+
push!(nt_pairs, :justify => :CM)
98+
elseif has_anchor && !has_coord
99+
# User gave anchor but no coordinate system — use paper coords
100+
anc = pop_anchor!(nt_pairs)
101+
push!(nt_pairs, :paper => isa(anc, Tuple) ? join(anc, '/') : string(anc))
102+
end
103+
end
104+
105+
nt = (; nt_pairs...)
106+
107+
if is_magnetic
108+
d[:compass] = nt # -> parse_Tm -> -Tm
109+
else
110+
d[:rose] = nt # -> parse_Td -> -Td
111+
end
112+
113+
haskey(d, :frame) || haskey(d, :B) || (d[:frame] = :none)
114+
helper_basemap(!first, true, d)
115+
end
116+
117+
# Helper to pop :anchor from nt_pairs and return its value
118+
function pop_anchor!(pairs::Vector{Pair{Symbol,Any}})
119+
for i in eachindex(pairs)
120+
if pairs[i].first === :anchor
121+
val = pairs[i].second
122+
deleteat!(pairs, i)
123+
return val
124+
end
125+
end
126+
return nothing
127+
end

test/test_PSs.jl

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,37 @@ r = basemap(region=("0.2t","0.35t",0,1), figsize=(-12,0.25), frame=(axes=:S, ann
3333
@test startswith(r, "psbasemap -R0.2t/0.35t/0/1 -JX-12/0.25 -Bsa1H -Bpa15mf5m -BS --FORMAT_CLOCK_MAP=-hham")
3434
@test_throws ErrorException("slanted option: Only 'parallel' is allowed for the y-axis") basemap(yaxis=(slanted=:o,), Vd=dbg2)
3535

36+
println(" COMPASS")
37+
# Standalone directional rose — auto canvas, auto center
38+
r = compass(width=3, fancy=true, Vd=dbg2);
39+
@test startswith(r, "psbasemap -R0/4.9/0/4.9 -JX4.9c -Tdx2.45/2.45+w3+f+jCM")
40+
# Standalone with labels
41+
r = compass(width=2.5, fancy=2, labels=",,,N", Vd=dbg2);
42+
@test contains(r, "-Td") && contains(r, "+f2") && contains(r, "+l,,,N")
43+
# Standalone with anchor (auto paper coords)
44+
r = compass(width=3, anchor=(5,5), fancy=true, Vd=dbg2);
45+
@test contains(r, "-Tdx5/5+w3+f")
46+
# Standalone with explicit paper coord
47+
r = compass(width=3, paper="7/7", fancy=1, Vd=dbg2);
48+
@test contains(r, "-Tdx7/7+w3+f1")
49+
# Standalone with explicit R/J — no auto canvas
50+
r = compass(region=(-5,5,-5,5), proj=:merc, width=2.5, anchor=(0,0), fancy=3, Vd=dbg2);
51+
@test startswith(r, "psbasemap -R-5/5/-5/5 -JM15c") && contains(r, "-Tdj0/0+w2.5+f3")
52+
# Magnetic compass — standalone
53+
r = compass(width=6, dec=-14.5, annot=(45,10,5,30,10,2), labels="", Vd=dbg2);
54+
@test contains(r, "-Tm") && contains(r, "+d-14.5") && contains(r, "+t45/10/5/30/10/2")
55+
# Magnetic compass with rose_primary/secondary
56+
r = compass(width=6, dec=-14.5, rose_primary=(0.25,:blue), rose_secondary=0.5, Vd=dbg2);
57+
@test contains(r, "+i0.25,blue") && contains(r, "+p0.5")
58+
# Overlay mode (compass!)
59+
basemap(region=(-10,10,-10,10), proj=:Mercator, frame=:auto, Vd=dbg2)
60+
r = compass!(width=2.5, anchor=(0,0), justify=:CM, fancy=true, labels=",,,N", Vd=dbg2);
61+
@test contains(r, " -R -J") || contains(r, "-R-10/10/-10/10 -J ")
62+
@test contains(r, "-Tdj0/0+w2.5+jCM+l,,,N+f")
63+
# frame=:none is default (no explicit frame draws no axes)
64+
r = compass(width=3, fancy=true, Vd=dbg2);
65+
@test !contains(r, "-B")
66+
3667
println(" PSCLIP")
3768
d = [0.2 0.2; 0.2 0.8; 0.8 0.8; 0.8 0.2; 0.2 0.2];
3869
psclip(d, J="X3i", R="0/1/0/1", N=true, V=:q);

0 commit comments

Comments
 (0)