Skip to content

Commit 52f478b

Browse files
authored
Merge pull request #74 from Sov-trotter/meta_exp
Support heterogeneous features
2 parents 92d9614 + 51a2ddf commit 52f478b

File tree

3 files changed

+230
-1
lines changed

3 files changed

+230
-1
lines changed

src/GeometryBasics.jl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ module GeometryBasics
3535
export GLTriangleFace, GLNormalMesh3D, GLPlainTriangleMesh, GLUVMesh3D, GLUVNormalMesh3D
3636
export AbstractMesh, Mesh, TriangleMesh
3737
export GLNormalMesh2D, PlainTriangleMesh
38-
38+
export MetaT, meta_table
39+
3940
# all the different predefined mesh types
4041
# Note: meshes can contain arbitrary meta information,
4142
export AbstractMesh, TriangleMesh, PlainMesh, GLPlainMesh, GLPlainMesh2D, GLPlainMesh3D

src/metadata.jl

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ Returns the Meta Type corresponding to `T`
3737
E.g:
3838
```julia
3939
MetaType(Point) == PointMeta
40+
```
4041
"""
4142
MetaType(::Type{T}) where T = error("No Meta Type for $T")
4243

@@ -47,6 +48,7 @@ Returns the original type containing no metadata for `T`
4748
E.g:
4849
```julia
4950
MetaFree(PointMeta) == Point
51+
```
5052
"""
5153
MetaFree(::Type{T}) where T = error("No meta free Type for $T")
5254

@@ -190,3 +192,99 @@ Base.size(x::MultiPolygonMeta) = size(metafree(x))
190192
@meta_type(Mesh, mesh, AbstractMesh, Element <: Polytope)
191193
Base.getindex(x::MeshMeta, idx::Int) = getindex(metafree(x), idx)
192194
Base.size(x::MeshMeta) = size(metafree(x))
195+
196+
197+
"""
198+
199+
MetaT(geometry, meta::NamedTuple)
200+
MetaT(geometry; meta...)
201+
202+
Returns a `MetaT` that holds a geometry and its metadata
203+
204+
`MetaT` acts the same as `Meta` method.
205+
The difference lies in the fact that it is designed to handle
206+
geometries and metadata of different/heterogeneous types.
207+
208+
eg: While a Point MetaGeometry is a `PointMeta`, the MetaT representation is `MetaT{Point}`
209+
The downside being it's not subtyped to `AbstractPoint` like a `PointMeta` is.
210+
211+
Example:
212+
```julia
213+
julia> MetaT(Point(1, 2), city = "Mumbai")
214+
MetaT{Point{2,Int64},(:city,),Tuple{String}}([1, 2], (city = "Mumbai",))
215+
```
216+
"""
217+
struct MetaT{T, Names, Types}
218+
main::T
219+
meta::NamedTuple{Names, Types}
220+
end
221+
222+
MetaT(x; kwargs...) = MetaT(x, values(kwargs))
223+
224+
"""
225+
226+
metafree(x::MetaT)
227+
metafree(x::Array{MetaT})
228+
229+
Free the MetaT from metadata
230+
i.e. returns the geometry/array of geometries
231+
"""
232+
function metafree(x::MetaT)
233+
getfield(x, :main)
234+
end
235+
metafree(x::AbstractVector{<: MetaT}) = map(metafree, x)
236+
237+
"""
238+
239+
meta(x::MetaT)
240+
meta(x::Array{MetaT})
241+
242+
Returns the metadata of a `MetaT`
243+
"""
244+
function meta(x::MetaT)
245+
getfield(x, :meta)
246+
end
247+
meta(x::AbstractVector{<: MetaT}) = map(meta, x)
248+
249+
# helper methods
250+
function Base.getproperty(x::MetaT, field::Symbol)
251+
if field == :main
252+
metafree(x)
253+
elseif field == :meta
254+
meta(x)
255+
else
256+
getproperty(meta(x), field)
257+
end
258+
end
259+
260+
Base.propertynames(x::MetaT) = (:main, propertynames(meta(x))...)
261+
getnamestypes(::Type{MetaT{T, Names, Types}}) where {T, Names, Types} = (T, Names, Types)
262+
263+
# explicitly give the "schema" of the object to StructArrays
264+
function StructArrays.staticschema(::Type{F}) where {F<:MetaT}
265+
T, names, types = getnamestypes(F)
266+
NamedTuple{(:main, names...), Base.tuple_type_cons(T, types)}
267+
end
268+
269+
# generate an instance of MetaT type
270+
function StructArrays.createinstance(::Type{F}, x, args...) where {F<:MetaT}
271+
T , names, types = getnamestypes(F)
272+
MetaT(x, NamedTuple{names, types}(args))
273+
end
274+
275+
"""
276+
Puts an iterable of MetaT's into a StructArray
277+
"""
278+
function meta_table(iter)
279+
cols = Tables.columntable(iter)
280+
meta_table(first(cols), Base.tail(cols))
281+
end
282+
283+
function meta_table(main, meta::NamedTuple{names, types}) where {names, types}
284+
F = MetaT{eltype(main), names, StructArrays.eltypes(types)}
285+
return StructArray{F}(; main=main, meta...)
286+
end
287+
288+
Base.getindex(x::MetaT, idx::Int) = getindex(metafree(x), idx)
289+
Base.size(x::MetaT) = size(metafree(x))
290+

