/**
* @file Creation of nodes according to the AntV specification
* @module @@graph/Node
*/
import G6 from '@antv/g6';
import { flatten, last } from 'ramda';
import E from '@@graph/ElementCreators';
import Group from '@@graph/wrappers/Group';
import { getSuffix } from '@@data/parsePrefix';
export const NODE_TYPE = 'graphNode';
export const PROP_LINE_HEIGHT = 12;
const nodeContainerColor = 'lightgrey';
const textColor = 'black';
const getAttrs = ctx => ({
'node-container': ({label}) => ({
width: ctx.measureText(label).width + 8,
height: 20,
stroke: textColor, // Apply the color to the stroke. For filling, use fill: cfg.color instead
fill: nodeContainerColor,
opacity: 1
}),
'node-title': (width, text) => ({
x: width / 2, // center
y: 4,
textAlign: 'center',
textBaseline: 'top',
text,
fill: textColor,
}),
property: ({predicate, type, i, ctx}) => {
const text = `${predicate}: ${type}`;
return {
x: 4, // center
y: PROP_LINE_HEIGHT * (i+1) + PROP_LINE_HEIGHT - 2,
textAlign: 'left',
textBaseline: 'top',
width: Math.round(ctx.measureText(text).width + 8),
text,
fill: textColor,
stroke: textColor
}
},
'property-container': propArr => ({
y: PROP_LINE_HEIGHT * 2 - 4,
width: Math.max(...propArr.map(p => p.attrs.width)),
height: propArr.length * PROP_LINE_HEIGHT + 6,
stroke: 'black',
fill: nodeContainerColor,
opacity: 1
})
});
const registeredNames = {};
const getVarName = (iri, sourceId) => {
const propSuffix = getSuffix(iri);
const entitySuffix = getSuffix(sourceId)
if (registeredNames[propSuffix]) {
return `${entitySuffix}_${propSuffix}`;
} else {
registeredNames[propSuffix] = true;
return propSuffix;
}
};
const getAddPropertyFn = (group, ctx, attrs) => ({source, predicate, target, targetType, varName, dataProperty}) => {
const propertyCount = Object.values(group.get('wrapper').propertyWrappers).length;
const id = `property_${source}-${predicate}-${target}`;
group.addShape(E.ObjectProperty({
id,
attrs: attrs.property({predicate, type: target, i: propertyCount, ctx}),
name: id,
data: {target, source, predicate, targetType, varName: varName || getVarName(predicate, source), dataProperty}
}));
const node = last(group.getChildren());
const wrapper = node.get('wrapper');
if (wrapper) {
wrapper.setNode(node)
const containerNode = group.get('item');
node.set('containerNode', containerNode);
wrapper.setContainerNode(containerNode);
}
group.get('wrapper').registerProperty(node);
}
const NodeImplementation = {
draw(cfg, group) {
const {data, id} = cfg;
const ctx = group.get('canvas').get('context');
ctx.save();
ctx.font = '12px sans-serif';
const attrs = getAttrs(ctx);
const dataPropertyCount = Object.keys(data.dataProperties).length;
const containerAttrs = attrs['node-container']({label: cfg.label});
const {width, height} = containerAttrs;
const dataProperties = Object.entries(data.dataProperties).map(([predicate, objects], i)=> {
const propId = `property_${id}-${predicate}-${objects[0]}`;
return E.DataProperty({
id: propId,
attrs: attrs.property({predicate, type: objects[0], i, ctx}),
name: propId,
data: {target: objects[0], targetType: objects[0], source: id, predicate, varName: getVarName(predicate, id), dataProperty: true}
})
});
let i = 0;
const objectProperties = flatten(Object.entries(data.objectProperties).map(([predicate, objects]) =>
objects.map(target => {
const propId = `property_${id}-${predicate}-${target}`;
return E.ObjectProperty({
id: propId,
attrs: attrs.property({predicate, type: target, i: i + dataPropertyCount, ctx}),
name: propId,
data: {target, source: id, predicate, targetType: target, varName: getVarName(predicate, id)}
})
})
));
const propertyContainerAttrs = attrs['property-container'](dataProperties.concat(objectProperties));
const expandIconAttrs = {
x: width + 3,
y: 5,
img: 'images/expand.png',
width: 10,
height: 10
};
const copyNodeIconAttrs = {
x: -14,
y: -14,
img: 'images/copy-icon.png',
width: 12,
height: 12
};
const deleteNodeIconAttrs = {
x: -30,
y: -14,
img: 'images/delete-icon.png',
width: 12,
height: 12
};
const selectAllIconAttrs = {
x: -15,
y: 3,
img: 'images/plus.png',
width: 14,
height: 14
};
const hideIconAttrs = {
x: -30,
y: 4,
img: 'images/eye-invisible.png',
width: 12,
height: 12
};
group.set('objectProperties', objectProperties);
group.set('data', data);
group.set('addProperty', getAddPropertyFn(group, ctx, attrs));
group.entityId = id;
const result = E.create(group, [
E.Node({id, attrs: containerAttrs, name: 'node-container', data: {varName: getSuffix(id), type: id} }),
E.Rect({id: `node_${id}-delete-node-container`, name: 'delete-node-container', attrs: {x: -32, y: -16, width: 16, height, fill: containerAttrs.fill, stroke: containerAttrs.stroke}}),
E.Image({id: `node_${id}-delete-node-icon`, name: 'delete-node-icon', attrs: deleteNodeIconAttrs}),
E.Rect({id: `node_${id}-copy-node-container`, name: 'copy-node-container', attrs: {x: -16, y: -16, width: 16, height, fill: containerAttrs.fill, stroke: containerAttrs.stroke}}),
E.Image({id: `node_${id}-copy-node-icon`, name: 'copy-node-icon', attrs: copyNodeIconAttrs}),
E.Rect({id: `node_${id}-varName-container`, attrs: {x: 0, y: -16, width: 32, height: 16, fill: containerAttrs.fill, stroke: containerAttrs.stroke}, name: 'node-varName-container'}),
E.Text({id: `node_${id}-varName`, attrs: {...attrs['node-title'](width, cfg.varName || cfg.label), y: -14, textAlign: 'left', x: 2}, name: 'node-varName'}),
E.Text({id: `node_${id}-title`, attrs: attrs['node-title'](width, cfg.label), name: 'node-title'}),
E.Rect({id: `node_${id}-select-all-container`, attrs: {x: -16, width: 16, height, fill: containerAttrs.fill, stroke: containerAttrs.stroke}, name: 'select-all-container'}),
E.Image({id: `node_${id}-select-all-icon`, name: 'select-all-icon', attrs: selectAllIconAttrs}),
E.Rect({id: `node_${id}-hide-icon-container`, attrs: {x: hideIconAttrs.x - 2, width: hideIconAttrs.width + 4, height, fill: containerAttrs.fill, stroke: containerAttrs.stroke}, name: 'hide-icon-container'}),
E.Image({id: `node_${id}-hide-icon`, name: 'hide-icon', attrs: hideIconAttrs}),
E.Rect({id: `node_${id}-expand-icon-container`, attrs: {x: width, width: 16, height, fill: containerAttrs.fill, stroke: containerAttrs.stroke}, name: 'expand-icon-container'}),
E.Image({id: `node_${id}-expand-icon`, name: 'expand-icon', attrs: expandIconAttrs}),
E.Rect({id: `node_${id}-prop-container`, attrs: propertyContainerAttrs, name: 'property-container'}),
...dataProperties,
...objectProperties
])[0];
ctx.restore();
return result;
},
afterDraw(cfg, group) {
group.getChildren().forEach(node => {
const wrapper = node.get('wrapper');
if (wrapper) {
wrapper.setNode(node)
const containerNode = group.get('item');
node.set('containerNode', containerNode);
wrapper.setContainerNode(containerNode);
}
});
const wrapper = new Group(group.entityId, group);
group.set('wrapper', wrapper);
}
};
export const createNode = data => ({
...data,
type: NODE_TYPE
});
export const getNodes = data => Object.entries(data).map(([id, {objectProperties, dataProperties}]) => createNode({
id: id,
label: id,
data: {objectProperties, dataProperties}
}));
export const measureText = (node, txt) => {
const ctx = node.get('canvas').get('context');
ctx.save();
ctx.font = '12px sans-serif';
const res = ctx.measureText(txt);
ctx.restore();
return res;
};
G6.registerNode(NODE_TYPE, NodeImplementation, 'single-node');