package main import ( "context" "encoding/json" "fmt" "sort" "strings" graphqueries "visualizador_instanciados/backend_go/graph_queries" ) const rdfsLabelIRI = "http://www.w3.org/2000/01/rdf-schema#label" func fetchGraphSnapshot( ctx context.Context, sparql *AnzoGraphClient, cfg Config, nodeLimit int, edgeLimit int, graphQueryID string, ) (GraphResponse, error) { def, ok := graphqueries.Get(graphQueryID) if !ok { return GraphResponse{}, fmt.Errorf("unknown graph_query_id: %s", graphQueryID) } edgesQ := def.EdgeQuery(edgeLimit, cfg.IncludeBNodes) raw, err := sparql.Query(ctx, edgesQ) if err != nil { return GraphResponse{}, err } var res sparqlResponse if err := json.Unmarshal(raw, &res); err != nil { return GraphResponse{}, fmt.Errorf("failed to parse SPARQL JSON: %w", err) } nodes, edges := graphFromSparqlBindings(res.Results.Bindings, nodeLimit, cfg.IncludeBNodes) // Layout: invert edges for hierarchy (target -> source). hierEdges := make([][2]int, 0, len(edges)) for _, e := range edges { hierEdges = append(hierEdges, [2]int{int(e.Target), int(e.Source)}) } layers, cycleErr := levelSynchronousKahnLayers(len(nodes), hierEdges) if cycleErr != nil { sample := make([]string, 0, 20) for _, nid := range cycleErr.RemainingNodeIDs { if len(sample) >= 20 { break } if nid >= 0 && nid < len(nodes) { sample = append(sample, nodes[nid].IRI) } } cycleErr.RemainingIRISample = sample return GraphResponse{}, cycleErr } idToIRI := make([]string, len(nodes)) for i := range nodes { idToIRI[i] = nodes[i].IRI } for _, layer := range layers { sortLayerByIRI(layer, idToIRI) } xs, ys := radialPositionsFromLayers(len(nodes), layers, 5000.0) for i := range nodes { nodes[i].X = xs[i] nodes[i].Y = ys[i] } // Attach labels for URI nodes. iris := make([]string, 0) for _, n := range nodes { if n.TermType == "uri" && n.IRI != "" { iris = append(iris, n.IRI) } } if len(iris) > 0 { labelByIRI, err := fetchRDFSLabels(ctx, sparql, iris, 500) if err != nil { return GraphResponse{}, err } for i := range nodes { if nodes[i].TermType != "uri" { continue } lbl, ok := labelByIRI[nodes[i].IRI] if !ok { continue } val := lbl nodes[i].Label = &val } } meta := &GraphMeta{ Backend: "anzograph", TTLPath: nil, SparqlEndpoint: cfg.EffectiveSparqlEndpoint(), IncludeBNodes: cfg.IncludeBNodes, GraphQueryID: graphQueryID, NodeLimit: nodeLimit, EdgeLimit: edgeLimit, Nodes: len(nodes), Edges: len(edges), } return GraphResponse{Nodes: nodes, Edges: edges, Meta: meta}, nil } type bestLabel struct { score int value string } func fetchRDFSLabels( ctx context.Context, sparql *AnzoGraphClient, iris []string, batchSize int, ) (map[string]string, error) { best := make(map[string]bestLabel) for i := 0; i < len(iris); i += batchSize { end := i + batchSize if end > len(iris) { end = len(iris) } batch := iris[i:end] values := make([]string, 0, len(batch)) for _, u := range batch { values = append(values, "<"+u+">") } q := fmt.Sprintf(` SELECT ?s ?label WHERE { VALUES ?s { %s } ?s <%s> ?label . } `, strings.Join(values, " "), rdfsLabelIRI) raw, err := sparql.Query(ctx, q) if err != nil { return nil, err } var res sparqlResponse if err := json.Unmarshal(raw, &res); err != nil { return nil, fmt.Errorf("failed to parse SPARQL JSON: %w", err) } for _, b := range res.Results.Bindings { sTerm, ok := b["s"] if !ok || sTerm.Value == "" { continue } lblTerm, ok := b["label"] if !ok || lblTerm.Type != "literal" || lblTerm.Value == "" { continue } score := labelScore(lblTerm.Lang) prev, ok := best[sTerm.Value] if !ok || score > prev.score { best[sTerm.Value] = bestLabel{score: score, value: lblTerm.Value} } } } out := make(map[string]string, len(best)) for iri, v := range best { out[iri] = v.value } return out, nil } func labelScore(lang string) int { lang = strings.ToLower(strings.TrimSpace(lang)) if lang == "en" { return 3 } if lang == "" { return 2 } return 1 } func sortIntsUnique(xs []int) []int { if len(xs) == 0 { return xs } sort.Ints(xs) out := xs[:0] var last int for i, v := range xs { if i == 0 || v != last { out = append(out, v) } last = v } return out }