diff --git a/GanjooRazor/Areas/User/Pages/Editor.cshtml b/GanjooRazor/Areas/User/Pages/Editor.cshtml
index f3b87bfd..78a0816a 100644
--- a/GanjooRazor/Areas/User/Pages/Editor.cshtml
+++ b/GanjooRazor/Areas/User/Pages/Editor.cshtml
@@ -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
+ این فیلد قافیه و ردیف را با هم و بدون فاصله میخواهد، نه فقط قافیه. اگر بعد از حروف قافیه کلمه یا کلماتی عیناً در همهٔ ابیات تکرار شده باشند (ردیف)، آنها هم باید در انتها بیایند.
+ برای نمونه در بیت «... اگر تو یله کنی» / «... اگر تو گله کنی» چون «کنی» عیناً در هر دو مصرع تکرار شده و ردیف شعر است، حروف قافیه و ردیف باید «لهکنی» نوشته شود؛ نه «له» (با نادیده گرفتن ردیف) و نه «ه کنی» (با فاصلهٔ اشتباه).
+ همچنین از حرکات (فتحه، ضمه، کسره) یا هر نشانهٔ اضافهٔ دیگر در این فیلد استفاده نشود.
+
diff --git a/GanjooRazor/Areas/User/Pages/Editor.cshtml.cs b/GanjooRazor/Areas/User/Pages/Editor.cshtml.cs
index 07d4c576..c83d2d2c 100644
--- a/GanjooRazor/Areas/User/Pages/Editor.cshtml.cs
+++ b/GanjooRazor/Areas/User/Pages/Editor.cshtml.cs
@@ -119,6 +119,19 @@ namespace GanjooRazor.Areas.User.Pages
///
public List Locations { get; set; }
+ ///
+ /// 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.
+ ///
+ public string AllLocationsJson =>
+ JsonConvert.SerializeObject(
+ (Locations ?? new List()).Select(l => new { l.Id, l.Name, l.Latitude, l.Longitude }),
+ new JsonSerializerSettings { ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver() }
+ );
+
///
/// poem geo date tags
diff --git a/GanjooRazor/Areas/User/Pages/Section.cshtml b/GanjooRazor/Areas/User/Pages/Section.cshtml
index a4833686..24d8f2f5 100644
--- a/GanjooRazor/Areas/User/Pages/Section.cshtml
+++ b/GanjooRazor/Areas/User/Pages/Section.cshtml
@@ -237,12 +237,12 @@ else
+ این فیلد قافیه و ردیف را با هم و بدون فاصله میخواهد، نه فقط قافیه. اگر بعد از حروف قافیه کلمه یا کلماتی عیناً در همهٔ ابیات تکرار شده باشند (ردیف)، آنها هم باید در انتها بیایند.
+ برای نمونه در بیت «... اگر تو یله کنی» / «... اگر تو گله کنی» چون «کنی» عیناً در هر دو مصرع تکرار شده و ردیف شعر است، حروف قافیه و ردیف باید «لهکنی» نوشته شود؛ نه «له» (با نادیده گرفتن ردیف) و نه «ه کنی» (با فاصلهٔ اشتباه).
+ همچنین از حرکات (فتحه، ضمه، کسره) یا هر نشانهٔ اضافهٔ دیگر در این فیلد استفاده نشود.
+
diff --git a/RMuseum/RMuseum.xml b/RMuseum/RMuseum.xml
index e255bfe6..167762c1 100644
--- a/RMuseum/RMuseum.xml
+++ b/RMuseum/RMuseum.xml
@@ -21394,6 +21394,14 @@
+
+
+ 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
+
+
add poem geo tag
diff --git a/RMuseum/Services/Implementation/GanjoorService-Partials/GanjoorService-PoemGeoDateTag.cs b/RMuseum/Services/Implementation/GanjoorService-Partials/GanjoorService-PoemGeoDateTag.cs
index b66e3e67..c3481a9d 100644
--- a/RMuseum/Services/Implementation/GanjoorService-Partials/GanjoorService-PoemGeoDateTag.cs
+++ b/RMuseum/Services/Implementation/GanjoorService-Partials/GanjoorService-PoemGeoDateTag.cs
@@ -15,6 +15,23 @@ namespace RMuseum.Services.Implementation
public partial class GanjoorService : IGanjoorService
{
+ ///
+ /// 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
+ ///
+ 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)
diff --git a/RMuseum/Services/Implementation/GanjoorService.cs b/RMuseum/Services/Implementation/GanjoorService.cs
index b83ab0ff..5ebf182a 100644
--- a/RMuseum/Services/Implementation/GanjoorService.cs
+++ b/RMuseum/Services/Implementation/GanjoorService.cs
@@ -2107,6 +2107,11 @@ namespace RMuseum.Services.Implementation
// GetPoemById above always loads this
var approvedGeoDateTags = poem.GeoDateTags ?? Array.Empty();
+ // 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(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(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);