test/runtests.jl

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,105 @@ using GeometryBasics: attributes
131131
end
132132
end
133133

134+
@testset "embedding MetaT" begin
135+
@testset "MetaT{Polygon}" begin
136+
polys = [Polygon(rand(Point{2, Float32}, 20)) for i in 1:10]
137+
multipol = MultiPolygon(polys)
138+
pnames = [randstring(4) for i in 1:10]
139+
numbers = LinRange(0.0, 1.0, 10)
140+
bin = rand(Bool, 10)
141+
# create a polygon
142+
poly = MetaT(polys[1], name = pnames[1], value = numbers[1], category = bin[1])
143+
# create a MultiPolygon with the right type & meta information!
144+
multipoly = MetaT(multipol, name = pnames, value = numbers, category = bin)
145+
@test multipoly isa MetaT
146+
@test poly isa MetaT
147+
148+
@test GeometryBasics.getcolumn(poly, :name) == pnames[1]
149+
@test GeometryBasics.getcolumn(multipoly, :name) == pnames
150+
151+
meta_p = MetaT(polys[1], boundingbox=Rect(0, 0, 2, 2))
152+
@test meta_p.boundingbox === Rect(0, 0, 2, 2)
153+
@test GeometryBasics.metafree(meta_p) == polys[1]
154+
@test GeometryBasics.metafree(poly) == polys[1]
155+
@test GeometryBasics.metafree(multipoly) == multipol
156+
@test GeometryBasics.meta(meta_p) == (boundingbox = GeometryBasics.HyperRectangle{2,Int64}([0, 0], [2, 2]),)
157+
@test GeometryBasics.meta(poly) == (name = pnames[1], value = 0.0, category = bin[1])
158+
@test GeometryBasics.meta(multipoly) == (name = pnames, value = numbers, category = bin)
159+
end
160+
161+
@testset "MetaT{Point}" begin
162+
p = Point(1.1, 2.2)
163+
@test p isa AbstractVector{Float64}
164+
pm = MetaT(Point(1.1, 2.2); a=1, b=2)
165+
p1 = Point(2.2, 3.6)
166+
p2 = [p, p1]
167+
@test coordinates(p2) == p2
168+
@test pm.meta === (a=1, b=2)
169+
@test pm.main === p
170+
@test propertynames(pm) == (:main, :a, :b)
171+
@test GeometryBasics.metafree(pm) == p
172+
@test GeometryBasics.meta(pm) == (a = 1, b = 2)
173+
end
174+
175+
@testset "MetaT{MultiPoint}" begin
176+
p = collect(Point{2, Float64}(x, x+1) for x in 1:5)
177+
@test p isa AbstractVector
178+
mpm = MetaT(MultiPoint(p); a=1, b=2)
179+
@test coordinates(mpm.main) == Point{2, Float64}[(x, x+1) for x in 1:5]
180+
@test mpm.meta === (a=1, b=2)
181+
@test mpm.main == p
182+
@test propertynames(mpm) == (:main, :a, :b)
183+
@test GeometryBasics.metafree(mpm) == p
184+
@test GeometryBasics.meta(mpm) == (a = 1, b = 2)
185+
end
186+
187+
@testset "MetaT{LineString}" begin
188+
linestring = MetaT(LineString(Point{2, Int}[(10, 10), (20, 20), (10, 40)]), a = 1, b = 2)
189+
@test linestring isa MetaT
190+
@test linestring.meta === (a = 1, b = 2)
191+
@test propertynames(linestring) == (:main, :a, :b)
192+
@test GeometryBasics.metafree(linestring) == LineString(Point{2, Int}[(10, 10), (20, 20), (10, 40)])
193+
@test GeometryBasics.meta(linestring) == (a = 1, b = 2)
194+
end
195+
196+
@testset "MetaT{MultiLineString}" begin
197+
linestring1 = LineString(Point{2, Int}[(10, 10), (20, 20), (10, 40)])
198+
linestring2 = LineString(Point{2, Int}[(40, 40), (30, 30), (40, 20), (30, 10)])
199+
multilinestring = MultiLineString([linestring1, linestring2])
200+
multilinestringmeta = MetaT(MultiLineString([linestring1, linestring2]); boundingbox = Rect(1.0, 1.0, 2.0, 2.0))
201+
@test multilinestringmeta isa MetaT
202+
@test multilinestringmeta.meta === (boundingbox = Rect(1.0, 1.0, 2.0, 2.0),)
203+
@test multilinestringmeta.main == multilinestring
204+
@test propertynames(multilinestringmeta) == (:main, :boundingbox)
205+
@test GeometryBasics.metafree(multilinestringmeta) == multilinestring
206+
@test GeometryBasics.meta(multilinestringmeta) == (boundingbox = GeometryBasics.HyperRectangle{2,Float64}([1.0, 1.0], [2.0, 2.0]),)
207+
end
208+
209+
#=
210+
So mesh works differently for MetaT
211+
since `MetaT{Point}` not subtyped to `AbstractPoint`
212+
=#
213+
214+
@testset "MetaT{Mesh}" begin
215+
@testset "per vertex attributes" begin
216+
points = rand(Point{3, Float64}, 8)
217+
tfaces = TetrahedronFace{Int}[(1, 2, 3, 4), (5, 6, 7, 8)]
218+
normals = rand(SVector{3, Float64}, 8)
219+
stress = LinRange(0, 1, 8)
220+
mesh_nometa = Mesh(points, tfaces)
221+
mesh = MetaT(mesh_nometa, normals = normals, stress = stress)
222+
223+
@test hasproperty(mesh, :stress)
224+
@test hasproperty(mesh, :normals)
225+
@test mesh.stress == stress
226+
@test mesh.normals == normals
227+
@test GeometryBasics.faces(mesh.main) == tfaces
228+
@test propertynames(mesh) == (:main, :normals, :stress)
229+
end
230+
end
231+
end
232+
134233
@testset "view" begin
135234
@testset "TupleView" begin
136235
x = [1, 2, 3, 4, 5, 6]
@@ -505,6 +604,37 @@ end
505604
@test <(x, x1)
506605
end
507606

