├── favicon.ico ├── FanFields2.png ├── fanfields2-16.png ├── fanfields2-32.png ├── fanfields2-64.png ├── LICENSE ├── iitc_plugin_fanfields2.meta.js ├── README.md ├── .github └── workflows │ └── telegram-notifications.yml └── iitc_plugin_fanfields2.user.js /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Heistergand/fanfields2/HEAD/favicon.ico -------------------------------------------------------------------------------- /FanFields2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Heistergand/fanfields2/HEAD/FanFields2.png -------------------------------------------------------------------------------- /fanfields2-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Heistergand/fanfields2/HEAD/fanfields2-16.png -------------------------------------------------------------------------------- /fanfields2-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Heistergand/fanfields2/HEAD/fanfields2-32.png -------------------------------------------------------------------------------- /fanfields2-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Heistergand/fanfields2/HEAD/fanfields2-64.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright © 2018-2025 IITC plugin: Fan Fields 2 4 | Copyright © 2018 Justus Kenklies 5 | 6 | Permission to use, copy, modify, and/or distribute this software for 7 | any purpose with or without fee is hereby granted, provided that the 8 | above copyright notice and this permission notice appear in all 9 | copies. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL 12 | WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED 13 | WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE 14 | AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL 15 | DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA 16 | OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 17 | TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 18 | PERFORMANCE OF THIS SOFTWARE. 19 | -------------------------------------------------------------------------------- /iitc_plugin_fanfields2.meta.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @author Heistergand 3 | // @id fanfields@heistergand 4 | // @name Fan Fields 2 5 | // @category Layer 6 | // @version 2.7.5.20251219 7 | // @description Calculate how to link the portals to create the largest tidy set of nested fields. Enable from the layer chooser. 8 | // @downloadURL https://github.com/Heistergand/fanfields2/raw/master/iitc_plugin_fanfields2.user.js 9 | // @updateURL https://github.com/Heistergand/fanfields2/raw/master/iitc_plugin_fanfields2.meta.js 10 | // @icon https://raw.githubusercontent.com/Heistergand/fanfields2/master/fanfields2-32.png 11 | // @icon64 https://raw.githubusercontent.com/Heistergand/fanfields2/master/fanfields2-64.png 12 | // @supportURL https://github.com/Heistergand/fanfields2/issues 13 | // @namespace https://github.com/Heistergand/fanfields2 14 | // @issueTracker https://github.com/Heistergand/fanfields2/issues 15 | // @homepageURL https://github.com/Heistergand/fanfields2/ 16 | // @depends draw-tools@breunigs 17 | // @recommends bookmarks@ZasoGD|draw-tools-plus@zaso|liveInventory@DanielOnDiordna|keys@xelio 18 | // @preview https://raw.githubusercontent.com/Heistergand/fanfields2/master/FanFields2.png 19 | // @match https://intel.ingress.com/* 20 | // @include https://intel.ingress.com/* 21 | // @grant none 22 | // ==/UserScript== 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fan Fields 2 2 | An INGRESS fan field planner plugin for IITC Desktop and Mobile. 3 | 4 | Use this plugin to easily plan your fanfields. It tells you how many keys you need for each portal, shows the total amount of fields and calculates the AP you will gain just for the links and fields. Works also for star fields and classic multilayer. Does not estimate MU, sorry for that. 5 | 6 | ## Prerequisites 7 | ### Required 8 | - [IITC CE](https://iitc.app) - Ingress Intel Total Conversion Community Edition 9 | - IITC plugin: [Draw tools](https://iitc.app/download_desktop#draw-tools-by-breunigs) by breunings 10 | 11 | ### Supported 12 | - IITC plugin: [Bookmarks for maps and portals](https://iitc.app/download_desktop#bookmarks-by-ZasoGD) by ZasoGD 13 | - IITC plugin: Arc 14 | - IITC plugin: [Keys](https://iitc.app/download_desktop#keys-by-xelio) by xelio 15 | - IITC Plugin: [Live Inventory](https://github.com/IITC-CE/Community-plugins?tab=readme-ov-file#live-inventory-by-eisfrei---fork-by-danielondiordna) by EisFrei - fork by DanielOnDiordna 16 | 17 | ## Features 18 | - Select portals in your area by drawing a polygon around them with DrawTools. 19 | - Let the magic happen: A fanfield Plan will instantly be shown as overlay. 20 | - Add more portals to your plan by just adding more polygons around them. 21 | - Toggle three layers: 22 | - Fanfield Links 23 | - Fanfield Fields 24 | - Fanfield Numbers _(Show statistics on each portal)_ 25 | - Show the list of portals 26 | - in the order to visit them 27 | - with the amount of keys you need 28 | - with the keys you already have _(if the Keys plugin is used and maintained)_ 29 | - count of outgoing links per portal 30 | - detailed link information: 31 | - link order 32 | - target portal name 33 | - link distance 34 | - Open the path along the Portals in google maps. 35 | - Tweak your plan: 36 | - Move the anchor portal along the hull of the area. 37 | - Set a marker to a portal inside the hull to make it a possible anchor. _(Yes, you can actually make a 360° fan field. Still has issues though.)_ 38 | - Toggle the build order clockwise or anticlockwise. 39 | - Toggle the main direction of the fan links: inbounding or outbounding at the anchor portal? 40 | - Set how many SBUL you plan to use 41 | - Toggle the link direction indicator. 42 | - Show respect to the current intel and avoid throwing crosslinks. _(In some cases useful, in others not at all...)_ 43 | - Toggle to use bookmarked portals only 44 | - Edit the portal visit order in a portal sequence editior. 45 | - View the straight-line route preview along the portal sequence. 46 | - Copy your plan to Drawtools Plugin. 47 | - Copy your plan to Bookmarks Plugin. 48 | - Copy your plan to Arcs Plugin. 49 | - Gather Stats including Build AP. _(Currently only for links and fields, not for destroying, capturing or deploying resos and mods)_ 50 | - mobile support 51 | 52 | 53 | ## Contribute 54 | Don't hesitate to send pull requests to the beta branch. 55 | 56 | 57 | ## Reviews 58 | ### Youtube review by Agent 57Cell 59 | Don't miss this review by Michael Hartley: 60 | https://www.youtube.com/watch?v=Z9TPlpnMYyI 61 | 62 | _Thank you, 57Cell for making awesome Ingress videos. This script wouldn't exist without your fanfields videos._ 63 | 64 | 65 | ### Field report by Agent KonnTower 66 | Agent [KonnTower](https://community.ingress.com/en/profile/KonnTower) wrote a report about his [exercise in maxfielding - ~244 fields from 89 portals](https://community.ingress.com/en/discussion/9791/an-exercise-in-maxfielding-244-fields-from-89-portals?) 67 | 68 | _It took three years until I stumbled over this report. It made me really happy to find it._ 69 | 70 | 71 | ## Tutorial Video 72 | english: https://youtu.be/jwn6p5xFGNY 73 | german https://youtu.be/IFgYGUdHNcs 74 | 75 | ## Donations 76 | The best way to say thank you is writing reports about where you used it and let me know about it. 77 | 78 | _If you think this is great and you really like to donate something: I have all I need. But the world is not what it seems, so head out and **donate blood** in your area, register as a **bone marrow donor** or **donate money to other charities**. It will change lives._ 79 | 80 | ## How it looks (on desktop) 81 | ### New sidebar design (no fieldset box anymore) 82 | ![image](https://github.com/Heistergand/fanfields2/assets/16416532/83d1f3b6-834d-4de2-8c03-041583ed0fe5) 83 | 84 | ### Overview 85 | You can still see the old fieldset box design here 86 | ![preview image](FanFields2.png) 87 | 88 | ### Use of Keys Plugin information 89 | ![image](https://github.com/Heistergand/fanfields2/assets/16416532/d23670b2-28c9-4bdb-ac33-d3b67e7e1193) 90 | 91 | ### Edit the portal visit order in a portal sequence editior 92 | Screenshot_20251208_005836 93 | 94 | Screenshot_20251208_005937 95 | -------------------------------------------------------------------------------- /.github/workflows/telegram-notifications.yml: -------------------------------------------------------------------------------- 1 | name: Telegram Notifications 2 | 3 | on: 4 | issues: 5 | types: [opened, edited, closed] 6 | issue_comment: 7 | types: [created] 8 | pull_request: 9 | types: [opened, edited, closed, reopened] 10 | pull_request_review_comment: 11 | types: [created] 12 | 13 | jobs: 14 | send_notification: 15 | runs-on: ubuntu-latest 16 | steps: 17 | 18 | # --- Escape Issue Body --- 19 | - name: Escape Issue Body 20 | if: ${{ github.event_name == 'issues' }} 21 | run: | 22 | ESCAPED_BODY=$(echo "${{ github.event.issue.body }}" | sed 's/&/\&/g; s//\>/g') 23 | echo "ESCAPED_BODY=$ESCAPED_BODY" >> $GITHUB_ENV 24 | 25 | # --- Escape Comment Body --- 26 | - name: Escape Comment Body 27 | if: ${{ github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment' }} 28 | run: | 29 | ESCAPED_COMMENT=$(echo "${{ github.event.comment.body }}" | sed 's/&/\&/g; s//\>/g') 30 | echo "ESCAPED_COMMENT=$ESCAPED_COMMENT" >> $GITHUB_ENV 31 | 32 | # --- Escape PR Body --- 33 | - name: Escape PR Body 34 | if: ${{ github.event_name == 'pull_request' }} 35 | run: | 36 | ESCAPED_PR_BODY=$(echo "${{ github.event.pull_request.body }}" | sed 's/&/\&/g; s//\>/g') 37 | echo "ESCAPED_PR_BODY=$ESCAPED_PR_BODY" >> $GITHUB_ENV 38 | 39 | # --- Telegram: Issue --- 40 | - name: Send Telegram (Issue) 41 | if: ${{ github.event_name == 'issues' }} 42 | id: tg_issue 43 | continue-on-error: true 44 | uses: appleboy/telegram-action@master 45 | with: 46 | to: ${{ secrets.TELEGRAM_TO }} 47 | token: ${{ secrets.TELEGRAM_TOKEN }} 48 | format: html 49 | disable_web_page_preview: true 50 | message: | 51 | 🚨 Issue Notification 🚨 52 | Issue URL: ${{ github.event.issue.html_url }} 53 | ${{ github.actor }} triggered issue event: ${{ github.event.action }} 54 | Title: ${{ github.event.issue.title }} 55 |
${{ env.ESCAPED_BODY }}
56 | 57 | - name: Send Telegram Fallback (Issue) 58 | if: ${{ github.event_name == 'issues' && steps.tg_issue.outcome == 'failure' }} 59 | uses: appleboy/telegram-action@master 60 | with: 61 | to: ${{ secrets.TELEGRAM_TO }} 62 | token: ${{ secrets.TELEGRAM_TOKEN }} 63 | format: html 64 | disable_web_page_preview: true 65 | message: | 66 | 🚨 Issue Notification 🚨 67 | Issue URL: ${{ github.event.issue.html_url }} 68 | ${{ github.actor }} triggered issue event: ${{ github.event.action }} 69 | Title: ${{ github.event.issue.title }} 70 |
cannot parse message to telegram
71 | 72 | # --- Telegram: Issue Comment --- 73 | - name: Send Telegram (Issue Comment) 74 | if: ${{ github.event_name == 'issue_comment' }} 75 | id: tg_comment 76 | continue-on-error: true 77 | uses: appleboy/telegram-action@master 78 | with: 79 | to: ${{ secrets.TELEGRAM_TO }} 80 | token: ${{ secrets.TELEGRAM_TOKEN }} 81 | format: html 82 | disable_web_page_preview: true 83 | message: | 84 | 📝 Comment on Issue 📝 85 | Comment URL: ${{ github.event.comment.html_url }} 86 | Issue URL: ${{ github.event.issue.html_url }} 87 | ${{ github.actor }} commented: 88 |
${{ env.ESCAPED_COMMENT }}
89 | 90 | - name: Send Telegram Fallback (Issue Comment) 91 | if: ${{ github.event_name == 'issue_comment' && steps.tg_comment.outcome == 'failure' }} 92 | uses: appleboy/telegram-action@master 93 | with: 94 | to: ${{ secrets.TELEGRAM_TO }} 95 | token: ${{ secrets.TELEGRAM_TOKEN }} 96 | format: html 97 | disable_web_page_preview: true 98 | message: | 99 | 📝 Comment on Issue 📝 100 | Comment URL: ${{ github.event.comment.html_url }} 101 | Issue URL: ${{ github.event.issue.html_url }} 102 | ${{ github.actor }} commented: 103 |
cannot parse message to telegram
104 | 105 | # --- Telegram: Pull Request --- 106 | - name: Send Telegram (Pull Request) 107 | if: ${{ github.event_name == 'pull_request' }} 108 | id: tg_pr 109 | continue-on-error: true 110 | uses: appleboy/telegram-action@master 111 | with: 112 | to: ${{ secrets.TELEGRAM_TO }} 113 | token: ${{ secrets.TELEGRAM_TOKEN }} 114 | format: html 115 | disable_web_page_preview: true 116 | message: | 117 | 🚀 Pull Request Notification 🚀 118 | PR URL: ${{ github.event.pull_request.html_url }} 119 | ${{ github.actor }} triggered PR event: ${{ github.event.action }} 120 | Title: ${{ github.event.pull_request.title }} 121 |
${{ env.ESCAPED_PR_BODY }}
122 | 123 | - name: Send Telegram Fallback (Pull Request) 124 | if: ${{ github.event_name == 'pull_request' && steps.tg_pr.outcome == 'failure' }} 125 | uses: appleboy/telegram-action@master 126 | with: 127 | to: ${{ secrets.TELEGRAM_TO }} 128 | token: ${{ secrets.TELEGRAM_TOKEN }} 129 | format: html 130 | disable_web_page_preview: true 131 | message: | 132 | 🚀 Pull Request Notification 🚀 133 | PR URL: ${{ github.event.pull_request.html_url }} 134 | ${{ github.actor }} triggered PR event: ${{ github.event.action }} 135 | Title: ${{ github.event.pull_request.title }} 136 |
cannot parse message to telegram
137 | 138 | # --- Telegram: PR Comment --- 139 | - name: Send Telegram (PR Comment) 140 | if: ${{ github.event_name == 'pull_request_review_comment' }} 141 | id: tg_pr_comment 142 | continue-on-error: true 143 | uses: appleboy/telegram-action@master 144 | with: 145 | to: ${{ secrets.TELEGRAM_TO }} 146 | token: ${{ secrets.TELEGRAM_TOKEN }} 147 | format: html 148 | disable_web_page_preview: true 149 | message: | 150 | 💬 Comment on Pull Request 💬 151 | Comment URL: ${{ github.event.comment.html_url }} 152 | PR URL: ${{ github.event.pull_request.html_url }} 153 | ${{ github.actor }} commented: 154 |
${{ env.ESCAPED_COMMENT }}
155 | 156 | - name: Send Telegram Fallback (PR Comment) 157 | if: ${{ github.event_name == 'pull_request_review_comment' && steps.tg_pr_comment.outcome == 'failure' }} 158 | uses: appleboy/telegram-action@master 159 | with: 160 | to: ${{ secrets.TELEGRAM_TO }} 161 | token: ${{ secrets.TELEGRAM_TOKEN }} 162 | format: html 163 | disable_web_page_preview: true 164 | message: | 165 | 💬 Comment on Pull Request 💬 166 | Comment URL: ${{ github.event.comment.html_url }} 167 | PR URL: ${{ github.event.pull_request.html_url }} 168 | ${{ github.actor }} commented: 169 |
cannot parse message to telegram
170 | -------------------------------------------------------------------------------- /iitc_plugin_fanfields2.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @author Heistergand 3 | // @id fanfields@heistergand 4 | // @name Fan Fields 2 5 | // @category Layer 6 | // @version 2.7.5.20251219 7 | // @description Calculate how to link the portals to create the largest tidy set of nested fields. Enable from the layer chooser. 8 | // @downloadURL https://github.com/Heistergand/fanfields2/raw/master/iitc_plugin_fanfields2.user.js 9 | // @updateURL https://github.com/Heistergand/fanfields2/raw/master/iitc_plugin_fanfields2.meta.js 10 | // @icon https://raw.githubusercontent.com/Heistergand/fanfields2/master/fanfields2-32.png 11 | // @icon64 https://raw.githubusercontent.com/Heistergand/fanfields2/master/fanfields2-64.png 12 | // @supportURL https://github.com/Heistergand/fanfields2/issues 13 | // @namespace https://github.com/Heistergand/fanfields2 14 | // @issueTracker https://github.com/Heistergand/fanfields2/issues 15 | // @homepageURL https://github.com/Heistergand/fanfields2/ 16 | // @depends draw-tools@breunigs 17 | // @recommends bookmarks@ZasoGD|draw-tools-plus@zaso|liveInventory@DanielOnDiordna|keys@xelio 18 | // @preview https://raw.githubusercontent.com/Heistergand/fanfields2/master/FanFields2.png 19 | // @match https://intel.ingress.com/* 20 | // @include https://intel.ingress.com/* 21 | // @grant none 22 | // ==/UserScript== 23 | 24 | /* 25 | 26 | Version History: 27 | -- Version History moved into the code. 28 | 29 | Todo: 30 | 31 | Add a kind of system to have a cluster of Fanfields 32 | Calculate distance to walk for the plan (crow / streets) 33 | Calculate the most efficient possible plan based on ways to walk and keys to farm 34 | Export to Tasks 35 | Calculate amount of possible rebuilds after flippinig the center portal 36 | Click on a link to flip it's direction 37 | 38 | */ 39 | 40 | function wrapper(plugin_info) { 41 | // ensure plugin framework is there, even if iitc is not yet loaded 42 | if(typeof window.plugin !== 'function') window.plugin = function() {}; 43 | plugin_info.buildName = 'main'; 44 | plugin_info.dateTimeVersion = '2025-12-19-021342'; 45 | plugin_info.pluginId = 'fanfields'; 46 | 47 | /* global L, $, dialog, map, portals, links, plugin, formatDistance -- eslint*/ 48 | /* exported setup, changelog -- eslint */ 49 | 50 | let arcname = window.PLAYER.team === 'ENLIGHTENED' ? 'Arc' : '***'; 51 | var changelog = [ 52 | { 53 | version: '2.7.5', 54 | changes: [ 55 | 'NEW: Print your task list.', 56 | 'NEW: Show fields in task list.', 57 | 'FIX: Click on portal in task list now flies to the portal.', 58 | 'FIX: Uniform dialog titles', 59 | ], 60 | }, 61 | { 62 | version: '2.7.4', 63 | changes: [ 64 | 'FIX: Respect Intel not working anymore.', 65 | 'FIX: Dialog width on mobile too small.', 66 | ], 67 | }, 68 | { 69 | version: '2.7.3', 70 | changes: [ 71 | 'FIX: Tooltip must be a glyph sequence.', 72 | 'FIX: Double-Click on leaflet buttons isn\'t zooming the map anymore.', 73 | ], 74 | }, 75 | { 76 | version: '2.7.2', 77 | changes: [ 78 | 'FIX: Code cleanup and refactoring.', 79 | ], 80 | }, 81 | { 82 | version: '2.7.1', 83 | changes: [ 84 | 'FIX: The linking algorithm from version 2.6.6 was not perfect.', 85 | ], 86 | }, 87 | { 88 | version: '2.7.0', 89 | changes: [ 90 | 'NEW: Added portal sequence editor to customise the visit order.', 91 | 'NEW: Added straight-line route preview along the portal sequence.', 92 | ], 93 | }, 94 | { 95 | version: '2.6.6', 96 | changes: [ 97 | 'NEW: New linking algorythm.', 98 | ], 99 | }, 100 | { 101 | version: '2.6.5', 102 | changes: [ 103 | 'FIX: Fixed last fix.', 104 | ], 105 | }, 106 | { 107 | version: '2.6.4', 108 | changes: [ 109 | 'FIX: Fixed compatibility with Inventory Overview plugin.', 110 | ], 111 | }, 112 | { 113 | version: '2.6.3', 114 | changes: [ 115 | 'FIX: Fixed some minor issues like spelling mistakes.', 116 | ], 117 | }, 118 | { 119 | version: '2.6.2', 120 | changes: [ 121 | 'NEW: Task list now contains a single navigation link for each portal.', 122 | ], 123 | }, 124 | { 125 | version: '2.6.1', 126 | changes: [ 127 | 'FIX: Counts of outgoing links and sbul are now correct when respecting intel and using outbounding mode.', 128 | ], 129 | }, 130 | { 131 | version: '2.6.0', 132 | changes: [ 133 | 'NEW: Add control buttons for better ux on mobile.', 134 | ], 135 | }, 136 | { 137 | version: '2.5.6', 138 | changes: [ 139 | 'NEW: Implementing link details in show-as-list dialog.', 140 | ], 141 | }, 142 | { 143 | version: '2.5.5', 144 | changes: [ 145 | 'FIX: Plugin did not work on IITC-Mobile.', 146 | ], 147 | }, 148 | { 149 | version: '2.5.4', 150 | changes: [ 151 | 'NEW: Option to only use bookmarked portals within the Fanfields (Toggle-Button)', 152 | ], 153 | }, 154 | { 155 | version: '2.5.3', 156 | changes: [ 157 | 'NEW: Saving to Bookmarks now creates a folder in the Bookmarks list.', 158 | ], 159 | }, 160 | { 161 | version: '2.5.2', 162 | changes: [ 163 | 'FIX: Prefer LiveInventory Plugin over Keys Plugin (hotfix)', 164 | ], 165 | }, 166 | { 167 | version: '2.5.1', 168 | changes: [ 169 | 'FIX: Prefer LiveInventory Plugin over Keys Plugin', 170 | ], 171 | }, 172 | { 173 | version: '2.5.0', 174 | changes: [ 175 | 'NEW: Integrate key counts from LiveInventory plugin.', 176 | ], 177 | }, 178 | { 179 | version: '2.4.1', 180 | changes: [ 181 | 'FIX: "Show as List" without having the Keys Plugin did not show any Keys.', 182 | ], 183 | }, 184 | { 185 | version: '2.4.0', 186 | changes: [ 187 | 'NEW: Integrate functionality with Key Plugin.', 188 | 'NEW: Replace fieldset box design with a separated sidebar box.', 189 | ], 190 | }, 191 | { 192 | version: '2.3.2', 193 | changes: [ 194 | 'NEW: Introducing code for upcoming multiple fanfields by Drawtools Colors', 195 | 'FIX: some code refactorings', 196 | 'FIX: SBUL defaults to 2 now, assuming most fields are done solo.', 197 | 'FIX: If a marker is not actually snapped onto a portal it does not act as fan point anymore.', 198 | 'FIX: When adding a marker, it\'s now selected as start portal.', 199 | ], 200 | }, 201 | { 202 | version: '2.3.1', 203 | changes: [ 204 | 'FIX: Portals were difficult to select underneath the fanfileds plan.', 205 | ], 206 | }, 207 | { 208 | version: '2.3.0', 209 | changes: [ 210 | 'NEW: Added '+arcname+' support.', 211 | ], 212 | }, 213 | { 214 | version: '2.2.9', 215 | changes: [ 216 | 'FIX: Link direction indicator did not work anymore.', 217 | 'NEW: Link direction indicator is now optional.', 218 | 'NEW: New plugin icon showing a hand fan.', 219 | ], 220 | }, 221 | { 222 | version: '2.2.8', 223 | changes: [ 224 | 'FIX: minor changes', 225 | ], 226 | }, 227 | { 228 | version: '2.2.7', 229 | changes: [ 230 | 'FIX: Menu Buttons in Mobile version are now actually buttons.', 231 | ], 232 | }, 233 | { 234 | version: '2.2.6', 235 | changes: [ 236 | 'NEW: Google Maps Portal Routing', 237 | ], 238 | }, 239 | { 240 | version: '2.2.5', 241 | changes: [ 242 | 'NEW: Set how many SBUL you plan to use.', 243 | 'FIX: Anchor shift button design changed', 244 | ], 245 | }, 246 | { 247 | version: '2.2.4', 248 | changes: [ 249 | 'FIX: Width of dialog boxes did extend screen size', 250 | 'FIX: Fixed what should have been fixed in 2.2.4', 251 | ], 252 | }, 253 | { 254 | version: '2.2.3', 255 | changes: [ 256 | 'FIX: Made Bookmark Plugin optional', 257 | 'NEW: Anchor shifting ("Cycle Start") is now bidirectional.', 258 | 'FIX: Some minor fixes and code formatting.', 259 | ], 260 | }, 261 | { 262 | version: '2.2.2', 263 | changes: [ 264 | 'NEW: Added favicon.ico to script header.', 265 | ], 266 | }, 267 | { 268 | version: '2.2.1', 269 | changes: [ 270 | 'FIX: Merged from Jormund fork (2.1.7): Fixed L.LatLng extension', 271 | ], 272 | }, 273 | 274 | { 275 | version: '2.2.0', 276 | changes: [ 277 | '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.', 278 | ], 279 | }, 280 | { 281 | version: '2.1.10', 282 | changes: [ 283 | 'FIX: minor fixes', 284 | ], 285 | }, 286 | { 287 | version: '2.1.9', 288 | changes: [ 289 | 'FIX: Fixed blank in header for compatibility with IITC-CE Button.', 290 | 'FIX: Fix for missing constants in leaflet verion 1.6.0.', 291 | ], 292 | }, 293 | { 294 | version: '2.1.8', 295 | changes: [ 296 | 'NEW: Added starting portal advance button to select among the list of perimeter portals.', 297 | ], 298 | }, 299 | { 300 | version: '2.1.7', 301 | changes: [ 302 | 'DEL: Removed marker and random selection of starting point portal.', 303 | 'NEW: Replaced with use of first outer hull portal. This ensures maximum fields will be generated.', 304 | ], 305 | }, 306 | { 307 | version: '2.1.5', 308 | changes: [ 309 | 'FIX: Minor syntax issue affecting potentially more strict runtimes', 310 | ], 311 | }, 312 | { 313 | version: '2.1.4', 314 | changes: [ 315 | 'FIX: Make the clockwise button change its label to "Counterclockwise" when toggled', 316 | ], 317 | }, 318 | { 319 | version: '2.1.3', 320 | changes: [ 321 | 'FIX: added id tags to menu button elements, ...just because.', 322 | ], 323 | }, 324 | { 325 | version: '2.1.2', 326 | changes: [ 327 | 'FIX: Minor issues', 328 | ], 329 | }, 330 | { 331 | version: '2.1.1', 332 | changes: [ 333 | 'FIX: changed List export format to display as a table', 334 | ], 335 | }, 336 | { 337 | version: '2.1.0', 338 | changes: [ 339 | 'NEW: Added save to DrawTools functionality', 340 | 'NEW: Added fanfield statistics', 341 | 'FIX: Changed some menu texts', 342 | 'VER: Increased Minor Version due to DrawTools Milestone', 343 | ], 344 | }, 345 | { 346 | version: '2.0.9', 347 | changes: [ 348 | 'NEW: Added the number of outgoing links to the simple list export', 349 | ], 350 | }, 351 | { 352 | version: '2.0.8', 353 | changes: [ 354 | 'NEW: Toggle the direction of the star-links (Inbound/Outbound) and calculate number of SBUL', 355 | 'FIX: Despite crosslinks, respecting the current intel did not handle done links', 356 | ], 357 | }, 358 | { 359 | version: '2.0.7', 360 | changes: [ 361 | 'FIX: Sorting of the portals was not accurate for far distance anchors when the angle was too equal.', 362 | 'NEW: Added option to respect current intel and not crossing lines.', 363 | ], 364 | }, 365 | { 366 | version: '2.0.6', 367 | changes: [ 368 | 'FIX: Plan messed up on multiple polygons.', 369 | ], 370 | }, 371 | { 372 | version: '2.0.5', 373 | changes: [ 374 | 'FIX: fan links abandoned when Marker was outside the polygon', 375 | 'BUG: Issue found where plan messes up when using more than one polygon (fixed in 2.0.6)', 376 | ], 377 | }, 378 | { 379 | version: '2.0.4', 380 | changes: [ 381 | 'NEW: Added Lock/Unlock button to freeze the plan and prevent recalculation on any events.', 382 | 'NEW: Added a simple text export (in a dialog box)', 383 | 'FIX: Several changes to the algorithm', 384 | 'BUG: Issue found where links are closing fields on top of portals that are successors in the list once you got around the startportal', 385 | ], 386 | }, 387 | { 388 | version: '2.0.3', 389 | changes: [ 390 | 'FIX: Counterclockwise did not work properly', 391 | 'NEW: Save as Bookmarks', 392 | ], 393 | }, 394 | { 395 | version: '2.0.2', 396 | changes: [ 397 | 'NEW: Added Menu', 398 | 'NEW: Added counterclockwise option', 399 | 'FIX: Minor Bugfixes', 400 | ], 401 | }, 402 | { 403 | version: '2.0.1', 404 | changes: [ 405 | 'NEW: Count keys to farm', 406 | 'NEW: Count total fields', 407 | 'NEW: Added labels to portals', 408 | 'FIX: Links were drawn in random order', 409 | 'FIX: Only fields to the center portal were drawn', 410 | ], 411 | }, 412 | ]; 413 | // PLUGIN START //////////////////////////////////////////////////////// 414 | 415 | // use own namespace for plugin 416 | /* jshint shadow:true */ 417 | window.plugin.fanfields = function() {}; 418 | var thisplugin = window.plugin.fanfields; 419 | 420 | // const values 421 | // zoom level used for projecting points between latLng and pixel coordinates. may affect precision of triangulation 422 | thisplugin.PROJECT_ZOOM = 16; 423 | 424 | thisplugin.LABEL_WIDTH = 100; 425 | thisplugin.LABEL_HEIGHT = 49; 426 | 427 | // constants no longer present in leaflet 1.6.0 428 | thisplugin.DEG_TO_RAD = Math.PI / 180; 429 | thisplugin.RAD_TO_DEG = 180 / Math.PI; 430 | 431 | 432 | thisplugin.labelLayers = {}; 433 | 434 | thisplugin.startingpoint = undefined; 435 | thisplugin.availableSBUL = 2; 436 | 437 | thisplugin.locations = []; 438 | thisplugin.fanpoints = []; 439 | thisplugin.sortedFanpoints = []; 440 | thisplugin.perimeterpoints = []; 441 | thisplugin.startingpointIndex = 0; 442 | 443 | 444 | 445 | thisplugin.links = []; 446 | thisplugin.linksLayerGroup = null; 447 | thisplugin.fieldsLayerGroup = null; 448 | thisplugin.numbersLayerGroup = null; 449 | 450 | 451 | // ghi#23 452 | thisplugin.orderPathLayerGroup = null; 453 | thisplugin.showOrderPath = false; 454 | thisplugin.manualOrderGuids = null; 455 | thisplugin.lastPlanSignature = null; 456 | 457 | 458 | thisplugin.selectPolygon = function() {}; 459 | thisplugin.saveBookmarks = function() { 460 | 461 | // loop thru portals and UN-Select them for bkmrks 462 | var bkmrkData, list; 463 | thisplugin.sortedFanpoints.forEach(function(point, index) { 464 | 465 | bkmrkData = window.plugin.bookmarks.findByGuid(point.guid); 466 | if(bkmrkData) { 467 | 468 | list = window.plugin.bookmarks.bkmrksObj.portals; 469 | 470 | delete list[bkmrkData.id_folder].bkmrk[bkmrkData.id_bookmark]; 471 | 472 | $('.bkmrk#'+bkmrkData.id_bookmark + '').remove(); 473 | 474 | window.plugin.bookmarks.saveStorage(); 475 | window.plugin.bookmarks.updateStarPortal(); 476 | 477 | 478 | window.runHooks('pluginBkmrksEdit', {"target": "portal", "action": "remove", "folder": bkmrkData.id_folder, "id": bkmrkData.id_bookmark, "guid":point.guid}); 479 | 480 | console.log('Fanfields2: removed BOOKMARKS portal ('+bkmrkData.id_bookmark+' situated in '+bkmrkData.id_folder+' folder)'); 481 | } 482 | }); 483 | 484 | 485 | let type = "folder"; 486 | let label = 'Fanfields2'; 487 | // Add new folder in the localStorage 488 | let folder_ID = window.plugin.bookmarks.generateID(); 489 | window.plugin.bookmarks.bkmrksObj.portals[folder_ID] = {"label":label,"state":1,"bkmrk":{}}; 490 | 491 | window.plugin.bookmarks.saveStorage(); 492 | window.plugin.bookmarks.refreshBkmrks(); 493 | window.runHooks('pluginBkmrksEdit', {"target": type, "action": "add", "id": folder_ID}); 494 | console.log('Fanfields2: added BOOKMARKS '+type+' '+folder_ID); 495 | 496 | thisplugin.addPortalBookmark = function(guid, latlng, label, folder_ID) { 497 | var bookmark_ID = window.plugin.bookmarks.generateID(); 498 | 499 | // Add bookmark in the localStorage 500 | window.plugin.bookmarks.bkmrksObj.portals[folder_ID].bkmrk[bookmark_ID] = {"guid":guid,"latlng":latlng,"label":label}; 501 | 502 | window.plugin.bookmarks.saveStorage(); 503 | window.plugin.bookmarks.refreshBkmrks(); 504 | window.runHooks('pluginBkmrksEdit', {"target": "portal", "action": "add", "id": bookmark_ID, "guid": guid}); 505 | console.log('Fanfields2: added BOOKMARKS portal '+bookmark_ID); 506 | } 507 | 508 | // loop again: ordered(!) to add them as bookmarks 509 | thisplugin.sortedFanpoints.forEach(function(point, index) { 510 | if (point.guid) { 511 | var p = window.portals[point.guid]; 512 | var ll = p.getLatLng(); 513 | 514 | //plugin.bookmarks.addPortalBookmark(point.guid, ll.lat+','+ll.lng, p.options.data.title); 515 | thisplugin.addPortalBookmark(point.guid, ll.lat+','+ll.lng, p.options.data.title, folder_ID) 516 | } 517 | }); 518 | }; 519 | 520 | thisplugin.updateStartingPoint = function(i) { 521 | thisplugin.startingpointIndex = i; 522 | thisplugin.startingpointGUID = thisplugin.perimeterpoints[thisplugin.startingpointIndex][0]; 523 | thisplugin.startingpoint = this.fanpoints[thisplugin.startingpointGUID]; 524 | 525 | // Reset manual order because the start/anchor changed (ghi#23) 526 | thisplugin.manualOrderGuids = null; 527 | 528 | thisplugin.updateLayer(); 529 | } 530 | 531 | // cycle to next starting point on the convex hull list of portals 532 | thisplugin.nextStartingPoint = function() { 533 | // *** startingpoint handling is duplicated in updateLayer(). 534 | 535 | var i = thisplugin.startingpointIndex + 1; 536 | if (i >= thisplugin.perimeterpoints.length) { 537 | i = 0; 538 | } 539 | thisplugin.updateStartingPoint(i); 540 | }; 541 | 542 | thisplugin.previousStartingPoint = function() { 543 | var i = thisplugin.startingpointIndex - 1; 544 | if (i < 0) { 545 | i = thisplugin.perimeterpoints.length -1; 546 | } 547 | thisplugin.updateStartingPoint(i); 548 | }; 549 | 550 | thisplugin.generateTasks = function() {}; 551 | thisplugin.reset = function() {}; 552 | 553 | 554 | thisplugin.helpDialogWidth = 650; 555 | 556 | thisplugin.help = function() { 557 | var width = thisplugin.helpDialogWidth; 558 | thisplugin.MaxDialogWidth = thisplugin.getMaxDialogWidth(); 559 | if (thisplugin.MaxDialogWidth < thisplugin.helpDialogWidth) { 560 | width = thisplugin.MaxDialogWidth; 561 | } 562 | dialog({ 563 | html: '

