// ==UserScript== // @author Heistergand // @id fanfields@heistergand // @name Fan Fields 2 // @category Layer // @version 2.7.7.20260201 // @description Calculate how to link the portals to create the largest tidy set of nested fields. Enable from the layer chooser. // @downloadURL https://raw.githubusercontent.com/IITC-CE/Community-plugins/master/dist/heistergand/fanfields.user.js // @updateURL https://raw.githubusercontent.com/IITC-CE/Community-plugins/master/dist/heistergand/fanfields.meta.js // @icon https://raw.githubusercontent.com/Heistergand/fanfields2/master/fanfields2-32.png // @icon64 https://raw.githubusercontent.com/Heistergand/fanfields2/master/fanfields2-64.png // @supportURL https://github.com/Heistergand/fanfields2/issues // @namespace https://github.com/Heistergand/fanfields2 // @issueTracker https://github.com/Heistergand/fanfields2/issues // @homepageURL https://github.com/Heistergand/fanfields2/ // @depends draw-tools@breunigs // @recommends bookmarks@ZasoGD|draw-tools-plus@zaso|liveInventory@DanielOnDiordna|keys@xelio // @preview https://raw.githubusercontent.com/Heistergand/fanfields2/master/FanFields2.png // @match https://intel.ingress.com/* // @include https://intel.ingress.com/* // @grant none // ==/UserScript== 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-01-142842'; 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 = [{ version: '2.7.7', changes: [ 'IMPROVE portal sequence editor usage for mobile', 'UPDATE help dialog content', 'IMPROVE task list design' ], }, { version: '2.7.6', changes: [ 'FIX: Some minor code cleanup', 'FIX: Minor cosmetics', ], }, { version: '2.7.5', changes: [ 'NEW: Print your task list.', 'NEW: Show fields in task list.', 'FIX: Click on portal in task list now flies to the portal.', 'FIX: Uniform dialog titles', ], }, { version: '2.7.4', changes: [ 'FIX: Respect Intel not working anymore.', 'FIX: Dialog width on mobile too small.', ], }, { version: '2.7.3', changes: [ 'FIX: Tooltip must be a glyph sequence.', 'FIX: Double-Click on leaflet buttons isn\'t zooming the map anymore.', ], }, { version: '2.7.2', changes: [ 'FIX: Code cleanup and refactoring.', ], }, { version: '2.7.1', changes: [ 'FIX: The linking algorithm from version 2.6.6 was not perfect.', ], }, { version: '2.7.0', changes: [ 'NEW: Added portal sequence editor to customise the visit order.', 'NEW: Added straight-line route preview along the portal sequence.', ], }, { version: '2.6.6', changes: [ 'NEW: New linking algorythm.', ], }, { version: '2.6.5', changes: [ 'FIX: Fixed last fix.', ], }, { version: '2.6.4', changes: [ 'FIX: Fixed compatibility with Inventory Overview plugin.', ], }, { version: '2.6.3', changes: [ 'FIX: Fixed some minor issues like spelling mistakes.', ], }, { version: '2.6.2', changes: [ 'NEW: Task list now contains a single navigation link for each portal.', ], }, { version: '2.6.1', changes: [ 'FIX: Counts of outgoing links and sbul are now correct when respecting intel and using outbounding mode.', ], }, { version: '2.6.0', changes: [ 'NEW: Add control buttons for better ux on mobile.', ], }, { version: '2.5.6', changes: [ 'NEW: Implementing link details in show-as-list dialog.', ], }, { version: '2.5.5', changes: [ 'FIX: Plugin did not work on IITC-Mobile.', ], }, { version: '2.5.4', changes: [ 'NEW: Option to only use bookmarked portals within the Fanfields (Toggle-Button)', ], }, { version: '2.5.3', changes: [ 'NEW: Saving to Bookmarks now creates a folder in the Bookmarks list.', ], }, { version: '2.5.2', changes: [ 'FIX: Prefer LiveInventory Plugin over Keys Plugin (hotfix)', ], }, { version: '2.5.1', changes: [ 'FIX: Prefer LiveInventory Plugin over Keys Plugin', ], }, { version: '2.5.0', changes: [ 'NEW: Integrate key counts from LiveInventory plugin.', ], }, { version: '2.4.1', changes: [ 'FIX: "Show as List" without having the Keys Plugin did not show any Keys.', ], }, { version: '2.4.0', changes: [ 'NEW: Integrate functionality with Key Plugin.', 'NEW: Replace fieldset box design with a separated sidebar box.', ], }, { version: '2.3.2', changes: [ 'NEW: Introducing code for upcoming multiple fanfields by Drawtools Colors', 'FIX: some code refactorings', 'FIX: SBUL defaults to 2 now, assuming most fields are done solo.', 'FIX: If a marker is not actually snapped onto a portal it does not act as fan point anymore.', 'FIX: When adding a marker, it\'s now selected as start portal.', ], }, { version: '2.3.1', changes: [ 'FIX: Portals were difficult to select underneath the fanfileds plan.', ], }, { version: '2.3.0', changes: [ 'NEW: Added ' + arcname + ' support.', ], }, { version: '2.2.9', changes: [ 'FIX: Link direction indicator did not work anymore.', 'NEW: Link direction indicator is now optional.', 'NEW: New plugin icon showing a hand fan.', ], }, { version: '2.2.8', changes: [ 'FIX: minor changes', ], }, { version: '2.2.7', changes: [ 'FIX: Menu Buttons in Mobile version are now actually buttons.', ], }, { version: '2.2.6', changes: [ 'NEW: Google Maps Portal Routing', ], }, { version: '2.2.5', changes: [ 'NEW: Set how many SBUL you plan to use.', 'FIX: Anchor shift button design changed', ], }, { version: '2.2.4', changes: [ 'FIX: Width of dialog boxes did extend screen size', 'FIX: Fixed what should have been fixed in 2.2.4', ], }, { version: '2.2.3', changes: [ 'FIX: Made Bookmark Plugin optional', 'NEW: Anchor shifting ("Cycle Start") is now bidirectional.', 'FIX: Some minor fixes and code formatting.', ], }, { version: '2.2.2', changes: [ 'NEW: Added favicon.ico to script header.', ], }, { version: '2.2.1', changes: [ 'FIX: Merged from Jormund fork (2.1.7): Fixed L.LatLng extension', ], }, { version: '2.2.0', changes: [ 'FIX: Reintroducing the marker function which was removed in 2.1.7 so that a Drawtools Marker can be used to force a portal inside (or outside) the hull to be the anchor.', ], }, { version: '2.1.10', changes: [ 'FIX: minor fixes', ], }, { version: '2.1.9', changes: [ 'FIX: Fixed blank in header for compatibility with IITC-CE Button.', 'FIX: Fix for missing constants in leaflet verion 1.6.0.', ], }, { version: '2.1.8', changes: [ 'NEW: Added starting portal advance button to select among the list of perimeter portals.', ], }, { version: '2.1.7', changes: [ 'DEL: Removed marker and random selection of starting point portal.', 'NEW: Replaced with use of first outer hull portal. This ensures maximum fields will be generated.', ], }, { version: '2.1.5', changes: [ 'FIX: Minor syntax issue affecting potentially more strict runtimes', ], }, { version: '2.1.4', changes: [ 'FIX: Make the clockwise button change its label to "Counterclockwise" when toggled', ], }, { version: '2.1.3', changes: [ 'FIX: added id tags to menu button elements, ...just because.', ], }, { version: '2.1.2', changes: [ 'FIX: Minor issues', ], }, { version: '2.1.1', changes: [ 'FIX: changed List export format to display as a table', ], }, { version: '2.1.0', changes: [ 'NEW: Added save to DrawTools functionality', 'NEW: Added fanfield statistics', 'FIX: Changed some menu texts', 'VER: Increased Minor Version due to DrawTools Milestone', ], }, { version: '2.0.9', changes: [ 'NEW: Added the number of outgoing links to the simple list export', ], }, { version: '2.0.8', changes: [ 'NEW: Toggle the direction of the star-links (Inbound/Outbound) and calculate number of SBUL', 'FIX: Despite crosslinks, respecting the current intel did not handle done links', ], }, { version: '2.0.7', changes: [ 'FIX: Sorting of the portals was not accurate for far distance anchors when the angle was too equal.', 'NEW: Added option to respect current intel and not crossing lines.', ], }, { version: '2.0.6', changes: [ 'FIX: Plan messed up on multiple polygons.', ], }, { version: '2.0.5', changes: [ 'FIX: fan links abandoned when Marker was outside the polygon', 'BUG: Issue found where plan messes up when using more than one polygon (fixed in 2.0.6)', ], }, { version: '2.0.4', changes: [ 'NEW: Added Lock/Unlock button to freeze the plan and prevent recalculation on any events.', 'NEW: Added a simple text export (in a dialog box)', 'FIX: Several changes to the algorithm', 'BUG: Issue found where links are closing fields on top of portals that are successors in the list once you got around the startportal', ], }, { version: '2.0.3', changes: [ 'FIX: Counterclockwise did not work properly', 'NEW: Save as Bookmarks', ], }, { version: '2.0.2', changes: [ 'NEW: Added Menu', 'NEW: Added counterclockwise option', 'FIX: Minor Bugfixes', ], }, { version: '2.0.1', changes: [ 'NEW: Count keys to farm', 'NEW: Count total fields', 'NEW: Added labels to portals', 'FIX: Links were drawn in random order', 'FIX: Only fields to the center portal were drawn', ], }, ]; // PLUGIN START //////////////////////////////////////////////////////// // use own namespace for plugin /* jshint shadow:true */ window.plugin.fanfields = function () {}; var thisplugin = window.plugin.fanfields; // const values // zoom level used for projecting points between latLng and pixel coordinates. may affect precision of triangulation thisplugin.PROJECT_ZOOM = 16; thisplugin.LABEL_WIDTH = 100; thisplugin.LABEL_HEIGHT = 49; // constants no longer present in leaflet 1.6.0 thisplugin.DEG_TO_RAD = Math.PI / 180; thisplugin.RAD_TO_DEG = 180 / Math.PI; thisplugin.labelLayers = {}; thisplugin.startingpoint = undefined; thisplugin.availableSBUL = 2; thisplugin.locations = []; thisplugin.fanpoints = []; thisplugin.sortedFanpoints = []; thisplugin.perimeterpoints = []; thisplugin.startingpointIndex = 0; thisplugin.links = []; thisplugin.linksLayerGroup = null; thisplugin.fieldsLayerGroup = null; thisplugin.numbersLayerGroup = null; // ghi#23 thisplugin.orderPathLayerGroup = null; thisplugin.showOrderPath = false; thisplugin.manualOrderGuids = null; thisplugin.lastPlanSignature = null; thisplugin.saveBookmarks = function () { // loop thru portals and UN-Select them for bkmrks var bkmrkData, list; thisplugin.sortedFanpoints.forEach(function (point, index) { bkmrkData = window.plugin.bookmarks.findByGuid(point.guid); if (bkmrkData) { list = window.plugin.bookmarks.bkmrksObj.portals; delete list[bkmrkData.id_folder].bkmrk[bkmrkData.id_bookmark]; $('.bkmrk#' + bkmrkData.id_bookmark + '') .remove(); window.plugin.bookmarks.saveStorage(); window.plugin.bookmarks.updateStarPortal(); window.runHooks('pluginBkmrksEdit', { "target": "portal", "action": "remove", "folder": bkmrkData.id_folder, "id": bkmrkData.id_bookmark, "guid": point.guid }); console.log('Fanfields2: removed BOOKMARKS portal (' + bkmrkData.id_bookmark + ' situated in ' + bkmrkData.id_folder + ' folder)'); } }); let type = "folder"; let label = 'Fanfields2'; // 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": {} }; window.plugin.bookmarks.saveStorage(); window.plugin.bookmarks.refreshBkmrks(); window.runHooks('pluginBkmrksEdit', { "target": type, "action": "add", "id": folder_ID }); console.log('Fanfields2: added BOOKMARKS ' + type + ' ' + folder_ID); thisplugin.addPortalBookmark = function (guid, latlng, label, folder_ID) { var bookmark_ID = window.plugin.bookmarks.generateID(); // Add bookmark in the localStorage window.plugin.bookmarks.bkmrksObj.portals[folder_ID].bkmrk[bookmark_ID] = { "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 }); console.log('Fanfields2: added BOOKMARKS portal ' + bookmark_ID); } // loop again: ordered(!) to add them as bookmarks thisplugin.sortedFanpoints.forEach(function (point, index) { if (point.guid) { var p = window.portals[point.guid]; var ll = p.getLatLng(); //plugin.bookmarks.addPortalBookmark(point.guid, ll.lat+','+ll.lng, p.options.data.title); thisplugin.addPortalBookmark(point.guid, ll.lat + ',' + ll.lng, p.options.data.title, folder_ID) } }); }; thisplugin.updateStartingPoint = function (i) { thisplugin.startingpointIndex = i; thisplugin.startingpointGUID = thisplugin.perimeterpoints[thisplugin.startingpointIndex][0]; thisplugin.startingpoint = this.fanpoints[thisplugin.startingpointGUID]; // Reset manual order because the start/anchor changed (ghi#23) thisplugin.manualOrderGuids = null; thisplugin.updateLayer(); } // cycle to next starting point on the convex hull list of portals thisplugin.nextStartingPoint = function () { // *** startingpoint handling is duplicated in updateLayer(). var i = thisplugin.startingpointIndex + 1; if (i >= thisplugin.perimeterpoints.length) { i = 0; } thisplugin.updateStartingPoint(i); }; thisplugin.previousStartingPoint = function () { var i = thisplugin.startingpointIndex - 1; if (i < 0) { i = thisplugin.perimeterpoints.length - 1; } thisplugin.updateStartingPoint(i); }; thisplugin.helpDialogWidth = 650; thisplugin.help = function () { let width = thisplugin.helpDialogWidth; thisplugin.MaxDialogWidth = thisplugin.getMaxDialogWidth(); if (thisplugin.MaxDialogWidth < thisplugin.helpDialogWidth) { width = thisplugin.MaxDialogWidth; } dialog({ html: '
Select portals
' +
'Using Drawtools, draw one or more polygons around the portals you want to work with. ' +
'Polygons can overlap each other or be completely separated. All portals within the polygons ' +
'count toward your planned fanfield. ' +
'Optional: toggle 🔖 Bookmarks only to restrict the selection to your bookmarked portals.
Show the plan
' +
'From the layer selector, enable the Fanfields layers (Links / Fields / Numbers). ' +
'The fanfield is calculated and shown as red links/fields on the intel. ' +
'Link directions can be indicated with dashed stubs at the origin portal — toggle Show link dir as needed.
Choose the anchor (start portal)
' +
'By default, the script selects an anchor portal from the convex hull of all selected portals. ' +
'Use the Cycle Start buttons to step through hull portals (previous/next). ' +
'To force an inside portal as anchor (totally legitimate), place a Drawtools marker snapped onto that portal, ' +
'then cycle until it becomes the anchor.
Build mode: inbounding / outbounding
' +
'A fanfield can be done inbounding by farming many keys at the anchor and linking to it from all other portals. ' +
'It can also be done outbounding by star-linking from the anchor until the maximum number of outgoing links is reached. ' +
'In outbounding mode you can set how many SBUL you plan to use (0–4) to calculate the outgoing link capacity.
Avoid blockers
' +
'If you need to plan around links/fields you cannot or do not want to destroy, use Respect Intel. ' +
'When enabled, the plan avoids crosslinks with currently visible intel links/fields.
Order & route planning
' +
'Switch between Clockwise and Counterclockwise order to find an easier route or squeeze out extra fields. ' +
'For fine control, open Manage Portal Order and drag & drop portals to customise your visit order. ' +
'Use Path to preview a straight-line route along the current portal sequence.
Freeze recalculation
' +
'Use 🔒 Locked to prevent the script from recalculating while you zoom into details or work with large areas. ' +
'Switch back to 🔓 Unlocked to refresh after changes.
Task list & exports
' +
'Open Task List to get a step-by-step plan including per-portal key requirements, outgoing link counts, and (optional) link details. ' +
'If you use a Keys/LiveInventory plugin, the task list can also show your available key counts. ' +
'The task list includes a navigation link for Google Maps and a print-friendly view. ' +
'You can also export the plan to Drawtools/Bookmarks to share or continue working with it.
Found a bug? Post your issues at GitHub:
' +
'https://github.com/Heistergand/fanfields2/issues
| 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() + " |
| Pos. | "; text += "Action | "; text += "Portal Name | "; text += "Keys | "; text += "Links | "; text += "Fields | "; text += "
|---|---|---|---|---|---|
| ' + (index) + ' | '; // Action text += ''; text += ' '; text += ' '; text += ' | '; // Portal Name text += ''; const gmapsHref = `https://www.google.com/maps/dir/?api=1&destination=${lat},${lng}&query_destination_id=(${uriTitle})`; // Two links are rendered: // - UI link: uses onclick to interact with IITC (flyToPortal) // - Print link: real href for PDF / printing // Visibility is controlled via CSS (@media print or print window styles) text += ` ${title}`; text += ` ${title}`; text += ' | '; // Keys text += ''; // Links text += ' | ' + 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; }); } // Fields (here: empty cell) text += '' + fieldSymbol.repeat(fieldsCreatedAtThisPortal); + ' | '; // Row End text += '
| ' + (index) + '.' + thisplugin.sortedFanpoints.indexOf(outPortal) + ' | '; // Action linkDetailText += ''; linkDetailText += 'Link to ' + thisplugin.sortedFanpoints.indexOf(outPortal); linkDetailText += ' | '; let outPortalTitle = 'unknown title'; if (outPortal.portal !== undefined) { outPortalTitle = outPortal.portal.options.data.title; } // Portal Name (Target) linkDetailText += '' + outPortalTitle + ' | '; // Keys (here: empty cell) linkDetailText += ''; // Link (Distance) linkDetailText += ' | ' + formatDistance(distance) + ' | '; // Fields let meta = portal.outgoingMeta?.[outPortal.guid]; let fieldsCreatedByThisLink = meta?.creatingFieldsWith?.length ?? 0; linkDetailText += '' + fieldSymbol.repeat(fieldsCreatedByThisLink); + ' | '; // Row End linkDetailText += '
No Fanfield plan calculated yet.
Draw a polygon and let Fanfields calculate first.
| '; html += ' | '; html += ' | # | '; html += 'Portal | '; html += 'Keys | '; html += 'Links out | '; html += '⚓ | '; } else { if (isMobile) { moveCell = '' + '' + '' + ' | '; } else { moveCell = '☰ | '; } } html += '
|---|---|---|---|---|---|
| ' + idx + ' | '; html += '' + title + (isAnchor ? ' (anchor)' : '') + ' | '; html += '' + keys + ' | '; html += '' + out + ' | '; html += '