diff --git a/Project.toml b/Project.toml index a6fa44f..5e098aa 100644 --- a/Project.toml +++ b/Project.toml @@ -5,6 +5,7 @@ version = "0.6.0" [deps] DelimitedFiles = "8bb1440f-4735-579b-a4ab-409b98df4dab" Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6" +MetaGraphs = "626554b9-1ddb-594c-aa3c-2596fe9399a5" Requires = "ae029012-a4dd-5104-9daa-d747884805df" SimpleTraits = "699a6c99-e7fa-54fc-8d76-47d257e15c1d" diff --git a/src/GraphML/GraphML.jl b/src/GraphML/GraphML.jl index 93ba5c7..f94aec8 100644 --- a/src/GraphML/GraphML.jl +++ b/src/GraphML/GraphML.jl @@ -6,6 +6,8 @@ using Graphs: AbstractGraphFormat import Graphs: loadgraph, loadgraphs, savegraph +using MetaGraphs + export GraphMLFormat @@ -13,6 +15,105 @@ export GraphMLFormat struct GraphMLFormat <: AbstractGraphFormat end +@enum GraphMLAttributesDomain atgraph atnode atedge atall +const graphMLAttributesDomain = Dict("graph" => atgraph, + "node" => atnode, + "edge" => atedge, + "all" => atall) + +@enum GraphlMLAttributesType atboolean atint atlong atfloat atdouble atstring +const graphMLAttributesType = Dict("int" => Int, + "boolean" => Bool, + "long" => Int128, + "float" => Float64, + "double" => Float64, + "string" => String) + +struct AttrKey{T} + id::String + name::String + domain::GraphMLAttributesDomain + type::Type{T} + default::Union{T,Nothing} +end + +function _get_key_props(doc::EzXML.Document) + ns = namespace(doc.root) + keynodes = findall("//x:key", doc.root, ["x"=>ns]) + keyprops = Dict{String,AttrKey}() + for keynode in keynodes + attrtype = graphMLAttributesType[strip(keynode["attr.type"])] + keyadded = false + for childnode in EzXML.eachnode(keynode) + if EzXML.nodename(childnode) == "default" + defaultcontent = strip(nodecontent(childnode)) + keyprops[keynode["id"]] = AttrKey(keynode["id"], keynode["attr.name"], graphMLAttributesDomain[keynode["for"]], attrtype, attrtype == String ? defaultcontent : parse(attrtype, defaultcontent) ) + keyadded = true + end + end + if !keyadded + keyprops[keynode["id"]] = AttrKey(keynode["id"], keynode["attr.name"], graphMLAttributesDomain[keynode["for"]], attrtype, nothing ) + end + end + return keyprops +end + +function _loadmetagraph_fromnode(graphnode::EzXML.Node, keyprops::Dict{String, AttrKey}) + ns = namespace(graphnode) + gr = graphnode["edgedefault"] == "directed" ? MetaDiGraph() : MetaGraph() + set_indexing_prop!(gr, :id) + defaults = [v for v in values(keyprops) if getfield(v,:default) !== nothing && getfield(v,:domain) == atnode] + for (i,node) in enumerate(findall("x:node", graphnode, ["x"=>ns])) + add_vertex!(gr) + set_prop!(gr, i, :id, node["id"]) + for def in defaults + set_prop!(gr, i, Symbol(def.name), def.default) + end + for data in findall("x:data", node, ["x"=>ns]) + set_prop!(gr, i, Symbol(keyprops[data["key"]].name), keyprops[data["key"]].type == String ? nodecontent(data) : parse(keyprops[data["key"]].type, nodecontent(data))) + end + end + + defaults = [v for v in values(keyprops) if getfield(v,:default) !== nothing && getfield(v,:domain) == atedge] + for edge in findall("x:edge", graphnode, ["x"=>ns]) + srcnode = gr[edge["source"],:id] + trgnode = gr[edge["target"],:id] + add_edge!(gr, srcnode, trgnode) + set_prop!(gr, srcnode, trgnode, :id, edge["id"]) + for def in defaults + set_prop!(gr, srcnode, trgnode, Symbol(def.name), def.default) + end + for data in findall("x:data", edge, ["x"=>ns]) + set_prop!(gr, srcnode, trgnode, Symbol(keyprops[data["key"]].name), keyprops[data["key"]].type == String ? strip(nodecontent(data)) : parse(keyprops[data["key"]].type, nodecontent(data))) + end + end + return gr +end + +function loadmetagraphml(io::IO, gname::String) + doc = readxml(io) + ns = namespace(doc.root) + keyprops = _get_key_props(doc) + + + graphnodes = findall("//x:graph", doc.root, ["x"=>ns]) + for graphnode in graphnodes + if graphnode["id"] == gname + return _loadmetagraph_fromnode(graphnode, keyprops) + end + end +end +function loadmetagraphml_mult(io::IO) + doc = readxml(io) + ns = namespace(doc.root) + keyprops = _get_key_props(doc) + + graphnodes = findall("//x:graph", doc.root, ["x"=>ns]) + + graphs = Dict(graphnode["id"] => _loadmetagraph_fromnode(graphnode, keyprops) + for graphnode in graphnodes) +end + function _graphml_read_one_graph(reader::EzXML.StreamReader, isdirected::Bool) nodes = Dict{String,Int}() xedges = Vector{Graphs.Edge}() @@ -129,10 +230,101 @@ end savegraphml(io::IO, g::Graphs.AbstractGraph, gname::String) = savegraphml_mult(io, Dict(gname => g)) +function _get_attr_type(mg::AbstractMetaGraph, attr, forel) + if forel == atnode + els = vertices(mg) + elseif forel == atedge + els = edges(mg) + end + for el in els + has_prop(mg, el, attr) && return typeof(get_prop(mg, el, attr)) + end +end +function savemetagraphml_mult(io::IO, dgr::Dict{String, T}) where T<:AbstractMetaGraph + xdoc = XMLDocument() + xroot = setroot!(xdoc, ElementNode("graphml")) + xroot["xmlns"] = "http://graphml.graphdrawing.org/xmlns" + xroot["xmlns:xsi"] = "http://www.w3.org/2001/XMLSchema-instance" + xroot["xsi:schemaLocation"] = "http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd" + + #adds keys + attrforellist = Vector{Tuple{Symbol, GraphMLAttributesDomain}}() + for mg in values(dgr) + vattrs = Set(v for keyset in keys.(values(mg.vprops)) for v in keyset) + eattrs = Set(v for keyset in keys.(values(mg.eprops)) for v in keyset) + for (attr, forel) in Iterators.flatten([zip(vattrs, Iterators.cycle([atnode])), + zip(eattrs, Iterators.cycle([atedge]))]) + (attr, forel) in attrforellist && continue + push!(attrforellist, (attr, forel)) + xkey = addelement!(xroot, "key") + xkey["attr.name"] = string(attr) + xkey["attr.type"] = first(Iterators.filter(x -> x.second == _get_attr_type(mg, attr, forel), graphMLAttributesType)).first + xkey["for"] = first(Iterators.filter(x -> x.second == forel, graphMLAttributesDomain)).first + xkey["id"] = string(attr) + end + end + + for (gname, mg) in dgr + xg = addelement!(xroot, "graph") + xg["id"] = gname + xg["edgedefault"] = is_directed(mg) ? "directed" : "undirected" + + for i in 1:nv(mg) + xv = addelement!(xg, "node") + if has_prop(mg, i, :id) + xv["id"] = get_prop(mg, i, :id) + else + xv["id"] = "n$(i-1)" + end + for (k,v) in props(mg, i) + k == :id && continue + xel = addelement!(xv, "data", string(v)) + xel["key"] = k + end + end + + m = 0 + for e in Graphs.edges(mg) + xe = addelement!(xg, "edge") + + if has_prop(mg, e, :id) + xe["id"] = get_prop(mg, e, :id) + else + xe["id"] = "e$(m)" + end + + if has_prop(mg, src(e), :id) + xe["source"] = get_prop(mg, src(e), :id) + else + xe["source"] = "n$(src(e)-1)" + end + if has_prop(mg, dst(e), :id) + xe["target"] = get_prop(mg, dst(e), :id) + else + xe["target"] = "n$(dst(e)-1)" + end + + for (k,v) in props(mg, e) + k == :id && continue + xel = addelement!(xe, "data", string(v)) + xel["key"] = k + end + m += 1 + end + end + prettyprint(io, xdoc) + return 1 +end + +loadgraph(io::IO, gname::String, ::GraphMLFormat, ::MGFormat) = loadmetagraphml(io, gname) +loadgraphs(io::IO, ::GraphMLFormat, ::MGFormat) = loadmetagraphml_mult(io) loadgraph(io::IO, gname::String, ::GraphMLFormat) = loadgraphml(io, gname) loadgraphs(io::IO, ::GraphMLFormat) = loadgraphml_mult(io) + +savegraph(io::IO, g::AbstractMetaGraph, gname::String, ::GraphMLFormat) = savemetagraphml_mult(io, Dict(gname => g)) savegraph(io::IO, g::AbstractGraph, gname::String, ::GraphMLFormat) = savegraphml(io, g, gname) savegraph(io::IO, d::Dict, ::GraphMLFormat) = savegraphml_mult(io, d) +savegraph(io::IO, d::Dict{String, T}, ::GraphMLFormat) where T<:AbstractMetaGraph = savemetagraphml_mult(io, d) end # module diff --git a/test/GraphML/runtests.jl b/test/GraphML/runtests.jl index 2fec88a..c4986c4 100644 --- a/test/GraphML/runtests.jl +++ b/test/GraphML/runtests.jl @@ -1,16 +1,69 @@ using Test using EzXML using GraphIO.GraphML +using Graphs, MetaGraphs @testset "GraphML" begin for g in values(allgraphs) readback_test(GraphMLFormat(), g, testfail=true) end fname = joinpath(testdir, "testdata", "warngraph.graphml") - + @test_logs (:warn, "Skipping unknown node 'warnnode' - further warnings will be suppressed") match_mode=:any loadgraphs(fname, GraphMLFormat()) @test_logs (:warn, "Skipping unknown XML element 'warnelement' - further warnings will be suppressed") match_mode=:any loadgraph(fname, "graph", GraphMLFormat()) d = loadgraphs(fname, GraphMLFormat()) write_test(GraphMLFormat(), d) end +function test_read_metagraph(dmg) + for v in vertices(dmg) + if get_prop(dmg, v, :id) == "N6" + @test get_prop(dmg, v, :VertexLabel) == "N6" + @test get_prop(dmg, v, :xcoord) == 170 + @test get_prop(dmg, v, :ycoord) == 0 + end + end + for e in edges(dmg) + if get_prop(dmg, e, :id) == "N0-N3" + @test get_prop(dmg, e, :LinkCapacity) == 100 + end + end +end + +@testset "GraphML-MGFormat" begin + # single graph + fname = joinpath(testdir, "testdata", "complex.graphml") + mg = open(fname, "r") do io + loadgraph(io, "main-graph", GraphMLFormat(), MGFormat()) + end + test_read_metagraph(mg) + + # re-read must be equal + ftname = joinpath(testdir, "testdata", "complex_main-graph_write.graphml") + savegraph(ftname, mg, "main-graph", GraphMLFormat()) + mg2 = open(ftname, "r") do io + loadgraph(io, "main-graph", GraphMLFormat(), MGFormat()) + end + @test mg == mg2 && mg.vprops == mg2.vprops && mg.eprops == mg2.eprops + rm(ftname) + + # multiple graphs + dmg = open(fname, "r") do io + loadgraphs(io, GraphMLFormat(), MGFormat()) + end + + @test length(dmg) == 2 + test_read_metagraph(dmg["main-graph"]) + + # re-read must be equal + ftname = joinpath(testdir, "testdata", "complex_write.graphml") + savegraph(ftname, mg, GraphMLFormat()) + dmg2 = open(ftname, "r") do io + loadgraphs(io, GraphMLFormat(), MGFormat()) + end + for (dmg_g, dmg2_g) in zip(values(dmg), values(dmg2)) + @test dmg_g == dmg2_g && dmg_g.vprops == dmg2_g.vprops && dmg_g.eprops == dmg2_g.eprops + end + rm(ftname) +end + diff --git a/test/testdata/complex.graphml b/test/testdata/complex.graphml new file mode 100644 index 0000000..753d6da --- /dev/null +++ b/test/testdata/complex.graphml @@ -0,0 +1,142 @@ + + + + + + + + 100.0 + + + + N0 + 0.0 + 0.0 + + + N1 + 10.0 + 100.0 + + + N2 + 110.0 + 100.0 + + + N3 + 100.0 + 0.0 + + + N4 + 20.0 + 200.0 + + + N5 + 120.0 + 200.0 + + + N6 + 170.0 + 0.0 + + + N7 + 230.0 + -100.0 + + + N8 + 260.0 + 140.0 + + + N9 + 300.0 + 0.0 + + + N10 + 310.0 + 200.0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + N0 + 0.0 + 0.0 + + + N1 + 10.0 + 100.0 + + + N2 + 110.0 + 100.0 + + + N3 + 100.0 + 0.0 + + + + + +