people tags #387
This commit is contained in:
parent
bb68c074b0
commit
e0ab9915b6
@ -23,7 +23,10 @@ else
|
||||
<div class="spacer"> </div>
|
||||
|
||||
<div class="poem">
|
||||
<h1>شجرهنامهها</h1>
|
||||
<h1>
|
||||
شجرهنامهها
|
||||
<a href="/PeopleGraph" class="up-btn up-btn--ghost up-btn--sm">نمایش شبکهٔ روابط همهٔ شخصیتها</a>
|
||||
</h1>
|
||||
@if (Model.FamilyTreeRoots == null || Model.FamilyTreeRoots.Count == 0)
|
||||
{
|
||||
<p><small>هنوز شجرهنامهای ثبت نشده است.</small></p>
|
||||
|
||||
104
GanjooRazor/Pages/PeopleGraph.cshtml
Normal file
104
GanjooRazor/Pages/PeopleGraph.cshtml
Normal file
@ -0,0 +1,104 @@
|
||||
@page
|
||||
@model GanjooRazor.Pages.PeopleGraphModel
|
||||
@{
|
||||
Layout = "_Layout";
|
||||
}
|
||||
@section Head {
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(Model.LastError))
|
||||
{
|
||||
@Model.LastError
|
||||
}
|
||||
else
|
||||
{
|
||||
<!-- #hdr2 -->
|
||||
|
||||
<div id="fa">
|
||||
|
||||
<div id="maincnt">
|
||||
|
||||
<partial name="_InlineLoginPartial" model="Model" />
|
||||
|
||||
<div class="spacer"> </div>
|
||||
|
||||
<div class="poem">
|
||||
<h1>
|
||||
شبکهٔ روابط شخصیتها
|
||||
<a href="/people" class="up-btn up-btn--ghost up-btn--sm">فهرست شجرهنامهها</a>
|
||||
</h1>
|
||||
<p class="up-hint"><small>
|
||||
این نمودار همهٔ خویشاوندیها و وابستگیهای غیرخویشاوندی ثبتشده را با هم نشان میدهد؛ برخلاف نمودار شجرهنامه که به یک خانواده محدود است.
|
||||
روی هر گره کلیک کنید تا فقط ارتباطهای همان شخص برجسته شود (کلیک دوباره برای بازگشت)؛ برای جابهجایی یک گره آن را بکشید (برای رها کردنش دوبار کلیک کنید).
|
||||
برای پیمایش نمودار آن را با ماوس بکشید و با غلتک ماوس بزرگ/کوچک کنید.
|
||||
</small></p>
|
||||
|
||||
<div style="margin-bottom:10px;">
|
||||
<input type="text" id="pg-search" placeholder="جستجوی نام شخصیت..." style="width:240px;" />
|
||||
<a role="button" id="pg-reset" class="up-btn up-btn--ghost up-btn--sm">نمایش همه</a>
|
||||
</div>
|
||||
|
||||
<div id="pg-legend" class="pg-legend"></div>
|
||||
|
||||
<div id="peoplegraph-container" style="position:relative;width:100%;height:65vh;border:1px solid #d8cdb8;border-radius:6px;overflow:hidden;background:#faf7f0;">
|
||||
<svg id="peoplegraph-svg" width="100%" height="100%"></svg>
|
||||
</div>
|
||||
|
||||
<div class="spacer"> </div>
|
||||
|
||||
<h2>فهرست ارتباطها</h2>
|
||||
<div style="max-height:40vh;overflow:auto;">
|
||||
<table class="up-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>شخص اول</th>
|
||||
<th>نوع ارتباط</th>
|
||||
<th>شخص دوم</th>
|
||||
<th>یادداشت</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="pg-table-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="spacer"> </div>
|
||||
|
||||
</div>
|
||||
|
||||
<partial name="_FooterPartial" model=@(new _FooterPartialModel(){StickyEnabled = true}) />
|
||||
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.pg-circle { fill: #fdf6e6; stroke: #8b6b4a; stroke-width: 1.5; }
|
||||
.pg-circle-tree { fill: #f3e2b8; stroke: #7a4a2a; stroke-width: 2; }
|
||||
.pg-label { font-size: 11px; fill: #4a3520; font-family: inherit; }
|
||||
.pg-legend { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 10px; }
|
||||
.pg-legend-item { font-size: 12px; display: inline-flex; align-items: center; gap: 4px; }
|
||||
.pg-legend-swatch { display: inline-block; width: 12px; height: 12px; border-radius: 3px; vertical-align: middle; }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
var peopleGraphData = @Html.Raw(Model.GraphDataJson);
|
||||
</script>
|
||||
<script src="/js/peoplegraph.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
GanjoorPeopleGraph.render({
|
||||
containerId: 'peoplegraph-container',
|
||||
svgId: 'peoplegraph-svg',
|
||||
tableBodyId: 'pg-table-body',
|
||||
legendId: 'pg-legend',
|
||||
searchInputId: 'pg-search',
|
||||
resetButtonId: 'pg-reset',
|
||||
data: peopleGraphData
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@if (ViewData["TrackingScript"] != null && !string.IsNullOrEmpty(ViewData["TrackingScript"].ToString()))
|
||||
{
|
||||
@Html.Raw(ViewData["TrackingScript"])
|
||||
}
|
||||
}
|
||||
54
GanjooRazor/Pages/PeopleGraph.cshtml.cs
Normal file
54
GanjooRazor/Pages/PeopleGraph.cshtml.cs
Normal file
@ -0,0 +1,54 @@
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Newtonsoft.Json;
|
||||
using RMuseum.Models.Ganjoor.ViewModels;
|
||||
|
||||
namespace GanjooRazor.Pages
|
||||
{
|
||||
/// <summary>
|
||||
/// force-directed "ontology" explorer over the whole known people network (kinship edges and
|
||||
/// non-family ties together), drawn client-side (vanilla JS/SVG - see the <script> block in
|
||||
/// PeopleGraph.cshtml) from what GET api/people/graph returns. The nowruzgan-style counterpart of
|
||||
/// the strict-tree /FamilyTree/{id} view: not limited to one family, shows every recorded
|
||||
/// relationship at once with a synced data table and click-to-focus ego network.
|
||||
/// </summary>
|
||||
public class PeopleGraphModel : LoginPartialEnabledPageModel
|
||||
{
|
||||
public PeopleGraphModel(HttpClient httpClient, IConfiguration configuration) : base(httpClient, configuration)
|
||||
{
|
||||
}
|
||||
|
||||
public string LastError { get; set; }
|
||||
|
||||
public GanjoorPersonGraphViewModel Graph { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Graph, re-serialized with an explicit camelCase contract (same convention FamilyTree.cshtml.cs
|
||||
/// uses for TreeDataJson) so the client-side layout script has a predictable shape to parse
|
||||
/// </summary>
|
||||
public string GraphDataJson =>
|
||||
JsonConvert.SerializeObject(
|
||||
Graph,
|
||||
new JsonSerializerSettings { ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver() }
|
||||
);
|
||||
|
||||
public async Task<IActionResult> OnGetAsync()
|
||||
{
|
||||
InitializeCommonPageState();
|
||||
|
||||
ViewData["Title"] = "گنجور » شبکهٔ روابط شخصیتها";
|
||||
|
||||
var response = await _httpClient.GetAsync($"{APIRoot.Url}/api/people/graph");
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
LastError = await ReadErrorMessageAsync(response);
|
||||
return Page();
|
||||
}
|
||||
Graph = JsonConvert.DeserializeObject<GanjoorPersonGraphViewModel>(await response.Content.ReadAsStringAsync());
|
||||
|
||||
return Page();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -100,11 +100,15 @@ namespace GanjooRazor.Pages
|
||||
{ PersonAffiliationType.Patron, "حامیِ" },
|
||||
{ PersonAffiliationType.Ally, "متحدِ" },
|
||||
{ PersonAffiliationType.Rival, "رقیبِ" },
|
||||
{ PersonAffiliationType.Servant, "خدمتکارِ" },
|
||||
{ PersonAffiliationType.Companion, "همراهِ" },
|
||||
{ PersonAffiliationType.Successor, "جانشینِ" },
|
||||
{ PersonAffiliationType.Other, "دارای نسبتی (به یادداشت نگاه کنید) با" },
|
||||
};
|
||||
|
||||
private static bool IsSymmetricAffiliation(PersonAffiliationType t) =>
|
||||
t == PersonAffiliationType.Ally || t == PersonAffiliationType.Rival || t == PersonAffiliationType.Other;
|
||||
t == PersonAffiliationType.Ally || t == PersonAffiliationType.Rival ||
|
||||
t == PersonAffiliationType.Companion || t == PersonAffiliationType.Other;
|
||||
|
||||
public async Task<IActionResult> OnGetAsync(int id)
|
||||
{
|
||||
|
||||
415
GanjooRazor/wwwroot/js/peoplegraph.js
Normal file
415
GanjooRazor/wwwroot/js/peoplegraph.js
Normal file
@ -0,0 +1,415 @@
|
||||
// Renders the force-directed "ontology" explorer on /PeopleGraph. Pure vanilla JS + SVG - no
|
||||
// external charting/physics library - same rationale as familytree.js: the graph is small (a few
|
||||
// hundred nodes/edges at most), drawn once and then lightly interacted with, and the project prefers
|
||||
// self-hosted, dependency-light front-end code over pulling in something like d3-force.
|
||||
//
|
||||
// Input (window.peopleGraphData, set inline by PeopleGraph.cshtml): the JSON of
|
||||
// RMuseum.Models.Ganjoor.ViewModels.GanjoorPersonGraphViewModel, camelCased -
|
||||
// { nodes: [{id, name, hasFamilyTree}], edges: [{person1Id, person1Name, person2Id, person2Name,
|
||||
// category, typeValue, degreeHint, note}] } where category is "Relation" (typeValue is
|
||||
// PersonRelationType: 0=Parent,1=Sibling,2=Spouse,3=Ancestor) or "Affiliation" (typeValue is
|
||||
// PersonAffiliationType: 0=Minister,1=Advisor,2=Courtier,3=Patron,4=Ally,5=Rival,6=Servant,
|
||||
// 7=Companion,8=Successor,99=Other).
|
||||
//
|
||||
// Unlike FamilyTree.cshtml (a strict tree layout for one connected component), this lays out the
|
||||
// WHOLE graph with a simple force simulation (mutual repulsion + spring edges + light centering,
|
||||
// cooled down like d3-force's alpha decay) since the data isn't tree-shaped - the same node can be
|
||||
// someone's minister AND brother-in-law AND rival, all at once - and a force layout is the natural
|
||||
// way to show that without picking one relationship to draw and hiding the rest.
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
// one entry per (category, typeValue) pair actually in use - label for the legend/table, color
|
||||
// for the edge/legend swatch, directional true when Person1 -> Person2 has a specific meaning
|
||||
// (drawn with an arrowhead), symmetric ones are drawn as plain lines
|
||||
var KIND_META = {
|
||||
'Relation:0': { label: 'پدر/مادر و فرزند', color: '#7a4a2a', directional: true },
|
||||
'Relation:1': { label: 'خواهر/برادر', color: '#4a7a5a', directional: false },
|
||||
'Relation:2': { label: 'همسر', color: '#a0455c', directional: false },
|
||||
'Relation:3': { label: 'نیا و نواده', color: '#8b6b4a', directional: true },
|
||||
'Affiliation:0': { label: 'وزیر', color: '#3a6ea5', directional: true },
|
||||
'Affiliation:1': { label: 'مشاور', color: '#3a8ea5', directional: true },
|
||||
'Affiliation:2': { label: 'درباری', color: '#5a7ea5', directional: true },
|
||||
'Affiliation:3': { label: 'حامی', color: '#2a5e8a', directional: true },
|
||||
'Affiliation:4': { label: 'دوست/متحد', color: '#3a9a4a', directional: false },
|
||||
'Affiliation:5': { label: 'دشمن/رقیب', color: '#c0392b', directional: false },
|
||||
'Affiliation:6': { label: 'خدمتکار', color: '#c07a2b', directional: true },
|
||||
'Affiliation:7': { label: 'همراه', color: '#5aa08a', directional: false },
|
||||
'Affiliation:8': { label: 'جانشین', color: '#8a4ac0', directional: true },
|
||||
'Affiliation:99': { label: 'سایر', color: '#888888', directional: false }
|
||||
};
|
||||
|
||||
function kindKey(edge) { return edge.category + ':' + edge.typeValue; }
|
||||
function kindMeta(edge) { return KIND_META[kindKey(edge)] || { label: edge.category, color: '#888888', directional: false }; }
|
||||
|
||||
function escapeHtml(s) {
|
||||
var d = document.createElement('div');
|
||||
d.textContent = s || '';
|
||||
return d.innerHTML;
|
||||
}
|
||||
|
||||
var SVG_NS = 'http://www.w3.org/2000/svg';
|
||||
function el(tag, attrs) {
|
||||
var e = document.createElementNS(SVG_NS, tag);
|
||||
if (attrs) {
|
||||
Object.keys(attrs).forEach(function (k) { e.setAttribute(k, attrs[k]); });
|
||||
}
|
||||
return e;
|
||||
}
|
||||
|
||||
function renderPeopleGraph(opts) {
|
||||
var container = document.getElementById(opts.containerId);
|
||||
var svg = document.getElementById(opts.svgId);
|
||||
var tableBody = document.getElementById(opts.tableBodyId);
|
||||
var legend = document.getElementById(opts.legendId);
|
||||
var searchInput = opts.searchInputId ? document.getElementById(opts.searchInputId) : null;
|
||||
var resetBtn = opts.resetButtonId ? document.getElementById(opts.resetButtonId) : null;
|
||||
var data = opts.data;
|
||||
|
||||
if (!container || !svg || !data || !data.nodes || data.nodes.length === 0) return;
|
||||
|
||||
var W = Math.max(container.clientWidth, 600);
|
||||
var H = Math.max(container.clientHeight, 500);
|
||||
|
||||
// --- build simulation state -----------------------------------------------------------
|
||||
var nodesById = {};
|
||||
var nodes = data.nodes.map(function (n) {
|
||||
var node = {
|
||||
id: n.id, name: n.name, hasFamilyTree: n.hasFamilyTree,
|
||||
x: W / 2 + (Math.random() - 0.5) * W * 0.6,
|
||||
y: H / 2 + (Math.random() - 0.5) * H * 0.6,
|
||||
vx: 0, vy: 0, fx: null, fy: null, degree: 0
|
||||
};
|
||||
nodesById[n.id] = node;
|
||||
return node;
|
||||
});
|
||||
|
||||
var edges = (data.edges || []).filter(function (e) {
|
||||
return nodesById[e.person1Id] && nodesById[e.person2Id];
|
||||
}).map(function (e) {
|
||||
nodesById[e.person1Id].degree++;
|
||||
nodesById[e.person2Id].degree++;
|
||||
return {
|
||||
source: nodesById[e.person1Id], target: nodesById[e.person2Id],
|
||||
meta: kindMeta(e), note: e.note, degreeHint: e.degreeHint, raw: e
|
||||
};
|
||||
});
|
||||
|
||||
// --- force simulation (simplified d3-force-alike, no external library) ----------------
|
||||
var REPULSION = 2600;
|
||||
var SPRING_LENGTH = 110;
|
||||
var SPRING_K = 0.02;
|
||||
var CENTER_K = 0.01;
|
||||
var DAMPING = 0.82;
|
||||
var alpha = 1, alphaDecay = 0.985, alphaMin = 0.005;
|
||||
|
||||
function tick() {
|
||||
if (alpha < alphaMin) return false;
|
||||
|
||||
// mutual repulsion - O(n^2), fine for a few hundred nodes
|
||||
for (var i = 0; i < nodes.length; i++) {
|
||||
for (var j = i + 1; j < nodes.length; j++) {
|
||||
var a = nodes[i], b = nodes[j];
|
||||
var dx = a.x - b.x, dy = a.y - b.y;
|
||||
var distSq = dx * dx + dy * dy || 0.01;
|
||||
var dist = Math.sqrt(distSq);
|
||||
var force = (REPULSION / distSq) * alpha;
|
||||
var fx = (dx / dist) * force, fy = (dy / dist) * force;
|
||||
a.vx += fx; a.vy += fy;
|
||||
b.vx -= fx; b.vy -= fy;
|
||||
}
|
||||
}
|
||||
|
||||
// spring attraction along edges
|
||||
edges.forEach(function (e) {
|
||||
var dx = e.target.x - e.source.x, dy = e.target.y - e.source.y;
|
||||
var dist = Math.sqrt(dx * dx + dy * dy) || 0.01;
|
||||
var force = SPRING_K * (dist - SPRING_LENGTH) * alpha;
|
||||
var fx = (dx / dist) * force, fy = (dy / dist) * force;
|
||||
e.source.vx += fx; e.source.vy += fy;
|
||||
e.target.vx -= fx; e.target.vy -= fy;
|
||||
});
|
||||
|
||||
// light centering so the whole graph doesn't drift off-canvas
|
||||
nodes.forEach(function (n) {
|
||||
n.vx += (W / 2 - n.x) * CENTER_K * alpha;
|
||||
n.vy += (H / 2 - n.y) * CENTER_K * alpha;
|
||||
});
|
||||
|
||||
nodes.forEach(function (n) {
|
||||
if (n.fx != null) { n.x = n.fx; n.y = n.fy; n.vx = 0; n.vy = 0; return; }
|
||||
n.vx *= DAMPING; n.vy *= DAMPING;
|
||||
n.x += n.vx; n.y += n.vy;
|
||||
});
|
||||
|
||||
alpha *= alphaDecay;
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- SVG scaffolding --------------------------------------------------------------------
|
||||
while (svg.firstChild) svg.removeChild(svg.firstChild);
|
||||
svg.setAttribute('width', W);
|
||||
svg.setAttribute('height', H);
|
||||
svg.removeAttribute('viewBox');
|
||||
|
||||
var canvas = el('g', { id: 'pg-canvas', transform: 'translate(0,0) scale(1)' });
|
||||
svg.appendChild(canvas);
|
||||
|
||||
// arrowhead marker per directional kind color (SVG markers can't take dynamic per-edge fill
|
||||
// easily, so define one marker per color actually in use)
|
||||
var defs = el('defs');
|
||||
canvas.appendChild(defs);
|
||||
var markerIdsByColor = {};
|
||||
function markerFor(color) {
|
||||
var safeId = 'pg-arrow-' + color.replace('#', '');
|
||||
if (markerIdsByColor[color]) return markerIdsByColor[color];
|
||||
var marker = el('marker', {
|
||||
id: safeId, viewBox: '0 0 10 10', refX: '8', refY: '5',
|
||||
markerWidth: '7', markerHeight: '7', orient: 'auto-start-reverse'
|
||||
});
|
||||
var path = el('path', { d: 'M 0 0 L 10 5 L 0 10 z', fill: color });
|
||||
marker.appendChild(path);
|
||||
defs.appendChild(marker);
|
||||
markerIdsByColor[color] = safeId;
|
||||
return safeId;
|
||||
}
|
||||
|
||||
var edgesGroup = el('g', { 'class': 'pg-edges' });
|
||||
var nodesGroup = el('g', { 'class': 'pg-nodes' });
|
||||
canvas.appendChild(edgesGroup);
|
||||
canvas.appendChild(nodesGroup);
|
||||
|
||||
var edgeEls = edges.map(function (e) {
|
||||
var line = el('line', {
|
||||
stroke: e.meta.color, 'stroke-width': 1.6, opacity: 0.55, 'class': 'pg-edge'
|
||||
});
|
||||
if (e.meta.directional) {
|
||||
line.setAttribute('marker-end', 'url(#' + markerFor(e.meta.color) + ')');
|
||||
}
|
||||
edgesGroup.appendChild(line);
|
||||
return { el: line, edge: e };
|
||||
});
|
||||
|
||||
function nodeRadius(n) {
|
||||
return (n.hasFamilyTree ? 10 : 7) + Math.min(6, n.degree * 0.6);
|
||||
}
|
||||
|
||||
var nodeEls = nodes.map(function (n) {
|
||||
var g = el('g', { 'class': 'pg-node', 'data-person-id': n.id, style: 'cursor:pointer' });
|
||||
var circle = el('circle', {
|
||||
r: nodeRadius(n),
|
||||
'class': n.hasFamilyTree ? 'pg-circle pg-circle-tree' : 'pg-circle'
|
||||
});
|
||||
var text = el('text', {
|
||||
'text-anchor': 'middle', dy: -(nodeRadius(n) + 6), 'class': 'pg-label', direction: 'rtl'
|
||||
});
|
||||
text.textContent = n.name;
|
||||
g.appendChild(circle);
|
||||
g.appendChild(text);
|
||||
nodesGroup.appendChild(g);
|
||||
return { el: g, circle: circle, text: text, node: n };
|
||||
});
|
||||
|
||||
function applyPositions() {
|
||||
edgeEls.forEach(function (item) {
|
||||
item.el.setAttribute('x1', item.edge.source.x);
|
||||
item.el.setAttribute('y1', item.edge.source.y);
|
||||
item.el.setAttribute('x2', item.edge.target.x);
|
||||
item.el.setAttribute('y2', item.edge.target.y);
|
||||
});
|
||||
nodeEls.forEach(function (item) {
|
||||
item.el.setAttribute('transform', 'translate(' + item.node.x + ',' + item.node.y + ')');
|
||||
});
|
||||
}
|
||||
|
||||
// --- table + legend ----------------------------------------------------------------------
|
||||
function buildLegend() {
|
||||
if (!legend) return;
|
||||
var usedKeys = {};
|
||||
edges.forEach(function (e) { usedKeys[kindKey(e.raw)] = e.meta; });
|
||||
legend.innerHTML = '';
|
||||
Object.keys(usedKeys).forEach(function (k) {
|
||||
var meta = usedKeys[k];
|
||||
var item = document.createElement('span');
|
||||
item.className = 'pg-legend-item';
|
||||
item.innerHTML = '<span class="pg-legend-swatch" style="background:' + meta.color + '"></span>' + escapeHtml(meta.label);
|
||||
legend.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
var tableRows = [];
|
||||
function buildTable() {
|
||||
if (!tableBody) return;
|
||||
tableBody.innerHTML = '';
|
||||
edges.forEach(function (e, idx) {
|
||||
var tr = document.createElement('tr');
|
||||
tr.setAttribute('data-edge-index', idx);
|
||||
tr.style.cursor = 'pointer';
|
||||
tr.innerHTML =
|
||||
'<td>' + escapeHtml(e.raw.person1Name) + '</td>' +
|
||||
'<td><span class="pg-legend-swatch" style="background:' + e.meta.color + '"></span> ' + escapeHtml(e.meta.label) + '</td>' +
|
||||
'<td>' + escapeHtml(e.raw.person2Name) + '</td>' +
|
||||
'<td><small>' + escapeHtml(e.note || '') + '</small></td>';
|
||||
tr.addEventListener('click', function () { focusOnPersons([e.raw.person1Id, e.raw.person2Id]); });
|
||||
tableBody.appendChild(tr);
|
||||
tableRows.push(tr);
|
||||
});
|
||||
}
|
||||
|
||||
// --- focus / ego-network highlighting -----------------------------------------------------
|
||||
var focusedIds = null; // null = nothing focused (everything full opacity)
|
||||
|
||||
function neighborsOf(personId) {
|
||||
var set = { };
|
||||
set[personId] = true;
|
||||
edges.forEach(function (e) {
|
||||
if (e.source.id === personId) set[e.target.id] = true;
|
||||
if (e.target.id === personId) set[e.source.id] = true;
|
||||
});
|
||||
return set;
|
||||
}
|
||||
|
||||
function focusOnPersons(personIds) {
|
||||
var set = {};
|
||||
personIds.forEach(function (id) {
|
||||
var nb = neighborsOf(id);
|
||||
Object.keys(nb).forEach(function (k) { set[k] = true; });
|
||||
});
|
||||
focusedIds = set;
|
||||
applyFocus();
|
||||
}
|
||||
|
||||
function clearFocus() {
|
||||
focusedIds = null;
|
||||
applyFocus();
|
||||
}
|
||||
|
||||
function applyFocus() {
|
||||
nodeEls.forEach(function (item) {
|
||||
var dim = focusedIds && !focusedIds[item.node.id];
|
||||
item.el.style.opacity = dim ? 0.15 : 1;
|
||||
});
|
||||
edgeEls.forEach(function (item) {
|
||||
var dim = focusedIds && !(focusedIds[item.edge.source.id] && focusedIds[item.edge.target.id]);
|
||||
item.el.style.opacity = dim ? 0.05 : 0.55;
|
||||
});
|
||||
tableRows.forEach(function (tr, idx) {
|
||||
var e = edges[idx];
|
||||
var dim = focusedIds && !(focusedIds[e.source.id] && focusedIds[e.target.id]);
|
||||
tr.style.display = dim ? 'none' : '';
|
||||
});
|
||||
}
|
||||
|
||||
nodeEls.forEach(function (item) {
|
||||
item.el.addEventListener('click', function (evt) {
|
||||
evt.stopPropagation();
|
||||
if (focusedIds && focusedIds[item.node.id] && Object.keys(focusedIds).length <= (item.node.degree + 1)) {
|
||||
// clicking an already-focused node again clears the focus (toggle)
|
||||
clearFocus();
|
||||
} else {
|
||||
focusOnPersons([item.node.id]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (resetBtn) {
|
||||
resetBtn.addEventListener('click', function () { clearFocus(); if (searchInput) searchInput.value = ''; });
|
||||
}
|
||||
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener('input', function () {
|
||||
var q = searchInput.value.trim();
|
||||
if (!q) { clearFocus(); return; }
|
||||
var matchIds = nodes.filter(function (n) { return n.name && n.name.indexOf(q) !== -1; }).map(function (n) { return n.id; });
|
||||
if (matchIds.length === 0) { focusedIds = {}; applyFocus(); return; }
|
||||
focusOnPersons(matchIds);
|
||||
});
|
||||
}
|
||||
|
||||
// --- drag to reposition (pins the node; double-click releases it) ------------------------
|
||||
var draggingNode = null;
|
||||
var panDragging = false, lastX = 0, lastY = 0;
|
||||
var scale = 1, tx = 0, ty = 0;
|
||||
|
||||
function applyTransform() {
|
||||
canvas.setAttribute('transform', 'translate(' + tx + ',' + ty + ') scale(' + scale + ')');
|
||||
}
|
||||
|
||||
function svgPoint(evt) {
|
||||
var rect = svg.getBoundingClientRect();
|
||||
return {
|
||||
x: (evt.clientX - rect.left - tx) / scale,
|
||||
y: (evt.clientY - rect.top - ty) / scale
|
||||
};
|
||||
}
|
||||
|
||||
nodeEls.forEach(function (item) {
|
||||
item.el.addEventListener('mousedown', function (evt) {
|
||||
evt.stopPropagation();
|
||||
draggingNode = item.node;
|
||||
});
|
||||
item.el.addEventListener('dblclick', function (evt) {
|
||||
evt.stopPropagation();
|
||||
item.node.fx = null; item.node.fy = null;
|
||||
alpha = Math.max(alpha, 0.3);
|
||||
ensureRunning();
|
||||
});
|
||||
});
|
||||
|
||||
svg.addEventListener('mousedown', function (evt) {
|
||||
if (draggingNode) return;
|
||||
panDragging = true; lastX = evt.clientX; lastY = evt.clientY;
|
||||
svg.style.cursor = 'grabbing';
|
||||
});
|
||||
window.addEventListener('mousemove', function (evt) {
|
||||
if (draggingNode) {
|
||||
var p = svgPoint(evt);
|
||||
draggingNode.fx = p.x; draggingNode.fy = p.y;
|
||||
draggingNode.x = p.x; draggingNode.y = p.y;
|
||||
if (alpha < 0.05) { alpha = 0.05; ensureRunning(); }
|
||||
return;
|
||||
}
|
||||
if (!panDragging) return;
|
||||
tx += (evt.clientX - lastX); ty += (evt.clientY - lastY);
|
||||
lastX = evt.clientX; lastY = evt.clientY;
|
||||
applyTransform();
|
||||
});
|
||||
window.addEventListener('mouseup', function () {
|
||||
draggingNode = null;
|
||||
panDragging = false;
|
||||
svg.style.cursor = 'grab';
|
||||
});
|
||||
svg.addEventListener('wheel', function (evt) {
|
||||
evt.preventDefault();
|
||||
var factor = evt.deltaY < 0 ? 1.1 : 0.9;
|
||||
scale = Math.min(3, Math.max(0.2, scale * factor));
|
||||
applyTransform();
|
||||
}, { passive: false });
|
||||
svg.style.cursor = 'grab';
|
||||
|
||||
// --- animation loop -----------------------------------------------------------------------
|
||||
var running = false;
|
||||
function ensureRunning() {
|
||||
if (running) return;
|
||||
running = true;
|
||||
requestAnimationFrame(step);
|
||||
}
|
||||
function step() {
|
||||
var keepGoing = tick();
|
||||
applyPositions();
|
||||
if (keepGoing) {
|
||||
requestAnimationFrame(step);
|
||||
} else {
|
||||
running = false;
|
||||
}
|
||||
}
|
||||
|
||||
buildLegend();
|
||||
buildTable();
|
||||
applyPositions();
|
||||
ensureRunning();
|
||||
}
|
||||
|
||||
window.GanjoorPeopleGraph = { render: renderPeopleGraph };
|
||||
})();
|
||||
@ -59,6 +59,25 @@ namespace RMuseum.Controllers
|
||||
return Ok(res.Result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// get the whole known network of people (every person with at least one kinship edge or
|
||||
/// non-family tie, plus every one of those edges/ties), for the force-directed "ontology"
|
||||
/// explorer at /PeopleGraph. Registered before the "{id}" route below (and constrained to
|
||||
/// int there) so this literal segment isn't swallowed as an id.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpGet("graph")]
|
||||
[AllowAnonymous]
|
||||
[ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(GanjoorPersonGraphViewModel))]
|
||||
[ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))]
|
||||
public async Task<IActionResult> GetPersonGraphAsync()
|
||||
{
|
||||
var res = await _personService.GetPersonGraphAsync();
|
||||
if (!string.IsNullOrEmpty(res.ExceptionString))
|
||||
return BadRequest(res.ExceptionString);
|
||||
return Ok(res.Result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// get person by id
|
||||
/// </summary>
|
||||
|
||||
@ -40,6 +40,24 @@ namespace RMuseum.Models.Ganjoor
|
||||
/// </summary>
|
||||
Rival = 5,
|
||||
|
||||
/// <summary>
|
||||
/// Person1 was a servant (personal attendant, not a court office) of Person2 - narrower than
|
||||
/// Courtier, for a purely domestic/personal-service tie rather than a court role
|
||||
/// </summary>
|
||||
Servant = 6,
|
||||
|
||||
/// <summary>
|
||||
/// Person1 and Person2 were companions/comrades - e.g. fellow travelers, brothers-in-arms
|
||||
/// (symmetric - order doesn't matter)
|
||||
/// </summary>
|
||||
Companion = 7,
|
||||
|
||||
/// <summary>
|
||||
/// Person1 succeeded Person2 in a role/office/throne (directional - Person1 is the one who
|
||||
/// came after)
|
||||
/// </summary>
|
||||
Successor = 8,
|
||||
|
||||
/// <summary>
|
||||
/// doesn't fit any of the above - rely on Note for what the tie actually is
|
||||
/// </summary>
|
||||
|
||||
@ -0,0 +1,98 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace RMuseum.Models.Ganjoor.ViewModels
|
||||
{
|
||||
/// <summary>
|
||||
/// the whole known network of people - every GanjoorRelatedPerson that has at least one kinship
|
||||
/// edge or non-family tie, plus every one of those edges/ties - served by GET api/people/graph
|
||||
/// for the force-directed "ontology" explorer at /PeopleGraph, the nowruzgan-style counterpart of
|
||||
/// the strict-tree /FamilyTree/{id} view. Loaded whole (like GetFamilyTreeAsync does for a single
|
||||
/// component) since the graph as a whole is small - a few hundred rows at most.
|
||||
/// </summary>
|
||||
public class GanjoorPersonGraphViewModel
|
||||
{
|
||||
/// <summary>
|
||||
/// every person that appears in at least one edge below
|
||||
/// </summary>
|
||||
public List<GanjoorPersonGraphNode> Nodes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// every kinship edge (GanjoorPersonRelation) and non-family tie (GanjoorPersonAffiliation),
|
||||
/// merged into one flat list - see GanjoorPersonGraphEdge.Category to tell them apart
|
||||
/// </summary>
|
||||
public List<GanjoorPersonGraphEdge> Edges { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// one node of the people graph
|
||||
/// </summary>
|
||||
public class GanjoorPersonGraphNode
|
||||
{
|
||||
/// <summary>
|
||||
/// GanjoorRelatedPerson.Id
|
||||
/// </summary>
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// GanjoorRelatedPerson.Name
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// true if this person captions a family tree (GanjoorRelatedPerson.FamilyTreeCaption not
|
||||
/// empty) - used client-side to draw such nodes slightly larger/more prominent, and to link
|
||||
/// to their /FamilyTree/{id} strict-tree view
|
||||
/// </summary>
|
||||
public bool HasFamilyTree { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// one edge of the people graph - either a kinship edge or a non-family tie, flattened to a
|
||||
/// common shape so the client can draw/list them uniformly
|
||||
/// </summary>
|
||||
public class GanjoorPersonGraphEdge
|
||||
{
|
||||
/// <summary>
|
||||
/// first person - directional meaning (if any) depends on Category/TypeValue, same as the
|
||||
/// underlying GanjoorPersonRelation/GanjoorPersonAffiliation row
|
||||
/// </summary>
|
||||
public int Person1Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// first person's name, denormalized here so the client doesn't need a second lookup
|
||||
/// </summary>
|
||||
public string Person1Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// second person
|
||||
/// </summary>
|
||||
public int Person2Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// second person's name
|
||||
/// </summary>
|
||||
public string Person2Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// "Relation" for a GanjoorPersonRelation edge, "Affiliation" for a GanjoorPersonAffiliation
|
||||
/// one - tells the client which enum TypeValue is a numeric value of
|
||||
/// </summary>
|
||||
public string Category { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// the numeric value of PersonRelationType (when Category is "Relation") or
|
||||
/// PersonAffiliationType (when Category is "Affiliation")
|
||||
/// </summary>
|
||||
public int TypeValue { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// see GanjoorPersonRelation.DegreeHint - null for an Affiliation edge
|
||||
/// </summary>
|
||||
public int? DegreeHint { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// free-text note
|
||||
/// </summary>
|
||||
public string Note { get; set; }
|
||||
}
|
||||
}
|
||||
@ -2838,6 +2838,15 @@
|
||||
</summary>
|
||||
<returns></returns>
|
||||
</member>
|
||||
<member name="M:RMuseum.Controllers.GanjoorRelatedPersonController.GetPersonGraphAsync">
|
||||
<summary>
|
||||
get the whole known network of people (every person with at least one kinship edge or
|
||||
non-family tie, plus every one of those edges/ties), for the force-directed "ontology"
|
||||
explorer at /PeopleGraph. Registered before the "{id}" route below (and constrained to
|
||||
int there) so this literal segment isn't swallowed as an id.
|
||||
</summary>
|
||||
<returns></returns>
|
||||
</member>
|
||||
<member name="M:RMuseum.Controllers.GanjoorRelatedPersonController.GetPersonAsync(System.Int32)">
|
||||
<summary>
|
||||
get person by id
|
||||
@ -11749,6 +11758,24 @@
|
||||
Person1 and Person2 were rivals/enemies (symmetric - order doesn't matter)
|
||||
</summary>
|
||||
</member>
|
||||
<member name="F:RMuseum.Models.Ganjoor.PersonAffiliationType.Servant">
|
||||
<summary>
|
||||
Person1 was a servant (personal attendant, not a court office) of Person2 - narrower than
|
||||
Courtier, for a purely domestic/personal-service tie rather than a court role
|
||||
</summary>
|
||||
</member>
|
||||
<member name="F:RMuseum.Models.Ganjoor.PersonAffiliationType.Companion">
|
||||
<summary>
|
||||
Person1 and Person2 were companions/comrades - e.g. fellow travelers, brothers-in-arms
|
||||
(symmetric - order doesn't matter)
|
||||
</summary>
|
||||
</member>
|
||||
<member name="F:RMuseum.Models.Ganjoor.PersonAffiliationType.Successor">
|
||||
<summary>
|
||||
Person1 succeeded Person2 in a role/office/throne (directional - Person1 is the one who
|
||||
came after)
|
||||
</summary>
|
||||
</member>
|
||||
<member name="F:RMuseum.Models.Ganjoor.PersonAffiliationType.Other">
|
||||
<summary>
|
||||
doesn't fit any of the above - rely on Note for what the tie actually is
|
||||
@ -13292,6 +13319,97 @@
|
||||
FullUrl
|
||||
</summary>
|
||||
</member>
|
||||
<member name="T:RMuseum.Models.Ganjoor.ViewModels.GanjoorPersonGraphViewModel">
|
||||
<summary>
|
||||
the whole known network of people - every GanjoorRelatedPerson that has at least one kinship
|
||||
edge or non-family tie, plus every one of those edges/ties - served by GET api/people/graph
|
||||
for the force-directed "ontology" explorer at /PeopleGraph, the nowruzgan-style counterpart of
|
||||
the strict-tree /FamilyTree/{id} view. Loaded whole (like GetFamilyTreeAsync does for a single
|
||||
component) since the graph as a whole is small - a few hundred rows at most.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:RMuseum.Models.Ganjoor.ViewModels.GanjoorPersonGraphViewModel.Nodes">
|
||||
<summary>
|
||||
every person that appears in at least one edge below
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:RMuseum.Models.Ganjoor.ViewModels.GanjoorPersonGraphViewModel.Edges">
|
||||
<summary>
|
||||
every kinship edge (GanjoorPersonRelation) and non-family tie (GanjoorPersonAffiliation),
|
||||
merged into one flat list - see GanjoorPersonGraphEdge.Category to tell them apart
|
||||
</summary>
|
||||
</member>
|
||||
<member name="T:RMuseum.Models.Ganjoor.ViewModels.GanjoorPersonGraphNode">
|
||||
<summary>
|
||||
one node of the people graph
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:RMuseum.Models.Ganjoor.ViewModels.GanjoorPersonGraphNode.Id">
|
||||
<summary>
|
||||
GanjoorRelatedPerson.Id
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:RMuseum.Models.Ganjoor.ViewModels.GanjoorPersonGraphNode.Name">
|
||||
<summary>
|
||||
GanjoorRelatedPerson.Name
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:RMuseum.Models.Ganjoor.ViewModels.GanjoorPersonGraphNode.HasFamilyTree">
|
||||
<summary>
|
||||
true if this person captions a family tree (GanjoorRelatedPerson.FamilyTreeCaption not
|
||||
empty) - used client-side to draw such nodes slightly larger/more prominent, and to link
|
||||
to their /FamilyTree/{id} strict-tree view
|
||||
</summary>
|
||||
</member>
|
||||
<member name="T:RMuseum.Models.Ganjoor.ViewModels.GanjoorPersonGraphEdge">
|
||||
<summary>
|
||||
one edge of the people graph - either a kinship edge or a non-family tie, flattened to a
|
||||
common shape so the client can draw/list them uniformly
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:RMuseum.Models.Ganjoor.ViewModels.GanjoorPersonGraphEdge.Person1Id">
|
||||
<summary>
|
||||
first person - directional meaning (if any) depends on Category/TypeValue, same as the
|
||||
underlying GanjoorPersonRelation/GanjoorPersonAffiliation row
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:RMuseum.Models.Ganjoor.ViewModels.GanjoorPersonGraphEdge.Person1Name">
|
||||
<summary>
|
||||
first person's name, denormalized here so the client doesn't need a second lookup
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:RMuseum.Models.Ganjoor.ViewModels.GanjoorPersonGraphEdge.Person2Id">
|
||||
<summary>
|
||||
second person
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:RMuseum.Models.Ganjoor.ViewModels.GanjoorPersonGraphEdge.Person2Name">
|
||||
<summary>
|
||||
second person's name
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:RMuseum.Models.Ganjoor.ViewModels.GanjoorPersonGraphEdge.Category">
|
||||
<summary>
|
||||
"Relation" for a GanjoorPersonRelation edge, "Affiliation" for a GanjoorPersonAffiliation
|
||||
one - tells the client which enum TypeValue is a numeric value of
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:RMuseum.Models.Ganjoor.ViewModels.GanjoorPersonGraphEdge.TypeValue">
|
||||
<summary>
|
||||
the numeric value of PersonRelationType (when Category is "Relation") or
|
||||
PersonAffiliationType (when Category is "Affiliation")
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:RMuseum.Models.Ganjoor.ViewModels.GanjoorPersonGraphEdge.DegreeHint">
|
||||
<summary>
|
||||
see GanjoorPersonRelation.DegreeHint - null for an Affiliation edge
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:RMuseum.Models.Ganjoor.ViewModels.GanjoorPersonGraphEdge.Note">
|
||||
<summary>
|
||||
free-text note
|
||||
</summary>
|
||||
</member>
|
||||
<member name="T:RMuseum.Models.Ganjoor.ViewModels.GanjoorPersonRelationsViewModel">
|
||||
<summary>
|
||||
a person plus their kinship/affiliation edges, resolved with the other side's name - the
|
||||
@ -18259,6 +18377,14 @@
|
||||
<param name="reviewNote"></param>
|
||||
<returns></returns>
|
||||
</member>
|
||||
<member name="M:RMuseum.Services.IGanjoorRelatedPersonService.GetPersonGraphAsync">
|
||||
<summary>
|
||||
get the whole known network of people (every person with at least one kinship edge or
|
||||
non-family tie, plus every one of those edges/ties) for the force-directed "ontology"
|
||||
explorer at /PeopleGraph - see GanjoorPersonGraphViewModel
|
||||
</summary>
|
||||
<returns></returns>
|
||||
</member>
|
||||
<member name="T:RMuseum.Services.IGanjoorService">
|
||||
<summary>
|
||||
Ganjoor Poems Content Privider Service
|
||||
@ -21811,6 +21937,12 @@
|
||||
<param name="reviewNote"></param>
|
||||
<returns></returns>
|
||||
</member>
|
||||
<member name="M:RMuseum.Services.Implementation.GanjoorRelatedPersonService.GetPersonGraphAsync">
|
||||
<summary>
|
||||
get the whole known network of people, for the force-directed "ontology" explorer
|
||||
</summary>
|
||||
<returns></returns>
|
||||
</member>
|
||||
<member name="F:RMuseum.Services.Implementation.GanjoorRelatedPersonService._context">
|
||||
<summary>
|
||||
Database Context
|
||||
|
||||
@ -138,5 +138,13 @@ namespace RMuseum.Services
|
||||
/// <param name="reviewNote"></param>
|
||||
/// <returns></returns>
|
||||
Task<RServiceResult<GanjoorPersonRelationEditSuggestion>> ModeratePersonRelationEditSuggestionAsync(Guid moderatorUserId, int suggestionId, CorrectionReviewResult result, string reviewNote);
|
||||
|
||||
/// <summary>
|
||||
/// get the whole known network of people (every person with at least one kinship edge or
|
||||
/// non-family tie, plus every one of those edges/ties) for the force-directed "ontology"
|
||||
/// explorer at /PeopleGraph - see GanjoorPersonGraphViewModel
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
Task<RServiceResult<GanjoorPersonGraphViewModel>> GetPersonGraphAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@ -805,6 +805,84 @@ namespace RMuseum.Services.Implementation
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// get the whole known network of people, for the force-directed "ontology" explorer
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task<RServiceResult<GanjoorPersonGraphViewModel>> GetPersonGraphAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var relations = await _context.GanjoorPersonRelations.ToListAsync();
|
||||
var affiliations = await _context.GanjoorPersonAffiliations.ToListAsync();
|
||||
|
||||
var involvedPersonIds = new HashSet<int>();
|
||||
foreach (var r in relations)
|
||||
{
|
||||
involvedPersonIds.Add(r.Person1Id);
|
||||
involvedPersonIds.Add(r.Person2Id);
|
||||
}
|
||||
foreach (var a in affiliations)
|
||||
{
|
||||
involvedPersonIds.Add(a.Person1Id);
|
||||
involvedPersonIds.Add(a.Person2Id);
|
||||
}
|
||||
|
||||
var persons = await _context.GanjoorRelatedPersons
|
||||
.Where(p => involvedPersonIds.Contains(p.Id))
|
||||
.ToListAsync();
|
||||
var personById = persons.ToDictionary(p => p.Id);
|
||||
|
||||
var nodes = persons.Select(p => new GanjoorPersonGraphNode()
|
||||
{
|
||||
Id = p.Id,
|
||||
Name = p.Name,
|
||||
HasFamilyTree = !string.IsNullOrEmpty(p.FamilyTreeCaption),
|
||||
}).ToList();
|
||||
|
||||
var edges = new List<GanjoorPersonGraphEdge>();
|
||||
|
||||
foreach (var r in relations)
|
||||
{
|
||||
edges.Add(new GanjoorPersonGraphEdge()
|
||||
{
|
||||
Person1Id = r.Person1Id,
|
||||
Person1Name = personById.TryGetValue(r.Person1Id, out var rp1) ? rp1.Name : "",
|
||||
Person2Id = r.Person2Id,
|
||||
Person2Name = personById.TryGetValue(r.Person2Id, out var rp2) ? rp2.Name : "",
|
||||
Category = "Relation",
|
||||
TypeValue = (int)r.RelationType,
|
||||
DegreeHint = r.DegreeHint,
|
||||
Note = r.Note,
|
||||
});
|
||||
}
|
||||
|
||||
foreach (var a in affiliations)
|
||||
{
|
||||
edges.Add(new GanjoorPersonGraphEdge()
|
||||
{
|
||||
Person1Id = a.Person1Id,
|
||||
Person1Name = personById.TryGetValue(a.Person1Id, out var ap1) ? ap1.Name : "",
|
||||
Person2Id = a.Person2Id,
|
||||
Person2Name = personById.TryGetValue(a.Person2Id, out var ap2) ? ap2.Name : "",
|
||||
Category = "Affiliation",
|
||||
TypeValue = (int)a.AffiliationType,
|
||||
Note = a.Note,
|
||||
});
|
||||
}
|
||||
|
||||
return new RServiceResult<GanjoorPersonGraphViewModel>(new GanjoorPersonGraphViewModel()
|
||||
{
|
||||
Nodes = nodes,
|
||||
Edges = edges,
|
||||
});
|
||||
}
|
||||
catch (Exception exp)
|
||||
{
|
||||
return new RServiceResult<GanjoorPersonGraphViewModel>(null, exp.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Database Context
|
||||
/// </summary>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user