Skip to content

Commit 4abac25

Browse files
authored
Merge pull request #46 from JuliaGeo/rs/trait_plots
add trait based plot recipes in a subpackage
2 parents 70413dc + a624478 commit 4abac25

File tree

5 files changed

+301
-0
lines changed

5 files changed

+301
-0
lines changed

.github/workflows/recipes.yml

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
name: GeoInterfaceRecipes CI
2+
on:
3+
pull_request:
4+
paths-ignore:
5+
- 'docs/**'
6+
- '*.md'
7+
branches:
8+
- master
9+
- breaking-release
10+
push:
11+
paths-ignore:
12+
- 'docs/**'
13+
- '*.md'
14+
branches:
15+
- master
16+
- breaking-release
17+
tags: '*'
18+
19+
concurrency:
20+
group: cairomakie-${{ github.ref }}
21+
cancel-in-progress: true
22+
23+
jobs:
24+
test:
25+
name: GeoInterfaceRecipes Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} - ${{ github.event_name }}
26+
runs-on: ${{ matrix.os }}
27+
strategy:
28+
fail-fast: false
29+
matrix:
30+
version:
31+
- '1.6'
32+
- '1' # automatically expands to the latest stable 1.x release of Julia
33+
os:
34+
- ubuntu-latest
35+
arch:
36+
- x64
37+
steps:
38+
- name: Checkout
39+
uses: actions/checkout@v2
40+
- uses: julia-actions/setup-julia@v1
41+
with:
42+
version: ${{ matrix.version }}
43+
arch: ${{ matrix.arch }}
44+
- uses: julia-actions/cache@v1
45+
- name: Install Julia dependencies
46+
shell: julia --project=monorepo {0}
47+
run: |
48+
using Pkg;
49+
# dev mono repo versions
50+
pkg"dev . ./GeoInterfaceRecipes"
51+
- name: Run the tests
52+
continue-on-error: true
53+
run: >
54+
julia --color=yes --project=monorepo -e 'using Pkg; Pkg.test("GeoInterfaceRecipes", coverage=true)'
55+
&& echo "TESTS_SUCCESSFUL=true" >> $GITHUB_ENV
56+
- name: Exit if tests failed
57+
if: ${{ env.TESTS_SUCCESSFUL != 'true' }}
58+
run: exit 1
59+
- uses: julia-actions/julia-processcoverage@v1
60+
- uses: codecov/codecov-action@v1
61+
with:
62+
file: lcov.info

GeoInterfaceRecipes/Project.toml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
name = "GeoInterfaceRecipes"
2+
uuid = "0329782f-3d07-4b52-b9f6-d3137cf03c7a"
3+
authors = ["JuliaGeo and contributors"]
4+
version = "1.0.0"
5+
6+
[deps]
7+
GeoInterface = "cf35fbd7-0cd7-5166-be24-54bfbe79505f"
8+
RecipesBase = "3cdcf5f2-1ef4-517c-9805-6587b60abb01"
9+
10+
[compat]
11+
GeoInterface = "1"
12+
RecipesBase = "1"
13+
julia = "1"
14+
15+
[extras]
16+
Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80"
17+
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
18+
19+
[targets]
20+
test = ["Plots", "Test"]

