#421 geo tag suggestion + typo fixes in editors
This commit is contained in:
parent
aaacfe0442
commit
fc241748e7
@ -121,6 +121,9 @@
|
||||
// GanjoorPoemGeoDateTagCorrection's shape so it can be sent as-is
|
||||
var geoDateTagSuggestions = @Html.Raw(Model.MyLastEditGeoDateTagsJson);
|
||||
var geoDateTagCoupletTexts = @Html.Raw(Newtonsoft.Json.JsonConvert.SerializeObject(Model.Couplets.ToDictionary(c => c.Item1.ToString(), c => c.Item2)));
|
||||
// full existing-location catalog ({id, name, latitude, longitude}[]), used to warn about a
|
||||
// new-location suggestion that's actually already catalogued - see _confirmNewLocationSuggestion
|
||||
var allGeoLocations = @Html.Raw(Model.AllLocationsJson);
|
||||
$(document).ready(function () {
|
||||
renderPendingGeoDateTags();
|
||||
});
|
||||
@ -197,6 +200,63 @@
|
||||
geoDateTagMap.setView([lat, lng], 12);
|
||||
}
|
||||
|
||||
// great-circle distance in km - just for catching obviously-duplicate locations, not
|
||||
// geodesy-grade precision
|
||||
function _geoDistanceKm(lat1, lng1, lat2, lng2) {
|
||||
var R = 6371;
|
||||
var dLat = (lat2 - lat1) * Math.PI / 180;
|
||||
var dLng = (lng2 - lng1) * Math.PI / 180;
|
||||
var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
||||
Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
|
||||
Math.sin(dLng / 2) * Math.sin(dLng / 2);
|
||||
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
}
|
||||
|
||||
// For a brand-new location suggestion, checks it against the full existing-location catalog
|
||||
// (allGeoLocations) before it's staged. Two cases:
|
||||
// - near-identical coordinates to an existing location: almost certainly the same real place,
|
||||
// typed in again instead of picked from the dropdown - blocked outright, no point asking.
|
||||
// - same name as an existing location but meaningfully different coordinates: could be a real
|
||||
// second place that happens to share a name, or could be a coordinate mistake - asks the
|
||||
// user to explicitly confirm rather than guessing either way.
|
||||
// Returns true if the tag should be added as-is.
|
||||
async function _confirmNewLocationSuggestion(tag) {
|
||||
if (!tag || tag.locationId != null || !tag.suggestedLocationName) {
|
||||
return true; // an existing/selected location, or no location at all - nothing to check
|
||||
}
|
||||
|
||||
var NEAR_DUPLICATE_KM = 5; // close enough that it's almost certainly the same real place
|
||||
var trimmedName = tag.suggestedLocationName.trim();
|
||||
|
||||
for (var i = 0; i < allGeoLocations.length; i++) {
|
||||
var existing = allGeoLocations[i];
|
||||
var distanceKm = _geoDistanceKm(tag.suggestedLatitude, tag.suggestedLongitude, existing.latitude, existing.longitude);
|
||||
if (distanceKm <= NEAR_DUPLICATE_KM) {
|
||||
upToast('این مکان با نام «' + existing.name + '» و مختصات بسیار نزدیک از قبل ثبت شده است. لطفاً بهجای پیشنهاد مکان جدید، همان را از فهرست «مکان» انتخاب کنید.', 'error');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
var sameNameOtherPlaces = allGeoLocations.filter(function (existing) {
|
||||
return existing.name && existing.name.trim().toLowerCase() === trimmedName.toLowerCase();
|
||||
});
|
||||
if (sameNameOtherPlaces.length > 0) {
|
||||
var distances = sameNameOtherPlaces.map(function (existing) {
|
||||
return Math.round(_geoDistanceKm(tag.suggestedLatitude, tag.suggestedLongitude, existing.latitude, existing.longitude));
|
||||
});
|
||||
var ok = await upConfirm(
|
||||
'مکانی با نام «' + trimmedName + '» از قبل با مختصات دیگری در فهرست ثبت شده است (فاصله حدود ' + distances.join('، ') + ' کیلومتر). ' +
|
||||
'آیا مطمئنید این مکان دیگری با همین نام است و اشتباهی در مختصات پیش نیامده؟',
|
||||
{ okText: 'بله، مکان دیگری است', cancelText: 'انصراف' }
|
||||
);
|
||||
if (!ok) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function renderPendingGeoDateTags() {
|
||||
var container = document.getElementById('pending-geotag-suggestions');
|
||||
if (geoDateTagSuggestions.length == 0) {
|
||||
@ -297,10 +357,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
function addGeoDateTagSuggestion() {
|
||||
async function addGeoDateTagSuggestion() {
|
||||
var tag = readGeoDateTagFromForm(true);
|
||||
if (tag == null) return;
|
||||
|
||||
var proceed = await _confirmNewLocationSuggestion(tag);
|
||||
if (!proceed) return;
|
||||
|
||||
geoDateTagSuggestions.push(tag);
|
||||
resetGeoDateTagForm();
|
||||
|
||||
@ -308,7 +371,7 @@
|
||||
upToast('به فهرست پیشنهادها افزوده شد. برای ثبت نهایی، دکمهٔ «ذخیره» را بزنید.');
|
||||
}
|
||||
|
||||
function savePoemCorrections(id, verseCount) {
|
||||
async function savePoemCorrections(id, verseCount) {
|
||||
var verseOrderText = [];
|
||||
var verseOrderMarkedForDelete = [];
|
||||
var versePositions = [];
|
||||
@ -370,6 +433,12 @@
|
||||
// button is only needed to stage more than one tag before saving
|
||||
var unstagedGeoDateTag = readGeoDateTagFromForm(false);
|
||||
if (unstagedGeoDateTag != null) {
|
||||
var proceedWithUnstaged = await _confirmNewLocationSuggestion(unstagedGeoDateTag);
|
||||
if (!proceedWithUnstaged) {
|
||||
// let them fix or clear the "add a new tag" form instead of silently dropping
|
||||
// it or saving everything else without it
|
||||
return;
|
||||
}
|
||||
geoDateTagSuggestions.push(unstagedGeoDateTag);
|
||||
resetGeoDateTagForm();
|
||||
renderPendingGeoDateTags();
|
||||
@ -1441,12 +1510,12 @@ else
|
||||
</div>
|
||||
|
||||
<div class="up-field">
|
||||
<label>حروف قافیهٔ فعلی</label>
|
||||
<label>حروف قافیه و ردیفِ فعلی</label>
|
||||
<p id="rhyme-letters-old" class="up-field-static">@Model.RhymeLetters</p>
|
||||
</div>
|
||||
|
||||
<div class="up-field">
|
||||
<label for="rhyme-letters"><span class="up-field-icon" aria-hidden="true">🔤</span> حروف قافیهٔ ویراستهٔ من (بدون فاصله و حرکات)</label>
|
||||
<label for="rhyme-letters"><span class="up-field-icon" aria-hidden="true">🔤</span> حروف قافیه و ردیفِ ویراستهٔ من (بدون فاصله و حرکات)</label>
|
||||
<div class="up-btn-group up-mt-0">
|
||||
@if (Model.MyLastEdit != null && !string.IsNullOrEmpty(Model.MyLastEdit.RhymeLetters))
|
||||
{
|
||||
@ -1458,6 +1527,11 @@ else
|
||||
}
|
||||
<a role="button" onclick="computePoemRhyme(@Model.PageInformation.Id, '#rhyme-letters')" class="up-btn up-btn--ghost">🧮 محاسبه</a>
|
||||
</div>
|
||||
<p class="up-hint">
|
||||
این فیلد قافیه و ردیف را با هم و بدون فاصله میخواهد، نه فقط قافیه. اگر بعد از حروف قافیه کلمه یا کلماتی عیناً در همهٔ ابیات تکرار شده باشند (ردیف)، آنها هم باید در انتها بیایند.
|
||||
برای نمونه در بیت «... اگر تو <b>یله کنی</b>» / «... اگر تو <b>گله کنی</b>» چون «کنی» عیناً در هر دو مصرع تکرار شده و ردیف شعر است، حروف قافیه و ردیف باید «<b>لهکنی</b>» نوشته شود؛ نه «له» (با نادیده گرفتن ردیف) و نه «ه کنی» (با فاصلهٔ اشتباه).
|
||||
همچنین از حرکات (فتحه، ضمه، کسره) یا هر نشانهٔ اضافهٔ دیگر در این فیلد استفاده نشود.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div id="secondr-btn">
|
||||
|
||||
@ -119,6 +119,19 @@ namespace GanjooRazor.Areas.User.Pages
|
||||
/// </summary>
|
||||
public List<GanjoorGeoLocation> Locations { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// camelCase JSON of Locations (id/name/latitude/longitude only), used client-side to warn
|
||||
/// about a new-location suggestion that's actually already in the catalog (same/near
|
||||
/// coordinates) or shares a name with a different, already-catalogued place. Pre-serialized
|
||||
/// here rather than inline in the .cshtml script block for the same reason as
|
||||
/// MyLastEditGeoDateTagsJson - avoids embedding a C# object initializer inside a JS statement.
|
||||
/// </summary>
|
||||
public string AllLocationsJson =>
|
||||
JsonConvert.SerializeObject(
|
||||
(Locations ?? new List<GanjoorGeoLocation>()).Select(l => new { l.Id, l.Name, l.Latitude, l.Longitude }),
|
||||
new JsonSerializerSettings { ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver() }
|
||||
);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// poem geo date tags
|
||||
|
||||
@ -237,12 +237,12 @@ else
|
||||
<h2 class="up-card__title">قافیه</h2>
|
||||
|
||||
<div class="up-field">
|
||||
<label><span class="up-field-icon" aria-hidden="true">🔤</span> حروف قافیهٔ فعلی</label>
|
||||
<label><span class="up-field-icon" aria-hidden="true">🔤</span> حروف قافیه و ردیفِ فعلی</label>
|
||||
<p id="rhyme-letters-old" class="up-field-static">@Model.PoemSection.RhymeLetters</p>
|
||||
</div>
|
||||
|
||||
<div class="up-field">
|
||||
<label for="rhyme-letters"><span class="up-field-icon" aria-hidden="true">🔤</span> حروف قافیهٔ پیشنهادی من (بدون فاصله و حرکات)</label>
|
||||
<label for="rhyme-letters"><span class="up-field-icon" aria-hidden="true">🔤</span> حروف قافیه و ردیفِ پیشنهادی من (بدون فاصله و حرکات)</label>
|
||||
<div class="up-btn-group up-mt-0">
|
||||
@if (Model.MyLastEdit != null && !string.IsNullOrEmpty(Model.MyLastEdit.RhymeLetters))
|
||||
{
|
||||
@ -254,6 +254,11 @@ else
|
||||
}
|
||||
<a role="button" onclick="computeSectionRhyme(@Model.PoemSection.Id, '#rhyme-letters')" class="up-btn up-btn--ghost">🧮 محاسبه</a>
|
||||
</div>
|
||||
<p class="up-hint">
|
||||
این فیلد قافیه و ردیف را با هم و بدون فاصله میخواهد، نه فقط قافیه. اگر بعد از حروف قافیه کلمه یا کلماتی عیناً در همهٔ ابیات تکرار شده باشند (ردیف)، آنها هم باید در انتها بیایند.
|
||||
برای نمونه در بیت «... اگر تو <b>یله کنی</b>» / «... اگر تو <b>گله کنی</b>» چون «کنی» عیناً در هر دو مصرع تکرار شده و ردیف شعر است، حروف قافیه و ردیف باید «<b>لهکنی</b>» نوشته شود؛ نه «له» (با نادیده گرفتن ردیف) و نه «ه کنی» (با فاصلهٔ اشتباه).
|
||||
همچنین از حرکات (فتحه، ضمه، کسره) یا هر نشانهٔ اضافهٔ دیگر در این فیلد استفاده نشود.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -21394,6 +21394,14 @@
|
||||
<param name="id"></param>
|
||||
<returns></returns>
|
||||
</member>
|
||||
<member name="M:RMuseum.Services.Implementation.GanjoorService._GeoDistanceKm(System.Double,System.Double,System.Double,System.Double)">
|
||||
<summary>
|
||||
great-circle distance between two points, in kilometers - used only for catching
|
||||
obviously-duplicate new-location suggestions (someone retyping a place that's already
|
||||
in the catalog under slightly different coordinates), so this doesn't need to be
|
||||
geodesy-grade precise, just good enough at the few-kilometers scale
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:RMuseum.Services.Implementation.GanjoorService.AddPoemGeoDateTagAsync(RMuseum.Models.Ganjoor.PoemGeoDateTag)">
|
||||
<summary>
|
||||
add poem geo tag
|
||||
|
||||
@ -15,6 +15,23 @@ namespace RMuseum.Services.Implementation
|
||||
public partial class GanjoorService : IGanjoorService
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// great-circle distance between two points, in kilometers - used only for catching
|
||||
/// obviously-duplicate new-location suggestions (someone retyping a place that's already
|
||||
/// in the catalog under slightly different coordinates), so this doesn't need to be
|
||||
/// geodesy-grade precise, just good enough at the few-kilometers scale
|
||||
/// </summary>
|
||||
private double _GeoDistanceKm(double lat1, double lng1, double lat2, double lng2)
|
||||
{
|
||||
const double earthRadiusKm = 6371.0;
|
||||
double dLat = (lat2 - lat1) * Math.PI / 180.0;
|
||||
double dLng = (lng2 - lng1) * Math.PI / 180.0;
|
||||
double a = Math.Sin(dLat / 2) * Math.Sin(dLat / 2) +
|
||||
Math.Cos(lat1 * Math.PI / 180.0) * Math.Cos(lat2 * Math.PI / 180.0) *
|
||||
Math.Sin(dLng / 2) * Math.Sin(dLng / 2);
|
||||
return earthRadiusKm * 2 * Math.Atan2(Math.Sqrt(a), Math.Sqrt(1 - a));
|
||||
}
|
||||
|
||||
private int? _PrepareLunarDateTotalNumber(PoemGeoDateTag tag)
|
||||
{
|
||||
if (tag.LunarYear == null)
|
||||
|
||||
@ -2107,6 +2107,11 @@ namespace RMuseum.Services.Implementation
|
||||
// GetPoemById above always loads this
|
||||
var approvedGeoDateTags = poem.GeoDateTags ?? Array.Empty<PoemGeoDateTag>();
|
||||
|
||||
// lazily loaded only if a tag actually suggests a brand new location - most
|
||||
// submissions either pick an existing location or only carry a date, so this
|
||||
// extra query is skipped entirely in the common case
|
||||
GanjoorGeoLocation[] allLocations = null;
|
||||
|
||||
foreach (var geoDateTag in correction.GeoDateTags)
|
||||
{
|
||||
if (!geoDateTag.MarkForDelete)
|
||||
@ -2119,6 +2124,29 @@ namespace RMuseum.Services.Implementation
|
||||
return new RServiceResult<GanjoorPoemCorrectionViewModel>(null, "برچسب جغرافیایی/تاریخی باید حداقل شامل مکان یا تاریخ باشد.");
|
||||
}
|
||||
|
||||
if (geoDateTag.LocationId == null && !string.IsNullOrWhiteSpace(geoDateTag.SuggestedLocationName)
|
||||
&& geoDateTag.SuggestedLatitude != null && geoDateTag.SuggestedLongitude != null)
|
||||
{
|
||||
// catches "this is already a catalogued place" regardless of which couplet/poem
|
||||
// it was previously tagged on - a hard reject, since coordinates a few kilometers
|
||||
// apart are effectively certainly the same real place, not a judgment call
|
||||
const double nearDuplicateKm = 5.0;
|
||||
if (allLocations == null)
|
||||
{
|
||||
allLocations = await _context.GanjoorGeoLocations.AsNoTracking().ToArrayAsync();
|
||||
}
|
||||
var nearExistingLocation = allLocations.FirstOrDefault(l =>
|
||||
_GeoDistanceKm((double)geoDateTag.SuggestedLatitude, (double)geoDateTag.SuggestedLongitude, l.Latitude, l.Longitude) <= nearDuplicateKm);
|
||||
if (nearExistingLocation != null)
|
||||
{
|
||||
return new RServiceResult<GanjoorPoemCorrectionViewModel>(null,
|
||||
$"مکانی با نام «{nearExistingLocation.Name}» با مختصات بسیار نزدیک از قبل در فهرست مکانها ثبت شده است. لطفاً بهجای پیشنهاد مکان جدید، همان را از فهرست «مکان» انتخاب کنید.");
|
||||
}
|
||||
// a same-name-but-far-away match is deliberately NOT rejected here - it can
|
||||
// legitimately be a different real place sharing a name, and the editor's
|
||||
// client-side check already asks the user to confirm that before it gets here
|
||||
}
|
||||
|
||||
// PoemGeoDateTag.CoupletIndex uses 0 for "whole poem", matching a null CoupletIndex here
|
||||
int effectiveCoupletIndex = geoDateTag.CoupletIndex ?? 0;
|
||||
var approvedForCouplet = approvedGeoDateTags.Where(t => t.CoupletIndex == effectiveCoupletIndex);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user