people tags #387
This commit is contained in:
parent
e0ab9915b6
commit
0cb288228d
@ -86,6 +86,14 @@
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/OverlappingMarkerSpiderfier-Leaflet/0.2.6/oms.min.js"></script>
|
||||
}
|
||||
}
|
||||
|
||||
@if (Model.IsCatPage || Model.IsPoetPage)
|
||||
{
|
||||
@* the on-demand "شخصیتها" tab (_CatPagePartial.cshtml / _PoetPagePartial.cshtml) can be
|
||||
clicked at any time on any cat/poet page, so this is loaded unconditionally here (unlike
|
||||
the leaflet.js block above, which only loads when geo tags are already known to exist) *@
|
||||
<script src="/js/peoplegraph.js"></script>
|
||||
}
|
||||
}
|
||||
|
||||
@if (Model.OfflineMode)
|
||||
|
||||
@ -1285,6 +1285,29 @@ namespace GanjooRazor.Pages
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// on-demand "شخصیتها" (characters) tab on a poet/category page - fetches the
|
||||
/// category-scoped person/relation graph (see GanjoorRelatedPersonService.GetCatPersonGraphAsync)
|
||||
/// and renders it with the same peoplegraph.js force-directed viewer used by /PeopleGraph
|
||||
/// </summary>
|
||||
/// <param name="catId"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<IActionResult> OnGetPersonGraphAsync(int catId)
|
||||
{
|
||||
var response = await _httpClient.GetAsync($"{APIRoot.Url}/api/ganjoor/cat/{catId}/persongraph");
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
return BadRequest(await ReadErrorMessageAsync(response));
|
||||
}
|
||||
var graph = JsonConvert.DeserializeObject<GanjoorPersonGraphViewModel>(await response.Content.ReadAsStringAsync());
|
||||
|
||||
return Partial("~/Pages/Partials/GanjoorPage/_PersonGraphPartial.cshtml", new _PersonGraphPartialModel()
|
||||
{
|
||||
CatId = catId,
|
||||
Graph = graph,
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<_CategoryWordsCountPartialModel> _GetCategoryWordCountsAsync(int catId, int poetId, bool remStopWords = false)
|
||||
{
|
||||
var wordSumsResponse = await _httpClient.GetAsync($"{APIRoot.Url}/api/ganjoor/wordsums/{catId}");
|
||||
|
||||
@ -32,6 +32,7 @@
|
||||
<div class="poemtablinks"><a href="#pretab">آمار</a></div>
|
||||
<div class="poemtablinks active"><a href="#words" onclick="javascript:loadWordCounts(@Model.GanjoorPage.PoetOrCat.Cat.Id, @Model.GanjoorPage.PoetOrCat.Poet.Id, false)">واژگان</a></div>
|
||||
<div class="poemtablinks"><a href="#recitations" onclick="javascript:loadCatRecitations(@Model.GanjoorPage.PoetOrCat.Cat.Id)">خوانشها</a></div>
|
||||
<div class="poemtablinks"><a href="#persongraph" onclick="javascript:loadPersonGraph(@Model.GanjoorPage.PoetOrCat.Cat.Id)">شخصیتها</a></div>
|
||||
<div class="poemtablinks"><a href="#topvisits" onclick="javascript:loadTopVisits('@Model.GanjoorPage.FullUrl')">پربازدیدها</a></div>
|
||||
</div>
|
||||
|
||||
@ -54,6 +55,7 @@
|
||||
<div class="poemtablinks"><a href="#pretab">آمار</a></div>
|
||||
<div class="poemtablinks"><a href="#words" onclick="javascript:loadWordCounts(@Model.GanjoorPage.PoetOrCat.Cat.Id, @Model.GanjoorPage.PoetOrCat.Poet.Id, false)">واژگان</a></div>
|
||||
<div class="poemtablinks active"><a href="#recitations" onclick="javascript:loadCatRecitations(@Model.GanjoorPage.PoetOrCat.Cat.Id)">خوانشها</a></div>
|
||||
<div class="poemtablinks"><a href="#persongraph" onclick="javascript:loadPersonGraph(@Model.GanjoorPage.PoetOrCat.Cat.Id)">شخصیتها</a></div>
|
||||
<div class="poemtablinks"><a href="#topvisits" onclick="javascript:loadTopVisits('@Model.GanjoorPage.FullUrl')">پربازدیدها</a></div>
|
||||
</div>
|
||||
<div class="related-images-frame poemtabcontent">
|
||||
@ -70,12 +72,29 @@
|
||||
}
|
||||
</div>
|
||||
|
||||
@* Person Graph ("characters in this work") Section *@
|
||||
<div class="poempretab" id="persongraph"></div>
|
||||
<div class="poemtab">
|
||||
<div class="poemtablinks"><a href="#pretab">آمار</a></div>
|
||||
<div class="poemtablinks"><a href="#words" onclick="javascript:loadWordCounts(@Model.GanjoorPage.PoetOrCat.Cat.Id, @Model.GanjoorPage.PoetOrCat.Poet.Id, false)">واژگان</a></div>
|
||||
<div class="poemtablinks"><a href="#recitations" onclick="javascript:loadCatRecitations(@Model.GanjoorPage.PoetOrCat.Cat.Id)">خوانشها</a></div>
|
||||
<div class="poemtablinks active"><a href="#persongraph" onclick="javascript:loadPersonGraph(@Model.GanjoorPage.PoetOrCat.Cat.Id)">شخصیتها</a></div>
|
||||
<div class="poemtablinks"><a href="#topvisits" onclick="javascript:loadTopVisits('@Model.GanjoorPage.FullUrl')">پربازدیدها</a></div>
|
||||
</div>
|
||||
<div class="related-images-frame poemtabcontent">
|
||||
<div class="century cursor-pointer" id="load-persongraph" onclick="loadPersonGraph(@Model.GanjoorPage.PoetOrCat.Cat.Id)">
|
||||
بارگذاری شخصیتهای این بخش
|
||||
</div>
|
||||
<div id="persongraph-placeholder"></div>
|
||||
</div>
|
||||
|
||||
@* Top Visits Section *@
|
||||
<div class="poempretab" id="topvisits"></div>
|
||||
<div class="poemtab">
|
||||
<div class="poemtablinks"><a href="#pretab">آمار</a></div>
|
||||
<div class="poemtablinks"><a href="#words" onclick="javascript:loadWordCounts(@Model.GanjoorPage.PoetOrCat.Cat.Id, @Model.GanjoorPage.PoetOrCat.Poet.Id, false)">واژگان</a></div>
|
||||
<div class="poemtablinks"><a href="#recitations" onclick="javascript:loadCatRecitations(@Model.GanjoorPage.PoetOrCat.Cat.Id)">خوانشها</a></div>
|
||||
<div class="poemtablinks"><a href="#persongraph" onclick="javascript:loadPersonGraph(@Model.GanjoorPage.PoetOrCat.Cat.Id)">شخصیتها</a></div>
|
||||
<div class="poemtablinks active"><a href="#topvisits" onclick="javascript:loadTopVisits('@Model.GanjoorPage.FullUrl')">پربازدیدها</a></div>
|
||||
</div>
|
||||
<div class="related-images-frame poemtabcontent">
|
||||
|
||||
@ -54,6 +54,7 @@
|
||||
<div class="poemtablinks"><a href="#recitations" onclick="javascript:loadCatRecitations(@Model.GanjoorPage.PoetOrCat.Cat.Id)">خوانشها</a></div>
|
||||
<div class="poemtablinks"><a href="#photos">تصاویر چهره</a></div>
|
||||
<div class="poemtablinks"><a href="#papersources">منابع کاغذی</a></div>
|
||||
<div class="poemtablinks"><a href="#persongraph" onclick="javascript:loadPersonGraph(@Model.GanjoorPage.PoetOrCat.Cat.Id)">شخصیتها</a></div>
|
||||
<div class="poemtablinks"><a href="#topvisits" onclick="javascript:loadTopVisits('@Model.GanjoorPage.FullUrl')">پربازدیدها</a></div>
|
||||
</div>
|
||||
|
||||
@ -71,6 +72,7 @@
|
||||
<div class="poemtablinks"><a href="#recitations" onclick="javascript:loadCatRecitations(@Model.GanjoorPage.PoetOrCat.Cat.Id)">خوانشها</a></div>
|
||||
<div class="poemtablinks"><a href="#photos">تصاویر چهره</a></div>
|
||||
<div class="poemtablinks"><a href="#papersources">منابع کاغذی</a></div>
|
||||
<div class="poemtablinks"><a href="#persongraph" onclick="javascript:loadPersonGraph(@Model.GanjoorPage.PoetOrCat.Cat.Id)">شخصیتها</a></div>
|
||||
<div class="poemtablinks"><a href="#topvisits" onclick="javascript:loadTopVisits('@Model.GanjoorPage.FullUrl')">پربازدیدها</a></div>
|
||||
</div>
|
||||
<div class="related-images-frame poemtabcontent">
|
||||
@ -86,6 +88,7 @@
|
||||
<div class="poemtablinks"><a href="#recitations" onclick="javascript:loadCatRecitations(@Model.GanjoorPage.PoetOrCat.Cat.Id)">خوانشها</a></div>
|
||||
<div class="poemtablinks"><a href="#photos">تصاویر چهره</a></div>
|
||||
<div class="poemtablinks"><a href="#papersources">منابع کاغذی</a></div>
|
||||
<div class="poemtablinks"><a href="#persongraph" onclick="javascript:loadPersonGraph(@Model.GanjoorPage.PoetOrCat.Cat.Id)">شخصیتها</a></div>
|
||||
<div class="poemtablinks"><a href="#topvisits" onclick="javascript:loadTopVisits('@Model.GanjoorPage.FullUrl')">پربازدیدها</a></div>
|
||||
</div>
|
||||
<div class="related-images-frame poemtabcontent">
|
||||
@ -109,6 +112,7 @@
|
||||
<div class="poemtablinks active"><a href="#recitations" onclick="javascript:loadCatRecitations(@Model.GanjoorPage.PoetOrCat.Cat.Id)">خوانشها</a></div>
|
||||
<div class="poemtablinks"><a href="#photos">تصاویر چهره</a></div>
|
||||
<div class="poemtablinks"><a href="#papersources">منابع کاغذی</a></div>
|
||||
<div class="poemtablinks"><a href="#persongraph" onclick="javascript:loadPersonGraph(@Model.GanjoorPage.PoetOrCat.Cat.Id)">شخصیتها</a></div>
|
||||
<div class="poemtablinks"><a href="#topvisits" onclick="javascript:loadTopVisits('@Model.GanjoorPage.FullUrl')">پربازدیدها</a></div>
|
||||
</div>
|
||||
<div class="related-images-frame poemtabcontent">
|
||||
@ -134,6 +138,7 @@
|
||||
<div class="poemtablinks"><a href="#recitations" onclick="javascript:loadCatRecitations(@Model.GanjoorPage.PoetOrCat.Cat.Id)">خوانشها</a></div>
|
||||
<div class="poemtablinks active"><a href="#photos">تصاویر چهره</a></div>
|
||||
<div class="poemtablinks"><a href="#papersources">منابع کاغذی</a></div>
|
||||
<div class="poemtablinks"><a href="#persongraph" onclick="javascript:loadPersonGraph(@Model.GanjoorPage.PoetOrCat.Cat.Id)">شخصیتها</a></div>
|
||||
<div class="poemtablinks"><a href="#topvisits" onclick="javascript:loadTopVisits('@Model.GanjoorPage.FullUrl')">پربازدیدها</a></div>
|
||||
</div>
|
||||
<div class="poemtabcontent">
|
||||
@ -169,6 +174,7 @@
|
||||
<div class="poemtablinks"><a href="#recitations" onclick="javascript:loadCatRecitations(@Model.GanjoorPage.PoetOrCat.Cat.Id)">خوانشها</a></div>
|
||||
<div class="poemtablinks"><a href="#photos">تصاویر چهره</a></div>
|
||||
<div class="poemtablinks active"><a href="#papersources">منابع کاغذی</a></div>
|
||||
<div class="poemtablinks"><a href="#persongraph" onclick="javascript:loadPersonGraph(@Model.GanjoorPage.PoetOrCat.Cat.Id)">شخصیتها</a></div>
|
||||
<div class="poemtablinks"><a href="#topvisits" onclick="javascript:loadTopVisits('@Model.GanjoorPage.FullUrl')">پربازدیدها</a></div>
|
||||
</div>
|
||||
<div class="poemtabcontent related-images-frame">
|
||||
@ -210,6 +216,7 @@
|
||||
<div class="poemtablinks"><a href="#recitations" onclick="javascript:loadCatRecitations(@Model.GanjoorPage.PoetOrCat.Cat.Id)">خوانشها</a></div>
|
||||
<div class="poemtablinks"><a href="#photos">تصاویر چهره</a></div>
|
||||
<div class="poemtablinks"><a href="#papersources">منابع کاغذی</a></div>
|
||||
<div class="poemtablinks"><a href="#persongraph" onclick="javascript:loadPersonGraph(@Model.GanjoorPage.PoetOrCat.Cat.Id)">شخصیتها</a></div>
|
||||
<div class="poemtablinks active"><a href="#topvisits" onclick="javascript:loadTopVisits('@Model.GanjoorPage.FullUrl')">پربازدیدها</a></div>
|
||||
</div>
|
||||
<div class="related-images-frame poemtabcontent">
|
||||
@ -219,6 +226,25 @@
|
||||
<div id="topvisits-placeholder"></div>
|
||||
</div>
|
||||
|
||||
@* Person Graph ("characters in this work") Section *@
|
||||
<div class="poempretab" id="persongraph"></div>
|
||||
<div class="poemtab">
|
||||
<div class="poemtablinks"><a href="#pretab">آمار</a></div>
|
||||
<div class="poemtablinks"><a href="#quoteds">مشق شعر</a></div>
|
||||
<div class="poemtablinks"><a href="#words" onclick="javascript:loadWordCounts(@Model.GanjoorPage.PoetOrCat.Cat.Id, @Model.GanjoorPage.PoetOrCat.Poet.Id, false)">واژگان</a></div>
|
||||
<div class="poemtablinks"><a href="#recitations" onclick="javascript:loadCatRecitations(@Model.GanjoorPage.PoetOrCat.Cat.Id)">خوانشها</a></div>
|
||||
<div class="poemtablinks"><a href="#photos">تصاویر چهره</a></div>
|
||||
<div class="poemtablinks"><a href="#papersources">منابع کاغذی</a></div>
|
||||
<div class="poemtablinks active"><a href="#persongraph" onclick="javascript:loadPersonGraph(@Model.GanjoorPage.PoetOrCat.Cat.Id)">شخصیتها</a></div>
|
||||
<div class="poemtablinks"><a href="#topvisits" onclick="javascript:loadTopVisits('@Model.GanjoorPage.FullUrl')">پربازدیدها</a></div>
|
||||
</div>
|
||||
<div class="related-images-frame poemtabcontent">
|
||||
<div class="century cursor-pointer" id="load-persongraph" onclick="loadPersonGraph(@Model.GanjoorPage.PoetOrCat.Cat.Id)">
|
||||
بارگذاری شخصیتهای این بخش
|
||||
</div>
|
||||
<div id="persongraph-placeholder"></div>
|
||||
</div>
|
||||
|
||||
@* Map *@
|
||||
@if (!Model.OfflineMode && Model.CategoryPoemGeoDateTags != null && Model.CategoryPoemGeoDateTags.Length > 0)
|
||||
{
|
||||
|
||||
@ -0,0 +1,64 @@
|
||||
@model GanjooRazor.Pages._PersonGraphPartialModel
|
||||
|
||||
@if (Model.Graph == null || Model.Graph.Nodes == null || Model.Graph.Nodes.Count == 0)
|
||||
{
|
||||
<p>برای این بخش هنوز شخصیتی برچسبگذاری نشده است.</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div id="pg-cat-section">
|
||||
<p class="up-hint"><small>
|
||||
روی هر گره کلیک کنید تا فقط ارتباطهای همان شخص برجسته شود (کلیک دوباره برای بازگشت)؛ برای جابهجایی یک گره آن را بکشید (برای رها کردنش دوبار کلیک کنید).
|
||||
برای پیمایش نمودار آن را با ماوس بکشید و با غلتک ماوس بزرگ/کوچک کنید.
|
||||
@if (Model.HasSecondaryNodes)
|
||||
{
|
||||
<text> گرههای نقطهچین/کمرنگ، خویشاوندان یا وابستگانی هستند که در متن این بخش نامشان نیامده، اما برای آشنایی بیشتر با داستان نشان داده شدهاند.</text>
|
||||
}
|
||||
</small></p>
|
||||
|
||||
<div style="margin-bottom:10px;">
|
||||
<input type="text" id="pg-cat-search" placeholder="جستجوی نام شخصیت..." style="width:240px;" />
|
||||
<a role="button" id="pg-cat-reset" class="up-btn up-btn--ghost up-btn--sm">نمایش همه</a>
|
||||
<a role="button" href="/PeopleGraph" class="up-btn up-btn--ghost up-btn--sm">نمایش شبکهٔ کامل شخصیتها</a>
|
||||
</div>
|
||||
|
||||
<div id="pg-cat-legend" class="pg-legend"></div>
|
||||
|
||||
<div id="pg-cat-container" style="position:relative;width:100%;height:55vh;border:1px solid #d8cdb8;border-radius:6px;overflow:hidden;background:#faf7f0;">
|
||||
<svg id="pg-cat-svg" width="100%" height="100%"></svg>
|
||||
</div>
|
||||
|
||||
<div class="spacer"> </div>
|
||||
|
||||
<div style="max-height:30vh;overflow:auto;">
|
||||
<table class="up-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>شخص اول</th>
|
||||
<th>نوع ارتباط</th>
|
||||
<th>شخص دوم</th>
|
||||
<th>یادداشت</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="pg-cat-table-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.pg-circle { fill: #fdf6e6; stroke: #8b6b4a; stroke-width: 1.5; }
|
||||
.pg-circle-tree { fill: #f3e2b8; stroke: #7a4a2a; stroke-width: 2; }
|
||||
.pg-circle-secondary { stroke-dasharray: 3,2; opacity: 0.7; }
|
||||
.pg-label { font-size: 11px; fill: #4a3520; font-family: inherit; }
|
||||
.pg-label-secondary { opacity: 0.7; font-style: italic; }
|
||||
.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>
|
||||
|
||||
@* not an executable script - just a data holder, parsed and rendered explicitly by
|
||||
loadPersonGraph() in bk.js once this fragment has been appended to the page, same
|
||||
convention used for the JSON blob is deliberately not the ajax response's inline <script>
|
||||
since script tags injected via $(data).appendTo(...) don't reliably auto-execute *@
|
||||
<script type="application/json" id="pg-cat-data">@Html.Raw(Model.GraphDataJson)</script>
|
||||
</div>
|
||||
}
|
||||
@ -0,0 +1,58 @@
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
using RMuseum.Models.Ganjoor.ViewModels;
|
||||
|
||||
namespace GanjooRazor.Pages
|
||||
{
|
||||
/// <summary>
|
||||
/// model for the on-demand "شخصیتها" (characters) tab shown on a poet/category page - renders
|
||||
/// the same force-directed graph as PeopleGraph.cshtml/peoplegraph.js, but scoped to one
|
||||
/// work/category (see GanjoorRelatedPersonService.GetCatPersonGraphAsync) instead of the whole
|
||||
/// site. Element ids are prefixed "pg-cat-" so this partial can sit on the same page as (and not
|
||||
/// collide with) any other graph instance.
|
||||
/// </summary>
|
||||
public class _PersonGraphPartialModel : PageModel
|
||||
{
|
||||
/// <summary>
|
||||
/// category id this graph is scoped to
|
||||
/// </summary>
|
||||
public int CatId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// the graph data returned by GET api/ganjoor/cat/{id}/persongraph
|
||||
/// </summary>
|
||||
public GanjoorPersonGraphViewModel Graph { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Graph, re-serialized with an explicit camelCase contract (same convention
|
||||
/// PeopleGraphModel.GraphDataJson/FamilyTree's TreeDataJson use), so peoplegraph.js has a
|
||||
/// predictable shape to parse regardless of which page embeds it
|
||||
/// </summary>
|
||||
public string GraphDataJson =>
|
||||
JsonConvert.SerializeObject(
|
||||
Graph,
|
||||
new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() }
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// true if the graph has at least one node pulled in only as a one-hop relative/affiliate
|
||||
/// (never itself tagged in this work's verses) - used to decide whether to show the
|
||||
/// explanatory legend note about dashed/secondary nodes
|
||||
/// </summary>
|
||||
public bool HasSecondaryNodes
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Graph?.Nodes == null)
|
||||
return false;
|
||||
foreach (var node in Graph.Nodes)
|
||||
{
|
||||
if (!node.DirectlyTagged)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -73,7 +73,9 @@ else
|
||||
<style>
|
||||
.pg-circle { fill: #fdf6e6; stroke: #8b6b4a; stroke-width: 1.5; }
|
||||
.pg-circle-tree { fill: #f3e2b8; stroke: #7a4a2a; stroke-width: 2; }
|
||||
.pg-circle-secondary { stroke-dasharray: 3,2; opacity: 0.7; }
|
||||
.pg-label { font-size: 11px; fill: #4a3520; font-family: inherit; }
|
||||
.pg-label-secondary { opacity: 0.7; font-style: italic; }
|
||||
.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; }
|
||||
|
||||
@ -1692,6 +1692,53 @@ function loadCatRecitations(catId) {
|
||||
|
||||
}
|
||||
|
||||
// on-demand "شخصیتها" tab on a poet/category page - loads the category-scoped person/relation
|
||||
// graph (see _PersonGraphPartial.cshtml / OnGetPersonGraphAsync) and hands its JSON payload to the
|
||||
// same peoplegraph.js force-directed renderer used by /PeopleGraph, with element ids prefixed
|
||||
// "pg-cat-" so it doesn't collide with anything else on the page
|
||||
function loadPersonGraph(catId) {
|
||||
|
||||
var loadButton = document.getElementById("load-persongraph");
|
||||
if (loadButton == null) {
|
||||
|
||||
return;
|
||||
}
|
||||
loadButton.remove();
|
||||
|
||||
var divParent = document.getElementById('persongraph-placeholder');
|
||||
var imgElementId = 'loadingpersongraphimg';
|
||||
divParent.innerHTML = divParent.innerHTML + '<div class="bnumdiv" id="remove-this-persongraph"><img id="' + imgElementId + '" src="/image/loading.gif" alt="بارگذاری"/></div>';
|
||||
$.ajax({
|
||||
type: "GET",
|
||||
url: '?Handler=PersonGraph&catId=' + String(catId),
|
||||
error: function () {
|
||||
if (document.getElementById("remove-this-persongraph") != null) {
|
||||
document.getElementById("remove-this-persongraph").remove();
|
||||
}
|
||||
},
|
||||
success: function (data) {
|
||||
if (document.getElementById("remove-this-persongraph") != null) {
|
||||
document.getElementById("remove-this-persongraph").remove();
|
||||
}
|
||||
$(data).appendTo(divParent);
|
||||
var dataElement = document.getElementById('pg-cat-data');
|
||||
if (dataElement != null) {
|
||||
var graphData = JSON.parse(dataElement.textContent);
|
||||
GanjoorPeopleGraph.render({
|
||||
containerId: 'pg-cat-container',
|
||||
svgId: 'pg-cat-svg',
|
||||
tableBodyId: 'pg-cat-table-body',
|
||||
legendId: 'pg-cat-legend',
|
||||
searchInputId: 'pg-cat-search',
|
||||
resetButtonId: 'pg-cat-reset',
|
||||
data: graphData
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/questions/56300132/how-to-override-css-prefers-color-scheme-setting
|
||||
// Return the system level color scheme, but if something's in local storage, return that
|
||||
// Unless the system scheme matches the the stored scheme, in which case... remove from local storage
|
||||
|
||||
@ -3,13 +3,20 @@
|
||||
// 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).
|
||||
// Data shape (RMuseum.Models.Ganjoor.ViewModels.GanjoorPersonGraphViewModel, camelCased) -
|
||||
// { nodes: [{id, name, hasFamilyTree, directlyTagged}], 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). directlyTagged is false only on
|
||||
// the category-scoped graph (GET api/ganjoor/cat/{id}/persongraph, the "characters in this work"
|
||||
// tab): such a node was pulled in as a one-hop relative/affiliate of someone actually named in the
|
||||
// work's verses, and is never false on the whole-site graph fed by PeopleGraph.cshtml.
|
||||
//
|
||||
// Called with an options object (see PeopleGraph.cshtml and _PersonGraphPartial.cshtml for two call
|
||||
// sites with different element-id prefixes and data payloads), so the same renderer serves both the
|
||||
// whole-site /PeopleGraph explorer and any number of category-scoped "شخصیتها" tabs on cat/poet
|
||||
// pages.
|
||||
//
|
||||
// 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,
|
||||
@ -77,6 +84,8 @@
|
||||
var nodes = data.nodes.map(function (n) {
|
||||
var node = {
|
||||
id: n.id, name: n.name, hasFamilyTree: n.hasFamilyTree,
|
||||
// absent on the whole-site graph payload (always directly tagged there) - default true
|
||||
directlyTagged: n.directlyTagged !== false,
|
||||
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
|
||||
@ -197,12 +206,19 @@
|
||||
|
||||
var nodeEls = nodes.map(function (n) {
|
||||
var g = el('g', { 'class': 'pg-node', 'data-person-id': n.id, style: 'cursor:pointer' });
|
||||
var circleClass = n.hasFamilyTree ? 'pg-circle pg-circle-tree' : 'pg-circle';
|
||||
if (!n.directlyTagged) {
|
||||
// context pulled in one hop out (a relative/affiliate never actually named in the
|
||||
// work's verses) - draw dashed/dimmer so it reads as secondary, not part of the text
|
||||
circleClass += ' pg-circle-secondary';
|
||||
}
|
||||
var circle = el('circle', {
|
||||
r: nodeRadius(n),
|
||||
'class': n.hasFamilyTree ? 'pg-circle pg-circle-tree' : 'pg-circle'
|
||||
'class': circleClass
|
||||
});
|
||||
var text = el('text', {
|
||||
'text-anchor': 'middle', dy: -(nodeRadius(n) + 6), 'class': 'pg-label', direction: 'rtl'
|
||||
'text-anchor': 'middle', dy: -(nodeRadius(n) + 6),
|
||||
'class': n.directlyTagged ? 'pg-label' : 'pg-label pg-label-secondary', direction: 'rtl'
|
||||
});
|
||||
text.textContent = n.name;
|
||||
g.appendChild(circle);
|
||||
|
||||
@ -3833,6 +3833,28 @@ namespace RMuseum.Controllers
|
||||
return Ok(res.Result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// get the network of people relevant to this category/work (every person tagged in a poem
|
||||
/// under its subtree, plus their relatives/affiliates one hop out) - for the "شخصیتها" tab
|
||||
/// on a category/poet page
|
||||
/// </summary>
|
||||
/// <param name="id"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet]
|
||||
[Route("cat/{id}/persongraph")]
|
||||
[AllowAnonymous]
|
||||
[ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(GanjoorPersonGraphViewModel))]
|
||||
[ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))]
|
||||
public async Task<IActionResult> GetCatPersonGraphAsync(int id)
|
||||
{
|
||||
var res =
|
||||
await _personService.GetCatPersonGraphAsync(id);
|
||||
|
||||
if (!string.IsNullOrEmpty(res.ExceptionString))
|
||||
return BadRequest(res.ExceptionString);
|
||||
return Ok(res.Result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// synchronize https:://naskban.ir links (logs in and then out to naskban.ir using auth info)
|
||||
/// </summary>
|
||||
@ -5091,6 +5113,12 @@ namespace RMuseum.Controllers
|
||||
/// </summary>
|
||||
protected IConfiguration Configuration { get; }
|
||||
|
||||
/// <summary>
|
||||
/// related people (family tree / person tagging) service - used here only for the
|
||||
/// category-scoped "cat/{id}/persongraph" endpoint; everything else about people lives in
|
||||
/// GanjoorRelatedPersonController
|
||||
/// </summary>
|
||||
protected readonly IGanjoorRelatedPersonService _personService;
|
||||
|
||||
/// <summary>
|
||||
/// constructor
|
||||
@ -5101,13 +5129,15 @@ namespace RMuseum.Controllers
|
||||
/// <param name="imageFileService"></param>
|
||||
/// <param name="memoryCache"></param>
|
||||
/// <param name="configuration"></param>
|
||||
/// <param name="personService"></param>
|
||||
public GanjoorController(
|
||||
IGanjoorService ganjoorService,
|
||||
IAppUserService appUserService,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
IImageFileService imageFileService,
|
||||
IMemoryCache memoryCache,
|
||||
IConfiguration configuration
|
||||
IConfiguration configuration,
|
||||
IGanjoorRelatedPersonService personService
|
||||
)
|
||||
{
|
||||
_ganjoorService = ganjoorService;
|
||||
@ -5116,6 +5146,7 @@ namespace RMuseum.Controllers
|
||||
_imageFileService = imageFileService;
|
||||
_memoryCache = memoryCache;
|
||||
Configuration = configuration;
|
||||
_personService = personService;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -44,6 +44,15 @@ namespace RMuseum.Models.Ganjoor.ViewModels
|
||||
/// to their /FamilyTree/{id} strict-tree view
|
||||
/// </summary>
|
||||
public bool HasFamilyTree { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// true unless this node was pulled in only because it's one hop away (a relative or
|
||||
/// affiliate) from a person actually tagged in a work's verses - see
|
||||
/// GetCatPersonGraphAsync. Always true for the whole-site graph (GetPersonGraphAsync),
|
||||
/// where there's no "work" to be tagged within. Client-side, a false value draws the node
|
||||
/// as secondary (e.g. dashed/dimmer) since it's context the reader wasn't shown directly.
|
||||
/// </summary>
|
||||
public bool DirectlyTagged { get; set; } = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@ -2241,6 +2241,15 @@
|
||||
<param name="id"></param>
|
||||
<returns></returns>
|
||||
</member>
|
||||
<member name="M:RMuseum.Controllers.GanjoorController.GetCatPersonGraphAsync(System.Int32)">
|
||||
<summary>
|
||||
get the network of people relevant to this category/work (every person tagged in a poem
|
||||
under its subtree, plus their relatives/affiliates one hop out) - for the "شخصیتها" tab
|
||||
on a category/poet page
|
||||
</summary>
|
||||
<param name="id"></param>
|
||||
<returns></returns>
|
||||
</member>
|
||||
<member name="M:RMuseum.Controllers.GanjoorController.SynchronizeNaskbanLinks(RSecurityBackend.Models.Auth.ViewModels.LoginViewModel)">
|
||||
<summary>
|
||||
synchronize https:://naskban.ir links (logs in and then out to naskban.ir using auth info)
|
||||
@ -2684,7 +2693,14 @@
|
||||
Configuration
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:RMuseum.Controllers.GanjoorController.#ctor(RMuseum.Services.IGanjoorService,RSecurityBackend.Services.IAppUserService,Microsoft.AspNetCore.Http.IHttpContextAccessor,RSecurityBackend.Services.IImageFileService,Microsoft.Extensions.Caching.Memory.IMemoryCache,Microsoft.Extensions.Configuration.IConfiguration)">
|
||||
<member name="F:RMuseum.Controllers.GanjoorController._personService">
|
||||
<summary>
|
||||
related people (family tree / person tagging) service - used here only for the
|
||||
category-scoped "cat/{id}/persongraph" endpoint; everything else about people lives in
|
||||
GanjoorRelatedPersonController
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:RMuseum.Controllers.GanjoorController.#ctor(RMuseum.Services.IGanjoorService,RSecurityBackend.Services.IAppUserService,Microsoft.AspNetCore.Http.IHttpContextAccessor,RSecurityBackend.Services.IImageFileService,Microsoft.Extensions.Caching.Memory.IMemoryCache,Microsoft.Extensions.Configuration.IConfiguration,RMuseum.Services.IGanjoorRelatedPersonService)">
|
||||
<summary>
|
||||
constructor
|
||||
</summary>
|
||||
@ -2694,6 +2710,7 @@
|
||||
<param name="imageFileService"></param>
|
||||
<param name="memoryCache"></param>
|
||||
<param name="configuration"></param>
|
||||
<param name="personService"></param>
|
||||
</member>
|
||||
<member name="M:RMuseum.Controllers.GanjoorPoetSuggestedPhotosController.GetPoetSuggestedPhotosAsync(System.Int32)">
|
||||
<summary>
|
||||
@ -13361,6 +13378,15 @@
|
||||
to their /FamilyTree/{id} strict-tree view
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:RMuseum.Models.Ganjoor.ViewModels.GanjoorPersonGraphNode.DirectlyTagged">
|
||||
<summary>
|
||||
true unless this node was pulled in only because it's one hop away (a relative or
|
||||
affiliate) from a person actually tagged in a work's verses - see
|
||||
GetCatPersonGraphAsync. Always true for the whole-site graph (GetPersonGraphAsync),
|
||||
where there's no "work" to be tagged within. Client-side, a false value draws the node
|
||||
as secondary (e.g. dashed/dimmer) since it's context the reader wasn't shown directly.
|
||||
</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
|
||||
@ -18385,6 +18411,16 @@
|
||||
</summary>
|
||||
<returns></returns>
|
||||
</member>
|
||||
<member name="M:RMuseum.Services.IGanjoorRelatedPersonService.GetCatPersonGraphAsync(System.Int32)">
|
||||
<summary>
|
||||
get the network of people relevant to one work/category (a poet's whole corpus, one book
|
||||
like the Shahnameh, or a narrower story within it) - every person tagged in a poem under
|
||||
catId's subtree, plus their relatives/affiliates one hop out even if never tagged
|
||||
themselves - see GanjoorPersonGraphNode.DirectlyTagged
|
||||
</summary>
|
||||
<param name="catId"></param>
|
||||
<returns></returns>
|
||||
</member>
|
||||
<member name="T:RMuseum.Services.IGanjoorService">
|
||||
<summary>
|
||||
Ganjoor Poems Content Privider Service
|
||||
@ -21943,6 +21979,43 @@
|
||||
</summary>
|
||||
<returns></returns>
|
||||
</member>
|
||||
<member name="M:RMuseum.Services.Implementation.GanjoorRelatedPersonService._GetCategorySubtreeIdsAsync(System.Int32)">
|
||||
<summary>
|
||||
get every category id in the subtree rooted at catId (catId itself plus every descendant,
|
||||
walked breadth-first) - a lighter-weight, purpose-built counterpart of
|
||||
GanjoorService._populateCategoryChildren (which builds full category objects via
|
||||
_GetCatById); this only needs ids, so it queries them directly
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:RMuseum.Services.Implementation.GanjoorRelatedPersonService.GetCatPersonGraphAsync(System.Int32)">
|
||||
<summary>
|
||||
get the network of people relevant to one work/category (e.g. a poet's Shahnameh, or one
|
||||
story within it like Nezami's Leyli o Majnoon) - the category-scoped counterpart of
|
||||
GetPersonGraphAsync, for the "شخصیتها" tab on a category/poet page. Starts from every
|
||||
person directly tagged (PoemGeoDateTag.PersonId) in a poem under catId's subtree, then
|
||||
adds their relatives/affiliates one hop out even when
|
||||
those relatives are never tagged in the work themselves - so, say, a hero's father still
|
||||
shows up if he's known but never named in a verse, which helps a reader unfamiliar with
|
||||
the story rather than leaving the tree looking broken. Those one-hop additions are marked
|
||||
DirectlyTagged = false so the client can draw them as secondary.
|
||||
</summary>
|
||||
<param name="catId"></param>
|
||||
<returns></returns>
|
||||
</member>
|
||||
<member name="M:RMuseum.Services.Implementation.GanjoorRelatedPersonService._BuildGraphNodes(System.Collections.Generic.List{RMuseum.Models.Ganjoor.GanjoorRelatedPerson},System.Collections.Generic.HashSet{System.Int32})">
|
||||
<summary>
|
||||
shared node-list builder for GetPersonGraphAsync/GetCatPersonGraphAsync - directlyTaggedIds
|
||||
null means "everyone counts as directly part of the graph" (the whole-site graph has no
|
||||
notion of one-hop additions)
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:RMuseum.Services.Implementation.GanjoorRelatedPersonService._BuildGraphEdges(System.Collections.Generic.List{RMuseum.Models.Ganjoor.GanjoorPersonRelation},System.Collections.Generic.List{RMuseum.Models.Ganjoor.GanjoorPersonAffiliation},System.Collections.Generic.Dictionary{System.Int32,RMuseum.Models.Ganjoor.GanjoorRelatedPerson})">
|
||||
<summary>
|
||||
shared edge-list builder for GetPersonGraphAsync/GetCatPersonGraphAsync - flattens
|
||||
GanjoorPersonRelation and GanjoorPersonAffiliation rows into the common
|
||||
GanjoorPersonGraphEdge shape, resolving both sides' names from personById
|
||||
</summary>
|
||||
</member>
|
||||
<member name="F:RMuseum.Services.Implementation.GanjoorRelatedPersonService._context">
|
||||
<summary>
|
||||
Database Context
|
||||
|
||||
@ -146,5 +146,15 @@ namespace RMuseum.Services
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
Task<RServiceResult<GanjoorPersonGraphViewModel>> GetPersonGraphAsync();
|
||||
|
||||
/// <summary>
|
||||
/// get the network of people relevant to one work/category (a poet's whole corpus, one book
|
||||
/// like the Shahnameh, or a narrower story within it) - every person tagged in a poem under
|
||||
/// catId's subtree, plus their relatives/affiliates one hop out even if never tagged
|
||||
/// themselves - see GanjoorPersonGraphNode.DirectlyTagged
|
||||
/// </summary>
|
||||
/// <param name="catId"></param>
|
||||
/// <returns></returns>
|
||||
Task<RServiceResult<GanjoorPersonGraphViewModel>> GetCatPersonGraphAsync(int catId);
|
||||
}
|
||||
}
|
||||
|
||||
@ -833,43 +833,10 @@ namespace RMuseum.Services.Implementation
|
||||
.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,
|
||||
});
|
||||
}
|
||||
// no "work" scope here, so every node is directly part of the graph - none of them
|
||||
// are a one-hop addition the way GetCatPersonGraphAsync's are
|
||||
var nodes = _BuildGraphNodes(persons, null);
|
||||
var edges = _BuildGraphEdges(relations, affiliations, personById);
|
||||
|
||||
return new RServiceResult<GanjoorPersonGraphViewModel>(new GanjoorPersonGraphViewModel()
|
||||
{
|
||||
@ -883,6 +850,181 @@ namespace RMuseum.Services.Implementation
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// get every category id in the subtree rooted at catId (catId itself plus every descendant,
|
||||
/// walked breadth-first) - a lighter-weight, purpose-built counterpart of
|
||||
/// GanjoorService._populateCategoryChildren (which builds full category objects via
|
||||
/// _GetCatById); this only needs ids, so it queries them directly
|
||||
/// </summary>
|
||||
private async Task<List<int>> _GetCategorySubtreeIdsAsync(int catId)
|
||||
{
|
||||
var result = new List<int> { catId };
|
||||
var queue = new Queue<int>();
|
||||
queue.Enqueue(catId);
|
||||
while (queue.Count > 0)
|
||||
{
|
||||
var current = queue.Dequeue();
|
||||
var childIds = await _context.GanjoorCategories
|
||||
.Where(c => c.ParentId == current)
|
||||
.Select(c => c.Id)
|
||||
.ToListAsync();
|
||||
foreach (var childId in childIds)
|
||||
{
|
||||
result.Add(childId);
|
||||
queue.Enqueue(childId);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// get the network of people relevant to one work/category (e.g. a poet's Shahnameh, or one
|
||||
/// story within it like Nezami's Leyli o Majnoon) - the category-scoped counterpart of
|
||||
/// GetPersonGraphAsync, for the "شخصیتها" tab on a category/poet page. Starts from every
|
||||
/// person directly tagged (PoemGeoDateTag.PersonId) in a poem under catId's subtree, then
|
||||
/// adds their relatives/affiliates one hop out even when
|
||||
/// those relatives are never tagged in the work themselves - so, say, a hero's father still
|
||||
/// shows up if he's known but never named in a verse, which helps a reader unfamiliar with
|
||||
/// the story rather than leaving the tree looking broken. Those one-hop additions are marked
|
||||
/// DirectlyTagged = false so the client can draw them as secondary.
|
||||
/// </summary>
|
||||
/// <param name="catId"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<RServiceResult<GanjoorPersonGraphViewModel>> GetCatPersonGraphAsync(int catId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var catExists = await _context.GanjoorCategories.Where(c => c.Id == catId).AnyAsync();
|
||||
if (!catExists)
|
||||
{
|
||||
return new RServiceResult<GanjoorPersonGraphViewModel>(null, "بخش پیدا نشد.");
|
||||
}
|
||||
|
||||
var catIds = await _GetCategorySubtreeIdsAsync(catId);
|
||||
|
||||
var directPersonIds = await _context.PoemGeoDateTags
|
||||
.Where(t => t.MachineGenerated == false && t.PersonId != null && catIds.Contains(t.Poem.CatId))
|
||||
.Select(t => t.PersonId.Value)
|
||||
.Distinct()
|
||||
.ToListAsync();
|
||||
|
||||
if (directPersonIds.Count == 0)
|
||||
{
|
||||
return new RServiceResult<GanjoorPersonGraphViewModel>(new GanjoorPersonGraphViewModel()
|
||||
{
|
||||
Nodes = new List<GanjoorPersonGraphNode>(),
|
||||
Edges = new List<GanjoorPersonGraphEdge>(),
|
||||
});
|
||||
}
|
||||
|
||||
var directIdSet = new HashSet<int>(directPersonIds);
|
||||
|
||||
var oneHopRelations = await _context.GanjoorPersonRelations
|
||||
.Where(r => directIdSet.Contains(r.Person1Id) || directIdSet.Contains(r.Person2Id))
|
||||
.ToListAsync();
|
||||
var oneHopAffiliations = await _context.GanjoorPersonAffiliations
|
||||
.Where(a => directIdSet.Contains(a.Person1Id) || directIdSet.Contains(a.Person2Id))
|
||||
.ToListAsync();
|
||||
|
||||
var allIdSet = new HashSet<int>(directIdSet);
|
||||
foreach (var r in oneHopRelations)
|
||||
{
|
||||
allIdSet.Add(r.Person1Id);
|
||||
allIdSet.Add(r.Person2Id);
|
||||
}
|
||||
foreach (var a in oneHopAffiliations)
|
||||
{
|
||||
allIdSet.Add(a.Person1Id);
|
||||
allIdSet.Add(a.Person2Id);
|
||||
}
|
||||
|
||||
// re-query rather than reuse oneHopRelations/oneHopAffiliations, so an edge between
|
||||
// two one-hop additions (not just their link back to a directly-tagged person) is
|
||||
// also included, now that the final node set is known
|
||||
var relations = await _context.GanjoorPersonRelations
|
||||
.Where(r => allIdSet.Contains(r.Person1Id) && allIdSet.Contains(r.Person2Id))
|
||||
.ToListAsync();
|
||||
var affiliations = await _context.GanjoorPersonAffiliations
|
||||
.Where(a => allIdSet.Contains(a.Person1Id) && allIdSet.Contains(a.Person2Id))
|
||||
.ToListAsync();
|
||||
|
||||
var persons = await _context.GanjoorRelatedPersons
|
||||
.Where(p => allIdSet.Contains(p.Id))
|
||||
.ToListAsync();
|
||||
var personById = persons.ToDictionary(p => p.Id);
|
||||
|
||||
var nodes = _BuildGraphNodes(persons, directIdSet);
|
||||
var edges = _BuildGraphEdges(relations, affiliations, personById);
|
||||
|
||||
return new RServiceResult<GanjoorPersonGraphViewModel>(new GanjoorPersonGraphViewModel()
|
||||
{
|
||||
Nodes = nodes,
|
||||
Edges = edges,
|
||||
});
|
||||
}
|
||||
catch (Exception exp)
|
||||
{
|
||||
return new RServiceResult<GanjoorPersonGraphViewModel>(null, exp.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// shared node-list builder for GetPersonGraphAsync/GetCatPersonGraphAsync - directlyTaggedIds
|
||||
/// null means "everyone counts as directly part of the graph" (the whole-site graph has no
|
||||
/// notion of one-hop additions)
|
||||
/// </summary>
|
||||
private static List<GanjoorPersonGraphNode> _BuildGraphNodes(List<GanjoorRelatedPerson> persons, HashSet<int> directlyTaggedIds)
|
||||
{
|
||||
return persons.Select(p => new GanjoorPersonGraphNode()
|
||||
{
|
||||
Id = p.Id,
|
||||
Name = p.Name,
|
||||
HasFamilyTree = !string.IsNullOrEmpty(p.FamilyTreeCaption),
|
||||
DirectlyTagged = directlyTaggedIds == null || directlyTaggedIds.Contains(p.Id),
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// shared edge-list builder for GetPersonGraphAsync/GetCatPersonGraphAsync - flattens
|
||||
/// GanjoorPersonRelation and GanjoorPersonAffiliation rows into the common
|
||||
/// GanjoorPersonGraphEdge shape, resolving both sides' names from personById
|
||||
/// </summary>
|
||||
private static List<GanjoorPersonGraphEdge> _BuildGraphEdges(List<GanjoorPersonRelation> relations, List<GanjoorPersonAffiliation> affiliations, Dictionary<int, GanjoorRelatedPerson> personById)
|
||||
{
|
||||
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 edges;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Database Context
|
||||
/// </summary>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user