UNPKG

graph-explorer

Version:

Graph Explorer can be used to explore and RDF graphs in SPARQL endpoints or on the web.

794 lines (722 loc) 22.1 kB
import { LinkConfiguration, PropertyConfiguration, } from "./sparqlDataProviderSettings"; import { RdfLiteral, isRdfLiteral, SparqlResponse, ClassBinding, ElementBinding, LinkBinding, isRdfIri, isRdfBlank, RdfIri, ElementImageBinding, LinkCountBinding, LinkTypeBinding, PropertyBinding, ElementTypeBinding, FilterBinding, Triple, } from "./sparqlModels"; import { Dictionary, LocalizedString, LinkType, ClassModel, ElementModel, LinkModel, Property, PropertyModel, LinkCount, ElementIri, ElementTypeIri, LinkTypeIri, PropertyTypeIri, isIriProperty, isLiteralProperty, sameLink, hashLink, } from "../model"; import { HashMap, getOrCreateSetInMap } from "../../viewUtils/collections"; const LABEL_URI = "http://www.w3.org/2000/01/rdf-schema#label"; const RDF_TYPE_URI = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type"; const EMPTY_MAP: ReadonlyMap<any, any> = new Map(); export function getClassTree( response: SparqlResponse<ClassBinding> ): ClassModel[] { const treeNodes = createClassMap(response.results.bindings); const allNodes: ClassModel[] = []; // createClassMap ensures we get both elements and parents and we can use treeNodes[treeNode.parent] safely treeNodes.forEach((node) => { allNodes.push(node); node.parents.forEach((parent) => { treeNodes.get(parent).children.push(node); }); node.parents = undefined; }); const withoutCycles = breakCyclesAndCalculateCounts(allNodes); const leafs = new Set<ElementTypeIri>(); for (const node of withoutCycles) { for (const child of node.children) { leafs.add(child.id); } } const tree = withoutCycles.filter((node) => !leafs.has(node.id)); return tree; } export function flattenClassTree(classTree: readonly ClassModel[]) { const all: ClassModel[] = []; const visitClasses = (classes: readonly ClassModel[]) => { for (const model of classes) { all.push(model); visitClasses(model.children); } }; visitClasses(classTree); return all; } /** * This extension of ClassModel is used only in processing, parent links are not needed in UI (yet?) */ interface HierarchicalClassModel extends ClassModel { parents?: Set<ElementTypeIri>; } function createClassMap( bindings: ClassBinding[] ): Map<ElementTypeIri, HierarchicalClassModel> { const treeNodes = new Map<ElementTypeIri, HierarchicalClassModel>(); for (const binding of bindings) { if (!isRdfIri(binding.class)) { continue; } const classIri = binding.class.value as ElementTypeIri; let node = treeNodes.get(classIri); if (!node) { node = createEmptyModel(classIri); treeNodes.set(classIri, node); } if (binding.label) { appendLabel(node.label, getLocalizedString(binding.label)); } if (binding.parent) { const parentIri = binding.parent.value as ElementTypeIri; node.parents.add(parentIri); } if (binding.instcount) { node.count = getInstCount(binding.instcount); } } // ensuring parent will always be there for (const binding of bindings) { if (binding.parent) { const parentIri = binding.parent.value as ElementTypeIri; if (!treeNodes.has(parentIri)) { treeNodes.set(parentIri, createEmptyModel(parentIri)); } } } function createEmptyModel(iri: ElementTypeIri): HierarchicalClassModel { return { id: iri as ElementTypeIri, children: [], label: { values: [] }, count: undefined, parents: new Set<ElementTypeIri>(), }; } return treeNodes; } function breakCyclesAndCalculateCounts(tree: ClassModel[]): ClassModel[] { const visiting = new Set<ElementTypeIri>(); function reduceChildren(acc: ClassModel[], node: ClassModel): ClassModel[] { if (visiting.has(node.id)) { // prevent unbounded recursion return acc; } // no more to count if (!node.children) { return; } // ensure all children have their counts completed; visiting.add(node.id); node.children = node.children.reduce(reduceChildren, []); visiting.delete(node.id); // we have to preserve no data here. If nor element nor childs have no count information, // we just pass undefined upwards. let childCount: number; node.children.forEach(({ count }) => { if (count === undefined) { return; } childCount = childCount === undefined ? count : childCount + count; }); if (childCount !== undefined) { node.count = node.count === undefined ? childCount : node.count + childCount; } acc.push(node); return acc; } return tree.reduce(reduceChildren, []); } export function getClassInfo( response: SparqlResponse<ClassBinding> ): ClassModel[] { const classes: Record<string, ClassModel> = {}; for (const binding of response.results.bindings) { if (!binding.class) { continue; } const id = binding.class.value as ElementTypeIri; const model = classes[id]; if (model) { appendLabel(model.label, getLocalizedString(binding.label)); const instanceCount = getInstCount(binding.instcount); if (instanceCount !== undefined) { model.count = Math.max(model.count, instanceCount); } } else { const label = getLocalizedString(binding.label); classes[id] = { id, children: [], label: { values: label ? [label] : [] }, count: getInstCount(binding.instcount), }; } } const classesList: ClassModel[] = []; for (const id in classes) { if (!Object.prototype.hasOwnProperty.call(classes, id)) { continue; } const model = classes[id]; classesList.push(model); } return classesList; } export function getPropertyInfo( response: SparqlResponse<PropertyBinding> ): Dictionary<PropertyModel> { const models: Dictionary<PropertyModel> = {}; for (const sProperty of response.results.bindings) { const sPropertyTypeId = sProperty.property.value as PropertyTypeIri; if (models[sPropertyTypeId]) { if (sProperty.label) { const label = models[sPropertyTypeId].label; if (label.values.length === 1 && !label.values[0].language) { label.values = []; } label.values.push(getLocalizedString(sProperty.label)); } } else { models[sPropertyTypeId] = getPropertyModel(sProperty); } } return models; } export function getLinkTypes( response: SparqlResponse<LinkTypeBinding> ): LinkType[] { const sInst = response.results.bindings; const linkTypes: LinkType[] = []; const linkTypesMap: Dictionary<LinkType> = {}; for (const sLink of sInst) { const sInstTypeId = sLink.link.value as LinkTypeIri; if (linkTypesMap[sInstTypeId]) { if (sLink.label) { const label = linkTypesMap[sInstTypeId].label; if (label.values.length === 1 && !label.values[0].language) { label.values = []; } label.values.push(getLocalizedString(sLink.label)); } } else { linkTypesMap[sInstTypeId] = getLinkTypeInfo(sLink); linkTypes.push(linkTypesMap[sInstTypeId]); } } return linkTypes; } export function triplesToElementBinding( triples: Triple[] ): SparqlResponse<ElementBinding> { const map: Dictionary<ElementBinding> = {}; const convertedResponse: SparqlResponse<ElementBinding> = { head: { vars: ["inst", "class", "label", "blankType", "propType", "propValue"], }, results: { bindings: [], }, }; for (const triple of triples) { const trippleId = triple.subject.value; if (!map[trippleId]) { map[trippleId] = createAndPushBinding(triple); } if (triple.predicate.value === LABEL_URI && isRdfLiteral(triple.object)) { // Label if (map[trippleId].label) { map[trippleId] = createAndPushBinding(triple); } map[trippleId].label = triple.object; } else if ( // Class triple.predicate.value === RDF_TYPE_URI && isRdfIri(triple.object) && isRdfIri(triple.predicate) ) { if (map[trippleId].class) { map[trippleId] = createAndPushBinding(triple); } map[trippleId].class = triple.object; } else if (!isRdfBlank(triple.object) && isRdfIri(triple.predicate)) { // Property if (map[trippleId].propType) { map[trippleId] = createAndPushBinding(triple); } map[trippleId].propType = triple.predicate; map[trippleId].propValue = triple.object; } } function createAndPushBinding(tripple: Triple): ElementBinding { const binding: ElementBinding = { inst: tripple.subject as RdfIri, }; convertedResponse.results.bindings.push(binding); return binding; } return convertedResponse; } export function getElementsInfo( response: SparqlResponse<ElementBinding>, types: ReadonlyMap<ElementIri, ReadonlySet<ElementTypeIri>> = EMPTY_MAP, propertyByPredicate: ReadonlyMap< string, readonly PropertyConfiguration[] > = EMPTY_MAP, openWorldProperties = true ): Dictionary<ElementModel> { const instancesMap: Dictionary<ElementModel> = {}; for (const binding of response.results.bindings) { if (!isRdfIri(binding.inst)) { continue; } const iri = binding.inst.value as ElementIri; let model = instancesMap[iri]; if (!model) { model = emptyElementInfo(iri); instancesMap[iri] = model; } enrichElement(model, binding); } if (!openWorldProperties || propertyByPredicate.size > 0) { for (const iri in instancesMap) { if (!Object.hasOwnProperty.call(instancesMap, iri)) { continue; } const model = instancesMap[iri]; const modelTypes = types.get(model.id); model.properties = mapPropertiesByConfig( model, modelTypes, propertyByPredicate, openWorldProperties ); } } return instancesMap; } function mapPropertiesByConfig( model: ElementModel, modelTypes: ReadonlySet<ElementTypeIri> | undefined, propertyByPredicate: ReadonlyMap<string, readonly PropertyConfiguration[]>, openWorldProperties: boolean ): ElementModel["properties"] { const mapped: ElementModel["properties"] = {}; for (const propertyIri in model.properties) { if (!Object.hasOwnProperty.call(model.properties, propertyIri)) { continue; } const properties = propertyByPredicate.get(propertyIri); if (properties && properties.length > 0) { for (const property of properties) { if (typeMatchesDomain(property, modelTypes)) { mapped[property.id] = model.properties[propertyIri]; } } } else if (openWorldProperties) { mapped[propertyIri] = model.properties[propertyIri]; } } return mapped; } export function enrichElementsWithImages( response: SparqlResponse<ElementImageBinding>, elementsInfo: Dictionary<ElementModel> ): void { const respElements = response.results.bindings; for (const respEl of respElements) { const elementInfo = elementsInfo[respEl.inst.value]; if (elementInfo) { elementInfo.image = respEl.image.value; } } } export function getElementTypes( response: SparqlResponse<ElementTypeBinding> ): Map<ElementIri, Set<ElementTypeIri>> { const types = new Map<ElementIri, Set<ElementTypeIri>>(); for (const binding of response.results.bindings) { if (isRdfIri(binding.inst) && isRdfIri(binding.class)) { const element = binding.inst.value as ElementIri; const type = binding.class.value as ElementTypeIri; getOrCreateSetInMap(types, element).add(type); } } return types; } export function getLinksInfo( response: SparqlResponse<LinkBinding>, types: ReadonlyMap<ElementIri, ReadonlySet<ElementTypeIri>> = EMPTY_MAP, linkByPredicateType: ReadonlyMap< string, readonly LinkConfiguration[] > = EMPTY_MAP, openWorldLinks = true ): LinkModel[] { const sparqlLinks = response.results.bindings; const links = new HashMap<LinkModel, LinkModel>(hashLink, sameLink); for (const binding of sparqlLinks) { const model: LinkModel = { sourceId: binding.source.value as ElementIri, linkTypeId: binding.type.value as LinkTypeIri, targetId: binding.target.value as ElementIri, properties: {}, }; if (links.has(model)) { // this can only happen due to error in sparql or when merging properties if (binding.propType) { const existing = links.get(model); mergeProperties( existing.properties, binding.propType, binding.propValue ); } } else { if (binding.propType) { mergeProperties(model.properties, binding.propType, binding.propValue); } const linkConfigs = linkByPredicateType.get(model.linkTypeId); if (linkConfigs && linkConfigs.length > 0) { for (const linkConfig of linkConfigs) { if (typeMatchesDomain(linkConfig, types.get(model.sourceId))) { const mappedModel: LinkModel = isDirectLink(linkConfig) ? { ...model, linkTypeId: linkConfig.id as LinkTypeIri } : model; links.set(mappedModel, mappedModel); } } } else if (openWorldLinks) { links.set(model, model); } } } const linkArray: LinkModel[] = []; links.forEach((value) => linkArray.push(value)); return linkArray; } export function getLinksTypesOf( response: SparqlResponse<LinkCountBinding> ): LinkCount[] { const sparqlLinkTypes = response.results.bindings.filter( (b) => !isRdfBlank(b.link) ); return sparqlLinkTypes.map((sLink: LinkCountBinding) => getLinkCount(sLink)); } export function getLinksTypeIds( response: SparqlResponse<LinkTypeBinding>, linkByPredicateType: ReadonlyMap< string, readonly LinkConfiguration[] > = EMPTY_MAP, openWorldLinks = true ): LinkTypeIri[] { const linkTypes: LinkTypeIri[] = []; for (const binding of response.results.bindings) { if (!isRdfIri(binding.link)) { continue; } const linkConfigs = linkByPredicateType.get(binding.link.value); if (linkConfigs && linkConfigs.length > 0) { for (const linkConfig of linkConfigs) { const mappedLinkType = isDirectLink(linkConfig) ? linkConfig.id : binding.link.value; linkTypes.push(mappedLinkType as LinkTypeIri); } } else if (openWorldLinks) { linkTypes.push(binding.link.value as LinkTypeIri); } } return linkTypes; } export function getLinkStatistics( response: SparqlResponse<LinkCountBinding> ): LinkCount | undefined { for (const binding of response.results.bindings) { if (isRdfIri(binding.link)) { return getLinkCount(binding); } } return undefined; } export function getFilteredData( response: SparqlResponse<ElementBinding & FilterBinding>, sourceTypes?: ReadonlySet<ElementTypeIri>, linkByPredicateType: ReadonlyMap< string, readonly LinkConfiguration[] > = EMPTY_MAP, openWorldLinks = true ): Dictionary<ElementModel> { const instancesMap: Dictionary<ElementModel> = {}; const resultTypes = new Map<ElementIri, Set<ElementTypeIri>>(); const outPredicates = new Map<ElementIri, Set<string>>(); const inPredicates = new Map<ElementIri, Set<string>>(); for (const binding of response.results.bindings) { if (!isRdfIri(binding.inst) && !isRdfBlank(binding.inst)) { continue; } const iri = binding.inst.value as ElementIri; let model = instancesMap[iri]; if (!model) { model = emptyElementInfo(iri); instancesMap[iri] = model; } enrichElement(model, binding); if (isRdfIri(binding.classAll)) { getOrCreateSetInMap(resultTypes, iri).add( binding.classAll.value as ElementTypeIri ); } if (!openWorldLinks && binding.link && binding.direction) { const predicates = binding.direction.value === "out" ? outPredicates : inPredicates; getOrCreateSetInMap(predicates, model.id).add(binding.link.value); } } if (!openWorldLinks) { for (const id of Object.keys(instancesMap)) { const model = instancesMap[id]; const targetTypes = resultTypes.get(model.id); const doesMatchesDomain = matchesDomainForLink( sourceTypes, outPredicates.get(model.id), linkByPredicateType ) && matchesDomainForLink( targetTypes, inPredicates.get(model.id), linkByPredicateType ); if (!doesMatchesDomain) { // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete instancesMap[id]; } } } return instancesMap; } function matchesDomainForLink( types: ReadonlySet<ElementTypeIri> | undefined, predicates: Set<string> | undefined, linkByPredicateType: ReadonlyMap<string, readonly LinkConfiguration[]> ) { if (!predicates) { return true; } let hasMatch = false; predicates.forEach((predicate) => { const matched = linkByPredicateType.get(predicate); if (matched) { for (const link of matched) { if (typeMatchesDomain(link, types)) { hasMatch = true; } } } }); return hasMatch; } export function isDirectLink(link: LinkConfiguration) { // link configuration is path-based if includes any variables const pathBased = /[?$][a-zA-Z]+\b/.test(link.path); return !pathBased; } export function isDirectProperty(property: PropertyConfiguration) { // property configuration is path-based if includes any variables const pathBased = /[?$][a-zA-Z]+\b/.test(property.path); return !pathBased; } function typeMatchesDomain( config: { readonly domain?: readonly string[] }, types: ReadonlySet<ElementTypeIri> | undefined ): boolean { if (!config.domain || config.domain.length === 0) { return true; } else if (!types) { return false; } else { for (const type of config.domain) { if (types.has(type as ElementTypeIri)) { return true; } } return false; } } /** * Modifies properties with merging with new values, couls be new peroperty or new value for existing properties. * @param properties * @param propType * @param propValue */ function mergeProperties( properties: Record<string, Property>, propType: RdfIri, propValue: RdfIri | RdfLiteral ) { let property = properties[propType.value]; if (isRdfIri(propValue)) { if (!property) { property = { type: "uri", values: [] }; } if ( isIriProperty(property) && property.values.every(({ value }) => value !== propValue.value) ) { property.values = [...property.values, propValue]; } } else if (isRdfLiteral(propValue)) { if (!property) { property = { type: "string", values: [] }; } const propertyValue = getLocalizedString(propValue); if ( isLiteralProperty(property) && property.values.every((value) => !isLocalizedEqual(value, propertyValue)) ) { property.values = [...property.values, propertyValue]; } } properties[propType.value] = property; } export function enrichElement(element: ElementModel, sInst: ElementBinding) { if (!element) { return; } if (sInst.label) { const label = getLocalizedString(sInst.label); if ( element.label.values.every((value) => !isLocalizedEqual(value, label)) ) { element.label.values.push(label); } } if ( sInst.class && element.types.indexOf(sInst.class.value as ElementTypeIri) < 0 ) { element.types.push(sInst.class.value as ElementTypeIri); } if (sInst.propType && sInst.propType.value !== LABEL_URI) { mergeProperties(element.properties, sInst.propType, sInst.propValue); } } function appendLabel( container: { values: LocalizedString[] }, newLabel: LocalizedString | undefined ) { if (!newLabel) { return; } for (const existing of container.values) { if (isLocalizedEqual(existing, newLabel)) { return; } } container.values.push(newLabel); } function isLocalizedEqual(left: LocalizedString, right: LocalizedString) { return left.language === right.language && left.value === right.value; } export function getLocalizedString( label: RdfLiteral ): LocalizedString | undefined { if (label) { return { value: label.value, language: label["xml:lang"], datatype: label.datatype ? { value: label.datatype } : undefined, }; } else { return undefined; } } export function getInstCount(instcount: RdfLiteral): number | undefined { return instcount ? +instcount.value : undefined; } export function getPropertyModel(node: PropertyBinding): PropertyModel { const label = getLocalizedString(node.label); return { id: node.property.value as PropertyTypeIri, label: { values: label ? [label] : [] }, }; } export function getLinkCount(sLinkType: LinkCountBinding): LinkCount { return { id: sLinkType.link.value as LinkTypeIri, inCount: getInstCount(sLinkType.inCount), outCount: getInstCount(sLinkType.outCount), }; } export function emptyElementInfo(id: ElementIri): ElementModel { const elementInfo: ElementModel = { id: id, label: { values: [] }, types: [], properties: {}, }; return elementInfo; } export function getLinkTypeInfo(sLinkInfo: LinkTypeBinding): LinkType { if (!sLinkInfo) { return undefined; } const label = getLocalizedString(sLinkInfo.label); return { id: sLinkInfo.link.value as LinkTypeIri, label: { values: label ? [label] : [] }, count: getInstCount(sLinkInfo.instcount), }; } export function prependAdditionalBindings<Binding>( base: SparqlResponse<Binding>, additional: SparqlResponse<Binding> | undefined ): SparqlResponse<Binding> { if (!additional) { return base; } return { head: { vars: base.head.vars }, results: { bindings: [...additional.results.bindings, ...base.results.bindings], }, }; }