Using Drawtools, draw one or more polygons around the portals you want to work with. '+ 564 | 'The Polygons can overlap each other or be completely separated. All portals within the polygons '+ 565 | 'count to your planned fanfield.

'+ 566 | 567 | '

From the layer selector, enable the 3 Fanfields layer for links, fields and numbers. '+ 568 | 'The fanfield will be calculated and shown in red links on the intel. Link directions are indicated '+ 569 | 'by dashed links at the portal to link from.

'+ 570 | 571 | '

The script selects an anchor portal from the hull of all selected portals. Use the Cycle Start '+ 572 | 'Button to select another hull portal as anchor.

'+ 573 | 574 | '

If you want to use portal as anchor, which is inside the hull, (which is totally legitimate), '+ 575 | 'place a marker on a portal to enforce it to be a possible anchor. Again, use the Cycle Start '+ 576 | 'Button until the Start Portal is where you want it to be.

'+ 577 | 578 | '

A Fanfield can be done inbounding by farming many keys at a portal and then link to it by all '+ 579 | 'the other portals. It can also be done outbounding by star-linking from the start portal until the maximum '+ 580 | 'number of outgoing links is reached. You can toggle that for planning accordingly.

'+ 581 | 582 | '

You might need to plan your field around links you cannot or do not want to destroy. This is where the '+ 583 | 'Respect Intel button comes into play. Toggle this to plan your fanfield avoiding crosslinks.