GeoInterfaceRecipes/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
[![Build Status](https://github.com/JuliaGeo/GeoInterface.jl/actions/workflows/CI.yml/badge.svg?branch=main)](https://github.com/JuliaGeo/GeoInterface.jl/actions/workflows/CI.yml?query=branch%3Amain)
2+
3+
# GeoInterfaceRecipes
4+
5+
Plot recipes for GeoInterface objects, using RecipesBase.jl
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
module GeoInterfaceRecipes
2+
3+
using GeoInterface, RecipesBase
4+
5+
const GI = GeoInterface
6+
7+
export @enable_geo_plots
8+
9+
"""
10+
GeoInterfaceRecipes.@enable_geo_plots(typ)
11+
12+
Macro to add plot recipes to a geometry type.
13+
"""
14+
macro enable_geo_plots(typ)
15+
quote
16+
# We recreate the apply_recipe functions manually here
17+
# as nesting the @recipe macro doesn't work.
18+
function RecipesBase.apply_recipe(plotattributes::Base.AbstractDict{Base.Symbol, Base.Any}, geom::$(esc(typ)))
19+
@nospecialize
20+
series_list = RecipesBase.RecipeData[]
21+
RecipesBase.is_explicit(plotattributes, :label) || (plotattributes[:label] = :none)
22+
Base.push!(series_list, RecipesBase.RecipeData(plotattributes, (GeoInterface.geomtrait(geom), geom)))
23+
return series_list
24+
end
25+
function RecipesBase.apply_recipe(plotattributes::Base.AbstractDict{Base.Symbol, Base.Any}, geom::Base.AbstractVector{<:Base.Union{Base.Missing,<:($(esc(typ)))}})
26+
@nospecialize
27+
series_list = RecipesBase.RecipeData[]
28+
RecipesBase.is_explicit(plotattributes, :label) || (plotattributes[:label] = :none)
29+
for g in Base.skipmissing(geom)
30+
Base.push!(series_list, RecipesBase.RecipeData(plotattributes, (GeoInterface.geomtrait(g), g)))
31+
end
32+
return series_list
33+
end
34+
end
35+
end
36+
37+
RecipesBase.@recipe function f(t::Union{GI.PointTrait,GI.MultiPointTrait}, geom)
38+
seriestype --> :scatter
39+
_coordvecs(t, geom)
40+
end
41+
42+
RecipesBase.@recipe function f(t::Union{GI.LineStringTrait,GI.MultiLineStringTrait}, geom)
43+
seriestype --> :path
44+
_coordvecs(t, geom)
45+
end
46+
47+
RecipesBase.@recipe function f(t::Union{GI.PolygonTrait,GI.MultiPolygonTrait}, geom)
48+
seriestype --> :shape
49+
_coordvecs(t, geom)
50+
end
51+
52+
RecipesBase.@recipe f(::GI.GeometryCollectionTrait, collection) = collect(getgeom(collection))
53+
54+
# Convert coordinates to the form used by Plots.jl
55+
_coordvecs(::GI.PointTrait, geom) = [tuple(GI.coordinates(geom)...)]
56+
function _coordvecs(::GI.MultiPointTrait, geom)
57+
n = GI.npoint(geom)
58+
# We use a fixed conditional instead of dispatch,
59+
# as `is3d` may not be known at compile-time
60+
if GI.is3d(geom)
61+
_geom2coordvecs!(ntuple(_ -> Array{Float64}(undef, n), 3)..., geom)
62+
else
63+
_geom2coordvecs!(ntuple(_ -> Array{Float64}(undef, n), 2)..., geom)
64+
end
65+
end
66+
function _coordvecs(::GI.LineStringTrait, geom)
67+
n = GI.npoint(geom)
68+
if GI.is3d(geom)
69+
vecs = ntuple(_ -> Array{Float64}(undef, n), 3)
70+
return _geom2coordvecs!(vecs..., geom)
71+
else
72+
vecs = ntuple(_ -> Array{Float64}(undef, n), 2)
73+
return _geom2coordvecs!(vecs..., geom)
74+
end
75+
end
76+
function _coordvecs(::GI.MultiLineStringTrait, geom)
77+
function loop!(vecs, geom)
78+
i1 = 1
79+
for line in GI.getgeom(geom)
80+
i2 = i1 + GI.npoint(line) - 1
81+
vvecs = map(v -> view(v, i1:i2), vecs)
82+
_geom2coordvecs!(vvecs..., line)
83+
map(v -> v[i2 + 1] = NaN, vecs)
84+
i1 = i2 + 2
85+
end
86+
return vecs
87+
end
88+
n = GI.npoint(geom) + GI.ngeom(geom)
89+
if GI.is3d(geom)
90+
vecs = ntuple(_ -> Array{Float64}(undef, n), 3)
91+
return loop!(vecs, geom)
92+
else
93+
vecs = ntuple(_ -> Array{Float64}(undef, n), 2)
94+
return loop!(vecs, geom)
95+
end
96+
end
97+
function _coordvecs(::GI.PolygonTrait, geom)
98+
ring = first(GI.getgeom(geom)) # currently doesn't plot holes
99+
points = GI.getpoint(ring)
100+
if GI.is3d(geom)
101+
return getcoord.(points, 1), getcoord.(points, 2), getcoord.(points, 3)
102+
else
103+
return getcoord.(points, 1), getcoord.(points, 2)
104+
end
105+
end
106+
function _coordvecs(::GI.MultiPolygonTrait, geom)
107+
function loop!(vecs, geom)
108+
i1 = 1
109+
for ring in GI.getring(geom)
110+
i2 = i1 + GI.npoint(ring) - 1
111+
range = i1:i2
112+
vvecs = map(v -> view(v, range), vecs)
113+
_geom2coordvecs!(vvecs..., ring)
114+
map(v -> v[i2 + 1] = NaN, vecs)
115+
i1 = i2 + 2
116+
end
117+
return vecs
118+
end
119+
n = GI.npoint(geom) + GI.nring(geom)
120+
if GI.is3d(geom)
121+
vecs = ntuple(_ -> Array{Float64}(undef, n), 3)
122+
return loop!(vecs, geom)
123+
else
124+
vecs = ntuple(_ -> Array{Float64}(undef, n), 2)
125+
return loop!(vecs, geom)
126+
end
127+
end
128+
129+
_coordvec(n) = Array{Float64}(undef, n)
130+
131+
function _geom2coordvecs!(xs, ys, geom)
132+
for (i, p) in enumerate(GI.getpoint(geom))
133+
xs[i] = GI.x(p)
134+
ys[i] = GI.y(p)
135+
end
136+
return xs, ys
137+
end
138+
function _geom2coordvecs!(xs, ys, zs, geom)
139+
for (i, p) in enumerate(GI.getpoint(geom))
140+
xs[i] = GI.x(p)
141+
ys[i] = GI.y(p)
142+
zs[i] = GI.z(p)
143+
end
144+
return xs, ys, zs
145+
end
146+
147+
end

GeoInterfaceRecipes/test/runtests.jl

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
using GeoInterfaceRecipes
2+
using GeoInterface
3+
using Plots
4+
using Test
5+
6+
7+
abstract type MyAbstractGeom{N} end
8+
# Implement interface
9+
struct MyPoint{N} <: MyAbstractGeom{N} end
10+
struct MyCurve{N} <: MyAbstractGeom{N} end
11+
struct MyPolygon{N} <: MyAbstractGeom{N} end
12+
struct MyMultiPoint{N} <: MyAbstractGeom{N} end
13+
struct MyMultiCurve{N} <: MyAbstractGeom{N} end
14+
struct MyMultiPolygon{N} <: MyAbstractGeom{N} end
15+
struct MyCollection{N} <: MyAbstractGeom{N} end
16+
17+
GeoInterfaceRecipes.@enable_geo_plots MyAbstractGeom
18+
19+
GeoInterface.isgeometry(::MyAbstractGeom) = true
20+
GeoInterface.is3d(::GeoInterface.AbstractGeometryTrait, ::MyAbstractGeom{N}) where {N} = N == 3
21+
GeoInterface.ncoord(::GeoInterface.AbstractGeometryTrait, geom::MyAbstractGeom{N}) where {N} = N
22+
GeoInterface.coordnames(::GeoInterface.AbstractGeometryTrait, ::MyAbstractGeom{2}) = (:X, :Y)
23+
GeoInterface.coordnames(::GeoInterface.AbstractGeometryTrait, ::MyAbstractGeom{3}) = (:X, :Y, :Z)
24+
25+
GeoInterface.geomtrait(::MyPoint) = GeoInterface.PointTrait()
26+
GeoInterface.getcoord(::GeoInterface.PointTrait, geom::MyPoint{2}, i::Integer) = (rand(1:10), rand(11:20))[i]
27+
GeoInterface.getcoord(::GeoInterface.PointTrait, geom::MyPoint{3}, i::Integer) = (rand(1:10), rand(11:20), rand(21:30))[i]
28+
29+
GeoInterface.geomtrait(::MyCurve) = GeoInterface.LineStringTrait()
30+
GeoInterface.ngeom(::GeoInterface.LineStringTrait, geom::MyCurve) = 3
31+
GeoInterface.getgeom(::GeoInterface.LineStringTrait, geom::MyCurve{N}, i) where {N} = MyPoint{N}()
32+
GeoInterface.convert(::Type{MyCurve}, ::GeoInterface.LineStringTrait, geom) = geom
33+
34+
GeoInterface.geomtrait(::MyPolygon) = GeoInterface.PolygonTrait()
35+
GeoInterface.ngeom(::GeoInterface.PolygonTrait, geom::MyPolygon) = 2
36+
GeoInterface.getgeom(::GeoInterface.PolygonTrait, geom::MyPolygon{N}, i) where {N} = MyCurve{N}()
37+
38+
GeoInterface.geomtrait(::MyMultiPolygon) = GeoInterface.MultiPolygonTrait()
39+
GeoInterface.ngeom(::GeoInterface.MultiPolygonTrait, geom::MyMultiPolygon) = 2
40+
GeoInterface.getgeom(::GeoInterface.MultiPolygonTrait, geom::MyMultiPolygon{N}, i) where {N} = MyPolygon{N}()
41+
42+
GeoInterface.geomtrait(::MyMultiPoint) = GeoInterface.MultiPointTrait()
43+
GeoInterface.ngeom(::GeoInterface.MultiPointTrait, geom::MyMultiPoint) = 10
44+
GeoInterface.getgeom(::GeoInterface.MultiPointTrait, geom::MyMultiPoint{N}, i) where {N} = MyPoint{N}()
45+
46+
GeoInterface.geomtrait(geom::MyCollection) = GeoInterface.GeometryCollectionTrait()
47+
GeoInterface.ncoord(::GeoInterface.GeometryCollectionTrait, geom::MyCollection{N}) where {N} = N
48+
GeoInterface.ngeom(::GeoInterface.GeometryCollectionTrait, geom::MyCollection) = 4
49+
GeoInterface.getgeom(::GeoInterface.GeometryCollectionTrait, geom::MyCollection{N}, i) where {N} = MyMultiPolygon{N}()
50+
51+
@testset "plot" begin
52+
# We just check if they actually run
53+
# 2d
54+
plot(MyPoint{2}())
55+
plot(MyCurve{2}())
56+
plot(MyMultiPoint{2}())
57+
plot(MyPolygon{2}())
58+
plot(MyMultiPolygon{2}())
59+
plot(MyCollection{2}())
60+
# 3d
61+
plot(MyPoint{3}())
62+
plot(MyCurve{3}())
63+
plot(MyMultiPoint{3}())
64+
plot(MyPolygon{3}())
65+
plot(MyMultiPolygon{3}())
66+
plot(MyCollection{3}())
67+
end

0 commit comments

Comments
 (0)