Thesaurus/templates/SubjectDetailsModal.tpl
2026-02-23 16:11:35 +01:00

746 lines
29 KiB
Smarty
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!-- Modal - Schlagwort Details mit semantischem Netz (2 Ebenen) -->
<div class="modal fade" id="DetailsModal" tabindex="-1" aria-labelledby="DetailsModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl" style="max-width: 1400px;">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="DetailsModalHeadline">
<i class="fas fa-tag"></i> Detailanzeige
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Schließen"></button>
</div>
<div class="modal-body">
<div class="row">
<!-- Linke Spalte: Stammdaten -->
<div class="col-md-3">
<div class="detail-info-panel">
<div class="form-group mb-3">
<label class="form-label fw-bold">Schlagwort</label>
<input type="text" id="display_term" class="form-control" disabled />
<input type="hidden" id="display_id" />
</div>
<div class="form-group mb-3">
<label class="form-label fw-bold">Deskriptor</label>
<input type="text" id="display_descriptor" class="form-control" disabled />
</div>
<div class="row mb-3">
<div class="col-6">
<label class="form-label fw-bold">Type</label>
<input type="text" id="display_type" class="form-control" disabled />
</div>
<div class="col-6">
<label class="form-label fw-bold">ID</label>
<input type="text" id="display_id_show" class="form-control" disabled />
</div>
</div>
<div class="form-group mb-3">
<label class="form-label fw-bold">Detailtype</label>
<input type="text" id="display_detailtype" class="form-control" disabled />
</div>
<div class="form-group mb-3">
<label class="form-label fw-bold">Klassifikation</label>
<input type="text" id="display_classification" class="form-control" disabled />
</div>
<div class="form-group mb-3">
<label class="form-label fw-bold">Scopenote</label>
<textarea class="form-control" id="display_scopenote" rows="3" disabled></textarea>
</div>
<div class="form-group mb-3">
<label class="form-label fw-bold"><i class="fas fa-equals me-1" style="color: #e91e63;"></i>Synonyme</label>
<textarea class="form-control" id="display_synonyms" rows="2" disabled style="font-size: 0.9rem;"></textarea>
</div>
<!-- Lade-Status -->
<div id="network-status" class="mt-3">
<small class="text-muted">
<i class="fas fa-circle-notch fa-spin me-1"></i>
<span id="status-text">Lade Netzwerk...</span>
</small>
</div>
</div>
</div>
<!-- Rechte Spalte: Semantisches Netz -->
<div class="col-md-9">
<div class="network-panel">
<div class="d-flex justify-content-between align-items-center mb-2">
<h6 class="mb-0"><i class="fas fa-project-diagram me-2"></i>Semantisches Netz (2 Ebenen)</h6>
<div class="network-toolbar">
<button class="btn btn-sm btn-outline-secondary" onclick="networkFit()" title="Ansicht anpassen">
<i class="fas fa-expand"></i>
</button>
<button class="btn btn-sm btn-outline-secondary" onclick="networkZoomIn()" title="Vergrößern">
<i class="fas fa-search-plus"></i>
</button>
<button class="btn btn-sm btn-outline-secondary" onclick="networkZoomOut()" title="Verkleinern">
<i class="fas fa-search-minus"></i>
</button>
<button class="btn btn-sm btn-outline-secondary" onclick="toggleSecondLevel()" title="2. Ebene ein/aus" id="btn-level">
<i class="fas fa-layer-group"></i> <span id="btn-level-text">2</span>
</button>
<button class="btn btn-sm btn-outline-secondary" onclick="togglePhysics()" title="Physik/Dynamik ein/aus" id="btn-physics">
<i class="fas fa-atom"></i>
</button>
</div>
</div>
<div id="network-container"></div>
<div class="network-legend">
<span class="legend-item"><span class="legend-dot" style="background: #1a3a5c;"></span> Zentraler Begriff</span>
<span class="legend-item"><span class="legend-dot" style="background: #28a745;"></span> Oberbegriff (BT)</span>
<span class="legend-item"><span class="legend-dot" style="background: #dc3545;"></span> Unterbegriff (NT)</span>
<span class="legend-item"><span class="legend-dot" style="background: #fd7e14;"></span> Verwandt (RT)</span>
<span class="legend-item"><span class="legend-dot" style="background: #e91e63;"></span> Synonym (SYN)</span>
<span class="legend-item"><span class="legend-dot" style="background: #6c757d;"></span> USE/UF</span>
<span class="legend-item"><span class="legend-dot" style="background: #adb5bd; opacity: 0.7;"></span> 2. Ebene</span>
</div>
</div>
</div>
</div>
<!-- Versteckter Container für alte Relationen-Tabelle (falls benötigt) -->
<div class="modal_content" style="display: none;"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
<i class="fas fa-times"></i> schliessen
</button>
</div>
</div>
</div>
</div>
<style>
/* Netzwerk-Container - größer für 2 Ebenen */
#network-container {
width: 100%;
height: 550px;
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
}
.network-panel {
background: white;
border-radius: var(--radius-md);
padding: 15px;
border: 1px solid var(--border-color);
}
.detail-info-panel {
background: var(--bg-light);
border-radius: var(--radius-md);
padding: 15px;
border: 1px solid var(--border-color);
}
.network-toolbar {
display: flex;
gap: 5px;
}
.network-toolbar .btn {
padding: 4px 8px;
}
.network-legend {
display: flex;
flex-wrap: wrap;
gap: 15px;
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid var(--border-light);
font-size: 12px;
}
.legend-item {
display: flex;
align-items: center;
gap: 5px;
}
.legend-dot {
width: 12px;
height: 12px;
border-radius: 50%;
display: inline-block;
}
/* Modal extra breit für 2 Ebenen */
#DetailsModal .modal-body {
max-height: 85vh;
overflow-y: auto;
}
#network-status {
padding: 8px;
background: white;
border-radius: 4px;
border: 1px solid var(--border-light);
}
</style>
<!-- vis.js Network Bibliothek -->
<script src="/libs/vis-network/current/vis-network.min.js"></script>
<script>if (typeof vis === "undefined") { document.write('<script src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"><\/script>'); }</script>
<script>
var network = null;
var networkData = null;
var nodesDataSet = null;
var edgesDataSet = null;
var centralNodeId = null;
var firstLevelNodeIds = [];
var secondLevelNodeIds = [];
var secondLevelEdgeIds = [];
var secondLevelVisible = true;
var secondLevelLoaded = false;
var physicsEnabled = true;
// Farben für verschiedene Relationstypen
var relationColors = {
'BT': { node: '#28a745', edge: '#28a745', label: 'Oberbegriff' },
'BROADERTERM': { node: '#28a745', edge: '#28a745', label: 'Oberbegriff' },
'NT': { node: '#dc3545', edge: '#dc3545', label: 'Unterbegriff' },
'NARROWERTERM': { node: '#dc3545', edge: '#dc3545', label: 'Unterbegriff' },
'RT': { node: '#fd7e14', edge: '#fd7e14', label: 'Verwandt' },
'RELATEDTERM': { node: '#fd7e14', edge: '#fd7e14', label: 'Verwandt' },
'USE': { node: '#6c757d', edge: '#6c757d', label: 'Benutze' },
'UF': { node: '#6c757d', edge: '#6c757d', label: 'Benutzt für' },
'USEDFOR': { node: '#6c757d', edge: '#6c757d', label: 'Benutzt für' },
'SYN': { node: '#e91e63', edge: '#e91e63', label: 'Synonym' },
'SYNONYM': { node: '#e91e63', edge: '#e91e63', label: 'Synonym' }
};
function createSemanticNetwork(centralTerm, centralId, relations, synonyms) {
console.log('🕸️ Erstelle semantisches Netz (2 Ebenen) für:', centralTerm);
console.log('📝 Synonyme:', synonyms);
centralNodeId = centralId;
firstLevelNodeIds = [];
secondLevelNodeIds = [];
secondLevelEdgeIds = [];
secondLevelVisible = true;
secondLevelLoaded = false;
physicsEnabled = true;
// Buttons zurücksetzen
var btn = document.getElementById('btn-level');
var btnText = document.getElementById('btn-level-text');
if (btn) {
btn.classList.remove('btn-warning');
btn.classList.add('btn-outline-secondary');
}
if (btnText) btnText.textContent = '2';
var btnPhysics = document.getElementById('btn-physics');
if (btnPhysics) {
btnPhysics.classList.remove('btn-warning');
btnPhysics.classList.add('btn-outline-secondary');
}
updateStatus('Baue erste Ebene auf...');
// DataSets erstellen
nodesDataSet = new vis.DataSet();
edgesDataSet = new vis.DataSet();
// Zentraler Knoten
nodesDataSet.add({
id: centralId,
label: wrapLabel(centralTerm, 20),
shape: 'ellipse',
color: {
background: '#1a3a5c',
border: '#0d1f30',
highlight: { background: '#2a5a8c', border: '#1a3a5c' },
hover: { background: '#2a5a8c', border: '#1a3a5c' }
},
font: { color: '#ffffff', size: 14, face: 'Arial', bold: true },
size: 40,
borderWidth: 3,
shadow: true,
level: 0
});
// Erste Ebene hinzufügen
if (relations && relations.length > 0) {
relations.forEach(function(rel, index) {
var relType = rel.Relationtype || 'RT';
var relColor = relationColors[relType] || relationColors['RT'];
var relId = rel.IDRelation || ('rel_' + index);
var relText = rel.TextRelation || 'Unbekannt';
// ID für späteres Nachladen speichern
if (relId && relId !== centralId) {
firstLevelNodeIds.push({
id: relId,
text: relText,
type: relType
});
}
// Knoten für die Relation (1. Ebene)
nodesDataSet.add({
id: relId,
label: wrapLabel(relText, 18),
shape: 'ellipse',
color: {
background: relColor.node,
border: shadeColor(relColor.node, -20),
highlight: { background: shadeColor(relColor.node, 20), border: relColor.node },
hover: { background: shadeColor(relColor.node, 20), border: relColor.node }
},
font: { color: '#ffffff', size: 12, face: 'Arial' },
size: 28,
borderWidth: 2,
shadow: true,
title: relType + ': ' + relText + ' (ID: ' + relId + ')',
level: 1
});
// Kante zum zentralen Knoten
var arrowDirection = getArrowDirection(relType);
edgesDataSet.add({
from: centralId,
to: relId,
label: relType,
color: { color: relColor.edge, highlight: relColor.edge, hover: relColor.edge },
width: 2,
arrows: arrowDirection,
font: { size: 10, color: '#666', strokeWidth: 3, strokeColor: '#ffffff' },
smooth: { type: 'curvedCW', roundness: 0.2 }
});
});
}
// Synonyme hinzufügen (kommaseparierter String)
console.log('📝 Synonyme Typ:', typeof synonyms);
console.log('📝 Synonyme Wert:', synonyms);
console.log('📝 Synonyme Länge:', synonyms ? synonyms.length : 'null/undefined');
if (synonyms && typeof synonyms === 'string' && synonyms.trim().length > 0) {
var synColor = relationColors['SYN'];
var synArray = synonyms.split(',').map(function(s) { return s.trim(); }).filter(function(s) { return s.length > 0; });
console.log('📝 Synonym-Array:', synArray);
synArray.forEach(function(synText, index) {
var synId = 'syn_' + index + '_' + synText.replace(/\s+/g, '_');
console.log(' Füge Synonym hinzu:', synText, '→ ID:', synId);
// Knoten für das Synonym
nodesDataSet.add({
id: synId,
label: wrapLabel(synText, 18),
shape: 'ellipse',
color: {
background: synColor.node,
border: shadeColor(synColor.node, -20),
highlight: { background: shadeColor(synColor.node, 20), border: synColor.node },
hover: { background: shadeColor(synColor.node, 20), border: synColor.node }
},
font: { color: '#ffffff', size: 12, face: 'Arial' },
size: 28,
borderWidth: 2,
shadow: true,
title: 'Synonym: ' + synText,
level: 1,
isSynonym: true,
synonymText: synText
});
// Kante zum zentralen Knoten (bidirektional, keine Pfeile)
edgesDataSet.add({
from: centralId,
to: synId,
label: 'SYN',
color: { color: synColor.edge, highlight: synColor.edge, hover: synColor.edge },
width: 2,
arrows: { to: { enabled: false }, from: { enabled: false } },
font: { size: 10, color: '#666', strokeWidth: 3, strokeColor: '#ffffff' },
smooth: { type: 'curvedCW', roundness: 0.2 },
dashes: [5, 5]
});
});
console.log('✅ ' + synArray.length + ' Synonyme hinzugefügt');
} else {
console.log('⚠️ Keine Synonyme vorhanden oder leer');
}
// Netzwerk-Container
var container = document.getElementById('network-container');
// Netzwerk-Daten
networkData = {
nodes: nodesDataSet,
edges: edgesDataSet
};
// Optionen
var options = {
nodes: {
shape: 'ellipse',
scaling: { min: 15, max: 40 }
},
edges: {
smooth: { type: 'curvedCW', roundness: 0.2 }
},
physics: {
enabled: true,
barnesHut: {
gravitationalConstant: -4000,
centralGravity: 0.2,
springLength: 180,
springConstant: 0.03,
damping: 0.09
},
stabilization: {
enabled: true,
iterations: 300,
updateInterval: 25
}
},
interaction: {
hover: true,
tooltipDelay: 200,
zoomView: true,
dragView: true
},
layout: {
improvedLayout: true
}
};
// Netzwerk erstellen
network = new vis.Network(container, networkData, options);
// Klick auf Knoten
network.on('click', function(params) {
if (params.nodes.length > 0) {
var clickedNodeId = params.nodes[0];
if (clickedNodeId != centralNodeId) {
console.log('🖱️ Knoten geklickt:', clickedNodeId);
ShowModalDetails(clickedNodeId);
}
}
});
// Stabilisierung abgeschlossen - dann zweite Ebene laden
network.once('stabilizationIterationsDone', function() {
console.log('✅ Erste Ebene stabilisiert');
network.fit({ animation: { duration: 500, easingFunction: 'easeInOutQuad' } });
// Zweite Ebene laden
setTimeout(function() {
loadSecondLevel();
}, 500);
});
}
function loadSecondLevel() {
if (firstLevelNodeIds.length === 0) {
updateStatus('Fertig - keine weiteren Relationen');
hideStatus();
return;
}
updateStatus('Lade 2. Ebene (0/' + firstLevelNodeIds.length + ')...');
var loadedCount = 0;
var totalCount = firstLevelNodeIds.length;
// Für jeden Knoten der ersten Ebene die Relationen laden
firstLevelNodeIds.forEach(function(nodeInfo, index) {
setTimeout(function() {
loadRelationsForNode(nodeInfo.id, nodeInfo.text, function() {
loadedCount++;
updateStatus('Lade 2. Ebene (' + loadedCount + '/' + totalCount + ')...');
if (loadedCount >= totalCount) {
secondLevelLoaded = true;
updateStatus('Fertig - ' + nodesDataSet.length + ' Begriffe geladen');
// Netzwerk neu stabilisieren
network.stabilize(100);
setTimeout(function() {
network.fit({ animation: { duration: 500, easingFunction: 'easeInOutQuad' } });
hideStatus();
}, 1000);
}
});
}, index * 100); // Gestaffeltes Laden um Server nicht zu überlasten
});
}
function loadRelationsForNode(nodeId, nodeText, callback) {
$.get("/Thesaurus/ajax/getDetailAuthorityData.php", {
authType: 'Subject',
offset: 0,
id: nodeId,
sort: 'Anchor.Text',
limit: 1
}, function(data, status) {
if (status === "success") {
try {
var metadata = JSON.parse(data);
var row = metadata['rows'];
if (row && row[0] && row[0].Relations) {
row[0].Relations.forEach(function(rel) {
var relId = rel.IDRelation;
var relText = rel.TextRelation;
var relType = rel.Relationtype || 'RT';
// Nicht den zentralen Knoten oder bereits existierende Knoten
if (relId && relId != centralNodeId) {
var existingNode = nodesDataSet.get(relId);
if (!existingNode) {
// Neuer Knoten der 2. Ebene (kleiner, blasser)
var relColor = relationColors[relType] || relationColors['RT'];
var fadedColor = fadeColor(relColor.node, 0.6);
nodesDataSet.add({
id: relId,
label: wrapLabel(relText, 15),
shape: 'ellipse',
color: {
background: fadedColor,
border: shadeColor(fadedColor, -15),
highlight: { background: relColor.node, border: shadeColor(relColor.node, -20) },
hover: { background: relColor.node, border: shadeColor(relColor.node, -20) }
},
font: { color: '#ffffff', size: 10, face: 'Arial' },
size: 18,
borderWidth: 1,
shadow: false,
title: '2. Ebene - ' + relType + ': ' + relText + ' (ID: ' + relId + ')',
level: 2
});
// ID für Toggle speichern
secondLevelNodeIds.push(relId);
}
// Kante hinzufügen (wenn noch nicht vorhanden)
var edgeId = nodeId + '_' + relId;
var reverseEdgeId = relId + '_' + nodeId;
if (!edgesDataSet.get(edgeId) && !edgesDataSet.get(reverseEdgeId)) {
var relColor = relationColors[relType] || relationColors['RT'];
var arrowDirection = getArrowDirection(relType);
edgesDataSet.add({
id: edgeId,
from: nodeId,
to: relId,
color: { color: fadeColor(relColor.edge, 0.5), highlight: relColor.edge, hover: relColor.edge },
width: 1,
arrows: arrowDirection,
dashes: true,
smooth: { type: 'curvedCW', roundness: 0.3 }
});
// ID für Toggle speichern
secondLevelEdgeIds.push(edgeId);
}
}
});
}
} catch (e) {
console.warn('⚠️ Fehler beim Parsen für Node', nodeId, e);
}
}
if (callback) callback();
}).fail(function() {
console.warn('⚠️ AJAX-Fehler für Node', nodeId);
if (callback) callback();
});
}
function getArrowDirection(relType) {
if (relType === 'BT' || relType === 'BROADERTERM') {
return { to: { enabled: true, scaleFactor: 1 } };
} else if (relType === 'NT' || relType === 'NARROWERTERM') {
return { from: { enabled: true, scaleFactor: 1 } };
}
return { to: { enabled: false }, from: { enabled: false } };
}
function updateStatus(text) {
$('#status-text').text(text);
$('#network-status').show();
}
function hideStatus() {
setTimeout(function() {
$('#network-status').fadeOut();
}, 2000);
}
// Hilfsfunktion: Farbe verblassen
function fadeColor(color, opacity) {
var R = parseInt(color.substring(1,3), 16);
var G = parseInt(color.substring(3,5), 16);
var B = parseInt(color.substring(5,7), 16);
// Mit Weiß mischen für verblassten Effekt
R = Math.round(R + (255 - R) * (1 - opacity));
G = Math.round(G + (255 - G) * (1 - opacity));
B = Math.round(B + (255 - B) * (1 - opacity));
var RR = R.toString(16).padStart(2, '0');
var GG = G.toString(16).padStart(2, '0');
var BB = B.toString(16).padStart(2, '0');
return "#" + RR + GG + BB;
}
// Hilfsfunktion: Label umbrechen
function wrapLabel(text, maxLength) {
if (!text) return '';
if (text.length <= maxLength) return text;
var words = text.split(' ');
var lines = [];
var currentLine = '';
words.forEach(function(word) {
if ((currentLine + ' ' + word).trim().length <= maxLength) {
currentLine = (currentLine + ' ' + word).trim();
} else {
if (currentLine) lines.push(currentLine);
currentLine = word;
}
});
if (currentLine) lines.push(currentLine);
return lines.join('\n');
}
// Hilfsfunktion: Farbe aufhellen/abdunkeln
function shadeColor(color, percent) {
var R = parseInt(color.substring(1,3), 16);
var G = parseInt(color.substring(3,5), 16);
var B = parseInt(color.substring(5,7), 16);
R = parseInt(R * (100 + percent) / 100);
G = parseInt(G * (100 + percent) / 100);
B = parseInt(B * (100 + percent) / 100);
R = (R < 255) ? R : 255;
G = (G < 255) ? G : 255;
B = (B < 255) ? B : 255;
R = (R > 0) ? R : 0;
G = (G > 0) ? G : 0;
B = (B > 0) ? B : 0;
var RR = ((R.toString(16).length == 1) ? "0" + R.toString(16) : R.toString(16));
var GG = ((G.toString(16).length == 1) ? "0" + G.toString(16) : G.toString(16));
var BB = ((B.toString(16).length == 1) ? "0" + B.toString(16) : B.toString(16));
return "#" + RR + GG + BB;
}
// Netzwerk-Steuerung
function networkFit() {
if (network) {
network.fit({
animation: { duration: 500, easingFunction: 'easeInOutQuad' }
});
}
}
function networkZoomIn() {
if (network) {
var scale = network.getScale() * 1.3;
network.moveTo({ scale: scale, animation: { duration: 300 } });
}
}
function networkZoomOut() {
if (network) {
var scale = network.getScale() / 1.3;
network.moveTo({ scale: scale, animation: { duration: 300 } });
}
}
function toggleSecondLevel() {
if (!network || !secondLevelLoaded) {
console.log('⚠️ 2. Ebene noch nicht geladen');
return;
}
secondLevelVisible = !secondLevelVisible;
var btn = document.getElementById('btn-level');
var btnText = document.getElementById('btn-level-text');
if (secondLevelVisible) {
// 2. Ebene einblenden
secondLevelNodeIds.forEach(function(nodeId) {
nodesDataSet.update({ id: nodeId, hidden: false });
});
secondLevelEdgeIds.forEach(function(edgeId) {
edgesDataSet.update({ id: edgeId, hidden: false });
});
btn.classList.remove('btn-warning');
btn.classList.add('btn-outline-secondary');
btnText.textContent = '2';
console.log('✅ 2. Ebene eingeblendet');
} else {
// 2. Ebene ausblenden
secondLevelNodeIds.forEach(function(nodeId) {
nodesDataSet.update({ id: nodeId, hidden: true });
});
secondLevelEdgeIds.forEach(function(edgeId) {
edgesDataSet.update({ id: edgeId, hidden: true });
});
btn.classList.remove('btn-outline-secondary');
btn.classList.add('btn-warning');
btnText.textContent = '1';
console.log('✅ 2. Ebene ausgeblendet');
}
// Ansicht anpassen
setTimeout(function() {
network.fit({ animation: { duration: 300, easingFunction: 'easeInOutQuad' } });
}, 100);
}
function togglePhysics() {
if (!network) return;
physicsEnabled = !physicsEnabled;
network.setOptions({ physics: { enabled: physicsEnabled } });
var btn = document.getElementById('btn-physics');
if (physicsEnabled) {
btn.classList.remove('btn-warning');
btn.classList.add('btn-outline-secondary');
console.log('✅ Physik aktiviert');
} else {
btn.classList.remove('btn-outline-secondary');
btn.classList.add('btn-warning');
console.log('✅ Physik deaktiviert - Knoten können manuell verschoben werden');
}
}
</script>