UNPKG

@myrmidon/cadmus-graph-ui-ex

Version:

Cadmus - extensions to semantic graph components.

2,252 lines 142 kB
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        &nbsp;\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        &nbsp;\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'\">&#x2b24;</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'\">&#x2b24;</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