'+ 584 | 585 | '

Use the Lock function to prevent the script from recalculating anything. This is useful '+ 586 | 'if you have a large area and want to zoom into details.

'+ 587 | 588 | '

Try to switch your plan to counterclockwise direction. Your route might be easier or harder '+ 589 | 'if you change directions. Also try different anchors to get one more field out of some portal '+ 590 | 'constellations.

'+ 591 | 592 | '

Copy your fanfield portals to bookmarks or drawtools to extend your possibilities to work '+ 593 | 'with the information.

'+ 594 | 595 | '
'+ 596 | 597 | '

Found a bug? Post your issues at GitHub:
https://github.com/Heistergand/fanfields2/issues

'+ 598 | '', 599 | id: 'plugin_fanfields2_alert_help', 600 | title: 'Fan Fields 2 - Help', 601 | width: width, 602 | closeOnEscape: true 603 | }); 604 | 605 | 606 | }; 607 | 608 | thisplugin.showStatistics = function() { 609 | var text = ""; 610 | if (this.sortedFanpoints.length > 3) { 611 | text = "" + 612 | "" + 613 | "" + 614 | "" + 615 | "" + 616 | //"" + 617 | "
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() + "
"; 618 | 619 | var width = 400; 620 | thisplugin.MaxDialogWidth = thisplugin.getMaxDialogWidth(); 621 | if (thisplugin.MaxDialogWidth < width) { 622 | width = thisplugin.MaxDialogWidth; 623 | } 624 | 625 | dialog({ 626 | html: text, 627 | id: 'plugin_fanfields2_alert_statistics', 628 | title: 'Fan Fields 2 - Statistics', 629 | width: width, 630 | closeOnEscape: true 631 | }); 632 | } 633 | 634 | 635 | } 636 | 637 | thisplugin.exportDrawtools = function() { 638 | var alatlng, blatlng, layer; 639 | $.each(thisplugin.sortedFanpoints, function(index, portal) { 640 | $.each(portal.outgoing, function(targetIndex, targetPortal) { 641 | 642 | alatlng = map.unproject(portal.point, thisplugin.PROJECT_ZOOM); 643 | blatlng = map.unproject(targetPortal.point, thisplugin.PROJECT_ZOOM); 644 | layer = L.geodesicPolyline([alatlng, blatlng], window.plugin.drawTools.lineOptions); 645 | window.plugin.drawTools.drawnItems.addLayer(layer); 646 | window.plugin.drawTools.save(); 647 | }); 648 | }); 649 | } 650 | 651 | thisplugin.exportArcs = function() { 652 | if (window.PLAYER.team === 'RESISTANCE') { 653 | // sorry 654 | return; 655 | }; 656 | var alatlng, blatlng, layer; 657 | $.each(thisplugin.sortedFanpoints, function(index, portal) { 658 | $.each(portal.outgoing, function(targetIndex, targetPortal) { 659 | window.selectedPortal = portal.guid; 660 | window.plugin.arcs.draw(); 661 | window.selectedPortal = targetPortal.guid; 662 | window.plugin.arcs.draw(); 663 | }); 664 | }); 665 | window.plugin.arcs.list(); 666 | } 667 | 668 | thisplugin.exportTasks = function() { 669 | //todo... 670 | } 671 | 672 | thisplugin.flyToPortal = function(latlng, guid) { 673 | 674 | window.map.flyTo(latlng, map.getZoom()); 675 | if (window.portals[guid]) window.renderPortalDetails(guid); 676 | else window.urlPortal = guid; 677 | } 678 | 679 | // Show as list 680 | thisplugin.exportText = function() { 681 | var text = ""; 682 | 683 | text+=""; 684 | text+=""; 685 | text+=""; 686 | text+=""; 687 | text+=""; 688 | text+=""; 689 | 690 | 691 | text+=""; 692 | let linkDetailText = ''; 693 | var gmnav='http://maps.google.com/maps/dir/'; 694 | 695 | thisplugin.sortedFanpoints.forEach(function(portal, index) { 696 | var p, lat, lng; 697 | var latlng = map.unproject(portal.point, thisplugin.PROJECT_ZOOM); 698 | lat = Math.round(latlng.lat * 10000000) / 10000000 699 | lng = Math.round(latlng.lng * 10000000) / 10000000 700 | gmnav+=`${lat},${lng}/`; 701 | p = portal.portal; 702 | // window.portals[portal.guid]; 703 | 704 | let rawTitle = "unknown title"; 705 | if (p !== undefined && p.options && p.options.data && p.options.data.title) { 706 | rawTitle = p.options.data.title; 707 | } 708 | 709 | let title = window.escapeHtmlSpecialChars(rawTitle); 710 | let uriTitle = encodeURIComponent(rawTitle); 711 | 712 | let availableKeysText = ''; 713 | let availableKeys = 0; 714 | if (window.plugin.keys || window.plugin.LiveInventory) { 715 | 716 | if (window.plugin.LiveInventory) { 717 | if (window.plugin.LiveInventory.keyGuidCount) { 718 | availableKeys = window.plugin.LiveInventory.keyGuidCount[portal.guid] || 0; 719 | } else if (window.plugin.LiveInventory.keyCount) { 720 | availableKeys = window.plugin.LiveInventory.keyCount.find(obj => obj.portalCoupler.portalGuid === portal.guid)?.count || 0; 721 | } 722 | } else { 723 | availableKeys = window.plugin.keys.keys[portal.guid] || 0; 724 | } 725 | // Beware of bugs in the above code; I have only proved it correct, not tried it! (Donald Knuth) 726 | 727 | let keyColorAttribute = ''; 728 | if (availableKeys >= portal.incoming.length) { 729 | keyColorAttribute = 'plugin_fanfields2_enoughKeys'; 730 | } else { 731 | keyColorAttribute = 'plugin_fanfields2_notEnoughKeys'; 732 | }; 733 | 734 | availableKeysText = keyColorAttribute + '>' + availableKeys + '/'; 735 | } else { 736 | availableKeysText = '>'; 737 | }; 738 | // Row start 739 | text+=''; 740 | // List Item Index (Pos.) 741 | text+=''; 742 | 743 | // Action 744 | 745 | text+=''; 749 | 750 | 751 | 752 | 753 | 754 | // Portal Name 755 | 756 | text+=''; 768 | 769 | // Keys 770 | text+=''; 773 | 774 | // Fields (here: empty cell) 775 | text+=''; 776 | 777 | // other 778 | //text+=''; 781 | 782 | // Row End 783 | text+=''; 784 | text+='\n'; 785 | if (portal.outgoing.length > 0) { 786 | // DetailBlock Start 787 | text+=''; 788 | portal.outgoing.forEach(function(outPortal, outIndex) { 789 | let distance = thisplugin.distanceTo(portal.point, outPortal.point); 790 | let measure = 'm'; 791 | 792 | 793 | // Row start 794 | let linkDetailText=''; 795 | 796 | // List Item Index (Pos.) 797 | linkDetailText+=''; 798 | 799 | // Action 800 | linkDetailText+=''; 803 | 804 | let outPortalTitle = 'unknown title'; 805 | if (outPortal.portal !== undefined) { 806 | outPortalTitle = outPortal.portal.options.data.title; 807 | } 808 | // Portal Name (Target) 809 | linkDetailText+=''; 810 | 811 | // Keys (here: empty cell) 812 | linkDetailText+=''; 813 | 814 | // Link (Distance) 815 | linkDetailText+=''; 816 | // Fields 817 | let meta = portal.outgoingMeta?.[outPortal.guid]; 818 | let fieldsCreated = meta?.creatingFieldsWith?.length ?? 0; 819 | let triangles = fieldsCreated === 2 ? '▲▲' : fieldsCreated === 1 ? '▲' : ''; 820 | 821 | linkDetailText+=''; 822 | // other 823 | //linkDetailText+=''; 826 | // Row End 827 | linkDetailText+='\n'; 828 | text+=linkDetailText; 829 | }); 830 | text+='\n'; 831 | } // end if portal.outgoing.length > 0 832 | }); 833 | text+='
Pos.ActionPortal NameKeysLinksFields
' + (index) + ''; 746 | text+=' '; 747 | text+=' '; 748 | text+=''; 757 | const gmapsHref = `https://www.google.com/maps/dir/?api=1&destination=${lat},${lng}&query_destination_id=(${uriTitle})`; 758 | 759 | // Two links are rendered: 760 | // - UI link: uses onclick to interact with IITC (flyToPortal) 761 | // - Print link: real href for PDF / printing 762 | // Visibility is controlled via CSS (@media print or print window styles) 763 | text+=` ${title}`; 764 | text+=` ${title}`; 765 | 766 | 767 | text+=''; 771 | // Links 772 | text+='' + portal.outgoing.length + ''; 779 | //text+=''; 780 | //text+='
'; 834 | if (window.plugin.keys || window.plugin.LiveInventory) { 835 | text+='
Adjust available keys using your keys plugin.
'; 836 | }; 837 | text+='
'; 838 | gmnav+='&nav=1'; 839 | 840 | text += '
' + 841 | ' ' + 842 | '
'; 843 | 844 | 845 | text+='Navigate with Google Maps'; 846 | 847 | 848 | 849 | thisplugin.exportDialogWidth = 500; 850 | 851 | var width = thisplugin.exportDialogWidth; 852 | thisplugin.MaxDialogWidth = thisplugin.getMaxDialogWidth(); 853 | if (thisplugin.MaxDialogWidth < thisplugin.exportDialogWidth) { 854 | width = thisplugin.MaxDialogWidth; 855 | } 856 | 857 | const toggleFunction = function() { 858 | $('[plugin_fanfields2_exportText_toggle="toggle"]').each(function() { 859 | const $toggle = $(this); 860 | const $label = $toggle.prev('.plugin_fanfields2_exportText_Label'); 861 | const $details = $toggle.parents().next('.plugin_fanfields2_exportText_LinkDetails'); 862 | 863 | if ($details.length) { 864 | $label.addClass('has-children'); 865 | } else { 866 | $toggle.remove(); // Remove the checkbox if there are no child elements 867 | $label.css('cursor', 'default'); // Reset the cursor back to default 868 | } 869 | }); 870 | $('[plugin_fanfields2_exportText_toggle="toggle"]').change(function(){ 871 | const isChecked = $(this).is(':checked'); 872 | $(this).parents().next('.plugin_fanfields2_exportText_LinkDetails').toggle(); 873 | $(this).prev('.plugin_fanfields2_exportText_Label').attr('aria-expanded', isChecked); 874 | }); 875 | }; 876 | 877 | dialog({ 878 | html: text, 879 | id: 'plugin_fanfields2_alert_textExport', 880 | title: 'Fan Fields 2 - Task List', 881 | width: width, 882 | closeOnEscape: true 883 | }); 884 | toggleFunction(); 885 | 886 | $('#plugin_fanfields2_export_pdf_btn').off('click').on('click', function() { 887 | thisplugin.exportTaskListToPDF(); 888 | }); 889 | 890 | 891 | }; 892 | 893 | 894 | thisplugin.exportTaskListToPDF = function() { 895 | const id = 'plugin_fanfields2_alert_textExport'; 896 | 897 | // Resolve the actual dialog content element. 898 | // IITC/jQuery-UI may wrap the original element inside a dialog container. 899 | 900 | let $dlg = $('#dialog-' + id + ' .ui-dialog-content'); 901 | if (!$dlg.length) $dlg = $('#dialog-' + id); 902 | if (!$dlg.length) $dlg = $('#' + id); 903 | if (!$dlg.length) return; 904 | 905 | 906 | // Ensure all link detail rows are expanded before exporting 907 | 908 | $dlg.find('[plugin_fanfields2_exportText_toggle="toggle"]').each(function() { 909 | const $toggle = $(this); 910 | const $label = $toggle.prev('.plugin_fanfields2_exportText_Label'); 911 | const $details = $toggle.parents().next('.plugin_fanfields2_exportText_LinkDetails'); 912 | if ($details.length) { 913 | $toggle.prop('checked', true); 914 | $details.show(); 915 | $label.attr('aria-expanded', true); 916 | } 917 | }); 918 | 919 | const htmlInner = $dlg.html(); 920 | 921 | // open new window for printing 922 | const w = window.open('', '_blank'); 923 | if (!w) return; 924 | 925 | const css = ` 926 | @page { margin: 12mm; } 927 | body { font-family: Arial, sans-serif; font-size: 10pt; color: #000; } 928 | h1 { font-size: 14pt; margin: 0 0 10px 0; } 929 | 930 | table { width: 100%; border-collapse: collapse; } 931 | th, td { border: 1px solid #666; padding: 4px 6px; vertical-align: top; } 932 | thead th { background: #eee; } 933 | 934 | .plugin_fanfields2_exportText_ui { display: none !important; } 935 | .plugin_fanfields2_exportText_print { display: inline !important; color: #000; text-decoration: none; } 936 | 937 | tbody.plugin_fanfields2_exportText_Portal td { font-weight: bold !important; } 938 | tbody.plugin_fanfields2_exportText_LinkDetails td { font-weight: normal !important; } 939 | 940 | button, input[type="checkbox"] { display: none !important; } 941 | .plugin_fanfields2_exportText_Label::before { display: none !important; } 942 | 943 | .plugin_fanfields2_exportText_LinkDetails { display: table-row-group !important; } 944 | `; 945 | 946 | 947 | w.document.open(); 948 | w.document.write(` 949 | 950 | 951 | 952 | 953 | Fan Fields 2 – Tasks 954 | 955 | 956 | 957 |

