@myrmidon/cadmus-graph-ui-ex
Version:
Cadmus - extensions to semantic graph components.
2,252 lines • 142 kB
JavaScript
import * as i0 from '@angular/core';
import { input, output, effect, ViewChild, ChangeDetectionStrategy, Component, model, Pipe, signal } from '@angular/core';
import { Subject, forkJoin, from, BehaviorSubject, Observable, take as take$1 } from 'rxjs';
import { takeUntil, take } from 'rxjs/operators';
import * as i1 from '@angular/material/button';
import { MatButtonModule, MatIconButton } from '@angular/material/button';
import * as i2 from '@angular/material/icon';
import { MatIconModule, MatIcon } from '@angular/material/icon';
import * as i3 from '@angular/material/tooltip';
import { MatTooltipModule, MatTooltip } from '@angular/material/tooltip';
import * as ForceGraph from 'force-graph';
import * as ForceGraph3D from '3d-force-graph';
import * as d3 from 'd3-force';
import * as THREE from 'three';
import { AsyncPipe } from '@angular/common';
import { MatProgressBar } from '@angular/material/progress-bar';
import { MatTabGroup, MatTab, MatTabLabel } from '@angular/material/tabs';
import * as i1$1 from '@angular/forms';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatPaginator } from '@angular/material/paginator';
import { MatCheckbox } from '@angular/material/checkbox';
import { MatList, MatListItem } from '@angular/material/list';
import { MatFormField } from '@angular/material/form-field';
import { MatInput } from '@angular/material/input';
import { RefLookupComponent } from '@myrmidon/cadmus-refs-lookup';
import * as i2$1 from '@myrmidon/cadmus-graph-ui';
import * as i3$1 from '@myrmidon/cadmus-api';
import { MatSelect } from '@angular/material/select';
import { MatOption } from '@angular/material/core';
import { MatChipListbox, MatChipOption, MatChipRemove } from '@angular/material/chips';
import * as i1$2 from '@myrmidon/ngx-mat-tools';
/**
* Force graph renderer component that can display graphs in 2D or 3D
* using force-graph and 3d-force-graph libraries.
*/
class ForceGraphRendererComponent {
graphContainer;
nodes = input([], /* @ts-ignore */
...(ngDevMode ? [{ debugName: "nodes" }] : /* istanbul ignore next */ []));
edges = input([], /* @ts-ignore */
...(ngDevMode ? [{ debugName: "edges" }] : /* istanbul ignore next */ []));
mode = input('2d', /* @ts-ignore */
...(ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []));
update$ = input(/* @ts-ignore */
...(ngDevMode ? [undefined, { debugName: "update$" }] : /* istanbul ignore next */ []));
center$ = input(/* @ts-ignore */
...(ngDevMode ? [undefined, { debugName: "center$" }] : /* istanbul ignore next */ []));
zoomToFit$ = input(/* @ts-ignore */
...(ngDevMode ? [undefined, { debugName: "zoomToFit$" }] : /* istanbul ignore next */ []));
nodeSelect = output();
nodeDoubleClick = output();
modeChange = output();
graph = null;
currentMode = '2d';
clickTimeout = null;
resizeObserver;
destroy$ = new Subject();
constructor() {
// React to mode changes after graph initialization
effect(() => {
const m = this.mode();
if (!this.graph)
return;
this.switchMode(m);
});
// React to data changes after graph initialization
effect(() => {
// Register signal dependencies
this.nodes();
this.edges();
this.updateGraphData();
});
}
ngAfterViewInit() {
this.currentMode = this.mode();
this.initGraph();
this.setupSubscriptions();
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
if (this.clickTimeout) {
clearTimeout(this.clickTimeout);
}
if (this.resizeObserver) {
this.resizeObserver.disconnect();
}
if (this.graph) {
// Properly destroy the graph instance
if (typeof this.graph._destructor === 'function') {
this.graph._destructor();
}
this.graph = null;
}
}
initGraph() {
this.createGraph();
this.updateGraphData();
}
createGraph() {
if (this.graph) {
if (typeof this.graph._destructor === 'function') {
this.graph._destructor();
}
}
const container = this.graphContainer.nativeElement;
container.innerHTML = ''; // Clear container
const width = container.clientWidth || 800;
const height = container.clientHeight || 600;
if (this.currentMode === '2d') {
this.graph = ForceGraph
.default()(container)
.width(width)
.height(height)
.backgroundColor('rgba(0,0,0,0)')
.nodeLabel('name')
.nodeColor(this.getNodeColor.bind(this))
.nodeVal(this.getNodeSize.bind(this))
.linkColor(this.getLinkColor.bind(this))
.linkWidth(this.getLinkWidth.bind(this))
.linkLabel(this.getLinkLabel.bind(this))
.linkDirectionalArrowLength(6)
.linkDirectionalArrowRelPos(0.8)
.onNodeClick(this.onNodeClick.bind(this))
.onNodeHover(this.onNodeHover.bind(this))
.onBackgroundClick(this.onBackgroundClick.bind(this))
.nodeCanvasObject(this.drawNode.bind(this))
.nodeCanvasObjectMode(() => 'replace')
.linkCanvasObject(this.drawLink.bind(this))
.linkCanvasObjectMode(() => 'after')
// Configure d3 forces properly
.d3Force('link', d3.forceLink().distance(120).strength(0.5))
.d3Force('charge', d3.forceManyBody().strength(-300))
.d3Force('collision', d3.forceCollide().radius(50));
}
else {
this.graph = ForceGraph3D
.default()(container)
.width(width)
.height(height)
.backgroundColor('rgba(240,240,240,0.1)')
.showNavInfo(false)
// Node configuration - use nodeThreeObject for static labels
.nodeThreeObject(this.createNode3DWithLabel.bind(this))
// Remove link labels - they'll be shown on nodes instead
// .linkThreeObject(this.createLink3DWithLabel.bind(this))
// .linkThreeObjectExtend(true)
.linkColor(this.getLinkColor.bind(this))
.linkWidth(this.getLinkWidth.bind(this))
.linkDirectionalArrowLength(4)
.linkDirectionalArrowRelPos(0.8)
.linkOpacity(0.6)
// Keep tooltip labels for additional info on hover
.nodeLabel(this.getNode3DLabel.bind(this))
.linkLabel(this.getLink3DLabel.bind(this))
// Event handlers
.onNodeClick(this.onNodeClick.bind(this))
.onNodeHover(this.onNodeHover.bind(this))
.onBackgroundClick(this.onBackgroundClick.bind(this))
// Configure d3 forces for 3D
.d3Force('link', d3.forceLink().distance(120).strength(0.5))
.d3Force('charge', d3.forceManyBody().strength(-300));
}
// Handle window resize
if (this.resizeObserver) {
this.resizeObserver.disconnect();
}
this.resizeObserver = new ResizeObserver(() => {
if (this.graph && container.clientWidth && container.clientHeight) {
this.graph.width(container.clientWidth).height(container.clientHeight);
}
});
this.resizeObserver.observe(container);
}
convertToForceGraphData() {
const forceNodes = this.nodes().map((node) => {
const label = node.label || String(node.id);
const count = node.data?.count;
const displayName = count ? `${label} (${count})` : label;
return {
id: node.id,
name: displayName, // This will be used by nodeLabel
val: this.getNodeSizeValue(node),
color: node.data?.customColor || node.data?.color || '#69b3a2',
group: node.data?.originId || String(node.id).charAt(0) || '1', // Group by first character for auto-coloring
__nodeData: node,
};
});
const forceLinks = this.edges().map((edge) => {
const label = edge.label || edge.data?.label || '';
return {
source: edge.source,
target: edge.target,
color: edge.data?.color || '#999',
width: edge.data?.width || 1,
label: this.truncateLabelFor3D(label), // Pre-truncate for 3D
__linkData: edge,
};
});
console.log('Force graph data for 3D:', {
nodes: forceNodes,
links: forceLinks,
});
return { nodes: forceNodes, links: forceLinks };
}
getNode3DLabel(node) {
const nodeData = node.__nodeData;
if (!nodeData)
return node.name || String(node.id);
const label = nodeData.label || String(nodeData.id);
// Add node count if available
const count = nodeData.data?.count;
const displayLabel = count ? `${label} (${count})` : label;
// Truncate for 3D display
return displayLabel.length > 15
? displayLabel.substring(0, 12) + '...'
: displayLabel;
}
getLink3DLabel(link) {
const label = this.getLinkLabel(link);
if (!label)
return '';
// More aggressive truncation for 3D to avoid performance issues
if (label.length > 15) {
if (label.includes(':')) {
const parts = label.split(':');
if (parts.length > 1) {
return parts[0] + ':' + parts[1].substring(0, 10) + '...';
}
}
return label.substring(0, 12) + '...';
}
return label;
}
truncateLabelFor3D(label) {
if (!label)
return '';
// More aggressive truncation for 3D to ensure performance
if (label.length > 20) {
if (label.includes(':')) {
const parts = label.split(':');
if (parts.length > 1) {
const prefix = parts[0];
const suffix = parts[1];
if (suffix.length > 12) {
return `${prefix}:${suffix.substring(0, 9)}...`;
}
return `${prefix}:${suffix}`;
}
}
return label.substring(0, 17) + '...';
}
return label;
}
updateGraphData() {
if (!this.graph)
return;
const graphData = this.convertToForceGraphData();
console.log('Updating graph data:', graphData); // Debug log
this.graph.graphData(graphData);
}
getNodeColor(node) {
return (node.__nodeData?.data?.customColor ||
node.__nodeData?.data?.color ||
node.color ||
'#69b3a2');
}
getNodeSize(node) {
if (node.__nodeData) {
return node.val || this.getNodeSizeValue(node.__nodeData);
}
return node.val || 4;
}
getNodeSizeValue(node) {
if (!node)
return 4;
// Different sizes based on node type
const id = String(node.id); // Convert to string for startsWith check
if (id.startsWith('N'))
return 8; // Node
if (id.startsWith('P'))
return 6; // Property
if (id.startsWith('L'))
return 4; // Literal
return 6;
}
getLinkColor(link) {
return link.__linkData?.data?.color || link.color || '#999';
}
getLinkWidth(link) {
return link.__linkData?.data?.width || link.width || 1;
}
getLinkLabel(link) {
return link.__linkData?.label || link.label || '';
}
drawNode(node, ctx, globalScale) {
if (!node.__nodeData || !node.x || !node.y)
return;
const nodeData = node.__nodeData;
const label = nodeData.label || String(nodeData.id);
const isSelected = nodeData.data?.selected;
// Node size and color
const size = this.getNodeSize(node) / Math.max(globalScale, 0.5);
const color = this.getNodeColor(node);
// Draw node shape based on type
ctx.save();
const nodeId = String(nodeData.id);
if (nodeId.startsWith('L')) {
// Literal nodes - diamond shape
ctx.fillStyle = color;
ctx.strokeStyle = isSelected ? '#e7d211' : '#666';
ctx.lineWidth = isSelected ? 2 / globalScale : 1 / globalScale;
ctx.beginPath();
ctx.moveTo(node.x, node.y - size);
ctx.lineTo(node.x + size, node.y);
ctx.lineTo(node.x, node.y + size);
ctx.lineTo(node.x - size, node.y);
ctx.closePath();
ctx.fill();
ctx.stroke();
}
else if (nodeId.startsWith('P')) {
// Property nodes - hexagon shape
ctx.fillStyle = color;
ctx.strokeStyle = isSelected ? '#e7d211' : '#666';
ctx.lineWidth = isSelected ? 2 / globalScale : 1 / globalScale;
const sides = 6;
const a = (Math.PI * 2) / sides;
ctx.beginPath();
for (let i = 0; i < sides; i++) {
const x = node.x + Math.cos(a * i) * size;
const y = node.y + Math.sin(a * i) * size;
if (i === 0)
ctx.moveTo(x, y);
else
ctx.lineTo(x, y);
}
ctx.closePath();
ctx.fill();
ctx.stroke();
}
else {
// Regular nodes - circle
ctx.fillStyle = color;
ctx.strokeStyle = isSelected ? '#e7d211' : '#666';
ctx.lineWidth = isSelected ? 2 / globalScale : 1 / globalScale;
ctx.beginPath();
ctx.arc(node.x, node.y, size, 0, 2 * Math.PI);
ctx.fill();
ctx.stroke();
}
// Draw label
if (globalScale > 0.3) {
const fontSize = Math.max(8, 10 / globalScale);
ctx.font = `${fontSize}px Arial`;
ctx.fillStyle = '#333';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
// Truncate long labels
let displayLabel = label;
if (displayLabel.length > 15) {
displayLabel = displayLabel.substring(0, 12) + '...';
}
ctx.fillText(displayLabel, node.x, node.y + size + fontSize);
}
ctx.restore();
}
drawLink(link, ctx, globalScale) {
if (!link.source || !link.target)
return;
const source = link.source;
const target = link.target;
if (!source.x || !source.y || !target.x || !target.y)
return;
const label = this.getLinkLabel(link);
if (!label || globalScale < 0.3)
return; // Show labels at lower zoom levels
// Calculate link vector and angle
const dx = target.x - source.x;
const dy = target.y - source.y;
const linkLength = Math.sqrt(dx * dx + dy * dy);
if (linkLength === 0)
return;
const angle = Math.atan2(dy, dx);
// Position label at 40% of the way from source to target
const labelX = source.x + dx * 0.4;
const labelY = source.y + dy * 0.4;
// Calculate offset perpendicular to the link to avoid overlap
const offsetDistance = 20 / globalScale;
const offsetX = -Math.sin(angle) * offsetDistance;
const offsetY = Math.cos(angle) * offsetDistance;
const finalX = labelX + offsetX;
const finalY = labelY + offsetY;
ctx.save();
// Smaller font size
const fontSize = Math.max(6, 7 / globalScale);
ctx.font = `${fontSize}px Arial`;
// Rotate text to be parallel to the link
ctx.translate(finalX, finalY);
// Keep text readable by avoiding upside-down text
let textAngle = angle;
if (Math.abs(angle) > Math.PI / 2) {
textAngle = angle + Math.PI;
}
ctx.rotate(textAngle);
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
// More generous truncation based on link length
let displayLabel = label;
const maxChars = Math.min(Math.max(15, Math.floor(linkLength / 8)), 25); // Dynamic based on link length, min 15, max 25
if (displayLabel.length > maxChars) {
// Try to be smart about truncation - keep the meaningful part
if (displayLabel.includes(':')) {
// For URIs like "crm:P98_brought_into_existence", try to keep the part after the colon
const parts = displayLabel.split(':');
if (parts.length > 1 && parts[1].length <= maxChars - 3) {
displayLabel = parts[0] + ':' + parts[1];
}
else if (parts[1].length > maxChars - 3) {
displayLabel =
parts[0] + ':' + parts[1].substring(0, maxChars - 6) + '...';
}
}
else {
// Regular truncation
displayLabel = displayLabel.substring(0, maxChars - 3) + '...';
}
}
// Measure text for background
const textMetrics = ctx.measureText(displayLabel);
const textWidth = textMetrics.width;
const textHeight = fontSize;
// Draw background
ctx.fillStyle = 'rgba(255, 255, 255, 0.85)';
ctx.fillRect(-textWidth / 2 - 1, -textHeight / 2 - 0.5, textWidth + 2, textHeight + 1);
// Draw border
ctx.strokeStyle = 'rgba(0, 0, 0, 0.15)';
ctx.lineWidth = 0.3 / globalScale;
ctx.strokeRect(-textWidth / 2 - 1, -textHeight / 2 - 0.5, textWidth + 2, textHeight + 1);
// Draw text
ctx.fillStyle = '#333';
ctx.fillText(displayLabel, 0, 0);
ctx.restore();
}
onNodeClick(node, event) {
if (!node.__nodeData)
return;
// Handle double-click detection with timeout
if (this.clickTimeout) {
clearTimeout(this.clickTimeout);
this.clickTimeout = null;
// Double click detected
this.nodeDoubleClick.emit(node.__nodeData);
}
else {
// Set timeout for single click
this.clickTimeout = setTimeout(() => {
this.clickTimeout = null;
this.nodeSelect.emit(node.__nodeData);
}, 300);
}
}
onNodeHover(node) {
if (this.graph) {
// Change cursor on hover for both 2D and 3D
const container = this.graphContainer.nativeElement;
container.style.cursor = node ? 'pointer' : 'default';
}
}
onBackgroundClick() {
// Deselect node on background click
this.nodeSelect.emit(null);
}
setupSubscriptions() {
const update$ = this.update$();
if (update$) {
update$.pipe(takeUntil(this.destroy$)).subscribe(() => {
this.updateGraphData();
});
}
const center$ = this.center$();
if (center$) {
center$.pipe(takeUntil(this.destroy$)).subscribe(() => {
this.centerView();
});
}
const zoomToFit$ = this.zoomToFit$();
if (zoomToFit$) {
zoomToFit$.pipe(takeUntil(this.destroy$)).subscribe(() => {
this.zoomToFit();
});
}
}
toggleMode() {
const newMode = this.currentMode === '2d' ? '3d' : '2d';
this.switchMode(newMode);
this.modeChange.emit(newMode);
}
switchMode(newMode) {
if (newMode === this.currentMode)
return;
this.currentMode = newMode;
this.createGraph();
this.updateGraphData();
}
centerView() {
if (this.graph) {
if (this.currentMode === '2d') {
this.graph.centerAt(0, 0, 1000);
}
else {
this.graph.cameraPosition({ x: 0, y: 0, z: 400 }, { x: 0, y: 0, z: 0 }, 1000);
}
}
}
zoomToFit() {
if (this.graph) {
this.graph.zoomToFit(1000, 50);
}
}
createNode3DWithLabel(node) {
const nodeData = node.__nodeData;
if (!nodeData)
return null;
console.log('Creating 3D node with label for:', nodeData.label);
const size = this.getNodeSize(node);
const color = this.getNodeColor(node);
const nodeId = String(nodeData.id);
// Create a group to hold both the node and the labels
const group = new THREE.Group();
// Create node geometry based on type
let geometry;
if (nodeId.startsWith('L')) {
geometry = new THREE.OctahedronGeometry(size);
}
else if (nodeId.startsWith('P')) {
geometry = new THREE.CylinderGeometry(size * 0.8, size * 0.8, size * 0.5, 6);
}
else {
geometry = new THREE.SphereGeometry(size);
}
const material = new THREE.MeshLambertMaterial({ color });
const mesh = new THREE.Mesh(geometry, material);
group.add(mesh);
// Create node label with count and incoming link labels
const label = nodeData.label || String(nodeData.id);
const count = nodeData.data?.count;
// Get incoming link labels
const incomingLinks = this.edges().filter((edge) => edge.target === nodeData.id);
const linkLabels = incomingLinks
.map((edge) => edge.label || edge.data?.label)
.filter((label) => label) // Remove empty labels
.slice(0, 3); // Limit to first 3 to avoid too long text
// Build display text
let displayText = label;
if (count || linkLabels.length > 0) {
const parts = [];
if (count) {
parts.push(count.toString());
}
if (linkLabels.length > 0) {
// Join multiple link labels with commas
const linkText = linkLabels.join(', ');
parts.push(linkText);
}
if (parts.length > 0) {
displayText = `${label} (${parts.join(': ')})`;
}
}
// Truncate for 3D - be more generous to keep labels readable
let truncatedText = displayText;
if (truncatedText.length > 25) {
// Try to keep the meaningful part - prioritize the link labels over the node label
if (linkLabels.length > 0 && count) {
// Format: "NodeLabel (count: linkLabel1, linkLabel2)"
const linkText = linkLabels.join(', ');
const maxLinkLength = 15; // Reserve space for link labels
const truncatedLinkText = linkText.length > maxLinkLength
? linkText.substring(0, maxLinkLength - 3) + '...'
: linkText;
const nodePartLength = Math.max(5, 25 - count.toString().length - truncatedLinkText.length - 5); // 5 for " (: )"
const truncatedLabel = label.length > nodePartLength
? label.substring(0, nodePartLength - 3) + '...'
: label;
truncatedText = `${truncatedLabel} (${count}: ${truncatedLinkText})`;
}
else {
// Fallback to simple truncation
truncatedText = truncatedText.substring(0, 22) + '...';
}
}
const nodeSprite = this.createTextSprite(truncatedText);
nodeSprite.position.set(0, size + 15, 0); // Position above node
group.add(nodeSprite);
console.log('Created node sprite with text:', truncatedText);
return group;
}
createTextSprite(text) {
const fontFace = 'Arial';
const fontSize = 24;
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
// Set font for measurement
context.font = `Normal ${fontSize}px ${fontFace}`;
const metrics = context.measureText(text);
const textWidth = metrics.width;
const padding = 10;
// Set canvas size
canvas.width = textWidth + padding * 2;
canvas.height = fontSize + padding * 2;
// Clear and draw background
context.fillStyle = 'rgba(255, 255, 255, 0.9)';
context.fillRect(0, 0, canvas.width, canvas.height);
// Draw border
context.strokeStyle = 'rgba(0, 0, 0, 0.3)';
context.lineWidth = 2;
context.strokeRect(0, 0, canvas.width, canvas.height);
// Draw text
context.font = `Normal ${fontSize}px ${fontFace}`;
context.textAlign = 'center';
context.textBaseline = 'middle';
context.fillStyle = 'rgba(0, 0, 0, 1)';
context.fillText(text, canvas.width / 2, canvas.height / 2);
// Create texture and sprite
const texture = new THREE.CanvasTexture(canvas);
const material = new THREE.SpriteMaterial({ map: texture });
const sprite = new THREE.Sprite(material);
// Scale the sprite appropriately
sprite.scale.set(canvas.width / 4, canvas.height / 4, 1);
return sprite;
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: ForceGraphRendererComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.0.0", type: ForceGraphRendererComponent, isStandalone: true, selector: "cadmus-force-graph-renderer", inputs: { nodes: { classPropertyName: "nodes", publicName: "nodes", isSignal: true, isRequired: false, transformFunction: null }, edges: { classPropertyName: "edges", publicName: "edges", isSignal: true, isRequired: false, transformFunction: null }, mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, update$: { classPropertyName: "update$", publicName: "update$", isSignal: true, isRequired: false, transformFunction: null }, center$: { classPropertyName: "center$", publicName: "center$", isSignal: true, isRequired: false, transformFunction: null }, zoomToFit$: { classPropertyName: "zoomToFit$", publicName: "zoomToFit$", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { nodeSelect: "nodeSelect", nodeDoubleClick: "nodeDoubleClick", modeChange: "modeChange" }, viewQueries: [{ propertyName: "graphContainer", first: true, predicate: ["graphContainer"], descendants: true, static: true }], ngImport: i0, template: `
<div class="renderer-container">
<div class="controls">
<button
type="button"
mat-icon-button
matTooltip="Switch to {{ mode() === '2d' ? '3D' : '2D' }} view"
(click)="toggleMode()"
>
<mat-icon>{{
mode() === '2d' ? 'view_in_ar' : 'view_quilt'
}}</mat-icon>
</button>
<button
type="button"
mat-icon-button
matTooltip="Center view"
(click)="centerView()"
>
<mat-icon>filter_center_focus</mat-icon>
</button>
<button
type="button"
mat-icon-button
matTooltip="Zoom to fit"
(click)="zoomToFit()"
>
<mat-icon>fit_screen</mat-icon>
</button>
</div>
<div #graphContainer class="graph-container"></div>
</div>
`, isInline: true, styles: [".renderer-container{width:100%;height:100%;min-height:600px;position:relative;display:flex;flex-direction:column}.controls{position:absolute;top:10px;right:10px;z-index:1000;display:flex;gap:4px;background:#ffffffe6;border-radius:4px;padding:4px;box-shadow:0 2px 4px #0000001a}.graph-container{width:100%;height:100%;min-height:600px;flex:1;position:relative}\n"], dependencies: [{ kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i1.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i3.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: ForceGraphRendererComponent, decorators: [{
type: Component,
args: [{ selector: 'cadmus-force-graph-renderer', template: `
<div class="renderer-container">
<div class="controls">
<button
type="button"
mat-icon-button
matTooltip="Switch to {{ mode() === '2d' ? '3D' : '2D' }} view"
(click)="toggleMode()"
>
<mat-icon>{{
mode() === '2d' ? 'view_in_ar' : 'view_quilt'
}}</mat-icon>
</button>
<button
type="button"
mat-icon-button
matTooltip="Center view"
(click)="centerView()"
>
<mat-icon>filter_center_focus</mat-icon>
</button>
<button
type="button"
mat-icon-button
matTooltip="Zoom to fit"
(click)="zoomToFit()"
>
<mat-icon>fit_screen</mat-icon>
</button>
</div>
<div #graphContainer class="graph-container"></div>
</div>
`, imports: [MatButtonModule, MatIconModule, MatTooltipModule], changeDetection: ChangeDetectionStrategy.OnPush, styles: [".renderer-container{width:100%;height:100%;min-height:600px;position:relative;display:flex;flex-direction:column}.controls{position:absolute;top:10px;right:10px;z-index:1000;display:flex;gap:4px;background:#ffffffe6;border-radius:4px;padding:4px;box-shadow:0 2px 4px #0000001a}.graph-container{width:100%;height:100%;min-height:600px;flex:1;position:relative}\n"] }]
}], ctorParameters: () => [], propDecorators: { graphContainer: [{
type: ViewChild,
args: ['graphContainer', { static: true }]
}], nodes: [{ type: i0.Input, args: [{ isSignal: true, alias: "nodes", required: false }] }], edges: [{ type: i0.Input, args: [{ isSignal: true, alias: "edges", required: false }] }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], update$: [{ type: i0.Input, args: [{ isSignal: true, alias: "update$", required: false }] }], center$: [{ type: i0.Input, args: [{ isSignal: true, alias: "center$", required: false }] }], zoomToFit$: [{ type: i0.Input, args: [{ isSignal: true, alias: "zoomToFit$", required: false }] }], nodeSelect: [{ type: i0.Output, args: ["nodeSelect"] }], nodeDoubleClick: [{ type: i0.Output, args: ["nodeDoubleClick"] }], modeChange: [{ type: i0.Output, args: ["modeChange"] }] } });
/**
* Triples filter.
*/
class TripleFilterComponent {
lookupService;
_graphService;
/**
* True if this component is disabled.
*/
disabled = input(false, /* @ts-ignore */
...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
/**
* True if this component should show a pager.
*/
hasPager = input(false, /* @ts-ignore */
...(ngDevMode ? [{ debugName: "hasPager" }] : /* istanbul ignore next */ []));
/**
* The total number of triples returned from the last
* page fetch operation. Used when hasPager is true.
*/
total = input(0, /* @ts-ignore */
...(ngDevMode ? [{ debugName: "total" }] : /* istanbul ignore next */ []));
/**
* The filter.
*/
filter = model({
pageNumber: 1,
pageSize: 10,
}, /* @ts-ignore */
...(ngDevMode ? [{ debugName: "filter" }] : /* istanbul ignore next */ []));
pageNumber;
pageSize;
litPattern;
litType;
litLanguage;
minLitNumber;
maxLitNumber;
subj;
isNotPred;
preds;
notPreds;
hasLiteralObj;
obj;
sid;
isSidPrefix;
tag;
form;
constructor(formBuilder, lookupService, _graphService) {
this.lookupService = lookupService;
this._graphService = _graphService;
// form
this.pageNumber = formBuilder.control(1, { nonNullable: true });
this.pageSize = formBuilder.control(10, { nonNullable: true });
this.litPattern = formBuilder.control(null);
this.litType = formBuilder.control(null);
this.litLanguage = formBuilder.control(null);
this.minLitNumber = formBuilder.control(null);
this.maxLitNumber = formBuilder.control(null);
this.subj = formBuilder.control(null);
this.preds = formBuilder.control([], { nonNullable: true });
this.isNotPred = formBuilder.control(false, { nonNullable: true });
this.notPreds = formBuilder.control([], { nonNullable: true });
this.hasLiteralObj = formBuilder.control(null);
this.obj = formBuilder.control(null);
this.sid = formBuilder.control(null);
this.isSidPrefix = formBuilder.control(false, { nonNullable: true });
this.tag = formBuilder.control(null);
this.form = formBuilder.group({
pageNumber: this.pageNumber,
pageSize: this.pageSize,
litPattern: this.litPattern,
litType: this.litType,
litLanguage: this.litLanguage,
minLitNumber: this.minLitNumber,
maxLitNumber: this.maxLitNumber,
subj: this.subj,
preds: this.preds,
isNotPred: this.isNotPred,
notPreds: this.notPreds,
hasLiteralObj: this.hasLiteralObj,
obj: this.obj,
sid: this.sid,
isSidPrefix: this.isSidPrefix,
tag: this.tag,
});
effect(() => {
this.updateForm(this.filter());
});
}
updateForm(filter) {
this.pageNumber.setValue(filter.pageNumber);
this.pageSize.setValue(filter.pageSize);
this.litPattern.setValue(filter.literalPattern || null);
this.litType.setValue(filter.literalType || null);
this.litLanguage.setValue(filter.literalLanguage || null);
this.minLitNumber.setValue(filter.minLiteralNumber || null);
this.maxLitNumber.setValue(filter.maxLiteralNumber || null);
this.hasLiteralObj.setValue(filter.hasLiteralObject || null);
this.sid.setValue(filter.sid || null);
this.isSidPrefix.setValue(filter.isSidPrefix || false);
this.tag.setValue(filter.tag || null);
// load the referenced nodes so we can show them by label
forkJoin({
s: filter.subjectId
? this._graphService.getNode(filter.subjectId)
: from([null]),
p: filter.predicateIds?.length
? this._graphService.getNodeSet(filter.predicateIds)
: from([]),
o: filter.objectId
? this._graphService.getNode(filter.objectId)
: from([null]),
}).subscribe((result) => {
this.subj.setValue(result.s);
this.preds.setValue(result.p.filter((n) => n));
this.obj.setValue(result.o);
this.form.markAsPristine();
});
}
getFilter() {
return {
pageNumber: +this.pageNumber.value,
pageSize: +this.pageSize.value,
literalPattern: this.litPattern.value || undefined,
literalType: this.litType.value || undefined,
literalLanguage: this.litLanguage.value || undefined,
minLiteralNumber: this.minLitNumber.value || undefined,
maxLiteralNumber: this.maxLitNumber.value || undefined,
subjectId: this.subj.value?.id || undefined,
predicateIds: this.preds.value?.length
? this.preds.value.map((n) => n.id)
: undefined,
notPredicateIds: this.notPreds.value?.length
? this.notPreds.value.map((n) => n.id)
: undefined,
hasLiteralObject: this.hasLiteralObj.value !== null
? this.hasLiteralObj.value
: undefined,
objectId: this.obj.value?.id || undefined,
sid: this.sid.value || undefined,
isSidPrefix: this.isSidPrefix.value,
tag: this.tag.value || undefined,
};
}
onPageChange(page) {
this.pageNumber.setValue(page.pageIndex + 1);
this.filter.set(this.getFilter());
}
onSubjectNodeChange(node) {
this.subj.setValue(node);
}
onObjectNodeChange(node) {
this.obj.setValue(node);
}
onPredicateNodeChange(node) {
if (!node) {
return;
}
const un = node;
if (this.isNotPred.value) {
const nodes = [...this.notPreds.value];
if (nodes.some((n) => n.id === un.id)) {
return;
}
nodes.push(un);
this.notPreds.setValue(nodes);
this.notPreds.updateValueAndValidity();
this.notPreds.markAsDirty();
}
else {
const nodes = [...this.preds.value];
if (nodes.some((n) => n.id === un.id)) {
return;
}
nodes.push(un);
this.preds.setValue(nodes);
this.preds.updateValueAndValidity();
this.preds.markAsDirty();
}
}
deleteNotPred(node) {
const nodes = [...this.notPreds.value];
const i = nodes.indexOf(node);
nodes.splice(i, 1);
this.notPreds.setValue(nodes);
this.notPreds.updateValueAndValidity();
this.notPreds.markAsDirty();
}
deletePred(node) {
const nodes = [...this.preds.value];
const i = nodes.indexOf(node);
nodes.splice(i, 1);
this.preds.setValue(nodes);
this.preds.updateValueAndValidity();
this.preds.markAsDirty();
}
reset() {
this.form.reset();
this.filter.set(this.getFilter());
}
apply() {
if (this.form.invalid) {
return;
}
this.filter.set(this.getFilter());
this.form.markAsPristine();
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: TripleFilterComponent, deps: [{ token: i1$1.FormBuilder }, { token: i2$1.GraphNodeLookupService }, { token: i3$1.GraphService }], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.0", type: TripleFilterComponent, isStandalone: true, selector: "cadmus-walker-triple-filter", inputs: { disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, hasPager: { classPropertyName: "hasPager", publicName: "hasPager", isSignal: true, isRequired: false, transformFunction: null }, total: { classPropertyName: "total", publicName: "total", isSignal: true, isRequired: false, transformFunction: null }, filter: { classPropertyName: "filter", publicName: "filter", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { filter: "filterChange" }, ngImport: i0, template: "<form [formGroup]=\"form\" (submit)=\"apply()\" [attr.disabled]=\"disabled()\">\r\n <!-- paginator -->\r\n @if (hasPager()) {\r\n <mat-paginator\r\n [length]=\"total\"\r\n [pageSize]=\"pageSize.value || 10\"\r\n [pageSizeOptions]=\"[5, 10, 20]\"\r\n (page)=\"onPageChange($event)\"\r\n aria-label=\"Select page\"\r\n />\r\n }\r\n <mat-tab-group>\r\n <!-- TRIPLE -->\r\n <mat-tab label=\"triple\">\r\n <!-- subject ID -->\r\n <div>\r\n <cadmus-refs-lookup\r\n label=\"subject\"\r\n [service]=\"lookupService\"\r\n (itemChange)=\"onSubjectNodeChange($event)\"\r\n />\r\n </div>\r\n <!-- predicate IDs -->\r\n <div>\r\n <cadmus-refs-lookup\r\n label=\"predicate\"\r\n [service]=\"lookupService\"\r\n [baseFilter]=\"{ tag: 'property' }\"\r\n (itemChange)=\"onPredicateNodeChange($event)\"\r\n />\r\n <mat-checkbox [formControl]=\"isNotPred\">not</mat-checkbox>\r\n <!-- notPreds -->\r\n @if (isNotPred.value) {\r\n <div>\r\n <mat-list dense>\r\n @for (n of notPreds.value; track n.id) {\r\n <mat-list-item>\r\n <span>{{ n.label }}</span>\r\n <button type=\"button\" mat-icon-button (click)=\"deleteNotPred(n)\">\r\n <mat-icon class=\"mat-warn\">clear</mat-icon>\r\n </button>\r\n </mat-list-item>\r\n }\r\n </mat-list>\r\n </div>\r\n }\r\n <!-- preds -->\r\n @if (!isNotPred.value) {\r\n <div>\r\n <mat-list dense>\r\n @for (n of preds.value; track n.id) {\r\n <mat-list-item>\r\n <span>{{ n.label }}</span>\r\n <button type=\"button\" mat-icon-button (click)=\"deletePred(n)\">\r\n <mat-icon class=\"mat-warn\">clear</mat-icon>\r\n </button>\r\n </mat-list-item>\r\n }\r\n </mat-list>\r\n </div>\r\n }\r\n </div>\r\n <!-- object ID -->\r\n <div>\r\n <cadmus-refs-lookup\r\n label=\"object\"\r\n [service]=\"lookupService\"\r\n (itemChange)=\"onObjectNodeChange($event)\"\r\n />\r\n </div>\r\n <!-- sid -->\r\n <div>\r\n <mat-form-field>\r\n <input\r\n matInput\r\n [formControl]=\"sid\"\r\n placeholder=\"sid\"\r\n maxlength=\"500\"\r\n />\r\n </mat-form-field>\r\n \r\n <mat-checkbox [formControl]=\"isSidPrefix\">prefix</mat-checkbox>\r\n </div>\r\n <!-- tag -->\r\n <div>\r\n <mat-form-field>\r\n <input\r\n matInput\r\n [formControl]=\"tag\"\r\n placeholder=\"tag\"\r\n maxlength=\"50\"\r\n />\r\n </mat-form-field>\r\n </div>\r\n </mat-tab>\r\n <!-- LITERAL -->\r\n <mat-tab label=\"literal\">\r\n <!-- litPattern -->\r\n <div>\r\n <mat-form-field>\r\n <input matInput [formControl]=\"litPattern\" placeholder=\"pattern\" />\r\n </mat-form-field>\r\n </div>\r\n <!-- litType -->\r\n <div>\r\n <mat-form-field>\r\n <input matInput [formControl]=\"litType\" placeholder=\"type\" />\r\n </mat-form-field>\r\n </div>\r\n <!-- litLanguage-->\r\n <div>\r\n <mat-form-field>\r\n <input matInput [formControl]=\"litLanguage\" placeholder=\"language\" />\r\n </mat-form-field>\r\n </div>\r\n <!-- minLitNumber, maxLitNumber -->\r\n <div>\r\n <mat-form-field style=\"width: 4em\">\r\n <input\r\n matInput\r\n type=\"number\"\r\n [formControl]=\"minLitNumber\"\r\n placeholder=\"min.\"\r\n />\r\n </mat-form-field>\r\n -\r\n <mat-form-field style=\"width: 4em\">\r\n <input\r\n matInput\r\n type=\"number\"\r\n [formControl]=\"maxLitNumber\"\r\n placeholder=\"max.\"\r\n />\r\n </mat-form-field>\r\n </div>\r\n </mat-tab>\r\n </mat-tab-group>\r\n <div\r\n class=\"btn-group\"\r\n role=\"group\"\r\n aria-label=\"toolbar\"\r\n style=\"margin-bottom: 10px\"\r\n >\r\n <button\r\n mat-icon-button\r\n type=\"button\"\r\n (click)=\"reset()\"\r\n matTooltip=\"Reset filters\"\r\n [disabled]=\"disabled()\"\r\n >\r\n <mat-icon class=\"mat-warn\">clear</mat-icon>\r\n </button>\r\n <button\r\n type=\"submit\"\r\n mat-icon-button\r\n [disabled]=\"disabled()\"\r\n matTooltip=\"Apply filters\"\r\n >\r\n <mat-icon class=\"mat-primary\">check_circle</mat-icon>\r\n </button>\r\n </div>\r\n</form>\r\n", styles: [""], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NumberValueAccessor, selector: "input[type=number]:not([ngNoCva])[formControlName],input[type=number]:not([ngNoCva])[formControl],input[type=number]:not([ngNoCva])[ngModel]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$1.MaxLengthValidator, selector: "[maxlength][formControlName],[maxlength][formControl],[maxlength][ngModel]", inputs: ["maxlength"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "component", type: MatPaginator, selector: "mat-paginator", inputs: ["color", "pageIndex", "length", "pageSize", "pageSizeOptions", "hidePageSize", "showFirstLastButtons", "selectConfig", "disabled"], outputs: ["page"], exportAs: ["matPaginator"] }, { kind: "component", type: MatTabGroup, selector: "mat-tab-group", inputs: ["color", "fitInkBarToContent", "mat-stretch-tabs", "mat-align-tabs", "dynamicHeight", "selectedIndex", "headerPosition", "animationDuration", "contentTabIndex", "disablePagination", "disableRipple", "preserveContent", "backgroundColor", "aria-label", "aria-labelledby"], outputs: ["selectedIndexChange", "focusChange", "animationDone", "selectedTabChange"], exportAs: ["matTabGroup"] }, { kind: "component", type: MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass", "id"], exportAs: ["matTab"] }, { kind: "component", type: RefLookupComponent, selector: "cadmus-refs-lookup", inputs: ["label", "limit", "baseFilter", "service", "item", "itemId", "required", "hasMore", "linkTemplate", "optDialog", "options", "lookupProviderOptions"], outputs: ["itemChange", "optionsChange", "moreRequest"] }, { kind: "component", type: MatCheckbox, selector: "mat-checkbox", inputs: ["aria-label", "aria-labelledby", "aria-describedby", "aria-expanded", "aria-controls", "aria-owns", "id", "required", "labelPosition", "name", "value", "disableRipple", "tabIndex", "color", "disabledInteractive", "checked", "disabled", "indeterminate"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { kind: "component", type: MatList, selector: "mat-list", exportAs: ["matList"] }, { kind: "component", type: MatListItem, selector: "mat-list-item, a[mat-list-item], button[mat-list-item]", inputs: ["activated"], exportAs: ["matListItem"] }, { kind: "component", type: MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "directive", type: MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: TripleFilterComponent, decorators: [{
type: Component,
args: [{ selector: 'cadmus-walker-triple-filter', imports: [
FormsModule,
ReactiveFormsModule,
MatPaginator,
MatTabGroup,
MatTab,
RefLookupComponent,
MatCheckbox,
MatList,
MatListItem,
MatIconButton,
MatIcon,
MatFormField,
MatInput,
MatTooltip,
], changeDetection: ChangeDetectionStrategy.OnPush, template: "<form [formGroup]=\"form\" (submit)=\"apply()\" [attr.disabled]=\"disabled()\">\r\n <!-- paginator -->\r\n @if (hasPager()) {\r\n <mat-paginator\r\n [length]=\"total\"\r\n [pageSize]=\"pageSize.value || 10\"\r\n [pageSizeOptions]=\"[5, 10, 20]\"\r\n (page)=\"onPageChange($event)\"\r\n aria-label=\"Select page\"\r\n />\r\n }\r\n <mat-tab-group>\r\n <!-- TRIPLE -->\r\n <mat-tab label=\"triple\">\r\n <!-- subject ID -->\r\n <div>\r\n <cadmus-refs-lookup\r\n label=\"subject\"\r\n [service]=\"lookupService\"\r\n (itemChange)=\"onSubjectNodeChange($event)\"\r\n />\r\n </div>\r\n <!-- predicate IDs -->\r\n <div>\r\n <cadmus-refs-lookup\r\n label=\"predicate\"\r\n [service]=\"lookupService\"\r\n [baseFilter]=\"{ tag: 'property' }\"\r\n (itemChange)=\"onPredicateNodeChange($event)\"\r\n />\r\n <mat-checkbox [formControl]=\"isNotPred\">not</mat-checkbox>\r\n <!-- notPreds -->\r\n @if (isNotPred.value) {\r\n <div>\r\n <mat-list dense>\r\n @for (n of notPreds.value; track n.id) {\r\n <mat-list-item>\r\n <span>{{ n.label }}</span>\r\n <button type=\"button\" mat-icon-button (click)=\"deleteNotPred(n)\">\r\n <mat-icon class=\"mat-warn\">clear</mat-icon>\r\n </button>\r\n </mat-list-item>\r\n }\r\n </mat-list>\r\n </div>\r\n }\r\n <!-- preds -->\r\n @if (!isNotPred.value) {\r\n <div>\r\n <mat-list dense>\r\n @for (n of preds.value; track n.id) {\r\n <mat-list-item>\r\n <span>{{ n.label }}</span>\r\n <button type=\"button\" mat-icon-button (click)=\"deletePred(n)\">\r\n <mat-icon class=\"mat-warn\">clear</mat-icon>\r\n </button>\r\n </mat-list-item>\r\n }\r\n </mat-list>\r\n </div>\r\n }\r\n </div>\r\n <!-- object ID -->\r\n <div>\r\n <cadmus-refs-lookup\r\n label=\"object\"\r\n [service]=\"lookupService\"\r\n (itemChange)=\"onObjectNodeChange($event)\"\r\n />\r\n </div>\r\n <!-- sid -->\r\n <div>\r\n <mat-form-field>\r\n <input\r\n matInput\r\n [formControl]=\"sid\"\r\n placeholder=\"sid\"\r\n maxlength=\"500\"\r\n />\r\n </mat-form-field>\r\n \r\n <mat-checkbox [formControl]=\"isSidPrefix\">prefix</mat-checkbox>\r\n </div>\r\n <!-- tag -->\r\n <div>\r\n <mat-form-field>\r\n <input\r\n matInput\r\n [formControl]=\"tag\"\r\n placeholder=\"tag\"\r\n maxlength=\"50\"\r\n />\r\n </mat-form-field>\r\n </div>\r\n </mat-tab>\r\n <!-- LITERAL -->\r\n <mat-tab label=\"literal\">\r\n <!-- litPattern -->\r\n <div>\r\n <mat-form-field>\r\n <input matInput [formControl]=\"litPattern\" placeholder=\"pattern\" />\r\n </mat-form-field>\r\n </div>\r\n <!-- litType -->\r\n <div>\r\n <mat-form-field>\r\n <input matInput [formControl]=\"litType\" placeholder=\"type\" />\r\n </mat-form-field>\r\n </div>\r\n <!-- litLanguage-->\r\n <div>\r\n <mat-form-field>\r\n <input matInput [formControl]=\"litLanguage\" placeholder=\"language\" />\r\n </mat-form-field>\r\n </div>\r\n <!-- minLitNumber, maxLitNumber -->\r\n <div>\r\n <mat-form-field style=\"width: 4em\">\r\n <input\r\n matInput\r\n type=\"number\"\r\n [formControl]=\"minLitNumber\"\r\n placeholder=\"min.\"\r\n />\r\n </mat-form-field>\r\n -\r\n <mat-form-field style=\"width: 4em\">\r\n <input\r\n matInput\r\n type=\"number\"\r\n [formControl]=\"maxLitNumber\"\r\n placeholder=\"max.\"\r\n />\r\n </mat-form-field>\r\n </div>\r\n </mat-tab>\r\n </mat-tab-group>\r\n <div\r\n class=\"btn-group\"\r\n role=\"group\"\r\n aria-label=\"toolbar\"\r\n style=\"margin-bottom: 10px\"\r\n >\r\n <button\r\n mat-icon-button\r\n type=\"button\"\r\n (click)=\"reset()\"\r\n matTooltip=\"Reset filters\"\r\n [disabled]=\"disabled()\"\r\n >\r\n <mat-icon class=\"mat-warn\">clear</mat-icon>\r\n </button>\r\n <button\r\n type=\"submit\"\r\n mat-icon-button\r\n [disabled]=\"disabled()\"\r\n matTooltip=\"Apply filters\"\r\n >\r\n <mat-icon class=\"mat-primary\">check_circle</mat-icon>\r\n </button>\r\n </div>\r\n</form>\r\n" }]
}], ctorParameters: () => [{ type: i1$1.FormBuilder }, { type: i2$1.GraphNodeLookupService }, { type: i3$1.GraphService }], propDecorators: { disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], hasPager: [{ type: i0.Input, args: [{ isSignal: true, alias: "hasPager", required: false }] }], total: [{ type: i0.Input, args: [{ isSignal: true, alias: "total", required: false }] }], filter: [{ type: i0.Input, args: [{ isSignal: true, alias: "filter", required: false }] }, { type: i0.Output, args: ["filterChange"] }] } });
/**
* Linked non-literal node filter.
*/
class LinkedNodeFilterComponent {
lookupService;
_graphService;
/**
* True if this component is disabled.
*/
disabled = input(/* @ts-ignore */
...(ngDevMode ? [undefined, { debugName: "disabled" }] : /* istanbul ignore next */ []));
/**
* True if this component should show a pager.
*/
hasPager = input(false, /* @ts-ignore */
...(ngDevMode ? [{ debugName: "hasPager" }] : /* istanbul ignore next */ []));
/**
* The total number of nodes returned from the last
* page fetch operation. Used when hasPager is true.
*/
total = input(0, /* @ts-ignore */
...(ngDevMode ? [{ debugName: "total" }] : /* istanbul ignore next */ []));
/**
* The filter.
*/
filter = model({
pageNumber: 1,
pageSize: 10,
otherNodeId: 0,
predicateId: 0,
}, /* @ts-ignore */
...(ngDevMode ? [{ debugName: "filter" }] : /* istanbul ignore next */ []));
otherNodeId;
predicateId;
isObject;
pageNumber;
pageSize;
uid;
isClass;
tag;
label;
sourceType;
sid;
isSidPrefix;
classes;
form;
constructor(formBuilder, lookupService, _graphService) {
this.lookupService = lookupService;
this._graphService = _graphService;
this.otherNodeId = 0;
this.predicateId = 0;
this.isObject = false;
// form
this.pageNumber = formBuilder.control(1, { nonNullable: true });
this.pageSize = formBuilder.control(10, { nonNullable: true });
this.uid = formBuilder.control(null);
this.isClass = formBuilder.control(null);
this.tag = formBuilder.control(null);
this.label = formBuilder.control(null);
this.sourceType = formBuilder.control(null);
this.sid = formBuilder.control(null);
this.isSidPrefix = formBuilder.control(false, { nonNullable: true });
this.classes = formBuilder.control([], { nonNullable: true });
this.form = formBuilder.group({
pageNumber: this.pageNumber,
pageSize: this.pageSize,
uid: this.uid,
isClass: this.isClass,
tag: this.tag,
label: this.label,
sourceType: this.sourceType,
sid: this.sid,
isSidPrefix: this.isSidPrefix,
classes: this.classes,
});
effect(() => {
this.updateForm(this.filter());
});
}
updateForm(filter) {
this.otherNodeId = filter.otherNodeId;
this.predicateId = filter.predicateId;
this.isObject = filter.isObject || false;
this.pageNumber.setValue(filter.pageNumber);
this.pageSize.setValue(filter.pageSize);
this.uid.setValue(filter.uid || null);
this.isClass.setValue(filter.isClass || null);
this.tag.setValue(filter.tag || null);
this.label.setValue(filter.label || null);
this.sourceType.setValue(filter.sourceType || null);
this.sid.setValue(filter.sid || null);
this.isSidPrefix.setValue(filter.isSidPrefix || false);
// load the referenced class nodes so we can show them by label
if (filter.classIds?.length) {
this._graphService
.getNodeSet(filter.classIds)
.pipe(take(1))
.subscribe((nodes) => {
this.classes.setValue(nodes.filter((n) => n));
this.form.markAsPristine();
});
}
else {
this.classes.setValue([]);
this.form.markAsPristine();
}
}
getFilter() {
return {
pageNumber: +this.pageNumber.value,
pageSize: +this.pageSize.value,
uid: this.uid.value || undefined,
isClass: this.isClass.value || undefined,
tag: this.tag.value || undefined,
label: this.label.value || undefined,
sourceType: this.sourceType.value || undefined,
sid: this.sid.value || undefined,
isSidPrefix: this.isSidPrefix.value ? true : undefined,
classIds: this.classes.value.length
? this.classes.value.map((n) => n.id)
: undefined,
otherNodeId: this.otherNodeId,
predicateId: this.predicateId,
isObject: this.isObject,
};
}
onPageChange(page) {
this.pageNumber.setValue(page.pageIndex + 1);
this.filter.set(this.getFilter());
}
onClassAdd(node) {
if (!node) {
return;
}
const nodes = [...this.classes.value];
nodes.push(node);
this.classes.setValue(nodes);
this.classes.updateValueAndValidity();
this.classes.markAsDirty();
}
onClassRemove(node) {
const nodes = [...this.classes.value];
const i = nodes.indexOf(node);
if (i > -1) {
nodes.splice(i, 1);
this.classes.setValue(nodes);
this.classes.updateValueAndValidity();
this.classes.markAsDirty();
}
}
reset() {
this.form.reset();
this.filter.set(this.getFilter());
}
apply() {
if (this.form.invalid) {
return;
}
this.filter.set(this.getFilter());
this.form.markAsPristine();
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: LinkedNodeFilterComponent, deps: [{ token: i1$1.FormBuilder }, { token: i2$1.GraphNodeLookupService }, { token: i3$1.GraphService }], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.0", type: LinkedNodeFilterComponent, isStandalone: true, selector: "cadmus-walker-linked-node-filter", inputs: { disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, hasPager: { classPropertyName: "hasPager", publicName: "hasPager", isSignal: true, isRequired: false, transformFunction: null }, total: { classPropertyName: "total", publicName: "total", isSignal: true, isRequired: false, transformFunction: null }, filter: { classPropertyName: "filter", publicName: "filter", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { filter: "filterChange" }, ngImport: i0, template: "<form [formGroup]=\"form\" (submit)=\"apply()\" [attr.disabled]=\"disabled()\">\r\n <!-- paginator -->\r\n @if (hasPager()) {\r\n <mat-paginator\r\n [length]=\"total\"\r\n [pageSize]=\"pageSize.value || 10\"\r\n [pageSizeOptions]=\"[5, 10, 20]\"\r\n (page)=\"onPageChange($event)\"\r\n aria-label=\"Select page\"\r\n />\r\n }\r\n\r\n <!-- label -->\r\n <div>\r\n <mat-form-field>\r\n <input matInput [formControl]=\"label\" placeholder=\"label\" />\r\n </mat-form-field>\r\n </div>\r\n <!-- uid -->\r\n <div>\r\n <mat-form-field>\r\n <input matInput [formControl]=\"uid\" placeholder=\"UID\" />\r\n </mat-form-field>\r\n </div>\r\n <!-- isClass -->\r\n <div>\r\n <mat-form-field>\r\n <mat-select [formControl]=\"isClass\" placeholder=\"class\">\r\n <mat-option [value]=\"0\">(any)</mat-option>\r\n <mat-option [value]=\"1\">not-class</mat-option>\r\n <mat-option [value]=\"2\">class</mat-option>\r\n </mat-select>\r\n </mat-form-field>\r\n </div>\r\n <!-- tag -->\r\n <div>\r\n <mat-form-field>\r\n <input matInput [formControl]=\"tag\" placeholder=\"tag\" />\r\n </mat-form-field>\r\n </div>\r\n <!-- sourceType -->\r\n <div>\r\n <mat-form-field>\r\n <mat-select [formControl]=\"sourceType\" placeholder=\"source type\">\r\n <mat-option [value]=\"null\">(any)</mat-option>\r\n <mat-option [value]=\"0\">user</mat-option>\r\n <mat-option [value]=\"1\">item</mat-option>\r\n <mat-option [value]=\"2\">part</mat-option>\r\n <mat-option [value]=\"3\">thesaurus</mat-option>\r\n <mat-option [value]=\"4\">implicit</mat-option>\r\n </mat-select>\r\n </mat-form-field>\r\n </div>\r\n <div>\r\n <!-- sid -->\r\n <div>\r\n <mat-form-field>\r\n <input matInput [formControl]=\"sid\" placeholder=\"SID\" />\r\n </mat-form-field>\r\n\r\n <!-- isSidPrefix -->\r\n <mat-checkbox [formControl]=\"isSidPrefix\">prefix</mat-checkbox>\r\n </div>\r\n </div>\r\n <!-- classes -->\r\n <div>\r\n <fieldset>\r\n <legend>classes</legend>\r\n <cadmus-refs-lookup\r\n label=\"class\"\r\n [service]=\"lookupService\"\r\n [baseFilter]=\"{ isClass: true }\"\r\n (itemChange)=\"onClassAdd($event)\"\r\n />\r\n\r\n @if (classes.value.length) {\r\n <mat-chip-listbox>\r\n @for (node of classes.value; track node.id) {\r\n <mat-chip-option\r\n [removable]=\"true\"\r\n (removed)=\"onClassRemove(node)\"\r\n matTooltip=\"{{ node.uri }}\"\r\n >{{ node.label }}\r\n <button type=\"button\" matChipRemove>\r\n <mat-icon>cancel</mat-icon>\r\n </button>\r\n </mat-chip-option>\r\n }\r\n </mat-chip-listbox>\r\n }\r\n </fieldset>\r\n </div>\r\n\r\n <div\r\n class=\"btn-group\"\r\n role=\"group\"\r\n aria-label=\"toolbar\"\r\n style=\"margin-bottom: 10px\"\r\n >\r\n <button\r\n mat-icon-button\r\n type=\"button\"\r\n (click)=\"reset()\"\r\n matTooltip=\"Reset filters\"\r\n [disabled]=\"disabled()\"\r\n >\r\n <mat-icon class=\"mat-warn\">clear</mat-icon>\r\n </button>\r\n <button\r\n type=\"submit\"\r\n mat-icon-button\r\n [disabled]=\"disabled()\"\r\n matTooltip=\"Apply filters\"\r\n >\r\n <mat-icon class=\"mat-primary\">check_circle</mat-icon>\r\n </button>\r\n </div>\r\n</form>\r\n", styles: ["fieldset{border:1px solid var(--mat-sys-on-surface-variant);border-radius:4px;padding:4px}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "component", type: MatPaginator, selector: "mat-paginator", inputs: ["color", "pageIndex", "length", "pageSize", "pageSizeOptions", "hidePageSize", "showFirstLastButtons", "selectConfig", "disabled"], outputs: ["page"], exportAs: ["matPaginator"] }, { kind: "component", type: MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "component", type: MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "component", type: MatCheckbox, selector: "mat-checkbox", inputs: ["aria-label", "aria-labelledby", "aria-describedby", "aria-expanded", "aria-controls", "aria-owns", "id", "required", "labelPosition", "name", "value", "disableRipple", "tabIndex", "color", "disabledInteractive", "checked", "disabled", "indeterminate"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { kind: "component", type: RefLookupComponent, selector: "cadmus-refs-lookup", inputs: ["label", "limit", "baseFilter", "service", "item", "itemId", "required", "hasMore", "linkTemplate", "optDialog", "options", "lookupProviderOptions"], outputs: ["itemChange", "optionsChange", "moreRequest"] }, { kind: "component", type: MatChipListbox, selector: "mat-chip-listbox", inputs: ["multiple", "aria-orientation", "selectable", "compareWith", "required", "hideSingleSelectionIndicator", "value"], outputs: ["change"] }, { kind: "component", type: MatChipOption, selector: "mat-basic-chip-option, [mat-basic-chip-option], mat-chip-option, [mat-chip-option]", inputs: ["selectable", "selected"], outputs: ["selectionChange"] }, { kind: "directive", type: MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "directive", type: MatChipRemove, selector: "[matChipRemove]" }, { kind: "component", type: MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: LinkedNodeFilterComponent, decorators: [{
type: Component,
args: [{ selector: 'cadmus-walker-linked-node-filter', imports: [
FormsModule,
ReactiveFormsModule,
MatPaginator,
MatFormField,
MatInput,
MatSelect,
MatOption,
MatCheckbox,
RefLookupComponent,
MatChipListbox,
MatChipOption,
MatTooltip,
MatChipRemove,
MatIcon,
MatIconButton,
], changeDetection: ChangeDetectionStrategy.OnPush, template: "<form [formGroup]=\"form\" (submit)=\"apply()\" [attr.disabled]=\"disabled()\">\r\n <!-- paginator -->\r\n @if (hasPager()) {\r\n <mat-paginator\r\n [length]=\"total\"\r\n [pageSize]=\"pageSize.value || 10\"\r\n [pageSizeOptions]=\"[5, 10, 20]\"\r\n (page)=\"onPageChange($event)\"\r\n aria-label=\"Select page\"\r\n />\r\n }\r\n\r\n <!-- label -->\r\n <div>\r\n <mat-form-field>\r\n <input matInput [formControl]=\"label\" placeholder=\"label\" />\r\n </mat-form-field>\r\n </div>\r\n <!-- uid -->\r\n <div>\r\n <mat-form-field>\r\n <input matInput [formControl]=\"uid\" placeholder=\"UID\" />\r\n </mat-form-field>\r\n </div>\r\n <!-- isClass -->\r\n <div>\r\n <mat-form-field>\r\n <mat-select [formControl]=\"isClass\" placeholder=\"class\">\r\n <mat-option [value]=\"0\">(any)</mat-option>\r\n <mat-option [value]=\"1\">not-class</mat-option>\r\n <mat-option [value]=\"2\">class</mat-option>\r\n </mat-select>\r\n </mat-form-field>\r\n </div>\r\n <!-- tag -->\r\n <div>\r\n <mat-form-field>\r\n <input matInput [formControl]=\"tag\" placeholder=\"tag\" />\r\n </mat-form-field>\r\n </div>\r\n <!-- sourceType -->\r\n <div>\r\n <mat-form-field>\r\n <mat-select [formControl]=\"sourceType\" placeholder=\"source type\">\r\n <mat-option [value]=\"null\">(any)</mat-option>\r\n <mat-option [value]=\"0\">user</mat-option>\r\n <mat-option [value]=\"1\">item</mat-option>\r\n <mat-option [value]=\"2\">part</mat-option>\r\n <mat-option [value]=\"3\">thesaurus</mat-option>\r\n <mat-option [value]=\"4\">implicit</mat-option>\r\n </mat-select>\r\n </mat-form-field>\r\n </div>\r\n <div>\r\n <!-- sid -->\r\n <div>\r\n <mat-form-field>\r\n <input matInput [formControl]=\"sid\" placeholder=\"SID\" />\r\n </mat-form-field>\r\n\r\n <!-- isSidPrefix -->\r\n <mat-checkbox [formControl]=\"isSidPrefix\">prefix</mat-checkbox>\r\n </div>\r\n </div>\r\n <!-- classes -->\r\n <div>\r\n <fieldset>\r\n <legend>classes</legend>\r\n <cadmus-refs-lookup\r\n label=\"class\"\r\n [service]=\"lookupService\"\r\n [baseFilter]=\"{ isClass: true }\"\r\n (itemChange)=\"onClassAdd($event)\"\r\n />\r\n\r\n @if (classes.value.length) {\r\n <mat-chip-listbox>\r\n @for (node of classes.value; track node.id) {\r\n <mat-chip-option\r\n [removable]=\"true\"\r\n (removed)=\"onClassRemove(node)\"\r\n matTooltip=\"{{ node.uri }}\"\r\n >{{ node.label }}\r\n <button type=\"button\" matChipRemove>\r\n <mat-icon>cancel</mat-icon>\r\n </button>\r\n </mat-chip-option>\r\n }\r\n </mat-chip-listbox>\r\n }\r\n </fieldset>\r\n </div>\r\n\r\n <div\r\n class=\"btn-group\"\r\n role=\"group\"\r\n aria-label=\"toolbar\"\r\n style=\"margin-bottom: 10px\"\r\n >\r\n <button\r\n mat-icon-button\r\n type=\"button\"\r\n (click)=\"reset()\"\r\n matTooltip=\"Reset filters\"\r\n [disabled]=\"disabled()\"\r\n >\r\n <mat-icon class=\"mat-warn\">clear</mat-icon>\r\n </button>\r\n <button\r\n type=\"submit\"\r\n mat-icon-button\r\n [disabled]=\"disabled()\"\r\n matTooltip=\"Apply filters\"\r\n >\r\n <mat-icon class=\"mat-primary\">check_circle</mat-icon>\r\n </button>\r\n </div>\r\n</form>\r\n", styles: ["fieldset{border:1px solid var(--mat-sys-on-surface-variant);border-radius:4px;padding:4px}\n"] }]
}], ctorParameters: () => [{ type: i1$1.FormBuilder }, { type: i2$1.GraphNodeLookupService }, { type: i3$1.GraphService }], propDecorators: { disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], hasPager: [{ type: i0.Input, args: [{ isSignal: true, alias: "hasPager", required: false }] }], total: [{ type: i0.Input, args: [{ isSignal: true, alias: "total", required: false }] }], filter: [{ type: i0.Input, args: [{ isSignal: true, alias: "filter", required: false }] }, { type: i0.Output, args: ["filterChange"] }] } });
/**
* Linked literal filter.
*/
class LinkedLiteralFilterComponent {
lookupService;
_graphService;
/**
* True if this component is disabled.
*/
disabled = input(/* @ts-ignore */
...(ngDevMode ? [undefined, { debugName: "disabled" }] : /* istanbul ignore next */ []));
/**
* True if this component should show a pager.
*/
hasPager = input(/* @ts-ignore */
...(ngDevMode ? [undefined, { debugName: "hasPager" }] : /* istanbul ignore next */ []));
/**
* The total number of triples returned from the last
* page fetch operation. Used when hasPager is true.
*/
total = input(0, /* @ts-ignore */
...(ngDevMode ? [{ debugName: "total" }] : /* istanbul ignore next */ []));
/**
* The filter.
*/
filter = model({
pageNumber: 1,
pageSize: 10,
}, /* @ts-ignore */
...(ngDevMode ? [{ debugName: "filter" }] : /* istanbul ignore next */ []));
pageNumber;
pageSize;
litPattern;
litType;
litLanguage;
minLitNumber;
maxLitNumber;
subj;
pred;
form;
constructor(formBuilder, lookupService, _graphService) {
this.lookupService = lookupService;
this._graphService = _graphService;
this.pageNumber = formBuilder.control(1, { nonNullable: true });
this.pageSize = formBuilder.control(10, { nonNullable: true });
this.litPattern = formBuilder.control(null);
this.litType = formBuilder.control(null);
this.litLanguage = formBuilder.control(null);
this.minLitNumber = formBuilder.control(null);
this.maxLitNumber = formBuilder.control(null);
this.subj = formBuilder.control(null);
this.pred = formBuilder.control(null);
this.form = formBuilder.group({
pageNumber: this.pageNumber,
pageSize: this.pageSize,
litPattern: this.litPattern,
litType: this.litType,
litLanguage: this.litLanguage,
minLitNumber: this.minLitNumber,
maxLitNumber: this.maxLitNumber,
subj: this.subj,
pred: this.pred,
});
effect(() => {
this.updateForm(this.filter());
});
}
updateForm(filter) {
this.pageNumber.setValue(filter.pageNumber);
this.pageSize.setValue(filter.pageSize);
this.litPattern.setValue(filter.literalPattern || null);
this.litType.setValue(filter.literalType || null);
this.litLanguage.setValue(filter.literalLanguage || null);
this.minLitNumber.setValue(filter.minLiteralNumber || null);
this.maxLitNumber.setValue(filter.maxLiteralNumber || null);
// load the referenced triples so we can show them by label
forkJoin({
s: filter.subjectId
? this._graphService.getNode(filter.subjectId)
: from([null]),
p: filter.predicateId
? this._graphService.getNode(filter.predicateId)
: from([]),
}).subscribe((result) => {
this.subj.setValue(result.s);
this.pred.setValue(result.p);
this.form.markAsPristine();
});
}
getFilter() {
return {
pageNumber: +this.pageNumber.value,
pageSize: +this.pageSize.value,
literalPattern: this.litPattern.value || undefined,
literalType: this.litType.value || undefined,
literalLanguage: this.litLanguage.value || undefined,
minLiteralNumber: this.minLitNumber.value || undefined,
maxLiteralNumber: this.maxLitNumber.value || undefined,
subjectId: this.subj.value?.id,
predicateId: this.pred.value?.id,
};
}
onSubjectNodeChange(node) {
this.subj.setValue(node);
}
onPredicateNodeChange(node) {
this.pred.setValue(node);
}
onPageChange(page) {
this.pageNumber.setValue(page.pageIndex + 1);
this.filter.set(this.getFilter());
}
reset() {
this.form.reset();
this.filter.set(this.getFilter());
}
apply() {
if (this.form.invalid) {
return;
}
this.filter.set(this.getFilter());
this.form.markAsPristine();
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: LinkedLiteralFilterComponent, deps: [{ token: i1$1.FormBuilder }, { token: i2$1.GraphNodeLookupService }, { token: i3$1.GraphService }], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.0", type: LinkedLiteralFilterComponent, isStandalone: true, selector: "cadmus-walker-linked-literal-filter", inputs: { disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, hasPager: { classPropertyName: "hasPager", publicName: "hasPager", isSignal: true, isRequired: false, transformFunction: null }, total: { classPropertyName: "total", publicName: "total", isSignal: true, isRequired: false, transformFunction: null }, filter: { classPropertyName: "filter", publicName: "filter", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { filter: "filterChange" }, ngImport: i0, template: "<form [formGroup]=\"form\" (submit)=\"apply()\" [attr.disabled]=\"disabled()\">\r\n <!-- paginator -->\r\n @if (hasPager()) {\r\n <mat-paginator\r\n [length]=\"total()\"\r\n [pageSize]=\"pageSize.value || 10\"\r\n [pageSizeOptions]=\"[5, 10, 20]\"\r\n (page)=\"onPageChange($event)\"\r\n aria-label=\"Select page\"\r\n />\r\n }\r\n\r\n <!-- subject ID -->\r\n <div>\r\n <cadmus-refs-lookup\r\n label=\"subject\"\r\n [service]=\"lookupService\"\r\n (itemChange)=\"onSubjectNodeChange($event)\"\r\n />\r\n </div>\r\n\r\n <!-- predicate ID -->\r\n <div>\r\n <cadmus-refs-lookup\r\n label=\"predicate\"\r\n [service]=\"lookupService\"\r\n [baseFilter]=\"{ tag: 'property' }\"\r\n (itemChange)=\"onPredicateNodeChange($event)\"\r\n />\r\n </div>\r\n\r\n <!-- LITERAL -->\r\n <!-- litPattern -->\r\n <div>\r\n <mat-form-field>\r\n <input matInput [formControl]=\"litPattern\" placeholder=\"pattern\" />\r\n </mat-form-field>\r\n </div>\r\n <!-- litType -->\r\n <div>\r\n <mat-form-field>\r\n <input matInput [formControl]=\"litType\" placeholder=\"type\" />\r\n </mat-form-field>\r\n </div>\r\n <!-- litLanguage-->\r\n <div>\r\n <mat-form-field>\r\n <input matInput [formControl]=\"litLanguage\" placeholder=\"language\" />\r\n </mat-form-field>\r\n </div>\r\n <!-- minLitNumber, maxLitNumber -->\r\n <div>\r\n <mat-form-field style=\"width: 5em\">\r\n <input\r\n matInput\r\n type=\"number\"\r\n [formControl]=\"minLitNumber\"\r\n placeholder=\"min.\"\r\n />\r\n </mat-form-field>\r\n -\r\n <mat-form-field style=\"width: 5em\">\r\n <input\r\n matInput\r\n type=\"number\"\r\n [formControl]=\"maxLitNumber\"\r\n placeholder=\"max.\"\r\n />\r\n </mat-form-field>\r\n </div>\r\n <div\r\n class=\"btn-group\"\r\n role=\"group\"\r\n aria-label=\"toolbar\"\r\n style=\"margin-bottom: 10px\"\r\n >\r\n <button\r\n mat-icon-button\r\n type=\"button\"\r\n (click)=\"reset()\"\r\n matTooltip=\"Reset filters\"\r\n [disabled]=\"disabled()\"\r\n >\r\n <mat-icon class=\"mat-warn\">clear</mat-icon>\r\n </button>\r\n <button\r\n type=\"submit\"\r\n mat-icon-button\r\n [disabled]=\"disabled()\"\r\n matTooltip=\"Apply filters\"\r\n >\r\n <mat-icon class=\"mat-primary\">check_circle</mat-icon>\r\n </button>\r\n </div>\r\n</form>\r\n", styles: [""], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NumberValueAccessor, selector: "input[type=number]:not([ngNoCva])[formControlName],input[type=number]:not([ngNoCva])[formControl],input[type=number]:not([ngNoCva])[ngModel]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "component", type: MatPaginator, selector: "mat-paginator", inputs: ["color", "pageIndex", "length", "pageSize", "pageSizeOptions", "hidePageSize", "showFirstLastButtons", "selectConfig", "disabled"], outputs: ["page"], exportAs: ["matPaginator"] }, { kind: "component", type: RefLookupComponent, selector: "cadmus-refs-lookup", inputs: ["label", "limit", "baseFilter", "service", "item", "itemId", "required", "hasMore", "linkTemplate", "optDialog", "options", "lookupProviderOptions"], outputs: ["itemChange", "optionsChange", "moreRequest"] }, { kind: "component", type: MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "component", type: MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "directive", type: MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: LinkedLiteralFilterComponent, decorators: [{
type: Component,
args: [{ selector: 'cadmus-walker-linked-literal-filter', imports: [
FormsModule,
ReactiveFormsModule,
MatPaginator,
RefLookupComponent,
MatFormField,
MatInput,
MatIconButton,
MatTooltip,
MatIcon,
], changeDetection: ChangeDetectionStrategy.OnPush, template: "<form [formGroup]=\"form\" (submit)=\"apply()\" [attr.disabled]=\"disabled()\">\r\n <!-- paginator -->\r\n @if (hasPager()) {\r\n <mat-paginator\r\n [length]=\"total()\"\r\n [pageSize]=\"pageSize.value || 10\"\r\n [pageSizeOptions]=\"[5, 10, 20]\"\r\n (page)=\"onPageChange($event)\"\r\n aria-label=\"Select page\"\r\n />\r\n }\r\n\r\n <!-- subject ID -->\r\n <div>\r\n <cadmus-refs-lookup\r\n label=\"subject\"\r\n [service]=\"lookupService\"\r\n (itemChange)=\"onSubjectNodeChange($event)\"\r\n />\r\n </div>\r\n\r\n <!-- predicate ID -->\r\n <div>\r\n <cadmus-refs-lookup\r\n label=\"predicate\"\r\n [service]=\"lookupService\"\r\n [baseFilter]=\"{ tag: 'property' }\"\r\n (itemChange)=\"onPredicateNodeChange($event)\"\r\n />\r\n </div>\r\n\r\n <!-- LITERAL -->\r\n <!-- litPattern -->\r\n <div>\r\n <mat-form-field>\r\n <input matInput [formControl]=\"litPattern\" placeholder=\"pattern\" />\r\n </mat-form-field>\r\n </div>\r\n <!-- litType -->\r\n <div>\r\n <mat-form-field>\r\n <input matInput [formControl]=\"litType\" placeholder=\"type\" />\r\n </mat-form-field>\r\n </div>\r\n <!-- litLanguage-->\r\n <div>\r\n <mat-form-field>\r\n <input matInput [formControl]=\"litLanguage\" placeholder=\"language\" />\r\n </mat-form-field>\r\n </div>\r\n <!-- minLitNumber, maxLitNumber -->\r\n <div>\r\n <mat-form-field style=\"width: 5em\">\r\n <input\r\n matInput\r\n type=\"number\"\r\n [formControl]=\"minLitNumber\"\r\n placeholder=\"min.\"\r\n />\r\n </mat-form-field>\r\n -\r\n <mat-form-field style=\"width: 5em\">\r\n <input\r\n matInput\r\n type=\"number\"\r\n [formControl]=\"maxLitNumber\"\r\n placeholder=\"max.\"\r\n />\r\n </mat-form-field>\r\n </div>\r\n <div\r\n class=\"btn-group\"\r\n role=\"group\"\r\n aria-label=\"toolbar\"\r\n style=\"margin-bottom: 10px\"\r\n >\r\n <button\r\n mat-icon-button\r\n type=\"button\"\r\n (click)=\"reset()\"\r\n matTooltip=\"Reset filters\"\r\n [disabled]=\"disabled()\"\r\n >\r\n <mat-icon class=\"mat-warn\">clear</mat-icon>\r\n </button>\r\n <button\r\n type=\"submit\"\r\n mat-icon-button\r\n [disabled]=\"disabled()\"\r\n matTooltip=\"Apply filters\"\r\n >\r\n <mat-icon class=\"mat-primary\">check_circle</mat-icon>\r\n </button>\r\n </div>\r\n</form>\r\n" }]
}], ctorParameters: () => [{ type: i1$1.FormBuilder }, { type: i2$1.GraphNodeLookupService }, { type: i3$1.GraphService }], propDecorators: { disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], hasPager: [{ type: i0.Input, args: [{ isSignal: true, alias: "hasPager", required: false }] }], total: [{ type: i0.Input, args: [{ isSignal: true, alias: "total", required: false }] }], filter: [{ type: i0.Input, args: [{ isSignal: true, alias: "filter", required: false }] }, { type: i0.Output, args: ["filterChange"] }] } });
/**
* Get the extended label for the specified graph node. This returns the label
* for N nodes, and the uri + "=" + the label for P nodes. That's because P
* nodes label is just the count of the triples group, so the predicate ID is
* got from the property node's data uri.
*/
class GraphNodeLabelPipe {
transform(value, ...args) {
const node = value;
if (!node?.id || !node?.label) {
return value;
}
if (node.id.startsWith('P') && node.data.uri) {
return `${node.data.uri}=${node.label}`;
}
return node.label;
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: GraphNodeLabelPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "22.0.0", ngImport: i0, type: GraphNodeLabelPipe, isStandalone: true, name: "graphNodeLabel" });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: GraphNodeLabelPipe, decorators: [{
type: Pipe,
args: [{ name: 'graphNodeLabel' }]
}] });
//#endregion
/**
* Graph walker.
* This class encapsulates data used for interactively exploring a graph
* starting from a designated origin node.
*/
class GraphWalker {
_graphService;
_nodes$;
_edges$;
_loading$;
_error$;
_rootNode;
_selectedNode$;
_pOutFilter$;
_pInFilter$;
_pLitFilter$;
_nOutFilter$;
_nInFilter$;
_childTotals$;
/**
* The page size. Default is 10.
*/
pageSize;
/**
* Max length of literal value to show. Default is 30.
*/
maxLiteralLen;
/**
* The nodes of the walker graph, which can represent nodes, literals,
* or property groups. You can easily determine the type of each node
* by looking at the first character of its id (N=node, L=literal,
* P=property).
*/
get nodes$() {
return this._nodes$.asObservable();
}
/**
* The edges connecting nodes.
*/
get edges$() {
return this._edges$.asObservable();
}
/**
* True if the walker is loading data.
*/
get loading$() {
return this._loading$.asObservable();
}
/**
* The last error occurred in communicating with the server, if any.
*/
get error$() {
return this._error$.asObservable();
}
/**
* The selected node. Only one node at a time can be selected.
*/
get selectedNode$() {
return this._selectedNode$.asObservable();
}
/**
* The outbound linked nodes filter for the selected P node.
*/
get pOutFilter$() {
return this._pOutFilter$.asObservable();
}
/**
* The inbound linked nodes filter for the selected P node.
*/
get pInFilter$() {
return this._pInFilter$.asObservable();
}
/**
* The literal linked nodes filter for the selected P node.
*/
get pLitFilter$() {
return this._pLitFilter$.asObservable();
}
/**
* The outbound triples filter for the selected N node.
*/
get nOutFilter$() {
return this._nOutFilter$.asObservable();
}
/**
* The inbound triples filter for the selected N node.
*/
get nInFilter$() {
return this._nInFilter$.asObservable();
}
/**
* The total items fetched for each filter of the selected node.
*/
get childTotals$() {
return this._childTotals$.asObservable();
}
constructor(_graphService) {
this._graphService = _graphService;
this._nodes$ = new BehaviorSubject([]);
this._edges$ = new BehaviorSubject([]);
this._loading$ = new BehaviorSubject(false);
this._error$ = new BehaviorSubject(null);
this._selectedNode$ = new BehaviorSubject(null);
this._pOutFilter$ = new BehaviorSubject(null);
this._pInFilter$ = new BehaviorSubject(null);
this._pLitFilter$ = new BehaviorSubject(null);
this._nOutFilter$ = new BehaviorSubject(null);
this._nInFilter$ = new BehaviorSubject(null);
this._childTotals$ = new BehaviorSubject({
nOut: 0,
nIn: 0,
pOut: 0,
pIn: 0,
pLit: 0,
});
// defaults
this.pageSize = 10;
this.maxLiteralLen = 30;
}
getSelectedNode() {
return this._selectedNode$.value;
}
toggleLoading(on) {
if (on) {
this._error$.next(null);
this._loading$.next(true);
}
else {
this._loading$.next(false);
}
}
setError(error) {
if (error) {
if (typeof error === 'string') {
this._error$.next(error);
console.error(error);
}
else {
this._error$.next('Walker error');
console.error('Walker error', error);
}
}
else {
this._error$.next('Walker error');
}
}
buildNodeId(id) {
return `N${id}`;
}
/**
* Get the numeric ID of the data node being the source of the graph node
* with the specified ID.
*
* @param id The graph node ID (...N + its data node numeric ID)
* @returns The data node numeric ID.
*/
getNodeNumericId(id) {
const i = id.indexOf('N');
return i === -1 ? 0 : +id.substring(i + 1);
}
getPredicateNumericId(id) {
const i = id.indexOf('P');
const j = id.indexOf('N');
return i === -1 ? 0 : +id.substring(i + 1, j);
}
buildEdgeId(source, target) {
return `E${source}_${target}`;
}
buildPropertyId(predicateId, nodeId) {
return `P${predicateId}N${nodeId}`;
}
buildLiteralId(tripleId) {
return `L${tripleId}`;
}
addEdgeIfAbsent(edge, edges) {
const id = edge.id;
if (edges.some((e) => e.id === id)) {
return;
}
const i = id.indexOf('_');
if (i > -1) {
const invId = 'E' + id.substring(i + 1) + '_' + id.substring(1, i);
if (edges.some((e) => e.id === invId)) {
return;
}
}
edges.push(edge);
}
resetFilters() {
this._nOutFilter$.next(null);
this._nInFilter$.next(null);
this._pOutFilter$.next(null);
this._pInFilter$.next(null);
this._pLitFilter$.next(null);
this._childTotals$.next({
nOut: 0,
nIn: 0,
pOut: 0,
pIn: 0,
pLit: 0,
});
}
/**
* Select the specified node. This also implies updating all the current
* filters, which depend on the selected node.
*
* @param id The node ID or null to deselect the selected node.
*/
selectNode(id) {
const node = id ? this._nodes$.value.find((n) => n.id === id) : null;
if (!node) {
this._selectedNode$.next(null);
this.resetFilters();
return;
}
// deselect
const old = this._nodes$.value.find((n) => n.data?.selected);
if (old) {
old.data.selected = undefined;
}
// select
node.data.selected = true;
this._selectedNode$.next(node);
switch (node.id.charAt(0)) {
case 'N': // non-literal node
const nd = node.data;
this._nOutFilter$.next(nd.outFilter);
this._nInFilter$.next(nd.inFilter);
this._pOutFilter$.next(null);
this._pInFilter$.next(null);
this._pLitFilter$.next(null);
this._childTotals$.next({
nOut: nd.outTotal || 0,
nIn: nd.inTotal || 0,
pOut: 0,
pIn: 0,
pLit: 0,
});
break;
case 'P': // props group
const pd = node.data;
this._nOutFilter$.next(null);
this._nInFilter$.next(null);
this._pOutFilter$.next(pd.outFilter);
this._pInFilter$.next(pd.inFilter);
this._pLitFilter$.next(pd.litFilter);
this._childTotals$.next({
nOut: 0,
nIn: 0,
pOut: pd.outTotal || 0,
pIn: pd.inTotal || 0,
pLit: pd.litTotal || 0,
});
break;
case 'L': // literal node
this.resetFilters();
break;
}
}
/**
* Reset the graph setting its origin to the specified node.
*
* @param id The ID of the origin node to start from ("root origin").
*/
reset(id) {
this.toggleLoading(true);
this._graphService
.getNode(id)
.pipe(take(1))
.subscribe({
next: (node) => {
const nodes = [];
const data = {
uri: node.uri,
sourceType: node.sourceType,
originId: '', // root origin
customColor: '#F89427',
outFilter: {
pageNumber: 1,
pageSize: this.pageSize,
},
inFilter: {
pageNumber: 1,
pageSize: this.pageSize,
},
sid: node.sid,
};
const n = {
id: this.buildNodeId(node.id),
label: node.label || node.uri,
data: data,
};
nodes.push(n);
this._rootNode = n;
this._edges$.next([]);
this._nodes$.next(nodes);
this.expandNode(n);
},
error: (error) => {
this.setError(error);
},
complete: () => {
this.toggleLoading(false);
},
});
}
/**
* Get the source and target graph nodes IDs from the specified edge ID.
*
* @param edgeId The ID of the edge graph node.
* @returns IDs of the source and target graph nodes.
*/
getEdgeEndIds(edgeId) {
const m = new RegExp('^E([^_]+)_(.+)$').exec(edgeId);
return m ? [m[1], m[2]] : null;
}
/**
* Remove all the nodes with the specified origin node ID.
*
* @param originId The ID of the origin graph node.
* @param nodes The nodes array to remove nodes from.
* @param removedIds The IDs of all the removed nodes.
*/
removeDescendantNodes(originId, nodes, removedIds) {
const ids = new Set();
for (let i = nodes.length - 1; i > -1; i--) {
if (nodes[i].data.originId === originId) {
ids.add(nodes[i].id);
nodes.splice(i, 1);
}
}
// recurse for each removed node
// (edges never derive from other edges directly)
ids.forEach((id) => {
this.removeDescendantNodes(id, nodes, removedIds);
});
// update the received IDs set
ids.forEach((id) => {
removedIds.add(id);
});
}
/**
* Remove all the children graph nodes of the specified origin graph node.
*
* @param originId The ID of the origin graph node.
* @param nodes The nodes array to remove nodes from.
* @param edges The edges array to remove edges from.
*/
removeChildren(originId, nodes, edges) {
const selectedId = this._selectedNode$.value?.id;
// remove all the nodes whose origin ID matches,
// collecting their IDs
const removedIds = new Set();
this.removeDescendantNodes(originId, nodes, removedIds);
// remove all the affected edges
for (let i = edges.length - 1; i > -1; i--) {
const endIds = this.getEdgeEndIds(edges[i].id);
if (endIds?.length &&
(removedIds.has(endIds[0]) || removedIds.has(endIds[1]))) {
edges.splice(i, 1);
}
}
// if the removed node was the selected one, select the root origin
if (selectedId && removedIds.has(selectedId)) {
this.selectNode(this._rootNode.id);
}
}
/**
* Build a new graph node representing a property.
*
* @param sourceId The source node ID.
* @param group The triple group emitting this node.
* @returns A new graph node.
*/
buildPropertyNode(sourceId, group) {
const nid = this.getNodeNumericId(sourceId);
const data = {
originId: sourceId,
uri: group.predicateUri,
customColor: '#FF5619',
outFilter: {
pageNumber: 1,
pageSize: this.pageSize,
otherNodeId: nid,
predicateId: group.predicateId,
},
inFilter: {
pageNumber: 1,
pageSize: this.pageSize,
otherNodeId: nid,
predicateId: group.predicateId,
},
litFilter: {
pageNumber: 1,
pageSize: this.pageSize,
predicateId: group.predicateId,
},
};
return {
id: this.buildPropertyId(group.predicateId, nid),
label: group.count.toString(),
data: data,
};
}
/**
* Expand the specified node, by loading its property groups.
*
* @param node The node to expand.
* @param outFilter The properties to update for the output filter.
* @param inFilter The properties to update for the input filter.
*/
expandNode(node, outFilter, inFilter) {
// prepare filters
const nid = this.getNodeNumericId(node.id);
// outbound: node=S
const outf = outFilter
? Object.assign(node.data.outFilter, outFilter, {
subjectId: nid,
})
: {
pageNumber: 1,
pageSize: this.pageSize,
subjectId: nid,
};
// inbound: node=O
const inf = inFilter
? Object.assign(node.data.inFilter, inFilter, {
objectId: nid,
})
: {
pageNumber: 1,
pageSize: this.pageSize,
objectId: nid,
};
// load
this.toggleLoading(true);
const nodes = [...this._nodes$.value];
const edges = [...this._edges$.value];
forkJoin({
outs: this._graphService.getTripleGroups(outf.pageNumber, outf.pageSize, outf),
ins: this._graphService.getTripleGroups(inf.pageNumber, inf.pageSize, inf),
}).subscribe({
next: (result) => {
node.data.expanded = true;
// update origin's filters
node.data.outFilter = outf;
node.data.inFilter = inf;
// remove previous children
this.removeChildren(node.id, nodes, edges);
// add outbound children
node.data.outTotal = result.outs.total;
for (let i = 0; i < result.outs.items.length; i++) {
const group = result.outs.items[i];
const prop = this.buildPropertyNode(node.id, group);
// when expanding a node (e.g. N17) into props, the prop's ID is
// P + predicate ID + N + origin node ID (e.g. P30N17).
// This prop node can then be expanded, too, thus producing
// a node with ID = N + node ID. When this in turn gets expanded,
// it will produce also the prop node it comes from, which
// must not be re-inserted in the graph. This node in the new
// expansion context will get ID from the source node, which
// is different from the node at the other end of the prop node:
// this was e.g. N17, while the new node is e.g. N18. So, the
// prop previously identified as P30N17 would now be identified
// as P30N18, thus producing a duplicate. To avoid this, we
// calculate an alias ID from the origin's origin: for N18,
// its origin being P30N17, this will be N17. This produces an
// alias P30N17, which being already present will avoid duplicates.
// const aliasId = `P${group.predicateId}N${this.getNodeNumericId(
// node.data.originId
// )}`;
// if (!nodes.some((n) => n.id === prop.id || n.id === aliasId)) {
if (!nodes.some((n) => n.id === prop.id)) {
nodes.push(prop);
// edge from origin node to object property
const edge = {
id: this.buildEdgeId(node.id, prop.id),
label: group.predicateUri,
source: node.id,
target: prop.id,
data: {
originId: node.id,
},
};
this.addEdgeIfAbsent(edge, edges);
}
}
// add inbound children
node.data.inTotal = result.ins.total;
for (let i = 0; i < result.ins.items.length; i++) {
const g = result.ins.items[i];
// subject property
const p = this.buildPropertyNode(node.id, g);
// edge from object property to origin node
const edge = {
id: this.buildEdgeId(p.id, node.id),
label: g.predicateUri,
source: p.id,
target: node.id,
data: {
originId: node.id,
},
};
// do not add an edge having the same source P and target N,
// whatever the P's source node
const r = new RegExp('^EP' + g.predicateId + 'N[0-9]+_' + node.id + '$');
if (!edges.some((e) => r.test(e.id))) {
if (!nodes.some((n) => n.id === p.id)) {
nodes.push(p);
}
this.addEdgeIfAbsent(edge, edges);
}
}
// update
this._nodes$.next(nodes);
this._edges$.next(edges);
},
error: (error) => {
node.data.error = 'Error loading properties';
this._nodes$.next(nodes);
this.setError(error);
},
complete: () => {
this.toggleLoading(false);
},
});
}
/**
* Expand the selected node, by loading its property groups.
*
* @param outFilter The properties to update for the output filter.
* @param inFilter The properties to update for the input filter.
*/
expandSelectedNode(outFilter, inFilter) {
if (!this._selectedNode$.value ||
!this._selectedNode$.value.id.startsWith('N')) {
return;
}
const node = this._selectedNode$.value;
this.expandNode(node, outFilter, inFilter);
}
buildLiteralLabel(triple) {
let value = triple.objectLiteral || '';
if (this.maxLiteralLen && value.length > this.maxLiteralLen) {
value = value.substring(0, this.maxLiteralLen) + '\u2026';
}
return value;
}
buildNonLiteralNode(sourceId, node) {
const nid = this.getNodeNumericId(sourceId);
const data = {
originId: sourceId,
customColor: '#80ff95',
uri: node.uri,
sourceType: node.sourceType,
isClass: node.isClass,
sid: node.sid,
tag: node.tag,
outFilter: {
pageNumber: 0,
pageSize: this.pageSize,
subjectId: nid,
},
inFilter: {
pageNumber: 0,
pageSize: this.pageSize,
objectId: nid,
},
};
return {
id: this.buildNodeId(node.id),
label: node.label,
data: data,
};
}
buildLiteralNode(sourceId, triple) {
const data = {
originId: sourceId,
customColor: '#ebe2e0',
value: triple.objectLiteral || '',
type: triple.literalType,
language: triple.literalLanguage,
number: triple.literalNumber,
};
return {
id: this.buildLiteralId(triple.id),
label: this.buildLiteralLabel(triple),
data: data,
};
}
/**
* Expand the currently selected properties group node, by loading its
* outbound nodes, inbound nodes, and literal nodes.
*
* @param node The property group node to expand.
* @param outFilter The properties to update for the outbound nodes filter.
* @param inFilter The properties to update for the inbound nodes filter.
* @param litFilter The properties to update for the literal nodes filter.
*/
expandProperty(node, outFilter, inFilter, litFilter) {
// prepare filters
const nid = this.getNodeNumericId(node.id);
const data = node.data;
const outf = Object.assign(data.outFilter, outFilter || {}, { isObject: true });
const inf = Object.assign(data.inFilter, inFilter || {}, {
isObject: false,
});
const litf = Object.assign(data.litFilter, litFilter || {}, { subjectId: nid, predicateId: this.getPredicateNumericId(node.id) });
// load
this.toggleLoading(true);
const nodes = [...this._nodes$.value];
const edges = [...this._edges$.value];
forkJoin({
outs: this._graphService.getLinkedNodes(outf.pageNumber, outf.pageSize, outf),
ins: this._graphService.getLinkedNodes(inf.pageNumber, inf.pageSize, inf),
lits: this._graphService.getLinkedLiterals(litf.pageNumber, litf.pageSize, litf),
}).subscribe({
next: (result) => {
node.data.expanded = true;
// update origin's filters
node.data.outFilter = outf;
node.data.inFilter = inf;
node.data.litFilter = litf;
// remove previous children
this.removeChildren(node.id, nodes, edges);
// add outbound children
node.data.outTotal = result.outs.total;
for (let i = 0; i < result.outs.items.length; i++) {
const child = result.outs.items[i];
const obj = this.buildNonLiteralNode(node.id, child);
if (!nodes.some((n) => n.id === obj.id)) {
nodes.push(obj);
}
// edge from property to non literal object node
const edge = {
id: this.buildEdgeId(node.id, obj.id),
label: '',
source: node.id,
target: obj.id,
data: {
originId: node.id,
},
};
this.addEdgeIfAbsent(edge, edges);
}
// add inbound children
node.data.inTotal = result.ins.total;
for (let i = 0; i < result.ins.items.length; i++) {
const child = result.ins.items[i];
const subj = this.buildNonLiteralNode(node.id, child);
if (!nodes.some((n) => n.id === subj.id)) {
nodes.push(subj);
}
// edge from subject node to property
const edge = {
id: this.buildEdgeId(subj.id, node.id),
label: '',
source: subj.id,
target: node.id,
data: {
originId: node.id,
},
};
this.addEdgeIfAbsent(edge, edges);
}
// add literal children
node.data.litTotal = result.lits.total;
for (let i = 0; i < result.lits.items.length; i++) {
const triple = result.lits.items[i];
const lit = this.buildLiteralNode(node.id, triple);
if (!nodes.some((n) => n.id === lit.id)) {
nodes.push(lit);
}
// edge from property to literal
const edge = {
id: this.buildEdgeId(node.id, lit.id),
label: lit.data.literalType || '',
source: node.id,
target: lit.id,
data: {
originId: node.id,
},
};
this.addEdgeIfAbsent(edge, edges);
}
// update
this._nodes$.next(nodes);
this._edges$.next(edges);
},
error: (error) => {
node.data.error = 'Error loading nodes';
this._nodes$.next(nodes);
this.setError(error);
},
complete: () => {
this.toggleLoading(false);
},
});
}
/**
* Expand the currently selected properties group node, by loading its
* outbound nodes, inbound nodes, and literal nodes. If there is no
* selection, or the selected node is not a properties group node, nothing
* is done.
*
* @param outFilter The properties to update for the outbound nodes filter.
* @param inFilter The properties to update for the inbound nodes filter.
* @param litFilter The properties to update for the literal nodes filter.
*/
expandSelectedProperty(outFilter, inFilter, litFilter) {
if (!this._selectedNode$.value ||
!this._selectedNode$.value.id.startsWith('P')) {
return;
}
const node = this._selectedNode$.value;
this.expandProperty(node, outFilter, inFilter, litFilter);
}
/**
* Toggle the specified node by expanding or collapsing it.
*
* @param node The node to toggle.
*/
toggleNode(node) {
if (node.data.expanded) {
const nodes = [...this._nodes$.value];
const edges = [...this._edges$.value];
this.removeChildren(node.id, nodes, edges);
node.data.expanded = undefined;
this._nodes$.next(nodes);
this._edges$.next(edges);
}
else {
if (node.id.startsWith('N')) {
this.expandNode(node);
}
else if (node.id.startsWith('P')) {
this.expandProperty(node);
}
}
}
}
/**
* Graph walker component. This starts from a given node, and let users
* walk along edges to discover new nodes.
*/
class GraphWalkerComponent {
_dialog;
_sub;
_walker;
/**
* The root origin node ID.
*/
nodeId = input(0, /* @ts-ignore */
...(ngDevMode ? [{ debugName: "nodeId" }] : /* istanbul ignore next */ []));
/**
* The graph service instance to use.
*/
graphService = input(/* @ts-ignore */
...(ngDevMode ? [undefined, { debugName: "graphService" }] : /* istanbul ignore next */ []));
/**
* True if user can pick a node from the graph.
*/
canPick = input(/* @ts-ignore */
...(ngDevMode ? [undefined, { debugName: "canPick" }] : /* istanbul ignore next */ []));
/**
* True if user can move to the source of a picked node when
* shift-clicking it.
*/
canMoveToSource = input(/* @ts-ignore */
...(ngDevMode ? [undefined, { debugName: "canMoveToSource" }] : /* istanbul ignore next */ []));
/**
* The graph visualization mode (2D or 3D).
*/
graphMode = signal('2d', /* @ts-ignore */
...(ngDevMode ? [{ debugName: "graphMode" }] : /* istanbul ignore next */ []));
/**
* Emitted when a graph node is picked by user.
*/
nodePick = output();
/**
* Emitted when the user requests to move to the source of a picked node.
*/
moveToSource = output();
// graph
nodes$;
edges$;
loading$;
error$;
// selected node
selectedNode$;
pOutFilter$;
pInFilter$;
pLitFilter$;
nOutFilter$;
nInFilter$;
childTotals$;
// ngx-graph actions
update$ = new Subject();
center$ = new Subject();
zoomToFit$ = new Subject();
constructor(_dialog) {
this._dialog = _dialog;
// Initialize observables with empty observables initially
this.nodes$ = new Observable();
this.edges$ = new Observable();
this.loading$ = new Observable();
this.error$ = new Observable();
this.selectedNode$ = new Observable();
this.pOutFilter$ = new Observable();
this.pInFilter$ = new Observable();
this.pLitFilter$ = new Observable();
this.nOutFilter$ = new Observable();
this.nInFilter$ = new Observable();
this.childTotals$ = new Observable();
// initialize walker when graphService is available
effect(() => {
const service = this.graphService();
console.log('GraphService effect triggered:', service);
if (service && !this._walker) {
console.log('Creating new GraphWalker');
this._walker = new GraphWalker(service);
this.setupObservables();
}
});
effect(() => {
const id = this.nodeId();
console.log('NodeId effect triggered:', id);
if (id && this._walker) {
console.log('Calling reset with id:', id);
this.reset(id);
}
});
}
setupObservables() {
if (!this._walker)
return;
this.nodes$ = this._walker.nodes$;
this.edges$ = this._walker.edges$;
this.loading$ = this._walker.loading$;
this.error$ = this._walker.error$;
this.selectedNode$ = this._walker.selectedNode$;
this.pOutFilter$ = this._walker.pOutFilter$;
this.pInFilter$ = this._walker.pInFilter$;
this.pLitFilter$ = this._walker.pLitFilter$;
this.nOutFilter$ = this._walker.nOutFilter$;
this.nInFilter$ = this._walker.nInFilter$;
this.childTotals$ = this._walker.childTotals$;
// add debugging
this.nodes$.subscribe((nodes) => {
console.log('GraphWalker nodes updated:', nodes);
});
this.edges$.subscribe((edges) => {
console.log('GraphWalker edges updated:', edges);
});
}
ngOnInit() {
this._sub = this.update$.subscribe((_) => {
this.onReset();
});
}
ngOnDestroy() {
this._sub?.unsubscribe();
}
onNodeSelect(node) {
if (node) {
this._walker?.selectNode(node.id);
}
}
reset(id) {
console.log('GraphWalker reset called with id:', id);
if (this._walker) {
this._walker.reset(id);
}
else {
console.log('GraphWalker instance not available');
}
}
onReset() {
if (!this.nodeId()) {
return;
}
this._dialog
.confirm('Reset', 'Reset the whole graph?')
.pipe(take$1(1))
.subscribe((yes) => {
if (yes) {
this.reset(this.nodeId());
}
});
}
onNodeDblClick(node) {
this._walker?.toggleNode(node);
}
onGraphModeChange(mode) {
this.graphMode.set(mode);
}
onPOutFilterChange(filter) {
this._walker?.expandSelectedProperty(filter);
}
onPInFilterChange(filter) {
this._walker?.expandSelectedProperty(null, filter);
}
onPLitFilterChange(filter) {
this._walker?.expandSelectedProperty(null, null, filter);
}
onNOutFilterChange(filter) {
this._walker?.expandSelectedNode(filter);
}
onNInFilterChange(filter) {
this._walker?.expandSelectedNode(null, filter);
}
pickSelectedNode(event) {
const node = this._walker?.getSelectedNode();
if (!node) {
return;
}
if (this.canMoveToSource() && event.shiftKey) {
if (node.data.sid) {
this.moveToSource.emit(node);
}
}
else {
this.nodePick.emit(node);
}
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: GraphWalkerComponent, deps: [{ token: i1$2.DialogService }], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.0", type: GraphWalkerComponent, isStandalone: true, selector: "cadmus-graph-walker", inputs: { nodeId: { classPropertyName: "nodeId", publicName: "nodeId", isSignal: true, isRequired: false, transformFunction: null }, graphService: { classPropertyName: "graphService", publicName: "graphService", isSignal: true, isRequired: false, transformFunction: null }, canPick: { classPropertyName: "canPick", publicName: "canPick", isSignal: true, isRequired: false, transformFunction: null }, canMoveToSource: { classPropertyName: "canMoveToSource", publicName: "canMoveToSource", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { nodePick: "nodePick", moveToSource: "moveToSource" }, ngImport: i0, template: "<div id=\"container\">\r\n <!-- graph -->\r\n <div id=\"graph\">\r\n <cadmus-force-graph-renderer\r\n [nodes]=\"(nodes$ | async) || []\"\r\n [edges]=\"(edges$ | async) || []\"\r\n [mode]=\"graphMode()\"\r\n [update$]=\"update$\"\r\n [center$]=\"center$\"\r\n [zoomToFit$]=\"zoomToFit$\"\r\n (nodeSelect)=\"onNodeSelect($event)\"\r\n (nodeDoubleClick)=\"onNodeDblClick($event)\"\r\n (modeChange)=\"onGraphModeChange($event)\"\r\n />\r\n </div>\r\n\r\n <!-- tools -->\r\n <div id=\"tools\">\r\n <div id=\"bar\">\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n matTooltip=\"Center\"\r\n (click)=\"center$.next(true)\"\r\n >\r\n <mat-icon>filter_center_focus</mat-icon>\r\n </button>\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n matTooltip=\"Zoom to fit\"\r\n (click)=\"zoomToFit$.next({ force: true })\"\r\n >\r\n <mat-icon>fit_screen</mat-icon>\r\n </button>\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n matTooltip=\"Reset\"\r\n (click)=\"update$.next(true)\"\r\n >\r\n <mat-icon class=\"mat-warn\">restart_alt</mat-icon>\r\n </button>\r\n @if (nodes$ | async; as nodes) {\r\n <span class=\"muted\"\r\n >N:{{ nodes.length }} E:{{ (edges$ | async)?.length }}</span\r\n >\r\n } @if (selectedNode$ | async; as selectedNode) {\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n matTooltip=\"Toggle the selected node\"\r\n [disabled]=\"!selectedNode\"\r\n (click)=\"onNodeDblClick(selectedNode)\"\r\n >\r\n <mat-icon class=\"mat-primary\">unfold_more</mat-icon>\r\n </button>\r\n } @if (canPick()) {\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n [matTooltip]=\"\r\n canMoveToSource()\r\n ? 'Pick selected node or (shift) its source'\r\n : 'Pick the selected node'\r\n \"\r\n [disabled]=\"!(selectedNode$ | async)\"\r\n (click)=\"pickSelectedNode($event)\"\r\n >\r\n <mat-icon class=\"mat-primary\">check_circle</mat-icon>\r\n </button>\r\n }\r\n </div>\r\n <!-- progress -->\r\n <div id=\"progress\">\r\n @if (loading$ | async) {\r\n <mat-progress-bar mode=\"indeterminate\"></mat-progress-bar>\r\n }\r\n </div>\r\n <div id=\"filters\">\r\n @if (selectedNode$ | async; as node) {\r\n <div id=\"filter-head\">\r\n <span [style.color]=\"node.data.color || 'black'\">⬤</span>\r\n <span class=\"node-uri\" matTooltip=\"{{ node.data.uri }}\">{{\r\n node | graphNodeLabel\r\n }}</span>\r\n <span class=\"muted node-id\">{{\r\n node.id\r\n }}</span>\r\n </div>\r\n }\r\n <mat-tab-group>\r\n <!-- N-outs -->\r\n @if (nOutFilter$ | async; as nOutFilter) {\r\n <mat-tab>\r\n <ng-template mat-tab-label>\r\n N<mat-icon>logout</mat-icon>\r\n </ng-template>\r\n <cadmus-walker-triple-filter\r\n [filter]=\"nOutFilter\"\r\n (filterChange)=\"onNOutFilterChange($event)\"\r\n ></cadmus-walker-triple-filter>\r\n </mat-tab>\r\n }\r\n <!-- N-ins -->\r\n @if (nInFilter$ | async; as nInFilter) {\r\n <mat-tab>\r\n <ng-template mat-tab-label> N<mat-icon>login</mat-icon> </ng-template>\r\n <cadmus-walker-triple-filter\r\n [filter]=\"nInFilter\"\r\n (filterChange)=\"onNInFilterChange($event)\"\r\n ></cadmus-walker-triple-filter>\r\n </mat-tab>\r\n }\r\n <!-- P-outs -->\r\n @if (pOutFilter$ | async; as pOutFilter) {\r\n <mat-tab>\r\n <ng-template mat-tab-label>\r\n P<mat-icon>logout</mat-icon>\r\n </ng-template>\r\n <cadmus-walker-linked-node-filter\r\n [filter]=\"pOutFilter\"\r\n (filterChange)=\"onPOutFilterChange($event)\"\r\n />\r\n </mat-tab>\r\n }\r\n <!-- P-ins -->\r\n @if (pInFilter$ | async; as pInFilter) {\r\n <mat-tab>\r\n <ng-template mat-tab-label> P<mat-icon>login</mat-icon> </ng-template>\r\n <cadmus-walker-linked-node-filter\r\n [filter]=\"pInFilter\"\r\n (filterChange)=\"onPInFilterChange($event)\"\r\n />\r\n </mat-tab>\r\n }\r\n <!-- P-lit -->\r\n @if (pLitFilter$ | async; as pLitFilter) {\r\n <mat-tab>\r\n <ng-template mat-tab-label>\r\n P<mat-icon>exit_to_app</mat-icon>\r\n </ng-template>\r\n <cadmus-walker-linked-literal-filter\r\n [filter]=\"pLitFilter\"\r\n (filterChange)=\"onPLitFilterChange($event)\"\r\n />\r\n </mat-tab>\r\n }\r\n </mat-tab-group>\r\n </div>\r\n </div>\r\n</div>\r\n", styles: ["div#filter-head{border:1px solid #f8f1ae;border-radius:4px;padding:4px;margin-bottom:4px;background-color:#f8f1ae;text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.muted{color:var(--mat-sys-on-surface-variant)}.node-uri{margin-left:6px}.node-id{margin-left:8px;font-size:90%}.selected{stroke-width:2px;stroke:#e7d211}#container{width:100%;max-width:100%;height:100vh;max-height:100vh;display:grid;grid-template-rows:1fr;grid-template-columns:324px 1fr;grid-template-areas:\"tools graph\";gap:8px}#graph{grid-area:graph;min-height:500px}#tools{grid-area:tools;padding:8px;border:1px solid var(--mat-sys-on-surface-variant);height:100%;max-height:100%;min-width:324px;max-width:324px;background-color:var(--mat-sys-surface);overflow-y:auto}#tools #bar{display:flex;align-items:center;flex-wrap:wrap}@media only screen and (max-width:959px){#container{height:100vh;grid-template-rows:auto 1fr;grid-template-columns:1fr;grid-template-areas:\"tools\" \"graph\"}#graph{min-height:400px}}\n"], dependencies: [{ kind: "component", type: ForceGraphRendererComponent, selector: "cadmus-force-graph-renderer", inputs: ["nodes", "edges", "mode", "update$", "center$", "zoomToFit$"], outputs: ["nodeSelect", "nodeDoubleClick", "modeChange"] }, { kind: "component", type: MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "directive", type: MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: MatProgressBar, selector: "mat-progress-bar", inputs: ["color", "value", "bufferValue", "mode"], outputs: ["animationEnd"], exportAs: ["matProgressBar"] }, { kind: "component", type: MatTabGroup, selector: "mat-tab-group", inputs: ["color", "fitInkBarToContent", "mat-stretch-tabs", "mat-align-tabs", "dynamicHeight", "selectedIndex", "headerPosition", "animationDuration", "contentTabIndex", "disablePagination", "disableRipple", "preserveContent", "backgroundColor", "aria-label", "aria-labelledby"], outputs: ["selectedIndexChange", "focusChange", "animationDone", "selectedTabChange"], exportAs: ["matTabGroup"] }, { kind: "component", type: MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass", "id"], exportAs: ["matTab"] }, { kind: "directive", type: MatTabLabel, selector: "[mat-tab-label], [matTabLabel]" }, { kind: "component", type: TripleFilterComponent, selector: "cadmus-walker-triple-filter", inputs: ["disabled", "hasPager", "total", "filter"], outputs: ["filterChange"] }, { kind: "component", type: LinkedNodeFilterComponent, selector: "cadmus-walker-linked-node-filter", inputs: ["disabled", "hasPager", "total", "filter"], outputs: ["filterChange"] }, { kind: "component", type: LinkedLiteralFilterComponent, selector: "cadmus-walker-linked-literal-filter", inputs: ["disabled", "hasPager", "total", "filter"], outputs: ["filterChange"] }, { kind: "pipe", type: AsyncPipe, name: "async" }, { kind: "pipe", type: GraphNodeLabelPipe, name: "graphNodeLabel" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: GraphWalkerComponent, decorators: [{
type: Component,
args: [{ selector: 'cadmus-graph-walker', imports: [
ForceGraphRendererComponent,
MatIconButton,
MatTooltip,
MatIcon,
MatProgressBar,
MatTabGroup,
MatTab,
MatTabLabel,
TripleFilterComponent,
LinkedNodeFilterComponent,
LinkedLiteralFilterComponent,
AsyncPipe,
GraphNodeLabelPipe,
], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div id=\"container\">\r\n <!-- graph -->\r\n <div id=\"graph\">\r\n <cadmus-force-graph-renderer\r\n [nodes]=\"(nodes$ | async) || []\"\r\n [edges]=\"(edges$ | async) || []\"\r\n [mode]=\"graphMode()\"\r\n [update$]=\"update$\"\r\n [center$]=\"center$\"\r\n [zoomToFit$]=\"zoomToFit$\"\r\n (nodeSelect)=\"onNodeSelect($event)\"\r\n (nodeDoubleClick)=\"onNodeDblClick($event)\"\r\n (modeChange)=\"onGraphModeChange($event)\"\r\n />\r\n </div>\r\n\r\n <!-- tools -->\r\n <div id=\"tools\">\r\n <div id=\"bar\">\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n matTooltip=\"Center\"\r\n (click)=\"center$.next(true)\"\r\n >\r\n <mat-icon>filter_center_focus</mat-icon>\r\n </button>\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n matTooltip=\"Zoom to fit\"\r\n (click)=\"zoomToFit$.next({ force: true })\"\r\n >\r\n <mat-icon>fit_screen</mat-icon>\r\n </button>\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n matTooltip=\"Reset\"\r\n (click)=\"update$.next(true)\"\r\n >\r\n <mat-icon class=\"mat-warn\">restart_alt</mat-icon>\r\n </button>\r\n @if (nodes$ | async; as nodes) {\r\n <span class=\"muted\"\r\n >N:{{ nodes.length }} E:{{ (edges$ | async)?.length }}</span\r\n >\r\n } @if (selectedNode$ | async; as selectedNode) {\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n matTooltip=\"Toggle the selected node\"\r\n [disabled]=\"!selectedNode\"\r\n (click)=\"onNodeDblClick(selectedNode)\"\r\n >\r\n <mat-icon class=\"mat-primary\">unfold_more</mat-icon>\r\n </button>\r\n } @if (canPick()) {\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n [matTooltip]=\"\r\n canMoveToSource()\r\n ? 'Pick selected node or (shift) its source'\r\n : 'Pick the selected node'\r\n \"\r\n [disabled]=\"!(selectedNode$ | async)\"\r\n (click)=\"pickSelectedNode($event)\"\r\n >\r\n <mat-icon class=\"mat-primary\">check_circle</mat-icon>\r\n </button>\r\n }\r\n </div>\r\n <!-- progress -->\r\n <div id=\"progress\">\r\n @if (loading$ | async) {\r\n <mat-progress-bar mode=\"indeterminate\"></mat-progress-bar>\r\n }\r\n </div>\r\n <div id=\"filters\">\r\n @if (selectedNode$ | async; as node) {\r\n <div id=\"filter-head\">\r\n <span [style.color]=\"node.data.color || 'black'\">⬤</span>\r\n <span class=\"node-uri\" matTooltip=\"{{ node.data.uri }}\">{{\r\n node | graphNodeLabel\r\n }}</span>\r\n <span class=\"muted node-id\">{{\r\n node.id\r\n }}</span>\r\n </div>\r\n }\r\n <mat-tab-group>\r\n <!-- N-outs -->\r\n @if (nOutFilter$ | async; as nOutFilter) {\r\n <mat-tab>\r\n <ng-template mat-tab-label>\r\n N<mat-icon>logout</mat-icon>\r\n </ng-template>\r\n <cadmus-walker-triple-filter\r\n [filter]=\"nOutFilter\"\r\n (filterChange)=\"onNOutFilterChange($event)\"\r\n ></cadmus-walker-triple-filter>\r\n </mat-tab>\r\n }\r\n <!-- N-ins -->\r\n @if (nInFilter$ | async; as nInFilter) {\r\n <mat-tab>\r\n <ng-template mat-tab-label> N<mat-icon>login</mat-icon> </ng-template>\r\n <cadmus-walker-triple-filter\r\n [filter]=\"nInFilter\"\r\n (filterChange)=\"onNInFilterChange($event)\"\r\n ></cadmus-walker-triple-filter>\r\n </mat-tab>\r\n }\r\n <!-- P-outs -->\r\n @if (pOutFilter$ | async; as pOutFilter) {\r\n <mat-tab>\r\n <ng-template mat-tab-label>\r\n P<mat-icon>logout</mat-icon>\r\n </ng-template>\r\n <cadmus-walker-linked-node-filter\r\n [filter]=\"pOutFilter\"\r\n (filterChange)=\"onPOutFilterChange($event)\"\r\n />\r\n </mat-tab>\r\n }\r\n <!-- P-ins -->\r\n @if (pInFilter$ | async; as pInFilter) {\r\n <mat-tab>\r\n <ng-template mat-tab-label> P<mat-icon>login</mat-icon> </ng-template>\r\n <cadmus-walker-linked-node-filter\r\n [filter]=\"pInFilter\"\r\n (filterChange)=\"onPInFilterChange($event)\"\r\n />\r\n </mat-tab>\r\n }\r\n <!-- P-lit -->\r\n @if (pLitFilter$ | async; as pLitFilter) {\r\n <mat-tab>\r\n <ng-template mat-tab-label>\r\n P<mat-icon>exit_to_app</mat-icon>\r\n </ng-template>\r\n <cadmus-walker-linked-literal-filter\r\n [filter]=\"pLitFilter\"\r\n (filterChange)=\"onPLitFilterChange($event)\"\r\n />\r\n </mat-tab>\r\n }\r\n </mat-tab-group>\r\n </div>\r\n </div>\r\n</div>\r\n", styles: ["div#filter-head{border:1px solid #f8f1ae;border-radius:4px;padding:4px;margin-bottom:4px;background-color:#f8f1ae;text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.muted{color:var(--mat-sys-on-surface-variant)}.node-uri{margin-left:6px}.node-id{margin-left:8px;font-size:90%}.selected{stroke-width:2px;stroke:#e7d211}#container{width:100%;max-width:100%;height:100vh;max-height:100vh;display:grid;grid-template-rows:1fr;grid-template-columns:324px 1fr;grid-template-areas:\"tools graph\";gap:8px}#graph{grid-area:graph;min-height:500px}#tools{grid-area:tools;padding:8px;border:1px solid var(--mat-sys-on-surface-variant);height:100%;max-height:100%;min-width:324px;max-width:324px;background-color:var(--mat-sys-surface);overflow-y:auto}#tools #bar{display:flex;align-items:center;flex-wrap:wrap}@media only screen and (max-width:959px){#container{height:100vh;grid-template-rows:auto 1fr;grid-template-columns:1fr;grid-template-areas:\"tools\" \"graph\"}#graph{min-height:400px}}\n"] }]
}], ctorParameters: () => [{ type: i1$2.DialogService }], propDecorators: { nodeId: [{ type: i0.Input, args: [{ isSignal: true, alias: "nodeId", required: false }] }], graphService: [{ type: i0.Input, args: [{ isSignal: true, alias: "graphService", required: false }] }], canPick: [{ type: i0.Input, args: [{ isSignal: true, alias: "canPick", required: false }] }], canMoveToSource: [{ type: i0.Input, args: [{ isSignal: true, alias: "canMoveToSource", required: false }] }], nodePick: [{ type: i0.Output, args: ["nodePick"] }], moveToSource: [{ type: i0.Output, args: ["moveToSource"] }] } });
/**
* Graph interfaces to replace ngx-graph types
*/
/*
* Public API Surface of cadmus-graph-ui-ex
*/
/**
* Generated bundle index. Do not edit.
*/
export { ForceGraphRendererComponent, GraphNodeLabelPipe, GraphWalker, GraphWalkerComponent, LinkedLiteralFilterComponent, LinkedNodeFilterComponent, TripleFilterComponent };
//# sourceMappingURL=myrmidon-cadmus-graph-ui-ex.mjs.map