diff --git a/iitc_plugin_fanfields2.meta.js b/iitc_plugin_fanfields2.meta.js
index e23ad66..209a123 100644
--- a/iitc_plugin_fanfields2.meta.js
+++ b/iitc_plugin_fanfields2.meta.js
@@ -3,7 +3,7 @@
// @id fanfields@heistergand
// @name Fan Fields 2
// @category Layer
-// @version 2.7.8.20260218
+// @version 2.8.0.20260219
// @description Calculate how to link the portals to create the largest tidy set of nested fields. Enable from the layer chooser.
// @downloadURL https://github.com/Heistergand/fanfields2/raw/master/iitc_plugin_fanfields2.user.js
// @updateURL https://github.com/Heistergand/fanfields2/raw/master/iitc_plugin_fanfields2.meta.js
diff --git a/iitc_plugin_fanfields2.user.js b/iitc_plugin_fanfields2.user.js
index 0e66877..f01e515 100644
--- a/iitc_plugin_fanfields2.user.js
+++ b/iitc_plugin_fanfields2.user.js
@@ -3,7 +3,7 @@
// @id fanfields@heistergand
// @name Fan Fields 2
// @category Layer
-// @version 2.7.8.20260218
+// @version 2.8.0.20260219
// @description Calculate how to link the portals to create the largest tidy set of nested fields. Enable from the layer chooser.
// @downloadURL https://github.com/Heistergand/fanfields2/raw/master/iitc_plugin_fanfields2.user.js
// @updateURL https://github.com/Heistergand/fanfields2/raw/master/iitc_plugin_fanfields2.meta.js
@@ -25,18 +25,25 @@ function wrapper(plugin_info) {
// ensure plugin framework is there, even if iitc is not yet loaded
if (typeof window.plugin !== 'function') window.plugin = function () {};
plugin_info.buildName = 'main';
- plugin_info.dateTimeVersion = '2026-02-18-000942';
+ plugin_info.dateTimeVersion = '2026-02-18-004642';
plugin_info.pluginId = 'fanfields';
/* global L, $, dialog, map, portals, links, plugin, formatDistance -- eslint*/
/* exported setup, changelog -- eslint */
var arcname = (window.PLAYER && window.PLAYER.team === 'ENLIGHTENED') ? 'Arc' : '***';
- var changelog = [
- {
+ var changelog = [{
+ version: '2.8.0',
+ changes: [
+ 'NEW: Add user warning when trying to link impossible lengths from underneath a field.',
+ 'IMPROVE print design',
+ 'FIX: adjust label position to avoid overlapping with portal names',
+ 'FIX: minor code cleanup.',
+ ],
+ },{
version: '2.7.8',
changes: [
- 'NEW: Respect Intel now supports filtering which factions\' links are treated as blockers (Issue #104).'
+ 'NEW: Respect Intel now supports filtering which factions\' links are treated as blockers (Issue #104).',
],
},
{
@@ -44,7 +51,7 @@ function wrapper(plugin_info) {
changes: [
'IMPROVE portal sequence editor usage for mobile',
'UPDATE help dialog content',
- 'IMPROVE task list design'
+ 'IMPROVE task list design',
],
},
{
@@ -428,6 +435,7 @@ function wrapper(plugin_info) {
thisplugin.LABEL_WIDTH = 100;
thisplugin.LABEL_HEIGHT = 49;
+ thisplugin.LABEL_PADDING_TOP = 27;
// constants no longer present in leaflet 1.6.0
thisplugin.DEG_TO_RAD = Math.PI / 180;
@@ -497,17 +505,17 @@ function wrapper(plugin_info) {
// Add new folder in the localStorage
let folder_ID = window.plugin.bookmarks.generateID();
window.plugin.bookmarks.bkmrksObj.portals[folder_ID] = {
- "label": label,
- "state": 1,
- "bkmrk": {}
+ 'label': label,
+ 'state': 1,
+ 'bkmrk': {}
};
window.plugin.bookmarks.saveStorage();
window.plugin.bookmarks.refreshBkmrks();
window.runHooks('pluginBkmrksEdit', {
- "target": type,
- "action": "add",
- "id": folder_ID
+ 'target': type,
+ 'action': 'add',
+ 'id': folder_ID
});
console.log('Fanfields2: added BOOKMARKS ' + type + ' ' + folder_ID);
@@ -516,18 +524,18 @@ function wrapper(plugin_info) {
// Add bookmark in the localStorage
window.plugin.bookmarks.bkmrksObj.portals[folder_ID].bkmrk[bookmark_ID] = {
- "guid": guid,
- "latlng": latlng,
- "label": label
+ 'guid': guid,
+ 'latlng': latlng,
+ 'label': label
};
window.plugin.bookmarks.saveStorage();
window.plugin.bookmarks.refreshBkmrks();
window.runHooks('pluginBkmrksEdit', {
- "target": "portal",
- "action": "add",
- "id": bookmark_ID,
- "guid": guid
+ 'target': 'portal',
+ 'action': 'add',
+ 'id': bookmark_ID,
+ 'guid': guid
});
console.log('Fanfields2: added BOOKMARKS portal ' + bookmark_ID);
}
@@ -640,16 +648,30 @@ function wrapper(plugin_info) {
thisplugin.showStatistics = function () {
- var text = "";
+ var text = '';
if (this.sortedFanpoints.length > 3) {
- text = "
| FanPortals: | " + (thisplugin.n - 1) + " |
" +
- "
| CenterKeys: | " + thisplugin.centerKeys + " |
" +
- "
| Total links / keys: | " + thisplugin.donelinks.length.toString() + " |
" +
- "
| Fields: | " + thisplugin.triangles.length.toString() + " |
" +
- "
| Build AP (links and fields): | " + (thisplugin.donelinks.length * 313 + thisplugin.triangles.length * 1250)
- .toString() + " |
" +
- //"
| Destroy AP (links and fields): | " + (thisplugin.sortedFanpoints.length*187 + thisplugin.triangles.length*750).toString() + " |
" +
- "
";
+ text = '';
+ var totalLinks = thisplugin.donelinks.length;
+ var validLinks = (thisplugin.validLinkCount !== undefined) ? thisplugin.validLinkCount : totalLinks;
+ var totalFields = thisplugin.triangles.length;
+ var validFields = (thisplugin.validTriangleCount !== undefined) ? thisplugin.validTriangleCount : totalFields;
+
+ var linksText = (validLinks !== totalLinks) ? (validLinks + ' / ' + totalLinks) : validLinks.toString();
+ var fieldsText = (validFields !== totalFields) ? (validFields + ' / ' + totalFields) : validFields.toString();
+
+ var warn = '';
+ if (validLinks !== totalLinks || validFields !== totalFields) {
+ warn = '| ⚠ Under-field invalid links excluded from counts |
';
+ }
+
+ text = '| FanPortals: | ' + (thisplugin.n - 1) + ' |
' +
+ '
| CenterKeys: | ' + thisplugin.centerKeys + ' |
' +
+ '
| Total links / keys: | ' + linksText + ' |
' +
+ '
| Fields: | ' + fieldsText + ' |
' +
+ '
| Build AP (links and fields): | ' + (validLinks * 313 + validFields * 1250).toString() + ' |
' +
+ warn +
+ '
';
+
var width = 400;
thisplugin.MaxDialogWidth = thisplugin.getMaxDialogWidth();
@@ -672,6 +694,9 @@ function wrapper(plugin_info) {
$.each(thisplugin.sortedFanpoints, function (index, portal) {
$.each(portal.outgoing, function (targetIndex, targetPortal) {
+ var meta = (portal.outgoingMeta && portal.outgoingMeta[targetPortal.guid]) ? portal.outgoingMeta[targetPortal.guid] : null;
+ if (meta && meta.invalidUnderField) return;
+
alatlng = map.unproject(portal.point, thisplugin.PROJECT_ZOOM);
blatlng = map.unproject(targetPortal.point, thisplugin.PROJECT_ZOOM);
layer = L.geodesicPolyline([alatlng, blatlng], window.plugin.drawTools.lineOptions);
@@ -689,6 +714,9 @@ function wrapper(plugin_info) {
var alatlng, blatlng, layer;
$.each(thisplugin.sortedFanpoints, function (index, portal) {
$.each(portal.outgoing, function (targetIndex, targetPortal) {
+
+ var meta = (portal.outgoingMeta && portal.outgoingMeta[targetPortal.guid]) ? portal.outgoingMeta[targetPortal.guid] : null;
+ if (meta && meta.invalidUnderField) return;
window.selectedPortal = portal.guid;
window.plugin.arcs.draw();
window.selectedPortal = targetPortal.guid;
@@ -711,17 +739,24 @@ function wrapper(plugin_info) {
// Show as list
thisplugin.exportText = function () {
- var text = "";
- let fieldSymbol = "▲";
+ var text = '';
+ let fieldSymbol = '▲';
- text += "| Pos. | ";
- text += "Action | ";
- text += "Portal Name | ";
- text += "Keys | ";
- text += "Links | ";
- text += "Fields | ";
+ text += 'Pos. | ';
+ text += 'Action | ';
+ text += 'Portal Name | ';
+ if (window.plugin.keys || window.plugin.LiveInventory) {
+ text += ''
+ + 'Keys (owned/needed)'
+ + 'Keys'
+ + ' | ';
+ } else {
+ text += 'Keys | ';
+ }
+ text += 'Links | ';
+ text += 'Fields | ';
- text += "
";
+ text += '';
var gmnav = 'http://maps.google.com/maps/dir/';
@@ -734,7 +769,7 @@ function wrapper(plugin_info) {
p = portal.portal;
// window.portals[portal.guid];
- let rawTitle = "unknown title";
+ let rawTitle = 'unknown title';
if (p !== undefined && p.options && p.options.data && p.options.data.title) {
rawTitle = p.options.data.title;
}
@@ -742,6 +777,8 @@ function wrapper(plugin_info) {
let title = window.escapeHtmlSpecialChars(rawTitle);
let uriTitle = encodeURIComponent(rawTitle);
+ var keysNeeded = (portal.incomingValidCount !== undefined) ? portal.incomingValidCount : portal.incoming.length;
+
let availableKeysText = '';
let availableKeys = 0;
if (window.plugin.keys || window.plugin.LiveInventory) {
@@ -759,7 +796,7 @@ function wrapper(plugin_info) {
// Beware of bugs in the above code; I have only proved it correct, not tried it! (Donald Knuth)
let keyColorAttribute = '';
- if (availableKeys >= portal.incoming.length) {
+ if (availableKeys >= keysNeeded) {
keyColorAttribute = 'plugin_fanfields2_enoughKeys';
} else {
keyColorAttribute = 'plugin_fanfields2_notEnoughKeys';
@@ -801,20 +838,20 @@ function wrapper(plugin_info) {
text += '';
// Keys
- text += '';
+ text += ' | ';
// Links
- text += ' | ' + portal.outgoing.length + ' | ';
+ text += '' + ((portal.outgoingValidCount !== undefined) ? portal.outgoingValidCount : portal.outgoing.length) + ' | ';
let fieldsCreatedAtThisPortal = 0
if (portal.outgoing.length > 0) {
portal.outgoing.forEach(function (outPortal, outIndex) {
let meta = portal.outgoingMeta?.[outPortal.guid];
- fieldsCreatedAtThisPortal += meta?.creatingFieldsWith?.length ?? 0;
+ fieldsCreatedAtThisPortal += (meta && meta.fieldsCreatedValid !== undefined) ? meta.fieldsCreatedValid : (meta && meta.creatingFieldsWith ? meta.creatingFieldsWith.length : 0);
});
}
// Fields (here: empty cell)
text += '' + fieldSymbol.repeat(fieldsCreatedAtThisPortal); + ' | ';
+ fieldsCreatedAtThisPortal + '">' + fieldSymbol.repeat(fieldsCreatedAtThisPortal) + '';
// Row End
text += '';
@@ -825,6 +862,8 @@ function wrapper(plugin_info) {
portal.outgoing.forEach(function (outPortal, outIndex) {
let distance = thisplugin.distanceTo(portal.point, outPortal.point);
+ let meta = portal.outgoingMeta?.[outPortal.guid];
+
// Row start
let linkDetailText = '';
@@ -834,6 +873,12 @@ function wrapper(plugin_info) {
// Action
linkDetailText += '| ';
linkDetailText += 'Link to ' + thisplugin.sortedFanpoints.indexOf(outPortal);
+ if (meta && meta.invalidUnderField) {
+ linkDetailText += ' ⚠';
+ if (meta.canFlipUnderField) {
+ linkDetailText += ' ↔';
+ }
+ }
linkDetailText += ' | ';
let outPortalTitle = 'unknown title';
@@ -849,11 +894,10 @@ function wrapper(plugin_info) {
// Link (Distance)
linkDetailText += '' + formatDistance(distance) + ' | ';
// Fields
- let meta = portal.outgoingMeta?.[outPortal.guid];
- let fieldsCreatedByThisLink = meta?.creatingFieldsWith?.length ?? 0;
+ let fieldsCreatedByThisLink = (meta && meta.fieldsCreatedValid !== undefined) ? meta.fieldsCreatedValid : (meta && meta.creatingFieldsWith ? meta.creatingFieldsWith.length : 0);
linkDetailText += '' + fieldSymbol.repeat(fieldsCreatedByThisLink); + ' | ';
+ fieldsCreatedByThisLink + '">' + fieldSymbol.repeat(fieldsCreatedByThisLink) + '';
// Row End
linkDetailText += '
\n';
@@ -983,6 +1027,18 @@ function wrapper(plugin_info) {
.plugin_fanfields2_exportText_Label::before { display: none !important; }
.plugin_fanfields2_exportText_LinkDetails { display: table-row-group !important; }
+
+ /* Keys alignment */
+ td[plugin_fanfields2_enoughKeys],
+
+ td[plugin_fanfields2_notEnoughKeys] {
+ text-align: center !important;
+ }
+
+ td[plugin_fanfields2_enoughKeys],
+ div[plugin_fanfields2_enoughKeys] {
+ color: #828284;
+ }
`;
@@ -1045,8 +1101,8 @@ function wrapper(plugin_info) {
var p = fp.portal;
var title = (p && p.options && p.options.data && p.options.data.title) ? p.options.data.title : 'unknown title';
- var keys = fp.incoming ? fp.incoming.length : 0;
- var out = fp.outgoing ? fp.outgoing.length : 0;
+ var keys = (fp.incomingValidCount !== undefined) ? fp.incomingValidCount : (fp.incoming ? fp.incoming.length : 0);
+ var out = (fp.outgoingValidCount !== undefined) ? fp.outgoingValidCount : (fp.outgoing ? fp.outgoing.length : 0);
var isAnchor = (fp.guid === that.startingpointGUID);
var trClass = isAnchor ? 'plugin_fanfields2_order_anchor' : 'plugin_fanfields2_order_row';
@@ -1267,6 +1323,7 @@ function wrapper(plugin_info) {
that.manualOrderGuids = null;
} else {
that.manualOrderGuids = guids;
+ that.showUnderFieldWarningOnce = true;
}
that.delayedUpdateLayer(0.2);
@@ -1658,6 +1715,11 @@ function wrapper(plugin_info) {
' font-style: italic;\n' +
'}\n');
+ addCSS('\n' +
+ '.plugin_fanfields2_warn {\n' +
+ ' color: #ffce00;\n' +
+ '}\n');
+
//plugin_fanfields2_exportText_LinkDetails
addCSS('\n' +
'.plugin_fanfields2_exportText_LinkDetails tr td {\n' +
@@ -1689,14 +1751,14 @@ function wrapper(plugin_info) {
addCSS('\n' +
- '.plugin_fanfields {\n' +
+ '.plugin_fanfields2_label {\n' +
' color: #FFFFBB;\n' +
' font-size: 11px;\n' +
' line-height: 13px;\n' +
' text-align: left;\n' +
' vertical-align: bottom;\n' +
' padding: 2px;\n' +
- ' padding-top: 15px;\n' +
+ ' padding-top: ' + thisplugin.LABEL_PADDING_TOP + 'px;\n' +
' overflow: hidden;\n' +
' text-shadow: 1px 1px #000, 1px -1px #000, -1px 1px #000, -1px -1px #000, 0 0 5px #000;\n' +
' pointer-events: none;\n' +
@@ -1708,15 +1770,17 @@ function wrapper(plugin_info) {
if (window.plugin.keys || window.plugin.LiveInventory) {
- addCSS('\n' +
- 'td[plugin_fanfields2_enoughKeys], div[plugin_fanfields2_enoughKeys] {\n' +
- ' color: #828284;\n' +
- '}\n' +
- 'td[plugin_fanfields2_notEnoughKeys] {\n' +
- ' /* color: #FFBBBB; */ \n' +
- '}\n' +
- ''
- );
+ addCSS(`
+ td[plugin_fanfields2_enoughKeys], div[plugin_fanfields2_enoughKeys] {
+ color: #828284;
+ text-align: center;
+ }
+ td[plugin_fanfields2_notEnoughKeys] {
+ /* color: #FFBBBB; */
+ text-align: center;
+ }
+
+ `);
};
@@ -2013,7 +2077,7 @@ function wrapper(plugin_info) {
var label = L.marker(latLng, {
icon: L.divIcon({
- className: 'plugin_fanfields',
+ className: 'plugin_fanfields2_label',
iconAnchor: [0, 0],
iconSize: [thisplugin.LABEL_WIDTH, thisplugin.LABEL_HEIGHT],
html: labelText
@@ -2097,6 +2161,233 @@ function wrapper(plugin_info) {
}
+ // Issue #96: Max distance (in meters) for creating a link from a portal that is underneath an already-existing field.
+ // Historically this was 500m; Niantic increased this temporarily to 2000m (matryoshka links). Keep configurable.
+ thisplugin.maxLinkUnderFieldDistance = 2000;
+
+ thisplugin.invalidUnderFieldLinks = {};
+ thisplugin.validTriangles = null;
+ thisplugin.validLinkCount = 0;
+ thisplugin.validTriangleCount = 0;
+
+ thisplugin.pointKey = function (p) {
+ return p.x + ',' + p.y;
+ };
+
+ thisplugin.getDirectedLinkKey = function (guidA, guidB) {
+ return guidA + '>' + guidB;
+ };
+
+ thisplugin.getUndirectedLinkKey = function (guidA, guidB) {
+ return (guidA < guidB) ? (guidA + '|' + guidB) : (guidB + '|' + guidA);
+ };
+
+ // Strict point-in-triangle test in projection space:
+ // returns true only if the point is inside, NOT on the border (vertices/edges are NOT "under a field").
+ thisplugin.pointInTriangleStrict = function (p, a, b, c) {
+ var eps = 1e-9;
+
+ function sign(p1, p2, p3) {
+ return (p1.x - p3.x) * (p2.y - p3.y) - (p2.x - p3.x) * (p1.y - p3.y);
+ }
+
+ var d1 = sign(p, a, b);
+ var d2 = sign(p, b, c);
+ var d3 = sign(p, c, a);
+
+ // On an edge => not "under"
+ if (Math.abs(d1) < eps || Math.abs(d2) < eps || Math.abs(d3) < eps) return false;
+
+ var hasNeg = (d1 < 0) || (d2 < 0) || (d3 < 0);
+ var hasPos = (d1 > 0) || (d2 > 0) || (d3 > 0);
+
+ return !(hasNeg && hasPos);
+ };
+
+ thisplugin.isPointUnderAnyTriangle = function (p, triangles) {
+ if (!triangles || triangles.length === 0) return false;
+ for (var i = 0; i < triangles.length; i++) {
+ var t = triangles[i];
+ if (thisplugin.pointInTriangleStrict(p, t.a, t.b, t.c)) return true;
+ }
+ return false;
+ };
+
+ // Validate the current plan against the "link from underneath a field" distance rule (Issue #96).
+ // Marks impossible links, computes a "valid" triangle list (fields that can actually be created),
+ // and provides per-portal counts that ignore invalid links.
+ thisplugin.validateUnderFieldLinks = function () {
+ var sorted = thisplugin.sortedFanpoints || [];
+ if (sorted.length < 3) {
+ thisplugin.invalidUnderFieldLinks = {};
+ thisplugin.validTriangles = null;
+ thisplugin.validLinkCount = 0;
+ thisplugin.validTriangleCount = 0;
+ return;
+ }
+
+ // Reset per-run state
+ thisplugin.invalidUnderFieldLinks = {};
+ thisplugin.validTriangles = [];
+ thisplugin.validLinkCount = 0;
+ thisplugin.validTriangleCount = 0;
+
+ var pointToGuid = {};
+ for (var i = 0; i < sorted.length; i++) {
+ pointToGuid[thisplugin.pointKey(sorted[i].point)] = sorted[i].guid;
+ }
+
+ // Track successful links (undirected) so we can decide which triangles can actually be formed.
+ var builtLinks = {};
+
+ // Track whether a portal is under a field at the moment we arrive there.
+ var portalUnderFieldAtVisit = {};
+
+ // Per-link (directed) valid field creation count.
+ var validFieldsByDirectedLink = {};
+
+ // Init per-portal "valid" stats
+ for (var pi = 0; pi < sorted.length; pi++) {
+ sorted[pi].incomingValidCount = 0;
+ sorted[pi].outgoingValidCount = 0;
+ sorted[pi].fieldsCreatedValidAtPortal = 0;
+ }
+
+ function addInvalid(srcGuid, dstGuid, distance, flippedOk) {
+ var dkey = thisplugin.getDirectedLinkKey(srcGuid, dstGuid);
+ thisplugin.invalidUnderFieldLinks[dkey] = {
+ srcGuid: srcGuid,
+ dstGuid: dstGuid,
+ distance: distance,
+ flippedOk: flippedOk
+ };
+ }
+
+ // First pass: walk portals in visit order, validate each outgoing link against fields built so far.
+ // We only add triangles for links that are valid AND whose prerequisite links exist.
+ for (var vi = 0; vi < sorted.length; vi++) {
+ var srcFp = sorted[vi];
+ var srcGuid = srcFp.guid;
+
+ portalUnderFieldAtVisit[srcGuid] = thisplugin.isPointUnderAnyTriangle(srcFp.point, thisplugin.validTriangles);
+
+ if (!srcFp.outgoing || srcFp.outgoing.length === 0) continue;
+
+ for (var oi = 0; oi < srcFp.outgoing.length; oi++) {
+ var dstFp = srcFp.outgoing[oi];
+ var dstGuid = dstFp.guid;
+
+ var distance = thisplugin.distanceTo(srcFp.point, dstFp.point);
+ var srcUnder = portalUnderFieldAtVisit[srcGuid];
+ var invalid = srcUnder && distance > thisplugin.maxLinkUnderFieldDistance;
+
+ var meta = (srcFp.outgoingMeta && srcFp.outgoingMeta[dstGuid]) ? srcFp.outgoingMeta[dstGuid] : null;
+
+ // Precompute whether flipping the link direction could avoid the under-field restriction in THIS visit order.
+ // This ignores key logistics and assumes you can throw from the other end when you visit it.
+ var dstUnderAtVisit = portalUnderFieldAtVisit[dstGuid];
+ if (dstUnderAtVisit === undefined) {
+ // Destination might be later in the route. We'll fill it in when we reach it.
+ // For now, treat it as "unknown" => compute later in a second pass.
+ dstUnderAtVisit = null;
+ }
+ var flippedOk = (dstUnderAtVisit === null) ? null : (!(dstUnderAtVisit && distance > thisplugin.maxLinkUnderFieldDistance));
+
+ if (invalid) {
+ addInvalid(srcGuid, dstGuid, distance, flippedOk);
+ if (meta) {
+ meta.invalidUnderField = true;
+ meta.canFlipUnderField = flippedOk;
+ meta.fieldsCreatedValid = 0;
+ }
+ continue;
+ }
+
+ // Link is valid in this direction.
+ thisplugin.validLinkCount++;
+ srcFp.outgoingValidCount++;
+ dstFp.incomingValidCount++;
+
+ builtLinks[thisplugin.getUndirectedLinkKey(srcGuid, dstGuid)] = true;
+
+ // Determine how many fields this link can REALLY create (only if prerequisites exist).
+ var fieldsCreatedByThisLink = 0;
+
+ if (meta && meta.creatingFieldsWith && meta.creatingFieldsWith.length) {
+ for (var ti = 0; ti < meta.creatingFieldsWith.length; ti++) {
+ var thirdPoint = meta.creatingFieldsWith[ti];
+ var thirdGuid = pointToGuid[thisplugin.pointKey(thirdPoint)];
+ if (!thirdGuid) continue;
+
+ var e1 = thisplugin.getUndirectedLinkKey(srcGuid, thirdGuid);
+ var e2 = thisplugin.getUndirectedLinkKey(dstGuid, thirdGuid);
+
+ if (!builtLinks[e1] || !builtLinks[e2]) {
+ // If any prerequisite link is invalid/missing, the field won't exist.
+ continue;
+ }
+
+ // Field exists.
+ thisplugin.validTriangles.push({
+ a: thirdPoint,
+ b: srcFp.point,
+ c: dstFp.point
+ });
+ fieldsCreatedByThisLink++;
+ }
+ }
+
+ validFieldsByDirectedLink[thisplugin.getDirectedLinkKey(srcGuid, dstGuid)] = fieldsCreatedByThisLink;
+ if (meta) meta.fieldsCreatedValid = fieldsCreatedByThisLink;
+
+ srcFp.fieldsCreatedValidAtPortal += fieldsCreatedByThisLink;
+ }
+ }
+
+ // Second pass: fill "flip ok" for destinations that were visited later.
+ // Now that portalUnderFieldAtVisit is complete, update any null values.
+ for (var k in thisplugin.invalidUnderFieldLinks) {
+ if (!Object.prototype.hasOwnProperty.call(thisplugin.invalidUnderFieldLinks, k)) continue;
+ var rec = thisplugin.invalidUnderFieldLinks[k];
+ if (rec.flippedOk !== null) continue;
+
+ var dstUnder = !!portalUnderFieldAtVisit[rec.dstGuid];
+ rec.flippedOk = !(dstUnder && rec.distance > thisplugin.maxLinkUnderFieldDistance);
+
+ var src = sorted.find(function (fp) { return fp.guid === rec.srcGuid; });
+ if (src && src.outgoingMeta && src.outgoingMeta[rec.dstGuid]) {
+ src.outgoingMeta[rec.dstGuid].canFlipUnderField = rec.flippedOk;
+ }
+ }
+
+ thisplugin.validTriangleCount = thisplugin.validTriangles.length;
+
+ // If this validation was triggered by a manual order apply, show a one-time warning dialog.
+ if (thisplugin.showUnderFieldWarningOnce) {
+ thisplugin.showUnderFieldWarningOnce = false;
+
+ var invalidCount = Object.keys(thisplugin.invalidUnderFieldLinks).length;
+ if (invalidCount > 0) {
+ var width = 420;
+ thisplugin.MaxDialogWidth = thisplugin.getMaxDialogWidth();
+ if (thisplugin.MaxDialogWidth < width) width = thisplugin.MaxDialogWidth;
+
+ dialog({
+ html:
+ 'Warning: ' + invalidCount + ' link(s) cannot be created from under existing fields in this visit order.
' +
+ 'Limit: ' + thisplugin.maxLinkUnderFieldDistance + 'm. Impossible links are shown dotted and are excluded from counts/fields.
' +
+ 'Some may work if thrown from the opposite portal (workaround shown in task list), but Fanfields2 does not flip directions automatically.
',
+ id: 'plugin_fanfields2_alert_underfield',
+ title: 'Fan Fields 2 - Under-field Links',
+ width: width,
+ closeOnEscape: true
+ });
+ }
+ }
+ };
+
+
+
// Compute the arrowhead in projection space (pixels)
@@ -2330,11 +2621,13 @@ function wrapper(plugin_info) {
if (n < 2) return;
var alatlng = map.unproject(a.point, thisplugin.PROJECT_ZOOM);
var labelText = "";
+ var keysNeeded = (a.incomingValidCount !== undefined) ? a.incomingValidCount : a.incoming.length;
+ var totalFields = (thisplugin.validTriangleCount !== undefined) ? thisplugin.validTriangleCount : triangles.length;
if (thisplugin.stardirection === thisplugin.starDirENUM.CENTRALIZING) {
- labelText = "START PORTAL
Keys: " + a.incoming.length + "
Total Fields: " + triangles.length.toString();
+ labelText = "START PORTAL
Keys: " + keysNeeded + "
Total Fields: " + totalFields.toString();
} else {
- labelText = "START PORTAL
Keys: " + a.incoming.length + ", SBUL: " + (centerSbul) + "
out: " + centerOutgoings + "
Total Fields: " +
- triangles.length.toString();
+ labelText = "START PORTAL
Keys: " + keysNeeded + ", SBUL: " + (centerSbul) + "
out: " + centerOutgoings + "
Total Fields: " +
+ totalFields.toString();
}
thisplugin.addLabel(thisplugin.startingpointGUID, alatlng, labelText);
}
@@ -2343,7 +2636,7 @@ function wrapper(plugin_info) {
if (n < 2) return;
var alatlng = map.unproject(a.point, thisplugin.PROJECT_ZOOM);
var labelText = "";
- labelText = number + "
Keys: " + a.incoming.length + "
out: " + a.outgoing.length;
+ labelText = number + "
Keys: " + ((a.incomingValidCount !== undefined) ? a.incomingValidCount : a.incoming.length) + "
out: " + ((a.outgoingValidCount !== undefined) ? a.outgoingValidCount : a.outgoing.length);
thisplugin.addLabel(a.guid, alatlng, labelText);
}
@@ -2789,6 +3082,8 @@ function wrapper(plugin_info) {
possibleline = {
a: a,
b: b,
+ guidA: (outbound === 1) ? this.sortedFanpoints[pb].guid : this.sortedFanpoints[pa].guid,
+ guidB: (outbound === 1) ? this.sortedFanpoints[pa].guid : this.sortedFanpoints[pb].guid,
bearing: bearing,
isJetLink: false,
isFanLink: (pb === 0),
@@ -2908,14 +3203,17 @@ function wrapper(plugin_info) {
"\nFanPortals: " + (n - 1) +
"\nCenterKeys:" + thisplugin.centerKeys +
"\nTotal links / keys: " + donelinks.length.toString() +
- "\nFields: " + triangles.length.toString() +
- "\nBuild AP: " + (donelinks.length * 313 + triangles.length * 1250)
+ "\nFields: " + ((thisplugin.validTriangleCount !== undefined) ? thisplugin.validTriangleCount : triangles.length).toString() +
+ "\nBuild AP: " + (((thisplugin.validLinkCount !== undefined) ? thisplugin.validLinkCount : donelinks.length) * 313 + ((thisplugin.validTriangleCount !== undefined) ? thisplugin.validTriangleCount : triangles.length) * 1250)
.toString() +
- "\nDestroy AP: " + (this.sortedFanpoints.length * 187 + triangles.length * 750)
+ "\nDestroy AP: " + ((this.sortedFanpoints.length * 187) + ((thisplugin.validTriangleCount !== undefined) ? thisplugin.validTriangleCount : triangles.length) * 750)
.toString());
}
+ // Issue #96: validate plan against under-field link distance constraints
+ thisplugin.validateUnderFieldLinks();
+
// remove any not wanted
thisplugin.clearAllPortalLabels();
@@ -2932,24 +3230,53 @@ function wrapper(plugin_info) {
});
$.each(thisplugin.links, function (idx, edge) {
+ var dirDashArray = null;
if (thisplugin.indicateLinkDirection) {
- thisplugin.linkDashArray = [10, 5, 5, 5, 5, 5, 5, 5, "100000"];
- } else {
- thisplugin.linkDashArray = null;
+ dirDashArray = [10, 5, 5, 5, 5, 5, 5, 5, "100000"];
+ }
+
+ var linkKey = null;
+ if (edge.guidA && edge.guidB) {
+ linkKey = thisplugin.getDirectedLinkKey(edge.guidA, edge.guidB);
}
- drawLink(edge.a, edge.b, {
+ var isInvalid = (linkKey && thisplugin.invalidUnderFieldLinks && thisplugin.invalidUnderFieldLinks[linkKey]);
+
+ var baseStyle = {
color: '#FF0000',
opacity: 1,
weight: 1.5,
clickable: false,
interactive: false,
- smoothFactor: 10,
- dashArray: thisplugin.linkDashArray,
- });
+ smoothFactor: 10
+ };
+
+ if (isInvalid) {
+ // Base: fine dotted line (keep color)
+ drawLink(edge.a, edge.b, $.extend({}, baseStyle, {
+ dashArray: [10,5,5,5,5,5,50,30,5,3,5,3,5,10,15,5,15,5,15,10,5,3,5,3,5,30],
+ lineCap: 'round'
+ }));
+
+
+ /*
+ // Overlay: direction indicator (if enabled)
+ if (dirDashArray) {
+ drawLink(edge.a, edge.b, $.extend({}, baseStyle, {
+ dashArray: dirDashArray
+ }));
+ }
+ */
+ } else {
+ drawLink(edge.a, edge.b, $.extend({}, baseStyle, {
+ dashArray: dirDashArray
+ }));
+ }
});
- $.each(triangles, function (idx, triangle) {
+ var trianglesToDraw = (thisplugin.validTriangles) ? thisplugin.validTriangles : triangles;
+
+ $.each(trianglesToDraw, function (idx, triangle) {
drawField(triangle.a, triangle.b, triangle.c, {
stroke: false,
fill: true,