607+
@testset "MetaT and heterogeneous data" begin
608+
ls = [LineString([Point(i, (i+1)^2/6), Point(i*0.86,i+5), Point(i/3, i/7)]) for i in 1:10]
609+
mls = MultiLineString([LineString([Point(i+1, (i)^2/6), Point(i*0.75,i+8), Point(i/2.5, i/6.79)]) for i in 5:10])
610+
poly = Polygon(Point{2, Int}[(40, 40), (20, 45), (45, 30), (40, 40)])
611+
geom = [ls..., mls, poly]
612+
prop = Any[(country_states = "India$(i)", rainfall = (i*9)/2) for i in 1:11]
613+
push!(prop, (country_states = 12, rainfall = 1000)) # a pinch of heterogeneity
614+
615+
feat = [MetaT(i, j) for (i,j) = zip(geom, prop)]
616+
sa = meta_table(feat)
617+
618+
@test nameof(eltype(feat)) == :MetaT
619+
@test eltype(sa) === MetaT{Any,(:country_states, :rainfall),Tuple{Any,Float64}}
620+
@test propertynames(sa) === (:main, :country_states, :rainfall)
621+
@test getproperty(sa, :country_states) isa Array{Any}
622+
@test getproperty(sa, :main) == geom
623+
624+
@test GeometryBasics.getnamestypes(typeof(feat[1])) ==
625+
(LineString{2,Float64,Point{2,Float64},Base.ReinterpretArray{GeometryBasics.Ngon{2,Float64,2,Point{2,Float64}},1,Tuple{Point{2,Float64},Point{2,Float64}},TupleView{Tuple{Point{2,Float64},Point{2,Float64}},2,1,Array{Point{2,Float64},1}}}},
626+
(:country_states, :rainfall), Tuple{String,Float64})
627+
628+
@test StructArrays.staticschema(typeof(feat[1])) ==
629+
NamedTuple{(:main, :country_states, :rainfall),Tuple{LineString{2,Float64,Point{2,Float64},Base.ReinterpretArray{GeometryBasics.Ngon{2,Float64,2,Point{2,Float64}},1,Tuple{Point{2,Float64},Point{2,Float64}},TupleView{Tuple{Point{2,Float64},Point{2,Float64}},2,1,Array{Point{2,Float64},1}}}},
630+
String,Float64}}
631+
632+
@test StructArrays.createinstance(typeof(feat[1]), LineString([Point(1, (2)^2/6), Point(1*0.86,6), Point(1/3, 1/7)]), "Mumbai", 100) isa typeof(feat[1])
633+
634+
@test Base.getindex(feat[1], 1) isa Line
635+
@test Base.size(feat[1]) == (2,)
636+
end
637+
508638
@testset "Tests from GeometryTypes" begin
509639
include("geometrytypes.jl")
510640
end

0 commit comments

Comments
 (0)