Fan Fields 2 – Tasks

958 | ${htmlInner} 959 | 960 | 961 | `); 962 | w.document.close(); 963 | 964 | w.focus(); 965 | setTimeout(() => w.print(), 250); 966 | }; 967 | 968 | 969 | // ghi#23 start (3) 970 | // Manage-Order-Dialog 971 | thisplugin.showManageOrderDialog = function() { 972 | var that = thisplugin; 973 | let manageOrderDialogTitle = 'Fan Fields 2 - Manage Portal Order'; 974 | 975 | if (!that.sortedFanpoints || that.sortedFanpoints.length === 0) { 976 | var widthEmpty = 350; 977 | thisplugin.MaxDialogWidth = thisplugin.getMaxDialogWidth(); 978 | if (that.MaxDialogWidth < widthEmpty) widthEmpty = that.MaxDialogWidth; 979 | dialog({ 980 | html: '

No Fanfield plan calculated yet.
Draw a polygon and let Fanfields calculate first.

', 981 | id: 'plugin_fanfields2_order_dialog_empty', 982 | title: manageOrderDialogTitle, 983 | width: widthEmpty, 984 | closeOnEscape: true 985 | }); 986 | return; 987 | } 988 | var orderDirty = false; 989 | 990 | function buildTableHTML() { 991 | var html = ''; 992 | html += ''; 993 | html += ''; 994 | html += ''; 995 | html += ''; 996 | html += ''; 997 | html += ''; 998 | html += ''; 999 | html += ''; 1000 | 1001 | that.sortedFanpoints.forEach(function(fp, idx) { 1002 | var p = fp.portal; 1003 | var title = (p && p.options && p.options.data && p.options.data.title) ? p.options.data.title : 'unknown title'; 1004 | 1005 | var keys = fp.incoming ? fp.incoming.length : 0; 1006 | var out = fp.outgoing ? fp.outgoing.length : 0; 1007 | 1008 | var isAnchor = (fp.guid === that.startingpointGUID); 1009 | var trClass = isAnchor ? 'plugin_fanfields2_order_anchor' : 'plugin_fanfields2_order_row'; 1010 | 1011 | // Grip column: handle for normal rows, anchor icon for the pinned row. 1012 | var gripCell = isAnchor 1013 | ? '' 1014 | : ''; 1015 | 1016 | html += ''; 1017 | html += gripCell; 1018 | html += ''; 1019 | html += ''; 1020 | html += ''; 1021 | html += ''; 1022 | html += ''; 1023 | }); 1024 | 1025 | html += '
#PortalKeysLinks out
' + idx + '' + title + (isAnchor ? ' (anchor)' : '') + '' + keys + '' + out + '
'; 1026 | html += '
'; 1027 | html += 'Drag & drop rows to change visit order. First row (anchor) is fixed.
'; 1028 | html += 'Click Apply to use this order for the fanfield calculation.'; 1029 | html += '
'; 1030 | html += '
'; 1031 | html += ' '; 1032 | html += ' '; 1033 | html += ' '; 1034 | html += '
'; 1035 | return html; 1036 | } 1037 | 1038 | var width = 450; 1039 | thisplugin.MaxDialogWidth = thisplugin.getMaxDialogWidth(); 1040 | if (that.MaxDialogWidth < width) width = that.MaxDialogWidth; 1041 | 1042 | dialog({ 1043 | html: '
' + buildTableHTML() + '
', 1044 | id: 'plugin_fanfields2_order_dialog', 1045 | title: manageOrderDialogTitle, 1046 | width: width, 1047 | closeOnEscape: true 1048 | }); 1049 | 1050 | function initDragAndButtons() { 1051 | var $tbody = $('#plugin_fanfields2_order_table tbody'); 1052 | 1053 | function renumberRows() { 1054 | // Update the "#" column to match the current DOM order. 1055 | $tbody.find('tr').each(function(i) { 1056 | $(this).find('td.plugin_fanfields2_order_idx').text(i); 1057 | }); 1058 | } 1059 | 1060 | function pinAnchorRow() { 1061 | // Keep anchor row at top (and prevent it from being displaced). 1062 | var $anchor = $tbody.find('tr.plugin_fanfields2_order_anchor'); 1063 | if ($anchor.length && $tbody.children().first()[0] !== $anchor[0]) { 1064 | $tbody.prepend($anchor); 1065 | } 1066 | } 1067 | 1068 | // Destroy old sortable if the dialog is rebuilt. 1069 | if ($tbody.data('ui-sortable')) $tbody.sortable('destroy'); 1070 | 1071 | pinAnchorRow(); 1072 | renumberRows(); 1073 | 1074 | $tbody.sortable({ 1075 | // Only non-anchor rows are draggable. 1076 | items: '> tr.plugin_fanfields2_order_row', 1077 | handle: '.plugin_fanfields2_order_handle', 1078 | axis: 'y', 1079 | helper: 'clone', 1080 | forcePlaceholderSize: true, 1081 | placeholder: 'plugin_fanfields2_order_sort_placeholder', 1082 | 1083 | start: function(e, ui) { 1084 | orderDirty = true; 1085 | 1086 | if (that.showOrderPath) { 1087 | that.setOrderPathActive(false); 1088 | $('#plugin_fanfields2_order_path').text('Path'); 1089 | } 1090 | 1091 | // Keep column widths stable while dragging. 1092 | ui.helper.children().each(function(i) { 1093 | $(this).width(ui.item.children().eq(i).width()); 1094 | }); 1095 | 1096 | // Make placeholder span the full row width. 1097 | var colCount = ui.item.children('td,th').length; 1098 | ui.placeholder 1099 | .addClass('plugin_fanfields2_order_sort_placeholder') 1100 | .html(' '); 1101 | 1102 | // Prevent placeholder from going above the anchor row. 1103 | var $anchor = $tbody.find('> tr.plugin_fanfields2_order_anchor'); 1104 | if ($anchor.length && ui.placeholder.index() === 0) { 1105 | ui.placeholder.insertAfter($anchor); 1106 | } 1107 | }, 1108 | 1109 | change: function(e, ui) { 1110 | // Prevent dropping above the anchor row. 1111 | var $anchor = $tbody.find('> tr.plugin_fanfields2_order_anchor'); 1112 | if ($anchor.length && ui.placeholder.index() === 0) { 1113 | ui.placeholder.insertAfter($anchor); 1114 | } 1115 | }, 1116 | 1117 | update: function() { 1118 | pinAnchorRow(); 1119 | renumberRows(); 1120 | } 1121 | }); 1122 | 1123 | // Rebind Reset and Apply buttons 1124 | $('#plugin_fanfields2_order_reset').off('click').on('click', function() { 1125 | that.manualOrderGuids = null; 1126 | that.updateLayer(); 1127 | 1128 | orderDirty = false; 1129 | 1130 | $('#plugin_fanfields2_order_dialog_inner').html(buildTableHTML()); 1131 | initDragAndButtons(); 1132 | 1133 | if (that.showOrderPath) { 1134 | that.updateOrderPath(); 1135 | } 1136 | }); 1137 | 1138 | $('#plugin_fanfields2_order_apply').off('click').on('click', function() { 1139 | var guids = []; 1140 | $('#plugin_fanfields2_order_table tbody tr').each(function() { 1141 | guids.push($(this).data('guid')); 1142 | }); 1143 | 1144 | // The first entry must remain the anchor. 1145 | if (guids[0] !== that.startingpointGUID) { 1146 | that.manualOrderGuids = null; 1147 | } else { 1148 | that.manualOrderGuids = guids; 1149 | } 1150 | 1151 | orderDirty = false; 1152 | 1153 | that.delayedUpdateLayer(0.2); 1154 | $('#plugin_fanfields2_order_dialog').dialog('close'); 1155 | }); 1156 | 1157 | $('#plugin_fanfields2_order_path').off('click').on('click', function() { 1158 | var newState = !that.showOrderPath; 1159 | that.setOrderPathActive(newState); 1160 | $(this).text(newState ? 'Hide path' : 'Path'); 1161 | }); 1162 | 1163 | $('#plugin_fanfields2_order_path').text(that.showOrderPath ? 'Hide path' : 'Path'); 1164 | } 1165 | 1166 | initDragAndButtons(); 1167 | }; 1168 | 1169 | 1170 | // ghi#23 end (3) 1171 | 1172 | 1173 | thisplugin.respectCurrentLinks = false; 1174 | thisplugin.toggleRespectCurrentLinks = function() { 1175 | thisplugin.respectCurrentLinks = !thisplugin.respectCurrentLinks; 1176 | if (thisplugin.respectCurrentLinks) { 1177 | $('#plugin_fanfields2_respectbtn').html('Respect Intel: ON'); 1178 | } else { 1179 | $('#plugin_fanfields2_respectbtn').html('Respect Intel: OFF'); 1180 | } 1181 | thisplugin.delayedUpdateLayer(0.2); 1182 | }; 1183 | 1184 | thisplugin.indicateLinkDirection = true; 1185 | thisplugin.toggleLinkDirIndicator = function() { 1186 | thisplugin.indicateLinkDirection = !thisplugin.indicateLinkDirection; 1187 | if (thisplugin.indicateLinkDirection) { 1188 | $('#plugin_fanfields2_direction_indicator_btn').html('Show link dir: ON'); 1189 | } else { 1190 | $('#plugin_fanfields2_direction_indicator_btn').html('Show link dir: OFF'); 1191 | } 1192 | thisplugin.delayedUpdateLayer(0.2); 1193 | }; 1194 | 1195 | thisplugin.is_locked = false; 1196 | thisplugin.lock = function() { 1197 | thisplugin.is_locked = !thisplugin.is_locked; 1198 | if (thisplugin.is_locked) { 1199 | $('#plugin_fanfields2_lockbtn').html('🔒 Locked'); // 🔒 1200 | } else { 1201 | $('#plugin_fanfields2_lockbtn').html('🔓 Unlocked'); // 🔓 1202 | } 1203 | }; 1204 | 1205 | thisplugin.use_bookmarks_only = false; 1206 | thisplugin.useBookmarksOnly = function () { 1207 | thisplugin.use_bookmarks_only = !thisplugin.use_bookmarks_only; 1208 | if (thisplugin.use_bookmarks_only) { 1209 | $('#plugin_fanfields2_bookarks_only_btn').html( 1210 | '🔖 Bookmarks only' 1211 | ); 1212 | } else { 1213 | $('#plugin_fanfields2_bookarks_only_btn').html( 1214 | '🔖 All Portals' 1215 | ); 1216 | } 1217 | thisplugin.delayedUpdateLayer(0.2); 1218 | }; 1219 | 1220 | 1221 | thisplugin.is_clockwise = true; 1222 | thisplugin.toggleclockwise = function() { 1223 | thisplugin.is_clockwise = !thisplugin.is_clockwise; 1224 | var clockwiseSymbol="", clockwiseWord=""; 1225 | if (thisplugin.is_clockwise) { 1226 | clockwiseSymbol = "↻" 1227 | clockwiseWord = "Clockwise"; 1228 | } 1229 | else { 1230 | clockwiseSymbol = "↺" 1231 | clockwiseWord = "Counterclockwise"; 1232 | } 1233 | 1234 | // Reset the order – new geometry, new base ordering (ghi#23) 1235 | thisplugin.manualOrderGuids = null; 1236 | 1237 | $('#plugin_fanfields2_clckwsbtn').html(clockwiseWord+' '+clockwiseSymbol+''); 1238 | thisplugin.delayedUpdateLayer(0.2); 1239 | }; 1240 | 1241 | thisplugin.starDirENUM = {CENTRALIZING:-1, RADIATING: 1}; 1242 | thisplugin.stardirection = thisplugin.starDirENUM.CENTRALIZING; 1243 | 1244 | thisplugin.toggleStarDirection = function() { 1245 | thisplugin.stardirection *= -1; 1246 | var html = "Outbounding"; 1247 | 1248 | if (thisplugin.stardirection == thisplugin.starDirENUM.CENTRALIZING) { 1249 | html = "Inbounding"; 1250 | $('#plugin_fanfields2_availablesbul').hide(); 1251 | } 1252 | else { 1253 | $('#plugin_fanfields2_availablesbul').show(); 1254 | } 1255 | 1256 | 1257 | $('#plugin_fanfields2_stardirbtn').html(html); 1258 | thisplugin.delayedUpdateLayer(0.2); 1259 | }; 1260 | 1261 | 1262 | 1263 | thisplugin.increaseSBUL = function() { 1264 | if (thisplugin.availableSBUL < 4) { 1265 | thisplugin.availableSBUL++; 1266 | $('#plugin_fanfields2_availablesbul_count').html(''+(thisplugin.availableSBUL)+''); 1267 | thisplugin.delayedUpdateLayer(0.2); 1268 | } 1269 | } 1270 | thisplugin.decreaseSBUL = function() { 1271 | if (thisplugin.availableSBUL > 0) { 1272 | thisplugin.availableSBUL--; 1273 | $('#plugin_fanfields2_availablesbul_count').html(''+(thisplugin.availableSBUL)+''); 1274 | thisplugin.delayedUpdateLayer(0.2); 1275 | } 1276 | } 1277 | 1278 | 1279 | thisplugin.setupCSS = function() { 1280 | 1281 | // Collect CSS in one place and inject/update a single