diff --git a/src/GeometryBasics.jl b/src/GeometryBasics.jl index 9dcef8b2..b8fc88ee 100644 --- a/src/GeometryBasics.jl +++ b/src/GeometryBasics.jl @@ -35,7 +35,8 @@ module GeometryBasics export GLTriangleFace, GLNormalMesh3D, GLPlainTriangleMesh, GLUVMesh3D, GLUVNormalMesh3D export AbstractMesh, Mesh, TriangleMesh export GLNormalMesh2D, PlainTriangleMesh - + export MetaT, meta_table + # all the different predefined mesh types # Note: meshes can contain arbitrary meta information, export AbstractMesh, TriangleMesh, PlainMesh, GLPlainMesh, GLPlainMesh2D, GLPlainMesh3D diff --git a/src/metadata.jl b/src/metadata.jl index 16983a73..4badeb16 100644 --- a/src/metadata.jl +++ b/src/metadata.jl @@ -37,6 +37,7 @@ Returns the Meta Type corresponding to `T` E.g: ```julia MetaType(Point) == PointMeta +``` """ MetaType(::Type{T}) where T = error("No Meta Type for $T") @@ -47,6 +48,7 @@ Returns the original type containing no metadata for `T` E.g: ```julia MetaFree(PointMeta) == Point +``` """ MetaFree(::Type{T}) where T = error("No meta free Type for $T") @@ -190,3 +192,99 @@ Base.size(x::MultiPolygonMeta) = size(metafree(x)) @meta_type(Mesh, mesh, AbstractMesh, Element <: Polytope) Base.getindex(x::MeshMeta, idx::Int) = getindex(metafree(x), idx) Base.size(x::MeshMeta) = size(metafree(x)) + + +""" + + MetaT(geometry, meta::NamedTuple) + MetaT(geometry; meta...) + +Returns a `MetaT` that holds a geometry and its metadata + +`MetaT` acts the same as `Meta` method. +The difference lies in the fact that it is designed to handle +geometries and metadata of different/heterogeneous types. + +eg: While a Point MetaGeometry is a `PointMeta`, the MetaT representation is `MetaT{Point}` +The downside being it's not subtyped to `AbstractPoint` like a `PointMeta` is. + +Example: +```julia +julia> MetaT(Point(1, 2), city = "Mumbai") +MetaT{Point{2,Int64},(:city,),Tuple{String}}([1, 2], (city = "Mumbai",)) +``` +""" +struct MetaT{T, Names, Types} + main::T + meta::NamedTuple{Names, Types} +end + +MetaT(x; kwargs...) = MetaT(x, values(kwargs)) + +""" + + metafree(x::MetaT) + metafree(x::Array{MetaT}) + +Free the MetaT from metadata +i.e. returns the geometry/array of geometries +""" +function metafree(x::MetaT) + getfield(x, :main) +end +metafree(x::AbstractVector{<: MetaT}) = map(metafree, x) + +""" + + meta(x::MetaT) + meta(x::Array{MetaT}) + +Returns the metadata of a `MetaT` +""" +function meta(x::MetaT) + getfield(x, :meta) +end +meta(x::AbstractVector{<: MetaT}) = map(meta, x) + +# helper methods +function Base.getproperty(x::MetaT, field::Symbol) + if field == :main + metafree(x) + elseif field == :meta + meta(x) + else + getproperty(meta(x), field) + end +end + +Base.propertynames(x::MetaT) = (:main, propertynames(meta(x))...) +getnamestypes(::Type{MetaT{T, Names, Types}}) where {T, Names, Types} = (T, Names, Types) + +# explicitly give the "schema" of the object to StructArrays +function StructArrays.staticschema(::Type{F}) where {F<:MetaT} + T, names, types = getnamestypes(F) + NamedTuple{(:main, names...), Base.tuple_type_cons(T, types)} +end + +# generate an instance of MetaT type +function StructArrays.createinstance(::Type{F}, x, args...) where {F<:MetaT} + T , names, types = getnamestypes(F) + MetaT(x, NamedTuple{names, types}(args)) +end + +""" +Puts an iterable of MetaT's into a StructArray +""" +function meta_table(iter) + cols = Tables.columntable(iter) + meta_table(first(cols), Base.tail(cols)) +end + +function meta_table(main, meta::NamedTuple{names, types}) where {names, types} + F = MetaT{eltype(main), names, StructArrays.eltypes(types)} + return StructArray{F}(; main=main, meta...) +end + +Base.getindex(x::MetaT, idx::Int) = getindex(metafree(x), idx) +Base.size(x::MetaT) = size(metafree(x)) + diff --git a/test/runtests.jl b/test/runtests.jl index 6e4ff6aa..3b550265 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -131,6 +131,105 @@ using GeometryBasics: attributes end end +@testset "embedding MetaT" begin + @testset "MetaT{Polygon}" begin + polys = [Polygon(rand(Point{2, Float32}, 20)) for i in 1:10] + multipol = MultiPolygon(polys) + pnames = [randstring(4) for i in 1:10] + numbers = LinRange(0.0, 1.0, 10) + bin = rand(Bool, 10) + # create a polygon + poly = MetaT(polys[1], name = pnames[1], value = numbers[1], category = bin[1]) + # create a MultiPolygon with the right type & meta information! + multipoly = MetaT(multipol, name = pnames, value = numbers, category = bin) + @test multipoly isa MetaT + @test poly isa MetaT + + @test GeometryBasics.getcolumn(poly, :name) == pnames[1] + @test GeometryBasics.getcolumn(multipoly, :name) == pnames + + meta_p = MetaT(polys[1], boundingbox=Rect(0, 0, 2, 2)) + @test meta_p.boundingbox === Rect(0, 0, 2, 2) + @test GeometryBasics.metafree(meta_p) == polys[1] + @test GeometryBasics.metafree(poly) == polys[1] + @test GeometryBasics.metafree(multipoly) == multipol + @test GeometryBasics.meta(meta_p) == (boundingbox = GeometryBasics.HyperRectangle{2,Int64}([0, 0], [2, 2]),) + @test GeometryBasics.meta(poly) == (name = pnames[1], value = 0.0, category = bin[1]) + @test GeometryBasics.meta(multipoly) == (name = pnames, value = numbers, category = bin) + end + + @testset "MetaT{Point}" begin + p = Point(1.1, 2.2) + @test p isa AbstractVector{Float64} + pm = MetaT(Point(1.1, 2.2); a=1, b=2) + p1 = Point(2.2, 3.6) + p2 = [p, p1] + @test coordinates(p2) == p2 + @test pm.meta === (a=1, b=2) + @test pm.main === p + @test propertynames(pm) == (:main, :a, :b) + @test GeometryBasics.metafree(pm) == p + @test GeometryBasics.meta(pm) == (a = 1, b = 2) + end + + @testset "MetaT{MultiPoint}" begin + p = collect(Point{2, Float64}(x, x+1) for x in 1:5) + @test p isa AbstractVector + mpm = MetaT(MultiPoint(p); a=1, b=2) + @test coordinates(mpm.main) == Point{2, Float64}[(x, x+1) for x in 1:5] + @test mpm.meta === (a=1, b=2) + @test mpm.main == p + @test propertynames(mpm) == (:main, :a, :b) + @test GeometryBasics.metafree(mpm) == p + @test GeometryBasics.meta(mpm) == (a = 1, b = 2) + end + + @testset "MetaT{LineString}" begin + linestring = MetaT(LineString(Point{2, Int}[(10, 10), (20, 20), (10, 40)]), a = 1, b = 2) + @test linestring isa MetaT + @test linestring.meta === (a = 1, b = 2) + @test propertynames(linestring) == (:main, :a, :b) + @test GeometryBasics.metafree(linestring) == LineString(Point{2, Int}[(10, 10), (20, 20), (10, 40)]) + @test GeometryBasics.meta(linestring) == (a = 1, b = 2) + end + + @testset "MetaT{MultiLineString}" begin + linestring1 = LineString(Point{2, Int}[(10, 10), (20, 20), (10, 40)]) + linestring2 = LineString(Point{2, Int}[(40, 40), (30, 30), (40, 20), (30, 10)]) + multilinestring = MultiLineString([linestring1, linestring2]) + multilinestringmeta = MetaT(MultiLineString([linestring1, linestring2]); boundingbox = Rect(1.0, 1.0, 2.0, 2.0)) + @test multilinestringmeta isa MetaT + @test multilinestringmeta.meta === (boundingbox = Rect(1.0, 1.0, 2.0, 2.0),) + @test multilinestringmeta.main == multilinestring + @test propertynames(multilinestringmeta) == (:main, :boundingbox) + @test GeometryBasics.metafree(multilinestringmeta) == multilinestring + @test GeometryBasics.meta(multilinestringmeta) == (boundingbox = GeometryBasics.HyperRectangle{2,Float64}([1.0, 1.0], [2.0, 2.0]),) + end + + #= + So mesh works differently for MetaT + since `MetaT{Point}` not subtyped to `AbstractPoint` + =# + + @testset "MetaT{Mesh}" begin + @testset "per vertex attributes" begin + points = rand(Point{3, Float64}, 8) + tfaces = TetrahedronFace{Int}[(1, 2, 3, 4), (5, 6, 7, 8)] + normals = rand(SVector{3, Float64}, 8) + stress = LinRange(0, 1, 8) + mesh_nometa = Mesh(points, tfaces) + mesh = MetaT(mesh_nometa, normals = normals, stress = stress) + + @test hasproperty(mesh, :stress) + @test hasproperty(mesh, :normals) + @test mesh.stress == stress + @test mesh.normals == normals + @test GeometryBasics.faces(mesh.main) == tfaces + @test propertynames(mesh) == (:main, :normals, :stress) + end + end +end + @testset "view" begin @testset "TupleView" begin x = [1, 2, 3, 4, 5, 6] @@ -505,6 +604,37 @@ end @test <(x, x1) end +@testset "MetaT and heterogeneous data" begin + ls = [LineString([Point(i, (i+1)^2/6), Point(i*0.86,i+5), Point(i/3, i/7)]) for i in 1:10] + 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]) + poly = Polygon(Point{2, Int}[(40, 40), (20, 45), (45, 30), (40, 40)]) + geom = [ls..., mls, poly] + prop = Any[(country_states = "India$(i)", rainfall = (i*9)/2) for i in 1:11] + push!(prop, (country_states = 12, rainfall = 1000)) # a pinch of heterogeneity + + feat = [MetaT(i, j) for (i,j) = zip(geom, prop)] + sa = meta_table(feat) + + @test nameof(eltype(feat)) == :MetaT + @test eltype(sa) === MetaT{Any,(:country_states, :rainfall),Tuple{Any,Float64}} + @test propertynames(sa) === (:main, :country_states, :rainfall) + @test getproperty(sa, :country_states) isa Array{Any} + @test getproperty(sa, :main) == geom + + @test GeometryBasics.getnamestypes(typeof(feat[1])) == + (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}}}}, + (:country_states, :rainfall), Tuple{String,Float64}) + + @test StructArrays.staticschema(typeof(feat[1])) == + 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}}}}, + String,Float64}} + + @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]) + + @test Base.getindex(feat[1], 1) isa Line + @test Base.size(feat[1]) == (2,) +end + @testset "Tests from GeometryTypes" begin include("geometrytypes.jl") end