diff --git a/data/nde/voorbeeld_lijst_organisaties_en_diensten-totaallijst_nederland.yaml b/data/nde/voorbeeld_lijst_organisaties_en_diensten-totaallijst_nederland.yaml index df8d97cf33..caa741dcd7 100644 --- a/data/nde/voorbeeld_lijst_organisaties_en_diensten-totaallijst_nederland.yaml +++ b/data/nde/voorbeeld_lijst_organisaties_en_diensten-totaallijst_nederland.yaml @@ -1665,6 +1665,10 @@ opmerkingen_inez: Geen eigen KvK inschrijving. samenwerkingsverband_platform: Collectie Gelderland museum_register: ja + wikidata_id: Q2693553 + type: + - M + - F - plaatsnaam_bezoekadres: Arnhem straat_en_huisnummer_bezoekadres: Zijpendaalseweg 44 organisatie: Kasteel Ammersoyen @@ -1674,6 +1678,10 @@ opmerkingen_inez: Geen eigen KvK inschrijving. samenwerkingsverband_platform: Collectie Gelderland museum_register: ja + wikidata_id: Q2447969 + type: + - M + - F - plaatsnaam_bezoekadres: Arnhem straat_en_huisnummer_bezoekadres: Zijpendaalseweg 44 organisatie: Kasteel Cannenburch @@ -1682,6 +1690,10 @@ type_organisatie: opengesteld monument opmerkingen_inez: Geen eigen KvK inschrijving. museum_register: ja + wikidata_id: Q2175336 + type: + - M + - F - plaatsnaam_bezoekadres: Doornenburg straat_en_huisnummer_bezoekadres: Kerkstraat 27 organisatie: Stichting tot Behoud van den Doornenburg @@ -1693,6 +1705,10 @@ museum_register: ja in_scope_voor_dc4eu: ja dc4eu_aansluit_route: via CN set=Collectie Gelderland + wikidata_id: Q2012221 + type: + - M + - F - plaatsnaam_bezoekadres: Arnhem straat_en_huisnummer_bezoekadres: Zijpendaalseweg 44 organisatie: Kasteel Doorwerth @@ -1703,6 +1719,10 @@ een belangenvereniging maar zijn niet de beheerders. samenwerkingsverband_platform: Collectie Gelderland museum_register: ja + wikidata_id: Q680425 + type: + - M + - F - plaatsnaam_bezoekadres: Arnhem straat_en_huisnummer_bezoekadres: Zijpendaalseweg 44 organisatie: Kasteel Hernen @@ -1712,6 +1732,10 @@ opmerkingen_inez: Geen eigen KvK inschrijving. samenwerkingsverband_platform: Collectie Gelderland museum_register: ja + wikidata_id: Q4322946 + type: + - M + - F - plaatsnaam_bezoekadres: Arnhem straat_en_huisnummer_bezoekadres: Zijpendaalseweg 44 organisatie: Kasteel Rosendael @@ -1721,6 +1745,10 @@ opmerkingen_inez: Geen eigen KvK inschrijving. samenwerkingsverband_platform: Collectie Gelderland museum_register: ja + wikidata_id: Q2047615 + type: + - M + - F - plaatsnaam_bezoekadres: Nijmegen straat_en_huisnummer_bezoekadres: Erasmuslaan 36 organisatie: Katholiek Documentatiecentrum in Nijmegen @@ -1729,6 +1757,9 @@ type_organisatie: documentatiecentrum opmerkingen_inez: Geen eigen KvK inschrijving. isil-code_na: NL-NmKDC + wikidata_id: Q13742228 + type: + - A - plaatsnaam_bezoekadres: Twello straat_en_huisnummer_bezoekadres: Dorpsstraat 11a organisatie: Historische Vereniging Voorst @@ -1738,6 +1769,9 @@ collectie_nederland: ja in_scope_voor_dc4eu: ja dc4eu_aansluit_route: via CN set=Collectie Gelderland + wikidata_id: Q98895215 + type: + - S - plaatsnaam_bezoekadres: Arnhem straat_en_huisnummer_bezoekadres: Velperweg 147 organisatie: Koninklijk Tehuis voor Oud-Militairen en Museum Bronbeek @@ -1749,12 +1783,17 @@ rijkscollectie: ja linked_data: ja datasetregister: Stamboeken (colonialcollections) + wikidata_id: Q61930724 + type: + - M - plaatsnaam_bezoekadres: Apeldoorn straat_en_huisnummer_bezoekadres: St Eustatius 18 organisatie: Stichting Korpora webadres_organisatie: https://www.korpora.nl/ type_organisatie: kenniscentrum museum_register: ja + type: + - S - plaatsnaam_bezoekadres: Otterlo straat_en_huisnummer_bezoekadres: Houtkampweg 6 organisatie: Stichting Kröller-Müller Museum @@ -1766,12 +1805,18 @@ museum_register: ja rijkscollectie: ja van_gogh_worldwide: ja + wikidata_id: Q1051928 + type: + - M - plaatsnaam_bezoekadres: Arnhem straat_en_huisnummer_bezoekadres: Markt 11 organisatie: provincie Gelderland webadres_organisatie: https://www.gelderland.nl/themas/organisatie/over-de-provincie/huis-der-provincie/provinciale-kunstcollectie type_organisatie: provincie samenwerkingsverband_platform: Collectie Gelderland + wikidata_id: Q775 + type: + - O - plaatsnaam_bezoekadres: Buren organisatie: Marechausseemuseum type_organisatie: museum @@ -1779,6 +1824,9 @@ Nationaal Militair Museum in Soesterberg. museum_register: ja rijkscollectie: ja + wikidata_id: Q18558954 + type: + - M - plaatsnaam_bezoekadres: Arnhem straat_en_huisnummer_bezoekadres: Utrechtseweg 87 organisatie: Stichting Museum Arnhem @@ -1790,6 +1838,9 @@ museum_register: ja modemuze: ja delfts_aardewerk: ja + wikidata_id: Q2114028 + type: + - M - plaatsnaam_bezoekadres: Bennekom straat_en_huisnummer_bezoekadres: Kerkstraat 1 organisatie: Kijk- en Luistermuseum @@ -1800,12 +1851,18 @@ museum_register: ja in_scope_voor_dc4eu: ja dc4eu_aansluit_route: via CN set=Collectie Gelderland + wikidata_id: Q13743314 + type: + - M - plaatsnaam_bezoekadres: Wageningen straat_en_huisnummer_bezoekadres: Bowlespark 1A organisatie: Stichting Historisch Museum De Casteelse Poort webadres_organisatie: https://www.casteelsepoort.nl/ type_organisatie: museum museum_register: ja + wikidata_id: Q2546552 + type: + - M - plaatsnaam_bezoekadres: Elburg straat_en_huisnummer_bezoekadres: Jufferenstraat 6-8 organisatie: Stichting Museum Elburg @@ -1814,6 +1871,9 @@ samenwerkingsverband_platform: Collectie Gelderland collectie_nederland: ja museum_register: ja + wikidata_id: Q56459645 + type: + - M - plaatsnaam_bezoekadres: Zutphen straat_en_huisnummer_bezoekadres: ’s Gravenhof 4 organisatie: Stichting Musea Zutphen (Stedelijk Museum Zutphen en Museum Henriette @@ -1822,7 +1882,9 @@ type_organisatie: museum samenwerkingsverband_platform: Collectie Gelderland museum_register: ja - + wikidata_id: Q56423939 + type: + - M - plaatsnaam_bezoekadres: Wijchen straat_en_huisnummer_bezoekadres: Kasteellaan 9 organisatie: Stichting Museum Kasteel Wijchen diff --git a/frontend/src/components/uml/UMLVisualization.tsx b/frontend/src/components/uml/UMLVisualization.tsx index 4be4fb936e..7161e57d7d 100644 --- a/frontend/src/components/uml/UMLVisualization.tsx +++ b/frontend/src/components/uml/UMLVisualization.tsx @@ -265,6 +265,72 @@ export const UMLVisualization: React.FC = ({ const methodHeight = 24; const nodePadding = 10; + // Helper function to calculate intersection point of a line with a rectangular box border + // Given a line from (x1,y1) to (x2,y2) and a rectangle centered at (cx,cy) with width w and height h, + // returns the point where the line intersects the rectangle border (from center outward) + const getBoxBorderIntersection = ( + cx: number, cy: number, // Rectangle center + w: number, h: number, // Rectangle width and height + targetX: number, targetY: number // Target point (other end of line) + ): { x: number; y: number } => { + const dx = targetX - cx; + const dy = targetY - cy; + + // If target is at center, return center + if (dx === 0 && dy === 0) { + return { x: cx, y: cy }; + } + + const halfW = w / 2; + const halfH = h / 2; + + // Calculate intersection with each edge and find the closest one + // The line exits through the edge that has the smallest positive t value + let t = Infinity; + + // Right edge (x = cx + halfW) + if (dx > 0) { + const tRight = halfW / dx; + if (tRight < t && Math.abs(dy * tRight) <= halfH) { + t = tRight; + } + } + + // Left edge (x = cx - halfW) + if (dx < 0) { + const tLeft = -halfW / dx; + if (tLeft < t && Math.abs(dy * tLeft) <= halfH) { + t = tLeft; + } + } + + // Bottom edge (y = cy + halfH) + if (dy > 0) { + const tBottom = halfH / dy; + if (tBottom < t && Math.abs(dx * tBottom) <= halfW) { + t = tBottom; + } + } + + // Top edge (y = cy - halfH) + if (dy < 0) { + const tTop = -halfH / dy; + if (tTop < t && Math.abs(dx * tTop) <= halfW) { + t = tTop; + } + } + + // If no valid intersection found (shouldn't happen), return center + if (t === Infinity) { + return { x: cx, y: cy }; + } + + return { + x: cx + dx * t, + y: cy + dy * t + }; + }; + diagram.nodes.forEach(node => { const attributeCount = node.attributes?.length || 0; const methodCount = node.methods?.length || 0; @@ -403,130 +469,7 @@ export const UMLVisualization: React.FC = ({ link.isReversed = link.isReversed || false; }); - // Draw nodes FIRST (so they appear behind links/arrows in SVG z-order) - // In SVG, elements drawn later appear on top - we want arrows visible above node boxes - const nodes = g.append('g') - .attr('class', 'nodes') - .selectAll('g') - .data(diagram.nodes) - .join('g') - .attr('class', (d) => `node node-${d.type}`) - .call(d3.drag() - .on('start', dragstarted) - .on('drag', dragged) - .on('end', dragended) as any); - - // Node background - nodes.append('rect') - .attr('class', 'node-rect') - .attr('width', (d) => d.width || nodeWidth) - .attr('height', (d) => d.height || nodeHeaderHeight) - .attr('rx', 8) - .attr('fill', 'white') - .attr('stroke', (d) => d.type === 'enum' ? '#ffc107' : '#0a3dfa') - .attr('stroke-width', 2) - .on('click', (event, d) => { - event.stopPropagation(); - setSelectedNode(d); - }); - - // Node header background - nodes.append('rect') - .attr('class', 'node-header') - .attr('width', (d) => d.width || nodeWidth) - .attr('height', nodeHeaderHeight) - .attr('rx', 8) - .attr('fill', (d) => d.type === 'enum' ? '#ffc107' : '#0a3dfa') - .attr('opacity', 0.1); - - // Node name - nodes.append('text') - .attr('class', 'node-name') - .attr('x', (d) => (d.width || nodeWidth) / 2) - .attr('y', nodeHeaderHeight / 2) - .attr('text-anchor', 'middle') - .attr('dominant-baseline', 'middle') - .attr('fill', '#172a59') - .attr('font-weight', 'bold') - .attr('font-size', '14px') - .text((d) => d.name); - - // Node type badge - nodes.append('text') - .attr('class', 'node-type') - .attr('x', 8) - .attr('y', 12) - .attr('fill', '#666') - .attr('font-size', '10px') - .attr('font-style', 'italic') - .text((d) => `«${d.type}»`); - - // Draw attributes section - nodes.each(function(d) { - if (!d.attributes || d.attributes.length === 0) return; - - const nodeGroup = d3.select(this); - let yOffset = nodeHeaderHeight + nodePadding; - - // Attributes divider - nodeGroup.append('line') - .attr('x1', 0) - .attr('y1', nodeHeaderHeight) - .attr('x2', d.width || nodeWidth) - .attr('y2', nodeHeaderHeight) - .attr('stroke', '#0a3dfa') - .attr('stroke-width', 1); - - // Attribute entries - d.attributes.forEach((attr, i) => { - nodeGroup.append('text') - .attr('class', 'node-attribute') - .attr('x', 10) - .attr('y', yOffset + i * attributeHeight) - .attr('fill', '#172a59') - .attr('font-size', '12px') - .text(`${attr.name}: ${attr.type}`); - }); - - yOffset += d.attributes.length * attributeHeight; - }); - - // Draw methods section - nodes.each(function(d) { - if (!d.methods || d.methods.length === 0) return; - - const nodeGroup = d3.select(this); - const attributeCount = d.attributes?.length || 0; - let yOffset = nodeHeaderHeight + nodePadding + - (attributeCount > 0 ? attributeCount * attributeHeight + nodePadding : 0); - - // Methods divider - nodeGroup.append('line') - .attr('x1', 0) - .attr('y1', yOffset - nodePadding) - .attr('x2', d.width || nodeWidth) - .attr('y2', yOffset - nodePadding) - .attr('stroke', '#0a3dfa') - .attr('stroke-width', 1); - - // Method entries - d.methods.forEach((method, i) => { - const methodText = method.returnType - ? `${method.name}(): ${method.returnType}` - : `${method.name}()`; - - nodeGroup.append('text') - .attr('class', 'node-method') - .attr('x', 10) - .attr('y', yOffset + i * methodHeight) - .attr('fill', '#172a59') - .attr('font-size', '12px') - .text(methodText); - }); - }); - - // Draw links AFTER nodes (so they appear on top in SVG z-order) - // This ensures arrows are visible above node boxes + // Draw links first (edges between nodes) const links = g.append('g') .attr('class', 'links') .selectAll('g') @@ -665,14 +608,164 @@ export const UMLVisualization: React.FC = ({ return label; }); + // Draw nodes (on top of links so boxes appear above edge lines) + const nodes = g.append('g') + .attr('class', 'nodes') + .selectAll('g') + .data(diagram.nodes) + .join('g') + .attr('class', (d) => `node node-${d.type}`) + .call(d3.drag() + .on('start', dragstarted) + .on('drag', dragged) + .on('end', dragended) as any); + + // Node background + nodes.append('rect') + .attr('class', 'node-rect') + .attr('width', (d) => d.width || nodeWidth) + .attr('height', (d) => d.height || nodeHeaderHeight) + .attr('rx', 8) + .attr('fill', 'white') + .attr('stroke', (d) => d.type === 'enum' ? '#ffc107' : '#0a3dfa') + .attr('stroke-width', 2) + .on('click', (event, d) => { + event.stopPropagation(); + setSelectedNode(d); + }); + + // Node header background + nodes.append('rect') + .attr('class', 'node-header') + .attr('width', (d) => d.width || nodeWidth) + .attr('height', nodeHeaderHeight) + .attr('rx', 8) + .attr('fill', (d) => d.type === 'enum' ? '#ffc107' : '#0a3dfa') + .attr('opacity', 0.1); + + // Node name + nodes.append('text') + .attr('class', 'node-name') + .attr('x', (d) => (d.width || nodeWidth) / 2) + .attr('y', nodeHeaderHeight / 2) + .attr('text-anchor', 'middle') + .attr('dominant-baseline', 'middle') + .attr('fill', '#172a59') + .attr('font-weight', 'bold') + .attr('font-size', '14px') + .text((d) => d.name); + + // Node type badge + nodes.append('text') + .attr('class', 'node-type') + .attr('x', 8) + .attr('y', 12) + .attr('fill', '#666') + .attr('font-size', '10px') + .attr('font-style', 'italic') + .text((d) => `«${d.type}»`); + + // Draw attributes section + nodes.each(function(d) { + if (!d.attributes || d.attributes.length === 0) return; + + const nodeGroup = d3.select(this); + let yOffset = nodeHeaderHeight + nodePadding; + + // Attributes divider + nodeGroup.append('line') + .attr('x1', 0) + .attr('y1', nodeHeaderHeight) + .attr('x2', d.width || nodeWidth) + .attr('y2', nodeHeaderHeight) + .attr('stroke', '#0a3dfa') + .attr('stroke-width', 1); + + // Attribute entries + d.attributes.forEach((attr, i) => { + nodeGroup.append('text') + .attr('class', 'node-attribute') + .attr('x', 10) + .attr('y', yOffset + i * attributeHeight) + .attr('fill', '#172a59') + .attr('font-size', '12px') + .text(`${attr.name}: ${attr.type}`); + }); + + yOffset += d.attributes.length * attributeHeight; + }); + + // Draw methods section + nodes.each(function(d) { + if (!d.methods || d.methods.length === 0) return; + + const nodeGroup = d3.select(this); + const attributeCount = d.attributes?.length || 0; + let yOffset = nodeHeaderHeight + nodePadding + + (attributeCount > 0 ? attributeCount * attributeHeight + nodePadding : 0); + + // Methods divider + nodeGroup.append('line') + .attr('x1', 0) + .attr('y1', yOffset - nodePadding) + .attr('x2', d.width || nodeWidth) + .attr('y2', yOffset - nodePadding) + .attr('stroke', '#0a3dfa') + .attr('stroke-width', 1); + + // Method entries + d.methods.forEach((method, i) => { + const methodText = method.returnType + ? `${method.name}(): ${method.returnType}` + : `${method.name}()`; + + nodeGroup.append('text') + .attr('class', 'node-method') + .attr('x', 10) + .attr('y', yOffset + i * methodHeight) + .attr('fill', '#172a59') + .attr('font-size', '12px') + .text(methodText); + }); + }); + // Update positions on tick (force simulation) or immediately (dagre) if (simulation) { simulation.on('tick', () => { + // Calculate edge endpoints at box borders for force simulation links.select('line') - .attr('x1', (d: any) => d.source.x) - .attr('y1', (d: any) => d.source.y) - .attr('x2', (d: any) => d.target.x) - .attr('y2', (d: any) => d.target.y); + .attr('x1', (d: any) => { + const sourceW = d.source.width || nodeWidth; + const sourceH = d.source.height || nodeHeaderHeight; + const intersection = getBoxBorderIntersection( + d.source.x, d.source.y, sourceW, sourceH, d.target.x, d.target.y + ); + return intersection.x; + }) + .attr('y1', (d: any) => { + const sourceW = d.source.width || nodeWidth; + const sourceH = d.source.height || nodeHeaderHeight; + const intersection = getBoxBorderIntersection( + d.source.x, d.source.y, sourceW, sourceH, d.target.x, d.target.y + ); + return intersection.y; + }) + .attr('x2', (d: any) => { + const targetW = d.target.width || nodeWidth; + const targetH = d.target.height || nodeHeaderHeight; + const intersection = getBoxBorderIntersection( + d.target.x, d.target.y, targetW, targetH, d.source.x, d.source.y + ); + return intersection.x; + }) + .attr('y2', (d: any) => { + const targetW = d.target.width || nodeWidth; + const targetH = d.target.height || nodeHeaderHeight; + const intersection = getBoxBorderIntersection( + d.target.x, d.target.y, targetW, targetH, d.source.x, d.source.y + ); + return intersection.y; + }); links.select('text') .attr('x', (d: any) => (d.source.x + d.target.x) / 2) @@ -682,22 +775,47 @@ export const UMLVisualization: React.FC = ({ }); } else { // Dagre layout - positions are already computed, update immediately + // Calculate edge endpoints at box borders links.select('line') .attr('x1', (d: any) => { const source = diagram.nodes.find(n => n.id === (typeof d.source === 'string' ? d.source : d.source.id)); - return source?.x || 0; + const target = diagram.nodes.find(n => n.id === (typeof d.target === 'string' ? d.target : d.target.id)); + if (!source || !target) return 0; + const intersection = getBoxBorderIntersection( + source.x!, source.y!, source.width || nodeWidth, source.height || nodeHeaderHeight, + target.x!, target.y! + ); + return intersection.x; }) .attr('y1', (d: any) => { const source = diagram.nodes.find(n => n.id === (typeof d.source === 'string' ? d.source : d.source.id)); - return source?.y || 0; + const target = diagram.nodes.find(n => n.id === (typeof d.target === 'string' ? d.target : d.target.id)); + if (!source || !target) return 0; + const intersection = getBoxBorderIntersection( + source.x!, source.y!, source.width || nodeWidth, source.height || nodeHeaderHeight, + target.x!, target.y! + ); + return intersection.y; }) .attr('x2', (d: any) => { + const source = diagram.nodes.find(n => n.id === (typeof d.source === 'string' ? d.source : d.source.id)); const target = diagram.nodes.find(n => n.id === (typeof d.target === 'string' ? d.target : d.target.id)); - return target?.x || 0; + if (!source || !target) return 0; + const intersection = getBoxBorderIntersection( + target.x!, target.y!, target.width || nodeWidth, target.height || nodeHeaderHeight, + source.x!, source.y! + ); + return intersection.x; }) .attr('y2', (d: any) => { + const source = diagram.nodes.find(n => n.id === (typeof d.source === 'string' ? d.source : d.source.id)); const target = diagram.nodes.find(n => n.id === (typeof d.target === 'string' ? d.target : d.target.id)); - return target?.y || 0; + if (!source || !target) return 0; + const intersection = getBoxBorderIntersection( + target.x!, target.y!, target.width || nodeWidth, target.height || nodeHeaderHeight, + source.x!, source.y! + ); + return intersection.y; }); links.select('text') @@ -732,23 +850,47 @@ export const UMLVisualization: React.FC = ({ nodes.attr('transform', (node: any) => `translate(${node.x - (node.width || nodeWidth) / 2}, ${node.y - (node.height || nodeHeaderHeight) / 2})` ); - // Update links + // Update links with border intersection calculation links.select('line') .attr('x1', (link: any) => { const source = diagram.nodes.find(n => n.id === (typeof link.source === 'string' ? link.source : link.source.id)); - return source?.x || 0; + const target = diagram.nodes.find(n => n.id === (typeof link.target === 'string' ? link.target : link.target.id)); + if (!source || !target) return 0; + const intersection = getBoxBorderIntersection( + source.x!, source.y!, source.width || nodeWidth, source.height || nodeHeaderHeight, + target.x!, target.y! + ); + return intersection.x; }) .attr('y1', (link: any) => { const source = diagram.nodes.find(n => n.id === (typeof link.source === 'string' ? link.source : link.source.id)); - return source?.y || 0; + const target = diagram.nodes.find(n => n.id === (typeof link.target === 'string' ? link.target : link.target.id)); + if (!source || !target) return 0; + const intersection = getBoxBorderIntersection( + source.x!, source.y!, source.width || nodeWidth, source.height || nodeHeaderHeight, + target.x!, target.y! + ); + return intersection.y; }) .attr('x2', (link: any) => { + const source = diagram.nodes.find(n => n.id === (typeof link.source === 'string' ? link.source : link.source.id)); const target = diagram.nodes.find(n => n.id === (typeof link.target === 'string' ? link.target : link.target.id)); - return target?.x || 0; + if (!source || !target) return 0; + const intersection = getBoxBorderIntersection( + target.x!, target.y!, target.width || nodeWidth, target.height || nodeHeaderHeight, + source.x!, source.y! + ); + return intersection.x; }) .attr('y2', (link: any) => { + const source = diagram.nodes.find(n => n.id === (typeof link.source === 'string' ? link.source : link.source.id)); const target = diagram.nodes.find(n => n.id === (typeof link.target === 'string' ? link.target : link.target.id)); - return target?.y || 0; + if (!source || !target) return 0; + const intersection = getBoxBorderIntersection( + target.x!, target.y!, target.width || nodeWidth, target.height || nodeHeaderHeight, + source.x!, source.y! + ); + return intersection.y; }); } }