UROverview Plus (URO+)

Adds a whole bunch of features to WME, which someday I may get around to documenting properly...

// ==UserScript==
// @name                UROverview Plus (URO+)
// @namespace           http://greasemonkey.chizzum.com
// @description         Adds a whole bunch of features to WME, which someday I may get around to documenting properly...
// @include             https://*.waze.com/*editor*
// @exclude             https://editor-beta.waze.com/*
// @exclude             https://beta.waze.com/*
// @exclude             https://www.waze.com/user/*editor/*
// @exclude             https://www.waze.com/*/user/*editor/*
// @grant               none
// @version             4.9
// ==/UserScript==

/*
=======================================================================================================================
Bug fixes - MUST BE CLEARED BEFORE RELEASE
=======================================================================================================================

=======================================================================================================================
Things to be checked
=======================================================================================================================

*/

/* JSHint Directives */
/* globals $: */
/* globals W: true */
/* globals I18n: */
/* globals OpenLayers: true */
/* globals require: */
/* globals ResizeObserver: */
/* globals _: */
/* globals trustedTypes: */
/* jshint bitwise: false */
/* jshint eqnull: true */
/* jshint esversion: 11 */
/* jshint undef: true */
/* jshint unused: true */

const uroRelease =
{
   version : "4.9",
   date : "20241005",
   changes : 
   [
      "Compatibility updates"
   ]
};

const uroEnums =
{
   FP_OPTS: 
   {
      filterUneditable: 0,
      filterInsideManagedAreas: 1,
      excludeMyAreas: 2,
      filterLockRanked: 3,
      filterFlagged: 4,
      filterNewPlace: 5,
      filterUpdatedDetails: 6,
      filterNewPhoto: 7,
      filterMinPURAge: 8,
      filterMaxPURAge: 9,
      invertPURFilters: 10,
      filterHighSeverity: 11,
      filterMedSeverity: 12,
      filterLowSeverity: 13,
      leavePURGeos: 14,
      filterCFPhone: 15,
      filterCFName: 16,
      filterCFEntryExitPoints: 17,
      filterCFOpeningHours: 18,
      filterCFAliases: 19,
      filterCFServices: 20,
      filterCFGeometry: 21,
      filterCFHouseNumber: 22,
      filterCFCategories: 23,
      filterCFDescription: 24,
      filterOnCFs: 25,
      thresholdMinPURDays: 26,
      thresholdMaxPURDays: 27,
      isLoggedIn: 28,
      userRank: 29,
      N_OPTS: 30
   },
   TRTC:
   {
      UNKNOWN: 0,
      WME: 1,
      WAZEFEED: 2,
      WAZEOTHER: 3
   },
   SRTC: 
   {
      UNKNOWN: 0,
      EXPIRED: 1,
      ACTIVE: 2,
      FUTURE: 3
   },
   DRTC:
   {
      NONE: 0,
      SEG_AB: 1,
      SEG_BA: 2,
      SEG_BI: 4,
      TURN_OUT: 8,
      TURN_IN: 16
   }
};
const uroCustomURTags = ['[ROADWORKS]','[CONSTRUCTION]','[CLOSURE]','[EVENT]','[NOTE]','[WSLM]','[BOG]','[DIFFICULT]'];
const uroImages = 
{
   HighlightedCameraImages :
   [
      // speed
      ["data:image/png;base64,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azfinpZRQzEk7D8r+zZU+QJElM9lB9ZKQ97r94TdFAxoodUbxwvk96MUZjoyrw/F6L1V7Qxi094DR/vddH30CQt12fUxoj6lBw05PbRDhavxEtUCHHUVipwF3pcZy7GQakgXfFVQdj21CSixLg2y08d7JiyojwU0Sqfq82gjg3TvbyJce/RxwFeF+Z/d3Tdq7pcLf/fr//GN9DEEfMfz3zqJ2QWvhHQ9AAAAYHpUWHRSYXcgcHJvZmlsZSB0eXBlIGlwdGMAAHjaPUnLDYBQDLp3CkeghWhdx76LNw/uH+tLFALhY+d1ly0TCmMqtGtAzR8+vBDcOiaDoDPb+Wo+1c/R60q1g7AYXwXsATEgFNfoGvlqAAAPW2lUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4KPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNC40LjAtRXhpdjIiPgogPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgeG1sbnM6aXB0Y0V4dD0iaHR0cDovL2lwdGMub3JnL3N0ZC9JcHRjNHhtcEV4dC8yMDA4LTAyLTI5LyIKICAgIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIgogICAgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIKICAgIHhtbG5zOnBsdXM9Imh0dHA6Ly9ucy51c2VwbHVzLm9yZy9sZGYveG1wLzEuMC8iCiAgICB4bWxuczpHSU1QPSJodHRwOi8vd3d3LmdpbXAub3JnL3htcC8iCiAgICB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iCiAgICB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iCiAgIHhtcE1NOkRvY3VtZW50SUQ9ImdpbXA6ZG9jaWQ6Z2ltcDo0YTZmYzJkMy0zNzBiLTRkMzYtOGIzNS02NmRmMjhiYzM2OTgiCiAgIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NmIzYTcxYTUtNjQxYS00YTU5LWE3OGQtODQ2YzFhYjYxODM5IgogICB4bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ9InhtcC5kaWQ6ZWU0ZGEyZGItNzcwMC00MDYyLTk5ZjItZmNjNzNlODA4NzYwIgogICBHSU1QOkFQST0iMi4wIgogICBHSU1QOlBsYXRmb3JtPSJXaW5kb3dzIgogICBHSU1QOlRpbWVTdGFtcD0iMTUyMDA5MTY2MjU3MzQxNCIKICAgR0lNUDpWZXJzaW9uPSIyLjkuOCIKICAgZGM6Rm9ybWF0PSJpbWFnZS9wbmciCiAgIHhtcDpDcmVhdG9yVG9vbD0iR0lNUCAyLjkvMi4xMCI+CiAgIDxpcHRjRXh0OkxvY2F0aW9uQ3JlYXRlZD4KICAgIDxyZGY6QmFnLz4KICAgPC9pcHRjRXh0OkxvY2F0aW9uQ3JlYXRlZD4KICAgPGlwdGNFeHQ6TG9jYXRpb25TaG93bj4KICAgIDxyZGY6QmFnLz4KICAgPC9pcHRjRXh0OkxvY2F0aW9uU2hvd24+CiAgIDxpcHRjRXh0OkFydHdvcmtPck9iamVjdD4KICAgIDxyZGY6QmFnLz4KICAgPC9pcHRjRXh0OkFydHdvcmtPck9iamVjdD4KICAgPGlwdGNFeHQ6UmVnaXN0cnlJZD4KICAgIDxyZGY6QmFnLz4KICAgPC9pcHRjRXh0OlJlZ2lzdHJ5SWQ+CiAgIDx4bXBNTTpIaXN0b3J5PgogICAgPHJkZjpTZXE+CiAgICAgPHJkZjpsaQogICAgICBzdEV2dDphY3Rpb249InNhdmVkIgogICAgICBzdEV2dDpjaGFuZ2VkPSIvIgogICAgICBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOjljMjhhYmUwLTJiNTEtNDcwYy1hZjYxLTIzNWE5Yjk3ZjcxNyIKICAgICAgc3RFdnQ6c29mdHdhcmVBZ2VudD0iR2ltcCAyLjkvMi4xMCAoV2luZG93cykiCiAgICAgIHN0RXZ0OndoZW49IjIwMTgtMDMtMDNUMTU6NDE6MDIiLz4KICAgIDwvcmRmOlNlcT4KICAgPC94bXBNTTpIaXN0b3J5PgogICA8cGx1czpJbWFnZVN1cHBsaWVyPgogICAgPHJkZjpTZXEvPgogICA8L3BsdXM6SW1hZ2VTdXBwbGllcj4KICAgPHBsdXM6SW1hZ2VDcmVhdG9yPgogICAgPHJkZjpTZXEvPgogICA8L3BsdXM6SW1hZ2VDcmVhdG9yPgogICA8cGx1czpDb3B5cmlnaHRPd25lcj4KICAgIDxyZGY6U2VxLz4KICAgPC9wbHVzOkNvcHlyaWdodE93bmVyPgogICA8cGx1czpMaWNlbnNvcj4KICAgIDxyZGY6U2VxLz4KICAgPC9wbHVzOkxpY2Vuc29yPgogIDwvcmRmOkRlc2NyaXB0aW9uPgogPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgIAo8P3hwYWNrZXQgZW5kPSJ3Ij8+txeqYAAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAAuIwAALiMBeKU/dgAAAAd0SU1FB+IDAw8pAmNZfO0AAA7kSURBVHja1Zx5kB1FHcc/8459b+9N2GSPhBiBBAQBBeVSI4GEoFQFSIASj+CRshDFQstAgXiiJcT7SLQKUVkQhOIIRVFA1gSSYCFHAgIakFSwSNjdhGU3yR7v7b43M/7Rr/N6erpn3stuTJyqrp7p6Zk3/X2/4/v7dc84HLrNiTme6ObHHFe1pQ4hOM4kA+cfpL4H5V+sFiS9LepY7W+SFj/mGEvbYQuUqTYVUx8bOL7Wphci6sNa9UzAJLRaBSdhuZ9nAMjTah0s50BUL3WI1M4ETkI5Thr6mFRP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"],
      // dummy
      ["data:image/png;base64,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LmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlRXZlbnQjIgogICAgeG1sbnM6cGx1cz0iaHR0cDovL25zLnVzZXBsdXMub3JnL2xkZi94bXAvMS4wLyIKICAgIHhtbG5zOkdJTVA9Imh0dHA6Ly93d3cuZ2ltcC5vcmcveG1wLyIKICAgIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIKICAgIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIKICAgeG1wTU06RG9jdW1lbnRJRD0iZ2ltcDpkb2NpZDpnaW1wOmViM2YzYTk1LWFmZDMtNDNiMS1hZTQ5LTNhN2IwMDk0Mjg0ZiIKICAgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo1NjJmOTkxYi1kNjk2LTRhNjItYjFjNS1mMTFhNzAwMTIzOTciCiAgIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDpmZmFiNmU3ZS03ZWNiLTQ2OGEtOGJlOC0yMjUwYjM0OTU1MjciCiAgIEdJTVA6QVBJPSIyLjAiCiAgIEdJTVA6UGxhdGZvcm09IldpbmRvd3MiCiAgIEdJTVA6VGltZVN0YW1wPSIxNTIwMDkxNjg5MDI1OTI3IgogICBHSU1QOlZlcnNpb249IjIuOS44IgogICBkYzpGb3JtYXQ9ImltYWdlL3BuZyIKICAgeG1wOkNyZWF0b3JUb29sPSJHSU1QIDIuOS8yLjEwIj4KICAgPGlwdGNFeHQ6TG9jYXRpb25DcmVhdGVkPgogICAgPHJkZjpCYWcvPgogICA8L2lwdGNFeHQ6TG9jYXRpb25DcmVhdGVkPgogICA8aXB0Y0V4dDpMb2NhdGlvblNob3duPgogICAgPHJkZjpCYWcvPgogICA8L2lwdGNFeHQ6TG9jYXRpb25TaG93bj4KICAgPGlwdGNFeHQ6QXJ0d29ya09yT2JqZWN0PgogICAgPHJkZjpCYWcvPgogICA8L2lwdGNFeHQ6QXJ0d29ya09yT2JqZWN0PgogICA8aXB0Y0V4dDpSZWdpc3RyeUlkPgogICAgPHJkZjpCYWcvPgogICA8L2lwdGNFeHQ6UmVnaXN0cnlJZD4KICAgPHhtcE1NOkhpc3Rvcnk+CiAgICA8cmRmOlNlcT4KICAgICA8cmRmOmxpCiAgICAgIHN0RXZ0OmFjdGlvbj0ic2F2ZWQiCiAgICAgIHN0RXZ0OmNoYW5nZWQ9Ii8iCiAgICAgIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6ZjZhNTRjMGMtZWI0ZS00YmZkLWI5NjctYjQyMWFmMzdiOWVkIgogICAgICBzdEV2dDpzb2Z0d2FyZUFnZW50PSJHaW1wIDIuOS8yLjEwIChXaW5kb3dzKSIKICAgICAgc3RFdnQ6d2hlbj0iMjAxOC0wMy0wM1QxNTo0MToyOSIvPgogICAgPC9yZGY6U2VxPgogICA8L3htcE1NOkhpc3Rvcnk+CiAgIDxwbHVzOkltYWdlU3VwcGxpZXI+CiAgICA8cmRmOlNlcS8+CiAgIDwvcGx1czpJbWFnZVN1cHBsaWVyPgogICA8cGx1czpJbWFnZUNyZWF0b3I+CiAgICA8cmRmOlNlcS8+CiAgIDwvcGx1czpJbWFnZUNyZWF0b3I+CiAgIDxwbHVzOkNvcHlyaWdodE93bmVyPgogICAgPHJkZjpTZXEvPgogICA8L3BsdXM6Q29weXJpZ2h0T3duZXI+CiAgIDxwbHVzOkxpY2Vuc29yPgogICAgPHJkZjpTZXEvPgogICA8L3BsdXM6TGljZW5zb3I+CiAgPC9yZGY6RGVzY3JpcHRpb24+CiA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgCjw/eHBhY2tldCBlbmQ9InciPz73ndT8AAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAB3RJTUUH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"],
      // rlc
      ["data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEIAAABACAYAAACunKHjAAAXtnpUWHRSYXcgcHJvZmlsZSB0eXBlIGV4aWYAAHja7ZpXcuW4lkX/MYoeAtyBGQ5sRM+gh99rkzeVprKqnvvpiJaq8koUCQLHbAPSnf/57+v+i68SWnTZaiu9FM9X7rnHwQ/Nv1/vZ/D5+ff5yvHzt/DzcZc/x33kUOIzvb+W8zl/cNy+X1A/F4T583FX12ec9hkofA38fCXdWT/PzyQ/A6X4Hg+f313/XDfyD8v5/B/XZ9jP4L/+nivB2MZ4Kbp4Uuf/9t4lMYPU0uAzvf9GHSn8nPltcEb8fezc14+/BO+238fOj88Z6edQOF8+J5RfYvQ5HuyX4+kra/GnGYVvP8af/+BvqP7Hrx9id+9u9553dSMXIlXcZ1HhncI7DU4kRzk9lxW+K/8bP9fnu/PdWOIiY9sv8jn9cqGHGBL3z2GHEW44z+cKiynmeGLlM8YV03OspRp7XE9Ssr7DjTX1tB05immRt8Th+DWX8Ny3P/dboXHnHTgzBgYLyt2v3+53B/+V76+B7lWJh/AEsz0hZ15RVcM0lDn9y1kkJNxP3uwTX77dV5a+fymxiQzaE+bGAoef7xDTwvfaSk+eE+eZz86/rRHq/gxAiLi3MZmQyIAvIVkowdcYawjEsZGfwcxjynGSgWAWd3CX3KRUSE6LujfX1PCcGy2+h4EWEmE0SiU1PQ2SlbNRPzU3amhYsuzMrFi1Zt1GSSUXK6XUIowaNdVcrZZaa6u9jpZabtZKq6213kaPPQFh1kuvrrfe+xjcdDD04OrBGWPMONPM02aZdbbZ51iUz8rLVll1tdXX2HGnTfvvsqvbbfc9TjiU0snHTjn1tNPPuNTaTTdfu+XW226/4ytrn6z+nLXwS+b+OmvhkzVlLD/n1e9Z43Ct34YIghNTzshYzIGMV2WAgo7KmW8h56jMKWe+R5rCIlkLpuTsoIyRwXxCtBu+cvc9c3+ZN2f5n8pb/LPMOaXuP5E5p9R9MvfHvP0ma1u8t56MvV2omPp0ATZOOG3ENsRJ//Kn+3cH+L820JkT6K7NBsmxfaInaVR3LyemNtK00pvqLCc0wmlpNpc4dZW5U7on5I40ueDfqZeT9dO8M+jzhkkW6bJZ2yh3wgajr15vAZDm6i4uMJ4a69vaODuuUScIf9eIcw8mxoX7WrpBF/V7S8uhdICwV6qo31Ip4XXc2bevc0q6ewe7cfeTFrSzhtlp61Bs0XY9udw8ezjtwkfhtli5x1XpbKv3XHdWpbHL6VML8MPqShxJd46hI/thVRHC3nVYGLTT2kxulTQvpTtTYVZBA2mMFXu5Cs2654xnUE5OT7xi7KMev2g3AllvNfs5esFOR0R0mgvtd9vRYfpg2VknMRFNDda2lMihlrE3wTr3XUUGam5L9v7N/frHOhLTp2OtT1BDk4fQmbumzpKY+rr+PlNnAve+Ez+OeX+iU4nOVnQYpd7vsWnjZnjiEPz4zBA8WqcBzn2uHQ9tPxYx8i2VsgEf5MGqsBe4d2D7M+qOcyZP3aw1g6DpdMu15nFnBytXjBRMOLVzB8cP+2apEfCjg2U3TgC0rA3UdG/tnGwldQidCkyUVogTllu7ADt2+ag7n+N6jLmfuufsBTyPbY8Rckk6iiCqY11NYKmOSwdO82WG455ZuB4knawyW0CwlxH9eLrM/xuf7vMDLXNbeOomvdUx65N81mLUghGy6wswao1iLXMxozkneKuT73KX0EEoG0gudtEOV2pLbGORaiaCrKiVST9MBJyK+an4g1ZWLYxqnnYW1Hb+SjW2twioDyvhwB177Z7yTkbT0u7P5T+fW6ic2d/j7v0D9ZdRjBDFU+lUAHW0dArjMvFKJeX30HOA4xwy8gJU6aLpTm0XNro3mq+khPU/Jw44ce5ZKQ467Kb9xA+hSkv0i4DaI9+wyNQ5KkC3xnomN0vpdnNY0N1eNSCO6CCjDxq4ly7VWMoDoMSKsvoVVN0/CsuUfErTm4Uey5zx0lMr0R/TegcQHeIlzwuhApv1MIPtSeBCP+yEThemUghC8CW3IyQqYFBdcQ/biTgkNelxZbUZWSuZ3xQEOO8v8GKH/AKRG2MUd1tGDdN2PfllnVqZu0/mRvljOiu94zRin/SI5x4MEHeBniNwSAHkPfqWVDGCckiJp+EBxpKbygpJcpULMr9dJPiWVBrQfRubsm4k73iYiMmANJnxnoQD3QCsqoPZjoPu2QM0BAAAGoeGoPpzHCRqA6tN5Xp9ftoD1azPWMZLUswK0A346UiJN/8gWUc41UuwWcJ4QBGF1E56YDm/YBdByBJWFzyGoiFY9TMk7uT7IBrDoYdyxfPIMaUlyF3T030Q0QQlzsWPbQozNRK4gJ5dc8grIaTaWaBKu3GdkV0fNxWqsMQ5cIsbmlYbxLcNaKoGCZ8VyjPHtPgIh1knq36nOKMl6iZsYKQV8iWUh0ohR2BdAeXY/DpWoFQtfj5X+dhpr6nKP087tRuGK59j6mCO6tis6vk8Km2EPkW9fp1R3zO4S06L38MAJln9XA4ioEz3hBvC3Cyq+/IoZYqbDves2B4wYTZLDV8O1ZTyXX3mPKnII7tzHDXQMvN/4LEnGTd1ueUNZZZ9AosN1Br1tNZ86wkN/NYTgrqENlkJvUa31DeQocaMFFkwEfIFbkkXZoCYSwdd90nUPKu4own8Ln1HuUhJgcTT5QrEUmb0cyz0CbqkIWBH2+TUH1axxSV3ROZYypjYLyEvnes32TwUe7hrOZ80RsLGJTzcYb67MKfaEWXoH0JDNz6hDiVn9DlzvlhBAKU1FDYuAnF+FoqtgHkKX9AFlpenbzMUG1Zu1Nxawx9AIawG7p1pVCXKHIGzPf/RpyQlTWflJSe5VRPTYXL+8Ekxs8hNERGbIIEP9uQMN12q1TqazfEbXgV6AuCGdNOESjPhnieCvtqLqOmAKfdCatfWRczJFxAGtQvIcDwC1XFhI5LSaYNZj4xGLbO0yVQo0QLURZMUnCoEn4StqMHR/KCQlOvTM9F39OcQOhoWBoCjFwRvncw8iFBfXMiPaMIwITQm5JUj0nhTltHvTC67d3ReRslSXlStfy4r/Sk3IvnTaFSDSpwL6GP8UAy6lv8OIwC1pT2c+MPpkgFkcbRrwPuOAe/m0Stg9soDacsdqNZFFRxuDT3Q4I4lgVK4UhRW60GpWn7hnVadhtQCawLjEPJ+4NgkOkX1LEtjMpxI4DKqwB+ST0J3HCVBQrluJpLXfKsCIF/5T0lvAgt4UfUaSpSehaj3S9SFIhAD2ueE3/5d7bsXeENfG60O1IDZMZ1+n6hdRY0/cGguxVnUGoQZycDtGGarSCOPdvcTSjBPNVLXBZBhRiyGZQENIXEooSSQ+BDh9QuTrL1G34AFqo4mwS+B4VbOmlgWPDO9A8Td5LxQGQTF1Gb+2a1HyATnnFQRde6MqvaNq9GVNSJSz5QDy/lV/7hmoJDuB6MuJYupVtnT8/RnO6ZdBqa+0HK1I2WwDXBvK4Av+qRg+ePKuVDgxWZCfjm4as22cOGFaq+tm7aSdv0qU9JOv/xNofrtFOWlev2lUn9TqJwEutN84g8g2iArdPLrHBy1hL5q9uenvGdsuS+UyKU+reyODxTGULNoAP/o7N8jEIQI2gH5kH17CAE5TA2jo0zw/+vf3c8ndAhx9tfNeqqOWsnpkPydzrcogRkiBUL/KF7+ivDYjjMwFpBwHf7mq9qKeJIIOcI5FXmLVUDlURURuqA2A+nenSTugHNCzWT6tTjEF7pL2yIbtZo4vVJtiCo0KtUXsY1oUYZdRyLdlNKIsxkd250QPGLNbdcBVigDRi4LQCWyNwPwPRSxIg4JyVAP/ItS3AuDQ4lWo/P8xUbDdZjlchBcDh+G/kUYYMGoR2QzAI6ygNS7/wZjWI+dTTHRLhaySeyBHNqPoOoo7O4gt109KcR2YzbCemBqExnCllTzDHHL43dHeUQEdjJSfCv0jV5E0uBOt5N/Dncf7MY5T5qNpeF0kZOwelizyt83wk6ImGaXb9ja3kCFlQp9Z3xad9Mv8KziBqSrjUXggewtcfT2eLI+zD9V/xdV7v5Yw43j/L5eLRsB5tffM0257lZQtVDJU2sNc5ETjDtdJLWvxRj48j+3iAf1GxCnQNfdz8CITI1LzYmgnKwc/X4MGHmkQsBFz4ExooQJ1JYsRW9uDG+2tfLxBXWFN8FRY3caFZr38e5g1e5CkJBCw+X3cC0+XY80OEn7POh6v/rQNoGnjD9iK3Ey4PMa15wcjPv82LBiUsyomJ4jMMK0QX0qAP9NFQtTIHWM+w0NhEuKFqQnGydX6gj8UBeH+Dbqq/iX1DcTKM9Gh5lp6PIZGg8r30/iClooiEpQ/owb8S0a99njYNjxbIeRO2FPFSktv18oIaoIdUDyQgTnSJx/xnTlAfEV/+LSwmqkTGU/sHHPXf17Xz1/m8/I7jM0uSpCZisW3suuLqPT58+XFW3YGDzxBIH7IhIeqIXoH/O9tImEsqvoHLCt2Q4JMu+QMo0Iw6Euc8NjYkxYD3HHhYFq9G9+B8I8fvyYn/m9K7SlwhbzoOegobxKVAEBS/QxzsLmOg05usrC0yExDRZ5lhSQc54qRIUyLLAHp0mFgkGQaQHz0IhDuzSy47t3pBcs57M2mnGzA4SEJk/FgBftUyJ20QZjYIioEaD6ylYWlMrWMyOaKhFVKRo7oCNnJ5UIFmKIJtcYWBUE+QBloNf9Heh7VISxbhSrCKkhDLgSO/UGZASMGlLQ/d0pRduNCBm1SWT27Us4wc8/CCb3BwUFgUxomuGNOHUEkfbL4QTCNOSsESOEB+29cdxCHVYUh4OKuLDI7iwMG3C6RoIYplQkipWmX11lhvgDYfW4rSJ7YcDQA8EasGDASThVVJR0xzNkQFSJHyu92yA4mR/miiASIcpyDWJf+/NArWSaIxw3ZZMpEBLx1GrBmjyw5ZOUHUSLiDbIz7QNzNTDiGOBkSijgChdM82W13V5U8I0vaY7Rs/YqibEQPTJ+1XIF2G7WsisgYSiCrGhGRstcoTIhkd1VYTWld83OM5akAHHNVYZkhT25hABtxyStByMNi6cEucGmSl0YtdH7My6bBc1IQ7g0WBNumOBXjcSR0AClk+xHZJUR/VYuZsQZoZaQJLZOQ9H9lDHkRdBOqAD5eS1b5DxUrBrTiSX2EIMhrPk9K0yxkvCxFyrJUKNdyp/kSw7Xw+5b6EfQmKG2cOZ2pTNfjTDmjsih7Vz1/bEXcyD9EZ/wHbafUeooiOCd+hEzigSCRiFH8UelVN+oLhvBPcTvfmkbSMog6WdLBsLamOfywex0T7Q7n6dbBUuIkCotS1Bh+lYxT/SzvzXpws2wrDTK6WE9M6i24kvjEhrNBmSJFHDSAUsDRmNGGko36OakApJ0ZEc68v1ZaCG+CWjUQAqeUAAmyJAI3EVFaXN7wG4VngCFGXhhyzqjQw9cRzau4sOME/oslILrZPmwYlGAJbOAhv1QANYPt5ar0W70NiNnSdiVlZ+XO1sITTr3k7q3uJArFuLGFkmnKltYguboNrQPEH2opa7tJvd5g1wLG2pJzYdcq9YkpgdVhj12DyGnAlRutlr0yMi2xBHiFoML7pARGjETUaDMLS55BAztdXjrrMfSb8VUVmJ/olRr08QVHgj6vkfEmTpxQrpgH20yYFaD3jSi1/rGCfyK5LHBjmiqP0lSzQe3QxnPuxcM+AEqWEJUL+sBZY08X7FaLDSWEhfEGd5baQFssaSMDmsi/q/EAReCTtXe58lakd7t4MIGRc1q30bAYlRr7vAEtrEM+aIFXQIniTLICCCKYufHac49ZCrQO0bCa4HCMoqRLfgK3oixa79IEqNMd70OmU4z0WYAUFEOWsGLYGBHJG2UMOgDnGXIOMCFIDjSDMV6LPAZJAn91y1a0OT5qSG1A/Uxd2lgl56etrvjFQrrvGKphHisnQKAiaSkmdpmXaqgBEHqGztVM85/WwbhARo9YxXuZV9f5BF8h0rKg7GBEgcVCMXhHFe5KhugBphNOw5cI7qJl5rBwRNKXIl2u4BO4Og4hi15AFKuMAiDZ26ZNdOqZ25DPBn1tgVtOBCqxBmgr4AvXNZrLwNN08UM6rb56OneVOPJ2jLrR1okz+YVKgL+bboD0DXEuhStO0wkZOBzFOzsGs/vWixIJDeOwH5fiTzb1bXactG7fbBp1/h6QUn9MjfUbv7R7n9/ZwKEZQr+a1KPtp2vwfF6BRM9P0qjTUtbR8SOti6exlDGqqhsFBZbWWmSKtR3ziIqofyBekTwTnwHeUvh4UYQMSfCr7gKBeGBLGTuKGBLl1Wa83+mmrIcOdvJlrPYQhlyo85ptALCmfJ7ZBBPbDc2vXUFonpvKtt5zVYbNImH+HZ2qnlc5j23JjXRGjV2PBlrdb6mjvmSsZatNBhyArt2R0TSMGzQfwUiR5IQ6ecuBR1PRAZ1yGCfCSCepghStZTZsobBVxAF+2fl+XFkLKsXBT1cEFrDbg9XA9sHBba3Hlx9UBJVcgCUQBKgaY0ERNAB6ZbJsiAPpj0MWapjFiwmHh5bX1on0XvaExz/DhN2y7oGLo4PCVCv2ZADvB4VD6GnT48Iy0U1yBd6CDwhWam8NLFT4fu9kTxUnOytrSNHprqAXVZF/N+ZDt7ekSInu3SsCj0pwypx/k+j5hqtuSoFbTcy6ro3vDOwe5rlOXeAZfZGvMEWIaAXKKujvCoc+0MaSeY9OeGYxvf9fDY/vv2j/b+G70PgjagUzscFfw6MQu/8xmnosYoH9XR006SeXMatKUNlZj0+gf6BS2vRyQrUjlxoWP8ZVAoTENHsTjg2j0SCOmXJqpmYlwA564WbdVf8/hI6Dghbxfn34zqfyUE7R9/lRCpQ9nArAFpoeeDIURJMZVIJQEYlIvNrReMno0YVBq6bDT+VBEozDNBAkiNprfGAOPYV4touxxYM/URpO1gDG0sIzVBpKDXbGhbyOcYshQkpwz9s52BK8mYGlbcC1SgBEW9vbQ8oivorTp4pp3U09namXhhcuccKRHQCaglqI8xAy3Ngb3GpMhVRaRPag0zuoCGbfF5hKdnfRKuyP489F4fzZNVjXENbU0i9hrY7GB0FFh7nwEdlFRtI5skBuiMAS0dHQwLE/YaMWeURwWT6EIUOrWcs17fQflnoqU9n/5RHTPCJwhKzLYovbBsUA77TilOQFxQwbHLhKFMMKf3cu6sLtDeUAoItvsBYUNpZIjILL39kfWygPyOntyrkaFgmNgKmg0m7tpqwkWUiWJTTnGjCJmqNy6usB3Zq/0flHu1MildOo/2N81cj22IkvbKUX4wPKwE3rjEigPgO4ceOsAjVOYgAdgAXOzsCM1G8pg26p+p65ZUTmlbz7Hmwtcu1FtwqyV6FS0udKXfqjZWJtoMhabdpIFoEpvrFQVq4WirECmFxECeDbjQADsEp0Opfh6EmZ5845qCXjfTw5ZuURpXfIk9nH0o97YJjl4CopvQwFuY42NNri0aFkBK2kSSg6l0waGQRDsolXMyJixfsO+8agPJu+EIIBLRsKEm8GVV6Mg2Ag+fTV0AC8DvDguXgRmLqMwOCpQLl+o5ARTnKaWHP1uIel8wnGHYdnO06PXaP6Gy5oN5Q2+nCAMRANRN1csttQmIHwhBosSOsqcQsACZBM4OjuFpA172+AFqQ5TgLHzWZfQ8vhdBo9eboOitlyRKz1DLnkK+K71+9V5S0hswTibhfQVmfF6BmRIPixb+vELT9YKGKOCMz9s/Z5K/QoW8T49lVptDyAYK4vPYiBhcPQr0930f4jeDYMq0i5U92DC1nQy871zd57k5ARvPFihodzYZxzRMtHLXDjCJh7OAelY1KHJamj5DkV7VR6MOjpeGZBZYrSN8vXrHz4SLGZjapAS8BwERA2K/x3FpfwVryC064e+mDaLyaMg8sSNRl4KKKQU/VRjyy0nvKQ20dwyChKV9Yb4Gngg8IZxVqJlo5OWMu4e5EDRra2P6eY4wJBMAMdNbPur6KjH6DRZ60jMDoEuvbAE+0NX2jtqjHh9+GL7966/WufEfekfv/wf69onwRpa7/wWx4CI3KtaymAAAAGB6VFh0UmF3IHByb2ZpbGUgdHlwZSBpcHRjAAB42j1Jyw2AUAy6dwpHaIFoXce+izcP7h/rSxQC4WPndZctE4IxBe0aruaPGFEObh2ToDOY7Xw1n+rn6HWlegNhGF91twcxSRTZ/znoVAAAD1tpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+Cjx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IlhNUCBDb3JlIDQuNC4wLUV4aXYyIj4KIDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+CiAgPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIKICAgIHhtbG5zOmlwdGNFeHQ9Imh0dHA6Ly9pcHRjLm9yZy9zdGQvSXB0YzR4bXBFeHQvMjAwOC0wMi0yOS8iCiAgICB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIKICAgIHhtbG5zOnN0RXZ0PSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VFdmVudCMiCiAgICB4bWxuczpwbHVzPSJodHRwOi8vbnMudXNlcGx1cy5vcmcvbGRmL3htcC8xLjAvIgogICAgeG1sbnM6R0lNUD0iaHR0cDovL3d3dy5naW1wLm9yZy94bXAvIgogICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICAgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIgogICB4bXBNTTpEb2N1bWVudElEPSJnaW1wOmRvY2lkOmdpbXA6ZTQxOTdmM2ItN2ViZi00OGIxLThkNTgtZWFjNTcyMjE0MGNmIgogICB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjg5NTkxMDU2LWNlM2ItNGIyMy04YzIxLTE0YTBhYTMyODU4NCIKICAgeG1wTU06T3JpZ2luYWxEb2N1bWVudElEPSJ4bXAuZGlkOmE5YTcxZjk4LWQ2MTYtNGM3My05MmU0LTNmZDdlZDAzODlkZSIKICAgR0lNUDpBUEk9IjIuMCIKICAgR0lNUDpQbGF0Zm9ybT0iV2luZG93cyIKICAgR0lNUDpUaW1lU3RhbXA9IjE1MjAwOTE2MjcxMjgzODciCiAgIEdJTVA6VmVyc2lvbj0iMi45LjgiCiAgIGRjOkZvcm1hdD0iaW1hZ2UvcG5nIgogICB4bXA6Q3JlYXRvclRvb2w9IkdJTVAgMi45LzIuMTAiPgogICA8aXB0Y0V4dDpMb2NhdGlvbkNyZWF0ZWQ+CiAgICA8cmRmOkJhZy8+CiAgIDwvaXB0Y0V4dDpMb2NhdGlvbkNyZWF0ZWQ+CiAgIDxpcHRjRXh0OkxvY2F0aW9uU2hvd24+CiAgICA8cmRmOkJhZy8+CiAgIDwvaXB0Y0V4dDpMb2NhdGlvblNob3duPgogICA8aXB0Y0V4dDpBcnR3b3JrT3JPYmplY3Q+CiAgICA8cmRmOkJhZy8+CiAgIDwvaXB0Y0V4dDpBcnR3b3JrT3JPYmplY3Q+CiAgIDxpcHRjRXh0OlJlZ2lzdHJ5SWQ+CiAgICA8cmRmOkJhZy8+CiAgIDwvaXB0Y0V4dDpSZWdpc3RyeUlkPgogICA8eG1wTU06SGlzdG9yeT4KICAgIDxyZGY6U2VxPgogICAgIDxyZGY6bGkKICAgICAgc3RFdnQ6YWN0aW9uPSJzYXZlZCIKICAgICAgc3RFdnQ6Y2hhbmdlZD0iLyIKICAgICAgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDphMzA2MWNkYS0xZTNkLTRjYjgtOGNiMy01NzliMzFjMTA4MDYiCiAgICAgIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkdpbXAgMi45LzIuMTAgKFdpbmRvd3MpIgogICAgICBzdEV2dDp3aGVuPSIyMDE4LTAzLTAzVDE1OjQwOjI3Ii8+CiAgICA8L3JkZjpTZXE+CiAgIDwveG1wTU06SGlzdG9yeT4KICAgPHBsdXM6SW1hZ2VTdXBwbGllcj4KICAgIDxyZGY6U2VxLz4KICAgPC9wbHVzOkltYWdlU3VwcGxpZXI+CiAgIDxwbHVzOkltYWdlQ3JlYXRvcj4KICAgIDxyZGY6U2VxLz4KICAgPC9wbHVzOkltYWdlQ3JlYXRvcj4KICAgPHBsdXM6Q29weXJpZ2h0T3duZXI+CiAgICA8cmRmOlNlcS8+CiAgIDwvcGx1czpDb3B5cmlnaHRPd25lcj4KICAgPHBsdXM6TGljZW5zb3I+CiAgICA8cmRmOlNlcS8+CiAgIDwvcGx1czpMaWNlbnNvcj4KICA8L3JkZjpEZXNjcmlwdGlvbj4KIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAKPD94cGFja2V0IGVuZD0idyI/PvOQ5mMAAAAGYktHRAD/AP8A/6C9p5MAAAAJcEhZcwAALiMAAC4jAXilP3YAAAAHdElNRQfiAwMPKBseKeVsAAAJfElEQVR42u1bXWgcxx3/ze7e7VnWl2vJ0jnCrkiKbRUbxw92nFIwwQ0plIIcDCmUvMQUTB+DyAfkrU8u5K0USvISQp5KbUoJIa2NnbYPwSGxQxI3NdhWPmxJOTknyWeddLe7eZjb3Oz//vNxurtEDxkYdnfuP7czv/l/z6xAd4vQ3Ls820rSxnPSycB7AQJtMz2r9NwkE8tzR2D0AgjuylWORjf5hLTRCsPVqQQ9Eg1u4h65qpP3NP8XMwDE5ErBEBsRjaBHYsFN3lOefYaGEw064ahBHzdoVVCoSLQFSNAjHQEyUd9wFRbRSAGIFRAihT5mJi02g2gIwgW+UgNypZwiCAixUiMAdQWEqJtWI+iSKFAQ1BUPDJXjDDCckILgN64egBoZR0J0imgHlKDDyVMgKBeok84pNVCuPuEKEG5IQag1qqcxz2BERbhySdCmJTCZSo/IvwpC/sYN3EwSIK1xnL2a2ilNkgCPPoofW5yu2NExswJhYn9ozCMFIeWAfGYkylCEyE6Qa0/vSf+8xueoaxwtYRIZsUHnyGOUIlWMOQJECCC8fh3/AeQqcyut45AkAaIIOHoUPwOwptT1Rq0RhRoR02t0ugJHkdD5BRwnqLoh3wAiTKu6ukI0VzghDKu2qZwBoGAYb41pU30RrdMVtOkceQZ/QFWQlBvSWqCioYISx62iQe8bgJriGHXMsWJmjU5X4OAP2HwCX2MmqVjkAYRU5qn8q7pBBUuhKziMNb2va1x22ESDixA9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"]
   ]
};

let uroURDupes = [];
let uroFID = -1;
let uroShownFID = -1;
let uroInhibitSave = true;
let uroConfirmIntercepted = false;
let uroMCLayer = null;
let uroVenueLayer = null;
let uro_uFP = [];
let uroPendingURSessionIDs = [];
let uroRequestedURSessionIDs = [];
let uroPlacesGroupsCollapsed = [];
let uroKnownProblemTypeIDs = [];
let uroKnownProblemTypeNames = [];
let uroPURsToHide = [];
let uroNullOpenLayers = false;
let uroNullURLayer = false;
let uroNullProblemLayer = false;
let uroNullMapViewport = false;
let uroURDialogIsOpen = false;
let uroHoveredURID = null;
let uroSelectedURID = null;
let uroURReclickAttempts = 0;
let uroPendingCommentDataRefresh = false;
let uroWaitingCommentDataRefresh = false;
let uroExpectedCommentCount = null;
let uroCachedLastCommentID = null;
let uroMouseIsDown = false;
let uroPopulatingRequestSessions = false;
let uroHidePopupOnPanelOpen = false;
let uroUserID = -1;
let uroMTEMode = false;
let uroDiv = null;
let uroAlerts = null;
let uroControls = null;
let uroCtrlHides = null;
let uroAMList = [];
let uroManagedAreas = [];
let uroIgnoreAreasUserID = null;
let uroMousedOverMapComment = null;
let uroMousedOverOtherObjectWithinMapComment = false;
let uroRTCObjs = null;
let uroUnstackedMasterID = null;
let uroStackList = [];
let uroStackType = null;
let uroMainTickHandlerID = null;
let uroMainTickStage = 0;
let uroSettingsApplied = false;
let uroInhibitURFiltering = false;


const uroUtils =  // utility functions
{
   GetExtent: function()
   {
      // From DaveAcincy
      let extent = new OpenLayers.Bounds(W.map.getExtent());
      extent = extent.transform('EPSG:4326', 'EPSG:3857');
      return extent;
   },
   ModifyHTML: function(htmlIn)
   {
      if(typeof trustedTypes === "undefined")
      {
         return htmlIn;
      }
      else
      {
         const escapeHTMLPolicy = trustedTypes.createPolicy("forceInner", {createHTML: (to_escape) => to_escape});
         return escapeHTMLPolicy.createHTML(htmlIn);
      }
   },
   CloneObject: function(objIn)
   {
      return JSON.parse(JSON.stringify(objIn));
   },
   GetCBChecked: function(cbID)
   {
      try
      {
         return(document.getElementById(cbID).checked);
      }
      catch(err)
      {
         uroDBG.AddLog('GetCBChecked() - '+cbID+' not found!');
         return null;
      }
   },
   SetCBChecked: function(cbID, state)
   {
      try
      {
         document.getElementById(cbID).checked = state;
      }
      catch(err)
      {
         uroDBG.AddLog('SetCBChecked() - '+cbID+' not found!');
      }
   },
   GetElmValue: function(elmID)
   {
      try
      {
         return(document.getElementById(elmID).value);
      }
      catch(err)
      {
         uroDBG.AddLog('GetElmValue() - '+elmID+' not found!');
         return null;
      }
   },
   SetOnClick: function(elm,fn)
   {
      try
      {
         if(typeof elm == 'object')
         {
            elm.onclick = fn;
         }
         else
         {
            document.getElementById(elm).onclick = fn;
         }
      }
      catch(err)
      {
         uroDBG.AddLog('SetOnClick() - '+elm+' not found!');
      }
   },
   AddEventListener: function(elm, eventType, eventFn, eventBool)
   {
      try
      {
         document.getElementById(elm).addEventListener(eventType, eventFn, eventBool);
      }
      catch(err)
      {
         uroDBG.AddLog('AddEventListener() - '+elm+' not found!');
      }
   },
   DateToDays: function(dateToConvert)
   {
      let dateNow = new Date();
   
      let elapsedSinceEpoch = dateNow.getTime();
      let elapsedSinceEvent = elapsedSinceEpoch - dateToConvert;
   
      dateNow.setHours(0);
      dateNow.setMinutes(0);
      dateNow.setSeconds(0);
      dateNow.setMilliseconds(0);
      let elapsedSinceMidnight = elapsedSinceEpoch - dateNow.getTime();
      dateNow.setHours(24);
      let pendingUntilMidnight = elapsedSinceEpoch - dateNow.getTime();
   
      if((elapsedSinceEvent < elapsedSinceMidnight) && (elapsedSinceEvent > pendingUntilMidnight))
      {
         // event occurred today...
         return 0;
      }
      else if(elapsedSinceEvent < 0)
      {
         // event occurrs at some point in the future after midnight today, so return a minimum value of -1...
         return -1 - Math.floor((pendingUntilMidnight - elapsedSinceEvent) / 86400000);
      }
      else
      {
         // event occurred at some point prior to midnight this morning, so return a minimum value of 1...
         return 1 + Math.floor((elapsedSinceEvent - elapsedSinceMidnight) / 86400000);
      }
   },
   GetURAge: function(urObj,ageType,getRaw)
   {
      if(ageType === 0)
      {
         if((urObj.attributes.driveDate === null)||(urObj.attributes.driveDate === 0)) return -1;
         if(getRaw) return urObj.attributes.driveDate;
         else return uroUtils.DateToDays(urObj.attributes.driveDate);
      }
      else if(ageType === 1)
      {
         if((urObj.attributes.resolvedOn === null)||(urObj.attributes.resolvedOn === 0)) return -1;
         if(getRaw) return urObj.attributes.resolvedOn;
         else return uroUtils.DateToDays(urObj.attributes.resolvedOn);
      }
      else
      {
         return -1;
      }
   },
   GetMCAge: function(mcAttrs, ageType, getRaw)
   {
      if(ageType === 0)
      {
         if((mcAttrs.createdOn === null)||(mcAttrs.createdOn === 0)) return -1;
         if(getRaw) return mcAttrs.createdOn;
         else return uroUtils.DateToDays(mcAttrs.createdOn);
      }
      else if(ageType === 1)
      {
         if((mcAttrs.updatedOn === null)||(mcAttrs.updatedOn === 0)) return -1;
         if(getRaw) return mcAttrs.updatedOn;
         else return uroUtils.DateToDays(mcAttrs.updatedOn);
      }
      else if(ageType === 2)
      {
         if((mcAttrs.endDate === null)||(mcAttrs.endDate === 0)) return -1;
         let tDate = new Date(mcAttrs.endDate);
         if(getRaw) return tDate;
         else return uroUtils.DateToDays(tDate);
      }
      else
      {
         return -1;
      }
   },
   GetPURAge: function(purObj)
   {
      if(purObj.attributes.venueUpdateRequests[0].attributes.dateAdded !== null)
      {
         return uroUtils.DateToDays(purObj.attributes.venueUpdateRequests[0].attributes.dateAdded);
      }
      else
      {
         return -1;
      }
   },
   GetCameraAge: function(camObj, mode)
   {
      if(mode === 0)
      {
         if(camObj.attributes.updatedOn === null) return -1;
         return uroUtils.DateToDays(camObj.attributes.updatedOn);
      }
      if(mode === 1)
      {
         if(camObj.attributes.createdOn === null) return -1;
         return uroUtils.DateToDays(camObj.attributes.createdOn);
      }
   },
   GetCommentAge: function(commentObj)
   {
      if(commentObj.createdOn === null) return -1;
      return uroUtils.DateToDays(commentObj.createdOn);
   },
   ParseDaysAgo: function(days)
   {
     if(days === 0) return 'today';
     else if(days === 1) return '1 day ago';
     else return days+' days ago';
   },
   ParseDaysToGo: function(days)
   {
     days = 0 - days;
     if(days === 0) return 'today';
     else if(days === 1) return 'in 1 day';
     else return 'in '+days+' days';
   },
   GetLocalisedSpeedString: function(thisSpeed)
   {
      if(thisSpeed !== null)
      {
         let conversionFactor = 1;  // default to metric
         let multipleFactor = 10;   // default to limits being set in multiples of 10
   
         let country = null;
   
         if((W.model.getTopCountry()) && (W.model.getTopCountry().attributes.name !== undefined))
         {
            country = W.model.getTopCountry().attributes.name;
         }
   
         if(country !== null)
         {
            // country-specific deviations from the above...
            if
            (
               (country == "United Kingdom") ||
               (country == "Jersey") ||
               (country == "Guernsey") ||
               (country == "United States")
            )
            {
               // countries using MPH
               conversionFactor = 1.609;
            }
            if
            (
               (country == "United States") ||
               (country == "Guernsey")
            )
            {
               // countries with speed limits set in multiples of 5
               multipleFactor = 5;
            }
         }
   
         let speed = Math.round(thisSpeed / conversionFactor);
         let retval = speed;
         if(conversionFactor == 1) retval += "KM/H";
         else retval += "MPH";
   
         return retval;
      }
      else return "not set";
   },
   Clickify: function(desc, suffix)
   {
      // The terminators array consists of pairs of characters which may be found at either
      // end of a URL.  The first entry in each pair indicates which character needs to be
      // found immediately prior to the URL ('' indicates any character) in order for the
      // second entry to be considered as a potential end of URL marker
      let terminators = [
                           ['',  ' '],
                           ['',  ','],
                           ['(', ')'],
                           ['[', ']'],
                           ['',  '\r'],
                           ['',  '\n']
                        ];
   
      if(desc === null) return '';
      if(desc === undefined) return '';
      if(desc === '') return '';
   
      if(desc.indexOf("https:  one.network") == 0)
      {
         desc = desc.replaceAll(' ','/');
      }
   
      desc = desc.replace(/<\/?[^>]+(>|$)/g, "");
      if(desc !== "null")
      {
         // At the moment we can only clickify links that start with http or https...
         if(desc.indexOf('http') != -1)
         {
            let links = desc.split("http");
            desc = '';
            let i, j, linkEndPos, descPostLink;
            for(i=0; i<links.length; i++)
            {
               if(links[i].indexOf('://') != -1)
               {
                  let prefix = links[i - 1][links[i - 1].length - 1];
                  links[i] = "http" + links[i];
                  linkEndPos = links[i].length + 1;
   
                  // work out where the end of the URL is, based on what the character immediately
                  // preceding the "http" is
                  for(j=0; j<terminators.length; j++)
                  {
                     if(links[i].indexOf(terminators[j][1]) !== -1)
                     {
                        if((prefix === terminators[j][0]) || (terminators[j][0] === ''))
                        {
                           linkEndPos = Math.min(linkEndPos, links[i].indexOf(terminators[j][1]));
                        }
                     }
                  }
   
                  descPostLink = '';
                  if(linkEndPos < links[i].length)
                  {
                     descPostLink = links[i].slice(linkEndPos);
                     links[i] = links[i].slice(0,linkEndPos);
                  }
   
                  desc += '<a target="_wazeUR" href="'+links[i]+'">'+links[i]+'</a>' + descPostLink;
               }
               else
               {
                  desc += links[i];
               }
            }
         }
         desc = desc.replace(/\n/g,"<br>");
         return desc + suffix;
      }
      else
      {
         return '';
      }
   },
   GetUserNameFromID: function(userID)
   {
      let userName;
      if(W.model.users.objects[userID] != null)
      {
         userName = W.model.users.objects[userID].attributes.userName;
         if(userName === undefined)
         {
            userName = userID;
         }
      }
      else
      {
         userName = userID;
      }
      return userName;
   },
   GetUserNameAndRank: function(userID)
   {
      let userName;
      let userLevel;
      if(W.model.users.objects[userID] != null)
      {
         userName = W.model.users.objects[userID].attributes.userName;
         if(userName === undefined)
         {
            userName = userID;
         }
         else
         {
            userName = '<a href="' + (W.Config.user_profile.url + userName) + '" target="_blank">' + userName + '</a>';
         }
         userLevel = W.model.users.objects[userID].attributes.rank + 1;
      }
      else
      {
         userName = userID;
         userLevel = '?';
      }
      return userName + ' (' + userLevel + ')';
   },
   GetDateTimeString: function(ts)
   {
      let tDateObj = new Date(ts);
      let dateLocale;
      let timeLocale;
      if(uroUtils.GetCBChecked('_cbDateFmtDDMMYY')) dateLocale = 'en-gb';
      if(uroUtils.GetCBChecked('_cbDateFmtMMDDYY')) dateLocale = 'en-us';
      if(uroUtils.GetCBChecked('_cbDateFmtYYMMDD')) dateLocale = 'ja';
      if(uroUtils.GetCBChecked('_cbTimeFmt24H')) timeLocale = 'en-gb';
      if(uroUtils.GetCBChecked('_cbTimeFmt12H')) timeLocale = 'en-us';
      return tDateObj.toLocaleDateString(dateLocale) + ' ' + tDateObj.toLocaleTimeString(timeLocale);
   },
   ParsePxString: function(pxString)
   {
      return parseInt(pxString.split("px")[0]);
   },
   TypeCast: function(varin)
   {
      if(varin == "null") return null;
      if(typeof varin == "string") return parseInt(varin);
      return varin;
   },
   Truncate: function(val)
   {
      if(val === null) return val;
      if(val < 0) return Math.ceil(val);
      return Math.floor(val);
   },
   KeywordPresent: function(desc, keyword, caseInsensitive)
   {
      let re;
      if(caseInsensitive) re = RegExp(keyword,'i');
      else re = RegExp(keyword);
   
      if(desc.search(re) != -1) return true;
      else return false;
   },
   AddStyle: function(ID, css) 
   {
      let head, style;
      head = document.getElementsByTagName('head')[0];
      if (!head) 
      {
         return;
      }
      
      uroUtils.RemoveStyle(ID); // in case it is already there
      style = document.createElement('style');
      style.type = 'text/css';
      style.innerHTML = uroUtils.ModifyHTML(css);
      style.id = ID;
      head.appendChild(style);
   },
   RemoveStyle: function(ID) 
   {
      let style = document.getElementById(ID);
      if (style) 
      {
         style.parentNode.removeChild(style); 
      }
   },
   GetTS: function(day, month, year, hours, mins)
   {
      let retval = new Date(0);
      retval.setDate(day);
      retval.setMonth(month - 1);
      retval.setYear(year);
      retval.setHours(hours);
      retval.setMinutes(mins);
      return retval.getTime();
   },
   ToHex: function(decValue, digits)
   {
      let modifier = 1;
      for(let i=0; i < digits; i++)
      {
         modifier *= 16;
      }
      // make sure decValue actually is an integer
      decValue = parseInt(decValue);
      // adding the modifier ensures we have enough digits, including
      // any leading zeros which may be required, for the required output
      decValue += modifier;
      let retval = decValue.toString(16);
      // after converting to hex with the modifier included, we'll have
      // as many digits as we need to represent decValue, but also an
      // extra leading 1, which now needs to be removed before returning
      // the result...
      retval = retval.substr(-digits);
      retval = retval.toUpperCase();
      return retval;
   },
   GetActiveSidebarTab: function()
   {
      let retval = null;

      let tabIcons = document.querySelectorAll('wz-navigation-item');
      for(let i = 0; i < tabIcons.length; ++i)
      {
         if(tabIcons[i].selected === true)
         {
            retval = tabIcons[i].attributes['data-for'].value;
            break;
         }
      }
      return retval;
   },
   IsClosureUIActive: function()
   {
      let retval = false;

      if(W.selectionManager.getSelectedWMEFeatures()[0]?.featureType === 'segment')
      {
         let ast = uroUtils.GetActiveSidebarTab();
         if(ast === 'feature_editor')
         {
            retval = document.querySelector('wz-tab.closures-tab').attributes['is-active'].value !== 'false';
         }
      }

      return retval;
   },
   ConvertWGS84ToMercator: function(point)
   {
      return point.transform(new OpenLayers.Projection("EPSG:4326"), new OpenLayers.Projection("EPSG:900913"));
   },
   ConvertMercatorToWGS84: function(point)
   {
      return point.transform(new OpenLayers.Projection("EPSG:900913"), new OpenLayers.Projection("EPSG:4326"));
   }
};
const uroTabs = // script tab handling
{
   MAX_PER_ROW: 6,
   IDS:
   {
      URS: 0,
      MPS: 1,
      MCS: 2,
      RTCS: 3,
      RAS: 4,
      PLACES: 5,
      CAMS: 6,
      OWL: 7,
      MISC: 8
   },
   FIELDS:
   {
      TABHEADER: 0,
      TABBODY: 1,
      LINKID: 2,
      TABTITLE: 3,
      SHOWFN: 4,
      CLICKFN: 5,
      STORAGE: 6,
      POPULATEFN: 7
   },
   CtrlTabs: [],
   selectedOWLGroup: null,

   SetStyleDisplay: function(elm,style)
   {
      try
      {
         if(typeof elm == 'object')
         {
            elm.style.display = style;
         }
         else
         {
            document.getElementById(elm).style.display = style;
         }
      }
      catch(err)
      {
         uroDBG.AddLog('SetStyleDisplay() - '+elm+' not found!');
      }
   },
   PopulateMP: function()
   {
      let tHTML = '';
      tHTML += '<input type="checkbox" id="_cbMPFilterOutsideArea">Hide MPs outside my editable area</input><br><br>';
      tHTML += '<b>Filter MPs by type:</b><br>';
      let i;
      for(i=0; i<uroKnownProblemTypeNames.length; i++)
      {
         tHTML += '<input type="checkbox" id="_cbMPFilter_T'+uroKnownProblemTypeIDs[i]+'">'+uroKnownProblemTypeNames[i]+'</input><br>';
      }
      tHTML += '<br><input type="checkbox" id="_cbMPFilterUnknownProblem">Unknown problem type</input><br><br>';

      tHTML += '&nbsp;&nbsp;<i>Specially tagged types</i><br>';
      tHTML += '&nbsp;&nbsp;<input type="checkbox" id="_cbFilterElgin">[Elgin]</input><br>';
      tHTML += '&nbsp;&nbsp;<input type="checkbox" id="_cbFilterTrafficCast">[TrafficCast]</input><br>';
      tHTML += '&nbsp;&nbsp;<input type="checkbox" id="_cbFilterTrafficMaster">[TM]</input><br>';
      tHTML += '&nbsp;&nbsp;<input type="checkbox" id="_cbFilterCaltrans">[Caltrans]</input><br>';
      tHTML += '&nbsp;&nbsp;<input type="checkbox" id="_cbFilterTFL">TfL</input><br>';

      tHTML += '<input type="checkbox" id="_cbMPFilterReopenedProblem">Reopened Problems</input><br><br>';

      tHTML += '<input type="checkbox" id="_cbInvertMPFilter">Invert operation of type filters?</input><br>';

      tHTML += '<br><b>Hide closed/solved/unidentified Problems:</b><br>';
      tHTML += '<input type="checkbox" id="_cbMPFilterClosed">Closed</input><br>';
      tHTML += '<input type="checkbox" id="_cbMPFilterSolved">Solved</input><br>';
      tHTML += '<input type="checkbox" id="_cbMPFilterUnidentified">Not identified</input><br><br>';

      tHTML += '<input type="checkbox" id="_cbMPClosedUserIDFilter" pairedWith="_cbMPNotClosedUserIDFilter">Closed</input> or ';
      tHTML += '<input type="checkbox" id="_cbMPNotClosedUserIDFilter" pairedWith="_cbMPClosedUserIDFilter">Not Closed</input> by user';
      tHTML += '<select id="_selectMPUserID" style="width:80%; height:22px;"></select><br>';

      tHTML += '<br><b>Hide problems (not turn) by severity:</b><br>';
      tHTML += '<input type="checkbox" id="_cbMPFilterLowSeverity">Low</input>&nbsp;&nbsp;';
      tHTML += '<input type="checkbox" id="_cbMPFilterMediumSeverity">Medium</input>&nbsp;&nbsp;';
      tHTML += '<input type="checkbox" id="_cbMPFilterHighSeverity">High</input><br>';

      tHTML += '<br><b>Show MPs based on start/end dates:</b><br>';
      tHTML += '<input type="checkbox" id="_cbMPFilterStartDate">Start</input>&nbsp;&nbsp;';
      tHTML += '<input type="number" min="1" max="31" size="2" style="width:50px;line-height:14px;height:22px;margin-bottom:4px;" id="_inputMPFilterStartDay"> / ';
      tHTML += '<input type="number" min="1" max="12" size="2" style="width:50px;line-height:14px;height:22px;margin-bottom:4px;" id="_inputMPFilterStartMonth"> / ';
      tHTML += '<input type="number" min="2010" max="2100" size="4" style="width:50px;line-height:14px;height:22px;margin-bottom:4px;" id="_inputMPFilterStartYear"><br>';
      tHTML += '<input type="checkbox" id="_cbMPFilterEndDate">End</input>&nbsp;&nbsp;';
      tHTML += '<input type="number" min="1" max="31" size="2" style="width:50px;line-height:14px;height:22px;margin-bottom:4px;" id="_inputMPFilterEndDay"> / ';
      tHTML += '<input type="number" min="1" max="12" size="2" style="width:50px;line-height:14px;height:22px;margin-bottom:4px;" id="_inputMPFilterEndMonth"> / ';
      tHTML += '<input type="number" min="2010" max="2100" size="4" style="width:50px;line-height:14px;height:22px;margin-bottom:4px;" id="_inputMPFilterEndYear"><br>';
      tHTML += '<input type="checkbox" id="_cbMPFilterEndDatePassed">End date in the past</input>';

      return tHTML;
   },
   PopulatePlaces: function()
   {
      let tHTML = '';
      tHTML += '<b>Filter PURs by category/status:</b><br>';
      tHTML += '<input type="checkbox" id="_cbFilterUneditablePlaceUpdates">Ones I can\'t edit</input><br>';
      tHTML += '<input type="checkbox" id="_cbPURFilterInsideManagedAreas">Ones within AM areas</input>';
      tHTML += '&nbsp;(<input type="checkbox" id="_cbPURExcludeUserArea">except my area)</input><br>';
      tHTML += '<i>Requires Area Manager layer to be enabled</i><br>';
      tHTML += '<input type="checkbox" id="_cbFilterLockRankedPlaceUpdates">Ones with non-zero lockRanks</input><br>';
      tHTML += '<input type="checkbox" id="_cbFilterNewPlacePUR">Ones for new places</input><br>';
      tHTML += '<input type="checkbox" id="_cbFilterUpdatedDetailsPUR">Ones for updated place details</input><br>';

      tHTML += '&nbsp;&nbsp;<input type="checkbox" id="_cbPURFilterCFPhone">Phone number</input><br>';
      tHTML += '&nbsp;&nbsp;<input type="checkbox" id="_cbPURFilterCFName">Name</input><br>';
      tHTML += '&nbsp;&nbsp;<input type="checkbox" id="_cbPURFilterCFEntryExitPoints">Entry//exit points</input><br>';
      tHTML += '&nbsp;&nbsp;<input type="checkbox" id="_cbPURFilterCFOpeningHours">Opening hours</input><br>';
      tHTML += '&nbsp;&nbsp;<input type="checkbox" id="_cbPURFilterCFAliases">Aliases</input><br>';
      tHTML += '&nbsp;&nbsp;<input type="checkbox" id="_cbPURFilterCFServices">Services</input><br>';
      tHTML += '&nbsp;&nbsp;<input type="checkbox" id="_cbPURFilterCFGeometry">Geometry</input><br>';
      tHTML += '&nbsp;&nbsp;<input type="checkbox" id="_cbPURFilterCFHouseNumber">House number</input><br>';
      tHTML += '&nbsp;&nbsp;<input type="checkbox" id="_cbPURFilterCFCategories">Categories</input><br>';
      tHTML += '&nbsp;&nbsp;<input type="checkbox" id="_cbPURFilterCFDescription">Description</input><br>';

      tHTML += '<input type="checkbox" id="_cbFilterNewPhotoPUR">Ones for new photos</input><br>';
      tHTML += '<input type="checkbox" id="_cbFilterFlaggedPUR">Ones flagged for attention</input><br>';
      tHTML += '<br><input type="checkbox" id="_cbInvertPURFilters">Invert PUR filters</input><br>';

      tHTML += '<br><b>Filter PURs by age of submission:</b><br>';
      tHTML += '<input type="checkbox" id="_cbEnablePURMinAgeFilter">Hide PURs less than </input>';
      tHTML += '<input type="number" min="1" size="3" style="width:50px;line-height:14px;height:22px;margin-bottom:4px;" id="_inputPURFilterMinDays"> days old<br>';
      tHTML += '<input type="checkbox" id="_cbEnablePURMaxAgeFilter">Hide PURs more than </input>';
      tHTML += '<input type="number" min="1" size="3" style="width:50px;line-height:14px;height:22px;margin-bottom:4px;" id="_inputPURFilterMaxDays"> days old<br>';

      tHTML += '<hr>';

      tHTML += '<br><b>Filter Places by state:</b><br>';
      tHTML += 'Hide if last edited<br>';
      tHTML += '<input type="checkbox" id="_cbPlaceFilterEditedLessThan"> less than </input>';
      tHTML += '<input type="number" min="1" size="3" style="width:50px;line-height:14px;height:22px;margin-bottom:4px;" id="_inputFilterPlaceEditMinDays"> days ago<br>';
      tHTML += '<input type="checkbox" id="_cbPlaceFilterEditedMoreThan"> more than </input>';
      tHTML += '<input type="number" min="1" size="3" style="width:50px;line-height:14px;height:22px;margin-bottom:4px;" id="_inputFilterPlaceEditMaxDays"> days ago<br>';

      tHTML += '<br>Hide if locked at level:<br>';
      tHTML += '&nbsp;&nbsp;&nbsp;<input type="checkbox" id="_cbHidePlacesL0">1</input>';
      tHTML += '&nbsp;&nbsp;&nbsp;<input type="checkbox" id="_cbHidePlacesL1">2</input>';
      tHTML += '&nbsp;&nbsp;&nbsp;<input type="checkbox" id="_cbHidePlacesL2">3</input>';
      tHTML += '&nbsp;&nbsp;&nbsp;<input type="checkbox" id="_cbHidePlacesL3">4</input>';
      tHTML += '&nbsp;&nbsp;&nbsp;<input type="checkbox" id="_cbHidePlacesL4">5</input>';
      tHTML += '&nbsp;&nbsp;&nbsp;<input type="checkbox" id="_cbHidePlacesL5">6</input>';
      tHTML += '&nbsp;&nbsp;&nbsp;<input type="checkbox" id="_cbHidePlacesStaff">Staff</input>';
      tHTML += '<br>&nbsp;&nbsp;&nbsp;<input type="checkbox" id="_cbHidePlacesAdLocked">AdLocked</input><br>';

      tHTML += '<br>Hide by geometry:<br>';
      tHTML += '&nbsp;&nbsp;&nbsp;<input type="checkbox" id="_cbHideAreaPlaces">Areas</input>';
      tHTML += '&nbsp;&nbsp;&nbsp;<input type="checkbox" id="_cbHidePointPlaces">Points</input>';

      tHTML += '<br><br><input type="checkbox" id="_cbHidePhotoPlaces" pairedWith="_cbHideNoPhotoPlaces">Hide or </input>';
      tHTML += '<input type="checkbox" id="_cbHideNoPhotoPlaces" pairedWith="_cbHidePhotoPlaces">show ones with photos</input><br>';

      tHTML += '<input type="checkbox" id="_cbHideLinkedPlaces" pairedWith="_cbHideNoLinkedPlaces">Hide or </input>';
      tHTML += '<input type="checkbox" id="_cbHideNoLinkedPlaces" pairedWith="_cbHideLinkedPlaces">show ones with external links</input><br>';

      tHTML += '<input type="checkbox" id="_cbHideDescribedPlaces" pairedWith="_cbHideNonDescribedPlaces">Hide or </input>';
      tHTML += '<input type="checkbox" id="_cbHideNonDescribedPlaces" pairedWith="_cbHideDescribedPlaces">show ones with descriptive text</input><br>';

      tHTML += '<input type="checkbox" id="_cbHideKeywordPlaces" pairedWith="_cbHideNoKeywordPlaces">Hide or </input>';
      tHTML += '<input type="checkbox" id="_cbHideNoKeywordPlaces" pairedWith="_cbHideKeywordPlaces">show ones with a name including</input><br>';
      tHTML += '<input type="text" style="font-size:14px; line-height:16px; height:22px; margin-bottom:4px;" id="_textKeywordPlace"><br>';

      tHTML += '<br><b>Show Places touched by a specific editor:</b><br>';
      tHTML += '<input type="checkbox" id="_cbShowOnlyPlacesCreatedBy">Created by</input>&nbsp;/&nbsp;';
      tHTML += '<input type="checkbox" id="_cbShowOnlyPlacesEditedBy">edited by</input><br>';
      tHTML += '<input type="text" style="font-size:14px; line-height:16px; height:22px; margin-bottom:4px;" id="_textPlacesEditor"><br>';
      tHTML += '<select id="_selectPlacesUserID" style="width:80%; height:22px;"></select><br>';

      tHTML += '<br><b>Hide Places touched by a specific editor:</b><br>';
      tHTML += '<input type="checkbox" id="_cbHideOnlyPlacesCreatedBy">Created by</input>&nbsp;/&nbsp;';
      tHTML += '<input type="checkbox" id="_cbHideOnlyPlacesEditedBy">edited by</input><br>';
      tHTML += '<input type="text" style="font-size:14px; line-height:16px; height:22px; margin-bottom:4px;" id="_textHidePlacesEditor"><br>';
      tHTML += '<select id="_selectHidePlacesUserID" style="width:80%; height:22px;"></select><br>';

      tHTML += '<br><br><b>Filter Places by category:</b><br>';
      tHTML += '<input type="checkbox" id="_cbLeavePURGeos" pairedWith="_cbHidePURsForFilteredPlaces">Keep place visible if linked PUR is hidden, or</input><br>';
      tHTML += '<input type="checkbox" id="_cbHidePURsForFilteredPlaces" pairedWith="_cbLeavePURGeos">Hide PURs linked to hidden places</input><br><br>';

      let nCategories = W.Config.venues.categories.length;
      let i;
      if(uroPlacesGroupsCollapsed.length != nCategories)
      {
         for(i=0; i<nCategories; i++)
         {
            uroPlacesGroupsCollapsed.push(false);
         }
      }

      for(i=0; i<nCategories; i++)
      {
         let parentCategory = W.Config.venues.categories[i];
         let localisedName = I18n.lookup("venues.categories." + parentCategory);

         if(uroPlacesGroupsCollapsed[i] === true)
         {
            tHTML += '<i class="fa fa-plus-square-o" style="cursor:pointer;font-size:14px;" id="_uroPlacesGroupState-'+i+'"></i>';
         }
         else
         {
            tHTML += '<i class="fa fa-minus-square-o" style="cursor:pointer;font-size:14px;" id="_uroPlacesGroupState-'+i+'"></i>';
         }

         tHTML += '&nbsp;<input type="checkbox" id="_cbPlacesFilter-'+parentCategory+'"><b>'+localisedName+'</b></input><br>';
         tHTML += '<div id="_uroPlacesGroup-'+i+'" style="padding:3px;border-width:2px;border-style:solid;border-color:#FFFFFF">';

         for(let ii=0; ii<W.Config.venues.subcategories[parentCategory].length; ii++)
         {
            let subCategory = W.Config.venues.subcategories[parentCategory][ii];
            localisedName = I18n.lookup("venues.categories." + subCategory);
            tHTML += '&nbsp;&nbsp;&nbsp;&nbsp;<input type="checkbox" id="_cbPlacesFilter-'+subCategory+'">'+localisedName+'</input><br>';
         }
         tHTML += '</div>';
      }
      tHTML += '<input type="checkbox" id="_cbFilterPrivatePlaces"><b>Residential Places</b></input><br>';
      tHTML += '<br><input type="checkbox" id="_cbInvertPlacesFilter">Invert Place filters?</input>';

      return tHTML;   
   },
   PopulateUR: function()
   {
      let iHTML = '';
      iHTML = '<br>';

      iHTML += '<input type="checkbox" id="_cbURFilterOutsideArea">Hide URs outside my editable area</input><br>';
      iHTML += '<input type="checkbox" id="_cbURFilterInsideManagedAreas">Hide URs within AM areas</input>';
      iHTML += '&nbsp;(<input type="checkbox" id="_cbURExcludeUserArea">except my area)</input><br>';
      iHTML += '&nbsp;<i>Requires Area Manager layer to be enabled</i><br>';
      iHTML += '<input type="checkbox" id="_cbNoFilterForURInURL">Don\'t filter selected UR</input><br><br>';
      iHTML += '<input type="checkbox" id="_cbURFilterDupes">Show only duplicate URs</input><br><br>';

      iHTML += '<b>Filter by type:</b><br>';
      iHTML += '<input type="checkbox" id="_cbFilterWazeAuto">Waze Automatic</input><br>';
      iHTML += '<input type="checkbox" id="_cbFilterIncorrectTurn">Incorrect turn</input><br>';
      iHTML += '<input type="checkbox" id="_cbFilterIncorrectAddress">Incorrect address</input><br>';
      iHTML += '<input type="checkbox" id="_cbFilterIncorrectRoute">Incorrect route</input><br>';
      iHTML += '<input type="checkbox" id="_cbFilterMissingRoundabout">Missing roundabout</input><br>';
      iHTML += '<input type="checkbox" id="_cbFilterGeneralError">General error</input><br>';
      iHTML += '<input type="checkbox" id="_cbFilterTurnNotAllowed">Turn not allowed</input><br>';
      iHTML += '<input type="checkbox" id="_cbFilterIncorrectJunction">Incorrect junction</input><br>';
      iHTML += '<input type="checkbox" id="_cbFilterMissingBridgeOverpass">Missing bridge overpass</input><br>';
      iHTML += '<input type="checkbox" id="_cbFilterWrongDrivingDirection">Wrong driving direction</input><br>';
      iHTML += '<input type="checkbox" id="_cbFilterMissingExit">Missing exit</input><br>';
      iHTML += '<input type="checkbox" id="_cbFilterMissingRoad">Missing road</input><br>';
      iHTML += '<input type="checkbox" id="_cbFilterBlockedRoad">Blocked road</input><br>';
      iHTML += '<input type="checkbox" id="_cbFilterMissingLandmark">Missing Landmark</input><br>';
      iHTML += '<input type="checkbox" id="_cbFilterSpeedLimits">Missing or Invalid Speed limit</input><br>';
      iHTML += '<input type="checkbox" id="_cbFilterUndefined">Undefined</input><br>';

      iHTML += '&nbsp;&nbsp;<i>Specially tagged types</i><br>';
      iHTML += '&nbsp;&nbsp;<input type="checkbox" id="_cbFilterRoadworks">[ROADWORKS]</input><br>';
      iHTML += '&nbsp;&nbsp;<input type="checkbox" id="_cbFilterConstruction">[CONSTRUCTION]</input><br>';
      iHTML += '&nbsp;&nbsp;<input type="checkbox" id="_cbFilterClosure">[CLOSURE]</input><br>';
      iHTML += '&nbsp;&nbsp;<input type="checkbox" id="_cbFilterEvent">[EVENT]</input><br>';
      iHTML += '&nbsp;&nbsp;<input type="checkbox" id="_cbFilterNote">[NOTE]</input><br>';
      iHTML += '&nbsp;&nbsp;<input type="checkbox" id="_cbFilterBOG">[BOG]</input><br>';
      iHTML += '&nbsp;&nbsp;<input type="checkbox" id="_cbFilterDifficult">[DIFFICULT]</input><br>';
      iHTML += '&nbsp;&nbsp;<input type="checkbox" id="_cbFilterWSLM">[WSLM]</input><br><br>';
      iHTML += '<input type="checkbox" id="_cbInvertURFilter">Invert operation of type filters?</input><br>';

      iHTML += '<hr>';

      iHTML += '<br><b>Hide by state:</b><br>';
      iHTML += '<input type="checkbox" id="_cbFilterOpenUR">Open</input><br>';
      iHTML += '<input type="checkbox" id="_cbFilterClosedUR">Closed</input><br>';
      iHTML += '<input type="checkbox" id="_cbFilterSolved">Solved</input><br>';
      iHTML += '<input type="checkbox" id="_cbFilterUnidentified">Not identified</input><br><br>';


      iHTML += '<br><b>Filter by age of submission:</b><br>';
      iHTML += '<input type="checkbox" id="_cbEnableMinAgeFilter">Hide URs less than </input>';
      iHTML += '<input type="number" min="1" size="3" style="width:50px;line-height:14px;height:22px;margin-bottom:4px;" id="_inputFilterMinDays"> days old<br>';
      iHTML += '<input type="checkbox" id="_cbEnableMaxAgeFilter">Hide URs more than </input>';
      iHTML += '<input type="number" min="1" size="3" style="width:50px;line-height:14px;height:22px;margin-bottom:4px;" id="_inputFilterMaxDays"> days old<br>';

      iHTML += '<br><b>Filter by other details:</b><br>';
      iHTML += '<input type="checkbox" id="_cbHideMyFollowed" pairedWith="_cbHideMyUnfollowed">Hide</input> or ';
      iHTML += '<input type="checkbox" id="_cbHideMyUnfollowed" pairedWith="_cbHideMyFollowed">show</input> URs I\'m following<br><br>';

      iHTML += '<input type="checkbox" id="_cbURDescriptionMustBePresent" pairedWith="_cbURDescriptionMustBeAbsent">Hide</input> or ';
      iHTML += '<input type="checkbox" id="_cbURDescriptionMustBeAbsent" pairedWith="_cbURDescriptionMustBePresent">show</input> URs with no description<br>';
      iHTML += '<input type="checkbox" id="_cbEnableKeywordMustBePresent">Hide URs not including </input>';
      iHTML += '<input type="text" style="font-size:14px; line-height:16px; height:22px; margin-bottom:4px;" id="_textKeywordPresent"><br>';
      iHTML += '<input type="checkbox" id="_cbEnableKeywordMustBeAbsent">Hide URs including </input>';
      iHTML += '<input type="text" style="font-size:14px; line-height:16px; height:22px; margin-bottom:4px;" id="_textKeywordAbsent"><br>';
      iHTML += '&nbsp;&nbsp;<input type="checkbox" id="_cbCaseInsensitive"><i>Case-insensitive matches?</i></input><br><br>';

      iHTML += 'With comments from me?<br>';
      iHTML += '<input type="checkbox" id="_cbHideMyComments" pairedWith="_cbHideAnyComments">Yes </input>';
      iHTML += '<input type="checkbox" id="_cbHideAnyComments" pairedWith="_cbHideMyComments">No</input><br>';
      iHTML += 'If last comment made by me?<br>';
      iHTML += '<input type="checkbox" id="_cbHideIfLastCommenter" pairedWith="_cbHideIfNotLastCommenter">Yes </input>';
      iHTML += '<input type="checkbox" id="_cbHideIfNotLastCommenter" pairedWith="_cbHideIfLastCommenter">No </input><br>';
      iHTML += 'If last comment made by UR reporter?<br>';
      iHTML += '<input type="checkbox" id="_cbHideIfReporterLastCommenter" pairedWith="_cbHideIfReporterNotLastCommenter">Yes </input>';
      iHTML += '<input type="checkbox" id="_cbHideIfReporterNotLastCommenter" pairedWith="_cbHideIfReporterLastCommenter">No</input><br>';

      iHTML += '<input type="checkbox" id="_cbEnableMinCommentsFilter">With less than </input>';
      iHTML += '<input type="number" min="1" size="3" style="width:50px;line-height:14px;height:22px;margin-bottom:4px;" id="_inputFilterMinComments"> comments<br>';
      iHTML += '<input type="checkbox" id="_cbEnableMaxCommentsFilter">With more than </input>';
      iHTML += '<input type="number" min="0" size="3" style="width:50px;line-height:14px;height:22px;margin-bottom:4px;" id="_inputFilterMaxComments"> comments<br><br>';

      iHTML += '<input type="checkbox" id="_cbEnableCommentAgeFilter2">Last comment less than </input>';
      iHTML += '<input type="number" min="1" size="3" style="width:50px;line-height:14px;height:22px;margin-bottom:4px;" id="_inputFilterCommentDays2"> days ago<br>';
      iHTML += '<input type="checkbox" id="_cbEnableCommentAgeFilter">Last comment more than </input>';
      iHTML += '<input type="number" min="1" size="3" style="width:50px;line-height:14px;height:22px;margin-bottom:4px;" id="_inputFilterCommentDays"> days ago<br>';
      iHTML += '&nbsp;&nbsp;<input type="checkbox" id="_cbIgnoreOtherEditorComments"><i>Ignore other editor comments?</i></input><br><br>';

      iHTML += '<input type="checkbox" id="_cbURUserIDFilter">Without comments from user</input>';
      iHTML += '<select id="_selectURUserID" style="width:80%; height:22px;"></select><br>';
      iHTML += '<input type="checkbox" id="_cbURResolverIDFilter">Not resolved by user</input>';
      iHTML += '<select id="_selectURResolverID" style="width:80%; height:22px;"></select>';

      iHTML += '<br><br><input type="checkbox" id="_cbInvertURStateFilter">Invert operation of state/age filters?</input><br>';
      iHTML += '<input type="checkbox" id="_cbNoFilterForTaggedURs">Don\'t apply state/age filters to tagged URs</input><br>';

      return iHTML;   
   },
   PopulateMC: function()
   {
      let iHTML = '';
      
      iHTML = '<br>';

      iHTML += '&nbsp;&nbsp;<i>Specially tagged types</i><br>';
      iHTML += '&nbsp;&nbsp;<input type="checkbox" id="_cbMCFilterRoadworks">[ROADWORKS]</input><br>';
      iHTML += '&nbsp;&nbsp;<input type="checkbox" id="_cbMCFilterConstruction">[CONSTRUCTION]</input><br>';
      iHTML += '&nbsp;&nbsp;<input type="checkbox" id="_cbMCFilterClosure">[CLOSURE]</input><br>';
      iHTML += '&nbsp;&nbsp;<input type="checkbox" id="_cbMCFilterEvent">[EVENT]</input><br>';
      iHTML += '&nbsp;&nbsp;<input type="checkbox" id="_cbMCFilterNote">[NOTE]</input><br>';
      iHTML += '&nbsp;&nbsp;<input type="checkbox" id="_cbMCFilterBOG">[BOG]</input><br>';
      iHTML += '&nbsp;&nbsp;<input type="checkbox" id="_cbMCFilterDifficult">[DIFFICULT]</input><br>';
      iHTML += '&nbsp;&nbsp;<input type="checkbox" id="_cbMCFilterWSLM">[WSLM]</input><br><br>';
      iHTML += '<input type="checkbox" id="_cbInvertMCFilter">Invert operation of type filters?</input><br>';

      iHTML += '<hr>';

      iHTML += '<br><b>Filter by description/comments/following:</b><br>';
      iHTML += '<input type="checkbox" id="_cbMCHideMyFollowed" pairedWith="_cbMCHideMyUnfollowed">Ones I am or </input>';
      iHTML += '<input type="checkbox" id="_cbMCHideMyUnfollowed" pairedWith="_cbMCHideMyFollowed">am not following</input><br><br>';

      iHTML += '<input type="checkbox" id="_cbMCDescriptionMustBePresent" pairedWith="_cbMCDescriptionMustBeAbsent">Hide</input> or ';
      iHTML += '<input type="checkbox" id="_cbMCDescriptionMustBeAbsent" pairedWith="_cbMCDescriptionMustBePresent">show</input> MCs with no description<br>';
      iHTML += '<input type="checkbox" id="_cbMCCommentsMustBePresent" pairedWith="_cbMCCommentsMustBeAbsent">Hide</input> or ';
      iHTML += '<input type="checkbox" id="_cbMCCommentsMustBeAbsent" pairedWith="_cbMCCommentsMustBePresent">show</input> MCs with no comments<br>';
      iHTML += '<input type="checkbox" id="_cbMCExpiryMustBePresent" pairedWith="_cbMCExpiryMustBeAbsent">Hide</input> or ';
      iHTML += '<input type="checkbox" id="_cbMCExpiryMustBeAbsent" pairedWith="_cbMCExpiryMustBePresent">show</input> MCs with no expiry date<br>';
      iHTML += '<input type="checkbox" id="_cbMCEnableKeywordMustBePresent">Hide MCs not including </input>';
      iHTML += '<input type="text" style="font-size:14px; line-height:16px; height:22px; margin-bottom:4px;" id="_textMCKeywordPresent"><br>';
      iHTML += '<input type="checkbox" id="_cbMCEnableKeywordMustBeAbsent">Hide MCs including </input>';
      iHTML += '<input type="text" style="font-size:14px; line-height:16px; height:22px; margin-bottom:4px;" id="_textMCKeywordAbsent"><br>';
      iHTML += '&nbsp;&nbsp;<input type="checkbox" id="_cbMCCaseInsensitive"><i>Case-insensitive matches?</i></input><br>';
      iHTML += '<input type="checkbox" id="_cbMCCreatorIDFilter">Show MCs created by user</input>';
      iHTML += '<select id="_selectMCCreatorID" style="width:80%; height:22px;"></select><br>';

      iHTML += '<br><input type="checkbox" id="_cbHideWRCMCs"><b>Hide Waze_roadclosures MCs</b></input><br>';

      iHTML += '<br><b>Hide MCs with lock level:</b><br>';
      iHTML += '&nbsp;&nbsp;&nbsp;<input type="checkbox" id="_cbHideMCRank0">L1</input>';
      iHTML += '&nbsp;&nbsp;&nbsp;<input type="checkbox" id="_cbHideMCRank1">L2</input>';
      iHTML += '&nbsp;&nbsp;&nbsp;<input type="checkbox" id="_cbHideMCRank2">L3</input>';
      iHTML += '&nbsp;&nbsp;&nbsp;<input type="checkbox" id="_cbHideMCRank3">L4</input>';
      iHTML += '&nbsp;&nbsp;&nbsp;<input type="checkbox" id="_cbHideMCRank4">L5</input>';
      iHTML += '&nbsp;&nbsp;&nbsp;<input type="checkbox" id="_cbHideMCRank5">L6</input>';
      iHTML += '<hr>';
      iHTML += '<input type="checkbox" id="_cbMCEnhancePointMCVisibility">Enhance visibility of point MCs</input>';

      return iHTML;
   },
   PopulateCams: function()
   {
      let iHTML = '';
      
      iHTML = '<br><b>Show Cameras created by:</b><br>';
      iHTML += '<input type="checkbox" id="_cbShowWorldCams" checked>world_* users</input><br>';
      iHTML += '<input type="checkbox" id="_cbShowUSACams" checked>usa_* users</input><br>';
      iHTML += '<input type="checkbox" id="_cbShowNonWorldCams" checked>other users</input><br>';

      iHTML += '<br><b>Show Cameras touched by a specific editor:</b><br>';
      iHTML += '<input type="checkbox" id="_cbShowOnlyCamsCreatedBy">Created by</input>&nbsp;/&nbsp;';
      iHTML += '<input type="checkbox" id="_cbShowOnlyCamsEditedBy">edited by</input><br>';
      iHTML += '<input type="text" style="font-size:14px; line-height:16px; height:22px; margin-bottom:4px;" id="_textCameraEditor"><br>';
      iHTML += '<select id="_selectCameraUserID" style="width:80%; height:22px;"></select><br>';
      iHTML += '<br><input type="checkbox" id="_cbShowOnlyMyCams">Show ONLY cameras created/edited by me</input><br>';

      iHTML += '<br><b>Show Cameras by type:</b><br>';
      iHTML += '<input type="checkbox" id="_cbShowSpeedCams" checked>Speed</input><br>';
      iHTML += '&nbsp;&nbsp;&nbsp;<input type="checkbox" id="_cbShowIfSpeedSet" checked> with speed data</input><br>';
      iHTML += '&nbsp;&nbsp;&nbsp;<input type="checkbox" id="_cbShowIfNoSpeedSet" checked> with no speed data</input><br>';
      iHTML += '&nbsp;&nbsp;&nbsp;<input type="checkbox" id="_cbShowIfInvalidSpeedSet" checked> with invalid speed data (zoom 16+)</input><br>';
      iHTML += '&nbsp;&nbsp;&nbsp;<i>NOTE: slows down WME when deselected</i><br>';
      iHTML += '<input type="checkbox" id="_cbShowRedLightCams" checked>Red Light</input><br>';
      iHTML += '&nbsp;&nbsp;&nbsp;<input type="checkbox" id="_cbShowRLCIfZeroSpeedSet" checked> with speed limit = 0</input><br>';
      iHTML += '&nbsp;&nbsp;&nbsp;<input type="checkbox" id="_cbShowRLCIfNonZeroSpeedSet" checked> with speed limit > 0</input><br>';
      iHTML += '&nbsp;&nbsp;&nbsp;<input type="checkbox" id="_cbShowRLCIfNoSpeedSet" checked> with no speed data</input><br>';
      iHTML += '<input type="checkbox" id="_cbShowDummyCams" checked>Dummy</input><br>';

      iHTML += '<br><b>Hide Cameras by creator:</b><br>';
      iHTML += '&nbsp;&nbsp;&nbsp;<input type="checkbox" id="_cbHideCreatedByMe">me</input>';
      iHTML += '&nbsp;&nbsp;&nbsp;<input type="checkbox" id="_cbHideCreatedByRank0">L1</input>';
      iHTML += '&nbsp;&nbsp;&nbsp;<input type="checkbox" id="_cbHideCreatedByRank1">L2</input>';
      iHTML += '&nbsp;&nbsp;&nbsp;<input type="checkbox" id="_cbHideCreatedByRank2">L3</input>';
      iHTML += '&nbsp;&nbsp;&nbsp;<input type="checkbox" id="_cbHideCreatedByRank3">L4</input>';
      iHTML += '&nbsp;&nbsp;&nbsp;<input type="checkbox" id="_cbHideCreatedByRank4">L5</input>';
      iHTML += '&nbsp;&nbsp;&nbsp;<input type="checkbox" id="_cbHideCreatedByRank5">L6</input>';

      iHTML += '<br><b>Hide Cameras by updater:</b><br>';
      iHTML += '&nbsp;&nbsp;&nbsp;<input type="checkbox" id="_cbHideUpdatedByMe">me</input>';
      iHTML += '&nbsp;&nbsp;&nbsp;<input type="checkbox" id="_cbHideUpdatedByRank0">L1</input>';
      iHTML += '&nbsp;&nbsp;&nbsp;<input type="checkbox" id="_cbHideUpdatedByRank1">L2</input>';
      iHTML += '&nbsp;&nbsp;&nbsp;<input type="checkbox" id="_cbHideUpdatedByRank2">L3</input>';
      iHTML += '&nbsp;&nbsp;&nbsp;<input type="checkbox" id="_cbHideUpdatedByRank3">L4</input>';
      iHTML += '&nbsp;&nbsp;&nbsp;<input type="checkbox" id="_cbHideUpdatedByRank4">L5</input>';
      iHTML += '&nbsp;&nbsp;&nbsp;<input type="checkbox" id="_cbHideUpdatedByRank5">L6</input>';

      iHTML += '<br><br><b><input type="checkbox" id="_cbHideManualLockedCams">Show only auto-locked cameras</input></b><br>';

      iHTML += '<br><b><input type="checkbox" id="_cbHideCWLCams">Hide cameras on watchlist</input></b><br>';

      iHTML += '<br><b><input type="checkbox" id="_cbInvertCamFilters">Invert operation of camera filters?</input></b><br>';

      iHTML += '<b><input type="checkbox" id="_cbHighlightInsteadOfHideCams">Highlight instead of hide</input></b><br>';
      
      return iHTML;
   },
   PopulateRTC: function()
   {
      let iHTML = '';
      
      iHTML = '<br><b>Hide Road Closures:</b><br>';
      // Hidden checkbox to avoid errors when applying settings from previous versions of the script where this was an active control...
      iHTML += '<input type="checkbox" id="_cbHideUserRTCs" style="display: none;" />';

      iHTML += '<table style="text-align:center;">';
      iHTML += '<tr><td/><td><div class="map-marker road-closure status-finished" style="margin-left:0px;margin-top:0px;" /></td><td><div class="map-marker road-closure status-active" style="margin-left:0px;margin-top:0px;" /></td><td><div class="map-marker road-closure status-not-started" style="margin-left:0px;margin-top:0px;" /></td><td>???</td></tr>';
      iHTML += '<tr><td>From WME</td><td><input type="checkbox" id="_cbHideExpiredEditorRTCs" /></td><td><input type="checkbox" id="_cbHideEditorRTCs" /></td><td><input type="checkbox" id="_cbHideFutureEditorRTCs" /></td><td><input type="checkbox" id="_cbHideUnknownEditorRTCs" /></td></tr>';
      iHTML += '<tr><td>From WazeFeed</td><td><input type="checkbox" id="_cbHideExpiredWazeFeedRTCs" /></td><td><input type="checkbox" id="_cbHideWazeFeedRTCs" /></td><td><input type="checkbox" id="_cbHideFutureWazeFeedRTCs" /></td><td><input type="checkbox" id="_cbHideUnknownWazeFeedRTCs" /></td></tr>';
      iHTML += '<tr><td>From Staff</td><td><input type="checkbox" id="_cbHideExpiredWazeRTCs" /></td><td><input type="checkbox" id="_cbHideWazeRTCs" /></td><td><input type="checkbox" id="_cbHideFutureWazeRTCs" /></td><td><input type="checkbox" id="_cbHideUnknownWazeRTCs" /></td></tr>';
      iHTML += '<tr><td>In Sidepanel</td><td><input type="checkbox" id="_cbHideExpiredSidepanelRTCs" /></td><td><input type="checkbox" id="_cbHideSidepanelRTCs" /></td><td><input type="checkbox" id="_cbHideFutureSidepanelRTCs" /></td><td><input type="checkbox" id="_cbHideUnknownSidepanelRTCs" /></td></tr>';
      iHTML += '</table><br>';

      iHTML += '<input type="checkbox" id="_cbShowMTERTCs" pairedWith="_cbHideMTERTCs">Show</input> or ';
      iHTML += '<input type="checkbox" id="_cbHideMTERTCs" pairedWith="_cbShowMTERTCs">hide RTCs associated with MTE: </input>';
      iHTML += '<select id="_selectRTCMTE" style="width:80%; height:22px;"></select><br>';
      iHTML += '<br>';
      iHTML += 'Hide if:<br>';
      iHTML += '<input type="checkbox" id="_cbEnableRTCDurationFilterLessThan">Duration less than </input>';
      iHTML += '<input type="number" min="1" size="3" style="width:50px;line-height:14px;height:22px;margin-bottom:4px;" id="_inputFilterRTCDurationLessThan"> days<br>';
      iHTML += '<input type="checkbox" id="_cbEnableRTCDurationFilterMoreThan">Duration more than </input>';
      iHTML += '<input type="number" min="1" size="3" style="width:50px;line-height:14px;height:22px;margin-bottom:4px;" id="_inputFilterRTCDurationMoreThan"> days<br>';
      
      iHTML += '<br><b>Filter by date/time:</b><br>'; 
      iHTML += '<input type="checkbox" id="_cbRTCFilterShowForTS" pairedWith="_cbRTCFilterHideForTS">Show</input> or ';
      iHTML += '<input type="checkbox" id="_cbRTCFilterHideForTS" pairedWith="_cbRTCFilterShowForTS">hide</input> RTCs active at<br>';
      iHTML += 'Date: <input type="number" min="1" max="31" size="2" style="width:50px;line-height:14px;height:22px;margin-bottom:4px;" id="_inputRTCFilterDay"> / ';
      iHTML += '<input type="number" min="1" max="12" size="2" style="width:50px;line-height:14px;height:22px;margin-bottom:4px;" id="_inputRTCFilterMonth"> / ';
      iHTML += '<input type="number" min="2010" max="2100" size="4" style="width:50px;line-height:14px;height:22px;margin-bottom:4px;" id="_inputRTCFilterYear"><br>';
      iHTML += 'Time: <input type="number" min="0" max="23" size="2" style="width:50px;line-height:14px;height:22px;margin-bottom:4px;" id="_inputRTCFilterHour">:';
      iHTML += '<input type="number" min="0" max="59" size="2" style="width:50px;line-height:14px;height:22px;margin-bottom:4px;" id="_inputRTCFilterMin">';
         
      return iHTML;
   },
   PopulateRA: function()
   {
      let iHTML = '';
      iHTML = '<br><b>Filter Restricted Areas:</b><br>';
      iHTML += '<input type="checkbox" id="_cbShowSpecificRA">Show a specific area: </input>';
      iHTML += '<select id="_selectRA" style="width:80%; height:22px;"></select><br><br>';

      iHTML += '<input type="checkbox" id="_cbRAEditorIDFilter">Show areas edited by user: </input>';
      iHTML += '<select id="_selectRAEditorID" style="width:80%; height:22px;"></select><br><br>';

      iHTML += 'Hide if:<br>';
      iHTML += '<input type="checkbox" id="_cbEnableRAAgeFilterLessThan">Last modified less than </input>';
      iHTML += '<input type="number" min="1" size="3" style="width:50px;line-height:14px;height:22px;margin-bottom:4px;" id="_inputFilterRAAgeLessThan"> days ago<br>';
      iHTML += '<input type="checkbox" id="_cbEnableRAAgeFilterMoreThan">Last modified more than </input>';
      iHTML += '<input type="number" min="1" size="3" style="width:50px;line-height:14px;height:22px;margin-bottom:4px;" id="_inputFilterRAAgeMoreThan"> days ago<br>';
      
      return iHTML;
   },
   PopulateMisc: function()
   {
      let iHTML = '';
      iHTML += '<br><b><input type="checkbox" id="_cbHideSegmentsWhenRoadsHidden" />Hide segment layer when road layer is hidden</b><br>';
      iHTML += '<br><b><input type="checkbox" id="_cbKillInertialPanning" />Stop inertial panning when mouse moves out of map area</b><br>';

      iHTML += '<br><br><b><input type="checkbox" id="_cbCommentCount" />Show comment count on UR markers</b><br>';

      iHTML += '<br><br><b><input type="checkbox" id="_cbAutoApplyClonedClosure" />Auto-apply cloned closures</b><br>';
      iHTML += '<b><input type="checkbox" id="_cbAutoScrollClosureList" />Auto-scroll to end of closures</b><br>';

      iHTML += '<br><br><b>Disable filtering above zoom level </b>';
      iHTML += '<input type="number" min="12" max="22" value="22" size="2" style="width:50px;;line-height:14px;height:22px;margin-bottom:4px;" id="_inputFilterMinZoomLevel" /><br>';

      iHTML += '<br><br><b>Marker Unstacking:</b><br>';
      iHTML += 'Distance threshold: <input type="number" min="1" max="30" value="15" size="2" style="width:50px;;line-height:14px;height:22px;margin-bottom:4px;" id="_inputUnstackSensitivity" /><br>';
      iHTML += 'Disable below zoom: <input type="number" min="12" max="22" value="15" size="2" style="width:50px;;line-height:14px;height:22px;margin-bottom:4px;" id="_inputUnstackZoomLevel" /><br>';

      iHTML += '<br><br><b>Popup mouse behaviour:</b><br>';
      iHTML += 'Mouseover show delay <input type="number" min="1" max="10" value="2" size="2" style="width:50px;line-height:14px;height:22px;margin-bottom:4px;" id="_inputPopupEntryTimeout" /> *100ms<br>';
      iHTML += 'Mouseout hide delay <input type="number" min="1" max="10" value="2" size="2" style="width:50px;line-height:14px;height:22px;margin-bottom:4px;" id="_inputPopupExitTimeout" /> *100ms<br>';
      iHTML += 'Auto-hide after <input type="number" min="0" max="10" value="0" size="2" style="width:50px;line-height:14px;height:22px;margin-bottom:4px;" id="_inputPopupAutoHideTimeout" /> seconds<br>';

      iHTML += '<br><br><b>Disable clustering for:</b><br>';
      iHTML += '<input type="checkbox" id="_cbInhibitURClusters" />URs<br>';
      iHTML += '<input type="checkbox" id="_cbInhibitMPClusters" />MPs<br>';
      iHTML += '<input type="checkbox" id="_cbInhibitPUClusters" />PURs<br>';
      iHTML += '<input type="checkbox" id="_cbInhibitESClusters" />ESs<br>';

      iHTML += '<br><br><b>Disable popup for:</b><br>';
      iHTML += '<input type="checkbox" id="_cbInhibitURPopup" />URs<br>';
      iHTML += '<input type="checkbox" id="_cbInhibitMPPopup" />MPs<br>';
      iHTML += '<input type="checkbox" id="_cbInhibitCamPopup" />Cameras<br>';
      iHTML += '<input type="checkbox" id="_cbInhibitSegPopup" />Segments<br>';
      iHTML += '&nbsp;&nbsp;<input type="checkbox" id="_cbInhibitSegGenericPopup" />Speed limit info<br>';
      iHTML += '<input type="checkbox" id="_cbInhibitLandmarkPopup" />Places<br>';
      iHTML += '<input type="checkbox" id="_cbInhibitPUPopup" />Place Updates<br>';
      iHTML += '<input type="checkbox" id="_cbInhibitMapCommentPopup" />Map Comments<br>';
      iHTML += '<input type="checkbox" id="_cbInhibitNodesPopup" />Junction Nodes<br>';

      iHTML += '<br><br><b>Date/Time formatting for popups:</b><br>';
      iHTML += '<input type="checkbox" id="_cbDateFmtDDMMYY" pairedWith="_cbDateFmtMMDDYY,_cbDateFmtYYMMDD" checked />day/month/year<br>';
      iHTML += '<input type="checkbox" id="_cbDateFmtMMDDYY" pairedWith="_cbDateFmtDDMMYY,_cbDateFmtYYMMDD" />month/day/year<br>';
      iHTML += '<input type="checkbox" id="_cbDateFmtYYMMDD" pairedWith="_cbDateFmtMMDDYY,_cbDateFmtDDMMYY" />year/month/day<br><br>';
      iHTML += '<input type="checkbox" id="_cbTimeFmt24H" pairedWith="_cbTimeFmt12H" checked />24 hour<br>';
      iHTML += '<input type="checkbox" id="_cbTimeFmt12H" pairedWith="_cbTimeFmt24H" />12 hour<br><br>';
      iHTML += '<i>Unticked uses browser default setting</i>';

      iHTML += '<br><br><b><input type="checkbox" id="_cbWhiteBackground" />Use custom background colour</b><br>';
      iHTML += 'R:<input type="number" min="0" max="255" value="255" size="3" style="width:50px;;line-height:14px;height:22px;margin-bottom:4px;" id="_inputCustomBackgroundRed" />';
      iHTML += 'G:<input type="number" min="0" max="255" value="255" size="3" style="width:50px;;line-height:14px;height:22px;margin-bottom:4px;" id="_inputCustomBackgroundGreen" />';
      iHTML += 'B:<input type="number" min="0" max="255" value="255" size="3" style="width:50px;;line-height:14px;height:22px;margin-bottom:4px;" id="_inputCustomBackgroundBlue" /><br>';

      iHTML += '<br><br><b>Replace "Next ..." button with "Done" for:</b><br>';
      iHTML += '<input type="checkbox" id="_cbInhibitNURButton" />URs<br>';
      iHTML += '<input type="checkbox" id="_cbInhibitNMPButton" />MPs<br>';
      iHTML += '<input type="checkbox" id="_cbInhibitNPURButton" />PURs<br>';

      iHTML += '<br><br><b>Disable on-click recentering for:</b><br>';
      iHTML += '<input type="checkbox" id="_cbInhibitURCentering" />URs<br>';
      iHTML += '<input type="checkbox" id="_cbInhibitMPCentering" />MPs<br>';
      iHTML += '<input type="checkbox" id="_cbInhibitPURCentering" />PURs<br>';
      iHTML += '<input type="checkbox" id="_cbInhibitPPURCentering" />PPURs<br>';
      iHTML += '<input type="checkbox" id="_cbInhibitRPURCentering" />RPURs<br>';

      iHTML += '<br><br><b><input type="checkbox" id="_cbHideAMLayer" />Hide Area Manager polygons</b><br>';
      iHTML += '<b><input type="checkbox" id="_cbMoveAMList" />Show AMs in topbar when AM layer is active</b><br>';
      iHTML += '<br><b><input type="checkbox" id="_cbDisablePlacesFiltering" />Disable Places filtering</b><br>';

      iHTML += '<br><br><b>Settings backup/restore/reset:</b><br>';
      iHTML += '<input type="button" id="_btnSettingsToText" value="Backup" />&nbsp;&nbsp;&nbsp;';
      iHTML += '<input type="button" id="_btnTextToSettings" value="Restore" />&nbsp;&nbsp;|&nbsp;&nbsp;';
      iHTML += '<input type="button" id="_btnResetSettings" value="Reset" /><br><br>';
      iHTML += '<textarea id="_txtSettings" value=""></textarea><br>';
      iHTML += '<input type="button" id="_btnClearSettingsText" value="Clear" /><br>';

      /*
      iHTML += '<br><br><b>Debug:</b><br>';
      iHTML += '<input type="button" id="_btnDebugToScreen" value="Show debug data" />';
      */   
      
      return iHTML;
   },
   PopulateOWL()
   {
      let camTypes = new Array("","","Speed", "Dummy", "Red Light");
      let iHTML = '';
   
      if(document.getElementById('_uroCWLGroupSelect') !== null)
      {
         uroTabs.selectedOWLGroup = document.getElementById('_uroCWLGroupSelect').selectedIndex;
      }
      iHTML = '<br><b>Camera Watchlist:</b><br><br>';
      iHTML += '<div id="_uroCWLCamList" style="height:65%;overflow:auto;">';
      if(uroOWL.CWLGroups.length > 0)
      {
         let camidx;
         for(let groupidx=0;groupidx<uroOWL.CWLGroups.length;groupidx++)
         {
            let groupObj = uroOWL.CWLGroups[groupidx];
            iHTML += '<div id="_uroCWLGroup-'+groupidx+'">';
            if(groupObj.groupCollapsed === true)
            {
               iHTML += '<i class="fa fa-plus-square-o" style="cursor:pointer;font-size:14px;" id="_uroCWLGroupState-'+groupidx+'"></i>';
            }
            else
            {
               iHTML += '<i class="fa fa-minus-square-o" style="cursor:pointer;font-size:14px;" id="_uroCWLGroupState-'+groupidx+'"></i>';
            }
            iHTML += '<b>'+groupObj.groupName+'</b><br>';
            groupObj.groupCount = 0;
            if(uroOWL.CamWatchObjects.length > 0)
            {
               for(camidx=0;camidx<uroOWL.CamWatchObjects.length;camidx++)
               {
                  let camObj = uroOWL.CamWatchObjects[camidx];
                  if(camObj.groupID == groupObj.groupID)
                  {
                     groupObj.groupCount++;
                     let changed = uroOWL.CamDataChanged(camidx);
                     let deleted = (camObj.loaded === false);
                     iHTML += '<div id="_uroCWL-'+camidx+'" style="padding:3px;border-width:2px;border-style:solid;border-color:#FFFFFF;background-color:';
                     if(camObj.server != W.app.getAppRegionCode())
                     {
                        if(camObj.server == '??') iHTML += '#A0A0A0;';
                        else iHTML += '#AAFFAA;';
                     }
                     else if(changed) iHTML += '#AAAAFF;';
                     else if(deleted) iHTML += '#FFAAAA;';
                     else iHTML += '#FFFFFF;';
   
                     if(groupObj.groupCollapsed === true) iHTML += 'display:none;">';
                     else iHTML += 'display:block;">';
   
                     iHTML += 'ID: '+camObj.fid;
                     iHTML += ' ('+camObj.server+')';
                     iHTML += ' Type: '+camTypes[camObj.watch.type];
                     if(camObj.server != W.app.getAppRegionCode())
                     {
                        if(camObj.server == '??')
                        {
                           iHTML += '<br><i>Unknown server</i>';
                        }
                        else
                        {
                           iHTML += '<br><i>Not on this server</i>';
                        }
                     }
                     else if(deleted)
                     {
                        iHTML += '<br>DELETED';
                     }
                     else if(changed)
                     {
                        if(camObj.current.type != camObj.watch.type)
                        {
                           iHTML += '<br>&nbsp;&nbsp;Type changed';
                           iHTML += ' ('+camObj.watch.type+' to '+camObj.current.type+')';
                        }
                        if(camObj.current.azymuth != camObj.watch.azymuth)
                        {
                           iHTML += '<br>&nbsp;&nbsp;Azimuth changed';
                           iHTML += ' ('+camObj.watch.azymuth+' to '+camObj.current.azymuth+')';
                        }
                        if(camObj.current.speed != camObj.watch.speed)
                        {
                           iHTML += '<br>&nbsp;&nbsp;Speed changed';
                           iHTML += ' ('+camObj.watch.speed+' to '+camObj.current.speed+')';
                        }
                        if(camObj.current.lat != camObj.watch.lat)
                        {
                           iHTML += '<br>&nbsp;&nbsp;Latitude changed';
                           iHTML += ' ('+camObj.watch.lat+' to '+camObj.current.lat+')';
                        }
                        if(camObj.current.lon != camObj.watch.lon)
                        {
                           iHTML += '<br>&nbsp;&nbsp;Longitude changed';
                           iHTML += ' ('+camObj.watch.lon+' to '+camObj.current.lon+')';
                        }
                     }
   
                     if(camObj.server == W.app.getAppRegionCode())
                     {
                        if(deleted === false)
                        {
                           iHTML += '&nbsp;<i class="fa fa-group" style="cursor:pointer;font-size:14px;color:#ccccff;" id="_uroCWLIcon1-'+camidx+'"></i>';
                        }
                        iHTML += '&nbsp;<i class="fa fa-arrow-circle-right" style="cursor:pointer;font-size:14px;color:#ccccff;" id="_uroCWLIcon2-'+camidx+'"></i>';
                     }
                     iHTML += '</div>';
                  }
               }
            }
            iHTML += '</div>';
         }
      }
      iHTML += '</div><div id="_uroCWLControls">';
      iHTML += '<hr>Group control:<br>';
      iHTML += '<select id="_uroCWLGroupSelect" style="width:40%;height:22px;"></select>&nbsp;<input type="button" id="_btnCWLGroupDel" value="Delete group"><br>';
      iHTML += '<input type="text" id="_uroCWLGroupEntry" style="width:40%;height:22px;">&nbsp;<input type="button" id="_btnCWLGroupAdd" value="Add group">';
      iHTML += '<br><input type="button" id="_btnRescanCamWatchList" value="Refresh camera data"><br><br>';
      iHTML += '<input type="button" id="_btnUpdateCamValues" value="Accept all changes"><br><br>';
      iHTML += '<b>Remove cameras from OWL:</b><br>';
      iHTML += '<input type="button" id="_btnRemoveDeletedCameras" value="Deleted">&nbsp;&nbsp;';
      iHTML += '<input type="button" id="_btnRemoveUnknownServerCameras" value="Unknown Server">&nbsp;&nbsp;';
      iHTML += '<input type="button" id="_btnClearCamWatchList" value="ALL Cameras">';
      iHTML += '</div>';
      uroTabs.CtrlTabs[uroTabs.IDS.OWL][uroTabs.FIELDS.TABBODY].innerHTML = uroUtils.ModifyHTML(iHTML);
   
      uroTabs.FinaliseOWLTab();
   },
   FinaliseOWLTab: function()
   {
      if(uroOWL.CamWatchObjects.length > 0)
      {
         if(document.getElementById("_uroCWL-0") == null)
         {
            window.setTimeout(uroTabs.FinaliseOWLTab,100);
            return;
         }
   
         for(let camidx=0;camidx<uroOWL.CamWatchObjects.length;camidx++)
         {
            document.getElementById("_uroCWL-"+camidx).onmouseover = uroOWL.HighlightCWLEntry;
            document.getElementById("_uroCWL-"+camidx).onmouseleave = uroOWL.UnhighlightCWLEntry;
   
            if(uroOWL.CamWatchObjects[camidx].server == W.app.getAppRegionCode())
            {
               let icon1 = document.getElementById("_uroCWLIcon1-"+camidx);
               let icon2 = document.getElementById("_uroCWLIcon2-"+camidx);
               if(icon1 !== null)
               {
                  icon1.onmouseover = uroOWL.CWLIconHighlight;
                  icon1.onmouseleave = uroOWL.CWLIconLowlight;
                  icon1.onclick = uroOWL.AssignCameraToGroup;
               }
               if(icon2 !== null)
               {
                  icon2.onmouseover = uroOWL.CWLIconHighlight;
                  icon2.onmouseleave = uroOWL.CWLIconLowlight;
                  icon2.onclick = uroOWL.GotoCam;
               }
            }
         }
      }
   
      if(document.getElementById('_btnClearCamWatchList') == null)
      {
         window.setTimeout(uroTabs.FinaliseOWLTab, 100);
         return;
      }
   
      uroUtils.AddEventListener('_btnClearCamWatchList', 'click', uroOWL.ClearCamWatchList, true);
      uroUtils.AddEventListener('_btnRemoveDeletedCameras', 'click', uroOWL.ClearDeletedCameras, true);
      uroUtils.AddEventListener('_btnRemoveUnknownServerCameras', 'click', uroOWL.ClearUnknownServerCameras, true);
      uroUtils.AddEventListener('_btnRescanCamWatchList', 'click', uroOWL.RescanCamWatchList, true);
      uroUtils.AddEventListener('_btnUpdateCamValues', 'click', uroOWL.AcceptCameraChanges, true);
      uroUtils.AddEventListener('_btnCWLGroupDel', 'click', uroOWL.RemoveCWLGroup, true);
      uroUtils.AddEventListener('_btnCWLGroupAdd', 'click', uroOWL.AddCWLGroup, true);
      if(document.getElementById('_uroCWLGroupSelect') !== null)
      {
         uroDBG.AddLog('populating CWL group list');
         uroOWL.PopulateCWLGroupSelect();
         let selector = document.getElementById('_uroCWLGroupSelect');
         if(uroTabs.selectedOWLGroup >= selector.length)
         {
            uroTabs.selectedOWLGroup = 0;
         }
         selector.selectedIndex = uroTabs.selectedOWLGroup;
      }
   
      if(uroOWL.CWLGroups.length > 0)
      {
         for(let groupidx=0;groupidx<uroOWL.CWLGroups.length;groupidx++)
         {
            if(uroOWL.CWLGroups[groupidx].groupCount === 0)
            {
               uroTabs.SetStyleDisplay('_uroCWLGroup-'+groupidx,'none');
            }
            else
            {
               uroUtils.SetOnClick('_uroCWLGroupState-'+groupidx,uroOWL.CWLGroupCollapseExpand);
            }
         }
      }
   },  
   ActiveTab: function(_id)
   {
      let e = document.getElementById(_id);
      e.style.backgroundColor = "greenyellow";
      e.style.borderTop = "1px solid";
      e.style.borderLeft = "1px solid";
      e.style.borderRight = "1px solid";
      e.style.borderBottom = "0px solid";
   },
   InactiveTab: function(_id)
   {
      let e = document.getElementById(_id);
      e.style.backgroundColor = "gainsboro";
      e.style.borderTop = "0px solid";
      e.style.borderLeft = "0px solid";
      e.style.borderRight = "0px solid";
      e.style.borderBottom = "1px solid";
   },
   InactiveAllTabs: function()
   {
      for(let i = 0; i < uroTabs.CtrlTabs.length; ++i)
      {
         uroTabs.InactiveTab(uroTabs.CtrlTabs[i][uroTabs.FIELDS.TABHEADER]);
         uroTabs.SetStyleDisplay(uroTabs.CtrlTabs[i][uroTabs.FIELDS.TABBODY], 'none');
      }   
   },
   ShowTab: function(tabID)
   {
      uroTabs.InactiveAllTabs();
      uroTabs.ActiveTab(uroTabs.CtrlTabs[tabID][uroTabs.FIELDS.TABHEADER]);
      uroTabs.SetStyleDisplay(uroTabs.CtrlTabs[tabID][uroTabs.FIELDS.TABBODY], 'block');
      return false;   
   },
   ShowURs: function()
   {
      uroTabs.ShowTab(uroTabs.IDS.URS);
      return false;
   },
   ShowMPs: function()
   {
      uroTabs.ShowTab(uroTabs.IDS.MPS);
      return false;
   },
   ShowMCs: function()
   {
      uroTabs.ShowTab(uroTabs.IDS.MCS);
      return false;
   },
   ShowPlaces: function()
   {
      uroTabs.ShowTab(uroTabs.IDS.PLACES);
      for(let idx=0;idx<uroPlacesGroupsCollapsed.length;idx++)
      {
         uroPlacesGroupCEHandler(idx);
      }
      return false;
   },
   ShowCams: function()
   {
      uroTabs.ShowTab(uroTabs.IDS.CAMS);
      return false;
   },
   ShowOWL: function()
   {
      uroTabs.ShowTab(uroTabs.IDS.OWL);
      uroTabs.PopulateOWL();
      return false;
   },
   ShowMisc: function()
   {
      uroTabs.ShowTab(uroTabs.IDS.MISC);
      return false;
   },
   ShowRTCs: function()
   {
      uroTabs.ShowTab(uroTabs.IDS.RTCS);
      return false;
   },
   ShowRAs: function()
   {
      uroTabs.ShowTab(uroTabs.IDS.RAS);
      return false;
   },
   ClickURs: function()
   {
      uroFilterURs();
   },
   ClickMPs: function()
   {
      uroFilterProblems();
   },
   ClickMCs: function()
   {
      uroFilterMapComments();
      uroLayers.MCLayerChanged();
   },
   ClickPlaces: function()
   {
      uroFilterPlaces();
   },
   ClickCams: function()
   {
      uroFilterCameras();
   },
   ClickOWL: function()
   {
      // no action required
   },
   ClickMisc: function()
   {
      uroFilterItems();
      uroUITweaks.ChangeMapBGColour();
      uroUITweaks.HideAMLayer();
      uroUITweaks.HideSegments();
      uroUITweaks.ChangeClustering();
   },
   ClickRTCs: function()
   {
      uroFilterRTCs();
      uroClosureListHandler();
   },
   ClickRAs: function()
   {
      uroFilterRAs();
   },
   CreateTabHeaders: function()
   {
      uroTabs.CtrlTabs =
      [
         ['_tabURs',    null, '_linkURs',    'URs',      uroTabs.ShowURs,     uroTabs.ClickURs,    'UROverviewUROptions',     uroTabs.PopulateUR],
         ['_tabMPs',    null, '_linkMPs',    'MPs',      uroTabs.ShowMPs,     uroTabs.ClickMPs,    'UROverviewMPOptions',     uroTabs.PopulateMP],
         ['_tabMCs',    null, '_linkMCs',    'MCs',      uroTabs.ShowMCs,     uroTabs.ClickMCs,    'UROverviewMCOptions',     uroTabs.PopulateMC],
         ['_tabRTCs',   null, '_linkRTCs',   'RTCs',     uroTabs.ShowRTCs,    uroTabs.ClickRTCs,   'UROverviewRTCOptions',    uroTabs.PopulateRTC],
         ['_tabRAs',    null, '_linkRAs',    'RAs',      uroTabs.ShowRAs,     uroTabs.ClickRAs,    'UROverviewRAOptions',     uroTabs.PopulateRA],
         ['_tabPlaces', null, '_linkPlaces', 'Places',   uroTabs.ShowPlaces,  uroTabs.ClickPlaces, 'UROverviewPlacesOptions', uroTabs.PopulatePlaces],
         ['_tabCams',   null, '_linkCams',   'Cams',     uroTabs.ShowCams,    uroTabs.ClickCams,   'UROverviewCameraOptions', uroTabs.PopulateCams],
         ['_tabOWL',    null, '_linkOWL',    'OWL',      uroTabs.ShowOWL,     uroTabs.ClickOWL,    null,                      null],
         ['_tabMisc',   null, '_linkMisc',   'Misc',     uroTabs.ShowMisc,    uroTabs.ClickMisc,   'UROverviewMiscOptions',   uroTabs.PopulateMisc]
      ];

      let i;
      let tabsTotal = uroTabs.CtrlTabs.length;
      let tabsPerRow = Math.ceil(tabsTotal / Math.ceil(tabsTotal / uroTabs.MAX_PER_ROW));
      let tabCount = 0;
      let tabbyHTML = '';
      for(i = 0; i < tabsTotal; ++i)
      {
         if(tabCount == 0)
         {
            tabbyHTML += '<table border=0 width="100%"><tr>';
         }
         tabbyHTML += '<td valign="center" align="center" id="'+uroTabs.CtrlTabs[i][uroTabs.FIELDS.TABHEADER]+'">';
         tabbyHTML += '<a href="#" id="'+uroTabs.CtrlTabs[i][uroTabs.FIELDS.LINKID]+'" style="text-decoration:none;font-size:12px">'+uroTabs.CtrlTabs[i][uroTabs.FIELDS.TABTITLE]+'</a></td>';
         if(((++tabCount == tabsPerRow) && (i < (tabsTotal - 1))) || (i == (tabsTotal - 1)))
         {
            tabbyHTML += '</tr></table>';
            tabCount = 0;
         }
      }
      return tabbyHTML;
   },
   CreateTabBodies: function()
   {
      for(let i = 0; i < uroTabs.CtrlTabs.length; ++i)
      {
         uroTabs.CtrlTabs[i][uroTabs.FIELDS.TABBODY] = document.createElement('div');
      }
   },
   AddToDOM: function()
   {
      let i;
      for(i = 0; i < uroTabs.CtrlTabs.length; ++i)
      {
         if(uroTabs.CtrlTabs[i][uroTabs.FIELDS.POPULATEFN] != null)
         {
            uroTabs.CtrlTabs[i][uroTabs.FIELDS.TABBODY].innerHTML = uroTabs.CtrlTabs[i][uroTabs.FIELDS.POPULATEFN]();
         }
         document.getElementById('uroControlsContainer').appendChild(uroTabs.CtrlTabs[i][uroTabs.FIELDS.TABBODY]);
         uroTabs.CtrlTabs[i][uroTabs.FIELDS.TABBODY].onclick = uroTabs.CtrlTabs[i][uroTabs.FIELDS.CLICKFN];
      }   
   }
};
const uroDBG = // debug output handling
{
   // true enables debug output during script startup
   showDebugOutput: true,
   // true keeps debug output enabled after script startup
   persistentDebugOutput: false,
   // true enables performance monitoring debug output
   performanceMonitoringOutput: false,

   recentDebug: [],

   Add: function(debugtext)
   {
      let ts = Math.round(performance.now());
      if(uroDBG.recentDebug.length == 100)
      {
         uroDBG.recentDebug.shift();
      }
      uroDBG.recentDebug.push(ts+': '+debugtext);
      console.debug('URO+DBG '+ts+':'+debugtext);
   },
   Dump: function()
   {
      if(uroDBG.recentDebug.length > 0)
      {
         document.getElementById('WazeMap').innerHTML = uroUtils.ModifyHTML('<textarea id="uroDbgOutput" style="width:100%;height:100%">');
         let dbgOutput = '';
         for(let i = 0; i < uroDBG.recentDebug.length; i++)
         {
            dbgOutput += uroDBG.recentDebug[i]+'\n';
         }
         document.getElementById('uroDbgOutput').textContent = dbgOutput;
      }
   },
   Toggle: function()
   {
      uroDBG.showDebugOutput = !uroDBG.showDebugOutput;
      let dbgMode = "none";
      if(uroDBG.showDebugOutput)
      {
         dbgMode = "inline";
      }
      document.getElementById('_uroDebugMode').style.display = dbgMode;
   },
   PerfMon: function(source, ts)
   {
      if(uroDBG.performanceMonitoringOutput === true)
      {
         console.log(source+': '+(performance.now() - ts));
      }
   },
   AddLog: function(logtext)
   {
      if(uroDBG.showDebugOutput) console.log('URO+: '+Date()+' '+logtext);
   }
};
const uroAFN = // area friendly name functions
{
   friendlyNames: [],
   hoverTime: -1,
   hoverObj: null,
   overlayShown: false,
   editHovered: false,
   editBox: null,
   friendlyAreaNames: null,
   AFNObject: function(fName, area, server)
   {
      this.fName = fName;
      this.area = area;
      this.server = server;
   },
   UpdateName: function(name, server, area)
   {
      let foundExisting = false;
      for(let i = 0; i < uroAFN.friendlyNames.length; i++)
      {
         if((uroAFN.friendlyNames[i].server == server) && (uroAFN.friendlyNames[i].area == area))
         {
            if(name === "")
            {
               this.friendlyNames.splice(i, 1);
               foundExisting = true;
            }
            else
            {
               uroAFN.friendlyNames[i].fName = name;
               foundExisting = true;
            }
         }
      }
   
      if((foundExisting === false) && (name !== ""))
      {
         uroAFN.friendlyNames.push(new uroAFN.AFNObject(name, area, server));
      }
      uroAFN.ReplaceAreaNames(true);
   },
   AreaNameHover: function()
   {
      if((uroAFN.hoverObj === null) || (uroAFN.hoverObj != this))
      {
         uroAFN.hoverTime = 0;
      }
      uroAFN.hoverObj = this;
   },
   AreaNameUnHover: function()
   {
      if(uroAFN.editHovered === true)
      {
         return false;
      }
      if(uroAFN.overlayShown)
      {
         uroAFN.hoverObj.removeChild(uroAFN.editBox);
      }
      uroAFN.hoverObj = null;
      uroAFN.hoverTime = -1;
      uroAFN.overlayShown = false;
   },
   EditHover: function()
   {
      uroAFN.editHovered = true;
      uroUtils.AddEventListener('uroANEditBox', 'mouseout', uroAFN.EditUnHover, false);
      uroUtils.AddEventListener('uroANEditBox', 'click', uroAFN.EditClick, false);
   },
   EditUnHover: function()
   {
      let newName = document.getElementById('_textAreaName').value;
      // sanitise name to avoid conflicts with config storage delimiters...
      newName = newName.replace(',','');
      newName = newName.replace(':','');
      let server = W.app.getAppRegionCode();
      let area = uroAFN.GetAreaArea(uroAFN.hoverObj.parentNode.children[1].innerText);
      uroAFN.hoverObj.removeChild(uroAFN.editBox);
      uroAFN.overlayShown = false;
      uroAFN.editHovered = false;
      uroAFN.UpdateName(newName, server, area);
   },
   EditClick: function(e)
   {
      // this traps the click to prevent it falling through to the underlying area name element and
      // potentially causing the map view to be relocated to that area...
      e.stopPropagation();
   },
   GetAreaArea: function(area)
   {
      area = parseFloat(area.split(' ')[0]);
      return area;
   },
   OverlaySetup: function()
   {
      uroAFN.overlayShown = true;
   
      uroAFN.editBox = document.createElement('div');
      uroAFN.editBox.id = "uroANEditBox";
      uroAFN.editBox.style.position = "absolute";
      uroAFN.editBox.style.top = '0px';
      uroAFN.editBox.style.left = '0px';
      uroAFN.editBox.style.width = "99%";
      uroAFN.hoverObj.appendChild(uroAFN.editBox);
      uroAFN.editBox.onmouseover = uroAFN.EditHover();
      let existingName = uroAFN.hoverObj.innerHTML;
      let italicTagPos = existingName.indexOf(' <i>');
      if(italicTagPos == -1)
      {
         existingName = "";
      }
      else
      {
         existingName = existingName.substr(0,italicTagPos);
      }
      uroAFN.editBox.innerHTML = uroUtils.ModifyHTML('<input type="text" style="font-size:14px; line-height:16px; height:22px; width:100%" id="_textAreaName" value="'+existingName+'">');
   },
   ReplaceAreaNames: function(replaceAfterNameChange)
   {
      if(document.getElementById('sidepanel-areas') === undefined)
      {
         return;
      }
   
      if(document.getElementById('sidepanel-areas').getElementsByClassName('result-list').length === 0)
      {
         return;
      }
   
      if(replaceAfterNameChange === false)
      {
         if(document.getElementById('sidepanel-areas').getElementsByClassName('result-list')[0].id == "friendlyNamed")
         {
            return;
         }
      }
   
      let panelRootObj = document.getElementById('sidepanel-areas').getElementsByClassName('result-list')[0];
      if(panelRootObj === undefined)
      {
         // we get here if the user doesn't have any areas defined...
         return;
      }
      let areaNameObjs = panelRootObj.getElementsByClassName('list-item-card-info');
      if(areaNameObjs.length === 0)
      {
         return;
      }
   
      let localisedManagedArea = I18n.lookup("user.areas.managed_area");
      for(let loop=0; loop < areaNameObjs.length; loop++)
      {
         if(areaNameObjs[loop].children.length === 2)
         {
            let title = areaNameObjs[loop].children[0].innerText;
            if(title.indexOf(localisedManagedArea) > -1)
            {
               let area = uroAFN.GetAreaArea(areaNameObjs[loop].children[1].innerText);
               areaNameObjs[loop].children[0].innerHTML = uroUtils.ModifyHTML(localisedManagedArea);
   
               for(let fnIdx=0; fnIdx < uroAFN.friendlyNames.length; fnIdx++)
               {
                  let fnObj = uroAFN.friendlyNames[fnIdx];
                  if((fnObj.area == area) && (fnObj.server == W.app.getAppRegionCode()))
                  {
                     areaNameObjs[loop].children[0].innerHTML = uroUtils.ModifyHTML(fnObj.fName +' <i>('+localisedManagedArea+')</i>');
                     break;
                  }
               }
               let titleObj = areaNameObjs[loop].getElementsByClassName('list-item-card-title')[0];
               titleObj.addEventListener("mouseover", uroAFN.AreaNameHover, false);
               titleObj.addEventListener("mouseout", uroAFN.AreaNameUnHover, false);
               titleObj.style.cursor = "text";
            }
         }
      }
      document.getElementById('sidepanel-areas').getElementsByClassName('result-list')[0].id = "friendlyNamed";
   },
   ApplyNames: function()
   {
      let objects = localStorage.UROverviewFriendlyAreaNames.split(':');
      uroAFN.friendlyNames = [];
   
      for(let objIdx=0;objIdx<objects.length;objIdx++)
      {
         let fields = objects[objIdx].split(',');
         uroAFN.friendlyNames.push(new uroAFN.AFNObject(fields[0],parseFloat(fields[1]),fields[2]));
      }
   
      uroAFN.ReplaceAreaNames(true);
   }   
};
const uroMarkers = // marker-related function
{
   elm : null,
   obj : null,
   id : null,
   type : null,
   lastOver : null,

   mouseX : null,
   mouseY : null,
   mouseButtons : null,
   clientX : null,
   clientY : null,
   armHover : false,
   entryTimeout : null,
   inhibitSetCenter : false,
   clickedOnCenter : null,
   clickedOnID : null,

   EntryTimeout: function()
   {
      uroMarkers.armHover = false;
      if(uroMarkers.lastOver !== null)
      {
         if(uroMarkers.type === 'cam')
         {
            if(uroUtils.GetCBChecked('_cbHighlightInsteadOfHideCams') === true)
            {
               if(uroMarkers.lastOver !== uroMarkers.id)
               {
                  window.setTimeout(uroFilterCameras, 50);
               }
            }
         }
         else if((uroMarkers.type == uroLayers.ID.UR) || (uroMarkers.type == uroLayers.ID.MP))
         {
            if(uroMarkers.type == uroLayers.ID.UR) uroHoveredURID = uroMarkers.id;
         }

         uroDBG.AddLog('hover over marker (Type ' + uroMarkers.type + ' / ID ' + uroMarkers.id + ')');
         uroPopup.Generate();
      }
   },
   MouseMove: function(e)
   {
      uroMarkers.buttons = e.buttons;
      uroMarkers.mouseX = e.pageX - document.getElementById('map').getBoundingClientRect().left;
      uroMarkers.mouseY = e.pageY - document.getElementById('map').getBoundingClientRect().top;
      uroMarkers.clientX = e.clientX;
      uroMarkers.clientY = e.clientY;

      if(uroMarkers.armHover === true)
      {
         let eto = uroUtils.GetElmValue('_inputPopupEntryTimeout') * 100;
         if(uroMarkers.entryTimeout !== null)
         {
            window.clearTimeout(uroMarkers.entryTimeout);
         }
         uroMarkers.entryTimeout = window.setTimeout(uroMarkers.EntryTimeout, eto);
      }
   },
   TranslateType: function(ft)
   {
      const TLU =
      [
         ["mapProblem", uroLayers.ID.MP],
         ["mapUpdateRequest", uroLayers.ID.UR],
         ["placeUpdate", uroLayers.ID.PUR],
         ["segmentSuggestionGeoIcon", uroLayers.ID.SegSug],
         ["camera", "cam"],
         ["node", "node"],
         ["comment", "comment"],
         ["venue", "venue"],
         ["segment", "segment"]
      ];

      let retval = null;
      for(let i = 0; i < TLU.length; ++i)
      {
         if(ft == TLU[i][0])
         {
            retval = TLU[i][1];
            break;
         }
      }
      return retval;
   },
   MouseOver: function(e)
   {
      let elm = null;
      let obj = null;
      let id = null;
      let markerType = null;
      let ft = e?.feature?.attributes?.wazeFeature?.featureType;
      if(ft !== undefined)
      {
         // single marker...
         obj = e.feature.attributes.wazeFeature._wmeObject;
         elm = W.userscripts.getMapElementByDataModel(obj);
         id = e.feature.attributes.wazeFeature.id;
         markerType = uroMarkers.TranslateType(ft);

         uroMarkers.elm = elm;
         uroMarkers.obj = obj;
         uroMarkers.id = id;
         uroMarkers.type = markerType;
         uroMarkers.lastOver = id;
         uroMarkers.armHover = true;

         uroMarkers.AddMarkerEventHandler();
      }
      /*
      else
      {
         if(e?.feature?.cluster !== undefined)
         {
            // cluster marker...
            let cm = e.feature.cluster;
            if(cm.length > 0)
            {
               // clusters are always of the same type, so just need to check the
               // first marker within the cluster to see what the type is for all
               // of them...
               ft = cm[0].attributes.wazeFeature.featureType;
               markerType = uroMarkers.TranslateType(ft);

               console.debug("Cluster Marker:");
               console.debug("  Type = " + ft);
               console.debug("  Cluster size = " + cm.length);
               console.debug(e);
            }
         }
      }
      */
   },
   MouseOver2: function(e)
   {
      let elm = null;
      let obj = null;
      let id = null;
      let markerType = null;
      let ft = e?.currentTarget?.attributes?.class?.value;
      if(ft !== undefined)
      {
         elm = e.currentTarget;
         if(ft.indexOf("permanentHazardMarker") !== -1)
         {
            elm = elm.parentNode;
            markerType = "phCam";
         }
         obj = W.userscripts.getDataModelByMapElement(elm);
         id = elm.attributes["data-id"].value;

         console.debug(ft);
         console.debug(id);

         uroMarkers.elm = elm;
         uroMarkers.obj = obj;
         uroMarkers.id = id;
         uroMarkers.type = markerType;
         uroMarkers.lastOver = id;
         uroMarkers.armHover = true;
      }
   },
   MouseOut: function()
   {
      uroMarkers.lastOver = null;
      if(uroMarkers.type !== null)
      {
         if(uroMarkers.type === 'cam')
         {
            if(uroUtils.GetCBChecked('_cbHighlightInsteadOfHideCams') === true)
            {
               window.setTimeout(uroFilterCameras, 50);
            }
         }
         uroDBG.AddLog('hover off '+uroMarkers.type+' ID '+uroMarkers.id);
         uroHoveredURID = null;

         uroFID = -1;
         if(uroStackType !== null)
         {
            let tStackType = uroStackType;
            uroRestackMarkers();
            if(tStackType == 1) 
            {
               uroFilterURs();
            }
            else if(tStackType == 2) 
            {
               uroFilterProblems();
            }
            else if(tStackType == 3) 
            {
               uroFilterPlaces();
            }
         }

         if(uroPopup.timer == -1)
         {
            uroPopup.timer = uroUtils.GetElmValue('_inputPopupExitTimeout');
         }   
      }
      else
      {
         uroDBG.AddLog('hover off unknown object...');
      }
   },
   MouseOut2: function()
   {
      uroMarkers.lastOver = null;
      if(uroMarkers.type !== null)
      {
         uroDBG.AddLog('hover off '+uroMarkers.type+' ID '+uroMarkers.id);
         uroHoveredURID = null;
         uroFID = -1;
         if(uroPopup.timer == -1)
         {
            uroPopup.timer = uroUtils.GetElmValue('_inputPopupExitTimeout');
         }   
      }
      else
      {
         uroDBG.AddLog('hover off unknown object...');
      }
   },   
   MouseDown: function()
   {
      // Do this stuff in the mousedown event rather than the click event so we fire before any of the native
      // click events - we need to ensure this happens for inhibiting marker centering, as we need to capture
      // the markerType ahead of our interceptor function being called to deal with the centering...
      if(uroMarkers.type !== null)
      {
         uroDBG.AddLog('clicked on '+uroMarkers.type+' marker '+uroMarkers.id);
         uroMarkers.clickedOnID = uroMarkers.id;
         uroMarkers.clickedOnCenter = W.map.getCenter();
   
         uroInhibitURFiltering = true;
   
         if(uroMarkers.inhibitSetCenter === false)
         {
            if(uroMarkers.Decentre() === true)
            {
               uroMarkers.inhibitSetCenter = true;
            }
         }
         
      }
   },
   Decentre: function()
   {
      let inhibit = false;
   
      inhibit = inhibit || ((uroMarkers.type == uroLayers.ID.UR) && (uroUtils.GetCBChecked("_cbInhibitURCentering")));
      inhibit = inhibit || ((uroMarkers.type == uroLayers.ID.MP) && (uroUtils.GetCBChecked("_cbInhibitMPCentering")));
      inhibit = inhibit || ((uroMarkers.type == uroLayers.ID.PUR) && (uroUtils.GetCBChecked("_cbInhibitPURCentering")));
      inhibit = inhibit || ((uroMarkers.type == uroLayers.ID.PPUR) && (uroUtils.GetCBChecked("_cbInhibitPPURCentering")));
      inhibit = inhibit || ((uroMarkers.type == uroLayers.ID.RPUR) && (uroUtils.GetCBChecked("_cbInhibitRPURCentering")));
   
      return inhibit;
   },
   RegisterEvents: function()
   {
      for(let i = 0; i < uroLayers.layers.length; ++i)
      {     
         if(uroLayers.layers[i].regEvt === true)
         {
            if((uroLayers.layers[i].isFeature === true) || (uroLayers.layers[i].isFeature === null))
            {
               uroLayers.layers[i].l.events.register("fe-feature-in", null, uroMarkers.MouseOver);
               uroLayers.layers[i].l.events.register("fe-feature-out", null, uroMarkers.MouseOut);
            }
            else if(uroLayers.layers[i].isFeature === false)
            {
               for(let j = 0; j < uroLayers.layers[i].mf.length; ++j)
               {
                  let mMarker = uroLayers.layers[i].mf[j];
                  if(mMarker !== null)
                  {
                     let mIcon = null;
                     
                     if(mMarker.element !== undefined)
                     {
                        if(uroLayers.layers[i].moChild === true)
                        {
                           mIcon = mMarker.element.firstChild;
                        }
                        else
                        {
                           mIcon = mMarker.element;
                        }
                     }
                     else if(mMarker.geometry !== undefined)
                     {
                        mIcon = document.querySelector('#'+mMarker.geometry.id);
                     }
                     else if(mMarker.tagName === "image")
                     {
                        mIcon = mMarker;
                     }
               
                     if((mIcon !== null) && (mIcon !== undefined))
                     {
                        mIcon.addEventListener("mouseover", uroMarkers.MouseOver2, true);
                        mIcon.addEventListener("mouseout", uroMarkers.MouseOut2, true);
                     }
                  }
               }
            }
         }
      }
   },
   AddMarkerEventHandler: function()
   {
      if
      (
         (uroMarkers.type === uroLayers.ID.UR) ||
         (uroMarkers.type === uroLayers.ID.MP) ||
         (uroMarkers.type === uroLayers.ID.PUR) ||
         (uroMarkers.type === uroLayers.ID.PPUR) ||
         (uroMarkers.type === uroLayers.ID.RPUR)
      )
      {
         let mMarker = uroGetMarker(uroMarkers.type, uroMarkers.id);
         if(mMarker !== null)
         {
            let mIcon = null;
            
            if(mMarker.element !== undefined)
            {
               mIcon = mMarker.element;
            }
            else if(mMarker.geometry !== undefined)
            {
               mIcon = document.querySelector('#'+mMarker.geometry.id);
            }
            else if(mMarker.tagName === "image")
            {
               mIcon = mMarker;
            }
      
            if((mIcon !== null) && (mIcon !== undefined))
            {
               mIcon.addEventListener("mousedown", uroMarkers.MouseDown, false);
            }
         }
      }
   }
};
const uroLayers = // layer functions
{
   // -----------------------------------------------------------------
   // NOTE CAREFULLY!
   // The contents of layers and ID MUST, MUST, MUST, remain
   // in sync at all times...
   layers :
   [
      {name: "update_requests",                 l: null, mf: null, isFeature: null, MO: null, regEvt: true,  moChild: false, getMF: true},
      {name: "mapProblems",                     l: null, mf: null, isFeature: null, MO: null, regEvt: true,  moChild: false, getMF: true},
      {name: "place_updates",                   l: null, mf: null, isFeature: null, MO: null, regEvt: true,  moChild: false, getMF: true},
      {name: "PARKING_PLACE_UPDATES",           l: null, mf: null, isFeature: null, MO: null, regEvt: true,  moChild: false, getMF: true},
      {name: "RESIDENTIAL_PLACE_UPDATES",       l: null, mf: null, isFeature: null, MO: null, regEvt: true,  moChild: false, getMF: true},
      {name: "closures",                        l: null, mf: null, isFeature: null, MO: null, regEvt: false, moChild: false, getMF: true},
      {name: "nodes",                           l: null, mf: null, isFeature: null, MO: null, regEvt: true,  moChild: false, getMF: false},
      {name: "segments",                        l: null, mf: null, isFeature: null, MO: null, regEvt: true,  moChild: false, getMF: false},
      {name: "venues",                          l: null, mf: null, isFeature: null, MO: null, regEvt: true,  moChild: false, getMF: false},
      {name: "mapComments",                     l: null, mf: null, isFeature: null, MO: null, regEvt: true,  moChild: false, getMF: false},
      {name: "speed_cameras",                   l: null, mf: null, isFeature: null, MO: null, regEvt: true,  moChild: false, getMF: false},
      {name: "closure_nodes",                   l: null, mf: null, isFeature: null, MO: null, regEvt: false, moChild: false, getMF: true},
      {name: "turn_closure",                    l: null, mf: null, isFeature: null, MO: null, regEvt: false, moChild: false, getMF: true},
      {name: "segment_suggestions_markers",     l: null, mf: null, isFeature: null, MO: null, regEvt: true,  moChild: false, getMF: true},
      {name: "edit_suggestions_markers",        l: null, mf: null, isFeature: null, MO: null, regEvt: true,  moChild: false, getMF: true},
      {name: "permanent_hazard_camera_markers", l: null, mf: null, isFeature: null, MO: null, regEvt: true,  moChild: true,  getMF: true}
   ],
   ID :
   {
      UR: 0,
      MP: 1,
      PUR: 2,
      PPUR: 3,
      RPUR: 4,
      RTC: 5,
      node: 6,
      seg: 7,
      venue: 8,
      MC: 9,
      cam: 10,
      RTCnode: 11,
      TRTCnode: 12,
      SegSug: 13,
      EditSug: 14,
      phCamera: 15
   },
   // -----------------------------------------------------------------

   BlobMouseOut: function()
   {
      let blobType = this.attributes.uroBlobType;
      if(blobType !== undefined)
      {
         let blobID = this.attributes.uroBlobID;
         uroDBG.AddLog('hover off '+blobType+' ID '+blobID);
         if(blobType == 'map_comment')
         {
            if(W.model.mapComments.objects[blobID] != undefined)
            {
               let geoID = W.model.mapComments.objects[blobID].attributes.geometry.id;
               if(geoID.indexOf('Point') != -1)
               {
                  // reapply visibility mods
                  let svgElm = document.getElementById(uroMCLayer.div.id+'_vroot');
                  for(let svgIdx = 0; svgIdx < svgElm.children.length; svgIdx++)
                  {
                     if(svgElm.children[svgIdx].id === geoID)
                     {
                        window.setTimeout(uroLayers.ReapplyPointMCVisibilityMods,10);
                     }
                  }
               }
            }
         }
      }
      else
      {
         uroDBG.AddLog('hover off unknown blob...');
      }
   },
   MCLayerChanged_changed: function()
   {
      uroLayers.MCLayerChanged();
   },
   MCLayerChanged_added: function()
   {
      uroLayers.MCLayerChanged();
   },
   MCLayerChanged_removed: function()
   {
      uroLayers.MCLayerChanged();
   },
   ReapplyPointMCVisibilityMods: function()
   {
      if(uroLayers.ApplyPointMCVisibilityMods() === false)
      {
         window.setTimeout(uroLayers.ReapplyPointMCVisibilityMods,100);
      }
   },
   ApplyPointMCVisibilityMods: function()
   {
      let retval = true;
      if(uroLayers.HasSelectedMCs() === true)
      {
         retval = false;
      }
      else
      {
         let svgElm = document.getElementById(uroMCLayer.div.id+'_vroot');
         for(let svgIdx = 0; svgIdx < svgElm.children.length; svgIdx++)
         {
            let svgChild = svgElm.children[svgIdx];
            if(svgChild.id.indexOf('Point') != -1)
            {
               if(uroUtils.GetCBChecked('_cbMCEnhancePointMCVisibility') === true)
               {
                  if(svgChild.getAttribute('r') == 6)
                  {
                     svgChild.setAttribute('fill','#ffff00');
                     svgChild.setAttribute('fill-opacity',0.75);
                     svgChild.setAttribute('r',12);
                     svgChild.setAttribute('touchedByURO',true);
                  }
                  else if((svgChild.getAttribute('touchedByURO') === "true")&&(svgChild.getAttribute('fill') === '#ffff00'))
                  {
                     // do nothing...
                  }
                  else
                  {
                     retval = false;
                     break;
                  }
               }
               else
               {
                  if((svgChild.getAttribute('touchedByURO') === "true")&&(svgChild.getAttribute('fill') === '#ffff00'))
                  {
                     svgChild.setAttribute('fill','#ffffff');
                     svgChild.setAttribute('fill-opacity',1);
                     svgChild.setAttribute('r',6);
                     svgChild.setAttribute('touchedByURO',false);
                  }
               }
            }
         }
      }
      return retval;
   },
   HasSelectedMCs: function()
   {
      let retval = false;
      for(let mcObj in W.model.mapComments.objects)
      {
         if(W.model.mapComments.objects[mcObj].isSelected() === true)
         {
            retval = true;
            break;
         }
      }
      return retval;
   },
   MCLayerChanged: function()
   {   
      uroInit.WazeBits();
      if(uroMCLayer != null)
      {
         if(uroLayers.HasSelectedMCs() === false)
         {
            uroDBG.AddLog('adding MC blob event handlers');
            let mcModel = null;
            for(let mObj=0; mObj<uroMCLayer.features.length; mObj++)
            {
               if(uroMCLayer.features[mObj]?.attributes?.wazeFeature?._wmeObject !== undefined)
               {
                  mcModel = uroMCLayer.features[mObj].attributes.wazeFeature._wmeObject;
                  {
                     if(mcModel.selected !== true)
                     {
                        let mcBlobID = mcModel.attributes.geometry.id;
                        let mcID = mcModel.attributes.id;
                        let mcBlob = document.getElementById(mcBlobID);
                        if(mcBlob !== null)
                        {
                           mcBlob.addEventListener("mouseout", uroLayers.blobMouseOut, false);
                           mcBlob.attributes.uroBlobID = mcID;
                           mcBlob.attributes.uroBlobType = "map_comment";
                           uroDBG.AddLog('added handlers to MC '+mcID);
                        }
                     }
                  }
               }
            }
            uroLayers.ApplyPointMCVisibilityMods();
         }
         else
         {
            uroDBG.AddLog('MC selected, handlers not added yet...');
         }
         
         uroFilterMapComments();
      }
   },
   VenueLayerChanged: function()
   {
      uroDBG.AddLog('adding place blob event handlers');
      for(let mObj=0; mObj<uroVenueLayer.features.length; mObj++)
      {
         // clicking on an area place now adds the polygon drag handles into the features[] array, so we need to test that
         // the current array entry isn't referring to one of these handles before trying to access the attributes...
         if(uroVenueLayer.features[mObj]?.attributes?.wazeFeature?._wmeObject !== undefined)
         {
            let mcBlobID = uroVenueLayer.features[mObj].attributes.wazeFeature._wmeObject.attributes.geometry.id;
            let mcID = uroVenueLayer.features[mObj].attributes.wazeFeature._wmeObject.attributes.id;
            let mcBlob = document.getElementById(mcBlobID);
            if(mcBlob !== null)
            {
               mcBlob.addEventListener("mouseout", uroLayers.blobMouseOut, false);
               mcBlob.attributes.uroBlobID = mcID;
               mcBlob.attributes.uroBlobType = "place";
            }
         }
      }
   },
   Observe_VenueLayer: function()
   {
      uroLayers.layers[uroLayers.ID.venue].MO.observe(uroVenueLayer.div,{childList: true, attributes : true, characterData : true, subtree: true});
   },
   Observe_URLayer: function()
   {
      // As URs are now displayed as SVG image elements rather than HTML elements, and as WME likes to re-render them seemingly
      // at random after they've been initially displayed, we hang the mutation observer off of the vectorRoot element, as this
      // is the parent SVG element for the markers, and the MO therefore seems to trigger reliably on each change to that level
      // of the SVG, including these random re-renders.  It's obvious if these re-renders aren't being captured correctly, as it
      // causes the comment count markers to randomly show up behind UR markers instead of always being in front of them...
      uroLayers.layers[uroLayers.ID.UR].MO.observe(uroLayers.layers[uroLayers.ID.UR].l.renderer.vectorRoot,{childList: true, attributes : true, characterData : true, subtree: true});
   },
   URLayerChanged: function()
   {
      uroDBG.AddLog('UR layer change detected');
      uroLayers.layers[uroLayers.ID.UR].MO.disconnect();
      uroFilterURs();
      uroLayers.Observe_URLayer();
   },
   PURLayerChanged: function()
   {
      uroDBG.AddLog('PUR layer change detected');
      uroLayers.layers[uroLayers.ID.PUR].MO.disconnect();
      uroFilterProblems();
      uroLayers.Observe_PURLayer();
   },
   Observe_PURLayer: function()
   {
      uroLayers.layers[uroLayers.ID.PUR].MO.observe(uroLayers.layers[uroLayers.ID.PUR].l.div,{childList: true, attributes : true, characterData : true, subtree: true});
   },
   PPURLayerChanged: function()
   {
      uroDBG.AddLog('PPUR layer change detected');
      uroLayers.layers[uroLayers.ID.PPUR].MO.disconnect();
      uroFilterProblems();
      uroLayers.Observe_PPURLayer();
   },
   Observe_PPURLayer: function()
   {
      uroLayers.layers[uroLayers.ID.PPUR].MO.observe(uroLayers.layers[uroLayers.ID.PPUR].l.div,{childList: true, attributes : true, characterData : true, subtree: true});
   },
   RPURLayerChanged: function()
   {
      uroDBG.AddLog('RPUR layer change detected');
      uroLayers.layers[uroLayers.ID.RPUR].MO.disconnect();
      uroFilterProblems();
      uroLayers.Observe_RPURLayer();
   },
   Observe_RPURLayer: function()
   {
      uroLayers.layers[uroLayers.ID.RPUR].MO.observe(uroLayers.layers[uroLayers.ID.RPUR].l.div,{childList: true, attributes : true, characterData : true, subtree: true});
   },

   RTCLayerChanged: function()
   {
      uroDBG.AddLog('reapplying closures filter');
      uroLayers.layers[uroLayers.ID.RTC].MO.disconnect();
      uroFilterRTCs();
      uroLayers.Observe_RTCLayer();
   },
   Observe_RTCLayer: function()
   {
      uroLayers.layers[uroLayers.ID.RTC].MO.observe(uroLayers.layers[uroLayers.ID.RTC].l.div,{childList: true, attributes : true, characterData : true, subtree: true});
   },
   RunChangeHandlers: function()
   {
      uroLayers.URLayerChanged();
      uroLayers.PURLayerChanged();
      uroLayers.PPURLayerChanged();
      uroLayers.RPURLayerChanged();
      uroLayers.VenueLayerChanged();
      uroLayers.RTCLayerChanged();

      uroLayers.MCLayerChanged();
   },
   InitialiseMOs: function()
   {
      uroLayers.layers[uroLayers.ID.UR].MO = new MutationObserver(uroLayers.URLayerChanged);
      uroLayers.layers[uroLayers.ID.PUR].MO = new MutationObserver(uroLayers.PURLayerChanged);
      uroLayers.layers[uroLayers.ID.PPUR].MO = new MutationObserver(uroLayers.PPURLayerChanged);
      uroLayers.layers[uroLayers.ID.RPUR].MO = new MutationObserver(uroLayers.RPURLayerChanged);
      uroLayers.layers[uroLayers.ID.venue].MO = new MutationObserver(uroLayers.VenueLayerChanged);
      uroLayers.layers[uroLayers.ID.RTC].MO = new MutationObserver(uroLayers.RTCLayerChanged);
      
      uroLayers.Observe_URLayer();
      uroLayers.Observe_PURLayer();
      uroLayers.Observe_PPURLayer();
      uroLayers.Observe_RPURLayer();
      uroLayers.Observe_VenueLayer();
      uroLayers.Observe_RTCLayer();
   },
   GetMarkersOrFeatures: function(layerID)
   {
      let retval = null;
   
      let findit = uroLayers.layers[layerID].l.features;
      if(findit !== undefined)
      {
         retval = findit;
         uroLayers.layers[layerID].isFeature = true;
         uroDBG.AddLog(uroLayers.layers[layerID].name + ' = features');
      }
      else
      {
         findit = uroLayers.layers[layerID].l.markers;
         if(findit !== undefined)
         {
            retval = findit;
            uroLayers.layers[layerID].isFeature = false;
            uroDBG.AddLog(uroLayers.layers[layerID].name + ' = markers');
         }
      }
   
      if(retval === null)
      {
         uroLayers.layers[layerID].isFeature = null;
         uroDBG.AddLog(uroLayers.layers[layerID].name + ' = unknown :-/');
      }
   
      return retval;
   },
   Init: function()
   {
      for(let i = 0; i < uroLayers.layers.length; ++i)
      {
         uroLayers.layers[i].l = W.map.getLayerByUniqueName(uroLayers.layers[i].name);
         if(uroLayers.layers[i].getMF === true)
         {
            uroLayers.layers[i].mf = uroLayers.GetMarkersOrFeatures(i);
         }
      }
   }
};
const uroAlertBox =  // alert box handling
{
   stack: [],
   tickAction: null,
   crossAction: null,
   inUse: false,
   
   ABObj: function(headericon, title, content, hasCross, tickText, crossText, tickAction, crossAction)
   {
      this.headericon = headericon;
      this.title = title;
      this.content = content;
      this.hasCross = hasCross;
      this.tickText = tickText;
      this.crossText = crossText;
      this.tickAction = tickAction;
      this.crossAction = crossAction;
   },
   Close: function()
   {
      document.getElementById('uroAlerts').childNodes[0].innerHTML = uroUtils.ModifyHTML('');
      document.getElementById('uroAlerts').childNodes[1].innerHTML = uroUtils.ModifyHTML('');
      document.getElementById('uroAlertTickBtnCaption').innerHTML = uroUtils.ModifyHTML('');
      document.getElementById('uroAlertCrossBtnCaption').innerHTML = uroUtils.ModifyHTML('');
      uroAlertBox.tickAction = null;
      uroAlertBox.crossAction = null;
      document.getElementById('uroAlerts').style.visibility = "hidden";
      document.getElementById('uroAlertCrossBtn').style.visibility = "hidden";
      uroAlertBox.inUse = false;
      if(uroAlertBox.stack.length > 0)
      {
         uroAlertBox.BuildFromStack();
      }
   },
   CloseWithTick: function()
   {
      if(typeof uroAlertBox.tickAction === 'function')
      {
         uroAlertBox.tickAction();
      }
      uroAlertBox.Close();
   },
   CloseWithCross: function()
   {
      if(typeof uroAlertBox.crossAction === 'function')
      {
         uroAlertBox.crossAction();
      }
      uroAlertBox.Close();
   },
   Show: function(headericon, title, content, hasCross, tickText, crossText, tickAction, crossAction)
   {
      uroAlertBox.stack.push(new uroAlertBox.ABObj(headericon, title, content, hasCross, tickText, crossText, tickAction, crossAction));
      if(uroAlertBox.inUse === false)
      {
         uroAlertBox.BuildFromStack();
      }
   },
   BuildFromStack: function()
   {
      uroAlertBox.inUse = true;
      uroAlertBox.tickAction = null;
      uroAlertBox.crossAction = null;
      let titleContent = '<span style="font-size:14px;padding:2px;">';
      titleContent += '<i class="fa '+uroAlertBox.stack[0].headericon+'"> </i>&nbsp;';
      titleContent += uroAlertBox.stack[0].title;
      titleContent += '</span>';
      document.getElementById('uroAlerts').childNodes[0].innerHTML = uroUtils.ModifyHTML(titleContent);
      document.getElementById('uroAlerts').childNodes[1].innerHTML = uroUtils.ModifyHTML(uroAlertBox.stack[0].content);
      document.getElementById('uroAlertTickBtnCaption').innerHTML = uroUtils.ModifyHTML(uroAlertBox.stack[0].tickText);
      if(uroAlertBox.stack[0].hasCross)
      {
         document.getElementById('uroAlertCrossBtnCaption').innerHTML = uroUtils.ModifyHTML(uroAlertBox.stack[0].crossText);
         document.getElementById('uroAlertCrossBtn').style.visibility = "visible";
         if(typeof uroAlertBox.stack[0].crossAction === "function")
         {
            uroAlertBox.crossAction = uroAlertBox.stack[0].crossAction;
         }
      }
      else
      {
         document.getElementById('uroAlertCrossBtn').style.visibility = "hidden";
      }
      if(typeof uroAlertBox.stack[0].tickAction === "function")
      {
         uroAlertBox.tickAction = uroAlertBox.stack[0].tickAction;
      }
      document.getElementById('uroAlerts').style.visibility = "";
      uroAlertBox.stack.shift();
   }   
};
const uroStartup =   // startup messaging to users
{
   ShowUpgradeNotes: function()
   {
      uroDBG.AddLog('let users know what\'s new in this release');

      let releaseNotes = '';
      releaseNotes += '<p>Thanks for installing URO+ '+uroRelease.version+' ('+uroRelease.date+')</p>';

      let loop;
      if(uroRelease.changes.length > 0)
      {
         releaseNotes += '<br>Changes since the last release:<br>';
         releaseNotes += '<ul>';
         for(loop=0; loop < uroRelease.changes.length; loop++)
         {
            releaseNotes += '<li>'+uroRelease.changes[loop];
         }
         releaseNotes += '</ul>';
      }

      uroAlertBox.Show('fa-info-circle', 'URO+ Release Notes', releaseNotes, false, "OK", "", null, null);
   }
};
const uroConfig = // configuration handling
{
   GatherSettings: function(container)
   {
      let options = '';
      if(typeof(container) == 'string')
      {
         container = document.getElementById(container);
      }
      let urOptions = container.getElementsByTagName('input');
      for (let optIdx=0;optIdx<urOptions.length;optIdx++)
      {
         // Don't save settings for any of the legacy input elements we've now hidden...
         if(urOptions[optIdx].style.display != "none")
         {
            let id = urOptions[optIdx].id;
            if((id.indexOf('_cb') === 0)||(id.indexOf('_text') === 0)||(id.indexOf('_input') === 0))
            {
               options += ':' + id;
               if(urOptions[optIdx].type == 'checkbox') options += ',' + urOptions[optIdx].checked.toString();
               else if((urOptions[optIdx].type == 'text')||(urOptions[optIdx].type == 'number')) options += ',' + urOptions[optIdx].value.toString();
            }
         }
      }
      return options;
   },
   GatherCamWatchList: function()
   {
      let liststr = '';
      for(let loop=0;loop<uroOWL.CamWatchObjects.length;loop++)
      {
         let camObj = uroOWL.CamWatchObjects[loop];
         if((camObj.fid != null) && (camObj.persistent === true))
         {
            if(loop > 0) liststr += ':';

            liststr += camObj.fid+',';
            liststr += camObj.watch.lon+',';
            liststr += camObj.watch.lat+',';
            liststr += camObj.watch.type+',';
            liststr += camObj.watch.azymuth+',';
            liststr += camObj.watch.speed+',';
            liststr += camObj.groupID+',';
            liststr += camObj.server;
         }
      }
      return liststr;
   },
   GatherCWLGroups: function()
   {
      let liststr = '';
      for(let loop=0;loop<uroOWL.CWLGroups.length;loop++)
      {
         let groupObj = uroOWL.CWLGroups[loop];
         if(groupObj.groupID != -1)
         {
            if(loop > 0) liststr += ':';

            liststr += groupObj.groupID+',';
            liststr += groupObj.groupName+',';
            liststr += groupObj.groupCollapsed;
         }
      }
      return liststr;
   },
   GatherPlacesGroups: function()
   {
      let liststr = '';
      for(let loop=0;loop<uroPlacesGroupsCollapsed.length;loop++)
      {
         if(loop > 0) liststr += ':';
         liststr += uroPlacesGroupsCollapsed[loop];
      }
      return liststr;
   },
   GatherAFNs: function()
   {
      let liststr = '';
      for(let loop=0; loop < uroAFN.friendlyNames.length; loop++)
      {
         let fnObj = uroAFN.friendlyNames[loop];
         if(loop > 0) liststr += ':';

         liststr += fnObj.fName+',';
         liststr += fnObj.area+',';
         liststr += fnObj.server;
      }
      return liststr;
   },
   SaveSettings: function()
   {
      if((uroInhibitSave) || (uroMTEMode === true) || (uroSettingsApplied === false))
      {
         uroDBG.AddLog('save inhibited');
         return;
      }

      if (localStorage)
      {
         try
         {
            for(let i = 0; i < uroTabs.CtrlTabs.length; ++i)
            {
               localStorage[uroTabs.CtrlTabs[i][uroTabs.FIELDS.STORAGE]] = uroConfig.GatherSettings(uroTabs.CtrlTabs[i][uroTabs.FIELDS.TABBODY]);
            }

            localStorage.UROverviewCamWatchList = uroConfig.GatherCamWatchList();
            localStorage.UROverviewCWLGroups = uroConfig.GatherCWLGroups();
            localStorage.UROverviewFriendlyAreaNames = uroConfig.GatherAFNs();
            localStorage.UROverviewPlacesGroups = uroConfig.GatherPlacesGroups();

            localStorage.UROverviewMasterEnable = uroUtils.GetCBChecked('_cbMasterEnable');
            localStorage.UROverviewCurrentVersion = uroRelease.version;

            uroDBG.AddLog('save complete');
         }
         catch(err)
         {
            uroDBG.AddLog('exception thrown during save - probably script reload whilst in MTE mode...');
         }
      }
      else
      {
         uroDBG.AddLog('no localStorage, save blocked');
      }
   },
   ApplySettings: function(settings)
   {
      uroSettingsApplied = true;
      if(settings != undefined)
      {
         if(document.querySelector('#_cbMasterEnable') === null)
         {
            uroSettingsApplied = false;
         }
         else
         {
            let options = settings.split(':');
            for(let optIdx=0;optIdx<options.length;optIdx++)
            {
               let fields = options[optIdx].split(',');
               if(fields[0].indexOf('_cb') === 0)
               {
                  if(document.getElementById(fields[0]) !== null)
                  {
                     uroUtils.SetCBChecked(fields[0], (fields[1] == 'true'));
                  }
               }
               else if((fields[0].indexOf('_input') === 0)||(fields[0].indexOf('_text') === 0))
               {
                  if(document.getElementById(fields[0]) !== null) document.getElementById(fields[0]).value = fields[1];
               }
            }
         }
      }
   },
   ApplyCamWatchList: function()
   {
      let objects = localStorage.UROverviewCamWatchList.split(':');
      uroOWL.CamWatchObjects = [];
      if(objects.length > 0)
      {
         for(let objIdx=0;objIdx<objects.length;objIdx++)
         {
            let fields = objects[objIdx].split(',');
            if(fields.length == 9)
            {
               // CWL entries with 9 fields include the validated property which is now redundant, so we need to strip this property before adding
               // the camera to the object collection.  Whilst WME no longer displays unapproved cameras, it's preferable at this stage to leave
               // any watched unapproved cameras in the object collection, just in case any of them were approved (and will therefore still be
               // present in WME) inbetween the last time the user ran URO and now.  For those unapproved cameras which were still unapproved when
               // removed from WME, URO will then list them as deleted and the user can then perform a single manual tidy-up of their watchlist to
               // remove them there as well.
               uroOWL.CamWatchObjects.push(new uroOWL.CamWatchObj(true,fields[0],fields[1],fields[2],fields[3],fields[4],fields[5],fields[7],fields[8]));
            }
            else if(fields.length == 8)
            {
               uroOWL.CamWatchObjects.push(new uroOWL.CamWatchObj(true,fields[0],fields[1],fields[2],fields[3],fields[4],fields[5],fields[6],fields[7]));
            }
         }
      }
   },
   ApplyCWLGroups: function()
   {
      let objects = localStorage.UROverviewCWLGroups.split(':');
      uroOWL.CWLGroups = [];

      if(objects.length === 0)
      {
         uroOWL.CWLGroups.push(new uroOWL.GroupObj(0,'No group',false));
      }
      else
      {
         for(let objIdx=0;objIdx<objects.length;objIdx++)
         {
            let fields = objects[objIdx].split(',');
            if(fields.length < 2)
            {
               fields.push(false);
            }
            uroOWL.CWLGroups.push(new uroOWL.GroupObj(fields[0],fields[1],(fields[2] == 'true')));
         }
      }
   },
   TranslateLegacyMPTab: function()
   {
      let options = localStorage.UROverviewMPOptions.split(':');
      for(let optIdx=0;optIdx<options.length;optIdx++)
      {
         let fields = options[optIdx].split(',');
         if(fields[0].indexOf('_cb') === 0)
         {
            if(fields[0] == '_cbMPFilterParkingLotInputAsPoint') uroUtils.SetCBChecked('_cbMPFilter_T50', (fields[1] == 'true'));
            if(fields[0] == '_cbMPMissingPLP_T70') uroUtils.SetCBChecked('_cbMPFilter_T70', (fields[1] == 'true'));
            if(fields[0] == '_cbMPMissingPLP_T71') uroUtils.SetCBChecked('_cbMPFilter_T71', (fields[1] == 'true'));
            if(fields[0] == '_cbMPFilterDrivingDirectionMismatch') uroUtils.SetCBChecked('_cbMPFilter_T101', (fields[1] == 'true'));
            if(fields[0] == '_cbMPFilterMissingJunction') uroUtils.SetCBChecked('_cbMPFilter_T102', (fields[1] == 'true'));
            if(fields[0] == '_cbMPFilterMissingRoad') uroUtils.SetCBChecked('_cbMPFilter_T103', (fields[1] == 'true'));
            if(fields[0] == '_cbMPFilterCrossroadsJunctionMissing') uroUtils.SetCBChecked('_cbMPFilter_T104', (fields[1] == 'true'));
            if(fields[0] == '_cbMPFilterRoadTypeMismatch') uroUtils.SetCBChecked('_cbMPFilter_T105', (fields[1] == 'true'));
            if(fields[0] == '_cbMPFilterRestrictedTurn') uroUtils.SetCBChecked('_cbMPFilter_T106', (fields[1] == 'true'));
            if(fields[0] == '_cbMPFilterTurnProblem') uroUtils.SetCBChecked('_cbMPFilter_T200', (fields[1] == 'true'));
            if(fields[0] == '_cbMPFilterRoadClosureProblem') uroUtils.SetCBChecked('_cbMPFilter_T300', (fields[1] == 'true'));
         }
      }
   },
   TranslateLegacyZoom: function()
   {
      let tZoom = parseInt(document.getElementById("_inputFilterMinZoomLevel").value);
      if(tZoom < 12)
      {
         tZoom += 12;
         document.getElementById("_inputFilterMinZoomLevel").value = tZoom;
      }
      tZoom = parseInt(document.getElementById("_inputUnstackZoomLevel").value);
      if(tZoom < 12)
      {
         tZoom += 12;
         document.getElementById("_inputUnstackZoomLevel").value = tZoom;
      }
   },
   LoadSettings: function()
   {
      let isNewInstall = true;
      let isUpgradeInstall = true;

      uroDBG.AddLog('loadSettings()');
      
      for(let i = 0; i < uroTabs.CtrlTabs.length; ++i)
      {
         if (uroTabs.CtrlTabs[i][uroTabs.FIELDS.STORAGE] != null)
         {
            uroDBG.AddLog('recover '+uroTabs.CtrlTabs[i][uroTabs.FIELDS.TABTITLE]+' tab settings');
            uroConfig.ApplySettings(localStorage[uroTabs.CtrlTabs[i][uroTabs.FIELDS.STORAGE]]);
            isNewInstall = false;
         }
      }
      
      if (localStorage.UROverviewMPOptions != null)
      {
         uroConfig.TranslateLegacyMPTab();
      }   
      if (localStorage.UROverviewMiscOptions != null)
      {
         uroConfig.TranslateLegacyZoom();
      }   

      if(localStorage.UROverviewCWLGroups != null)
      {
         uroDBG.AddLog('recover CWL groups');
         uroConfig.ApplyCWLGroups();
         isNewInstall = false;
      }
      else
      {
         uroDBG.AddLog('set default CWL group');
         uroOWL.CWLGroups.push(new uroOWL.GroupObj(0,'No group',false));
      }

      if(localStorage.UROverviewCamWatchList != null)
      {
         uroDBG.AddLog('recover camera watchlist');
         uroConfig.ApplyCamWatchList();
         uroOWL.GetCurrentCamWatchListObjects();
         isNewInstall = false;
      }
   /*
      if(localStorage.UROverviewSegWatchList != null)
      {
         uroDBG.AddLog('recover segment watchlist');
         uroApplySegWatchList();
         uroGetCurrentSegWatchListObjects();
         isNewInstall = false;
      }

      if(localStorage.UROverviewPlaceWatchList != null)
      {
         uroDBG.AddLog('recover places watchlist');
         uroApplyPlaceWatchList();
         //uroGetCurrentPlaceWatchListObjects();
         isNewInstall = false;
      }

      if(localStorage.UROverviewPlacesGroups != null)
      {
         uroDBG.AddLog('recover places groups');
         uroApplyPlacesGroups();
         isNewInstall = false;
      }
   */
      if(localStorage.UROverviewCurrentVersion != null)
      {
         uroDBG.AddLog('comparing install versions');
         if(localStorage.UROverviewCurrentVersion == uroRelease.version)
         {
            isUpgradeInstall = false;
         }
      }

      if(localStorage.UROverviewFriendlyAreaNames != null)
      {
         uroDBG.AddLog('recover friendly area names');
         uroAFN.ApplyNames();
         isNewInstall = false;
      }

      if(localStorage.UROverviewMasterEnable != null)
      {
         uroDBG.AddLog('recover master enable state');
         document.getElementById('_cbMasterEnable').checked = (localStorage.UROverviewMasterEnable == "true");
         uroDBG.AddLog('enable checkbox state set...');
      }

      if((isNewInstall)||(isUpgradeInstall))
      {
         uroStartup.ShowUpgradeNotes();
      }

      uroInhibitSave = false;
   },
   DefaultSettings: function()
   {
      uroAlertBox.Show("fa-warning", "URO+ Warning", "Resetting URO+ settings <b>cannot</b> be undone.<br>Are you <i>sure</i> you want to do this?", true, "Reset settings", "Keep settings", uroConfig.DefaultSettingsAction, null);
   },
   DefaultSettingsAction: function()
   {
      let defaultSettings = '';

      defaultSettings += '[UROverviewUROptions][len=1849]:_cbURFilterOutsideArea,false:_cbURFilterInsideManagedAreas,false:_cbURExcludeUserArea,false:_cbNoFilterForURInURL,false:_cbURFilterDupes,false:_cbFilterWazeAuto,false:_cbFilterIncorrectTurn,false:_cbFilterIncorrectAddress,false:_cbFilterIncorrectRoute,false:_cbFilterMissingRoundabout,false:_cbFilterGeneralError,false:_cbFilterTurnNotAllowed,false:_cbFilterIncorrectJunction,false:_cbFilterMissingBridgeOverpass,false:_cbFilterWrongDrivingDirection,false:_cbFilterMissingExit,false:_cbFilterMissingRoad,false:_cbFilterBlockedRoad,false:_cbFilterMissingLandmark,false:_cbFilterSpeedLimits,false:_cbFilterUndefined,false:_cbFilterRoadworks,false:_cbFilterConstruction,false:_cbFilterClosure,false:_cbFilterEvent,false:_cbFilterNote,false:_cbFilterBOG,false:_cbFilterDifficult,false:_cbFilterWSLM,false:_cbInvertURFilter,false:_cbFilterOpenUR,false:_cbFilterClosedUR,false:_cbFilterSolved,false:_cbFilterUnidentified,false:_cbEnableMinAgeFilter,false:_inputFilterMinDays,60:_cbEnableMaxAgeFilter,false:_inputFilterMaxDays,62:_cbHideMyFollowed,false:_cbHideMyUnfollowed,false:_cbURDescriptionMustBePresent,false:_cbURDescriptionMustBeAbsent,false:_cbEnableKeywordMustBePresent,false:_textKeywordPresent,:_cbEnableKeywordMustBeAbsent,false:_textKeywordAbsent,:_cbCaseInsensitive,false:_cbHideMyComments,false:_cbHideAnyComments,false:_cbHideIfLastCommenter,false:_cbHideIfNotLastCommenter,false:_cbHideIfReporterLastCommenter,false:_cbHideIfReporterNotLastCommenter,false:_cbEnableMinCommentsFilter,false:_inputFilterMinComments,1:_cbEnableMaxCommentsFilter,false:_inputFilterMaxComments,0:_cbEnableCommentAgeFilter2,false:_inputFilterCommentDays2,:_cbEnableCommentAgeFilter,false:_inputFilterCommentDays,1:_cbIgnoreOtherEditorComments,false:_cbURUserIDFilter,false:_cbURResolverIDFilter,false:_cbInvertURStateFilter,false:_cbNoFilterForTaggedURs,false[END]';
      defaultSettings += '[UROverviewMiscOptions][len=1157]:_cbHideSegmentsWhenRoadsHidden,false:_cbKillInertialPanning,false:_cbCommentCount,false:_cbAutoApplyClonedClosure,false:_cbAutoScrollClosureList,false:_inputFilterMinZoomLevel,22:_inputUnstackSensitivity,30:_inputUnstackZoomLevel,22:_inputPopupEntryTimeout,10:_inputPopupExitTimeout,2:_inputPopupAutoHideTimeout,0:_cbInhibitURClusters,false:_cbInhibitMPClusters,false:_cbInhibitPUClusters,false:_cbInhibitURPopup,false:_cbInhibitMPPopup,false:_cbInhibitCamPopup,false:_cbInhibitSegPopup,false:_cbInhibitSegGenericPopup,false:_cbInhibitLandmarkPopup,false:_cbInhibitPUPopup,false:_cbInhibitMapCommentPopup,false:_cbInhibitNodesPopup,false:_cbDateFmtDDMMYY,true:_cbDateFmtMMDDYY,false:_cbDateFmtYYMMDD,false:_cbTimeFmt24H,true:_cbTimeFmt12H,false:_cbWhiteBackground,false:_inputCustomBackgroundRed,30:_inputCustomBackgroundGreen,30:_inputCustomBackgroundBlue,30:_cbInhibitNURButton,false:_cbInhibitNMPButton,false:_cbInhibitNPURButton,false:_cbInhibitURCentering,false:_cbInhibitMPCentering,false:_cbInhibitPURCentering,false:_cbInhibitPPURCentering,false:_cbInhibitRPURCentering,false:_cbHideAMLayer,false:_cbMoveAMList,false:_cbDisablePlacesFiltering,false[END]';
      defaultSettings += '[UROverviewPlacesOptions][len=6292]:_cbFilterUneditablePlaceUpdates,false:_cbPURFilterInsideManagedAreas,false:_cbPURExcludeUserArea,false:_cbFilterLockRankedPlaceUpdates,false:_cbFilterNewPlacePUR,false:_cbFilterUpdatedDetailsPUR,false:_cbPURFilterCFPhone,false:_cbPURFilterCFName,false:_cbPURFilterCFEntryExitPoints,false:_cbPURFilterCFOpeningHours,false:_cbPURFilterCFAliases,false:_cbPURFilterCFServices,false:_cbPURFilterCFGeometry,false:_cbPURFilterCFHouseNumber,false:_cbPURFilterCFCategories,false:_cbPURFilterCFDescription,false:_cbFilterNewPhotoPUR,false:_cbFilterFlaggedPUR,false:_cbInvertPURFilters,false:_cbEnablePURMinAgeFilter,false:_inputPURFilterMinDays,3:_cbEnablePURMaxAgeFilter,false:_inputPURFilterMaxDays,4:_cbPlaceFilterEditedLessThan,false:_inputFilterPlaceEditMinDays,:_cbPlaceFilterEditedMoreThan,false:_inputFilterPlaceEditMaxDays,:_cbHidePlacesL0,false:_cbHidePlacesL1,false:_cbHidePlacesL2,false:_cbHidePlacesL3,false:_cbHidePlacesL4,false:_cbHidePlacesL5,false:_cbHidePlacesStaff,false:_cbHidePlacesAdLocked,false:_cbHideAreaPlaces,false:_cbHidePointPlaces,false:_cbHidePhotoPlaces,false:_cbHideNoPhotoPlaces,false:_cbHideLinkedPlaces,false:_cbHideNoLinkedPlaces,false:_cbHideDescribedPlaces,false:_cbHideNonDescribedPlaces,false:_cbHideKeywordPlaces,false:_cbHideNoKeywordPlaces,false:_textKeywordPlace,:_cbShowOnlyPlacesCreatedBy,false:_cbShowOnlyPlacesEditedBy,false:_textPlacesEditor,theMadcabbie:_cbHideOnlyPlacesCreatedBy,false:_cbHideOnlyPlacesEditedBy,false:_textHidePlacesEditor,theMadcabbie:_cbLeavePURGeos,false:_cbHidePURsForFilteredPlaces,false:_cbPlacesFilter-CAR_SERVICES,false:_cbPlacesFilter-CAR_WASH,false:_cbPlacesFilter-CHARGING_STATION,false:_cbPlacesFilter-GARAGE_AUTOMOTIVE_SHOP,false:_cbPlacesFilter-GAS_STATION,false:_cbPlacesFilter-CRISIS_LOCATIONS,false:_cbPlacesFilter-DONATION_CENTERS,false:_cbPlacesFilter-OTHER_CRISIS_LOCATIONS,false:_cbPlacesFilter-SHELTER_LOCATIONS,false:_cbPlacesFilter-CULTURE_AND_ENTERTAINEMENT,false:_cbPlacesFilter-ART_GALLERY,false:_cbPlacesFilter-CASINO,false:_cbPlacesFilter-CLUB,false:_cbPlacesFilter-TOURIST_ATTRACTION_HISTORIC_SITE,false:_cbPlacesFilter-MOVIE_THEATER,false:_cbPlacesFilter-MUSEUM,false:_cbPlacesFilter-MUSIC_VENUE,false:_cbPlacesFilter-PERFORMING_ARTS_VENUE,false:_cbPlacesFilter-GAME_CLUB,false:_cbPlacesFilter-STADIUM_ARENA,false:_cbPlacesFilter-THEME_PARK,false:_cbPlacesFilter-ZOO_AQUARIUM,false:_cbPlacesFilter-RACING_TRACK,false:_cbPlacesFilter-THEATER,false:_cbPlacesFilter-FOOD_AND_DRINK,false:_cbPlacesFilter-RESTAURANT,false:_cbPlacesFilter-BAKERY,false:_cbPlacesFilter-DESSERT,false:_cbPlacesFilter-CAFE,false:_cbPlacesFilter-FAST_FOOD,false:_cbPlacesFilter-FOOD_COURT,false:_cbPlacesFilter-BAR,false:_cbPlacesFilter-ICE_CREAM,false:_cbPlacesFilter-LODGING,false:_cbPlacesFilter-HOTEL,false:_cbPlacesFilter-HOSTEL,false:_cbPlacesFilter-CAMPING_TRAILER_PARK,false:_cbPlacesFilter-COTTAGE_CABIN,false:_cbPlacesFilter-BED_AND_BREAKFAST,false:_cbPlacesFilter-NATURAL_FEATURES,false:_cbPlacesFilter-ISLAND,false:_cbPlacesFilter-SEA_LAKE_POOL,false:_cbPlacesFilter-RIVER_STREAM,false:_cbPlacesFilter-FOREST_GROVE,false:_cbPlacesFilter-FARM,false:_cbPlacesFilter-CANAL,false:_cbPlacesFilter-SWAMP_MARSH,false:_cbPlacesFilter-DAM,false:_cbPlacesFilter-OTHER,false:_cbPlacesFilter-CONSTRUCTION_SITE,false:_cbPlacesFilter-OUTDOORS,false:_cbPlacesFilter-PARK,false:_cbPlacesFilter-PLAYGROUND,false:_cbPlacesFilter-BEACH,false:_cbPlacesFilter-SPORTS_COURT,false:_cbPlacesFilter-GOLF_COURSE,false:_cbPlacesFilter-PLAZA,false:_cbPlacesFilter-PROMENADE,false:_cbPlacesFilter-POOL,false:_cbPlacesFilter-SCENIC_LOOKOUT_VIEWPOINT,false:_cbPlacesFilter-SKI_AREA,false:_cbPlacesFilter-PARKING_LOT,false:_cbPlacesFilter-PROFESSIONAL_AND_PUBLIC,false:_cbPlacesFilter-COLLEGE_UNIVERSITY,false:_cbPlacesFilter-SCHOOL,false:_cbPlacesFilter-CONVENTIONS_EVENT_CENTER,false:_cbPlacesFilter-GOVERNMENT,false:_cbPlacesFilter-LIBRARY,false:_cbPlacesFilter-CITY_HALL,false:_cbPlacesFilter-ORGANIZATION_OR_ASSOCIATION,false:_cbPlacesFilter-PRISON_CORRECTIONAL_FACILITY,false:_cbPlacesFilter-COURTHOUSE,false:_cbPlacesFilter-CEMETERY,false:_cbPlacesFilter-FIRE_DEPARTMENT,false:_cbPlacesFilter-POLICE_STATION,false:_cbPlacesFilter-MILITARY,false:_cbPlacesFilter-HOSPITAL_URGENT_CARE,false:_cbPlacesFilter-DOCTOR_CLINIC,false:_cbPlacesFilter-OFFICES,false:_cbPlacesFilter-POST_OFFICE,false:_cbPlacesFilter-RELIGIOUS_CENTER,false:_cbPlacesFilter-KINDERGARDEN,false:_cbPlacesFilter-FACTORY_INDUSTRIAL,false:_cbPlacesFilter-EMBASSY_CONSULATE,false:_cbPlacesFilter-INFORMATION_POINT,false:_cbPlacesFilter-EMERGENCY_SHELTER,false:_cbPlacesFilter-TRASH_AND_RECYCLING_FACILITIES,false:_cbPlacesFilter-SHOPPING_AND_SERVICES,false:_cbPlacesFilter-ARTS_AND_CRAFTS,false:_cbPlacesFilter-BANK_FINANCIAL,false:_cbPlacesFilter-SPORTING_GOODS,false:_cbPlacesFilter-BOOKSTORE,false:_cbPlacesFilter-PHOTOGRAPHY,false:_cbPlacesFilter-CAR_DEALERSHIP,false:_cbPlacesFilter-FASHION_AND_CLOTHING,false:_cbPlacesFilter-CONVENIENCE_STORE,false:_cbPlacesFilter-PERSONAL_CARE,false:_cbPlacesFilter-DEPARTMENT_STORE,false:_cbPlacesFilter-PHARMACY,false:_cbPlacesFilter-ELECTRONICS,false:_cbPlacesFilter-FLOWERS,false:_cbPlacesFilter-FURNITURE_HOME_STORE,false:_cbPlacesFilter-GIFTS,false:_cbPlacesFilter-GYM_FITNESS,false:_cbPlacesFilter-SWIMMING_POOL,false:_cbPlacesFilter-HARDWARE_STORE,false:_cbPlacesFilter-MARKET,false:_cbPlacesFilter-SUPERMARKET_GROCERY,false:_cbPlacesFilter-JEWELRY,false:_cbPlacesFilter-LAUNDRY_DRY_CLEAN,false:_cbPlacesFilter-SHOPPING_CENTER,false:_cbPlacesFilter-MUSIC_STORE,false:_cbPlacesFilter-PET_STORE_VETERINARIAN_SERVICES,false:_cbPlacesFilter-TOY_STORE,false:_cbPlacesFilter-TRAVEL_AGENCY,false:_cbPlacesFilter-ATM,false:_cbPlacesFilter-CURRENCY_EXCHANGE,false:_cbPlacesFilter-CAR_RENTAL,false:_cbPlacesFilter-TELECOM,false:_cbPlacesFilter-TRANSPORTATION,false:_cbPlacesFilter-AIRPORT,false:_cbPlacesFilter-BUS_STATION,false:_cbPlacesFilter-FERRY_PIER,false:_cbPlacesFilter-SEAPORT_MARINA_HARBOR,false:_cbPlacesFilter-SUBWAY_STATION,false:_cbPlacesFilter-TRAIN_STATION,false:_cbPlacesFilter-BRIDGE,false:_cbPlacesFilter-TUNNEL,false:_cbPlacesFilter-TAXI_STATION,false:_cbPlacesFilter-JUNCTION_INTERCHANGE,false:_cbPlacesFilter-REST_AREAS,false:_cbPlacesFilter-CARPOOL_SPOT,false:_cbFilterPrivatePlaces,false:_cbInvertPlacesFilter,false[END]';
      defaultSettings += '[UROverviewPlacesGroups][len=71]false:false:false:false:false:false:false:false:false:false:false:false[END]';
      defaultSettings += '[UROverviewMPOptions][len=1446]:_cbMPFilterOutsideArea,false:_cbMPFilter_T1,false:_cbMPFilter_T2,false:_cbMPFilter_T3,false:_cbMPFilter_T5,false:_cbMPFilter_T6,false:_cbMPFilter_T7,false:_cbMPFilter_T8,false:_cbMPFilter_T10,false:_cbMPFilter_T11,false:_cbMPFilter_T12,false:_cbMPFilter_T13,false:_cbMPFilter_T14,false:_cbMPFilter_T15,false:_cbMPFilter_T16,false:_cbMPFilter_T17,false:_cbMPFilter_T19,false:_cbMPFilter_T20,false:_cbMPFilter_T21,false:_cbMPFilter_T22,false:_cbMPFilter_T23,false:_cbMPFilter_T50,false:_cbMPFilter_T51,false:_cbMPFilter_T52,false:_cbMPFilter_T53,false:_cbMPFilter_T70,false:_cbMPFilter_T71,false:_cbMPFilter_T101,false:_cbMPFilter_T102,false:_cbMPFilter_T103,false:_cbMPFilter_T104,false:_cbMPFilter_T105,false:_cbMPFilter_T106,false:_cbMPFilter_T200,false:_cbMPFilter_T300,false:_cbMPFilterUnknownProblem,false:_cbFilterElgin,false:_cbFilterTrafficCast,false:_cbFilterTrafficMaster,false:_cbFilterCaltrans,false:_cbFilterTFL,false:_cbMPFilterReopenedProblem,false:_cbInvertMPFilter,false:_cbMPFilterClosed,false:_cbMPFilterSolved,false:_cbMPFilterUnidentified,false:_cbMPClosedUserIDFilter,false:_cbMPNotClosedUserIDFilter,false:_cbMPFilterLowSeverity,false:_cbMPFilterMediumSeverity,false:_cbMPFilterHighSeverity,false:_cbMPFilterStartDate,false:_inputMPFilterStartDay,:_inputMPFilterStartMonth,:_inputMPFilterStartYear,:_cbMPFilterEndDate,false:_inputMPFilterEndDay,:_inputMPFilterEndMonth,:_inputMPFilterEndYear,:_cbMPFilterEndDatePassed,false[END]';
      defaultSettings += '[UROverviewMasterEnable][len=4]true[END]';
      defaultSettings += '[UROverviewCWLGroups][len=16]0,No group,false[END]';
      defaultSettings += '[UROverviewMCOptions][len=828]:_cbMCFilterRoadworks,false:_cbMCFilterConstruction,false:_cbMCFilterClosure,false:_cbMCFilterEvent,false:_cbMCFilterNote,false:_cbMCFilterBOG,false:_cbMCFilterDifficult,false:_cbMCFilterWSLM,false:_cbInvertMCFilter,false:_cbMCHideMyFollowed,false:_cbMCHideMyUnfollowed,false:_cbMCDescriptionMustBePresent,false:_cbMCDescriptionMustBeAbsent,false:_cbMCCommentsMustBePresent,false:_cbMCCommentsMustBeAbsent,false:_cbMCExpiryMustBePresent,false:_cbMCExpiryMustBeAbsent,false:_cbMCEnableKeywordMustBePresent,false:_textMCKeywordPresent,:_cbMCEnableKeywordMustBeAbsent,false:_textMCKeywordAbsent,:_cbMCCaseInsensitive,false:_cbMCCreatorIDFilter,false:_cbHideWRCMCs,false:_cbHideMCRank0,false:_cbHideMCRank1,false:_cbHideMCRank2,false:_cbHideMCRank3,false:_cbHideMCRank4,false:_cbHideMCRank5,false:_cbMCEnhancePointMCVisibility,false[END]';
      defaultSettings += '[UROverviewRTCOptions][len=710]:_cbHideExpiredEditorRTCs,false:_cbHideEditorRTCs,false:_cbHideFutureEditorRTCs,false:_cbHideExpiredWazeFeedRTCs,false:_cbHideWazeFeedRTCs,false:_cbHideFutureWazeFeedRTCs,false:_cbHideExpiredWazeRTCs,false:_cbHideWazeRTCs,false:_cbHideFutureWazeRTCs,false:_cbHideExpiredSidepanelRTCs,false:_cbHideSidepanelRTCs,false:_cbHideFutureSidepanelRTCs,false:_cbShowMTERTCs,false:_cbHideMTERTCs,false:_cbEnableRTCDurationFilterLessThan,false:_inputFilterRTCDurationLessThan,:_cbEnableRTCDurationFilterMoreThan,false:_inputFilterRTCDurationMoreThan,:_cbRTCFilterShowForTS,false:_cbRTCFilterHideForTS,false:_inputRTCFilterDay,15:_inputRTCFilterMonth,6:_inputRTCFilterYear,2024:_inputRTCFilterHour,11:_inputRTCFilterMin,15[END]';
      defaultSettings += '[UROverviewCameraOptions][len=908]:_cbShowWorldCams,true:_cbShowUSACams,true:_cbShowNonWorldCams,true:_cbShowOnlyCamsCreatedBy,false:_cbShowOnlyCamsEditedBy,false:_textCameraEditor,:_cbShowOnlyMyCams,false:_cbShowSpeedCams,true:_cbShowIfSpeedSet,true:_cbShowIfNoSpeedSet,true:_cbShowIfInvalidSpeedSet,true:_cbShowRedLightCams,true:_cbShowRLCIfZeroSpeedSet,true:_cbShowRLCIfNonZeroSpeedSet,true:_cbShowRLCIfNoSpeedSet,true:_cbShowDummyCams,true:_cbHideCreatedByMe,false:_cbHideCreatedByRank0,false:_cbHideCreatedByRank1,false:_cbHideCreatedByRank2,false:_cbHideCreatedByRank3,false:_cbHideCreatedByRank4,false:_cbHideCreatedByRank5,false:_cbHideUpdatedByMe,false:_cbHideUpdatedByRank0,false:_cbHideUpdatedByRank1,false:_cbHideUpdatedByRank2,false:_cbHideUpdatedByRank3,false:_cbHideUpdatedByRank4,false:_cbHideUpdatedByRank5,false:_cbHideManualLockedCams,false:_cbHideCWLCams,false:_cbInvertCamFilters,false:_cbHighlightInsteadOfHideCams,false[END]';
      defaultSettings += '[UROverviewRAOptions][len=178]:_cbShowSpecificRA,false:_cbRAEditorIDFilter,false:_cbEnableRAAgeFilterLessThan,false:_inputFilterRAAgeLessThan,39:_cbEnableRAAgeFilterMoreThan,false:_inputFilterRAAgeMoreThan,38[END]';
      defaultSettings += '[UROverviewCurrentVersion][len=3]4.5[END]';      
      defaultSettings += '[UROverviewCamWatchList][len=0][END]';
      defaultSettings += '[UROverviewFriendlyAreaNames][len=0][END]';
      defaultSettings += '[UROverviewPlaceWatchList][len=0][END]';
      defaultSettings += '[UROverviewSegWatchList][len=0][END]';

      document.getElementById('_txtSettings').value = defaultSettings;
      uroConfig.TextToSettings();
      document.getElementById('_txtSettings').value = '';
   },
   SettingsToText: function()
   {
      let txtSettings = '';

      uroConfig.SaveSettings();

      for(let lsEntry in localStorage)
      {
         if(lsEntry.indexOf('UROverview') === 0)
         {
            txtSettings += '['+lsEntry+'][len=' + localStorage[lsEntry].length + ']' + localStorage[lsEntry] + '[END]\n';
         }
      }

      document.getElementById('_txtSettings').value = txtSettings;
      document.getElementById('_txtSettings').focus();
      document.getElementById('_txtSettings').select();
   },
   TextToSettings: function()
   {
      let txtSettings = '';
      txtSettings = uroUtils.GetElmValue('_txtSettings');
      if(txtSettings.indexOf('[END]') == -1) return;

      let subText = txtSettings.split('[END]');
      for(let i=0;i<subText.length;i++)
      {
         let aPos = subText[i].indexOf('[');
         let bPos = subText[i].indexOf(']');
         if((aPos != -1) && (bPos != -1))
         {
            let settingID = subText[i].substr(aPos+1,bPos-1-aPos);
            subText[i] = subText[i].substr(bPos+1);
            bPos = subText[i].indexOf(']');
            if(bPos != -1)
            {
               let settingLength = subText[i].substr(5,bPos-5);
               subText[i] = subText[i].substr(bPos+1);
               if(subText[i].length == settingLength)
               {
                  localStorage[settingID] = subText[i];
               }
            }
         }
      }
      uroConfig.LoadSettings();
   },
   ClearSettingsText: function()
   {
      document.getElementById('_txtSettings').value = '';
   }
};
const uroRTCClone = // RTC cloning
{
   ConfirmDelete : true,
   ToDelete : 0,
   Reason : null,
   Event : null,
   Direction : null,
   StartDate : null,
   StartTime : null,
   EndDate : null,
   EndTime : null,
   IgnoreTraffic : null,
   ClosedNodes : null,
   PendingClone : -1,
   PendingCloneIncrement : 0,

   Complete: function()
   {
      let loop;
      
      if(document.getElementsByClassName('edit-closure').length === 0)
      {
         window.setTimeout(uroRTCClone.Complete,100);
         return;
      }

      if(uroFixMTEDropDown(document.getElementById('closure_eventId')) == false)
      {
         window.setTimeout(uroRTCClone.Complete,100);
         return;
      }

      // need to generate a change event on each of the form fields, because WME appears to be silently populating some hidden
      // closure object with the details as they're entered manually, and if we just set the form values without then forcing
      // the change event as well then WME will end up using its default values instead of the ones we've so lovingly copied...
      let form = $('#edit-panel .closures .edit-closure form');

      if(uroRTCClone.Reason !== null)
      {
         let fObj = form.find('wz-text-input#closure_reason');
         fObj.val(uroRTCClone.Reason);
         fObj.change();
      }
      if(uroRTCClone.Direction !== null)
      {
         let fObj = form.find('wz-select#closure_direction');
         fObj[0].value = uroRTCClone.Direction;
         fObj.change();
      }
      if(uroRTCClone.StartDate !== null)
      {
         let fObj = form.find('input#closure_startDate');
         fObj.val(uroRTCClone.StartDate);
         fObj.change();
      }
      if(uroRTCClone.StartTime !== null)
      {
         let fObj = form.find('div.form-group.start-date-form-group input.time-picker-input');
         fObj.focus();
         fObj.val(uroRTCClone.StartTime);
         fObj.change();
      }

      if(uroRTCClone.IgnoreTraffic !== null)
      {
         let fObj = form.find('wz-checkbox#closure_permanent');
         fObj.val(uroRTCClone.IgnoreTraffic);
         fObj.change();
      }
      if(uroRTCClone.EndTime !== null)
      {
         let fObj = form.find('div.form-group.end-date-form-group input.time-picker-input');
         fObj.focus();
         fObj.val(uroRTCClone.EndTime);
         fObj.change();
      }

      // the current version of WME wipes any existing end date as soon as the end time is altered, so we now need
      // to set the date after the time instead of before as in earlier versions of this function...
      if(uroRTCClone.EndDate !== null)
      {
         let fObj = form.find('input#closure_endDate');
         fObj.val(uroRTCClone.EndDate);
         fObj.change();
      }

      // the old method of setting the MTE just by changing the value attribute on closure_eventId no longer
      // seems to work as expected (it runs OK from the dev console, but not within the scope of the userscript),
      // so just as we do for setting the event to None, we set the event to the desired value here by finding
      // the appropriate menu entry and clicking on it...
      let cEvents = document.getElementById('closure_eventId').getElementsByTagName('wz-option');
      for(let i of cEvents)
      {
         if(i.value == uroRTCClone.Event)
         {
            i.click();
            break;
         }
      }

      let nNodes = uroRTCClone.ClosedNodes.length;
      if(nNodes > 0)
      {
         let fObj = form.find('wz-toggle-switch');
         for(loop = 0; loop < nNodes; ++loop)
         {
            if(uroRTCClone.ClosedNodes[loop] === true)
            {
               fObj[loop].click();
            }
         }
      }      
      if(uroUtils.GetCBChecked('_cbAutoApplyClonedClosure') == true)
      {
         window.setTimeout(uroRTCClone.ClickSave,100);
      }

      uroRTCClone.PendingClone = -1;
   },
   ClickSave: function()
   {
      document.getElementsByClassName('closures')[0].getElementsByClassName('save-button')[0].click();
   },
   Copy: function()
   {
      // grab the current closure details from the UI...
      uroRTCClone.Reason = uroGetShadowElementProperty('closure_reason', 'input', 'value');
      uroRTCClone.Direction = uroGetElementProperty('closure_direction', 0, 'value');
      uroRTCClone.StartDate = uroGetElementProperty('closure_startDate', 0, 'value');
      uroRTCClone.StartTime = document.querySelector('.start-date-form-group').querySelector('.time-picker-input').value;
      uroRTCClone.EndDate = uroGetElementProperty('closure_endDate', 0, 'value');
      uroRTCClone.EndTime = document.querySelector('.end-date-form-group').querySelector('.time-picker-input').value;
      uroRTCClone.Event = uroGetElementProperty('closure_eventId', 0, 'value');
      uroRTCClone.IgnoreTraffic = uroGetElementProperty('closure_permanent', 0, 'checked');
      uroRTCClone.ClosedNodes = [];
      let nNodes = document.getElementsByClassName('fromNodeClosed').length;
      if(nNodes > 0)
      {
         for(let loop = 0; loop < nNodes; ++loop)
         {
            uroRTCClone.ClosedNodes.push(document.getElementsByClassName('fromNodeClosed')[loop].checked);
         }
      }

      document.getElementsByClassName('closures')[0].getElementsByClassName('cancel-button')[0].click();

      // auto-increment the start and end dates
      uroRTCClone.StartDate = uroIncrementClosureDate(uroRTCClone.StartDate,uroRTCClone.PendingCloneIncrement);
      uroRTCClone.EndDate = uroIncrementClosureDate(uroRTCClone.EndDate,uroRTCClone.PendingCloneIncrement);

      uroRTCClone.PendingClone = -2;
   },
   Clone: function()
   {
      uroRTCClone.PendingCloneIncrement = parseInt(this.id.split('-')[1]);
      uroRTCClone.PendingClone = parseInt(this.id.split('-')[2]);
   },
   DeleteNextOnList: function()
   {
      let nClosures = document.querySelectorAll('.closure-item.is-editable').length;
      if(nClosures > 0)
      {
         if (nClosures != uroRTCClone.ToDelete)
         {
            uroRTCClone.ToDelete = nClosures;
            let ctObj = document.querySelector('.closure-item.is-editable');
            let deleteMenuEntry = ctObj.querySelector('wz-menu-item.delete');
            if(deleteMenuEntry !== null)
            {
               deleteMenuEntry.click();
            }
         }
         window.setTimeout(uroRTCClone.DeleteNextOnList,100);
      }
      else
      {
         uroRTCClone.ConfirmDelete = true;
      }
   },
   DeleteAll: function()
   {
      uroRTCClone.ConfirmDelete = true;
      uroAlertBox.Show("fa-warning", "URO+ Warning", I18n.lookup("closures.delete_confirm_no_reason")+' ('+I18n.lookup("closures.apply_to_all")+')', true, "Yes", "No", uroRTCClone.DeleteAllAction, null);
   },
   DeleteAllAction: function()
   {
      uroRTCClone.ConfirmDelete = false;
      let nClosures = document.getElementsByClassName('closure-item').length;
      if(nClosures > 0)
      {
         uroRTCClone.ToDelete = -1;
         uroRTCClone.DeleteNextOnList();
      }
      else
      {
         uroRTCClone.ConfirmDelete = true;
      }
   }   
};
const uroOWL = /// camera watchlist
{
   CWLGroups : [],
   CamWatchObjects : [],

   GroupObj: function(groupID, groupName, groupCollapsed)
   {
      groupID = uroUtils.TypeCast(groupID);
      this.groupID = groupID;
      this.groupName = groupName;
      this.groupCount = 0;
      this.groupCollapsed = groupCollapsed;
   },
   CamWatchObjCheckProps: function(type, azymuth, speed, lat, lon)
   {
      if(type !== null) type = uroUtils.TypeCast(type);
      if(azymuth !== null) azymuth = uroUtils.Truncate(uroUtils.TypeCast(azymuth)%360);
      if(speed !== null) speed = uroUtils.Truncate(uroUtils.TypeCast(speed));
      if(lat !== null) lat = uroUtils.Truncate(uroUtils.TypeCast(lat));
      if(lon !== null) lon = uroUtils.Truncate(uroUtils.TypeCast(lon));

      this.type = type;
      this.azymuth = azymuth;
      this.speed = speed;
      this.lat = lat;
      this.lon = lon;
   },
   CamWatchObj: function(persistent, fid, lon, lat, type, azymuth, speed, groupID, server)
   {
      fid = uroUtils.TypeCast(fid);
      groupID = uroUtils.TypeCast(groupID);
      if(typeof persistent == "string") persistent = (persistent == "true");
      if(server === "undefined") server = "??";

      this.fid = fid;
      this.persistent = persistent;
      this.loaded = false;
      this.server = server;
      this.groupID = groupID;
      this.watch = new uroOWL.CamWatchObjCheckProps(type, azymuth, speed, lat, lon);
      this.current = new uroOWL.CamWatchObjCheckProps(null, null, null, null, null);
   },
   CamDataChanged: function(idx)
   {
      let camObj = uroOWL.CamWatchObjects[idx];
      if(camObj.loaded === false) return false;
      if(camObj.current.type != camObj.watch.type) return true;
      if(camObj.current.azymuth != camObj.watch.azymuth) return true;
      if(camObj.current.speed != camObj.watch.speed) return true;
      if(camObj.current.lat != camObj.watch.lat) return true;
      if(camObj.current.lon != camObj.watch.lon) return true;
      return false;
   },
   FindCWLGroupByIdx: function(groupIdx)
   {
      let groupName = '';
      for(let loop=0;loop<uroOWL.CWLGroups.length;loop++)
      {
         if(uroOWL.CWLGroups[loop].groupID == groupIdx)
         {
            groupName = uroOWL.CWLGroups[loop].groupName;
            break;
         }
      }
      return groupName;
   },
   IsCamOnWatchList: function(fid)
   {
      for(let loop=0;loop<uroOWL.CamWatchObjects.length;loop++)
      {
         if(uroOWL.CamWatchObjects[loop].fid == fid) return loop;
      }
      return -1;
   },
   AddCurrentCamWatchData: function(idx, lat, lon, type, azymuth, speed, server)
   {
      let camObj = uroOWL.CamWatchObjects[idx];
      camObj.loaded = true;
      camObj.server = server;
      camObj.current = new uroOWL.CamWatchObjCheckProps(type, azymuth, speed, lat, lon);
      return(uroOWL.CamDataChanged(idx));
   },
   AddCamToWatchList: function()
   {
      if(uroOWL.IsCamOnWatchList(uroShownFID) == -1)
      {
         let camObj = W.model.cameras.objects[uroShownFID];
         uroOWL.CamWatchObjects.push(new uroOWL.CamWatchObj(true, uroShownFID, camObj.geometry.x, camObj.geometry.y, camObj.attributes.type, camObj.attributes.azymuth, camObj.attributes.speed, 0, W.app.getAppRegionCode()));
         uroOWL.AddCurrentCamWatchData(uroOWL.CamWatchObjects.length-1, camObj.geometry.y, camObj.geometry.x, camObj.attributes.type, camObj.attributes.azymuth, camObj.attributes.speed, W.app.getAppRegionCode());
         uroDBG.AddLog('added camera '+uroShownFID+' to watchlist');
         uroTabs.PopulateOWL();
      }
   },
   RemoveCamFromWatchList: function()
   {
      let camidx = uroOWL.IsCamOnWatchList(uroShownFID);
      if(camidx != -1)
      {
         uroOWL.CamWatchObjects.splice(camidx,1);
         uroDBG.AddLog('removed camera '+uroShownFID+' from watchlist');
         uroTabs.PopulateOWL();
      }
   },
   UpdateCamWatchList: function()
   {
      let camIdx = uroOWL.IsCamOnWatchList(uroShownFID);
      if(camIdx != -1)
      {
         let camObj = W.model.cameras.objects[uroShownFID];
         uroOWL.CamWatchObjects[camIdx].watch = new uroOWL.CamWatchObjCheckProps(camObj.attributes.type, camObj.attributes.azymuth, camObj.attributes.speed, camObj.geometry.y, camObj.geometry.x);
      }
   },
   ClearCamWatchList: function()
   {
      uroAlertBox.Show("fa-warning", "URO+ Warning", "Removing all cameras from the OWL <b>cannot</b> be undone.<br>Are you <i>sure</i> you want to do this?", true, "Delete ALL Cameras", "Keep Cameras", uroOWL.ClearCamWatchListAction, null);
   },
   ClearCamWatchListAction: function()
   {
      uroOWL.CamWatchObjects = [];
      uroTabs.PopulateOWL();
   },
   RetrieveCameras: function(lat, lon)
   {
      let camPos = new OpenLayers.LonLat();
      let camChanged = false;

      camPos.lon = lon;
      camPos.lat = lat;
      camPos = uroUtils.ConvertMercatorToWGS84(camPos);

      let camURL = 'https://' + document.location.host;
      camURL += W.Config.api_base;
      camURL += '/Features?language=en&cameras=true&bbox=';
      let latl = camPos.lat - 0.25;
      let latu = camPos.lat + 0.25;
      let lonl = camPos.lon - 0.25;
      let lonr = camPos.lon + 0.25;
      camURL += lonl+','+latl+','+lonr+','+latu;
      uroDBG.AddLog('retrieving camera data around '+camPos.lon+','+camPos.lat);

      let camReq = new XMLHttpRequest();
      camReq.open('GET',camURL,false);
      try
      {
         camReq.send();
         uroDBG.AddLog('response '+camReq.status+' received for camera data request');
         if (camReq.status === 200)
         {
            let camData = JSON.parse(camReq.responseText);
            for(let camIdx = 0; camIdx < camData.cameras.objects.length; camIdx++)
            {
               let camObj = camData.cameras.objects[camIdx];
               let listIdx = uroOWL.IsCamOnWatchList(camObj.id);
               if(listIdx != -1)
               {
                  camPos.lon = camObj.geometry.coordinates[0];
                  camPos.lat = camObj.geometry.coordinates[1];
                  camPos = uroUtils.ConvertWGS84ToMercator(camPos);
                  camPos.lon = uroUtils.Truncate(camPos.lon);
                  camPos.lat = uroUtils.Truncate(camPos.lat);
                  camChanged = (uroOWL.AddCurrentCamWatchData(listIdx, camPos.lat, camPos.lon, camObj.type, camObj.azymuth, camObj.speed, W.app.getAppRegionCode()) || camChanged);
               }
            }
         }
         else
         {
            uroDBG.AddLog('camera data request failed (status != 200)');
         }
      }
      catch(err)
      {
         uroDBG.AddLog('camera data request failed (exception '+err+' caught)');
      }
      return camChanged;
   },
   GetCurrentCamWatchListObjects: function()
   {
      let camChanged = false;
      let camsChanged = [];
      let camsDeleted = [];
      let camidx;
      let camObj;
      for(camidx=0;camidx<uroOWL.CamWatchObjects.length;camidx++)
      {
         camObj = uroOWL.CamWatchObjects[camidx];
         if((camObj.loaded === false) && ((camObj.server == W.app.getAppRegionCode()) || (camObj.server == '??')))
         {
            if(typeof W.model.cameras.objects[camObj.fid] == 'object')
            {
               if(W.model.cameras.objects[camObj.fid].state != "Delete")
               {
                  let wazeObj = W.model.cameras.objects[camObj.fid];
                  camChanged = (uroOWL.AddCurrentCamWatchData(camidx, wazeObj.geometry.y, wazeObj.geometry.x, wazeObj.attributes.type, wazeObj.attributes.azymuth, wazeObj.attributes.speed, W.app.getAppRegionCode()) || camChanged);
               }
               else
               {
                  camChanged = (uroOWL.RetrieveCameras(camObj.watch.lat, camObj.watch.lon) || camChanged);
               }
            }
            else
            {
               camChanged = (uroOWL.RetrieveCameras(camObj.watch.lat, camObj.watch.lon) || camChanged);
            }
         }
      }

      if(camChanged)
      {
         for(camidx=0;camidx<uroOWL.CamWatchObjects.length;camidx++)
         {
            if(uroOWL.CamDataChanged(camidx))
            {
               camsChanged.push(uroOWL.CamWatchObjects[camidx]);
            }
         }
      }

      for(camidx=0;camidx<uroOWL.CamWatchObjects.length;camidx++)
      {
         camObj = uroOWL.CamWatchObjects[camidx];
         if((camObj.loaded === false) && (camObj.server == W.app.getAppRegionCode()))
         {
            camsDeleted.push(camObj);
         }
      }
      if((camsChanged.length > 0) || (camsDeleted.length > 0))
      {
         let alertStr = '';
         for(camidx=0;camidx<camsChanged.length;camidx++)
         {
            alertStr += 'Camera ID '+camsChanged[camidx].fid+' in group "'+uroOWL.FindCWLGroupByIdx(camsChanged[camidx].groupID)+'" has been changed<br>';
         }
         alertStr += '<br>';
         for(camidx=0;camidx<camsDeleted.length;camidx++)
         {
            alertStr += 'Camera ID '+camsDeleted[camidx].fid+' in group "'+uroOWL.FindCWLGroupByIdx(camsDeleted[camidx].groupID)+'" has been deleted<br>';
         }
         uroAlertBox.Show("fa-info-circle", "URO+ Camera Watchlist Alert", alertStr, false, "OK", null, null, null);
      }
   },
   ClearDeletedCameras: function()
   {
      for(let camidx=uroOWL.CamWatchObjects.length-1;camidx>=0;camidx--)
      {
         if(uroOWL.CamWatchObjects[camidx].loaded === false)
         {
            uroShownFID = uroOWL.CamWatchObjects[camidx].fid;
            uroOWL.RemoveCamFromWatchList();
         }
      }
   },
   AcceptCameraChanges: function()
   {
      for(let camidx=0; camidx < uroOWL.CamWatchObjects.length; camidx++)
      {
         if(uroOWL.CamDataChanged(camidx))
         {
            uroOWL.CamWatchObjects[camidx].watch.type = uroOWL.CamWatchObjects[camidx].current.type;
            uroOWL.CamWatchObjects[camidx].watch.azymuth = uroOWL.CamWatchObjects[camidx].current.azymuth;
            uroOWL.CamWatchObjects[camidx].watch.speed = uroOWL.CamWatchObjects[camidx].current.speed;
            uroOWL.CamWatchObjects[camidx].watch.lat = uroOWL.CamWatchObjects[camidx].current.lat;
            uroOWL.CamWatchObjects[camidx].watch.lon = uroOWL.CamWatchObjects[camidx].current.lon;
         }
      }
      uroTabs.PopulateOWL();
   },
   ClearUnknownServerCameras: function()
   {
      let confirmMsg = '<p>Cameras with an unknown server <i>cannot</i> be automatically verified by URO+</p>';
      confirmMsg += 'It is recommended that you manually load WME from each server (World, USA/Canada and Israel) to give URO+ a chance of locating these cameras.<br>';
      confirmMsg += 'If the cameras then continue to show up as an unknown server, it is safe to delete them...<br><br>';
      confirmMsg += 'Do you still wish to proceed with deleting all unknown server cameras?';

      uroAlertBox.Show("fa-warning", "URO+ Warning", confirmMsg, true, "Delete unknown cameras", "Keep unknown cameras", uroOWL.ClearUnknownServerCamerasAction, null);
   },
   ClearUnknownServerCamerasAction: function()
   {
      for(let camidx=uroOWL.CamWatchObjects.length-1;camidx>=0;camidx--)
      {
         if(uroOWL.CamWatchObjects[camidx].server == '??')
         {
            uroShownFID = uroOWL.CamWatchObjects[camidx].fid;
            uroOWL.RemoveCamFromWatchList();
         }
      }
   },
   RescanCamWatchList: function()
   {
      for(let camidx=0;camidx<uroOWL.CamWatchObjects.length;camidx++)
      {
         uroOWL.CamWatchObjects[camidx].loaded = false;
      }
      uroOWL.GetCurrentCamWatchListObjects();
      uroTabs.PopulateOWL();
   },
   GotoCam: function()
   {
      let camidx = this.id.substr(13);
      let camPos = new OpenLayers.LonLat();
      camPos.lon = uroOWL.CamWatchObjects[camidx].watch.lon;
      camPos.lat = uroOWL.CamWatchObjects[camidx].watch.lat;
      W.map.setCenter(camPos,16);
      W.map.camerasLayer.setVisibility(true);
      return false;
   },
   HighlightCWLEntry: function()
   {
      this.style.backgroundColor = '#FFFFAA';
      return false;
   },
   UnhighlightCWLEntry: function()
   {
      let camidx = this.id.substr(8);
      let changed = uroOWL.CamDataChanged(camidx);
      let deleted = (uroOWL.CamWatchObjects[camidx].loaded === false);

      if(uroOWL.CamWatchObjects[camidx].server != W.app.getAppRegionCode())
      {
         if(uroOWL.CamWatchObjects[camidx].server == '??') this.style.backgroundColor = '#A0A0A0';
         else this.style.backgroundColor = '#AAFFAA';
      }
      else if(changed) this.style.backgroundColor = '#AAAAFF';
      else if(deleted) this.style.backgroundColor = '#FFAAAA';
      else this.style.backgroundColor = '#FFFFFF';
      return false;
   },
   CWLIconHighlight: function()
   {
      this.style.color="#0000ff";
      return false;
   },
   CWLIconLowlight: function()
   {
      this.style.color="#ccccff";
      return false;
   },
   PopulateCWLGroupSelect: function()
   {
      let selector = document.getElementById('_uroCWLGroupSelect');
      while(selector.options.length > 0)
      {
         selector.options.remove(0);
      }
      for(let loop=0;loop<uroOWL.CWLGroups.length;loop++)
      {
         let groupObj = uroOWL.CWLGroups[loop];
         if(groupObj.groupID != -1)
         {
            selector.options.add(new Option(groupObj.groupName,groupObj.groupID));
         }
      }
   },
   GetNextCWLGroupID: function()
   {
      let nextID = 1;
      for(let loop=0;loop<uroOWL.CWLGroups.length;loop++)
      {
         if(uroOWL.CWLGroups[loop].groupID >= nextID)
         {
            nextID = uroOWL.CWLGroups[loop].groupID + 1;
         }
      }
      return nextID;
   },
   FindCWLGroupByName: function(groupName)
   {
      let groupID = -1;
      for(let loop=0;loop<uroOWL.CWLGroups.length;loop++)
      {
         if((uroOWL.CWLGroups[loop].groupName == groupName) && (uroOWL.CWLGroups[loop].groupID != -1))
         {
            groupID = uroOWL.CWLGroups[loop].groupID;
            break;
         }
      }
      return groupID;
   },
   AddCWLGroup: function()
   {
      let groupID = uroOWL.GetNextCWLGroupID();
      let groupName = uroUtils.GetElmValue('_uroCWLGroupEntry');
      if(uroOWL.FindCWLGroupByName(groupName) == -1)
      {
         uroOWL.CWLGroups.push(new uroOWL.GroupObj(groupID,groupName,false));
         uroOWL.PopulateCWLGroupSelect();
      }
   },
   RemoveCWLGroup: function()
   {
      let loop;
      let selector = document.getElementById('_uroCWLGroupSelect');
      let groupID = parseInt(selector.selectedOptions[0].value);
      if(groupID === 0) return false;   // prevent deletion of the default group

      for(loop=0;loop<uroOWL.CamWatchObjects.length;loop++)
      {
         let cwObj = uroOWL.CamWatchObjects[loop];
         if(cwObj.groupID == groupID)
         {
            cwObj.groupID = 0;
         }
      }
      for(loop=0;loop<uroOWL.CWLGroups.length;loop++)
      {
         let groupObj = uroOWL.CWLGroups[loop];
         if(groupObj.groupID == groupID)
         {
            groupObj.groupID = -1;
         }
      }
      uroTabs.PopulateOWL();
   },
   AssignCameraToGroup: function()
   {
      let camidx = this.id.substr(13);
      let selector = document.getElementById('_uroCWLGroupSelect');
      uroOWL.CamWatchObjects[camidx].groupID = parseInt(selector.selectedOptions[0].value);
      uroTabs.PopulateOWL();
      return false;
   },
   CWLGroupCollapseExpand: function()
   {
      let groupidx = this.id.substr(18);
      if(uroOWL.CWLGroups[groupidx].groupCollapsed === true) uroOWL.CWLGroups[groupidx].groupCollapsed = false;
      else uroOWL.CWLGroups[groupidx].groupCollapsed = true;
      uroTabs.PopulateOWL();
      return false;
   }
};
const uroIgnore = // ignore list
{
   IsOnList: function(fid)
   {
      if(sessionStorage.UROverview_FID_IgnoreList.indexOf('fid:'+fid) == -1) return false;
      else return true;
   },
   EnableControls: function()
   {
      let btnState = "visible";
      if(sessionStorage.UROverview_FID_IgnoreList === '')
      {
         btnState = "hidden";
      }
      try
      {
         document.getElementById('_btnUndoLastHide').style.visibility = btnState;
         document.getElementById('_btnClearSessionHides').style.visibility = btnState;
         uroFilterItems();
      }
      catch(err)
      {
         uroDBG.AddLog('exception thrown in uroIgnore.EnableControls()');
      }
   },
   Add: function()
   {
      if(!uroIgnore.IsOnList(uroShownFID))
      {
         sessionStorage.UROverview_FID_IgnoreList += 'fid:'+uroShownFID;
         uroDBG.AddLog('added fid '+uroShownFID+' to ignore list');
         uroDBG.AddLog(sessionStorage.UROverview_FID_IgnoreList);
         uroDiv.style.visibility = 'hidden';
         uroIgnore.EnableControls();

         W.map.events.register("mousemove", null, uroFilterItemsOnMove);
      }
      return false;
   },
   RemoveLastAdded: function()
   {
      let ignorelist = sessionStorage.UROverview_FID_IgnoreList;
      let fidpos = ignorelist.lastIndexOf('fid:');
      if(fidpos != -1)
      {
         ignorelist = ignorelist.slice(0,fidpos);
         sessionStorage.UROverview_FID_IgnoreList = ignorelist;
         uroDBG.AddLog('removed last fid from ignore list');
         uroDBG.AddLog(sessionStorage.UROverview_FID_IgnoreList);
         uroIgnore.EnableControls();
      }
   },
   RemoveAll: function()
   {
      sessionStorage.UROverview_FID_IgnoreList = '';
      uroIgnore.EnableControls();
   }
};
const uroPopup = // map object popup handling
{
   hasIgnoreLink : null,
   hasDeleteLink : null,
   hasAddWatchLink : null,
   hasRemoveWatchLink : null,
   hasUpdateWatchLink : null,
   hasRecentreSessionLink : null,
   hasOpenInNewTabLink : null,

   isVenue : null,
   isMapComment : null,
   isUR : null,
   isProblem : null,
   isTurnProb : null,
   isPlaceUpdate : null,
   
   timer : -2,
   autoHideTimer : 0,
   
   mouseIn : false,
   shown : false,
   shownType : null,
   newType : null,
   hovered : null,
   unstackedX : null,
   unstackedY : null,
   renderIntent : null,
   pX : null,
   pY : null,
   suppressed : false,

   GetFormattedLocks: function(attrs)
   {
      let autoLock = attrs.rank;
      let userLock = attrs.lockRank;
      let retval = '<b>' + I18n.lookup("edit.segment.fields.lock") + ': </b>';
      if(userLock !== null)
      {
         retval += 'M' + (userLock+1) + ' / ';
      }
      retval += 'A' + (autoLock+1);
      return retval;
   },
   UR: function()
   {
      let result = '';

      uroPopup.unstackedX = uroUtils.ParsePxString(uroMarkers.elm.style.left);
      uroPopup.unstackedY = uroUtils.ParsePxString(uroMarkers.elm.style.top);
                  
      // check for stacking...
      if(uroShownFID != uroMarkers.id)
      {
         uroCheckStacking(uroLayers.ID.UR,uroMarkers.id, uroPopup.unstackedX, uroPopup.unstackedY);
      }

      if(uroUtils.GetCBChecked('_cbInhibitURPopup') === false)
      {
         if(uroMousedOverMapComment !== null)
         {
            uroDBG.AddLog('setting uroMousedOverOtherObjectWithinMapComment for UR highlight');
            uroMousedOverOtherObjectWithinMapComment = true;
         }

         uroPopup.isUR = true;
         uroPopup.newPopupType = uroMarkers.type;
         let ureq = W.model.mapUpdateRequests.objects[uroMarkers.id];

         if(ureq.attributes != undefined)
         {
            uroFID = uroMarkers.id;

            uroDBG.AddLog('building popup for UR '+uroMarkers.id);
            result = '<b>Update Request ('+uroMarkers.id+'): ' + I18n.lookup("update_requests.types." + ureq.attributes.type) + '</b><br>';

            result += uroUtils.Clickify(ureq.attributes.description, '<br>');
            let uroDaysOld = uroUtils.GetURAge(ureq,0,false);
            let uroSubmittedTS = uroUtils.GetURAge(ureq,0,true);
            if(uroSubmittedTS != -1)
            {
               uroSubmittedTS = uroUtils.GetDateTimeString(uroSubmittedTS);
            }
            if(uroDaysOld != -1)
            {
               result += '<i>Submitted ' + uroUtils.ParseDaysAgo(uroDaysOld) + ' ';
               if(uroSubmittedTS != -1) result += '(' + uroSubmittedTS + ') ';
               if(ureq.attributes.guestUserName != null)
               {
                  result += 'via Livemap';
                  if(ureq.attributes.guestUserName !== '')
                  {
                     result += ' by '+ureq.attributes.guestUserName.replace(/<\/?[^>]+(>|$)/g, "");
                  }
               }
               result += '</i>';
            }
            if(ureq.attributes.resolvedOn !== null)
            {
               let daysResolved = uroUtils.GetURAge(ureq,1,false);
               let uroResolvedTS = uroUtils.GetURAge(ureq,1,true);
               if(uroResolvedTS != -1)
               {
                  uroResolvedTS = uroUtils.GetDateTimeString(uroResolvedTS);
               }

               if(daysResolved != -1)
               {
                  result += '<br><i>Closed ' + uroUtils.ParseDaysAgo(daysResolved) + ' ';
                  if(uroResolvedTS != -1) result += '(' + uroResolvedTS + ')</i>';

                  result += '<br><i>Marked as ';
                  if(ureq.attributes.resolution === 0) result += 'solved';
                  else if(ureq.attributes.resolution == 1) result += 'not identified';
                  else result += 'unknown';
                  if(ureq.attributes.resolvedBy !== null)
                  {
                     result += ' by '+uroUtils.GetUserNameAndRank(ureq.attributes.resolvedBy);
                  }
                  result += '</i>';
               }
            }
            if(W.model.updateRequestSessions.objects[uroMarkers.id] != null)
            {
               let hasMyComments = uroURHasMyComments(uroMarkers.id);
               let nComments = W.model.updateRequestSessions.objects[uroMarkers.id].attributes.comments.length;
               result += '<br>' + nComments + ' comment';
               if(nComments != 1) result += 's';
               if((hasMyComments === false) && (nComments > 0)) result += ' (none by me)';
               if(nComments > 0)
               {
                  let commentDaysOld = uroUtils.GetCommentAge(W.model.updateRequestSessions.objects[uroMarkers.id].attributes.comments[nComments-1]);
                  if(commentDaysOld != -1)
                  {
                     result += ', last update '+uroUtils.ParseDaysAgo(commentDaysOld);
                  }
               }
            }
            if(uroURDupes.length > 0)
            {
               let thisID = parseInt(uroMarkers.id);
               for(let i = 0; i < uroURDupes.length; ++i)
               {
                  if(uroURDupes[i][0] === thisID)
                  {
                     result += '<br><br>Duplicate of: ';
                     let dupes = 0;
                     for(let j = 0; j < uroURDupes[i][1].length; ++j)
                     {
                        if(uroURDupes[i][1][j] !== thisID)
                        {
                           if(dupes > 0)
                           {
                              result += ', ';
                           }   
                           result += uroURDupes[i][1][j];
                           ++dupes;
                        }
                     }
                  }
               }
            }
            uroPopup.result += result;
            uroPopup.UMPExtras(ureq);
            uroPopup.Show();
         }
      }
   },
   MP: function()
   {
      let result = '';

      uroPopup.unstackedX = uroUtils.ParsePxString(uroMarkers.elm.style.left);
      uroPopup.unstackedY = uroUtils.ParsePxString(uroMarkers.elm.style.top);
      
      // check for stacking...
      if(uroShownFID != uroMarkers.id)
      {
         ////uroCheckStacking(uroLayers.ID.MP,uroMarkers.id, uroPopup.unstackedX, uroPopup.unstackedY);
      }

      if(uroUtils.GetCBChecked('_cbInhibitMPPopup') === false)
      {
         if(uroMousedOverMapComment !== null)
         {
            uroDBG.AddLog('setting uroMousedOverOtherObjectWithinMapComment for MP highlight');
            uroMousedOverOtherObjectWithinMapComment = true;
         }

         uroPopup.isProblem = true;
         uroPopup.newPopupType = uroMarkers.type;
         let ureq = W.model.mapProblems.objects[uroMarkers.id];

         uroFID = uroMarkers.id;
         uroDBG.AddLog('building popup for problem '+uroMarkers.id);
         if(uroPopup.isTurnProb) result = '<b>Turn Problem ('+uroMarkers.id+'): ' + I18n.lookup("problems.types.turn.title");
         else
         {
            result = '<b>Map Problem ('+uroMarkers.id+'): ';

            let problemType = ureq.attributes.subType;
            if(problemType == 300)
            {
               result += I18n.lookup("problems.panel.closure.title");
            }
            else
            {
               if(I18n.lookup("problems.types." + problemType) === undefined) result += 'Unknown problem type ('+problemType+')';
               else result += I18n.lookup("problems.types." + problemType + ".title");
            }
         }
         result += '</b><br>';

         if(ureq.attributes.description != null)
         {
            result += 'Description: ' + ureq.attributes.description + '<br>';
         }
         if(ureq.attributes.extraInfo != null)
         {
            result += 'ExtraInfo: ' + uroUtils.Clickify(ureq.attributes.extraInfo, '<br>');
         }
         if(ureq.attributes.provider != null)
         {
            result += 'Provider: ' + ureq.attributes.provider + '<br>';
         }
         if(ureq.attributes.startTime != null)
         {
            result += 'From: ' + uroUtils.GetDateTimeString(ureq.attributes.startTime) + '<br>';
         }
         if(ureq.attributes.endTime != null)
         {
            result += 'To: ' + uroUtils.GetDateTimeString(ureq.attributes.endTime) + '<br>';
         }                              
         if(ureq.attributes.resolvedOn != null)
         {
            let daysResolved = uroUtils.GetURAge(ureq,1,false);
            if(daysResolved != -1)
            {
               result += '<br><i>Closed ' + uroUtils.ParseDaysAgo(daysResolved) + ' ';
               if(ureq.attributes.resolvedBy != null)
               {
                  result += ' by '+uroUtils.GetUserNameAndRank(ureq.attributes.resolvedBy);
               }

               if((ureq.attributes.open === true) && (ureq.attributes.resolvedOn != null))
               {
                  result += '<br>Reopened by Waze';
               }
               result += '</i>';
            }
         }
         uroPopup.result += result;
         uroPopup.UMPExtras(ureq);
      }
   },
   PUR: function()
   {
      if(uroMarkers?.elm?.style == undefined)
      {
         return;
      }

      let result = '';

      uroPopup.unstackedX = uroUtils.ParsePxString(uroMarkers.elm.style.left);
      uroPopup.unstackedY = uroUtils.ParsePxString(uroMarkers.elm.style.top);

      if(uroShownFID != uroMarkers.id)
      {
         // check for stacking...
         uroCheckStacking(uroMarkers.type, uroMarkers.id, uroPopup.unstackedX, uroPopup.unstackedY);
      }

      if(uroUtils.GetCBChecked('_cbInhibitPUPopup') === false)
      {
         let ureq = W.model.venues.objects[uroMarkers.id];

         uroPopup.newPopupType = uroMarkers.type;
         if(uroMousedOverMapComment !== null)
         {
            uroDBG.AddLog('setting uroMousedOverOtherObjectWithinMapComment for PUR highlight');
            uroMousedOverOtherObjectWithinMapComment = true;
         }

         uroPopup.isPlaceUpdate = true;

         uroFID = uroMarkers.id;

         if(uroMarkers.type == uroLayers.ID.PUR)
         {
            uroDBG.AddLog('building popup for placeUpdate '+uroMarkers.id);
         }
         else if(uroMarkers.type == uroLayers.ID.RPUR)
         {
            uroDBG.AddLog('building popup for residentialPlaceUpdate '+uroMarkers.id);
         }
         else
         {
            uroDBG.AddLog('building popup for parkingPlaceUpdate '+uroMarkers.id);
         }

         result = '<b>';
         if(ureq.attributes.name === '') result += 'Unnamed landmark';
         else result += ureq.attributes.name;
         result += '</b><br>';

         result += '<ul>';
         for(let idx = 0; idx < ureq.attributes.categories.length; idx++)
         {
            result += '<li>' + I18n.lookup("venues.categories." + ureq.attributes.categories[idx]);
         }
         result += '</ul>';

         if(ureq.attributes.residential === true)
         {
            result += '<i>Residential</i>';
         }

         let daysOld = uroUtils.GetPURAge(ureq);
         if(daysOld != -1)
         {
            result += '<br><i>Submitted '+uroUtils.ParseDaysAgo(daysOld)+'</i>';
         }

         uroPopup.result += result;
         uroPopup.UMPExtras(ureq);
      }
   },
   Venue: function()
   {
      if(uroUtils.GetCBChecked('_cbInhibitLandmarkPopup') === false)
      {
         let result = '';
         let navpointPos=new OpenLayers.LonLat();
         {
            if(uroPopup.renderIntent === 'highlight')
            {
               if(uroUtils.GetExtent().intersectsBounds(uroMarkers.obj.attributes.geometry.getBounds()))
               {
                  if(uroMousedOverMapComment !== null)
                  {
                     uroDBG.AddLog('setting uroMousedOverOtherObjectWithinMapComment for place highlight');
                     uroMousedOverOtherObjectWithinMapComment = true;
                  }

                  if(uroPopup.newPopupType === null)
                  {
                     uroFID = uroMarkers.obj.attributes.id;
                     uroDBG.AddLog('building popup for place '+uroFID);

                     navpointPos = uroGetVenueNavPoint(uroFID);
                     
                     result += '<b>';
                     if(uroMarkers.obj.attributes.name === '')
                     {
                        if(uroMarkers.obj.attributes.residential === true) result += '<i>Residential</i>';
                        else result += '<i>Unnamed</i>';
                     }
                     else result += uroUtils.Clickify(uroMarkers.obj.attributes.name, '');
                     if(uroMarkers.obj.attributes.externalProviderIDs.length > 0)
                     {
                        result += ' <i>(linked)</i>';
                     }
                     if(uroMarkers.obj.attributes.adLocked)
                     {
                        result += ' <i>(AdLocked)</i>';
                     }
                     result += '</b><br>';
                     if(uroMarkers.obj.attributes.brand !== null)
                     {
                        result += '<i>Brand: ' + uroMarkers.obj.attributes.brand + '</i><br>';
                     }
                     let vDesc = uroMarkers.obj.attributes.description;
                     if(vDesc !== '')
                     {
                        result += '"<i>' + uroUtils.Clickify(vDesc, '') + '</i>"<br>';
                     }

                     let userLock = uroMarkers.obj.attributes.lockRank;
                     result += '<b>Lock: </b>' + (userLock+1);

                     result += '<hr>';
                     result += uroGetAddress(uroMarkers.obj.attributes.streetID, uroMarkers.obj.attributes.houseNumber, false, false, false);
                     result += '<ul>';
                     for(let idx = 0; idx < uroMarkers.obj.attributes.categories.length; idx++)
                     {
                        result += '<li>' + I18n.lookup("venues.categories." + uroMarkers.obj.attributes.categories[idx]);
                     }
                     result += '</ul>';

                     let npLink = document.location.href;
                     let npLayers = '';
                     npLink = npLink.substr(0,npLink.indexOf('?zoomLevel'));
                     npLink += '?zoomLevel=17&lat='+navpointPos.lat+'&lon='+navpointPos.lon+npLayers;

                     let targetTab = "_uroTab_" + Math.round(Math.random()*1000000);
                     result += '<hr>Jump to nav point: <a href="'+npLink+'" id="_openInNewTab" target="'+targetTab+'">in new tab</a> - ';
                     uroPopup.hasOpenInNewTabLink = true;
                     result += '<a href="#" id="_recentreSession">in this tab</a>';
                     uroPopup.hasRecentreSessionLink = true;

                     uroPopup.newPopupType = 'venue';
                     uroPopup.isVenue = true;

                     uroPopup.result += result;
                     uroPopup.Show();
                  }
                  else
                  {
                     let otherID = uroMarkers.obj.attributes.id;
                     uroDBG.AddLog('venue '+otherID+' is also highlighted');
                  }
               }
               else
               {
                  uroDBG.AddLog('landmark '+uroFID+' has renderIntent==highlight but is offscreen... blocking popup');
               }
            }
         }
      }
   },
   Camera: function()
   {
      if(uroUtils.GetCBChecked('_cbInhibitCamPopup') === false)
      {
         let result = '';
         let ureq = uroMarkers.obj;

         if(uroPopup.renderIntent === "highlight")
         {
            if(uroMousedOverMapComment !== null)
            {
               uroDBG.AddLog('setting uroMousedOverOtherObjectWithinMapComment for camera highlight');
               uroMousedOverOtherObjectWithinMapComment = true;
            }
            uroPopup.newPopupType = 'camera';
            uroFID = uroMarkers.id;
            uroDBG.AddLog('building popup for camera '+uroFID);
            if(I18n.lookup("edit.camera.fields.type") === undefined)
            {
               result += '<b>Camera: ' + ureq.TYPES[ureq.attributes.type] + '</b>';
            }
            else
            {
               result += '<b>Camera: ' + I18n.lookup("edit.camera.fields.type." + ureq.attributes.type) + '</b>';
            }
            result += '<br>';
            result += '<b>ID:</b> ' + uroFID + ' - ';
            result += uroPopup.GetFormattedLocks(uroMarkers.obj.attributes);
            result += '<br>';

            result += '<b>Created by</b> ';
            let userID;

            if(W.model.users.getByIds([ureq.attributes.createdBy])[0] != null)
            {
               userID = ureq.attributes.createdBy;
               result += uroUtils.GetUserNameAndRank(userID);
            }
            else result += 'unknown';
            result += ', ';
            let camAge = uroUtils.GetCameraAge(ureq,1);
            if(camAge != -1)
            {
               result += uroUtils.ParseDaysAgo(camAge);
            }
            else result += 'unknown days ago';
            result += '<br><b>Updated by</b> ';
            if(W.model.users.getByIds([ureq.attributes.updatedBy])[0] != null)
            {
               userID = ureq.attributes.updatedBy;
               result += uroUtils.GetUserNameAndRank(userID);
            }
            else result += 'unknown';
            result += ', ';
            camAge = uroUtils.GetCameraAge(ureq,0);
            if(camAge != -1)
            {
               result += uroUtils.ParseDaysAgo(camAge);
            }
            else result += 'unknown days ago';

            if(ureq.attributes.type !== 4)
            {
               result += '<br><b>Speed data:</b> ';
               result += uroUtils.GetLocalisedSpeedString(ureq.attributes.speed);
               if(uroIsCamSpeedValid(ureq) === false)
               {
                  result += ' (differs to nearest segment)';
               }
            }
            result += '<hr><ul>';
            if(uroOWL.IsCamOnWatchList(uroFID) != -1)
            {
               result += '<li><a href="#" id="_updatewatchlist">Update watchlist entry</a>';
               result += '<li><a href="#" id="_removefromwatchlist">Remove from watchlist</a>';
               uroPopup.hasUpdateWatchLink = true;
               uroPopup.hasRemoveWatchLink = true;
            }
            else
            {
               result += '<li><a href="#" id="_addtowatchlist">Add to watchlist</a>';
               uroPopup.hasAddWatchLink = true;
            }
            if(ureq.attributes.permissions !== 0)
            {
               result += '<li><a href="#" id="_deleteobject">Delete Camera</a>';
               uroPopup.hasDeleteLink = true;
            }
            result += '</ul>';
            uroPopup.result += result;
            uroPopup.Show();
         }
      }   
   },
   Comment: function()
   {
      if(uroUtils.GetCBChecked('_cbInhibitMapCommentPopup') === false)
      {
         if(uroMCLayer.name !== 'mapComments')
         {
            uroInit.WazeBits();
         }
         if(uroMCLayer !== null)
         {
            let result = '';

            if(uroPopup.renderIntent == 'highlight')
            {
               let moAttrs = uroMarkers.obj.attributes;
               if(uroUtils.GetExtent().intersectsBounds(moAttrs.geometry.getBounds()))
               {
                  if(uroPopup.newPopupType === null)
                  {
                     if((uroMousedOverMapComment === moAttrs.id) && (uroMousedOverOtherObjectWithinMapComment === true))
                     {
                        uroDBG.AddLog('inhibit popup for map comment '+uroMousedOverMapComment);
                     }
                     else
                     {
                        uroMousedOverOtherObjectWithinMapComment = false;
                        if(moAttrs.geometry.id.indexOf('Polygon') !== -1)
                        {
                           // only capture ID for area comments...
                           uroMousedOverMapComment = moAttrs.id;
                        }
                        uroFID = moAttrs.id;
                        uroDBG.AddLog('building popup for map comment '+uroFID);

                        result += '<b>';
                        if(moAttrs.subject === '')
                        {
                           result += '<i>No subject</i>';
                        }
                        else result += uroUtils.Clickify(moAttrs.subject, '');
                        result += '</b><br>';
                        result += uroUtils.Clickify(moAttrs.body, '<br>');

                        let mcDaysOld = uroUtils.GetMCAge(moAttrs, 0, false);
                        let mcSubmittedTS = uroUtils.GetMCAge(moAttrs, 0, true);
                        if(mcSubmittedTS != -1)
                        {
                           mcSubmittedTS = uroUtils.GetDateTimeString(mcSubmittedTS);
                        }
                        if(mcDaysOld != -1)
                        {
                           result += '<i>Submitted ' + uroUtils.ParseDaysAgo(mcDaysOld) + ' ';
                           if(mcSubmittedTS != -1) result += '(' + mcSubmittedTS + ') ';
                           if(moAttrs.createdBy != null)
                           {
                              result += ' by '+uroUtils.GetUserNameAndRank(moAttrs.createdBy);
                           }
                           result += '</i><br>';
                        }
                        mcDaysOld = uroUtils.GetMCAge(moAttrs, 1, false);
                        mcSubmittedTS = uroUtils.GetMCAge(moAttrs, 1, true);
                        if(mcSubmittedTS != -1)
                        {
                           mcSubmittedTS = uroUtils.GetDateTimeString(mcSubmittedTS);
                        }
                        if(mcDaysOld != -1)
                        {
                           result += '<i>Updated ' + uroUtils.ParseDaysAgo(mcDaysOld) + ' ';
                           if(mcSubmittedTS != -1) result += '(' + mcSubmittedTS + ') ';
                           if(moAttrs.createdBy != null)
                           {
                              result += ' by '+uroUtils.GetUserNameAndRank(moAttrs.updatedBy);
                           }
                           result += '</i><br>';
                        }

                        mcDaysOld = uroUtils.GetMCAge(moAttrs,2,false);
                        mcSubmittedTS = uroUtils.GetMCAge(moAttrs,2,true);
                        if(mcDaysOld != -1)
                        {
                           result += '<i>Expires ' + uroUtils.ParseDaysToGo(mcDaysOld) + ' ';
                           result += '(' + uroUtils.GetDateTimeString(mcSubmittedTS) +')</i><br>';
                        }

                        let mcHasMyComments = false;
                        let mcNComments = moAttrs.conversation.length;
                        if(mcNComments > 0)
                        {
                           for(let j=0; j<mcNComments; j++)
                           {
                              if(moAttrs.conversation[j].userID == uroUserID)
                              {
                                 mcHasMyComments = true;
                                 break;
                              }
                           }
                        }
                        result += '<br>' + mcNComments +' comment';
                        if(mcNComments != 1) result += 's';
                        if((mcHasMyComments === false) && (mcNComments > 0)) result += ' (none by me)';

                        // add "ignore for this session" link
                        result += '<br><a href="#" id="_addtoignore">Hide for this session</a>';
                        uroPopup.hasIgnoreLink = true;

                        uroPopup.newPopupType = 'map_comment';
                        uroPopup.isMapComment = true;

                        uroPopup.result += result;
                        uroPopup.Show();
                     }
                  }
                  else
                  {
                     let mcOtherID = moAttrs.id;
                     uroDBG.AddLog('map comment '+mcOtherID+' is also highlighted');
                  }
               }
               else
               {
                  uroDBG.AddLog('map comment '+uroFID+' has renderIntent==highlight but is offscreen... blocking popup');
               }
            }
         }
      }
   },
   AddClosureRow: function(rcObj)
   {
      let startDate = rcObj.attributes.startDate;
      let endDate = "unknown";
      if(rcObj.attributes.endDate !== null)
      {
         endDate = rcObj.attributes.endDate;
      }
      let provider = "---";
      if(rcObj.attributes.provider !== null)
      {
         provider = rcObj.attributes.provider;
      }
      else if(rcObj.attributes.createdBy !== null)
      {
         provider = uroUtils.GetUserNameAndRank(rcObj.attributes.createdBy);
      }
      let reason = "---";
      if(rcObj.attributes.reason !== null)
      {
         reason = rcObj.attributes.reason;
      }
      let mte = "---";
      if(rcObj.attributes.eventId !== null)
      {
         try
         {
            mte = W.model.majorTrafficEvents.objects[rcObj.attributes.eventId].attributes.names[0].value;
         }
         catch(err)
         {
         }
      }
   
      let startOffset = uroGetRTCOffset(rcObj.attributes.startDate);
      let duration = uroGetRTCDuration(rcObj);
      let durationStr = '';

      if(duration > 1)
      {
         durationStr = I18n.lookup("datetime.distance_in_words.x_days").other;
         durationStr = durationStr.replace("%{count}", duration);
      }
      else
      {
         durationStr = I18n.lookup("datetime.distance_in_words.x_days").one;
         if(duration === 0)
         {
            durationStr = "<" + durationStr;
         }
      }
   
      let state = uroGetRTCState(rcObj);
      let status = uroGetRTCStateText(rcObj);
      let bgCol = '';
      if(state === uroEnums.SRTC.EXPIRED)
      {
         bgCol = '#A0A0A0';
      }
      else if(state === uroEnums.SRTC.ACTIVE)
      {
         bgCol = '#FFFFFF';
      }
      else if(state === uroEnums.SRTC.FUTURE)
      {
         // For future closures, override the default status text with an indication
         // of how many days until the start of the closure
         if(startOffset == 0)
         {
            status = I18n.lookup("date.today");
            status = status.charAt(0).toUpperCase() + status.slice(1);
         }
         else if(startOffset == 1)
         {
            status = I18n.lookup("datetime.distance_in_words.x_days").one;
         }
         else
         {
            status = I18n.lookup("datetime.distance_in_words.x_days").other;
            status = status.replace("%{count}", startOffset);
         }
         bgCol = '#C0C0C0';
      }
   
      let result = '';
      result += '<tr bgcolor="' + bgCol + '">';
      result += '<td nowrap>' + status + '</td>';
      result += '<td nowrap>' + startDate + ' to ' + endDate + ' (' + durationStr + ')</td>';
      result += '<td nowrap>' + provider + '</td>';
      result += '<td nowrap>' + reason + '</td>';
      result += '<td nowrap>' + mte + '</td>';
      result += '</td></tr>';
      return result;
   },
   AddClosureDetails: function(cTypes, addType, addDesc)
   {
      let retval = '';
      if((cTypes & addType) === addType)
      {
         retval += '<tr><td colspan=4><b>' + addDesc + ':</b></td></tr>';
         for(let closure in uroRTCObjs)
         {
            if(uroRTCObjs.hasOwnProperty(closure))
            {
               let cObj = uroRTCObjs[closure];
               if(cObj.direction === addType)
               {
                  retval += uroPopup.AddClosureRow(cObj);
               }
            }
         }
      }
      return retval;
   },
   Segment: function()
   {
      if(uroUtils.GetCBChecked('_cbInhibitSegPopup') === false)
      {
         {
            if(uroUtils.GetExtent().intersectsBounds(uroMarkers.obj.attributes.geometry.getBounds()))
            {
               let result = '';
               let doPopUp = false;
               let restObj;

               if(uroMousedOverMapComment !== null)
               {
                  uroDBG.AddLog('setting uroMousedOverOtherObjectWithinMapComment for segment highlight');
                  uroMousedOverOtherObjectWithinMapComment = true;
               }

               let streetID = uroMarkers.obj.attributes.primaryStreetID;
               if(streetID !== null)
               {
                  // generic segment data
                  if(uroUtils.GetCBChecked('_cbInhibitSegGenericPopup') === false)
                  {
                     doPopUp = true;
                     uroDBG.AddLog('building popup for segment '+streetID);

                     let isToll = ((uroMarkers.obj.attributes.fwdToll == true) || (uroMarkers.obj.attributes.revToll == true));
                     result += uroGetAddress(streetID, null, true, false, isToll);

                     if(uroMarkers.obj.attributes.streetIDs.length > 0)
                     {
                        // list any alternate names
                        result += '<br>Alternate names:<br>';
                        for(let i = 0; i < uroMarkers.obj.attributes.streetIDs.length; ++i)
                        {
                           result += '&nbsp;<i>' + W.model.streets.objects[uroMarkers.obj.attributes.streetIDs[i]].attributes.name + ', ';
                           let cityName = "";
                           if(W.model.cities.objects[W.model.streets.objects[uroMarkers.obj.attributes.streetIDs[i]].attributes.cityID] != undefined)
                           {
                              cityName = W.model.cities.objects[W.model.streets.objects[uroMarkers.obj.attributes.streetIDs[i]].attributes.cityID].attributes.name;
                           }
                           if(cityName != "")
                           {
                              result += cityName;
                           }
                           else
                           {
                              result += ' no city';
                           }
                           result += '</i><br>';
                        }
                        result += '<br>';
                     }

                     result += '<b>ID: </b>'+uroMarkers.obj.attributes.id+' - ';
                     result += uroPopup.GetFormattedLocks(uroMarkers.obj.attributes);
                     result += ' - ';

                     let level = uroMarkers.obj.attributes.level;
                     result += '<b>' + I18n.lookup("edit.segment.fields.level") +': </b>';
                     if(level == 0)
                     {
                        result += I18n.lookup("edit.segment.levels")[0];
                     }
                     else
                     {
                        result += level;
                     }
                     result += '<br>';

                     let leBy = uroMarkers.obj.attributes.updatedBy;
                     let leOn = uroMarkers.obj.attributes.updatedOn;
                     if(leOn == null)
                     {
                        leBy = uroMarkers.obj.attributes.createdBy;
                        leOn = uroMarkers.obj.attributes.createdOn;
                     }
                     result += "<b>Last edit by</b> " + uroUtils.GetUserNameAndRank(leBy) + " <b>on</b> " + uroUtils.GetDateTimeString(leOn) + "<br><br>";

                     let fwdSpeed = uroMarkers.obj.attributes.fwdMaxSpeed;
                     let revSpeed = uroMarkers.obj.attributes.revMaxSpeed;
                     let fwdLanes = uroMarkers.obj.attributes.fwdLaneCount;
                     let revLanes = uroMarkers.obj.attributes.revLaneCount;
                     let fwdWidth = 'Not set';
                     let revWidth = 'Not set';
                     if(uroMarkers.obj.attributes.fromLanesInfo != null)
                     {                     
                        fwdWidth = uroGetLengthString(uroMarkers.obj.attributes.fromLanesInfo.laneWidth);
                     }
                     if(uroMarkers.obj.attributes.toLanesInfo != null)
                     {
                        revWidth = uroGetLengthString(uroMarkers.obj.attributes.toLanesInfo.laneWidth);
                     }
                     let fwdUnverified = uroMarkers.obj.attributes.fwdMaxSpeedUnverified;
                     let revUnverified = uroMarkers.obj.attributes.revMaxSpeedUnverified;
                     let fwdASC = ((uroMarkers.obj.attributes.fwdFlags & 1) === 1);
                     let revASC = ((uroMarkers.obj.attributes.revFlags & 1) === 1);
                     let roadType = uroMarkers.obj.attributes.roadType;
                     let verifyLimits = true;
                     if((roadType === 17) || (roadType === 20))
                     {
                        verifyLimits = false;
                     }

                     result += '<table border=1><tr><th>Dir</th><th>Speed</th><th>ASC</th><th>Lanes</th><th>Width</th></tr>';
                     if(uroMarkers.obj.attributes.fwdDirection)
                     {
                        result += '<tr><td><b>A-B</b></td><td>'+uroUtils.GetLocalisedSpeedString(fwdSpeed)+'</td><td>';
                        if(fwdASC == true)
                        {
                           result += 'Yes';
                        }
                        else
                        {
                           result += 'No';
                        }
                        result += '</td><td>';
                        if(fwdLanes > 0)
                        {
                           result += fwdLanes;
                        }
                        else
                        {
                           result += 'Not set';
                        }
                        result += '</td><td>'+fwdWidth+'</td></tr>';
                     }
                     if(uroMarkers.obj.attributes.revDirection)
                     {
                        result += '<tr><td><b>B-A</b></td><td>'+uroUtils.GetLocalisedSpeedString(revSpeed)+'</td><td>';
                        if(revASC == true)
                        {
                           result += 'Yes';
                        }
                        else
                        {
                           result += 'No';
                        }
                        result += '</td><td>';
                        if(revLanes > 0)
                        {
                           result += revLanes;
                        }
                        else
                        {
                           result += 'Not set';
                        }
                        result += '</td><td>'+revWidth+'</td></tr>';
                     }
                     result += '</table>';

                     if((uroMarkers.obj.attributes.fwdDirection) && (uroMarkers.obj.attributes.revDirection) && (fwdSpeed != revSpeed) && (!fwdUnverified) && (!revUnverified))
                     {
                        result += '<br>Two-way segment has different verified speed limits...';
                     }
                  }

                  // segment restrictions
                  if(uroMarkers.obj.attributes.restrictions.length > 0)
                  {
                     result += '<br><table border=1>';
                     doPopUp = true;
                     let fwdResult = '<tr><td colspan=13><b>A-B restrictions:</b></td></tr>';
                     let revResult = '<tr><td colspan=13><b>B-A restrictions:</b></td></tr>';
                     let bothResult = '<tr><td colspan=13><b>Two-way restrictions:</b></td></tr>';

                     let nABRestrictions = 0;
                     let nBARestrictions = 0;
                     let nBothRestrictions = 0;
                     for(let idx = 0; idx < uroMarkers.obj.attributes.restrictions.length; idx++)
                     {
                        restObj = uroMarkers.obj.attributes.restrictions[idx];
                        if(restObj._direction === "FWD")
                        {
                           nABRestrictions++;
                           fwdResult += uroFormatRestriction(restObj);
                        }
                        else if(restObj._direction === "REV")
                        {
                           nBARestrictions++;
                           revResult += uroFormatRestriction(restObj);
                        }
                        else if(restObj._direction === "BOTH")
                        {
                           nBothRestrictions++;
                           bothResult += uroFormatRestriction(restObj);
                        }
                        else
                        {
                           uroDBG.AddLog("unknown restriction direction...");
                        }
                     }
                     if(nABRestrictions > 0)
                     {
                        result += fwdResult;
                     }
                     if(nBARestrictions > 0)
                     {
                        result += revResult;
                     }
                     if(nBothRestrictions > 0)
                     {
                        result += bothResult;
                     }
                     result += '</table>';
                  }
                  
                  if(uroLayers.layers[uroLayers.ID.RTC].l.getVisibility() === true)
                  {
                     let closureTypes = uroGetSelectedSegmentRTCs(uroMarkers.obj.attributes.id);
                     if(closureTypes !== uroEnums.DRTC.NONE)
                     {
                        result += '<br><table border=1 width="100%">';

                        result += uroPopup.AddClosureDetails(closureTypes, uroEnums.DRTC.SEG_AB, "A-B closures");
                        result += uroPopup.AddClosureDetails(closureTypes, uroEnums.DRTC.SEG_BA, "B-A closures");
                        result += uroPopup.AddClosureDetails(closureTypes, uroEnums.DRTC.SEG_BI, "Two-way closures");
                        result += uroPopup.AddClosureDetails(closureTypes, uroEnums.DRTC.TURN_OUT, "Outbound turn closures");
                        result += uroPopup.AddClosureDetails(closureTypes, uroEnums.DRTC.TURN_IN, "Inbound turn closures");
                        
                        doPopUp = true;
                        result += '</table>';
                     }
                  }

                  if(doPopUp === true)
                  {
                     uroFID = uroMarkers.obj.attributes.id;
                     uroPopup.newPopupType = 'segment_restriction';
                  }
               }
               uroPopup.result += result;
               uroPopup.Show();
            }
            else
            {
               uroDBG.AddLog('segment '+uroFID+' has renderIntent==highlight but is offscreen... blocking popup');
            }
         }
      }
   },
   Node: function()
   {
      if(uroUtils.GetCBChecked('_cbInhibitNodesPopup') === false)
      {
         let result = '';
         let ureq = W.model.nodes.objects[uroMarkers.id];
         if(ureq === undefined)
         {
            uroMarkers.id = null;
         }
         else
         {
            if(uroMousedOverMapComment !== null)
            {
               uroDBG.AddLog('setting uroMousedOverOtherObjectWithinMapComment for node highlight');
               uroMousedOverOtherObjectWithinMapComment = true;
            }
            uroPopup.newPopupType = 'node';
            uroFID = uroMarkers.id;
            uroDBG.AddLog('building popup for node '+uroFID);
            result += '<b>Node: ' + uroFID + '</b><br>';

            let nodeSegRTCs = [];
            for(let k=0; k<ureq.attributes.segIDs.length; k++)
            {
               let nodeSegID = ureq.attributes.segIDs[k];
               let nodeSegObj = W.model.segments.objects[nodeSegID];
               if(nodeSegObj !== undefined)
               {
                  let nodeStreetID = nodeSegObj.attributes.primaryStreetID;
                  result += uroGetAddress(nodeStreetID, null, false, true, false);
               }
               uroGetSelectedSegmentRTCs(nodeSegID);
               nodeSegRTCs = nodeSegRTCs.concat(uroRTCObjs);
            }
         }
         uroPopup.result += result;
         uroPopup.Show();
      }   
   },
   UMPExtras: function(ureq)
   {
      // add "open new WME tab" link
      let urPos=new OpenLayers.LonLat();
      if(uroPopup.isPlaceUpdate)
      {
         urPos.lon = ureq.getOLGeometry().getCentroid().x;
         urPos.lat = ureq.getOLGeometry().getCentroid().y;
      }
      else
      {
         if(ureq.attributes.geometry.realX === undefined)
         {
            urPos.lon = ureq.attributes.geometry.x;
            urPos.lat = ureq.attributes.geometry.y;
         }
         else
         {
            urPos.lon = ureq.attributes.geometry.realX;
            urPos.lat = ureq.attributes.geometry.realY;
         }
      }
      urPos = uroUtils.ConvertMercatorToWGS84(urPos);
      let urLink = document.location.href;
      let urLayers = '';
      urLink = urLink.substr(0,urLink.indexOf('?zoomLevel'));
      urLink += '?zoomLevel=17&lat='+urPos.lat+'&lon='+urPos.lon+urLayers;

      if(uroPopup.isUR) urLink += '&mapUpdateRequest='+uroMarkers.id;
      else if(uroPopup.isTurnProb) urLink += '&showturn='+uroMarkers.id+'&endshow';
      else if(uroPopup.isProblem) urLink += '&mapProblem='+uroMarkers.id;
      else if(uroPopup.isPlaceUpdate)
      {
         if(uroMarkers.type == uroLayers.ID.PUR)
         {
            urLink += '&showpur='+uroMarkers.id+'&endshow';
         }
         else
         {
            urLink += '&showppur='+uroMarkers.id+'&endshow';
         }
      }

      let targetTab = "_uroTab_" + Math.round(Math.random()*1000000);
      uroPopup.result += '<hr><ul><li><a href="'+urLink+'" id="_openInNewTab" target="'+targetTab+'">Open in new tab</a> - ';
      uroPopup.hasOpenInNewTabLink = true;
      uroPopup.result += '<a href="#" id="_recentreSession">centre in current tab</a>';
      uroPopup.hasRecentreSessionLink = true;

      // add "open new livemap tab" link
      let lmLink = null;
      if(document.getElementById("livemap-link") != null)
      {
         uroDBG.AddLog('Livemap link in livemap-link id element');
         lmLink = document.getElementById("livemap-link").href;
      }
      else if(document.getElementsByClassName("livemap-link") != null)
      {
         uroDBG.AddLog('Livemap link in livemap-link class element');
         lmLink = document.getElementsByClassName("livemap-link")[0].href;
      }
      else
      {
         uroDBG.AddLog('Livemap link not found...');
      }
      if(lmLink !== null)
      {
         let zpos = lmLink.indexOf('?');
         if(zpos > -1) lmLink = lmLink.substr(0,zpos);
         lmLink += '?zoom=17&lat='+urPos.lat+'&lon='+urPos.lon+'&layers=BTTTT';
         uroPopup.result += '<li><a href="'+lmLink+'" target="_lmTab">Open in new livemap tab</a>';
      }
      if(!uroPopup.isPlaceUpdate)
      {
         // add "ignore for this session" link
         uroPopup.result += '<li><a href="#" id="_addtoignore">Hide for this session</a></ul>';
         uroPopup.hasIgnoreLink = true;
      }

      uroPopup.Show();
   },
   Preamble: function()
   {
      let retval = true;

      if
      (
         (uroMarkers.type === null) &&
         (uroPopup.timer === 0)
      )
      {
         if(uroPopup.shown === true)
         {
            uroPopup.Hide();
         }
         uroMousedOverMapComment = null;
      
         retval = false;
      }
   
      if(retval === true)
      {
         if(uroMTEMode) 
         {
            retval = false;
         }
      }

      if(retval === true)
      {
         if(!uroInit.initialised) 
         {
            retval = false;
         }
      }

      if(retval === true)
      {
         if((uroMouseIsDown) && (uroMarkers.mouseButtons === 0))
         {
            uroDBG.AddLog('trapped erroneous mousedown state');
            uroMouseIsDown = false;
         }
         if(uroMouseIsDown)
         {
            retval = false;
         }
      }

      if(retval === true)
      {
         if(OpenLayers === null)
         {
            if(uroNullOpenLayers === false)
            {
               uroDBG.AddLog('caught null OpenLayers');
               uroNullOpenLayers = true;
            }
            retval = false;
         }
         else
         {
            uroNullOpenLayers = false;
         }
      }

      if(retval === true)
      {
         if(uroLayers.layers[uroLayers.ID.UR].l === null)
         {
            if(uroNullURLayer === false)
            {
               uroDBG.AddLog('caught null UR layer');
               uroNullURLayer = true;
            }
            retval = false;
         }
         else
         {
            uroNullURLayer = false;
         }
      }

      if(retval === true)
      {
         if(uroLayers.layers[uroLayers.ID.MP].l === null)
         {
            if(uroNullProblemLayer === false)
            {
               uroDBG.AddLog('caught null problem layer');
               uroNullProblemLayer = true;
            }
            retval = false;
         }
         else
         {
            uroNullProblemLayer = false;
         }
      }

      if(retval === true)
      {
         if(uroUtils.GetCBChecked('_cbMasterEnable') === false)
         {
            retval = false;
         }
      }

      if(retval === true)
      {
         if(uroTestPointerOutsideMap(uroMarkers.clientX, uroMarkers.clientY))
         {
            retval = false;
         }
      }


      if(retval === true)
      {
         uroPopup.mouseX = uroMarkers.mouseX;
         uroPopup.mouseY = uroMarkers.mouseY;
         
         uroPopup.result = '';
         uroPopup.hasIgnoreLink = false;
         uroPopup.hasDeleteLink = false;
         uroPopup.hasAddWatchLink = false;
         uroPopup.hasRemoveWatchLink = false;
         uroPopup.hasUpdateWatchLink = false;
         uroPopup.hasRecentreSessionLink = false;
         uroPopup.hasOpenInNewTabLink = false;
         uroPopup.isVenue = false;
         uroPopup.isMapComment = false;
         uroPopup.newPopupType = null;
         uroPopup.ureqID = null;
         uroPopup.isUR = false;
         uroPopup.isProblem = false;
         uroPopup.isTurnProb = false;
         uroPopup.isPlaceUpdate = false;
         uroPopup.renderIntent = uroGetFeatureRenderIntent(uroMarkers.obj);

         uroInit.WazeBits();

         let mouseLonLat = W.map.getLonLatFromViewPortPx(new OpenLayers.Pixel(uroPopup.mouseX, uroPopup.mouseY));
         let mousePoint = new OpenLayers.Geometry.Point(mouseLonLat.lon, mouseLonLat.lat);
         if(uroMousedOverMapComment !== null)
         {
            if(W.model.mapComments.objects[uroMousedOverMapComment] === undefined)
            {
               uroDBG.AddLog('clearing uroMousedOverMapComment: object no longer exists in current map view');
               uroMousedOverMapComment = null;
            }
            else if(W.model.mapComments.objects[uroMousedOverMapComment].attributes.geometry.containsPoint(mousePoint) === false)
            {
               uroDBG.AddLog('clearing uroMousedOverMapComment: pointer no longer within comment boundary');
               uroMousedOverMapComment = null;
            }
         }
      
         let popupXOffset = document.getElementById('editor-container').getBoundingClientRect().x;
         let popupYOffset = $(document.getElementById("WazeMap")).offset().top;
         uroPopup.pX = uroPopup.mouseX + popupXOffset + 10;
         uroPopup.pY = uroPopup.mouseY + popupYOffset;
      }

      return retval;
   },
   Show: function()
   {
      if(uroPopup.suppressed === false)
      {
         if((uroFID != uroShownFID) || (uroPopup.newPopupType != uroPopup.shownType))
         {
            if(uroFID != uroShownFID) uroDBG.AddLog('FID mismatch, show popup: '+uroFID+'/'+uroShownFID);
            else uroDBG.AddLog('Popup type mismatch: '+uroPopup.newPopupType+'/'+uroPopup.shownType);
            uroShownFID = uroFID;
            uroPopup.shownType = uroPopup.newPopupType;
            uroPopup.shown = false;
         }
         if(uroPopup.shown === false)
         {
            uroPopup.shown = true;
            uroDiv.style.height = "auto";
            uroDiv.style.width = "auto";
            uroDiv.style.overflow = "auto";
            uroDiv.innerHTML = uroUtils.ModifyHTML(uroPopup.result);
   
            if((uroFID != -1) && (uroPopup.hasIgnoreLink === true))
            {
               uroUtils.AddEventListener('_addtoignore','click', uroIgnore.Add, true);
            }
            if(uroPopup.hasDeleteLink === true)
            {
               uroUtils.AddEventListener('_deleteobject','click', uroDeleteObject, true);
            }
            if(uroPopup.hasRemoveWatchLink === true)
            {
               uroUtils.AddEventListener('_removefromwatchlist','click', uroOWL.RemoveCamFromWatchList, true);
            }
            if(uroPopup.hasAddWatchLink === true)
            {
               uroUtils.AddEventListener('_addtowatchlist','click', uroOWL.AddCamToWatchList, true);
            }
            if(uroPopup.hasUpdateWatchLink === true)
            {
               uroUtils.AddEventListener('_updatewatchlist','click', uroOWL.UpdateCamWatchList, true);
            }
            if(uroPopup.hasOpenInNewTabLink === true)
            {
               uroUtils.AddEventListener('_openInNewTab','mouseup', uroOpenNewTab, true);
            }
            if(uroPopup.hasRecentreSessionLink === true)
            {
               if(uroPopup.isUR) uroUtils.AddEventListener('_recentreSession', 'click', uroRecentreSessionOnUR, true);
               else if((uroPopup.isProblem)||(uroPopup.isTurnProb)) uroUtils.AddEventListener('_recentreSession', 'click', uroRecentreSessionOnMP, true);
               else if(uroPopup.isPlaceUpdate)
               {
                  if(uroPopup.newPopupType == uroLayers.ID.PUR)
                  {
                     uroUtils.AddEventListener('_recentreSession', 'click', uroRecentreSessionOnPUR, true);
                  }
                  else
                  {
                     uroUtils.AddEventListener('_recentreSession', 'click', uroRecentreSessionOnPPUR, true);
                  }
               }
               else if(uroPopup.isVenue) uroUtils.AddEventListener('_recentreSession', 'click', uroRecentreSessionOnVenueNavPoint, true);
            }
   
   
            // restrict the popup width to be no wider than just under half the window width to avoid it
            // completely overlapping the marker it's associated with - by keeping it to just below half
            // the window width we guarantee that it'll fit either to the left or the right of the marker
            // no matter how far across the screen the marker is located...
            let rw = parseInt(uroDiv.clientWidth);
            if(rw > (window.innerWidth * 0.45))
            {
               rw = (window.innerWidth * 0.45);
               uroDiv.style.width = rw+'px';
            }
            // get the div height after any adjustment of the width above, to account for whatever content
            // reflow may have occurred as a result of reducing the width...
            let rh = parseInt(uroDiv.clientHeight);
   
            // similarly restrict the popup height to avoid it dropping of the bottom of the screen if a
            // segment has a bunch of closures/restrictions
            rh = parseInt(uroDiv.clientHeight);
            if(rh > (window.innerHeight * 0.80))
            {
               rh = (window.innerHeight * 0.80);
               uroDiv.style.height = rh+'px';
               uroDiv.style.overflow = 'scroll';
            }
   
            let origPopupX = uroPopup.pX;
            let movedLeft = false;
            if((uroPopup.pX + rw) > window.innerWidth)
            {
               // where the popup would be off the right hand side of the screen, move it completely over to the
               // other side of the mouse pointer
               uroPopup.pX -= (rw + 20);
               if(uroPopup.pX < 0) uroPopup.pX = 0;
               movedLeft = true;
            }
            if((uroPopup.pY + rh) > window.innerHeight)
            {
               // where the popup would be off the bottom of the screen, shift it up just far enough to be
               // fully visible
               uroPopup.pY -= (((uroPopup.pY + rh) - window.innerHeight) + 30);
            }
            if(uroPopup.pY < 0) uroPopup.pY = 0;
            uroDiv.style.top = uroPopup.pY+'px';
            uroDiv.style.left = uroPopup.pX+'px';
   
            if(movedLeft === true)
            {
               // after relocating the popup to the left of the pointer, it may end up resizing itself
               // which may then cause it to completely overlap the UR marker, so perform one more check
               // of the div width and nudge to the left if required...
               rw = parseInt(uroDiv.clientWidth);
               if(rw > (window.innerWidth * 0.45))
               {
                  rw = (window.innerWidth * 0.45);
                  uroDiv.style.width = rw+'px';
               }
               let nudgeDist = parseInt(20 + (uroPopup.pX + rw) - origPopupX);
               if((uroPopup.pX + rw + 30) >= origPopupX)
               {
                  uroDiv.style.left = parseInt(uroPopup.pX - nudgeDist)+'px';
               }
            }

            uroDBG.AddLog('display popup at '+uroPopup.pX+','+uroPopup.pY);
            
            uroDiv.style.visibility = 'visible';
            uroPopup.autoHideTimer = (uroUtils.GetElmValue('_inputPopupAutoHideTimeout') * 10);
         }
         uroPopup.timer = -1;
      }
   
   },
   Generate: function()
   {
      if(uroPopup.Preamble() === true)
      {
         if(uroMarkers.type === uroLayers.ID.UR) uroPopup.UR();
         else if(uroMarkers.type === uroLayers.ID.MP) uroPopup.MP();
         else if((uroMarkers.type === uroLayers.ID.PUR)||(uroMarkers.type === uroLayers.ID.PPUR)||(uroMarkers.type === uroLayers.ID.RPUR)) uroPopup.PUR();
         else if(uroMarkers.type === "venue") uroPopup.Venue();
         else if(uroMarkers.type === "cam") uroPopup.Camera();
         else if(uroMarkers.type === "comment") uroPopup.Comment();
         else if(uroMarkers.type === "segment") uroPopup.Segment();
         else if(uroMarkers.type === "node") uroPopup.Node();
         else
         {
            uroDBG.AddLog("request to generate popup for unknown type - " + uroMarkers.type);
         }
      }
   },
   Hide: function()
   {
      if(uroPopup.shown)
      {
         uroDiv.style.visibility = 'hidden';
         uroPopup.shown = false;
         uroPopup.timer = -2;
         uroShownFID = -1;
      }
      uroPopup.suppressed = false;
   },
   Suppress: function()
   {
      uroDiv.style.visibility = 'hidden';
      window.getSelection().removeAllRanges();
      uroPopup.suppressed = true;
   },
   MouseOver: function()
   {
      uroPopup.mouseIn = true;
   },
   MouseOut: function()
   {
      uroPopup.mouseIn = false;
   }   
};
const uroURExtras =  // UR marker enhancements
{
   urList : [],
   URListEntry: function(id, customType, hasMyComments, nComments, ageLastComment)
   {
      this.id = id;
      this.customType = customType;
      this.hasMyComments = hasMyComments;
      this.nComments = nComments;
      this.ageLastComment = ageLastComment;
   },
   AddToList: function(urID, customType, hasMyComments, nComments, ageLastComment)
   {
      uroURExtras.urList.push(new uroURExtras.URListEntry(urID, customType, hasMyComments, nComments, ageLastComment));
   },
   AddCommentCounts: function()
   {
      // Remove existing count elements before (re-)rendering, as WME doesn't automatically do this
      // for us now that the way it handles the marker layers has changed...
      let toRemove = document.querySelectorAll('#uroCommentCount');
      let trCount = toRemove.length;
      while(trCount > 0)
      {
         --trCount;
         toRemove[trCount].remove();
      }
   
      let addCommentCount = false;
      let nURs = uroURExtras.urList.length;

      if(uroUtils.GetCBChecked('_cbMasterEnable') === true)
      {
         addCommentCount = ((nURs > 0) && (uroUtils.GetCBChecked('_cbCommentCount') === true));
      }

      if(addCommentCount === true)
      {
         for(let i = 0; i < nURs; ++i)
         {
            let uObj = uroURExtras.urList[i];
            let nComments = uObj.nComments;
            let marker = uroGetMarker(uroLayers.ID.UR, uObj.id);
            if((marker !== null) && (nComments !== 0))
            {                  
               let mx = (marker.x.baseVal.value - 15);
               let my = (marker.y.baseVal.value + 35);
               let newSpan = '<foreignObject id="uroCommentCount" x="' + mx + '" y="' + my + '" width="100%" height="100%" style="pointer-events: none;">';

               newSpan += '<div style="position:absolute;';
               if(uObj.hasMyComments === true)
               {
                  newSpan += 'background-color: yellow;';
               }
               else
               {
                  newSpan += 'background-color: white;';
               }
               newSpan += 'font-size: 14px;font-weight: 800;';
               newSpan += 'border-width: 1px;border-style: solid;border-radius: 12px;padding-left: 4px;padding-right: 4px;z-index: 1;';
               newSpan += '">';
               newSpan += nComments + 'c ';
               newSpan += uObj.ageLastComment + 'd';
               newSpan += '</div>';

               newSpan += '</foreignObject>';
               marker.insertAdjacentHTML("afterend", newSpan);
            }
         }
      }   
   }   
};
const uroInit =   // script initialisation
{
   initialised : false,
   setupListeners : true,
   finalisingListenerSetup : false,

   WazeBitsAvailable: function()
   {
      uroDBG.AddLog('All WazeBits present and correct...');
      W.prefs.on('change:isImperial', uroInit.Initialise);
   
      uroLayers.Init();
   
      // To avoid creating multiple URO tabs in the event that the initialise code
      // gets called more than once, we now test for the presence of our tab and skip
      // if it's already present
      if(document.getElementById('uroTabHeader') === null)
      {
         uroInit.SetupUI();
      }
   },
   WaitForW: function()
   {
      if(document.getElementsByClassName("sandbox").length > 0)
      {
         uroDBG.AddLog('WME practice mode detected, script is disabled...');
         return;
      }
   
      if(window.W === undefined)
      {
         window.setTimeout(uroInit.WaitForW, 100);
         return;
      }
   
      if (W.userscripts?.state?.isReady)
      {
         uroInit.WazeBitsAvailable();
      } 
      else 
      {
         document.addEventListener("wme-ready", uroInit.WazeBitsAvailable, {once: true});
      }
   },
   AddInterceptor: function()
   {
      uroDBG.AddLog('Adding interceptor functions...');
   
      // add interceptor function for window.confirm(), to inhibit the closure deletion confirmation that would
      // pop up for each individual closure when we're using the delete all button - the user has already 
      // confirmed the delete action using our own requester
      let _confirm = window.confirm;
      window.confirm = function(msg)
      {
         let cm_delete_confirm = I18n.lookup("closures.delete_confirm").split('"')[0].trimRight(1);
   
         if(msg.indexOf(cm_delete_confirm) != -1)
         {
            uroDBG.AddLog('intercepted closure delete confirmation...');
            if(uroRTCClone.ConfirmDelete)
            {
               return _confirm(msg);
            }
            else
            {
               return true;
            }
         }
         else if(typeof(msg) == 'undefined')
         {
            uroDBG.AddLog('Intercepted blank confirmation...');
            return true;
         }
         else
         {
            return _confirm(msg);
         }
      };
      
      uroConfirmIntercepted = true;
   },
   Initialise: function()
   {
      uroInit.initialised = false;
      uroInit.setupListeners = true;
      uroInit.finalisingListenerSetup = false;
      uroInit.WaitForW();
   },
   WaitForControlsContainer: function()
   {
      if(document.getElementById('uroControlsContainer') === null)
      {
         window.setTimeout(uroInit.WaitForControlsContainer,500);
      }
      else
      {
         let updateURL = 'https://greasyfork.org/scripts/1952-uroverview-plus-uro';
   
         uroDBG.AddLog('adding controls to sidebar container...');
         let tabbyHTML = '<div id="uroTabHeader"><b><a href="'+updateURL+'" target="_blank">UROverview Plus</a></b> <label id="_uroVersion">'+uroRelease.version+'</label>';
         tabbyHTML += '<label id="_uroDebugMode">(dbg)</label>';
         tabbyHTML += '&nbsp;<input type="checkbox" id="_cbMasterEnable" checked>Enabled</input>';
         tabbyHTML += uroTabs.CreateTabHeaders();
         tabbyHTML += '</div>';
         document.getElementById('uroControlsContainer').innerHTML = uroUtils.ModifyHTML(tabbyHTML);
   
         uroTabs.CreateTabBodies();
   
         // other sidebar elements
         uroAMList = document.createElement('div');
         uroAMList.style.color = "#ffff00";
         uroCtrlHides = document.createElement('div');
   
         // Object watchlist tab
         {
            uroOWL.CWLGroups = [];
            uroTabs.PopulateOWL();
         }
   
         // footer for tabs container
         uroCtrlHides.id = 'uroCtrlHides';
         let tHTML = '<input type="button" id="_btnUndoLastHide" value="Undo last hide" />&nbsp;&nbsp;&nbsp;';
         tHTML += '<input type="button" id="_btnClearSessionHides" value="Undo all hides" /><p>';
         uroCtrlHides.innerHTML = uroUtils.ModifyHTML(tHTML);
   
         // footer for AM list
         uroAMList.id = 'uroAMList';
         
         uroTabs.CtrlTabs[0][uroTabs.FIELDS.SHOWFN]();
   
         window.addEventListener("beforeunload", uroConfig.SaveSettings, false);
      }
   },
   AddTab_API: async function()
   {
      let {tabLabel, tabPane} = W.userscripts.registerSidebarTab("URO+");
      tabLabel.innerText = "URO+";
      tabPane.innerHTML = uroUtils.ModifyHTML(uroControls.innerHTML);
      await W.userscripts.waitForElementConnected(tabPane);
      uroInit.CompleteUISetup();
   },
   CompleteUISetup: function()
   {
      uroDBG.AddLog('waiting for controls container...');
      uroInit.WaitForControlsContainer();
   
      uroGetProblemTypes();
      
      uroTabs.AddToDOM();
      document.getElementById('uroControlsContainer').appendChild(uroCtrlHides);
   
      uroInit.WazeBits();
   
      uroDiv.addEventListener("mouseover", uroPopup.MouseOver, false);
      uroDiv.addEventListener("mouseout", uroPopup.MouseOut, false);
   
      if(sessionStorage.UROverview_FID_IgnoreList === undefined) sessionStorage.UROverview_FID_IgnoreList = '';
      if(sessionStorage.UROverview_FID_WatchList === undefined) sessionStorage.UROverview_FID_WatchList = '';
      if(uroConfirmIntercepted === false) uroInit.AddInterceptor();
   
      uroUtils.AddStyle('urostyle_UnstackedMarkers', '.map-marker.marker-selected { transform: scale(1) !important; }');
   
      uroMainTickHandlerID = window.setInterval(uroMainTick,1000);
   },
   SetupUI: function()
   {
      // create a new div to display the UR details floaty-box
      uroDiv = document.createElement('div');
      uroDiv.id = "uroDiv";
      uroDiv.style.position = 'absolute';
      uroDiv.style.visibility = 'hidden';
      uroDiv.style.top = '0';
      uroDiv.style.left = '0';
      uroDiv.style.zIndex = 10000;
      uroDiv.style.backgroundColor = 'aliceblue';
      uroDiv.style.borderWidth = '3px';
      uroDiv.style.borderStyle = 'solid';
      uroDiv.style.borderRadius = '10px';
      uroDiv.style.boxShadow = '5px 5px 10px Silver';
      uroDiv.style.padding = '4px';
      document.body.appendChild(uroDiv);
   
      // create a new div to display script alerts
      uroAlerts = document.createElement('div');
      uroAlerts.id = "uroAlerts";
      uroAlerts.style.position = 'fixed';
      uroAlerts.style.visibility = 'hidden';
      uroAlerts.style.top = '50%';
      uroAlerts.style.left = '50%';
      uroAlerts.style.zIndex = 10000;
      uroAlerts.style.backgroundColor = 'aliceblue';
      uroAlerts.style.borderWidth = '3px';
      uroAlerts.style.borderStyle = 'solid';
      uroAlerts.style.borderRadius = '10px';
      uroAlerts.style.boxShadow = '5px 5px 10px Silver';
      uroAlerts.style.padding = '4px';
      uroAlerts.style.webkitTransform = "translate(-50%, -50%)";
      uroAlerts.style.transform = "translate(-50%, -50%)";
   
      let alertsHTML = '<div id="header" style="padding: 4px; background-color:LightGreen; font-weight: bold;">Alert title goes here...</div>';
      alertsHTML += '<div id="content" style="padding: 4px; background-color:White; overflow:auto;max-height:500px">Alert content goes here...</div>';
      alertsHTML += '<div id="controls" align="center" style="padding: 4px;">';
      alertsHTML += '<span id="uroAlertTickBtn" style="cursor:pointer;font-size:14px;border:thin outset black;padding:2px 10px 2px 10px;">';
      alertsHTML += '<i class="fa fa-check"> </i>';
      alertsHTML += '<span id="uroAlertTickBtnCaption" style="font-weight: bold;"></span>';
      alertsHTML += '</span>';
      alertsHTML += '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;';
      alertsHTML += '<span id="uroAlertCrossBtn" style="cursor:pointer;font-size:14px;border:thin outset black;padding:2px 10px 2px 10px;">';
      alertsHTML += '<i class="fa fa-times"> </i>';
      alertsHTML += '<span id="uroAlertCrossBtnCaption" style="font-weight: bold;"></span>';
      alertsHTML += '</span>';
      alertsHTML += '</div>';
      uroAlerts.innerHTML = uroUtils.ModifyHTML(alertsHTML);
      document.body.appendChild(uroAlerts);
   
      uroControls = document.createElement('section');
      uroControls.style.fontSize = '12px';
      uroControls.style.height = '100%';
      uroControls.id = "sidepanel-uroverview";
      uroControls.className = "tab-pane";
      uroControls.innerHTML = uroUtils.ModifyHTML('<div id="uroControlsContainer" style="display:flex;flex-direction:column;height:80vh;"></div>');
   
      uroInit.AddTab_API();
   },
   WazeBits: function()
   {
      uroMCLayer = null;

      for(let i=0; i < W.map.layers.length; i++)
      {
         if(W.map.layers[i].name == 'mapComments') uroMCLayer = W.map.layers[i];
         if(W.map.layers[i].name == 'venues') uroVenueLayer = W.map.layers[i];
      }
   },   
   FinalizeListenerSetup: function()
   {
      uroInit.finalisingListenerSetup = true;
   
      // filter markers when the marker objects are modified (this happens whenever WME needs to load fresh marker data
      // due to having panned/zoomed the map beyond the extents of the previously loaded data)
      W.model.mapUpdateRequests.on("objectschanged", uroFilterURs_onObjectsChanged);
      W.model.mapUpdateRequests.on("objectsadded", uroFilterURs_onObjectsAdded);
      W.model.mapUpdateRequests.on("objectsremoved", uroFilterURs_onObjectsRemoved);
   
      W.model.updateRequestSessions.on("objectsadded", uroUREvent_onObjectsAdded);
   
      W.model.cameras.on("objectschanged", uroFilterCameras);
      W.model.cameras.on("objectsadded", uroFilterCameras);
      W.model.cameras.on("objectsremoved", uroFilterCameras);
   
      W.model.mapProblems.on("objectschanged", uroFilterProblems);
      W.model.mapProblems.on("objectsadded", uroFilterProblems);
      W.model.mapProblems.on("objectsremoved", uroFilterProblems);
   
      W.model.venues.on("objectschanged", uroFilterPlaces);
      W.model.venues.on("objectsadded", uroFilterPlaces);
      W.model.venues.on("objectsremoved", uroFilterPlaces);
   
      W.model.mapComments.on("objectschanged", uroLayers.MCLayerChanged_changed);
      W.model.mapComments.on("objectsadded", uroLayers.MCLayerChanged_added);
      W.model.mapComments.on("objectsremoved", uroLayers.MCLayerChanged_removed);
   
      uroMarkers.RegisterEvents();
   
      uroLayers.InitialiseMOs();
   
      uroUtils.AddEventListener('_btnUndoLastHide', "click", uroIgnore.RemoveLastAdded, true);
      uroUtils.AddEventListener('_btnClearSessionHides', "click", uroIgnore.RemoveAll, true);
      uroIgnore.EnableControls();
   
      uroUtils.AddEventListener('_btnClearCamWatchList', "click", uroOWL.ClearCamWatchList, true);
      uroUtils.AddEventListener('_btnSettingsToText', "click", uroConfig.SettingsToText, true);
      uroUtils.AddEventListener('_btnTextToSettings', "click", uroConfig.TextToSettings, true);
      uroUtils.AddEventListener('_btnResetSettings', "click", uroConfig.DefaultSettings, true);
      uroUtils.AddEventListener('_btnClearSettingsText', "click", uroConfig.ClearSettingsText, true);
      uroUtils.AddEventListener('_cbMasterEnable', "click", uroFilterItems_MasterEnableClick, true);
   
   /*
      uroUtils.AddEventListener('_btnDebugToScreen',"click", uroDBG.Dump, true);
   */
   
      uroUtils.AddEventListener('uroDiv', "dblclick", uroPopup.Suppress, true);
   
      uroUtils.AddEventListener('_selectCameraUserID', "change", uroCamEditorSelected, true);
      uroUtils.AddEventListener('_selectPlacesUserID', "change", uroPlacesEditorSelected, true);
      uroUtils.AddEventListener('_selectHidePlacesUserID', "change", uroHidePlacesEditorSelected, true);
   
      uroUtils.AddEventListener('uroAlertTickBtn', 'click', uroAlertBox.CloseWithTick, true);
      uroUtils.AddEventListener('uroAlertCrossBtn', 'click', uroAlertBox.CloseWithCross, true);
   
      for(let i = 0; i < uroTabs.CtrlTabs.length; ++i)
      {
         uroUtils.SetOnClick(uroTabs.CtrlTabs[i][uroTabs.FIELDS.LINKID], uroTabs.CtrlTabs[i][uroTabs.FIELDS.SHOWFN]);
      }
   
      for(let idx=0;idx<W.Config.venues.categories.length;idx++)
      {
         uroUtils.SetOnClick('_uroPlacesGroupState-'+idx,uroPlacesGroupCollapseExpand);
      }
   
      uroDBG.AddLog('finalise onload');
   
      uroNewLookCheckDetailsRequest();
      // filter markers as and when the map is moved
      W.map.events.register("moveend", null, uroMapMoveEnd.Handler);
      W.map.events.register("moveend", null, uroGetAMs); // uroGetAMs accesses e, so has to be called directly from the event handler
      W.map.events.register("mousemove", null, uroGetAMs);
      W.map.events.register("mousemove", null, uroMarkers.MouseMove);
      W.map.events.registerPriority("mousedown", null, uroMouseDown);
   
      // trap mousedown on Streetview marker drag
      if(document.getElementsByClassName('street-view-control').length === 0) return;
      document.getElementsByClassName('street-view-control')[0].onmousedown = uroMouseDown;
   
      W.map.events.register("mouseup", null, uroMouseUp);
      W.map.events.register("mouseout", null, uroMouseOut);
   
      uroSetSectionTabStyles();
   
      uroConfig.LoadSettings();
   
      uroUITweaks.ChangeClustering();

      uroDBG.AddLog('getting user ID...');
      uroUserID = W.loginManager.user.attributes.id;
      uroDBG.AddLog('...ID is '+uroUserID);
      uroDBG.AddLog('filtering...');
      uroFilterItems();
      uroDBG.AddLog('...done');
   
      uroDBG.showDebugOutput = uroDBG.persistentDebugOutput;
      let dbgMode = "none";
      if(uroDBG.showDebugOutput)
      {
         dbgMode = "inline";
      }
      document.getElementById('_uroDebugMode').style.display = dbgMode;
      uroUtils.AddEventListener('_uroVersion',"click", uroDBG.Toggle, true);
   
      // add exclusiveCB click handlers to all checkboxes with a pairedWith attribute
      uroDBG.AddLog('adding exclusiveCB handlers...');
      let cbList = document.getElementsByTagName('input');
      for (let optIdx=0;optIdx<cbList.length;optIdx++)
      {
         if((cbList[optIdx].id.indexOf('_cb') === 0) && (cbList[optIdx].attributes.pairedWith != null))
         {
            uroUtils.SetOnClick(cbList[optIdx].id,uroExclusiveCB);
         }
      }
      uroDBG.AddLog('...done');
   
      // manually call the layer-change handlers on startup, since there's a good chance WME will already have
      // completed its own startup layer changes before our handlers get registered, preventing the marker handlers
      // from being set up as expected on any markers which are visible in the startup map view before the user forces
      // a layer update by panning/zooming/etc...
      uroLayers.RunChangeHandlers();

      uroUITweaks.Setup();
   
      uroInit.setupListeners = false;
      uroMainTickStage = 0;
      window.clearInterval(uroMainTickHandlerID);
      window.setInterval(uroMainTick, 10);
   
      uroInit.initialised = true;
   }   
};
const uroUITweaks =  // native UI enhancements
{
   MO_SidePanel : null,
   MO_ReportPanel : null,

   ChangeMapBGColour: function()
   {
      let mapviewport = document.getElementById("WazeMap").getElementsByClassName("olMapViewport")[0];
      if((uroUtils.GetCBChecked('_cbWhiteBackground') === true) && (uroUtils.GetCBChecked('_cbMasterEnable') === true))
      {
         let customColour = '#' + uroUtils.ToHex(uroUtils.GetElmValue('_inputCustomBackgroundRed'),2);
         customColour += uroUtils.ToHex(uroUtils.GetElmValue('_inputCustomBackgroundGreen'),2);
         customColour += uroUtils.ToHex(uroUtils.GetElmValue('_inputCustomBackgroundBlue'),2);
         mapviewport.style.setProperty('background',customColour,'important');
      }
      else
      {
         mapviewport.style.setProperty('background',"#354148",'important');
      }
   },
   HideAMLayer: function()
   {
      // If this sounds like a weird function - why not just switch off the layer from the layers menu? - then
      // remember that in order for URO+ to be able to display in its own tab the list of AMs under the current
      // mouse pointer location, which is somewhat more useful than the list given in the topbar, it needs the
      // AM layer to be activated so that the AM areas data is loaded into WME.  It doesn't however need the layer
      // to then be visible, and since having a bunch of purple polygons covering the map can make for a rather
      // difficult editing experience, being able to hide the polys whilst retaining the area information is
      // of real benefit...
      if((uroUtils.GetCBChecked('_cbHideAMLayer')) && (uroUtils.GetCBChecked('_cbMasterEnable')))
      {
         W.map.managedAreasLayer.setOpacity(0);
      }
      else
      {
         W.map.managedAreasLayer.setOpacity(1);
      }
   },
   HideSegments: function()
   {
      // Hides the vector segments when the raster segment layer is hidden
      if(uroUtils.GetCBChecked('_cbHideSegmentsWhenRoadsHidden'))
      {
         W.map.segmentLayer.drawn = W.map.roadLayer.visibility;
         W.map.nodeLayer.drawn = W.map.roadLayer.visibility;
      }
      else
      {
         W.map.segmentLayer.drawn = true;
         W.map.nodeLayer.drawn = true;
      }
   },
   SetClusteringFor: function(layerID, toDisable)
   {
      let strat = uroLayers.layers[layerID].l.strategies;
      if((strat !== undefined) && (strat.length > 0))
      {
         if(toDisable === true)
         {
            strat[0].threshold = 100000;
         }
         else
         {
            strat[0].threshold = 10;
         }
      }
   },
   ChangeClustering: function()
   {
      uroUITweaks.SetClusteringFor(uroLayers.ID.UR, uroUtils.GetCBChecked('_cbInhibitURClusters'));
      uroUITweaks.SetClusteringFor(uroLayers.ID.MP, uroUtils.GetCBChecked('_cbInhibitMPClusters'));
      uroUITweaks.SetClusteringFor(uroLayers.ID.PUR, uroUtils.GetCBChecked('_cbInhibitPUClusters'));
      uroUITweaks.SetClusteringFor(uroLayers.ID.PPUR, uroUtils.GetCBChecked('_cbInhibitPUClusters'));
      uroUITweaks.SetClusteringFor(uroLayers.ID.RPUR, uroUtils.GetCBChecked('_cbInhibitPUClusters'));
      uroUITweaks.SetClusteringFor(uroLayers.ID.SegSug, uroUtils.GetCBChecked('_cbInhibitESClusters'));      
      uroUITweaks.SetClusteringFor(uroLayers.ID.EditSug, uroUtils.GetCBChecked('_cbInhibitESClusters'));

      // If the map is zoomed out far enough such that clustering could be occurring, perform a
      // zoom in-out to force a redraw of the markers based on whatever the new clustering
      // settings are...
      if(W.map.getZoom() < 16)
      {
         W.map.zoomIn();
         W.map.zoomOut();
      }
   },
   ReportPanelChange: function()
   {
      // Inhibit map re-centering when opening a report
      if(uroMarkers.inhibitSetCenter === true)
      {
         let bcr = document.querySelector('#panel-container').getBoundingClientRect();
         if(bcr.width > 0)
         {
            uroMarkers.inhibitSetCenter = false;
            W.map.setCenter(uroMarkers.clickedOnCenter);
         }
      }

      // "panel-container" now also gets used to show the turn closure UI, so reuse this MO handler
      // as a way to also apply the MTE dropdown fix here...
      let mteDropDown = document.querySelector('#panel-container #closure_eventId');
      if(mteDropDown !== undefined)
      {
         uroFixMTEDropDown(mteDropDown);
      }
   },
   CheckForClosurePanel: function()
   {
      if(uroUtils.IsClosureUIActive() === true)
      {
         // Closure panel active
         let uroMO_ClosureUI = new MutationObserver(uroClosureEditUIChanged);
         uroMO_ClosureUI.disconnect();
         uroMO_ClosureUI.observe(document.querySelector('wz-tab.closures-tab'),{subtree: true, attributes: true});
         uroClosureEditUIChanged();

         let cl = document.querySelector('.closures-list');
         if(cl !== null)
         {
            let uroRO_ClosureUI = new ResizeObserver(uroScrollToEndOfClosures);
            uroRO_ClosureUI.disconnect();
            uroRO_ClosureUI.observe(cl);
         }
         uroScrollToEndOfClosures();
      }      
   },
   SidePanelChange: function()
   {
      // The sidepanel MO only fires when the sidepanel changes in bulk - i.e. when first rendered, or
      // when changing to show details for a different type of map object.  Once this version of the
      // panel is open, any internal changes, such as selecting a different tab within the panel, do
      // NOT trigger the MO.  Consequently, if a segment is selected and the sidepanel doesn't then
      // open with the closures tab already selected, the MO will fire at this point rather than at the
      // point where the closures tab is selected...
      //
      // To ensure the closures UI enhancements are applied consistently, we therefore need to set up an
      // onclick handler to deal with the sidepanel opening into a different tab and the user then
      // clicking through into the closures tab after this MO has already fired, but we ALSO still need
      // to check for the availability of the closures UI here as well just in case the sidepanel opens
      // into the closures tab directly.
      //
      // Easy really...

      let elmToClick = document.querySelector('#edit-panel');
      if(elmToClick !== null)
      {
         uroUtils.SetOnClick(elmToClick, uroUITweaks.CheckForClosurePanel);
      }
      uroUITweaks.CheckForClosurePanel();
   },
   Setup: function()
   {
      uroUITweaks.MO_SidePanel = new MutationObserver(uroUITweaks.SidePanelChange);
      uroUITweaks.MO_SidePanel.observe(document.getElementById('edit-panel'), {childList: true, subtree: true});
      uroUITweaks.MO_ReportPanel = new MutationObserver(uroUITweaks.ReportPanelChange);
      uroUITweaks.MO_ReportPanel.observe(document.getElementById('panel-container'), {childList: true, subtree: true});

      uroUITweaks.ChangeMapBGColour();
      uroUITweaks.HideAMLayer();
      uroUITweaks.HideSegments();
   }
};
const uroMapMoveEnd =   // things that happen after a move of the map view
{
   lat : null,
   lon: null,
   zoom: null,
   Handler: function()
   {
      let mc = W.map.getCenter();
      let z = W.map.getZoom();
      if((mc.lat != this.lat) || (mc.lon != this.lon) || (z != this.zoom))
      {
         // Apply any filters which need to be updated when the map view changes,
         // and which won't be applied via an event handler or mutation observer
         // attached to the relevant layer etc...
         uroFilterProblems();
         uroFilterPlaces();
         uroFilterCameras();
         uroFilterURs();
         uroFilterRAs();
         uroFilterMapComments();

         uroMiscUITweaksHandler();
      
         uroLayers.MCLayerChanged();
   
         this.lat = mc.lat;
         this.lon = mc.lon;
         this.zoom = z;
      }
   }
};


// ================================================================================================
// Here be the unfactored wilderness...
// ================================================================================================


function uroFixMTEDropDown(mteDropDown)
{
   // Auto-selects the "None" event within the MTE dropdown element passed into the function, to avoid the user having
   // to manually select it - the only time you'd need to select something other than None is when you're assigning a
   // closure to a MTE, at which point you need to manually select the appropriate event from the dropdown anyway, so
   // pre-selecting None doesn't increase the workload for setting up a MTE-related closure, and it reduces the workload
   // for setting up other closures...
   //
   // The only possible negative to this hack is that it means the user can set up a MTE-related closure without being
   // reminded by WME to select the appropriate event from the list, because now that we're defaulting it to None, WME
   // will allow the closure to be set without complaining that no event is set...  But TBH, that's a small price to pay
   // compared with the far, FAR, larger irritation of forcing users to always select None for all the closures that
   // get added every single day without ever needing to be associated with a MTE - if a handful of closures end up being
   // added with the user having forgotten to select the MTE, then no biggie.  The closure will still at least have been
   // added and its effect on routing around the event will therefore still be just as it would be if the MTE had been
   // associated with the closure - the only difference is that if someone then bothers to look at the overview map of the
   // MTE, they won't see that particular segment listed as a closure.  I can live with the tiny risk of that causing any
   // real problems, when weighed up against the millions of extra clicks saved through pre-selecting None...

   let retval = false;

   
   // Make sure the closure event list is available, and that we haven't already messed with it.
   if((mteDropDown !== null) && (mteDropDown.tag != "touchedByURO"))
   {   
      // The event dropdown is now some byzantine piece of DOM manipulation to generate something which looks like a
      // regular select list, but which can't be manipulated like one...  The first gotcha is that the selected item
      // exists only within a shadow DOM section within the dropdown rather than simply being part of the list from
      // which we'd be able to read off its selected index.  So to check whether or not the user has selected an
      // event already, we need to drill down into this shadow DOM to get its text contents, and compare those against
      // the I18n translation for the choose event text.  What a palaver...
      let shadowElm = mteDropDown.shadowRoot.querySelectorAll('.selected-value')[0];
      if(shadowElm !== undefined)
      {
         let eventText = mteDropDown.shadowRoot.querySelectorAll('.selected-value')[0].innerText;
         // Sometimes we get here before WME has finished rendering, so if the event text hasn't been set yet then we
         // need to return false and let the caller deal with it...
         if(eventText !== '')
         {
            if(eventText == I18n.lookup('closures.choose_event'))
            {
               // Having now established that, yes, the closure hasn't yet been associated with any event, it's surprisingly
               // easy to change it to "None" - we just generate a click event on the first child element in the main DOM (not
               // the shadow DOM this time), which replicates what the user would do to select None manually.
               mteDropDown.children[0].click();
            }
            // Tag the event list to prevent further processing attempts whilst the edit UI for this closure remains open.
            mteDropDown.tag = "touchedByURO";
            retval = true;
         }
      }
   }

   return retval;
}
let uroPendingURSessionsTotal;
let uroFinalizeTimeoutHandle = null;
function uroFinalizeURSessionsGet()
{
   if(uroPendingURSessionsTotal != uroPendingURSessionIDs.length)
   {
      uroPendingURSessionsTotal = uroPendingURSessionIDs.length;
      if(uroFinalizeTimeoutHandle !== null)
      {
         window.clearTimeout(uroFinalizeTimeoutHandle);
         uroFinalizeTimeoutHandle = null;
      }
      uroFinalizeTimeoutHandle = window.setTimeout(uroFinalizeURSessionsGet, 500);
      return;
   }

   let idList = [];

   while((idList.length < 50) && (uroPendingURSessionIDs.length))
   {
      let id = uroPendingURSessionIDs.shift();
      idList.push(id);
   }

   if(idList.length > 0)
   {
      uroDBG.AddLog('grabbing '+idList.length+' updateRequestSessions, IDs: '+idList);
      W.model.updateRequestSessions.getAsync(idList);
   }

   if((uroPendingURSessionIDs.length) || (uroRequestedURSessionIDs.length))
   {
      window.setTimeout(uroGetUpdateRequestSessions,1000);
   }
   else
   {
      uroPopulatingRequestSessions = false;
   }
}
function uroGetUpdateRequestSessions()
{
   uroPendingURSessionsTotal = uroPendingURSessionIDs.length;
   if(uroFinalizeTimeoutHandle !== null)
   {
      window.clearTimeout(uroFinalizeTimeoutHandle);
      uroFinalizeTimeoutHandle = null;
   }
   uroFinalizeTimeoutHandle = window.setTimeout(uroFinalizeURSessionsGet,500);
}
function uroRefreshUpdateRequestSessions()
{
   let urcount = 0;
   uroPendingURSessionIDs = [];
   uroRequestedURSessionIDs = [];
   uroPopulatingRequestSessions = true;
   for (let urID in W.model.mapUpdateRequests.objects)
   {
      if(W.model.mapUpdateRequests.objects.hasOwnProperty(urID))
      {
         if(W.model.updateRequestSessions.objects[urID] === undefined)
         {
            uroPendingURSessionIDs.push(urID);
         }
         urcount++;
      }
   }
   uroGetUpdateRequestSessions();
}
function uroURHasMyComments(fid)
{
   if(uroUserID === -1)
   {
      return false;
   }
   let nComments = W.model.updateRequestSessions.objects[fid].attributes.comments.length;
   if(nComments === 0)
   {
      return false;
   }

   for(let cidx=0; cidx<nComments; cidx++)
   {
      if(W.model.updateRequestSessions.objects[fid].attributes.comments[cidx].userID == uroUserID)
      {
         return true;
      }
   }

   return false;
}
function uroIsFilteringEnabled(ignoreZoom)
{
   let retval = false;
   if
   (
      (uroUtils.GetCBChecked('_cbMasterEnable') === true) && 
      (
         (ignoreZoom === true) || 
         (W.map.getZoom() <= uroUtils.GetElmValue('_inputFilterMinZoomLevel'))
      )
   )
   {
      retval = true;
   }
   return retval;
}
function uroUpdateMTEList()
{
   if(Object.keys(W.model.majorTrafficEvents.objects).length === 0) return;

   let selectedIdx = null;
   let idx;
   let mteNames = [];
   let mteIDs = [];
   for(idx in W.model.majorTrafficEvents.objects)
   {
      if(W.model.majorTrafficEvents.objects.hasOwnProperty(idx))
      {
         let name = W.model.majorTrafficEvents.objects[idx].attributes.names[0]?.value;
         if(mteNames.indexOf(name) == -1)
         {
            mteNames.push(name);
            mteIDs.push(idx);
         }
      }
   }
   // check for any previously selected ID in the list, then clear it and repopulate
   // using the newly gathered ID collection from above, and finally reselect the
   // previously selected MTE if its still present in the new list...
   let selector;
   let selectedID;
   let selectorEntry;

   selector = document.getElementById('_selectRTCMTE');
   selectedID = null;
   if(selector.selectedOptions[0] != null)
   {
      selectedID = selector.selectedOptions[0].value;
   }
   while(selector.options.length > 0)
   {
      selector.options.remove(0);
   }
   selector.options.add(new Option('<select a MTE>', null));
   if(mteNames.length > 0)
   {
      selectorEntry = '';
      for(idx=0; idx<mteNames.length; idx++)
      {
         selectorEntry = mteNames[idx];
         selector.options.add(new Option(selectorEntry, mteIDs[idx]));
         if(mteIDs[idx] == selectedID)
         {
            selectedIdx = idx+1;
         }
      }
   }

   if(selectedIdx !== null)
   {
      selector.selectedIndex = selectedIdx;
   }
}
function uroRTCMarkerInfo(mIdx, isVisible)
{
   let pri = null;
   let status = null;
   let dir = null;
   let pos = null;
   let mID = null;

   let mObj = uroLayers.layers[uroLayers.ID.RTC].l.markers[mIdx];
   if(mObj !== undefined)
   {
      // Store the marker status - if we've already made a note of the original
      // status by adding an "orig_"-prefixed classname, extract the status from
      // that rather than from the class currently being used to display the
      // marker, to preserve the original status regardless of what we may have
      // done to it subsequently as a result of our RTC filtering setup...
      let cList = mObj.element.classList;
      for(let i = 0; i < cList.length; ++i)
      {
         if(cList[i].indexOf('orig_') != -1)
         {
            status = cList[i].replace('orig_', '');
         }
      }
      if(status === null)
      {
         for(let i = 0; i < cList.length; ++i)
         {
            if(cList[i].indexOf('status-') != -1)
            {
               status = cList[i];
            }
         }
      }

      // Assign a priority level to the marker so we know whether to show or hide
      // it if it's stacked up with others on the same segment - the level set here
      // follows the priority used by WME to decide which closures to show normally.
      if(status == "status-active")
      {
         pri = 2;
      }
      else if(status == "status-not-started")
      {
         pri = 1;
      }
      else
      {
         pri = 0;
      }

      // To avoid the need to cross-reference with the closure model object, use the
      // classname of the arrow attached to this marker to determine if the closure is
      // in the forward or reverse direction.
      if(mObj.element.childNodes.length == 1)
      {
         if(mObj.element.childNodes[0].className.indexOf('forward') != -1)
         {
            dir = "fwd";
         }
         else if(mObj.element.childNodes[0].className.indexOf('backward') != -1)
         {
            dir = "rev";
         }
      }

      pos = mObj.px;
      mID = mObj.element.dataset.id;
   }

   this.isVisible = isVisible;
   this.pos = pos;
   this.pri = pri;
   this.mID = mID;
   this.status = status;
   this.dir = dir;
}
function uroFilterRTCs()
{
   let pmTStart = performance.now();
   let pmFunction = "uroFilterRTCs";

   if(uroFilterPreamble() === false) return;

   let closureLayer = uroLayers.layers[uroLayers.ID.RTC].l;
   if(closureLayer.markers.length === 0) return;

   let uFR_filterActiveFromWME = uroUtils.GetCBChecked('_cbHideEditorRTCs');
   let uFR_filterActiveFromWazeFeed = uroUtils.GetCBChecked('_cbHideWazeFeedRTCs');
   let uFR_filterActiveFromWazeOther = uroUtils.GetCBChecked('_cbHideWazeRTCs');
   let uFR_filterFutureFromWME = uroUtils.GetCBChecked('_cbHideFutureEditorRTCs');
   let uFR_filterFutureFromWazeFeed = uroUtils.GetCBChecked('_cbHideFutureWazeFeedRTCs');
   let uFR_filterFutureFromWazeOther = uroUtils.GetCBChecked('_cbHideFutureWazeRTCs');
   let uFR_filterExpiredFromWME = uroUtils.GetCBChecked('_cbHideExpiredEditorRTCs');
   let uFR_filterExpiredFromWazeFeed = uroUtils.GetCBChecked('_cbHideExpiredWazeFeedRTCs');
   let uFR_filterExpiredFromWazeOther = uroUtils.GetCBChecked('_cbHideExpiredWazeRTCs');
   let uFR_filterUnknownFromWME = uroUtils.GetCBChecked('_cbHideUnknownEditorRTCs');
   let uFR_filterUnknownFromWazeFeed = uroUtils.GetCBChecked('_cbHideUnknownWazeFeedRTCs');
   let uFR_filterUnknownFromWazeOther = uroUtils.GetCBChecked('_cbHideUnknownWazeRTCs');
   let uFR_filterShowForMTE = uroUtils.GetCBChecked('_cbShowMTERTCs');
   let uFR_filterHideForMTE = uroUtils.GetCBChecked('_cbHideMTERTCs');
   let uFR_filterHideDurationLessThan = uroUtils.GetCBChecked('_cbEnableRTCDurationFilterLessThan');
   let uFR_filterHideDurationMoreThan = uroUtils.GetCBChecked('_cbEnableRTCDurationFilterMoreThan');
   let uFR_thresholdDurationLessThan = uroUtils.GetElmValue('_inputFilterRTCDurationLessThan');
   let uFR_thresholdDurationMoreThan = uroUtils.GetElmValue('_inputFilterRTCDurationMoreThan');

   let uFR_filterShowForTS = uroUtils.GetCBChecked('_cbRTCFilterShowForTS');
   let uFR_filterHideForTS = uroUtils.GetCBChecked('_cbRTCFilterHideForTS');
   
   let tsD = uroUtils.GetElmValue('_inputRTCFilterDay');
   let tsMo = uroUtils.GetElmValue('_inputRTCFilterMonth');
   let tsY = uroUtils.GetElmValue('_inputRTCFilterYear');
   let tsH = uroUtils.GetElmValue('_inputRTCFilterHour');
   let tsMi = uroUtils.GetElmValue('_inputRTCFilterMin');
   let filterTS = uroUtils.GetTS(tsD, tsMo, tsY, tsH, tsMi);
   uroUpdateMTEList();
   let mteID = null;
   let selectorMTE = document.getElementById('_selectRTCMTE');
   if(selectorMTE?.selectedOptions[0] != null)
   {
      mteID = selectorMTE.selectedOptions[0].value;
   }

   let uFR_masterEnable = uroIsFilteringEnabled(false);

   let markerInfo = [];

   // Pass 1 - determine which filtering to apply to each of the RTC markers
   let markerIdx = 0;
   for (let rtcObj in W.model.roadClosures.objects)
   {
      let isVisible = true;

      if(uFR_masterEnable === true)
      {   
         let rtcModel = W.model.roadClosures.objects[rtcObj];

         if(mteID !== null)
         {
            if((uFR_filterShowForMTE === true) && (rtcModel.attributes.eventId !== mteID))
            {
               isVisible = false;
            }
            if((uFR_filterHideForMTE === true) && (rtcModel.attributes.eventId === mteID))
            {
               isVisible = false;
            }
         }

         let rtcType = uroGetRTCOrigin(rtcModel);
         let rtcState = uroGetRTCState(rtcModel);

         if(rtcType == uroEnums.TRTC.WAZEFEED)
         {
            if
            (
               ((rtcState === uroEnums.SRTC.ACTIVE) && (uFR_filterActiveFromWazeFeed === true)) ||
               ((rtcState === uroEnums.SRTC.FUTURE) && (uFR_filterFutureFromWazeFeed === true)) ||
               ((rtcState === uroEnums.SRTC.EXPIRED) && (uFR_filterExpiredFromWazeFeed === true)) ||
               ((rtcState === uroEnums.SRTC.UNKNOWN) && (uFR_filterUnknownFromWazeFeed === true))
            )
            {
               isVisible = false;
            }
         }
         else if(rtcType == uroEnums.TRTC.WAZEOTHER)
         {
            if
            (
               ((rtcState === uroEnums.SRTC.ACTIVE) && (uFR_filterActiveFromWazeOther === true)) ||
               ((rtcState === uroEnums.SRTC.FUTURE) && (uFR_filterFutureFromWazeOther === true)) ||
               ((rtcState === uroEnums.SRTC.EXPIRED) && (uFR_filterExpiredFromWazeOther === true)) ||
               ((rtcState === uroEnums.SRTC.UNKNOWN) && (uFR_filterUnknownFromWazeOther === true))
            )
            {
               isVisible = false;
            }
         }
         else
         {
            if
            (
               ((rtcState === uroEnums.SRTC.ACTIVE) && (uFR_filterActiveFromWME === true)) ||
               ((rtcState === uroEnums.SRTC.FUTURE) && (uFR_filterFutureFromWME === true)) ||
               ((rtcState === uroEnums.SRTC.EXPIRED) && (uFR_filterExpiredFromWME === true)) ||
               ((rtcState === uroEnums.SRTC.UNKNOWN) && (uFR_filterUnknownFromWME === true))
            )
            {
               isVisible = false;
            }
         }
         
         let rtcDuration = uroGetRTCDuration(rtcModel);
         if(uFR_filterHideDurationLessThan === true)
         {
            if(rtcDuration < uFR_thresholdDurationLessThan)
            {
               isVisible = false;
            }
         }
         if(uFR_filterHideDurationMoreThan === true)
         {
            if(rtcDuration > uFR_thresholdDurationMoreThan)
            {
               isVisible = false;
            }
         }
         
         if((uFR_filterShowForTS === true) || (uFR_filterHideForTS === true))
         {
            let startTS = new Date(rtcModel.attributes.startDate).getTime();
            let endTS = new Date(rtcModel.attributes.endDate).getTime();
            
            if(uFR_filterShowForTS === true)
            {
               if((filterTS < startTS) || (filterTS > endTS))
               {
                  isVisible = false;
               }
            }
            if (uFR_filterHideForTS === true)
            {
               if((filterTS >= startTS) && (filterTS <= endTS))
               {
                  isVisible = false;
               }            
            }
         }
      }

      markerInfo.push(new uroRTCMarkerInfo(markerIdx, isVisible));
      ++markerIdx;
   }

   // Pass 2 - based on the initial filtering results, determine which markers *actually* should be
   // made visible or hidden according both to our filtering settings AND any masking of this marker
   // due to the presence of other higher priority markers at the same position which have also been
   // marked as visible following the filtering pass...
   //
   // For added merriment, we also deal with the WME limitation that prevents it displaying two 
   // different types of closure arrow if a segment has a one-way closure which is a higher priority
   // than a closure in the opposite direction.

   let cNodesDiv = uroLayers.layers[uroLayers.ID.RTCnode].l.div;
   for (let i = 0; i < markerIdx; ++i)
   {
      let status = markerInfo[i].status;

      // Only apply this pass to closures which haven't already been hidden in pass 1, AND which
      // have a valid marker position
      if((markerInfo[i].isVisible === true) && (markerInfo[i].pos !== null))
      {
         // Iterate through all the other markers, looking for any which are also still visible
         // and have the same marker position as the one we're currently processing
         for (let j = 0; j < markerIdx; ++j)
         {
            if(j != i)
            {
               if((markerInfo[j].isVisible === true) && (markerInfo[j].pos !== null))
               {
                  if((markerInfo[i].pos.x == markerInfo[j].pos.x) && (markerInfo[i].pos.y == markerInfo[j].pos.y))
                  {            
                     if(markerInfo[j].pri > markerInfo[i].pri)
                     {
                        if(markerInfo[j].dir == markerInfo[i].dir)
                        {
                           // Mark the currently processed marker to be hidden only if this higher priority
                           // marker is for a closure in the same direction - if it's not, then we need to
                           // leave the current marker visible so that its arrow remains visible...
                           markerInfo[i].isVisible = false;
                           break;
                        }
                        else
                        {
                           // Otherwise, if we're leaving the current marker visible then we'll need to 
                           // alter its status class to match this higher-priority marker, so that the
                           // marker which is shown to the user remains correct regardless of what the
                           // relative stacking order of the markers is on this segment.
                           status = markerInfo[j].status;
                        }
                     }
                  }
               }
            }
         }
      }

      let marker = closureLayer.markers[i].element;
      let markerClass = marker.className;

      // Remove the hidden class if present - this allows markers natively hidden by WME due to being masked
      // by higher priority markers to become visible again if our own filtering settings have hidden those
      // higher priority marker...
      markerClass = markerClass.replace(" road-closure-hidden", "");
      if(markerInfo[i].isVisible == false)
      {
         // Apply the hidden class for any markers WE'VE decided need to be hidden
         markerClass += " road-closure-hidden";
      }
      else
      {
         // For any markers which we're leaving visible, first check to see if it's a marker we haven't yet
         // seen.  If so, then we need to store its original status class for future reference.
         if(markerClass.indexOf('orig_') == -1)
         {
            markerClass += (' orig_'+markerInfo[i].status);
         }
         
         // Now, to ensure this segment displays the appropriate marker for the highest priority closure 
         // still visible on it, we remove the existing status class and replace it with the one we
         // chose above
         markerClass = markerClass.replace(" status-finished"," ");
         markerClass = markerClass.replace(" status-active"," ");
         markerClass = markerClass.replace(" status-not-started"," ");
         markerClass += ' '+status;
      }
      marker.className = markerClass;
        
      let toHide = cNodesDiv.querySelectorAll("[data-id='"+markerInfo[i].mID+"']");   
      for(let j = 0; j < toHide.length; ++j)
      {
         if(markerInfo[i].isVisible === false)
         {
            toHide[j].style.visibility = "hidden";
         }
         else
         {
            toHide[j].style.visibility = "";
         }
      }
   }

   uroDBG.PerfMon(pmFunction, pmTStart);
}
function uroUpdateRAList()
{
   if(Object.keys(W.model.restrictedDrivingAreas.objects).length === 0) return;

   let selectedIdx = null;
   let idx;
   let raNames = [];
   for(idx in W.model.restrictedDrivingAreas.objects)
   {
      if(W.model.restrictedDrivingAreas.objects.hasOwnProperty(idx))
      {
         let name = W.model.restrictedDrivingAreas.objects[idx].attributes.name;
         if(raNames.indexOf(name) == -1)
         {
            raNames.push(name);
         }
      }
   }
   // check for any previously selected name in the list, then clear it and repopulate
   // using the newly gathered collection from above, and finally reselect the
   // previously selected MTE if its still present in the new list...
   let selector;
   let selectedName;
   let selectorEntry;

   selector = document.getElementById('_selectRA');
   selectedName = null;
   if(selector.selectedOptions[0] != null)
   {
      selectedName = selector.selectedOptions[0].value;
   }
   while(selector.options.length > 0)
   {
      selector.options.remove(0);
   }
   selector.options.add(new Option('<select a RA>', null));
   if(raNames.length > 0)
   {
      selectorEntry = '';
      for(idx=0; idx<raNames.length; idx++)
      {
         selectorEntry = raNames[idx];
         selector.options.add(new Option(selectorEntry, selectorEntry));
         if(selectorEntry == selectedName)
         {
            selectedIdx = idx+1;
         }
      }
   }

   if(selectedIdx !== null)
   {
      selector.selectedIndex = selectedIdx;
   }
}
function uroUpdateEditorList(modelObj, listElement, useCreated, useUpdated, useResolved, useCommenter)
{
   if(Object.keys(modelObj).length === 0) return;

   let selector = document.getElementById(listElement);

   let selectedUser = null;
   if(selector.selectedOptions[0] != null)
   {
      selectedUser = parseInt(selector.selectedOptions[0].value);
   }

   while(selector.options.length > 0)
   {
      selector.options.remove(0);
   }

   let selectedIdx = null;
   let listedIDs = [];
   let idx;
   for(idx in modelObj)
   {
      if(modelObj.hasOwnProperty(idx))
      {
         let obj;
         if(useCommenter == true)
         {
            obj = modelObj[idx];
            if(obj.attributes.comments.length > 0)
            {
               for(let cidx=0; cidx < obj.attributes.comments.length; cidx++)
               {
                  let userID = obj.attributes.comments[cidx].userID;                  
                  if((listedIDs.indexOf(userID) == -1) && (userID != -1))
                  {
                     listedIDs.push(userID);                     
                  }
               }
            }
         }
         else
         {
            obj = modelObj[idx].attributes;
            let cbID = null;
            let ubID = null;
            let rbID = null;
            if(useCreated == true) cbID = obj.createdBy;
            if(useUpdated == true) ubID = obj.updatedBy;
            if(useResolved == true) ubID = obj.resolvedBy;
            
            if((cbID !== null) && (listedIDs.indexOf(cbID) == -1))
            {
               listedIDs.push(cbID);
            }
            if((ubID !== null) && (listedIDs.indexOf(ubID) == -1))
            {
               listedIDs.push(ubID);
            }
            if((rbID !== null) && (listedIDs.indexOf(rbID) == -1))
            {
               listedIDs.push(rbID);
            }
         }
      }
   }

   selector.options.add(new Option('<select a user>', null));
   if(listedIDs.length > 0)
   {
      let users = W.model.users.getByIds(listedIDs);
      let selectorEntry = '';
      for(idx=0; idx<users.length; idx++)
      {
         if(listedIDs.indexOf(users[idx].id) != -1)
         {
            listedIDs.splice(listedIDs.indexOf(users[idx]), 1);
         }
         
         if(users[idx].attributes.userName === undefined)
         {
            selectorEntry = users[idx].attributes.id;
         }
         else
         {
            selectorEntry = users[idx].attributes.userName;
         }
         selector.options.add(new Option(selectorEntry, users[idx].id));
         if(users[idx].attributes.id == selectedUser)
         {
            selectedIdx = idx+1;
         }
      }
   }

   if(selectedIdx !== null)
   {
      selector.selectedIndex = selectedIdx;
   }
}
function uroGetUserID(filterNameID, tbUserName)
{
   if(filterNameID === null)
   {
      for(let idx in W.model.users.objects)
      {
         if(W.model.users.objects.hasOwnProperty(idx))
         {
            if(W.model.users.objects[idx].attributes.userName == tbUserName)
            {
               filterNameID = W.model.users.objects[idx].attributes.id;
               break;
            }
         }
      }      
   }
   return filterNameID;
}  
function uroFilterRAs()
{
   let pmTStart = performance.now();
   let pmFunction = "uroFilterRAs";

   if(uroFilterPreamble() === false) return;
   let uFURs_masterEnable = uroIsFilteringEnabled(false);
   let filterByArea = uroUtils.GetCBChecked('_cbShowSpecificRA');
   let filterByLastEditor = uroUtils.GetCBChecked('_cbRAEditorIDFilter');
   let filterByMinAge = uroUtils.GetCBChecked('_cbEnableRAAgeFilterLessThan');
   let filterByMaxAge = uroUtils.GetCBChecked('_cbEnableRAAgeFilterMoreThan');
   let thresholdMinAge = uroUtils.GetElmValue('_inputFilterRAAgeLessThan');
   let thresholdMaxAge = uroUtils.GetElmValue('_inputFilterRAAgeMoreThan');
   
   let selectorRA = document.getElementById('_selectRA');
   if(filterByArea === false)
   {
      while(selectorRA.options.length > 0)
      {
         selectorRA.options.remove(0);
      }
   }
   let shownRA = null;
   if(filterByArea === true)
   {
      if(selectorRA.options.length === 0)
      {
         uroUpdateRAList();
      }
      if(selectorRA.selectedOptions[0] != null)
      {
         shownRA = selectorRA.selectedOptions[0].value;
      }
   }

   let filterNameID = null;
   if(filterByLastEditor == true)
   {
      uroUpdateEditorList(W.model.restrictedDrivingAreas.objects, '_selectRAEditorID', true, true, false, false);
      let selector = document.getElementById('_selectRAEditorID');
      if(selector.selectedIndex > 0)
      {
         filterNameID = document.getElementById('_selectRAEditorID').selectedOptions[0].value;
      }
   }
   
   let nRANames = document.querySelectorAll('.restricted-driving-area-name-marker').length;
   for (let raIdx = 0; raIdx < W.map.restrictedDrivingAreaLayer.features.length; raIdx++)
   {
      let raObj = W.map.restrictedDrivingAreaLayer.features[raIdx].attributes.wazeFeature;
      if(raObj !== undefined)
      {
         let raStyle = 'visible';
         if(uFURs_masterEnable === true)
         {
            if(shownRA !== null)
            {
               if(raObj._wmeObject.attributes.name != shownRA) raStyle = 'hidden';
            }
            
            if(filterNameID !== null)
            {
               if((raObj._wmeObject.attributes.createdBy != filterNameID) && (raObj._wmeObject.attributes.updatedBy != filterNameID))
               {
                  raStyle = 'hidden';
               }
            }
         
            let raAge = uroUtils.DateToDays(raObj._wmeObject.attributes.updatedOn);
            if(filterByMinAge == true)
            {
               if(raAge < thresholdMinAge) raStyle = 'hidden';
            }
            if(filterByMaxAge == true)
            {
               if(raAge > thresholdMaxAge) raStyle = 'hidden';
            }
         }
         
         let geoID = W.map.restrictedDrivingAreaLayer.features[raIdx].geometry.id;
         if(document.getElementById(geoID) !== null)
         {
            document.getElementById(geoID).style.visibility = raStyle;
         }

         // This doesn't always work, as the order in which the markers are listed on their layer isn't guaranteed
         // to match the order in which the corresponding RA polys are listed on theirs...  
         if(raIdx < nRANames)
         {
            document.querySelectorAll('.restricted-driving-area-name-marker')[raIdx].style.visibility = raStyle;
         }
      }
   }      
   uroDBG.PerfMon(pmFunction, pmTStart);
}
function uroFilterPlaceMarker(mObj, vObj, uFP_masterEnable)
{
   if((mObj === undefined) || (vObj === undefined))
   {
      return;
   }

   let purAge = null;
   let placeStyle = 'visible';
   let hasBalloon = false;

   if(uFP_masterEnable === true)
   {
      if(uro_uFP[uroEnums.FP_OPTS.filterInsideManagedAreas] === true)
      {
         let tPt = [];
         tPt.push(vObj.attributes.geoJSONGeometry.coordinates[0]);
         tPt.push(vObj.attributes.geoJSONGeometry.coordinates[1]);
         if(uroCheckGeometryWithinManagedAreas(tPt) === true) placeStyle = 'hidden';
      }

      if((placeStyle == 'visible') && (uro_uFP[uroEnums.FP_OPTS.filterUneditable] === true))
      {
         if(vObj.attributes.permissions === 0)
         {
            placeStyle = 'hidden';
         }
         if((placeStyle == 'visible') && (uro_uFP[uroEnums.FP_OPTS.isLoggedIn]))
         {
            if(uro_uFP[uroEnums.FP_OPTS.userRank] < vObj.attributes.lockRank)
            {
               placeStyle = 'hidden';
            }
         }
         if((placeStyle == 'visible') && (vObj.attributes.adLocked))
         {
            placeStyle = 'hidden';
         }
      }

      if((placeStyle == 'visible') && (uro_uFP[uroEnums.FP_OPTS.filterLockRanked] === true))
      {
         if(vObj.attributes.lockRank !== 0)
         {
            placeStyle = 'hidden';
         }
      }

      let urEntries = vObj.attributes.venueUpdateRequests;
      if((placeStyle == 'visible') && (urEntries !== undefined))
      {
         hasBalloon = (urEntries.length > 1);

         for(let i = 0; i < urEntries.length; ++i)
         {
            let ut = urEntries[i].attributes.updateType;
            if((uro_uFP[uroEnums.FP_OPTS.filterFlagged] === true) && (ut === "flag"))
            {
               placeStyle = 'hidden';
            }
            else if((uro_uFP[uroEnums.FP_OPTS.filterNewPlace] === true) && (ut === "ADD_VENUE"))
            {
               placeStyle = 'hidden';
            }
            else if((uro_uFP[uroEnums.FP_OPTS.filterUpdatedDetails] === true) && (ut === "UPDATE_VENUE"))
            {
               placeStyle = 'hidden';
            }
            else if((uro_uFP[uroEnums.FP_OPTS.filterNewPhoto] === true) && (ut === "ADD_IMAGE"))
            {
               placeStyle = 'hidden';
            }
         }

         if((placeStyle == 'visible') && (uro_uFP[uroEnums.FP_OPTS.filterOnCFs] === true))
         {
            let nVUR = urEntries.length;
            while(nVUR > 0)
            {
               nVUR--;
               let tCF = urEntries[nVUR].attributes.changedFields;
               if(tCF !== undefined)
               {
                  if(tCF.length > 0)
                  {
                     let tFN = tCF[0].attributes.fieldName;
                     if((tFN == "phone") && (uro_uFP[uroEnums.FP_OPTS.filterCFPhone] === true))
                     {
                        placeStyle = 'hidden';
                     }
                     else if((tFN == "name") && (uro_uFP[uroEnums.FP_OPTS.filterCFName] === true))
                     {
                        placeStyle = 'hidden';
                     }
                     else if((tFN == "entryExitPoints") && (uro_uFP[uroEnums.FP_OPTS.filterCFEntryExitPoints] === true))
                     {
                        placeStyle = 'hidden';
                     }
                     else if((tFN == "openingHours") && (uro_uFP[uroEnums.FP_OPTS.filterCFOpeningHours] === true))
                     {
                        placeStyle = 'hidden';
                     }
                     else if((tFN == "aliases") && (uro_uFP[uroEnums.FP_OPTS.filterCFAliases] === true))
                     {
                        placeStyle = 'hidden';
                     }
                     else if((tFN == "services") && (uro_uFP[uroEnums.FP_OPTS.filterCFServices] === true))
                     {
                        placeStyle = 'hidden';
                     }
                     else if((tFN == "geometry") && (uro_uFP[uroEnums.FP_OPTS.filterCFGeometry] === true))
                     {
                        placeStyle = 'hidden';
                     }
                     else if((tFN == "houseNumber") && (uro_uFP[uroEnums.FP_OPTS.filterCFHouseNumber] === true))
                     {
                        placeStyle = 'hidden';
                     }
                     else if((tFN == "categories") && (uro_uFP[uroEnums.FP_OPTS.filterCFCategories] === true))
                     {
                        placeStyle = 'hidden';
                     }
                     else if((tFN == "description") && (uro_uFP[uroEnums.FP_OPTS.filterCFDescription] === true))
                     {
                        placeStyle = 'hidden';
                     }
                  }
               }
            }
         }
      }

      if(uro_uFP[uroEnums.FP_OPTS.invertPURFilters] === true)
      {
         if(placeStyle == 'hidden') placeStyle = 'visible';
         else placeStyle = 'hidden';
      }

      if(uro_uFP[uroEnums.FP_OPTS.filterMinPURAge] || uro_uFP[uroEnums.FP_OPTS.filterMaxPURAge])
      {
         purAge = uroUtils.GetPURAge(vObj);
         if(uro_uFP[uroEnums.FP_OPTS.filterMinPURAge] === true)
         {
            if(purAge < uro_uFP[uroEnums.FP_OPTS.thresholdMinPURDays]) placeStyle = 'hidden';
         }
         if(uro_uFP[uroEnums.FP_OPTS.filterMaxPURAge] === true)
         {
            if(purAge > uro_uFP[uroEnums.FP_OPTS.thresholdMaxPURDays]) placeStyle = 'hidden';
         }
      }

      if(uroPURsToHide.indexOf(vObj.attributes.id) !== -1)
      {
         placeStyle = 'hidden';
      }
   }

   mObj.style.visibility = placeStyle;
   if(hasBalloon === true)
   {
      // for PURs related to multiple change requests, we also need to apply the
      // filtering to the text inside the balloon that indicates how many CRs
      // the PUR represents - the balloon itself gets filtered as part of the
      // main marker above, but the text is in a seperate element...
      let pObj = mObj.parentNode.parentNode;
      let tObjs = pObj.getElementsByTagName("text");
      for(let i = 0; i < tObjs.length; ++i)
      {
         tObjs[i].style.visibility = placeStyle;
      }
   }

   if((uro_uFP[uroEnums.FP_OPTS.leavePURGeos] === false) && (placeStyle === 'hidden'))
   {
      if(vObj.model != null)
      {
         if(vObj.attributes.geometry != null)
         {
            let puGeo = document.getElementById(vObj.attributes.geometry.id);
            if(puGeo !== null)
            {
               puGeo.style.visibility = 'hidden';
            }
         }
      }
   }
}
function uroFilterPlaces()
{
   let pmTStart = performance.now();
   let pmFunction = "uroFilterPlaces";

   if(uroFilterPreamble() === false) return;

   let moObj = uroGetHighlightedMapFeature();
   let renderIntent = uroGetFeatureRenderIntent(moObj);
   if(moObj != null)
   {
      if(moObj.featureType === 'venue')
      {
         if((renderIntent == 'select') || (renderIntent == 'highlightselected'))
         {
            return;
         }
      }
   }

   if(uroUtils.GetCBChecked('_cbDisablePlacesFiltering') === true) return;

   uroUpdateVenueEditorLists();

   let filterNameID = null;
   let tbUserName = uroUtils.GetElmValue('_textPlacesEditor');
   let selector = document.getElementById('_selectPlacesUserID');
   if(selector.selectedIndex > 0)
   {
      let selUserName = document.getElementById('_selectPlacesUserID').selectedOptions[0].innerHTML;
      if(selUserName == tbUserName)
      {
         filterNameID = document.getElementById('_selectPlacesUserID').selectedOptions[0].value;
      }
   }
   filterNameID = uroGetUserID(filterNameID, tbUserName);

   let filterHideNameID = null;
   let tbHideUserName = uroUtils.GetElmValue('_textHidePlacesEditor');
   let selectorHide = document.getElementById('_selectHidePlacesUserID');
   if(selectorHide.selectedIndex > 0)
   {
      let selHideUserName = document.getElementById('_selectHidePlacesUserID').selectedOptions[0].innerHTML;
      if(selHideUserName == tbHideUserName)
      {
         filterHideNameID = document.getElementById('_selectHidePlacesUserID').selectedOptions[0].value;
      }
   }
   filterHideNameID = uroGetUserID(filterHideNameID, tbHideUserName);

   let filterCats = [];
   for(let i=0; i<W.Config.venues.categories.length; i++)
   {
      let parentCategory = W.Config.venues.categories[i];
      let subCategory;

      if(uroUtils.GetCBChecked('_cbPlacesFilter-'+parentCategory) === true)
      {
         filterCats.push(parentCategory);
         for(let i1=0; i1<W.Config.venues.subcategories[parentCategory].length; i1++)
         {
            subCategory = W.Config.venues.subcategories[parentCategory][i1];
            filterCats.push(subCategory);
         }
      }
      else
      {
         for(let i2=0; i2<W.Config.venues.subcategories[parentCategory].length; i2++)
         {
            subCategory = W.Config.venues.subcategories[parentCategory][i2];
            if(uroUtils.GetCBChecked('_cbPlacesFilter-'+subCategory) === true)
            {
               filterCats.push(subCategory);
            }
         }
      }
   }

   let placeStyle;

   let uFP_filterEditedLessThan = uroUtils.GetCBChecked('_cbPlaceFilterEditedLessThan');
   let uFP_filterEditedMoreThan = uroUtils.GetCBChecked('_cbPlaceFilterEditedMoreThan');
   let uFP_filterL0 = uroUtils.GetCBChecked('_cbHidePlacesL0');
   let uFP_filterL1 = uroUtils.GetCBChecked('_cbHidePlacesL1');
   let uFP_filterL2 = uroUtils.GetCBChecked('_cbHidePlacesL2');
   let uFP_filterL3 = uroUtils.GetCBChecked('_cbHidePlacesL3');
   let uFP_filterL4 = uroUtils.GetCBChecked('_cbHidePlacesL4');
   let uFP_filterL5 = uroUtils.GetCBChecked('_cbHidePlacesL5');
   let uFP_filterStaff = uroUtils.GetCBChecked('_cbHidePlacesStaff');
   let uFP_filterAL = uroUtils.GetCBChecked('_cbHidePlacesAdLocked');
   let uFP_filterOnLockLevel = (uFP_filterL0 || uFP_filterL1 || uFP_filterL2 || uFP_filterL3 || uFP_filterL4 || uFP_filterL5 || uFP_filterStaff);
   let uFP_filterNoPhotos = uroUtils.GetCBChecked('_cbHideNoPhotoPlaces');
   let uFP_filterWithPhotos = uroUtils.GetCBChecked('_cbHidePhotoPlaces');
   let uFP_filterNoLinks = uroUtils.GetCBChecked('_cbHideNoLinkedPlaces');
   let uFP_filterWithLinks = uroUtils.GetCBChecked('_cbHideLinkedPlaces');
   let uFP_filterNoDescription = uroUtils.GetCBChecked('_cbHideNonDescribedPlaces');
   let uFP_filterWithDescription = uroUtils.GetCBChecked('_cbHideDescribedPlaces');
   let uFP_filterNoKeyword = uroUtils.GetCBChecked('_cbHideKeywordPlaces');
   let uFP_filterKeyword = uroUtils.GetCBChecked('_cbHideNoKeywordPlaces');
   let uFP_filterPrivate = uroUtils.GetCBChecked('_cbFilterPrivatePlaces');
   let uFP_invertFilters = uroUtils.GetCBChecked('_cbInvertPlacesFilter');
   let uFP_masterEnable = uroIsFilteringEnabled(false);
   let uFP_filterAreaPlaces = uroUtils.GetCBChecked('_cbHideAreaPlaces');
   let uFP_filterPointPlaces = uroUtils.GetCBChecked('_cbHidePointPlaces');
   let uFP_filterCreatedBy = uroUtils.GetCBChecked('_cbShowOnlyPlacesCreatedBy');
   let uFP_filterEditedBy = uroUtils.GetCBChecked('_cbShowOnlyPlacesEditedBy');
   let uFP_filterHideCreatedBy = uroUtils.GetCBChecked('_cbHideOnlyPlacesCreatedBy');
   let uFP_filterHideEditedBy = uroUtils.GetCBChecked('_cbHideOnlyPlacesEditedBy');

   let uFP_hidePURsForFilteredPlaces = uroUtils.GetCBChecked('_cbHidePURsForFilteredPlaces');

   let uFP_NameKeyword = document.getElementById('_textKeywordPlace').value.toLowerCase();
   let uFP_thresholdMinDays = document.getElementById('_inputFilterPlaceEditMinDays').value;
   let uFP_thresholdMaxDays = document.getElementById('_inputFilterPlaceEditMaxDays').value;

   uroPURsToHide = [];

   for(let v=0; v<uroVenueLayer.features.length; v++)
   {
      placeStyle = 'visible';
      if(uFP_masterEnable === true)
      {
         let lmObj = uroVenueLayer.features[v];

         // when an area place is selected, the drag points for editing the place outline now get added as objects into uroVenueLayer.features,
         // however none of these objects had the .attributes.repositoryObject property - whilst the devs have now replaced this with the almost
         // identical .wazeFeature._wmeObject property, it's unclear if drag points still need to be excluded from this scan, so the check
         // remains in place as a "let's just make sure it has it before trying to use it"...
         if(lmObj?.attributes?.wazeFeature?._wmeObject != null)
         {
            lmObj = lmObj.attributes.wazeFeature._wmeObject.attributes;
            if(lmObj.id < 0)
            {
               // don't apply filtering to newly-created places - this allows the user to leave their filtering settings unchanged whilst
               // adding a new place which, once saved, would then be hidden...
               break;
            }

            if(uFP_filterAreaPlaces)
            {
               if(lmObj.geometry.id.indexOf('Polygon') !== -1)
               {
                  placeStyle = 'hidden';
               }
            }
            if(uFP_filterPointPlaces)
            {
               if(lmObj.geometry.id.indexOf('Point') !== -1)
               {
                  placeStyle = 'hidden';
               }
            }


            if(placeStyle == 'visible')
            {
               if((uFP_filterEditedLessThan) || (uFP_filterEditedMoreThan))
               {
                  let editDate = lmObj.updatedOn;
                  if(editDate === undefined)
                  {
                     // where a place has never been edited since its creation, use the creation date instead...
                     editDate = lmObj.createdOn;
                  }
                  if(editDate != null)
                  {
                     let editDaysAgo = uroUtils.DateToDays(editDate);
                     if(uFP_filterEditedLessThan)
                     {
                        if(editDaysAgo < uFP_thresholdMinDays)
                        {
                           placeStyle = 'hidden';
                        }
                     }
                     if(uFP_filterEditedMoreThan)
                     {
                        if(editDaysAgo > uFP_thresholdMaxDays)
                        {
                           placeStyle = 'hidden';
                        }
                     }
                  }
               }
            }

            if(placeStyle == 'visible')
            {
               if(uFP_filterOnLockLevel)
               {
                  let lockLevel = lmObj.lockRank;
                  if ((uFP_filterL0) && (lockLevel === 0)) placeStyle = 'hidden';
                  if ((uFP_filterL1) && (lockLevel === 1)) placeStyle = 'hidden';
                  if ((uFP_filterL2) && (lockLevel === 2)) placeStyle = 'hidden';
                  if ((uFP_filterL3) && (lockLevel === 3)) placeStyle = 'hidden';
                  if ((uFP_filterL4) && (lockLevel === 4)) placeStyle = 'hidden';
                  if ((uFP_filterL5) && (lockLevel === 5)) placeStyle = 'hidden';
                  if ((uFP_filterStaff) && (lockLevel === 6)) placeStyle = 'hidden';
               }
            }

            if(placeStyle == 'visible')
            {
               if(uFP_filterAL)
               {
                  if(lmObj.adLocked) placeStyle = 'hidden';
               }
            }

            if(placeStyle == 'visible')
            {
               if(uFP_filterNoPhotos || uFP_filterWithPhotos)
               {
                  let nPhotos = 0;
                  for(let loop=0; loop<lmObj.images.length; loop++)
                  {
                     if(lmObj.images[loop].attributes.approved) nPhotos++;
                  }
                  if((uFP_filterNoPhotos) && (nPhotos === 0)) placeStyle = 'hidden';
                  if((uFP_filterWithPhotos) && (nPhotos !== 0)) placeStyle = 'hidden';
               }
            }

            if(placeStyle == 'visible')
            {
               if(uFP_filterNoLinks || uFP_filterWithLinks)
               {
                  let nLinks = lmObj.externalProviderIDs.length;
                  if((uFP_filterNoLinks) && (nLinks === 0)) placeStyle = 'hidden';
                  if((uFP_filterWithLinks) && (nLinks !== 0)) placeStyle = 'hidden';
               }
            }

            if(placeStyle == 'visible')
            {
              if(uFP_filterNoDescription || uFP_filterWithDescription)
              {
                let lDesc = lmObj.description.length;
                if((uFP_filterNoDescription) && (lDesc === 0)) placeStyle = 'hidden';
                if((uFP_filterWithDescription) && (lDesc !== 0)) placeStyle = 'hidden';
              }
            }

            if(placeStyle == 'visible')
            {
               if((uFP_filterPrivate === true) && (lmObj.residential === true))
               {
                  placeStyle = 'hidden';
               }
               else
               {
                  for(let cat=0; cat<filterCats.length; cat++)
                  {
                     if(_.includes(lmObj.categories, filterCats[cat]))
                     {
                        placeStyle = 'hidden';
                        break;
                     }
                  }
               }
            }

            if(placeStyle == 'visible')
            {
               if(uFP_filterNoKeyword || uFP_filterKeyword)
               {
                  let venueName = lmObj.name.toLowerCase();
                  let noKeywordMatch = true;
                  if(uFP_NameKeyword === '')
                  {
                     noKeywordMatch = (venueName !== '');
                  }
                  else
                  {
                     noKeywordMatch = (venueName.indexOf(uFP_NameKeyword) === -1);
                  }

                  if(!noKeywordMatch && uFP_filterNoKeyword) placeStyle = 'hidden';
                  if(noKeywordMatch && uFP_filterKeyword) placeStyle = 'hidden';
               }
            }

            if(placeStyle == 'visible')
            {
               if(filterNameID != null)
               {
                  if(uFP_filterCreatedBy === true)
                  {
                     if(filterNameID != lmObj.createdBy) placeStyle = 'hidden';
                  }
                  if(uFP_filterEditedBy === true)
                  {
                     if(filterNameID != lmObj.updatedBy) placeStyle = 'hidden';
                  }
               }
            }
            if(placeStyle == 'visible')
            {
               if(filterHideNameID != null)
               {
                  if(uFP_filterHideCreatedBy === true)
                  {
                     if(filterHideNameID == lmObj.createdBy) placeStyle = 'hidden';
                  }
                  if(uFP_filterHideEditedBy === true)
                  {
                     if(filterHideNameID == lmObj.updatedBy) placeStyle = 'hidden';
                  }
               }
            }

            if(uFP_invertFilters === true)
            {
               if(placeStyle == 'hidden') placeStyle = 'visible';
               else placeStyle = 'hidden';
            }
         }

         if((placeStyle == 'hidden') && (uFP_hidePURsForFilteredPlaces === true))
         {
            uroPURsToHide.push(lmObj.id);
         }
      }

      let geoID = uroVenueLayer.features[v].geometry.id;
      if(document.getElementById(geoID) !== null)
      {
         document.getElementById(geoID).style.visibility = placeStyle;
      }
   }

   uro_uFP[uroEnums.FP_OPTS.filterUneditable] = uroUtils.GetCBChecked('_cbFilterUneditablePlaceUpdates');
   uro_uFP[uroEnums.FP_OPTS.filterInsideManagedAreas] = uroUtils.GetCBChecked('_cbPURFilterInsideManagedAreas');
   uro_uFP[uroEnums.FP_OPTS.excludeMyAreas] = uroUtils.GetCBChecked('_cbPURExcludeUserArea');
   uro_uFP[uroEnums.FP_OPTS.filterLockRanked] = uroUtils.GetCBChecked('_cbFilterLockRankedPlaceUpdates');
   uro_uFP[uroEnums.FP_OPTS.filterFlagged] = uroUtils.GetCBChecked("_cbFilterFlaggedPUR");
   uro_uFP[uroEnums.FP_OPTS.filterNewPlace] = uroUtils.GetCBChecked("_cbFilterNewPlacePUR");
   uro_uFP[uroEnums.FP_OPTS.filterUpdatedDetails] = uroUtils.GetCBChecked("_cbFilterUpdatedDetailsPUR");
   uro_uFP[uroEnums.FP_OPTS.filterNewPhoto] = uroUtils.GetCBChecked("_cbFilterNewPhotoPUR");
   uro_uFP[uroEnums.FP_OPTS.filterMinPURAge] = uroUtils.GetCBChecked('_cbEnablePURMinAgeFilter');
   uro_uFP[uroEnums.FP_OPTS.filterMaxPURAge] = uroUtils.GetCBChecked('_cbEnablePURMaxAgeFilter');
   uro_uFP[uroEnums.FP_OPTS.invertPURFilters] = uroUtils.GetCBChecked('_cbInvertPURFilters');
   uro_uFP[uroEnums.FP_OPTS.leavePURGeos] = uroUtils.GetCBChecked('_cbLeavePURGeos');
   uro_uFP[uroEnums.FP_OPTS.filterCFPhone] = uroUtils.GetCBChecked('_cbPURFilterCFPhone');
   uro_uFP[uroEnums.FP_OPTS.filterCFName] = uroUtils.GetCBChecked('_cbPURFilterCFName');
   uro_uFP[uroEnums.FP_OPTS.filterCFEntryExitPoints] = uroUtils.GetCBChecked('_cbPURFilterCFEntryExitPoints');
   uro_uFP[uroEnums.FP_OPTS.filterCFOpeningHours] = uroUtils.GetCBChecked('_cbPURFilterCFOpeningHours');
   uro_uFP[uroEnums.FP_OPTS.filterCFAliases] = uroUtils.GetCBChecked('_cbPURFilterCFAliases');
   uro_uFP[uroEnums.FP_OPTS.filterCFServices] = uroUtils.GetCBChecked('_cbPURFilterCFServices');
   uro_uFP[uroEnums.FP_OPTS.filterCFGeometry] = uroUtils.GetCBChecked('_cbPURFilterCFGeometry');
   uro_uFP[uroEnums.FP_OPTS.filterCFHouseNumber] = uroUtils.GetCBChecked('_cbPURFilterCFHouseNumber');
   uro_uFP[uroEnums.FP_OPTS.filterCFCategories] = uroUtils.GetCBChecked('_cbPURFilterCFCategories');
   uro_uFP[uroEnums.FP_OPTS.filterCFDescription] = uroUtils.GetCBChecked('_cbPURFilterCFDescription');

   uro_uFP[uroEnums.FP_OPTS.filterOnCFs] = 
   (
      uro_uFP[uroEnums.FP_OPTS.filterCFPhone] || 
      uro_uFP[uroEnums.FP_OPTS.filterCFName] || 
      uro_uFP[uroEnums.FP_OPTS.filterCFEntryExitPoints] || 
      uro_uFP[uroEnums.FP_OPTS.filterCFOpeningHours]
   );
   uro_uFP[uroEnums.FP_OPTS.filterOnCFs] = 
   (
      uro_uFP[uroEnums.FP_OPTS.filterOnCFs] || 
      uro_uFP[uroEnums.FP_OPTS.filterCFAliases] || 
      uro_uFP[uroEnums.FP_OPTS.filterCFServices] || 
      uro_uFP[uroEnums.FP_OPTS.filterCFGeometry]
   );
   uro_uFP[uroEnums.FP_OPTS.filterOnCFs] = 
   (
      uro_uFP[uroEnums.FP_OPTS.filterOnCFs] || 
      uro_uFP[uroEnums.FP_OPTS.filterCFHouseNumber] || 
      uro_uFP[uroEnums.FP_OPTS.filterCFCategories] || 
      uro_uFP[uroEnums.FP_OPTS.filterCFDescription]
   );

   uro_uFP[uroEnums.FP_OPTS.thresholdMinPURDays] = uroUtils.GetElmValue('_inputPURFilterMinDays');
   uro_uFP[uroEnums.FP_OPTS.thresholdMaxPURDays] = uroUtils.GetElmValue('_inputPURFilterMaxDays');
   uro_uFP[uroEnums.FP_OPTS.isLoggedIn] = W.loginManager.isLoggedIn();
   uro_uFP[uroEnums.FP_OPTS.userRank] = W.loginManager.user.attributes.rank;

   uro_uFP[uroEnums.FP_OPTS.filterInsideManagedAreas] = uro_uFP[uroEnums.FP_OPTS.filterInsideManagedAreas] && (uroGetManagedAreas() !== 0);
   if(uroUtils.GetCBChecked('_cbPURExcludeUserArea') == true)
   {
      uroIgnoreAreasUserID = W.loginManager.user.attributes.id;
   }

   uroPrepForFilterPlaceMarker(uroLayers.ID.PUR, uFP_masterEnable);
   uroPrepForFilterPlaceMarker(uroLayers.ID.PPUR, uFP_masterEnable);
   uroPrepForFilterPlaceMarker(uroLayers.ID.RPUR, uFP_masterEnable);

   uroDBG.PerfMon(pmFunction, pmTStart);
}
function uroPrepForFilterPlaceMarker(markerType, masterEnable)
{
   if(uroLayers.layers[markerType].l?.getVisibility() === true)
   {
      let pu;
      let mObj;
      let vObj;   
      let idList = uroGetMarkerIDs(markerType);
      for(pu of idList)
      {
         mObj = uroGetMarker(markerType, pu);
         if(mObj !== null)
         {
            vObj = W.model.venues.objects[pu];
            uroFilterPlaceMarker(mObj, vObj, masterEnable);
         }
      }
   }
}
function uroGetClosestSegmentToPoint(p)
{
   let retval = null;
   if(W.map.getZoom() >= 16)
   {
      let minDist = 99999999;

      for(let s in W.model.segments.objects)
      {
         if(W.model.segments.objects.hasOwnProperty(s))
         {
            let seg = W.model.segments.getObjectById(s);
            let dist = seg.attributes.geometry.distanceTo(p);
            if(dist < minDist)
            {
               minDist = dist;
               retval = s;
            }
         }
      }
   }
   return retval;
}
function uroIsCamSpeedValid(camObj)
{
   let retval = true;

   let cPoint = camObj.attributes.geometry.getCentroid();
   let nSeg = uroGetClosestSegmentToPoint(cPoint);
   if(nSeg !== null)
   {
      let fwdSpeed = W.model.segments.getObjectById(nSeg).attributes.fwdMaxSpeed;
      let revSpeed = W.model.segments.getObjectById(nSeg).attributes.revMaxSpeed;
      let camSpeed = camObj.attributes.speed;
      if(W.model.isImperial == true)
      {
         fwdSpeed = Math.round(fwdSpeed / 1.609);
         revSpeed = Math.round(revSpeed / 1.609);
         camSpeed = Math.round(camSpeed / 1.609);
      }
      if((camSpeed !== fwdSpeed) && (camSpeed !== revSpeed))
      {
         retval = false;
      }
   }

   return retval;
}
function uroFilterCameras()
{
   let pmTStart = performance.now();
   let pmFunction = "uroFilterCameras";

   if(uroFilterPreamble() === false)
   {
      return;
   }

   if(uroMouseIsDown === false) W.map.camerasLayer.redraw();

   if(uroIsFilteringEnabled(false) === true)
   {
      uroUpdateEditorList(W.model.cameras.objects, '_selectCameraUserID', true, true, false, false);
      let tbUserName = uroUtils.GetElmValue('_textCameraEditor');
      let selector = document.getElementById('_selectCameraUserID');
      let filterNameID = null;
      if(selector.selectedIndex > 0)
      {
         let selUserName = document.getElementById('_selectCameraUserID').selectedOptions[0].innerHTML;
         if(selUserName == tbUserName)
         {
            filterNameID = document.getElementById('_selectCameraUserID').selectedOptions[0].value;
         }
      }
      filterNameID = uroGetUserID(filterNameID, tbUserName);

      let isChecked_cbShowOnlyCamsCreatedBy = uroUtils.GetCBChecked('_cbShowOnlyCamsCreatedBy');
      let isChecked_cbShowOnlyCamsEditedBy = uroUtils.GetCBChecked('_cbShowOnlyCamsEditedBy');
      let isChecked_cbShowOnlyMyCams = uroUtils.GetCBChecked('_cbShowOnlyMyCams');
      let isChecked_cbShowWorldCams = uroUtils.GetCBChecked('_cbShowWorldCams');
      let isChecked_cbShowUSACams = uroUtils.GetCBChecked('_cbShowUSACams');
      let isChecked_cbShowNonWorldCams = uroUtils.GetCBChecked('_cbShowNonWorldCams');
      let isChecked_cbShowSpeedCams = uroUtils.GetCBChecked('_cbShowSpeedCams');
      let isChecked_cbShowRedLightCams = uroUtils.GetCBChecked('_cbShowRedLightCams');
      let isChecked_cbShowDummyCams = uroUtils.GetCBChecked('_cbShowDummyCams');
      let isChecked_cbShowIfNoSpeedSet = uroUtils.GetCBChecked('_cbShowIfNoSpeedSet');
      let isChecked_cbShowIfSpeedSet = uroUtils.GetCBChecked('_cbShowIfSpeedSet');
      let isChecked_cbShowIfInvalidSpeedSet = uroUtils.GetCBChecked('_cbShowIfInvalidSpeedSet');
      let isChecked_cbShowRLCIfNoSpeedSet = uroUtils.GetCBChecked('_cbShowRLCIfNoSpeedSet');
      let isChecked_cbShowRLCIfNonZeroSpeedSet = uroUtils.GetCBChecked('_cbShowRLCIfNonZeroSpeedSet');
      let isChecked_cbShowRLCIfZeroSpeedSet = uroUtils.GetCBChecked('_cbShowRLCIfZeroSpeedSet');
      let isChecked_cbHideCreatedByMe = uroUtils.GetCBChecked('_cbHideCreatedByMe');
      let isChecked_cbHideCreatedByRank0 = uroUtils.GetCBChecked('_cbHideCreatedByRank0');
      let isChecked_cbHideCreatedByRank1 = uroUtils.GetCBChecked('_cbHideCreatedByRank1');
      let isChecked_cbHideCreatedByRank2 = uroUtils.GetCBChecked('_cbHideCreatedByRank2');
      let isChecked_cbHideCreatedByRank3 = uroUtils.GetCBChecked('_cbHideCreatedByRank3');
      let isChecked_cbHideCreatedByRank4 = uroUtils.GetCBChecked('_cbHideCreatedByRank4');
      let isChecked_cbHideCreatedByRank5 = uroUtils.GetCBChecked('_cbHideCreatedByRank5');
      let isChecked_cbHideUpdatedByMe = uroUtils.GetCBChecked('_cbHideUpdatedByMe');
      let isChecked_cbHideUpdatedByRank0 = uroUtils.GetCBChecked('_cbHideUpdatedByRank0');
      let isChecked_cbHideUpdatedByRank1 = uroUtils.GetCBChecked('_cbHideUpdatedByRank1');
      let isChecked_cbHideUpdatedByRank2 = uroUtils.GetCBChecked('_cbHideUpdatedByRank2');
      let isChecked_cbHideUpdatedByRank3 = uroUtils.GetCBChecked('_cbHideUpdatedByRank3');
      let isChecked_cbHideUpdatedByRank4 = uroUtils.GetCBChecked('_cbHideUpdatedByRank4');
      let isChecked_cbHideUpdatedByRank5 = uroUtils.GetCBChecked('_cbHideUpdatedByRank5');
      let isChecked_HideManualLockedCams = uroUtils.GetCBChecked('_cbHideManualLockedCams');
      let isChecked_cbHideCWLCams = uroUtils.GetCBChecked('_cbHideCWLCams');
      let isChecked_cbHighlightInsteadOfHideCams = uroUtils.GetCBChecked('_cbHighlightInsteadOfHideCams');
      let isChecked_InvertFiltere = uroUtils.GetCBChecked('_cbInvertCamFilters');

      let nCameras = uroLayers.layers[uroLayers.ID.cam].l.features.length;
      for (let i = 0; i < nCameras; ++i)
      {
         let uroCamUpdater = '';
         let uroCamUpdaterRank = -1;
         let uroCamCreator = '';
         let uroCamCreatorRank = -1;
         let wf = uroLayers.layers[uroLayers.ID.cam].l.features[i].attributes.wazeFeature;
         // When a camera is selected, the alignment/positioning UI elements get added to features[].
         // As these elements aren't camera markers and therefore have no attributes, we need to
         // ignore them to prevent errors in the filtering code below...
         if(wf !== undefined)
         {
            let uroCam = wf._wmeObject;
            let uroCamStyle = 'visible';

            if(uroCam.attributes.createdBy !== null)
            {
               if(W.model.users.objects[uroCam.attributes.createdBy] != null)
               {
                  uroCamCreator = W.model.users.objects[uroCam.attributes.createdBy].attributes.userName;
                  uroCamCreatorRank = W.model.users.objects[uroCam.attributes.createdBy].attributes.rank;
               }
            }

            if(uroCam.attributes.updatedBy !== null)
            {
               if(W.model.users.objects[uroCam.attributes.updatedBy] != null)
               {
                  uroCamUpdater = W.model.users.objects[uroCam.attributes.updatedBy].attributes.userName;
                  uroCamUpdaterRank = W.model.users.objects[uroCam.attributes.updatedBy].attributes.rank;
               }
            }

            let uroCamType = uroCam.attributes.type;
            let camIsAutoLocked = (uroCam.attributes.lockRank === null);

            if(isChecked_HideManualLockedCams === true)
            {
               if(camIsAutoLocked === false) uroCamStyle = 'hidden';
            }

            if(filterNameID != null)
            {
               if(isChecked_cbShowOnlyCamsCreatedBy === true)
               {
                  if(filterNameID != uroCam.attributes.createdBy) uroCamStyle = 'hidden';
               }
               if(isChecked_cbShowOnlyCamsEditedBy === true)
               {
                  if(filterNameID != uroCam.attributes.updatedBy) uroCamStyle = 'hidden';
               }
            }

            if(isChecked_cbShowOnlyMyCams === true)
            {
               if((uroUserID != uroCam.attributes.createdBy)&&(uroUserID != uroCam.attributes.updatedBy)) uroCamStyle = 'hidden';
            }

            if((isChecked_cbShowWorldCams === false) || (isChecked_cbShowUSACams === false) || (isChecked_cbShowNonWorldCams === false))
            {
               let posWorld = uroCamCreator.indexOf('world_');
               let posUSA = uroCamCreator.indexOf('usa_');

               if((isChecked_cbShowWorldCams === false) && (posWorld === 0)) uroCamStyle = 'hidden';
               if((isChecked_cbShowUSACams === false) && (posUSA === 0)) uroCamStyle = 'hidden';
               if((isChecked_cbShowNonWorldCams === false) && (posWorld !== 0) && (posUSA !== 0)) uroCamStyle = 'hidden';
            }

            if((isChecked_cbShowSpeedCams === false) || (isChecked_cbShowRedLightCams === false) || (isChecked_cbShowDummyCams === false))
            {
               if((isChecked_cbShowSpeedCams === false) && (uroCamType == 2)) uroCamStyle = 'hidden';
               if((isChecked_cbShowRedLightCams === false) && (uroCamType == 4)) uroCamStyle = 'hidden';
               if((isChecked_cbShowDummyCams === false) && (uroCamType == 3)) uroCamStyle = 'hidden';
            }

            if((isChecked_cbShowSpeedCams === true) && (uroCamType == 2))
            {
               if((isChecked_cbShowIfNoSpeedSet === false) && (uroCam.attributes.speed === null)) uroCamStyle = 'hidden';
               if((isChecked_cbShowIfSpeedSet === false) && (uroCam.attributes.speed !== null)) uroCamStyle = 'hidden';
               if(isChecked_cbShowIfInvalidSpeedSet === false)
               {
                  if(uroIsCamSpeedValid(uroCam) === false)
                  {
                     uroCamStyle = 'hidden';
                  }
               }
            }

            if((isChecked_cbShowRedLightCams === true) && (uroCamType == 4))
            {
               if((isChecked_cbShowRLCIfNoSpeedSet === false) && (uroCam.attributes.speed === null)) uroCamStyle = 'hidden';
               if((isChecked_cbShowRLCIfNonZeroSpeedSet === false) && (uroCam.attributes.speed > 0)) uroCamStyle = 'hidden';
               if((isChecked_cbShowRLCIfZeroSpeedSet === false) && (uroCam.attributes.speed === 0)) uroCamStyle = 'hidden';
            }

            if(isChecked_cbHideCreatedByMe === true)
            {
               if(uroUserID == uroCam.attributes.createdBy) uroCamStyle = 'hidden';
            }
            if((isChecked_cbHideCreatedByRank0 === true) && (uroCamCreatorRank === 0)) uroCamStyle = 'hidden';
            if((isChecked_cbHideCreatedByRank1 === true) && (uroCamCreatorRank == 1)) uroCamStyle = 'hidden';
            if((isChecked_cbHideCreatedByRank2 === true) && (uroCamCreatorRank == 2)) uroCamStyle = 'hidden';
            if((isChecked_cbHideCreatedByRank3 === true) && (uroCamCreatorRank == 3)) uroCamStyle = 'hidden';
            if((isChecked_cbHideCreatedByRank4 === true) && (uroCamCreatorRank == 4)) uroCamStyle = 'hidden';
            if((isChecked_cbHideCreatedByRank5 === true) && (uroCamCreatorRank == 5)) uroCamStyle = 'hidden';

            if(isChecked_cbHideUpdatedByMe === true)
            {
               if(uroUserID == uroCam.attributes.updatedBy) uroCamStyle = 'hidden';
            }
            if((isChecked_cbHideUpdatedByRank0 === true) && (uroCamUpdaterRank === 0)) uroCamStyle = 'hidden';
            if((isChecked_cbHideUpdatedByRank1 === true) && (uroCamUpdaterRank == 1)) uroCamStyle = 'hidden';
            if((isChecked_cbHideUpdatedByRank2 === true) && (uroCamUpdaterRank == 2)) uroCamStyle = 'hidden';
            if((isChecked_cbHideUpdatedByRank3 === true) && (uroCamUpdaterRank == 3)) uroCamStyle = 'hidden';
            if((isChecked_cbHideUpdatedByRank4 === true) && (uroCamUpdaterRank == 4)) uroCamStyle = 'hidden';
            if((isChecked_cbHideUpdatedByRank5 === true) && (uroCamUpdaterRank == 5)) uroCamStyle = 'hidden';

            if((isChecked_cbHideCWLCams === true) && (uroOWL.IsCamOnWatchList(uroCam.attributes.id) != -1)) uroCamStyle = 'hidden';

            if(isChecked_InvertFiltere === true)
            {
               if(uroCamStyle == "hidden")
               {
                  uroCamStyle = "";
               }
               else
               {
                  uroCamStyle = "hidden";
               }
            }

            let uroCamGeometryID =  uroLayers.layers[uroLayers.ID.cam].l.features[i].geometry.id;
            let svgElm = document.getElementById(uroCamGeometryID);

            if(svgElm !== null)
            {
               let origImage;
               if(uroCamStyle == "hidden")
               {
                  if(isChecked_cbHighlightInsteadOfHideCams === true)
                  {
                     // set the "highlight" camera image here...
                     let hrefImage = svgElm.getAttribute("xlink:href");
                     origImage = svgElm.getAttribute("origImage");
                     if((hrefImage === origImage) || (origImage === null))
                     {
                        svgElm.setAttribute("origImage", hrefImage);
                        svgElm.setAttribute("xlink:href", uroImages.HighlightedCameraImages[(uroCamType-2)]);

                        svgElm.addEventListener("mouseover", uroMarkers.MouseOver, false);
                        svgElm.addEventListener("mouseout", uroMarkers.MouseOut, false);
                     }
                  }
                  else
                  {
                     svgElm.remove();
                  }
               }
               else
               {
                  // restore the original camera image here...
                  if(svgElm.getAttribute("origImage") !== null)
                  {
                     origImage = svgElm.getAttribute("origImage");
                     svgElm.setAttribute("xlink:href", origImage);
                     svgElm.removeAttribute("origImage");
                  }
               }
            }
         }
      }
   }
   uroDBG.PerfMon(pmFunction, pmTStart);
}
function uroFilterMapComments()
{
   let pmTStart = performance.now();
   let pmFunction = "uroFilterMapComments";

   if(uroFilterPreamble() === false) return;

   let uFURs_masterEnable = uroIsFilteringEnabled(false);
   let filterDescMustBePresent = uroUtils.GetCBChecked('_cbMCDescriptionMustBePresent');
   let filterDescMustBeAbsent = uroUtils.GetCBChecked('_cbMCDescriptionMustBeAbsent');
   let filterKeywordMustBePresent = uroUtils.GetCBChecked('_cbMCEnableKeywordMustBePresent');
   let filterKeywordMustBeAbsent = uroUtils.GetCBChecked('_cbMCEnableKeywordMustBeAbsent');
   let filterMyFollowed = uroUtils.GetCBChecked('_cbMCHideMyFollowed');
   let filterMyUnfollowed = uroUtils.GetCBChecked('_cbMCHideMyUnfollowed');
   let filterRoadworks = uroUtils.GetCBChecked('_cbMCFilterRoadworks');
   let filterConstruction = uroUtils.GetCBChecked('_cbMCFilterConstruction');
   let filterClosure = uroUtils.GetCBChecked('_cbMCFilterClosure');
   let filterEvent = uroUtils.GetCBChecked('_cbMCFilterEvent');
   let filterNote = uroUtils.GetCBChecked('_cbMCFilterNote');
   let filterWSLM = uroUtils.GetCBChecked('_cbMCFilterWSLM');
   let filterBOG = uroUtils.GetCBChecked('_cbMCFilterBOG');
   let filterDifficult = uroUtils.GetCBChecked('_cbMCFilterDifficult');
   let invertFilters = uroUtils.GetCBChecked('_cbInvertMCFilter');
   let keywordPresent = uroUtils.GetElmValue('_textMCKeywordPresent');
   let keywordAbsent = uroUtils.GetElmValue('_textMCKeywordAbsent');
   let caseInsensitive = uroUtils.GetCBChecked('_cbMCCaseInsensitive');
   let filterCommentsMustBePresent = uroUtils.GetCBChecked('_cbMCCommentsMustBePresent');
   let filterCommentsMustBeAbsent = uroUtils.GetCBChecked('_cbMCCommentsMustBeAbsent');

   let filterExpiryMustBePresent = uroUtils.GetCBChecked('_cbMCExpiryMustBePresent');
   let filterExpiryMustBeAbsent = uroUtils.GetCBChecked('_cbMCExpiryMustBeAbsent');
   let filterByCreatorEnable = uroUtils.GetCBChecked('_cbMCCreatorIDFilter');
   let filterL1 = uroUtils.GetCBChecked('_cbHideMCRank0');
   let filterL2 = uroUtils.GetCBChecked('_cbHideMCRank1');
   let filterL3 = uroUtils.GetCBChecked('_cbHideMCRank2');
   let filterL4 = uroUtils.GetCBChecked('_cbHideMCRank3');
   let filterL5 = uroUtils.GetCBChecked('_cbHideMCRank4');
   let filterL6 = uroUtils.GetCBChecked('_cbHideMCRank5');
   
   let filterWRCMC = uroUtils.GetCBChecked('_cbHideWRCMCs');
   
   let selectorCreator = document.getElementById('_selectMCCreatorID');

   if(filterByCreatorEnable === false)
   {
      while(selectorCreator.options.length > 0)
      {
         selectorCreator.options.remove(0);
      }
   }
   let creatorUser = null;
   if(filterByCreatorEnable === true)
   {
      if(selectorCreator.options.length === 0)
      {
         uroUpdateEditorList(W.model.mapComments.objects, '_selectMCCreatorID', true, false, false, false);
      }
      if(selectorCreator.selectedOptions[0] != null)
      {
         creatorUser = parseInt(selectorCreator.selectedOptions[0].value);
      }
   }

   for (let mcIdx = 0; mcIdx < uroMCLayer.features.length; mcIdx++)
   {
      {
         let mcObj = uroMCLayer?.features[mcIdx]?.attributes?.wazeFeature?._wmeObject;
         if(mcObj !== undefined)
         {
            let desc = '';
            if(mcObj.attributes.subject !== null) desc += mcObj.attributes.subject.replace(/<\/?[^>]+(>|$)/g, "");
            if(mcObj.attributes.body !== null) desc += mcObj.attributes.body.replace(/<\/?[^>]+(>|$)/g, "");
            let nComments = mcObj.attributes.conversation.length;
            if(nComments > 0)
            {
               for(let cIdx=0; cIdx < nComments; cIdx++)
               {
                  desc += mcObj.attributes.conversation[cIdx].text.replace(/<\/?[^>]+(>|$)/g, "");
               }
            }

            let mcStyle = 'visible';
            if(uroIgnore.IsOnList(mcObj.attributes.id)) mcStyle = 'hidden';
            
            if(uFURs_masterEnable === true)
            {
               let ukroadworks_ur = false;
               let construction_ur = false;
               let closure_ur = false;
               let event_ur = false;
               let note_ur = false;
               let wslm_ur = false;
               let bog_ur = false;
               let difficult_ur = false;

               let filterByNotIncludedKeyword = false;
               let filterByIncludedKeyword = true;

               let customType = uroGetCustomType(null, "mc", desc);
               if(customType === 0) ukroadworks_ur = true;
               else if(customType === 1) construction_ur = true;
               else if(customType === 2) closure_ur = true;
               else if(customType === 3) event_ur = true;
               else if(customType === 4) note_ur = true;
               else if(customType === 5) wslm_ur = true;
               else if(customType === 6) bog_ur = true;
               else if(customType === 7) difficult_ur = true;

               let rank = mcObj.attributes.lockRank;
               let expiry = mcObj.attributes.endDate;                  

               // keywords
               if(mcStyle == 'visible')
               {
                  if(filterDescMustBePresent === true)
                  {
                     if(desc === '') mcStyle = 'hidden';
                  }
                  if(filterDescMustBeAbsent === true)
                  {
                     if(desc !== '') mcStyle = 'hidden';
                  }

                  if(filterCommentsMustBePresent === true)
                  {
                     if(nComments === 0) mcStyle = 'hidden';
                  }
                  if(filterCommentsMustBeAbsent === true)
                  {
                     if(nComments > 0) mcStyle = 'hidden';
                  }

                  if(filterKeywordMustBePresent === true)
                  {
                     let keywordIsPresentInDesc = uroUtils.KeywordPresent(desc,keywordPresent,caseInsensitive);
                     filterByIncludedKeyword = (filterByIncludedKeyword && (!keywordIsPresentInDesc));
                  }
                  if(filterKeywordMustBeAbsent === true)
                  {
                     let keywordIsAbsentInDesc = uroUtils.KeywordPresent(desc,keywordAbsent,caseInsensitive);
                     filterByNotIncludedKeyword = (filterByNotIncludedKeyword || keywordIsAbsentInDesc);
                  }

                  filterByNotIncludedKeyword = (filterByNotIncludedKeyword && filterKeywordMustBeAbsent);
                  filterByIncludedKeyword = (filterByIncludedKeyword && filterKeywordMustBePresent);
                  if(filterByNotIncludedKeyword || filterByIncludedKeyword)
                  {
                     mcStyle = 'hidden';
                  }

               }

               //lock rank
               if(mcStyle == 'visible')
               {
                  if((filterL1 === true) && (rank == 0)) mcStyle = 'hidden';
                  if((filterL2 === true) && (rank == 1)) mcStyle = 'hidden';
                  if((filterL3 === true) && (rank == 2)) mcStyle = 'hidden';
                  if((filterL4 === true) && (rank == 3)) mcStyle = 'hidden';
                  if((filterL5 === true) && (rank == 4)) mcStyle = 'hidden';
                  if((filterL6 === true) && (rank == 5)) mcStyle = 'hidden';
               }

               // expiry
               if(mcStyle == 'visible')
               {
                  if((filterExpiryMustBePresent === true) && (expiry === null)) mcStyle = 'hidden';
                  if((filterExpiryMustBeAbsent === true) && (expiry != null)) mcStyle = 'hidden';
               }

               // is following?
               if(mcStyle == 'visible')
               {
                  if(mcObj.attributes.isFollowing === true)
                  {
                     if(filterMyFollowed === true) mcStyle = 'hidden';
                  }
                  else
                  {
                     if(filterMyUnfollowed === true) mcStyle = 'hidden';
                  }
               }

               if(mcStyle == 'visible')
               {
                  if(creatorUser !== null)
                  {
                     if(mcObj.attributes.createdBy != creatorUser) mcStyle = 'hidden';
                  }
                  
                  if(filterWRCMC === true)
                  {
                     if(mcObj.attributes.createdBy == 304740435) mcStyle = 'hidden';
                  }
               }

               // custom tags
               if(mcStyle == 'visible')
               {
                  if(ukroadworks_ur === true)
                  {
                     if(filterRoadworks === true) mcStyle = 'hidden';
                  }
                  else if(construction_ur === true)
                  {
                     if(filterConstruction === true) mcStyle = 'hidden';
                  }
                  else if(closure_ur === true)
                  {
                     if(filterClosure === true) mcStyle = 'hidden';
                  }
                  else if(event_ur === true)
                  {
                     if(filterEvent === true) mcStyle = 'hidden';
                  }
                  else if(note_ur === true)
                  {
                     if(filterNote === true) mcStyle = 'hidden';
                  }
                  else if(wslm_ur === true)
                  {
                     if(filterWSLM === true) mcStyle = 'hidden';
                  }
                  else if(bog_ur === true)
                  {
                     if(filterBOG === true) mcStyle = 'hidden';
                  }
                  else if(difficult_ur === true)
                  {
                     if(filterDifficult === true) mcStyle = 'hidden';
                  }

                  if(invertFilters === true)
                  {
                     if(mcStyle == 'hidden') mcStyle = 'visible';
                     else mcStyle = 'hidden';
                  }
               }
            }

            let geoID = uroMCLayer.features[mcIdx].geometry.id;
            if(document.getElementById(geoID) !== null)
            {
               document.getElementById(geoID).style.visibility = mcStyle;
            }
         }
      }
   }
   uroDBG.PerfMon(pmFunction, pmTStart);
}
function uroFilterURs_onObjectsChanged()
{
   if(uroFilterPreamble())
   {
      if(uroURDialogIsOpen === true)
      {
         uroFilterURs();
      }
   }
}
function uroFilterURs_onObjectsAdded()
{
   if(uroFilterPreamble())
   {
   }
}
function uroFilterURs_onObjectsRemoved()
{
   if(uroFilterPreamble())
   {
   }
}
function uroGetManagedAreas()
{
   uroManagedAreas = [];
   uroIgnoreAreasUserID = null;

   for(let maObj in W.model.managedAreas.objects)
   {
      if(W.model.managedAreas.objects.hasOwnProperty(maObj))
      {
         uroManagedAreas.push(W.model.managedAreas.objects[maObj]);
      }
   }
   return uroManagedAreas.length;
}
function uroCheckGeometryWithinManagedAreas(geo)
{
   let retval = false;
   let ignoreUserMA = false;

   // If we're ignoring the user's managed area, then we first check to see if
   // the geopoint lies within that - if so then we can skip checking all the
   // other areas in the list...
   if(uroIgnoreAreasUserID !== null)
   {
      for(let uma = 0; uma < uroManagedAreas.length; ++uma)
      {
         if(uroIgnoreAreasUserID == uroManagedAreas[uma].attributes.userID)
         {
            ignoreUserMA = uroContainsPoint(uroManagedAreas[uma].attributes.geoJSONGeometry.coordinates[0], geo);
            break;
         }
      }
   }

   // Point either isn't within the user's area, or we're not ignoring it, so
   // check the rest of the areas in the list
   if(ignoreUserMA == false)
   {
      for(let ma = 0; ma < uroManagedAreas.length; ++ma)
      {
         if(uroIgnoreAreasUserID != uroManagedAreas[ma].attributes.userID)
         {
            retval = uroContainsPoint(uroManagedAreas[ma].attributes.geoJSONGeometry.coordinates[0], geo);
            break;
         }
      }
   }

   return retval;
}
function uroGetURDriveGeoms()
{
   let retval = [];

   for (let urobj in W.model.mapUpdateRequests.objects)
   {
      if(W.model.mapUpdateRequests.objects.hasOwnProperty(urobj))
      {
         let ureq = W.model.mapUpdateRequests.objects[urobj];
         let ureqID = ureq.attributes.id;

         let hasGeo = false;
         let thisRet = [];
         thisRet.push(ureqID);
         thisRet.push(null);
         thisRet.push([]);

         let latMin = 9999;
         let latMax = -9999;
         let lonMin = 9999;
         let lonMax = -9999;

         let urs = W.model.updateRequestSessions.objects[ureqID];
         if((urs !== undefined) && (urs.attributes.driveGeometry !== undefined))
         {
            let cPairs = [];
            for(let i = 0; i < urs.attributes.driveGeometry.coordinates.length; ++i)
            {
               for(let j = 0; j < urs.attributes.driveGeometry.coordinates[i].length; ++j)
               {
                  if((i === 0) || (j > 0))
                  {
                     let coords = urs.attributes.driveGeometry.coordinates[i][j];
                     cPairs.push(coords);

                     if(coords[0] > lonMax)
                     {
                        lonMax = coords[0];
                     }
                     if(coords[0] < lonMin)
                     {
                        lonMin = coords[0];
                     }
                     if(coords[1] > latMax)
                     {
                        latMax = coords[1];
                     }
                     if(coords[1] < latMin)
                     {
                        latMin = coords[1];
                     }

                     hasGeo = true;
                  }
               }
            }
            let bbox = [];
            bbox.push(lonMin);
            bbox.push(lonMax);
            bbox.push(latMin);
            bbox.push(latMax);
            thisRet.push(bbox);
            thisRet.push(cPairs);
         }

         if(hasGeo === true)
         {
            retval.push(thisRet);
         }
      }
   }
   return retval;
}
function uroCompareDriveGeos(geoA, geoB)
{
   const matchLength = 5;
   let retval = false;

   if((geoA.length >= matchLength) && (geoB.length >= matchLength))
   {
      for(let i = 0; i < (geoA.length - matchLength); ++i)
      {
         for(let j = 0; j < (geoB.length - matchLength); ++j)
         {
            if((geoA[i][0] == geoB[j][0]) && (geoA[i][1] == geoB[j][1]))
            {
               retval = true;
               for(let k = 1; k < matchLength; ++k)
               {
                  if((geoA[i+k][0] != geoB[j+k][0]) || (geoA[i+k][1] != geoB[j+k][1]))
                  {
                     retval = false;
                     break;
                  }
               }
            }
            if(retval === true)
            {
               break;
            }
         }

         if(retval === true)
         {
            break;
         }
      }
   }
   return retval;
}
function uroCompareDriveBBoxes(bbA, bbB)
{
   let retval = true;

   if
   (
      (bbA[0] > bbB[1]) ||
      (bbA[1] < bbB[0]) ||
      (bbA[2] > bbB[3]) ||
      (bbA[3] < bbB[2])
   )
   {
      retval = false;
   }

   return retval;
}
function uroGetURDupes()
{
   uroURDupes = [];

   // To determine which URs are duplicates of one another (i.e. have been raised by the same user
   // during the same section of a journey), we first compare the geometries of the drive tracks
   // attached to any URs which have them - as this is based on the users GPS position rather than
   // the WME map data, it makes it vanishingly unlikely that any two users would have identical
   // GPS positions (especially given the level of accuracy to which the track points are stored)
   // even if they were driving exactly the same route at the same speed, in the same lane etc.
   //
   // To accelerate this geometry comparison, we start by performing a simple bounding box overlap
   // check for the two geometries under consideration - if there's no overlap then there can't be
   // any geometry match, so no need to continue onto the more detailed comparision of the GPS
   // tracks themselves...
   let driveGeos = uroGetURDriveGeoms();
   if(driveGeos.length > 1)
   {
      for(let i = 0; i < (driveGeos.length - 1); ++i)
      {
         if(driveGeos[i].length !== 5)
         {
            driveGeos[i][1] = false;
         }
         else
         {
            for(let j = (i + 1); j < driveGeos.length; ++j)
            {
               if(driveGeos[j].length === 5)
               {
                  let geoMatch = uroCompareDriveBBoxes(driveGeos[i][3], driveGeos[j][3]);
                  if(geoMatch === true)
                  {
                     geoMatch = uroCompareDriveGeos(driveGeos[i][4], driveGeos[j][4]);
                     if(geoMatch === true)
                     {
                        driveGeos[i][2].push(driveGeos[j][0]);
                        driveGeos[j][2].push(driveGeos[i][0]);
                     }
                  }
               }
            }
         }
      }

      for(let i = 0; i < driveGeos.length; ++i)
      {
         if(driveGeos[i][2].length > 0)
         {
            let res = [];
            res.push(driveGeos[i][0]);
            res.push(driveGeos[i][2]);
            uroURDupes.push(res);
         }
      }
   }

   // Once we've done the initial drive track comparision, uroURDupes will contain a list of
   // all the URs which were matched up based on that.  However, as the track comparision is
   // inherently limited by the amount of track data included with each UR, this initial
   // comparison may mean some more widely spaced URs aren't flagged as duplicates simply
   // because there's insufficient overlap between their tracks, even though there may have
   // been further URs dropped inbetween to which they were matched.
   //
   // e.g. if a user drops 4 URs along a section of their journey, spaced such that each of
   // the GPS tracks generates a comparison match with the UR either side of it, but no
   // further than that, we would get:
   //
   // UR A......UR B.......UR C......UR D
   //
   // Matches: A to B, B to A & C, C to B & D, D to C
   // 
   // Note how, although we know all of these URs are in fact duplicates, and can infer this
   // from seeing that e.g. A is flagged only as a duplicate of B, however as B is flagged as
   // a duplicate of A & C, and C is flagged as a duplicate of B & D, A MUST therefore be a
   // duplicate of B, C & D...
   //
   // To fix this, we now run a merging pass over each of the entries in uroURDupes - for
   // each entry we see if its ID appears in any of the other entries as a duplicate, and
   // if so we merge the duplicates for both entries.

   for(let i = 0; i < uroURDupes.length; ++i)
   {
      let urID = uroURDupes[i][0];
      for(let j = 0; j < uroURDupes.length; ++j)
      {
         if(i != j)
         {
            if(uroURDupes[j][1].includes(urID) === true)
            {
               // https://stackoverflow.com/questions/1584370/how-to-merge-two-arrays-in-javascript-and-de-duplicate-items
               let mergedDupes = [...new Set([...uroURDupes[i][1], ...uroURDupes[j][1]])];
               uroURDupes[i][1] = mergedDupes;
               uroURDupes[j][1] = mergedDupes;
            }
         }
      }
   }
}
function uroFilterURs()
{
   let pmTStart = performance.now();
   let pmFunction = "uroFilterURs";

   if(uroUserID === -1) 
   {
      return;
   }

   if(uroInhibitURFiltering === true)
   {
      return;
   }


   // compatibility fix for URComments - based on code supplied by RickZabel
   let hasActiveURFilters = false;
   if(uroIsFilteringEnabled(false) === true)
   {
      let urTabInputs = uroTabs.CtrlTabs[uroTabs.IDS.URS][uroTabs.FIELDS.TABBODY].getElementsByTagName('input');
      for(let loop = 0; loop < urTabInputs.length; loop++)
      {
         if(urTabInputs[loop].type == 'checkbox')
         {
            let ignoreCB = false;
            ignoreCB = ignoreCB || (urTabInputs[loop].id == '_cbCaseInsensitive');
            ignoreCB = ignoreCB || (urTabInputs[loop].id == '_cbNoFilterForTaggedURs');
            if((urTabInputs[loop].checked) && (ignoreCB === false))
            {
               hasActiveURFilters = true;
               break;
            }
         }
      }
   }
   sessionStorage.UROverview_hasActiveURFilters = hasActiveURFilters;
   if(uroFilterPreamble() === false) return;
   uroRefreshUpdateRequestSessions();
   let selectorResolver = document.getElementById('_selectURResolverID');
   let selectorCommentUser = document.getElementById('_selectURUserID');
   if(uroUtils.GetCBChecked('_cbURResolverIDFilter') === false)
   {
      while(selectorResolver.options.length > 0)
      {
         selectorResolver.options.remove(0);
      }
   }
   if(uroUtils.GetCBChecked('_cbURUserIDFilter') === false)
   {
      while(selectorCommentUser.options.length > 0)
      {
         selectorCommentUser.options.remove(0);
      }
   }
   if(Object.keys(W.model.updateRequestSessions.objects).length === 0)
   {
      // This may be the case if the user has disabled the UR layer, so call
      // AddCommentCounts to clear any existing ones from the map view...
      uroURExtras.AddCommentCounts();
      return;
   }
   let commenterUser = null;
   if(uroUtils.GetCBChecked('_cbURUserIDFilter') === true)
   {
      if(selectorCommentUser.options.length === 0)
      {
         uroUpdateEditorList(W.model.updateRequestSessions.objects, '_selectURUserID', false, false, false, true);
      }
      if(selectorCommentUser.selectedOptions[0] != null)
      {
         commenterUser = parseInt(selectorCommentUser.selectedOptions[0].value);
      }
   }
   let resolverUser = null;
   if(uroUtils.GetCBChecked('_cbURResolverIDFilter') === true)
   {
      if(selectorResolver.options.length === 0)
      {
         uroUpdateEditorList(W.model.mapUpdateRequests.objects, '_selectURResolverID', false, false, true, false);
      }
      if(selectorResolver.selectedOptions[0] != null)
      {
         resolverUser = parseInt(selectorResolver.selectedOptions[0].value);
      }
   }
   uroURExtras.urList = [];
   uroGetURDupes();

   let uFURs_masterEnable = uroIsFilteringEnabled(false);
   let filterOutsideEditableArea = uroUtils.GetCBChecked('_cbURFilterOutsideArea');
   let filterInsideManagedAreas = uroUtils.GetCBChecked('_cbURFilterInsideManagedAreas');
   let filterSolved = uroUtils.GetCBChecked('_cbFilterSolved');
   let filterUnidentified = uroUtils.GetCBChecked('_cbFilterUnidentified');
   let filterClosed = uroUtils.GetCBChecked('_cbFilterClosedUR');
   let filterOpen = uroUtils.GetCBChecked('_cbFilterOpenUR');
   let filterDescMustBePresent = uroUtils.GetCBChecked('_cbURDescriptionMustBePresent');
   let filterDescMustBeAbsent = uroUtils.GetCBChecked('_cbURDescriptionMustBeAbsent');
   let filterKeywordMustBePresent = uroUtils.GetCBChecked('_cbEnableKeywordMustBePresent');
   let filterKeywordMustBeAbsent = uroUtils.GetCBChecked('_cbEnableKeywordMustBeAbsent');
   let filterMinURAge = uroUtils.GetCBChecked('_cbEnableMinAgeFilter');
   let filterMaxURAge = uroUtils.GetCBChecked('_cbEnableMaxAgeFilter');
   let filterMinComments = uroUtils.GetCBChecked('_cbEnableMinCommentsFilter');
   let filterMaxComments = uroUtils.GetCBChecked('_cbEnableMaxCommentsFilter');
   let filterReporterLastCommenter = uroUtils.GetCBChecked('_cbHideIfReporterLastCommenter');
   let filterReporterNotLastCommenter = uroUtils.GetCBChecked('_cbHideIfReporterNotLastCommenter');
   let filterHideAnyComments = uroUtils.GetCBChecked('_cbHideAnyComments');
   let filterHideNotLastCommenter = uroUtils.GetCBChecked('_cbHideIfNotLastCommenter');
   let filterHideMyComments = uroUtils.GetCBChecked('_cbHideMyComments');
   let filterIfLastCommenter = uroUtils.GetCBChecked('_cbHideIfLastCommenter');
   let filterIfNotLastCommenter = uroUtils.GetCBChecked('_cbHideIfNotLastCommenter');
   let filterCommentMinAge = uroUtils.GetCBChecked('_cbEnableCommentAgeFilter2');
   let filterCommentMaxAge = uroUtils.GetCBChecked('_cbEnableCommentAgeFilter');
   let filterUserID = uroUtils.GetCBChecked('_cbURUserIDFilter');
   let filterMyFollowed = uroUtils.GetCBChecked('_cbHideMyFollowed');
   let filterMyUnfollowed = uroUtils.GetCBChecked('_cbHideMyUnfollowed');

   let filterWazeAuto = uroUtils.GetCBChecked('_cbFilterWazeAuto');
   let filterRoadworks = uroUtils.GetCBChecked('_cbFilterRoadworks');
   let filterConstruction = uroUtils.GetCBChecked('_cbFilterConstruction');
   let filterClosure = uroUtils.GetCBChecked('_cbFilterClosure');
   let filterEvent = uroUtils.GetCBChecked('_cbFilterEvent');
   let filterNote = uroUtils.GetCBChecked('_cbFilterNote');
   let filterWSLM = uroUtils.GetCBChecked('_cbFilterWSLM');
   let filterBOG = uroUtils.GetCBChecked('_cbFilterBOG');
   let filterDifficult = uroUtils.GetCBChecked('_cbFilterDifficult');

   let filterIncorrectTurn = uroUtils.GetCBChecked('_cbFilterIncorrectTurn');
   let filterIncorrectAddress = uroUtils.GetCBChecked('_cbFilterIncorrectAddress');
   let filterIncorrectRoute = uroUtils.GetCBChecked('_cbFilterIncorrectRoute');
   let filterMissingRoundabout = uroUtils.GetCBChecked('_cbFilterMissingRoundabout');
   let filterGeneralError = uroUtils.GetCBChecked('_cbFilterGeneralError');
   let filterTurnNotAllowed = uroUtils.GetCBChecked('_cbFilterTurnNotAllowed');
   let filterIncorrectJunction = uroUtils.GetCBChecked('_cbFilterIncorrectJunction');
   let filterMissingBridgeOverpass = uroUtils.GetCBChecked('_cbFilterMissingBridgeOverpass');
   let filterWrongDrivingDirection = uroUtils.GetCBChecked('_cbFilterWrongDrivingDirection');
   let filterMissingExit = uroUtils.GetCBChecked('_cbFilterMissingExit');
   let filterMissingRoad = uroUtils.GetCBChecked('_cbFilterMissingRoad');
   let filterMissingLandmark = uroUtils.GetCBChecked('_cbFilterMissingLandmark');
   let filterNativeSpeedLimit = uroUtils.GetCBChecked('_cbFilterSpeedLimits');
   let filterBlockedRoad = uroUtils.GetCBChecked('_cbFilterBlockedRoad');
   let filterUndefined = uroUtils.GetCBChecked('_cbFilterUndefined');

   let invertURFilters = uroUtils.GetCBChecked('_cbInvertURFilter');
   let invertURStateFilters = uroUtils.GetCBChecked('_cbInvertURStateFilter');
   let noFilterTaggedURs = uroUtils.GetCBChecked('_cbNoFilterForTaggedURs');
   let noFilterURInURL = uroUtils.GetCBChecked('_cbNoFilterForURInURL');
   let showOnlyDupes = uroUtils.GetCBChecked('_cbURFilterDupes');

   let keywordPresent = uroUtils.GetElmValue('_textKeywordPresent');
   let keywordAbsent = uroUtils.GetElmValue('_textKeywordAbsent');
   let caseInsensitive = uroUtils.GetCBChecked('_cbCaseInsensitive');
   let thresholdMinAge = uroUtils.GetElmValue('_inputFilterMinDays');
   let thresholdMaxAge = uroUtils.GetElmValue('_inputFilterMaxDays');
   let thresholdMinComments = uroUtils.GetElmValue('_inputFilterMinComments');
   let thresholdMaxComments = uroUtils.GetElmValue('_inputFilterMaxComments');
   let thresholdMaxCommentAge = uroUtils.GetElmValue('_inputFilterCommentDays');
   let thresholdMinCommentAge = uroUtils.GetElmValue('_inputFilterCommentDays2');
   let ignoreOtherEditorComments = uroUtils.GetCBChecked('_cbIgnoreOtherEditorComments');
   let urcFilteringIsActive = false;
   let urcCB = document.getElementById('URCommentsFilterEnabled');
   if(urcCB !== null)
   {
      if(urcCB.checked)
      {
         urcFilteringIsActive = true;
      }
   }
   urcCB = document.getElementById('URCommentUROOnlyMyUR');
   if(urcCB !== null)
   {
      if(urcCB.checked)
      {
         urcFilteringIsActive = true;
      }
   }
   urcCB = document.getElementById('URCommentUROHideTagged');
   if(urcCB !== null)
   {
      if(urcCB.checked)
      {
         urcFilteringIsActive = true;
      }
   }

   filterInsideManagedAreas = filterInsideManagedAreas && (uroGetManagedAreas() !== 0);
   if(uroUtils.GetCBChecked('_cbURExcludeUserArea') == true)
   {
      uroIgnoreAreasUserID = W.loginManager.user.attributes.id;
   }

   for (let urobj in W.model.mapUpdateRequests.objects)
   {
      if(W.model.mapUpdateRequests.objects.hasOwnProperty(urobj))
      {
         let ureq = W.model.mapUpdateRequests.objects[urobj];
         let ureqID = ureq.attributes.id;

         let urStyle = 'visible';
         let inhibitFiltering = ((ureqID == uroSelectedURID) && (noFilterURInURL));

         let hasMyComments = false;
         let nComments = 0;
         let desc = ureq.attributes.description;
         let customType = uroGetCustomType(ureqID, uroLayers.ID.UR, desc);
         let ageLastComment = null;
         if(W.model.updateRequestSessions.objects[ureqID] != null)
         {
            nComments = W.model.updateRequestSessions.objects[ureqID].attributes.comments.length;
            if(nComments != 0)
            {
               ageLastComment = uroUtils.GetCommentAge(W.model.updateRequestSessions.objects[ureqID].attributes.comments[nComments-1]);
            }
            if((uFURs_masterEnable === false) && (nComments === 0))
            {
               // when master enable is turned off, we want to make sure that all URs, including ones that were previously hidden, are correctly
               // displayed in their native form - i.e. no comment count or custom conversation bubbles.  The easiest way to achieve this is to
               // force the AddCommentCounts code to test for the presence of these bubbles on each UR, which we do by setting a non-zero
               // comment count for each UR...  For URs which genuinely do have no comments we use -1 to indicate that we're not really setting
               // a comment count, but that we still need to do something that wouldn't be achieved by using 0.
               nComments = -1;
            }
         }

         // check UR against current session ignore list...
         if(uroIgnore.IsOnList(ureqID)) urStyle = 'hidden';

         if((uFURs_masterEnable === true) && (inhibitFiltering === false))
         {
            let wazeauto_ur = false;
            let ukroadworks_ur = false;
            let construction_ur = false;
            let closure_ur = false;
            let event_ur = false;
            let note_ur = false;
            let wslm_ur = false;
            let bog_ur = false;
            let difficult_ur = false;

            let filterByNotIncludedKeyword = false;
            let filterByIncludedKeyword = true;

            if(desc !== null) desc = desc.replace(/<\/?[^>]+(>|$)/g, "");
            else desc = '';

            if(customType === 0) ukroadworks_ur = true;
            else if(customType === 1) construction_ur = true;
            else if(customType === 2) closure_ur = true;
            else if(customType === 3) event_ur = true;
            else if(customType === 4) note_ur = true;
            else if(customType === 5) wslm_ur = true;
            else if(customType === 6) bog_ur = true;
            else if(customType === 7) difficult_ur = true;

            // check UR against editable area...

            if(filterOutsideEditableArea === true)
            {
               if(ureq.canEdit() === false) urStyle = 'hidden';
            }

            if(filterInsideManagedAreas === true)
            {
               if(uroCheckGeometryWithinManagedAreas(ureq.attributes.geoJSONGeometry.coordinates) === true) urStyle = 'hidden';
            }

            if(showOnlyDupes === true)
            {
               let isDupe = false;
               for(let i = 0; i < uroURDupes.length; ++i)
               {
                  if(uroURDupes[i][0] === ureqID)
                  {
                     isDupe = true;
                     break;
                  }
               }
               if(isDupe === false) urStyle = 'hidden';
            }

            // state-age filtering
            if(urStyle == 'visible')
            {
               // check against closed/not identified filtering if enabled...
               if(filterSolved === true)
               {
                  if(ureq.attributes.resolution === 0) urStyle = 'hidden';
               }
               if(filterUnidentified === true)
               {
                  if(ureq.attributes.resolution == 1) urStyle = 'hidden';
               }

               if((ureq.attributes.resolvedOn !== null) && (filterClosed === true))
               {
                  urStyle = 'hidden';
               }

               if((ureq.attributes.resolvedOn === null) && (filterOpen === true))
               {
                  urStyle = 'hidden';
               }

               if(urStyle == 'visible')
               {
                  // check UR against keyword filtering if enabled...
                  if(filterDescMustBePresent === true)
                  {
                     if(desc === '') urStyle = 'hidden';
                  }
                  if(filterDescMustBeAbsent === true)
                  {
                     if(desc !== '') urStyle = 'hidden';
                  }

                  if(filterKeywordMustBePresent === true)
                  {
                     let keywordIsPresentInDesc = uroUtils.KeywordPresent(desc,keywordPresent,caseInsensitive);
                     filterByIncludedKeyword = (filterByIncludedKeyword && (!keywordIsPresentInDesc));
                  }
                  if(filterKeywordMustBeAbsent === true)
                  {
                     let keywordIsAbsentInDesc = uroUtils.KeywordPresent(desc,keywordAbsent,caseInsensitive);
                     filterByNotIncludedKeyword = (filterByNotIncludedKeyword || keywordIsAbsentInDesc);
                  }
               }

               if(urStyle == 'visible')
               {
                  // do age-based filtering if enabled
                  if(filterMinURAge === true)
                  {
                     if(uroUtils.GetURAge(ureq,0,false) < thresholdMinAge) urStyle = 'hidden';
                  }
                  if(filterMaxURAge === true)
                  {
                     if(uroUtils.GetURAge(ureq,0,false) > thresholdMaxAge) urStyle = 'hidden';
                  }
               }

               if(urStyle == 'visible')
               {
                  if(resolverUser !== null)
                  {
                     if(ureq.attributes.resolvedBy != resolverUser) urStyle = 'hidden';
                  }
               }

               if(urStyle == 'visible')
               {
                  // do comments/following filtering
                  if(W.model.updateRequestSessions.objects[ureqID] != null)
                  {
                     nComments = W.model.updateRequestSessions.objects[ureqID].attributes.comments.length;
                     let commentDaysOld = -1;


                     if(filterMinComments === true)
                     {
                        if(nComments < thresholdMinComments) urStyle = 'hidden';
                     }
                     if(filterMaxComments === true)
                     {
                        if(nComments > thresholdMaxComments) urStyle = 'hidden';
                     }


                     if(nComments > 0)
                     {
                        let reporterIsLastCommenter = false;
                        if(W.model.updateRequestSessions.objects[ureqID].attributes.comments[nComments-1].userID == -1) reporterIsLastCommenter = true;

                        if(filterReporterLastCommenter === true)
                        {
                           if(reporterIsLastCommenter === true) urStyle = 'hidden';
                        }
                        else if(filterReporterNotLastCommenter === true)
                        {
                           if(reporterIsLastCommenter === false) urStyle = 'hidden';
                        }

                        hasMyComments = uroURHasMyComments(ureqID);
                        if(hasMyComments === false)
                        {
                           if(filterHideAnyComments === true) urStyle = 'hidden';
                           if(filterHideNotLastCommenter === true) urStyle = 'hidden';
                        }
                        else
                        {
                           if(filterHideMyComments === true) urStyle = 'hidden';

                           let userIsLastCommenter = false;
                           if(W.model.updateRequestSessions.objects[ureqID].attributes.comments[nComments-1].userID == uroUserID) userIsLastCommenter = true;

                           if(filterIfLastCommenter === true)
                           {
                              if(userIsLastCommenter === true) urStyle = 'hidden';
                           }
                           else if(filterIfNotLastCommenter === true)
                           {
                              if(userIsLastCommenter === false) urStyle = 'hidden';
                           }
                        }

                        let cidx;
                        if(ignoreOtherEditorComments === false)
                        {
                           commentDaysOld = ageLastComment;
                        }
                        else
                        {
                           for(cidx=0; cidx<nComments; cidx++)
                           {
                              let cObj = W.model.updateRequestSessions.objects[ureqID].attributes.comments[cidx];
                              if((cObj.userID == uroUserID) || (cObj.userID == -1))
                              {
                                 commentDaysOld = uroUtils.GetCommentAge(cObj);
                              }
                           }
                        }
                        if((filterCommentMinAge === true) && (commentDaysOld != -1))
                        {
                           if(thresholdMinCommentAge > commentDaysOld) urStyle = 'hidden';
                        }
                        if((filterCommentMaxAge === true) && (commentDaysOld != -1))
                        {
                           if(thresholdMaxCommentAge < commentDaysOld) urStyle = 'hidden';
                        }

                        if((commenterUser !== null) && (urStyle != 'hidden'))
                        {
                           urStyle = 'hidden';
                           for(cidx=0; cidx<nComments; cidx++)
                           {
                              if(W.model.updateRequestSessions.objects[ureqID].attributes.comments[cidx].userID == commenterUser)
                              {
                                 urStyle = 'visible';
                                 break;
                              }
                           }
                        }

                        let commentText = '';
                        for(cidx=0; cidx<nComments; cidx++)
                        {
                           commentText += W.model.updateRequestSessions.objects[ureqID].attributes.comments[cidx].text;
                        }

                        if(filterKeywordMustBePresent === true)
                        {
                           let keywordIsPresentInComments = uroUtils.KeywordPresent(commentText,keywordPresent,caseInsensitive);
                           filterByIncludedKeyword = (filterByIncludedKeyword && (!keywordIsPresentInComments));
                        }
                        if(filterKeywordMustBeAbsent === true)
                        {
                           let keywordIsAbsentInComments = uroUtils.KeywordPresent(commentText,keywordAbsent,caseInsensitive);
                           filterByNotIncludedKeyword = (filterByNotIncludedKeyword || keywordIsAbsentInComments);
                        }
                     }
                     else
                     {
                        if(filterUserID === true)
                        {
                           urStyle = 'hidden';
                        }
                     }

                     filterByNotIncludedKeyword = (filterByNotIncludedKeyword && filterKeywordMustBeAbsent);
                     filterByIncludedKeyword = (filterByIncludedKeyword && filterKeywordMustBePresent);
                     if(filterByNotIncludedKeyword || filterByIncludedKeyword)
                     {
                        urStyle = 'hidden';
                     }

                     if(W.model.updateRequestSessions.objects[ureqID].attributes.isFollowing === true)
                     {
                        if(filterMyFollowed === true) urStyle = 'hidden';
                     }
                     else
                     {
                        if(filterMyUnfollowed === true) urStyle = 'hidden';
                     }
                  }
               }

               if(invertURStateFilters === true)
               {
                 if(urStyle == 'hidden') urStyle = 'visible';
                 else urStyle = 'hidden';
               }
            }

            // type filtering
            if(urStyle == 'visible')
            {
               // Test for Waze automatic URs before any others - these always (?) get inserted as General Error URs,
               // so we can't filter them by type...
               if(desc.indexOf('Waze Automatic:') != -1)
               {
                  wazeauto_ur = true;
               }

               if(wazeauto_ur === true)
               {
                  if(filterWazeAuto === true) urStyle = 'hidden';
               }

               else if(ukroadworks_ur === true)
               {
                  if(filterRoadworks === true) urStyle = 'hidden';
               }
               else if(construction_ur === true)
               {
                  if(filterConstruction === true) urStyle = 'hidden';
               }
               else if(closure_ur === true)
               {
                  if(filterClosure === true) urStyle = 'hidden';
               }
               else if(event_ur === true)
               {
                  if(filterEvent === true) urStyle = 'hidden';
               }
               else if(note_ur === true)
               {
                  if(filterNote === true) urStyle = 'hidden';
               }
               else if(wslm_ur === true)
               {
                  if(filterWSLM === true) urStyle = 'hidden';
               }
               else if(bog_ur === true)
               {
                  if(filterBOG === true) urStyle = 'hidden';
               }
               else if(difficult_ur === true)
               {
                  if(filterDifficult === true) urStyle = 'hidden';
               }

               else if(ureq.attributes.type == 6)
               {
                  if(filterIncorrectTurn === true) urStyle = 'hidden';
               }
               else if(ureq.attributes.type == 7)
               {
                  if (filterIncorrectAddress === true) urStyle = 'hidden';
               }
               else if(ureq.attributes.type == 8)
               {
                  if(filterIncorrectRoute === true) urStyle = 'hidden';
               }
               else if(ureq.attributes.type == 9)
               {
                  if(filterMissingRoundabout === true) urStyle = 'hidden';
               }
               else if(ureq.attributes.type == 10)
               {
                  if(filterGeneralError === true) urStyle = 'hidden';
               }
               else if(ureq.attributes.type == 11)
               {
                  if(filterTurnNotAllowed === true) urStyle = 'hidden';
               }
               else if(ureq.attributes.type == 12)
               {
                  if(filterIncorrectJunction === true) urStyle = 'hidden';
               }
               else if(ureq.attributes.type == 13)
               {
                  if(filterMissingBridgeOverpass === true) urStyle = 'hidden';
               }
               else if(ureq.attributes.type == 14)
               {
                  if(filterWrongDrivingDirection === true) urStyle = 'hidden';
               }
               else if(ureq.attributes.type == 15)
               {
                  if(filterMissingExit === true) urStyle = 'hidden';
               }
               else if(ureq.attributes.type == 16)
               {
                  if(filterMissingRoad === true) urStyle = 'hidden';
               }
               else if(ureq.attributes.type == 18)
               {
                  if(filterMissingLandmark === true) urStyle = 'hidden';
               }
               else if(ureq.attributes.type == 19)
               {
                  if(filterBlockedRoad === true) urStyle = 'hidden';
               }
               else if(ureq.attributes.type == 23)
               {
                  if(filterNativeSpeedLimit === true) urStyle = 'hidden';
               }
               else if(filterUndefined === true) urStyle = 'hidden';

               if(invertURFilters === true)
               {
                 if(urStyle == 'hidden') urStyle = 'visible';
                 else urStyle = 'hidden';
               }
            }

            // stage-age filtering override for tagged URs
            if(noFilterTaggedURs === true)
            {
               if(ukroadworks_ur === true)
               {
                  if(filterRoadworks === false) urStyle = 'visible';
               }
               else if(construction_ur === true)
               {
                  if(filterConstruction === false) urStyle = 'visible';
               }
               else if(closure_ur === true)
               {
                  if(filterClosure === false) urStyle = 'visible';
               }
               else if(event_ur === true)
               {
                  if(filterEvent === false) urStyle = 'visible';
               }
               else if(note_ur === true)
               {
                  if(filterNote === false) urStyle = 'visible';
               }
               else if(wslm_ur === true)
               {
                  if(filterWSLM === false) urStyle = 'visible';
               }
            }
         }
         // only touch marker visibility if we've got active filter settings, or if URComments is not
         // doing any filtering of its own
         if((hasActiveURFilters === true) || (urcFilteringIsActive === false) || (uFURs_masterEnable === false))
         {
            let urMarker = uroGetMarker(uroLayers.ID.UR,urobj);
            if(urMarker !== null)
            {
               urMarker.style.visibility = urStyle;
            }
         }
         
         if(urStyle != 'hidden')
         {
            uroURExtras.AddToList(ureqID, customType, hasMyComments, nComments, ageLastComment);
         }
      }
   }
   uroURExtras.AddCommentCounts();
   uroDBG.PerfMon(pmFunction, pmTStart);
}
function uroGetProblemTypes()
{
   uroKnownProblemTypeIDs = [];
   uroKnownProblemTypeNames = [];
   let tProblemList = I18n.lookup("problems.types");
   for(let tObj in tProblemList)
   {
      if(tObj !== undefined)
      {
         uroKnownProblemTypeIDs.push(parseInt(tObj));
         uroKnownProblemTypeNames.push(tProblemList[tObj].title);
      }
   }
}
function uroFilterProblems()
{
   let pmTStart = performance.now();
   let pmFunction = "uroFilterProblems";

   if(uroFilterPreamble() === false) return;
   let selector;

   if((uroUtils.GetCBChecked('_cbMPNotClosedUserIDFilter') === false) && (uroUtils.GetCBChecked('_cbMPClosedUserIDFilter') === false))
   {
      selector = document.getElementById('_selectMPUserID');
      while(selector.options.length > 0)
      {
         selector.options.remove(0);
      }
   }

   let solverUser = null;
   if((uroUtils.GetCBChecked('_cbMPNotClosedUserIDFilter') === true) || (uroUtils.GetCBChecked('_cbMPClosedUserIDFilter') === true))
   {
      selector = document.getElementById('_selectMPUserID');
      if(selector.options.length === 0)
      {
         uroUpdateEditorList(W.model.mapProblems.objects, '_selectMPUserID', false, false, true, false);
      }
      if(selector.selectedOptions[0] != null)
      {
         solverUser = parseInt(selector.selectedOptions[0].value);
      }
   }

   let uFP_masterEnable = uroIsFilteringEnabled(false);
   let filter_OutsideEditableArea = uroUtils.GetCBChecked('_cbMPFilterOutsideArea');
   let filter_Solved = uroUtils.GetCBChecked('_cbMPFilterSolved');
   let filter_Unidentified = uroUtils.GetCBChecked('_cbMPFilterUnidentified');
   let filter_Closed = uroUtils.GetCBChecked('_cbMPFilterClosed');
   let filter_NotClosedUserID = uroUtils.GetCBChecked('_cbMPNotClosedUserIDFilter');
   let filter_ClosedUserID = uroUtils.GetCBChecked('_cbMPClosedUserIDFilter');
   let filter_Reopened = uroUtils.GetCBChecked('_cbMPFilterReopenedProblem');

   let filter_LowSeverity = uroUtils.GetCBChecked('_cbMPFilterLowSeverity');
   let filter_MediumSeverity = uroUtils.GetCBChecked('_cbMPFilterMediumSeverity');
   let filter_HighSeverity = uroUtils.GetCBChecked('_cbMPFilterHighSeverity');

   let filterTypes = [];
   let i;
   for(i=0; i<uroKnownProblemTypeIDs.length; i++)
   {
      if(uroUtils.GetCBChecked('_cbMPFilter_T'+uroKnownProblemTypeIDs[i])) filterTypes.push(uroKnownProblemTypeIDs[i]);
   }
   let filter_TypeUnknown = uroUtils.GetCBChecked('_cbMPFilterUnknownProblem');

   let filter_TaggedElgin = uroUtils.GetCBChecked('_cbFilterElgin');
   let filter_TaggedTrafficCast = uroUtils.GetCBChecked('_cbFilterTrafficCast');
   let filter_TaggedTrafficMaster = uroUtils.GetCBChecked('_cbFilterTrafficMaster');
   let filter_TaggedCaltrans = uroUtils.GetCBChecked('_cbFilterCaltrans');
   let filter_TaggedTFL = uroUtils.GetCBChecked('_cbFilterTFL');

   let filter_Invert = uroUtils.GetCBChecked('_cbInvertMPFilter');

   let filter_StartDateEnabled = uroUtils.GetCBChecked('_cbMPFilterStartDate');
   let filter_EndDateEnabled = uroUtils.GetCBChecked('_cbMPFilterEndDate');
   let filter_EndDatePassed = uroUtils.GetCBChecked('_cbMPFilterEndDatePassed');

   let tsD = uroUtils.GetElmValue('_inputMPFilterStartDay');
   let tsM = uroUtils.GetElmValue('_inputMPFilterStartMonth');
   let tsY = uroUtils.GetElmValue('_inputMPFilterStartYear');
   let startDate = uroUtils.GetTS(tsD, tsM, tsY, 0, 0);

   tsD = uroUtils.GetElmValue('_inputMPFilterEndDay');
   tsM = uroUtils.GetElmValue('_inputMPFilterEndMonth');
   tsY = uroUtils.GetElmValue('_inputMPFilterEndYear');
   let endDate = uroUtils.GetTS(tsD, tsM, tsY, 0, 0);
   
   let nowTime = (new Date()).getTime();

   for (let urobj in W.model.mapProblems.objects)
   {
      if(W.model.mapProblems.objects.hasOwnProperty(urobj))
      {
         let problem = W.model.mapProblems.objects[urobj];

         if(problem.attributes.origJSONGeo === undefined)
         {
            // Store a copy of the original marker position if we haven't already done so
            problem.attributes.origJSONGeo = uroUtils.CloneObject(problem.attributes.geoJSONGeometry);
         }
         else
         {
            // Restore the original position if we do have a copy of it, to undo any adjustments
            // that may have been made in the last filtering pass
            problem.attributes.geoJSONGeometry = uroUtils.CloneObject(problem.attributes.origJSONGeo);
         }


         let ureqID = problem.attributes.id;

         let problemStyle = 'visible';
         // check problem against current session ignore list...
         if(uroIgnore.IsOnList(ureqID)) problemStyle = 'hidden';

         if(uFP_masterEnable === true)
         {
            if(filter_OutsideEditableArea === true)
            {
               if(problem.canEdit() === false)
               {
                  problemStyle = 'hidden';
               }
            }

            if(filter_EndDatePassed == true)
            {
               if(problem.attributes.endTime > nowTime)
               {
                  problemStyle = 'hidden';
               }
            }
            if(filter_StartDateEnabled == true)
            {
               let tStart = new Date(problem.attributes.startTime);
               tStart.setHours(0);
               tStart.setMinutes(0);
               tStart.setSeconds(0);
               tStart = tStart.getTime();
               if(tStart != startDate)
               {
                  problemStyle = 'hidden';
               }
            }
            if(filter_EndDateEnabled == true)
            {
               let tEnd = new Date(problem.attributes.endTime);
               tEnd.setHours(0);
               tEnd.setMinutes(0);
               tEnd.setSeconds(0);
               tEnd = tEnd.getTime();
               if(tEnd != endDate)
               {
                  problemStyle = 'hidden';
               }
            }

            // check against closed/not identified filtering if enabled...
            let geoID = problem.getOLGeometry().id;
            if(geoID !== null)
            {
               if(document.getElementById(geoID) !== null)
               {
                  let problem_marker_img = document.getElementById(geoID).href.baseVal;
                  if(filter_Solved === true)
                  {
                     if(problem_marker_img.indexOf('_solved') != -1) problemStyle = 'hidden';
                  }
                  if(filter_Unidentified === true)
                  {
                     if(problem_marker_img.indexOf('_rejected') != -1) problemStyle = 'hidden';
                  }
               }
            }

            if(filter_Closed === true)
            {
               if(problem.attributes.open === false)
               {
                  problemStyle = 'hidden';
               }
            }

            if(problemStyle == 'visible')
            {
               if(solverUser !== null)
               {
                  if((filter_NotClosedUserID === true) && (problem.attributes.resolvedBy == solverUser)) problemStyle = 'hidden';
                  if((filter_ClosedUserID === true) && (problem.attributes.resolvedBy != solverUser)) problemStyle = 'hidden';
               }
            }

            if(problemStyle == 'visible')
            {
               let problemType = problem.attributes.subType;
               let desc = '';
               if(problem.attributes.description != null)
               {
                  desc = problem.attributes.description;
               }      
               let customType = uroGetCustomType(ureqID, uroLayers.ID.MP, desc);

               if(customType === 100)
               {
                  if(filter_TaggedElgin === true) problemStyle = 'hidden';
               }
               else if(customType === 101)
               {
                  if(filter_TaggedTrafficCast === true) problemStyle = 'hidden';
               }
               else if(customType === 102)
               {
                  if(filter_TaggedTrafficMaster === true) problemStyle = 'hidden';
               }
               else if(customType === 103)
               {
                  if(filter_TaggedCaltrans === true) problemStyle = 'hidden';
               }
               else if(customType === 104)
               {
                  if(filter_TaggedTFL === true) problemStyle = 'hidden';
               }
               else if(uroKnownProblemTypeIDs.indexOf(problemType) !== -1)
               {
                  if(filterTypes.indexOf(problemType) !== -1)
                  {
                     problemStyle = 'hidden';
                  }
               }
               else if(filter_TypeUnknown === true) problemStyle = 'hidden';

               if(filter_Reopened === true)
               {
                  if((problem.attributes.open === true) && (problem.attributes.resolvedOn !== null))
                  {
                     problemStyle = 'hidden';
                  }
               }


               if(filter_Invert === true)
               {
                  if(problemStyle == 'hidden') problemStyle = 'visible';
                  else problemStyle = 'hidden';
               }


               if(problem.attributes.weight <= 3)
               {
                  if(filter_LowSeverity === true) problemStyle = 'hidden';
               }
               else if(problem.attributes.weight <= 7)
               {
                  if(filter_MediumSeverity === true) problemStyle = 'hidden';
               }
               else if(filter_HighSeverity === true) problemStyle = 'hidden';
            }
         }

         let marker = uroGetMarker(uroLayers.ID.MP, urobj);
         if(marker !== null)
         {
            marker.style.visibility = problemStyle;
            if(problemStyle === 'hidden')
            {
               // To ensure WME displays the details for the topmost marker that's
               // left visible in a stack, alter the coords for any hidden markers
               // to place them in a location that isn't likely to be clicked on...
               problem.attributes.geoJSONGeometry.coordinates[0] = 0;
               problem.attributes.geoJSONGeometry.coordinates[1] = 90;
            }   
         }
      }
   }

   uroDBG.PerfMon(pmFunction, pmTStart);
}
function uroFilterPreamble()
{
   let mapviewport = document.getElementsByClassName("olMapViewport")[0];
   if(mapviewport === null)
   {
      if(uroNullMapViewport === false)
      {
         uroDBG.AddLog('caught null mapviewport');
         uroNullMapViewport = true;
      }
      return false;
   }

   let uiElms = uroTabs.CtrlTabs[uroTabs.IDS.MISC][uroTabs.FIELDS.TABBODY];
   if(uiElms == null)
   {
      uroDBG.AddLog('caught missing UI');
      return false;
   }
   if(uiElms.innerHTML.length === 0)
   {
      uroDBG.AddLog('caught empty UI');
      return false;
   }

   if(uroSettingsApplied === false)
   {
      return false;
   }

   uroNullMapViewport = false;

   return true;
}
function uroFilterItems_MasterEnableClick()
{
   if(uroUtils.GetCBChecked('_cbMasterEnable') === false)
   {
      uroPopup.Hide();
   }
   uroFilterItems();
}
function uroFilterItems()
{
   uroFilterProblems();
   uroFilterPlaces();
   uroFilterCameras();
   uroFilterURs();
   uroFilterRTCs();
   uroFilterRAs();
   uroFilterMapComments();
}
function uroFilterItemsOnMove()
{
   W.map.events.unregister('mousemove',null,uroFilterItemsOnMove);
   uroFilterItems();
}
function uroDeleteObject()
{
   uroDBG.AddLog('delete camera ID '+uroShownFID);
   if(W.model.cameras.objects[uroShownFID] === null)
   {
      uroDBG.AddLog('camera object not found...');
      return false;
   }
   uroOWL.RemoveCamFromWatchList();
   let actionObj = require('Waze/Action/DeleteObject');
   let deleteAction = new actionObj(W.model.cameras.objects[uroShownFID], null);
   W.model.actionManager.add(deleteAction);
   uroPopup.MouseOut();
   uroPopup.Hide();
   return false;
}
function uroCheckCommentsForTag(idSrc)
{
   let ursObj = W.model.updateRequestSessions.objects[idSrc];
   if(typeof(ursObj) == 'undefined') return -1;
   if(ursObj.attributes.comments.length === 0) return -1;

   for(let idx=ursObj.attributes.comments.length-1; idx>=0; idx--)
   {
      for(let tag=0; tag<uroCustomURTags.length; tag++)
      {
         let keyword = uroCustomURTags[tag];
         if(ursObj.attributes.comments[idx].text.indexOf(keyword) != -1)
         {
            return tag;
         }
      }
   }
   return -1;
}
function uroGetCustomType(idSrc, markerType, desc)
{
   let provider = '';
   if(desc === null) desc = '';
   if(markerType == uroLayers.ID.UR)
   {
      let ureq = W.model.mapUpdateRequests.objects[idSrc];
      // early test for native speed limit URs
      if(ureq.attributes.type == 23) return 98;
   }
   else if(markerType == uroLayers.ID.MP)
   {
      let mp = W.model.mapProblems.objects[idSrc];
      if(mp.attributes.provider != null)
      {
         provider = mp.attributes.provider;
      }
   }

   if(desc !== '')
   {
      if((markerType == uroLayers.ID.UR) || (markerType == 'mc'))
      {
         for(let tag=0; tag<uroCustomURTags.length; tag++)
         {
            let keyword = uroCustomURTags[tag];
            if(desc.indexOf(keyword) != -1)
            {
               return tag;
            }
         }
      }

      if(markerType == uroLayers.ID.MP)
      {
         if(desc.indexOf('[Elgin]') != -1) return 100;
         if(desc.indexOf('[ELGIN]') != -1) return 100;
         if(desc.indexOf('[elginroadworks]') != -1) return 100;
         if(desc.indexOf('[one.network]') != -1) return 100;
         if(desc.indexOf('[TrafficCast]') != -1) return 101;
         if(desc.indexOf('[TM]') != -1) return 102;
         if(desc.indexOf('[Caltrans]') != -1) return 103;
         if(desc.indexOf('[TfL Open Data]') != -1) return 104;
         if(provider.indexOf('London TFL Closures') != -1) return 104;
      }
   }

   if(markerType == uroLayers.ID.UR)
   {
      return uroCheckCommentsForTag(idSrc);
   }

   return -1;
}
function uroGetRestrictionLanes(disposition)
{
   let retval = '';
   if(disposition == 1) retval += 'All lanes';
   else if(disposition == 2) retval += 'Left lane';
   else if(disposition == 3) retval += 'Middle lane';
   else if(disposition == 4) retval += 'Right lane';
   else retval += ' - ';
   return retval;
}
function uroGetRestrictionLaneType(laneType)
{
   let retval = '';
   if(laneType === null) retval += ' - ';
   else
   {
      if(laneType == 1) retval += 'HOV';
      else if(laneType == 2) retval += 'HOT';
      else if(laneType == 3) retval += 'Express';
      else if(laneType == 4) retval += 'Bus lane';
      else if(laneType == 5) retval += 'Fast lane';
      else retval += ' - ';
   }
   return retval;
}
let uroVehicleTypes =
[
   [1280, 'fa-car'],
   [1024, 'fa-motorcycle'],
   [272,  'fa-taxi'],
   [1808, 'fa-bolt']
];
function uroGetRestrictionVehicleTypes(restObj, allowInit, profileKey)
{
   let i;
   let j;
   let k;
   let tVT;
   let retval = [];
   for(i = 0; i < uroVehicleTypes.length; ++i)
   {
      retval.push(allowInit);
   }
   let tRest = restObj._driveProfiles.get(profileKey);
   if(tRest !== undefined)
   {
      for(i = 0; i < tRest._driveProfiles.length; ++i)
      {
         tVT = tRest._driveProfiles[i].getVehicleTypes();
         {
            if(tVT.length > 0)
            {
               for(j = 0; j < tVT.length; ++j)
               {
                  for(k = 0; k < uroVehicleTypes.length; ++k)
                  {
                     if(tVT[j] == uroVehicleTypes[k][0])
                     {
                        retval[k] = !allowInit;
                     }
                  }
               }
            }
         }
      }
   }
   return retval;
}
function uroFormatRestriction(restObj)
{
   let retval = '';

   if(restObj._defaultType == "DIFFICULT")
   {
      retval = '<tr><td colspan=13>Difficult Turn';
   }
   else
   {
      let roDays = null;
      let roFromDate = null;
      let roToDate = null;
      let roFromTime = null;
      let roToTime = null;
      let roRepeats = false;
      let roAllDay = false;
      if(restObj._days !== undefined)
      {
         roDays = restObj._days;
         roFromDate = restObj._fromDate;
         roToDate = restObj._toDate;
         roFromTime = restObj._fromTime;
         roToTime = restObj._toTime;
      }
      else if(restObj._timeFrames.length > 0)
      {
         if(restObj._timeFrames[0]._weekdays !== undefined)
         {
            roDays = restObj._timeFrames[0]._weekdays;
            roFromDate = restObj._timeFrames[0]._startDate;
            roToDate = restObj._timeFrames[0]._endDate;
            roFromTime = restObj._timeFrames[0]._fromTime;
            roToTime = restObj._timeFrames[0]._toTime;
            roRepeats = restObj._timeFrames[0]._repeatYearly;
         }
      }

      if((roFromTime === null) && (roToTime === null))
      {
         roFromTime = "0:00";
         roToTime = "23:59";
         roAllDay = true;
      }

      let hasExpired = false;
      let isFuture = false;
      let tNow = Date.now();
      let tFrom = null;
      let tTo = null;
      
      if(roFromDate !== null)
      {
         tFrom = new Date(roFromDate + " " + roFromTime);
         isFuture = (tFrom.getTime() > tNow);
      }
      if(roToDate !== null)
      {
         tTo = new Date(roToDate + " " + roToTime);
         hasExpired = (tTo.getTime() < tNow);
      }
      if((hasExpired === true) && (roRepeats === true))
      {
         while(tTo.getTime() < tNow)
         {
            tFrom.setFullYear(tFrom.getFullYear() + 1);
            tTo.setFullYear(tTo.getFullYear() + 1);
         }
         isFuture = (tFrom.getTime() > tNow);
         hasExpired = false;
      }


      if(isFuture === true)
      {
         retval = '<tr bgcolor="#8080FF">';
      }
      else if(hasExpired === true)
      {
         retval = '<tr bgcolor="#FFFFC0">';
      }
      else
      {
         retval = '<tr>';
      }

      if(roDays === null)
      {
         roDays = 127;
      }

      retval += '<td style="text-align:center;">';
      if((roDays & 1) == 1) retval += 'M';
      else retval += '-';
      retval += '</td><td style="text-align:center;">';
      if((roDays & 2) == 2) retval += 'Tu';
      else retval += '-';
      retval += '</td><td style="text-align:center;">';
      if((roDays & 4) == 4) retval += 'W';
      else retval += '-';
      retval += '</td><td style="text-align:center;">';
      if((roDays & 8) == 8) retval += 'Th';
      else retval += '-';
      retval += '</td><td style="text-align:center;">';
      if((roDays & 16) == 16) retval += 'F';
      else retval += '-';
      retval += '</td><td style="text-align:center;">';
      if((roDays & 32) == 32) retval += 'Sa';
      else retval += '-';
      retval += '</td><td style="text-align:center;">';
      if((roDays & 64) == 64) retval += 'Su';
      else retval += '-';

      retval += '</td><td nowrap style="text-align:center;">';

      if(roFromDate === null) retval += 'All dates';
      else retval += tFrom.toISOString().slice(0,10) + ' to ' + tTo.toISOString().slice(0,10);
      if(roRepeats === true)
      {
         retval += '&nbsp;<i class="fa fa-repeat"> </i>';
      }

      retval += '</td><td nowrap style="text-align:center;">';

      if((restObj._allDay === true) || (roAllDay === true)) retval += 'All day';
      else retval += roFromTime + ' to ' + roToTime;

      retval += '</td><td nowrap style="text-align:center;">';

      retval += uroGetRestrictionLanes(restObj._disposition);

      retval += '</td><td nowrap style="text-align:center;">';

      retval += uroGetRestrictionLaneType(restObj._laneType);

      retval += '</td><td nowrap style="text-align:center;">';

      // for brevity, the popup only displays the allowed/prohibited restriction for the driveable vehicle types in the app...
      let typesAllowed = [];
      if((restObj._defaultType == "BLOCKED") || (restObj._defaultType == "TOLL"))
      {
         if(restObj._defaultType == "TOLL")
         {
            retval += I18n.lookup('restrictions.editing.segment.toll_road');
         }
         typesAllowed = uroGetRestrictionVehicleTypes(restObj, false, "FREE");
      }
      else
      {
         typesAllowed = uroGetRestrictionVehicleTypes(restObj, true, "BLOCKED");
      }

      let i;
      for(i = 0; i < uroVehicleTypes.length; ++i)
      {
         if(typesAllowed[i] === true)
         {
            retval += '<i class="fa '+uroVehicleTypes[i][1]+'" style="color:#000000;"> </i>&nbsp;';
         }
         else
         {
            retval += '<i class="fa '+uroVehicleTypes[i][1]+'" style="color:#d0d0d0;"> </i>&nbsp;';
         }
      }

      retval += '</td><td>';
      retval += uroUtils.Clickify(restObj._description, '');
   }

   retval += '</td></tr>';

   return retval;
}
function uroOpenURDialog(urID)
{
   let t = {showNext: false, nextButtonString: I18n.lookup('problems.panel.done')};
   let urObj = W.model.mapUpdateRequests.objects[urID];
   W.reqres.request("problems:browse", _.extend(t, {problem: urObj}));
}
function uroRecentreSessionOnUR()
{
   //uroGetMarker(uroLayers.ID.UR, uroShownFID).element.click();
   uroOpenURDialog(uroShownFID);
   W.map.moveTo(uroGetMarker(uroLayers.ID.UR, uroShownFID).lonlat, 17);
   uroPopup.Hide();
   return false;
}
function uroRecentreSessionOnMP()
{
   uroGetMarker(uroLayers.ID.MP, uroShownFID).element.click();
   W.map.moveTo(uroGetMarker(uroLayers.ID.MP, uroShownFID).lonlat, 17);
   uroPopup.Hide();
   return false;
}
function uroRecentreSessionOnPUR()
{
   uroGetMarker(uroLayers.ID.PUR, uroShownFID).element.click();
   W.map.moveTo(uroGetMarker(uroLayers.ID.PUR, uroShownFID).lonlat, 17);
   uroPopup.Hide();
   return false;
}
function uroRecentreSessionOnPPUR()
{
   uroGetMarker(uroLayers.ID.PPUR, uroShownFID).element.click();
   W.map.moveTo(uroGetMarker(uroLayers.ID.PPUR, uroShownFID).lonlat, 17);
   uroPopup.Hide();
   return false;
}
function uroRecentreSessionOnVenueNavPoint()
{
   W.map.moveTo(uroGetVenueNavPoint(uroShownFID), 17);
   uroPopup.Hide();
   return false;
}
function uroStackListObj(fid,x,y)
{
   this.fid = fid;
   this.x = uroUtils.TypeCast(x);
   this.y = uroUtils.TypeCast(y);
}
function uroRestackMarkers()
{
   if(uroStackList.length === 0) return;

   if(uroLayers.layers[uroStackType].mf !== null)
   {
      uroDBG.AddLog('restacking markers...');
      // strip off the .realX/realY attributes from any UR object we've previously added it to, to allow
      // the native recentering to work again...
      let idList = uroGetMarkerIDs(uroStackType);
      for(let marker of idList)
      {
         let testMarkerAttributes = uroGetAttributes(uroStackType, marker);
         if(testMarkerAttributes.geometry.realX != null)
         {
            testMarkerAttributes.geometry.x = testMarkerAttributes.geometry.realX;
            testMarkerAttributes.geometry.y = testMarkerAttributes.geometry.realY;
            delete(testMarkerAttributes.geometry.realX);
            delete(testMarkerAttributes.geometry.realY);
         }
      }
      // now restack any markers that were repositioned...
      for(let idx=0; idx<uroStackList.length; idx++)
      {
         let orig_x = uroStackList[idx].x + 'px';
         let orig_y = uroStackList[idx].y + 'px';
         let fid = uroStackList[idx].fid;

         if(uroGetMarker(uroStackType, fid) != null)
         {
            uroGetMarker(uroStackType, fid).element.style.left = orig_x;
            uroGetMarker(uroStackType, fid).element.style.top = orig_y;
         }
      }
      uroStackList = [];
      uroUnstackedMasterID = null;
      uroStackType = null;
      uroDBG.AddLog('...stacked!');
   }
}
function uroIsIDAlreadyUnstacked(idSrc)
{
   if(uroStackList.length === 0) return false;
   for(let idx=0; idx<uroStackList.length; idx++)
   {
      if(uroStackList[idx].fid == idSrc) return true;
   }
   return false;
}
function uroCheckStacking(stackType, masterID, unstackedX, unstackedY)
{
   //// WIP
   return;
   
   if(typeof(masterID) === 'number')
   {
      masterID = masterID.toString();
   }

   if(uroIsIDAlreadyUnstacked(masterID) === true) return;
   if(uroStackType !== null) return;

   uroDBG.AddLog('checking for marker stack, masterID: '+masterID+', stackType: '+stackType);
   let stackList = [];
   stackList.push(masterID);
   let threshSquared = uroUtils.GetElmValue('_inputUnstackSensitivity');
   threshSquared *= threshSquared;

   let marker;

   let offset = 0.000000001;
   if(uroLayers.layers[stackType].mf !== null)
   {
      let idList = uroGetMarkerIDs(stackType);
      let showOpen = true;
      let showClosed = false;
      let showTypes = null;
      if(stackType === uroLayers.ID.UR)
      {
         showTypes = W?.issueTrackerController?.app?.attributes?.issueTrackerFilter?.attributes?.mapUpdateRequestsFilter?.attributes?.status;
      }
      else if(stackType === uroLayers.ID.MP)
      {
         showTypes = W?.issueTrackerController?.app?.attributes?.issueTrackerFilter?.attributes?.mapProblemsFilter?.attributes?.status;
      }
      if(showTypes !== null)
      {
         showOpen = ((showTypes == 'OPEN') || (showTypes == 'BOTH'));
         showClosed = ((showTypes == 'CLOSED') || (showTypes == 'BOTH'));
      }

      for(marker of idList)
      {
         let testMarkerObj = uroGetMarker(stackType, marker);
         let testMarkerAttributes = uroGetAttributes(stackType, marker);
         if((testMarkerAttributes !== null) && (testMarkerObj !== null))
         {
            // if multiple markers are stacked exactly on top of one another, WME will always open up the one which it would have rendered on the
            // top of the stack in the absence of any URO+ filtering, regardless of which UR pin actually receives the click event.  To prevent
            // this, we give each pin in the stack a unique set of false coordinates, storing the original coordinates in newly created
            // properties so they can be restored later on.
            //
            // As of 3.169, the offset added to create the false coordinates has now been changed to a fractional value as opposed to the large integer
            // it previously was, as the latter method has now been seen to cause problems with displaying the "A" marker when a road closure request MP
            // is being viewed.  By using a small fractional offset added to each stacked marker, the risk of accidentally setting the offset coords to
            // those of another marker is low
            if(testMarkerAttributes.geometry.realX === undefined)
            {
               testMarkerAttributes.geometry.realX = testMarkerAttributes.geometry.x;
               testMarkerAttributes.geometry.x += offset;
               testMarkerAttributes.geometry.realY = testMarkerAttributes.geometry.y;
               testMarkerAttributes.geometry.y += offset;
               offset += 0.000000001;
            }

            let includeInStack = (testMarkerObj.element.style.visibility != 'hidden');
            let isClosed = testMarkerObj.element.classList.contains("recently-closed");
            includeInStack = includeInStack && ((isClosed && showClosed) || (!isClosed && showOpen));
            if(includeInStack)
            {
               if(testMarkerAttributes.id != masterID)
               {
                  let bcr = testMarkerObj.element.getBoundingClientRect();
                  let xdiff = unstackedX - bcr.x;
                  let ydiff = unstackedY - bcr.y;
                  let distSquared = ((xdiff * xdiff) + (ydiff * ydiff));
                  if(distSquared < threshSquared)
                  {
                     stackList.push(testMarkerAttributes.id);
                  }
               }
            }
         }
      }
   }

   let inhibitUnstacking = (W.map.getZoom() < uroUtils.GetElmValue('_inputUnstackZoomLevel'));
   inhibitUnstacking = inhibitUnstacking || (stackList.length < 2);

   if(inhibitUnstacking == false)
   {
      uroStackType = stackType;
      if(uroUnstackedMasterID != masterID)
      {
         uroDBG.AddLog('unstacked ID mismatch, relocating markers...');
         uroRestackMarkers();
         uroUnstackedMasterID = masterID;
         uroStackList = [];

         // push the highlighted marker onto the stacklist so uroIsIDAlreadyUnstacked() will return true
         uroStackList.push(new uroStackListObj(masterID,unstackedX,unstackedY));

         for(let shoveIdx=0; shoveIdx < stackList.length; shoveIdx++)
         {
            let fid = stackList[shoveIdx];
            let stackMarker = uroGetMarker(stackType, fid);
            if(stackMarker !== null)
            {
               let x = uroUtils.ParsePxString(stackMarker.element.style.left);
               let y = uroUtils.ParsePxString(stackMarker.element.style.top);
               // store the unstacked marker positions so they can be reinstated later
               uroStackList.push(new uroStackListObj(fid,x,y));
               stackMarker.element.style.left = unstackedX + 'px';
               stackMarker.element.style.top = unstackedY + 'px';
               unstackedX += 5;
               unstackedY -= 20;
            }
         }


         // hide other markers to prevent confusion with the unstacked markers
         let listIDs = uroGetMarkerIDs(stackType);
         for(marker in listIDs)
         {
            if(listIDs.hasOwnProperty(marker))
            {
               let toHideMarker = uroGetMarker(stackType, marker);
               if(toHideMarker !== null)
               {
                  let toHideID = toHideMarker.id;
                  if(uroIsIDAlreadyUnstacked(toHideID) === false)
                  {
                     toHideMarker = uroGetMarker(stackType, toHideID);
                     if(toHideMarker !== null)
                     {
                        toHideMarker.element.style.visibility = 'hidden';
                     }
                  }
               }
            }
         }
      }
   }
   else
   {
      uroRestackMarkers();
   }
}
function uroGetVenueNavPoint(uroFID)
{
   let retval = W.map.getUnprojectedCenter();   // allow the function to return a safe value in case we can't find the requested venue object...

   let vObj = W.model.venues.objects[uroFID];
   if(vObj !== undefined)
   {
      if(vObj.attributes.entryExitPoints.length > 0)
      {
         // if the venue has any navpoints defined, use the position of the first one
         let tPoint = vObj.attributes.entryExitPoints[0].getPoint();
         retval.lon = tPoint.coordinates[0];
         retval.lat = tPoint.coordinates[1];
      }
      else
      {
         // otherwise use the centrepoint of the venue point or polygon
         let tPoint = vObj.attributes.geometry.getCentroid();
         let tLL = new OpenLayers.LonLat();
         tLL.lon = tPoint.x;
         tLL.lat = tPoint.y;
         tLL = uroUtils.ConvertMercatorToWGS84(tLL);
         retval.lon = tLL.lon;
         retval.lat = tLL.lat;
      }
   }
   return retval;
}
function uroOpenNewTab()
{
   // flush the current settings into localStorage before the new tab opens, so that when its instance of
   // URO+ fires up it'll have the same settings as this one
   uroConfig.SaveSettings();
   return true;
}
function uroGetRTCDuration(rcObj)
{
   let duration = new Date(rcObj.attributes.endDate) - new Date(rcObj.attributes.startDate);
   return Math.floor(duration / 86400000);
}
function uroGetRTCOffset(rcDate)
{
   let dateObj = new Date(rcDate);
   return (0 - uroUtils.DateToDays(dateObj));
}
function uroGetRTCOrigin(rcObj)
{
   let retval = uroEnums.TRTC.UNKNOWN;

   if(rcObj !== undefined)
   {
      if(rcObj.attributes.createdBy == -5)
      {
         retval = uroEnums.TRTC.WAZEFEED;
      }
      else if((W.model.users.objects[rcObj.attributes.createdBy] !== undefined) && (W.model.users.objects[rcObj.attributes.createdBy].attributes.rank == 6))
      {
         retval = uroEnums.TRTC.WAZEOTHER;
      }
      else
      {
         retval = uroEnums.TRTC.WME;
      }
   }

   return retval;
}
function uroGetRTCState(rcObj)
{
   let retval = uroEnums.SRTC.UNKNOWN;
   let rcStatus = rcObj.attributes.closureStatus;

   if(rcStatus === "ACTIVE")
   {
      retval = uroEnums.SRTC.ACTIVE;
   }
   else if(rcStatus === "NOT_STARTED")
   {
      retval = uroEnums.SRTC.FUTURE;
   }
   else if(rcStatus.indexOf("FINISHED") != -1)
   {
      retval = uroEnums.SRTC.EXPIRED;
   }
   // Haven't seen one of these yet, so assuming it should be treated
   // the same as an expired closure...
   else if(rcStatus === "SUSPENDED")
   {
      retval = uroEnums.SRTC.EXPIRED;
   }

   return retval;
}
function uroGetRTCStateText(rcObj)
{
   let retval = "---";
   let i18 = I18n.lookup("closures.statuses")[rcObj.attributes.closureStatus];
   if(i18 !== undefined)
   {
      retval = i18;
   }
   return retval;
}
function uroGetAddress(streetID, houseNumber, formatForSegmentPopup, formatForNodePopup, showAsToll)
{
   let result = '';
   if((houseNumber !== undefined) && (houseNumber !== null))
   {
      result += houseNumber + ' ';
   }

   if(streetID != null)
   {
      let streetName = I18n.lookup('edit.address.no_street');
      let doesStreetIDExist = true;
      if(W.model.streets.objects[streetID] === undefined)
      {
         streetName = 'non-existent streetID';
         doesStreetIDExist = false;
      }
      else
      {
         if((streetName !== null) && (W.model.streets.objects[streetID].attributes.isEmpty === false))
         {
            streetName = W.model.streets.objects[streetID].attributes.name;
         }
      }
      if(formatForSegmentPopup === true)
      {
         if(showAsToll == true)
         {
            result += '<i class="fa fa-credit-card"></i> ';
         }
         result += '<b>'+streetName+'</b><br>';
      }
      else
      {
         result += streetName + ', ';
      }

      if(doesStreetIDExist === true)
      {
         let cityName = I18n.lookup('edit.address.no_city');
         let doesCityIDExist = true;
         let cityID = W.model.streets.objects[streetID].attributes.cityID;
         if(W.model.cities.objects[cityID] === undefined)
         {
            cityName = 'non-existent cityID';
            doesCityIDExist = false;
         }
         else
         {
            if(W.model.cities.objects[cityID].attributes.name !== "")
            {
               cityName = W.model.cities.objects[cityID].attributes.name;
            }
         }
         result += cityName + ', ';

         if(doesCityIDExist === true)
         {
            let stateID = W.model.cities.objects[cityID].attributes.stateID;
            if(W.model.states.objects[stateID] === undefined)
            {
               result += 'non-existent stateID';
            }
            else
            {
               result += W.model.states.objects[stateID].attributes.name;
            }
         }
      }
   }
   result += '<br>';

   return result;
}
function uroGetSelectedSegmentRTCs(segID)
{
   let closureTypes = uroEnums.DRTC.NONE;
   let RTCObjs = [];
   let selectedSegs = [];

   if(segID === null)
   {
      // segID should always be set to a valid segment ID if we're being called from the segment mouseover
      // handler, so if it's null it implies we've instead been called from the closure panel handler where
      // we might therefore be dealing with a multi-segment selection...
      selectedSegs = W.selectionManager.getSegmentSelection().segments;
   }

   if((selectedSegs.length > 0) || (segID !== null))
   {
      for(let roadClosure in W.model.roadClosures.objects)
      {
         if(W.model.roadClosures.objects.hasOwnProperty(roadClosure))
         {
            let rcObj = W.model.roadClosures.objects[roadClosure];
            rcObj.segIDs = [rcObj.attributes.segID]; // copy the segID property into an array so we can push extra segIDs into it later...

            // set a direction value corresponding to the A-B or B-A setting - if we later end up combining an A-B and B-A closure
            // into a two-way closure, we can then change the direction value to indicate this as well
            if(rcObj.attributes.forward === true)
            {
               rcObj.direction = uroEnums.DRTC.SEG_AB;
            }
            else
            {
               rcObj.direction = uroEnums.DRTC.SEG_BA;
            }

            // for each of the selected or moused-over segments, find all the closures which have matching segIDs
            if(segID !== null)
            {
               if(rcObj.attributes.segID == segID)
               {
                  RTCObjs.push(rcObj);
               }
            }
            else
            {
               for(let i = 0; i < selectedSegs.length; ++i)
               {
                  if(rcObj.attributes.segID == selectedSegs[i].attributes.id)
                  {
                     RTCObjs.push(rcObj);
                     break;
                  }
               }
            }
         }
      }

      // RTCObjs now contains all of the segment closures relating to all of the segments of interest, so
      // we can begin to organise them such that by the time we exit this function, the array will then contain
      // an optimised list of closures that matches up to the list shown in the closure sidepanel, taking into
      // account closures applying to all segments vs some, closures that can be merged into two-ways etc.

      // first sort the closure by their start date, with a secondary sort by direction for those closures
      // that have the same start date
      RTCObjs = RTCObjs.sort(function(a,b)
      {
         if(a.attributes.startDate === b.attributes.startDate)
         {
            if(a.direction == uroEnums.DRTC.SEG_AB) return -1;
            return 1;
         }
         if(a.attributes.startDate > b.attributes.startDate) return 1;
         return -1;
      });

      // if we've got at least two closures in the sorted list, we then test adjacent list entries
      // to see if they contain closure details which are identical except for their segment IDs, and
      // combine them if so
      if(RTCObjs.length > 1)
      {
         let i = 0;
         while(i < (RTCObjs.length - 1))
         {
            if(
               (RTCObjs[i].attributes.createdBy == RTCObjs[i+1].attributes.createdBy) &&
               (RTCObjs[i].attributes.endDate == RTCObjs[i+1].attributes.endDate) &&
               (RTCObjs[i].attributes.eventId == RTCObjs[i+1].attributes.eventId) &&
               (RTCObjs[i].attributes.location == RTCObjs[i+1].attributes.location) &&
               (RTCObjs[i].attributes.reason == RTCObjs[i+1].attributes.reason) &&
               (RTCObjs[i].attributes.startDate == RTCObjs[i+1].attributes.startDate) &&
               (RTCObjs[i].direction == RTCObjs[i+1].direction)
            )
            {
               RTCObjs[i].segIDs.push(RTCObjs[i+1].attributes.segID);
               RTCObjs.splice(i+1, 1);
            }
            else
            {
               ++i;
            }
         }
      }

      // after that first trimming of the list, if there are still two or more entries then
      // we perform a second pass, this time merging any adjacent entries which have the same
      // segment IDs in their segIDs arrays - these are two-way closures applying to all those
      // segments, and so we also change the direction value to indicate two-way vs A-B or B-A
      if(RTCObjs.length > 1)
      {
         let i = 0;
         while(i < (RTCObjs.length - 1))
         {
            if
            (
               (RTCObjs[i].segIDs.sort().join(',') == RTCObjs[i+1].segIDs.sort().join(',')) &&
               (RTCObjs[i].attributes.createdBy == RTCObjs[i+1].attributes.createdBy) &&
               (RTCObjs[i].attributes.endDate == RTCObjs[i+1].attributes.endDate) &&
               (RTCObjs[i].attributes.eventId == RTCObjs[i+1].attributes.eventId) &&
               (RTCObjs[i].attributes.location == RTCObjs[i+1].attributes.location) &&
               (RTCObjs[i].attributes.reason == RTCObjs[i+1].attributes.reason) &&
               (RTCObjs[i].attributes.startDate == RTCObjs[i+1].attributes.startDate)
            )
            {
               RTCObjs[i].direction = uroEnums.DRTC.SEG_BI;
               RTCObjs.splice(i+1, 1);
            }
            ++i;
         }
      }
   }

   let RTTCObjs = [];
   if(segID !== null)
   {
      // If we've been called from the segment popup handler, we now also check for any turn closures
      // associated with this segment, so that their details can also be shown in the popup...
      for(let turnClosure in W.model.turnClosures.objects)
      {
         if(W.model.turnClosures.objects.hasOwnProperty(turnClosure))
         {
            let tcObj = W.model.turnClosures.objects[turnClosure];
            
            if(tcObj.attributes.fromSegID === segID)
            {
               tcObj.direction = uroEnums.DRTC.TURN_OUT;
               RTTCObjs.push(tcObj);
            }
            else if(tcObj.attributes.toSegID === segID)
            {
               tcObj.direction = uroEnums.DRTC.TURN_IN;
               RTTCObjs.push(tcObj);
            }
         }
      }

      if(RTTCObjs.length > 1)
      {
         RTTCObjs = RTTCObjs.sort(function(a,b)
         {
            if(a.attributes.startDate === b.attributes.startDate)
            {
               if(a.direction == uroEnums.DRTC.TURN_OUT) return -1;
               return 1;
            }
            if(a.attributes.startDate > b.attributes.startDate) return 1;
            return -1;
         });
      }
   }

   uroRTCObjs = RTCObjs.concat(RTTCObjs);
   for(let i = 0; i < uroRTCObjs.length; ++i)
   {
      closureTypes |= uroRTCObjs[i].direction;
   }

   // the closure list ordering at this point doesn't always match up to the order used by the closures panel when
   // a mixture of "all segment" and "some segment" closures are present - need to work out what ordering rules
   // WME is using here...
   return closureTypes;
}
function uroGetLengthString(length)
{
   let retval = '';
   if(length == null)
   {
      retval = "Default";
   }
   else if(W.model.isImperial == true)
   {
      retval = (length / (12 * 2.54)).toFixed(1) + "ft";
   }
   else
   {
      retval = (length / 100).toFixed(1) + "m";
   }

   return retval;
}
function uroGetHighlightedMapFeature()
{
   let featureID = W.selectionManager.mouseInFeature;
   let retval = null;

   if(featureID !== undefined)
   {
      let isSelected = W.selectionManager.isSelected(featureID);
      if(isSelected === false)
      {
         retval = W.selectionManager.getObjectByFeatureId(featureID);
      }
   }
   
   return retval;
}
function uroGetFeatureRenderIntent(moObj)
{
   let retval = "unknown";

   if(moObj !== null)
   {
      let isSelected = moObj.selected;

      if(isSelected === true)
      {
         retval = "highlightselected";
      }
      else
      {
         retval = "highlight";
      }
   }

   return retval;
}
function uroExclusiveCB()
{
   let cbChecked = uroUtils.GetCBChecked(this.id);

   if(cbChecked === true)
   {
      let pairedList = this.attributes.pairedWith.value.split(',');
      for(let i=0; i<pairedList.length; i++)
      {
         uroUtils.SetCBChecked(pairedList[i], false);
      }
   }
}
function uroContainsPoint(geo, point)
{
   let retval = false;
   try
   {
      let j = 1;
      for(let i = 0; i < geo.length; ++i)
      {
         if
         (
            ((point[1] >= geo[i][1]) && (point[1] <= geo[j][1])) ||
            ((point[1] >= geo[j][1]) && (point[1] <= geo[i][1]))
         )
         {
            let lx = geo[i][0];
            if(geo[i][1] != geo[j][1])
            {
               let g = ((point[1] - geo[i][1]) / (geo[j][1] - geo[i][1]));
               lx += (g * (geo[j][0] - geo[i][0]));
            }

            if(point[0] <= lx)
            {
               retval = !retval;
            }
         }
         if(++j == geo.length)
         {
            j = 0;
         }
      }
   }
   catch
   {
   }
   return retval;
}
function uroGetAMs(e)
{
   if(uroMTEMode) return;
   if(!uroFilterPreamble) return;
   if(!uroInit.initialised) return;
   if(document.getElementById("uroAMList") == null) return;
   if(document.getElementsByClassName('topbar') == null) return;

   if(uroUtils.GetCBChecked("_cbMoveAMList") === false)
   {
      document.getElementsByClassName('area-managers-region')[0].style.display = "block";
      uroAMList.innerHTML = uroUtils.ModifyHTML("");
      document.getElementsByClassName('topbar')[0].style.backgroundColor=null;
      return;
   }

   document.getElementsByClassName('topbar')[0].style.backgroundColor="#000000";
   document.getElementsByClassName('area-managers-region')[0].style.display = "none";

   let amList = '';
   let tName = '';
   if(W.map.managedAreasLayer.getVisibility() === true)
   {
      let mouseX = e.pageX - document.getElementById('map').getBoundingClientRect().left;
      let mouseY = e.pageY - document.getElementById('map').getBoundingClientRect().top;
      let mousePixel = W.map.getLonLatFromPixel(new OpenLayers.Pixel(mouseX, mouseY));
      let mousePoint = [];
      mousePoint.push(mousePixel.lon);
      mousePoint.push(mousePixel.lat);

      for(let amObj in W.model.managedAreas.objects)
      {
         let nc = W.model.managedAreas.objects[amObj].attributes.geoJSONGeometry.coordinates.length;
         for(let i = 0; i < nc; ++i)
         {
            let geo = W.model.managedAreas.objects[amObj].attributes.geoJSONGeometry.coordinates[i];
            if(nc > 1)
            {
               geo = geo[0];
            }

            if(uroContainsPoint(geo, mousePoint) === true)
            {
               let amName = uroUtils.GetUserNameFromID(W.model.managedAreas.objects[amObj].attributes.userID);
               if(amList.indexOf(amName) === -1)
               {
                  if(amList !== '') amList += ', ';
                  tName = uroUtils.GetUserNameAndRank(W.model.managedAreas.objects[amObj].attributes.userID);
                  if(tName.indexOf('a href') !== -1)
                  {
                     tName = tName.replace('a href', 'a style="color:#c0c0ff;" href');
                  }
                  amList += tName;
               }
            }
         }
      }
      if(amList === '')
      {
         amList = 'none';
      }
      amList = "&nbsp;-&nbsp;<b>Area Managers:</b> "+amList;
   }
   document.getElementById("uroAMList").innerHTML = uroUtils.ModifyHTML(amList);
}
function uroNewTabAtMouseLoc(x, y)
{
   let tPix = new OpenLayers.Pixel(x,y);
   let mPos = uroUtils.ConvertMercatorToWGS84(W.map.getLonLatFromPixel(tPix));
   let nZoom = W.map.getZoom();
   if(nZoom < 17) nZoom = 17;
   let nHref = window.location.origin + window.location.pathname;
   nHref += '?lon=' + mPos.lon;
   nHref += '&lat=' + mPos.lat;
   nHref += '&zoomLevel=' + nZoom;
   window.open(nHref);
}
function uroMouseDown(e)
{
   uroMouseIsDown = true;
   if((e.altKey === true) && (e.ctrlKey === true))
   {
      uroNewTabAtMouseLoc(e.offsetX, e.offsetY);
   }
}
function uroMouseUp()
{
   uroMouseIsDown = false;
}
function uroTestPointerOutsideMap(mX, mY)
{
   let mapElm = document.getElementById("map");
   if(mapElm === undefined) return;
   let mapBCR = mapElm.getBoundingClientRect();

   if
   (
      (mX < mapBCR.left) ||
      (mX > mapBCR.right) ||
      (mY < mapBCR.top) ||
      (mY > mapBCR.bottom)
   )
   {
      if(uroUtils.GetCBChecked('_cbKillInertialPanning') === true)
      {
         let controller = null;
         if (W.map.navigationControl) 
         {
		      controller = W.map.navigationControl;
         } 
         else if(W.map.controls.find(control => control.CLASS_NAME == 'OpenLayers.Control.Navigation'))
         {
            controller = W.map.controls.find(control => control.CLASS_NAME == 'OpenLayers.Control.Navigation');
         }
         if (controller !== null)
         {
            controller.dragPan.panMapStart();
         }
      }
      return true;
   }
   else
   {
      return false;
   }
}
function uroMouseOut(e)
{
   if(uroTestPointerOutsideMap(e.clientX, e.clientY))
   {
      uroPopup.Hide();
   }
}
function uroUREvent_onObjectsAdded()
{
   if(uroUtils.GetCBChecked('_cbURResolverIDFilter') === true)
   {
      uroUpdateEditorList(W.model.mapUpdateRequests.objects, '_selectURResolverID', false, false, true, false);
   }
   if(uroPopulatingRequestSessions === false)
   {
      uroFilterURs();
   }
}
function uroGetSelectedURCommentCount()
{
   if(W.model.updateRequestSessions.objects[uroSelectedURID] != null)
   {
      let cachedCommentCount = W.model.updateRequestSessions.objects[uroSelectedURID].attributes.comments.length;
      uroDBG.AddLog(uroSelectedURID+':'+cachedCommentCount+' '+uroExpectedCommentCount);

      // if there aren't the same number of cached comments as there are comments in the UR dialog list, initiate
      // a refresh of the comment data...
      if(cachedCommentCount != uroExpectedCommentCount)
      {
         if(uroPendingCommentDataRefresh === true)
         {
            if(cachedCommentCount > 0)
            {
               uroCachedLastCommentID = W.model.updateRequestSessions.objects[uroSelectedURID].attributes.comments[cachedCommentCount-1].id;
            }
            else
            {
               uroCachedLastCommentID = null;
            }
            uroDBG.AddLog('updateRequestSessions refresh required for UR '+uroSelectedURID);
            if(uroCachedLastCommentID !== null)
            {
               uroDBG.AddLog('last comment ID for this UR is '+uroCachedLastCommentID);
            }
            else
            {
               uroDBG.AddLog('first comment for this UR, no previous comment to ID');
            }
            let idList = [];
            idList.push(uroSelectedURID);
            // need to delete the existing cache object first, as .get() is only capable of creating new objects,
            // it doesn't seem able to update an existing object with new data
            W.model.updateRequestSessions.remove(W.model.updateRequestSessions.objects[uroSelectedURID]);
            W.model.updateRequestSessions.getAsync(idList);

            // the call to .get() initiates a XMLHttpRequest for the data, so we now need to switch modes - the
            // refresh process has started so we're no longer pending, but we are now waiting for the XMLHttpRequest
            // to return something...
            uroPendingCommentDataRefresh = false;
            uroWaitingCommentDataRefresh = true;
         }
         else
         {
            if(cachedCommentCount > 0)
            {
               let currentLastCommentID = W.model.updateRequestSessions.objects[uroSelectedURID].attributes.comments[cachedCommentCount-1].id;
               if(currentLastCommentID == uroCachedLastCommentID)
               {
                  // most recent comment loaded for this UR is the same one that was present at the start of this
                  // refresh process, so kick back into pending mode so we can retry the .get()...
                  uroDBG.AddLog('latest comment ID still the same, reverting to pending mode...');
                  uroPendingCommentDataRefresh = true;
               }
               else
               {
                  // something may have gone awry here - the most recent comment loaded for this UR doesn't have the
                  // same ID as the one present at the start of the refresh process, yet the comment counts still don't
                  // match up, which suggests either a comment got lost along the way or someone else has commented on
                  // the same UR at almost the same time.  To get out of the loop this would create, assume that a
                  // mismatch in the IDs means the .get() has completed successfully no matter what the new comment
                  // count is, and take this new count to be the count we were expecting all along...
                  uroDBG.AddLog('latest comment ID different, but expected count not correct...');
                  uroExpectedCommentCount = cachedCommentCount;
               }
            }
            else
            {
               uroDBG.AddLog('first comment on this UR not received yet, reverting to pending mode...');
               uroPendingCommentDataRefresh = true;
            }
         }

      }
      else
      {
         // if the WME session is loaded with a UR already selected, such that WME has opened the UR dialog as part
         // of the session startup process, adding new comments to the UR cause the cached data to be updated immediately.
         // This prevents URO+ from switching into waiting mode in the above block of code, so we have to instead do
         // it here by comparing the cached count against the expected count following the Send click event.
         if(cachedCommentCount >= uroExpectedCommentCount)
         {
            uroPendingCommentDataRefresh = false;
            uroWaitingCommentDataRefresh = true;
            uroExpectedCommentCount = null;
         }

         // once the cached data has been updated, refilter the URs so that the new comment count is taken into account
         // immediately for filtering and display purposes
         if(uroWaitingCommentDataRefresh === true)
         {
            uroWaitingCommentDataRefresh = false;
            uroFilterURs();
            uroDBG.AddLog('refresh complete');
         }
      }
   }
}
function uroAddedComment()
{
   // when the user clicks the Send button to submit a new UR comment, this event handler fires before the new comment is
   // posted to the server and thus also before the comment list gets updated in the UR dialog.  So we take the current
   // comment count and, if the new comment edit box isn't empty, increment it by 1 to get the expected count.  Then we
   // set the pending flag true to initiate a session refresh on the next 100ms tick
   uroExpectedCommentCount = W.model.updateRequestSessions.objects[uroSelectedURID].attributes.comments.length;
   if(document.getElementsByClassName('new-comment-text')[0].value !== '')
   {
      uroExpectedCommentCount++;
      uroDBG.AddLog('new comment added to UR '+uroSelectedURID+', cache refresh required...');
      uroPendingCommentDataRefresh = true;
   }
   else
   {
      uroPendingCommentDataRefresh = false;
   }
}
function uroInhibitNextUpdateRequestButton(e)
{
   e.stopPropagation();

   let doClick = true;
   if(document.getElementsByClassName('form-control new-comment-text').length > 0)
   {
      if(document.getElementsByClassName('form-control new-comment-text')[0].textLength > 0)
      {
         uroAlertBox.Show("fa-warning", "URO+ Warning", "Comment not sent, close report panel anyway?", true, "Yes", "No", uroCloseReportPanel, null);
		 // set doClick to false here, as uroCloseReportPanel will be called by the alert box handler if required...
		 doClick = false;
      }
   }
   // no alert box has been generated, so close the panel
   if(doClick)
   {
	   uroCloseReportPanel();
   }
}
function uroCloseReportPanel()
{
   document.getElementsByClassName('close-panel')[0].click();
}
function uroIncrementClosureDate(oldDate, incByDays)
{
   // Thanks to WME no longer using consistent ISO8601 date formatting when displaying
   // closure details, parsing the date string is now somewhat more involved.  Thanks devs...  

   // Default to returning the existing date, just in case we can't increment it
   let retval = oldDate;
   let sepChar = null;

   // Search through oldDate for a non-digit character, so we know what's
   // being used to seperate the year, month and date values by this locale...
   for(let i = 0; i < oldDate.length; ++i)
   {
      if((oldDate[i] < '0') || (oldDate[i] > '9'))
      {
         sepChar = oldDate[i];
         break;
      }
   }

   if(sepChar != null)
   {
      // First build a "probe" date object we can use to determine what the user's WME locale
      // does with dates.
      let incDate = new Date();
      incDate.setFullYear(3000);
      incDate.setMonth(10);
      incDate.setDate(22);

      // With these three carefully chosen date elements set, we now generate a localised datestring
      // using the WME locale setting - the first character of which then tells us the date
      // format - 1 = MDY (ack ptui), 2 = DMY (good), 3 = YMD (sweet!)
      let localeDate = incDate.toLocaleDateString(I18n.locale);
      let dateFormat = localeDate[0];

      // Now we know the seperator character and the date format, so we can finally start to parse
      // the existing date string...
      let oldDateBits = oldDate.split(sepChar);
   
      let datePos;
      let monthPos;
      let yearPos;
      if(dateFormat == '1')
      {
         datePos = 1;
         monthPos = 0;
         yearPos = 2;
      }
      else if(dateFormat == '2')
      {
         datePos = 0;
         monthPos = 1;
         yearPos = 2;
      }
      else
      {
         datePos = 2;
         monthPos = 1;
         yearPos = 0;
      }
      
      incDate.setFullYear(parseInt(oldDateBits[yearPos]));
      incDate.setMonth(parseInt(oldDateBits[monthPos]) - 1);
      incDate.setDate(parseInt(oldDateBits[datePos]));
      let tDate = new Date(incDate.getTime());
      incDate.setDate(tDate.getDate() + incByDays);

      retval = incDate.toLocaleDateString(I18n.locale);

      // Except for those pesky locales where toLocaleDateString() doesn't *quite*
      // return what WME is expecting.  Because, you know, why make it easy for
      // scripters when you can chuck in a few subtle curveballs like this, eh...

      if(I18n.locale == 'bg')
      {
         // Remove the "r." suffix
         retval = retval.split(" ")[0] + " ";
      }
   }
   
   return retval;
}
function uroGetElementProperty(elmName, elmOffset, elmProperty)
{
   let retval = null;
   if(document.getElementsByName(elmName).length > elmOffset)
   {
      retval = document.getElementsByName(elmName)[elmOffset][elmProperty];
   }
   else if(document.getElementById(elmName) !== null)
   {
      retval = document.getElementById(elmName)[elmProperty];
   }
   return retval;
}
function uroGetShadowElementProperty(elmName, shadowElmType, property)
{
   let retval = null;
   let tObj = document.getElementById(elmName);
   if(tObj !== null)
   {
      let sObj = tObj.shadowRoot.querySelector(shadowElmType);
      if(sObj !== null)
      {
         retval = sObj[property];
      }
   }
   return retval;
}
let rtcTotal = null;
let rtcSegIDs = null;
function uroScrollToEndOfClosures()
{
   let cItems = document.querySelectorAll('.closure-item');
   let nClosures = cItems.length;
   let segIDs = W.selectionManager.getSelectedFeatureIds();
   let sameSegs = false;
   if((segIDs !== null) && (rtcSegIDs !== null))
   {
      if(segIDs.length == rtcSegIDs.length)
      {
         sameSegs = true;
         for(let i = 0; i < segIDs.length; ++i)
         {
            if(segIDs[i] != rtcSegIDs[i])
            {
               sameSegs = false;
               break;
            }
         }
      }
   }
   if(sameSegs === false)
   {
      rtcTotal = null;
      rtcSegIDs = segIDs;
   }
   if(nClosures > rtcTotal)
   {
      if(cItems[0].getBoundingClientRect().height != 0)
      {
         rtcTotal = nClosures;
         if(uroUtils.GetCBChecked('_cbAutoScrollClosureList') == true)
         {
            // Scroll to the end of the closure tab, as that's where the closure you're most likely to be cloning
            // is located...
            cItems[cItems.length - 1].scrollIntoView();
         }
      }
   }
}
function uroClosureEditUIChanged()
{
   if(document.getElementsByClassName('edit-closure').length === 1)
   {
      // note: this also fires when the UI is closed, due to the change events triggered as its elements are removed 
      // prior to the tab itself closing...

      let notReady = 0;
      let mteDropDown = document.getElementById('closure_eventId');
      if(document.getElementById('closure_reason').shadowRoot.querySelector('input') === null) notReady += 1;
      if(document.getElementById('closure_direction').shadowRoot.querySelector('.selected-value') === null) notReady += 2;
      else if(document.getElementById('closure_direction').shadowRoot.querySelector('.selected-value').innerText === "") notReady += 4;
      if(document.getElementById('closure_startDate') === null) notReady += 8;
      if(document.getElementById('closure_endDate') === null) notReady += 16;
      if(mteDropDown.shadowRoot.querySelector('.selected-value') === null) notReady += 32;
      else if(mteDropDown.shadowRoot.querySelector('.selected-value').innerText === "") notReady += 64;
      if(document.getElementById('closure_permanent').shadowRoot.querySelector('.wz-checkbox') === null) notReady += 128;
      if(notReady === 0)
      {
         if(uroRTCClone.PendingClone === -3)
         {
            uroRTCClone.Complete();
         }
         else if(uroRTCClone.PendingClone !== -1)
         {
            uroRTCClone.Copy();
         }
         else
         {
            uroFixMTEDropDown(mteDropDown);
         }
      }
   }
   else
   {
      if(uroRTCClone.PendingClone === -2)
      {
         // generate a click event on the Add a closure button to open up the closure editing UI, then
         // wait for the UI to finish opening...
         document.getElementsByClassName('add-closure-button')[0].click();
         uroRTCClone.PendingClone = -3;
      }
      uroClosureListHandler();
   }
}
function uroTSTPopupHandler()
{
   if(document.getElementsByClassName('panel')[0] === undefined)
   {
      uroHidePopupOnPanelOpen = true;
   }

   if(uroPopup.shown === true)
   {
      let hidePopup = false;

      if(document.getElementsByClassName('panel')[0] != null)
      {
         if(uroHidePopupOnPanelOpen === true)
         {
            hidePopup = true;
            uroHidePopupOnPanelOpen = false;
         }
      }

      if(hidePopup === true)
      {
         uroPopup.Hide();
      }
   }

   if((uroAFN.hoverObj !== null) && (uroAFN.hoverTime != -1) && (uroAFN.overlayShown === false))
   {
      if(++uroAFN.hoverTime > 5)
      {
         uroAFN.OverlaySetup();
      }
   }
   uroAFN.ReplaceAreaNames(false);

   if(uroPopup.autoHideTimer > 0)
   {
      if(--uroPopup.autoHideTimer === 0)
      {
         uroPopup.Hide();
      }
   }

   if(uroPopup.timer > 0)
   {
      if(uroPopup.mouseIn === false)
      {
         uroPopup.timer--;
      }
   }
   if(uroPopup.timer === 0)
   {
      uroPopup.Hide();
   }
}
function uroTSTNextBtnHandler()
{
   // replaces the "next xxx" button on UR, MP and PUR editing UIs

   // Correctly determining what WME is displaying for the "next" button in the UR/MP/(P)PUR panel is not trivial due to
   // inconsistencies in the panel behaviour depending on whether it was opened by clicking directly on the relevant
   // marker, or by clicking on the associated feed entry...  For PURs, there's also the added complication of multi-part
   // update requests, where the same marker/panel are used to access more than one request and where, therefore, we need
   // to enable access to all requests contained within the PUR, but still inhibit the "next" button once the last
   // request in the multi-part sequence has been viewed.
   //
   // For directly-accesed markers, the "next" button caption is:
   //
   //    URs   = "Next update request" (update_requests.panel.next)
   //    MPs   = "Next map problem" (problems.panel.next)
   //    PURs  = "Next place" for single-part PURs or for the last part of a multi-part PUR (venues.update_requests.panel.next_venue)
   //          = "Next" for all but the last part of a multi-part PUR (venues.update_requests.panel.next)
   //    PPURs = "Next place" (venues.update_requests.panel.next_venue)
   //
   // For markers accessed via the feed, the "next" button caption always appears to be "Next issue" (feed.issues.next)



   if(W.map.panelRegion.hasView() === true)
   {
      let nurButton = W.map.panelRegion.$el[0].getElementsByClassName('next')[0];
      if(nurButton === undefined)
      {
         nurButton = W.map.panelRegion.$el[0].getElementsByClassName('next-venue')[0];
      }
      if(nurButton !== undefined)
      {
         let doneString = I18n.lookup('problems.panel.done');
         let btnCaptionIsNextPlace = (nurButton.innerHTML.indexOf(I18n.lookup('venues.update_requests.panel.next_venue')) !== -1);
         let btnCaptionIsDefaultUR = (nurButton.innerHTML.indexOf(I18n.lookup('update_requests.panel.next')) !== -1);
         let btnCaptionIsDefaultMP = (nurButton.innerHTML.indexOf(I18n.lookup('problems.panel.next')) !== -1);
         let btnCaptionIsNextIssue = (nurButton.innerHTML.indexOf(I18n.lookup('feed.issues.next')) !== -1);

         let updateButton = false;

         let panelClass = W.map.panelRegion.$el[0].childNodes[0].childNodes[0].className;
         let isURorMPPanel = (panelClass.indexOf('problem-edit') !== -1);
         let isPURPanel = (panelClass.indexOf('place-update') !== -1);

         if(isURorMPPanel === true)
         {
            // user has enabled UR button mod?
            if(uroUtils.GetCBChecked('_cbInhibitNURButton') === true)
            {
               // the native UR panel button will always either be "Next update request" or "Next issue"
               updateButton = ((btnCaptionIsDefaultUR) || (btnCaptionIsNextIssue));
            }

            // user has enabled MP button mod?
            if(uroUtils.GetCBChecked('_cbInhibitNMPButton') === true)
            {
               // there's no way to determine if the edit panel has been opened for a UR or a MP, however as MPs
               // don't currently appear in the feed, the native button only uses "Next map problem" as its caption
               updateButton = (updateButton || btnCaptionIsDefaultMP);
            }
         }
         else if(isPURPanel === true)
         {
            if(uroUtils.GetCBChecked('_cbInhibitNPURButton') === true)
            {
               // for a (P)PUR, only modify the button if it's showing the "Next place" or "Next issue" caption, to
               // avoid messing up the "Next" button used to move to the next part of a multi-part PUR...
               updateButton = ((btnCaptionIsNextPlace === true) || (btnCaptionIsNextIssue));
            }
         }

         if(updateButton === true)
         {
            uroDBG.AddLog('inhibit Next UR/MP/PUR button');

            // alter the button caption
            nurButton.innerHTML = uroUtils.ModifyHTML(doneString);
            // Add a new click handler to override the native one - this acts both to prevent the normal action of the "Next UR/MP/PUR" button in
            // moving to the next UR/MP/PUR, and also allows us to warn about closing the UR panel if there's an unsent comment...
            nurButton.addEventListener("click", uroInhibitNextUpdateRequestButton, false);
         }
      }
      uroInhibitURFiltering = false;
   }
}
function uroTSTCommentAddedHandler()
{
   // test for the opening or closing of the UR editing dialog so we can detect when a new comment is added
   let URDialogIsOpen = false;
   let panelOpen = (document.getElementById('panel-container').firstChild !== null);

   if(panelOpen)
   {
      URDialogIsOpen = (document.getElementById('panel-container').getElementsByClassName('conversation').length > 0);
   }

   if(URDialogIsOpen)
   {
      let thisSelectedURID = document.getElementsByClassName('permalink')[0].href.split('&mapUpdateRequest=');
      if(thisSelectedURID.length > 1)
      {
         thisSelectedURID = thisSelectedURID[1].split('&')[0];
      }
      else
      {
         thisSelectedURID = null;
      }

      if((thisSelectedURID != uroSelectedURID) || ((thisSelectedURID != uroMarkers.clickedOnID) && (uroMarkers.clickedOnID != null)))
      {
         // if the user selects a new UR whilst the editing dialog is still open, treat it in the
         // same way as if the user had selected that UR with the dialog closed
         uroURDialogIsOpen = false;
         uroSelectedURID = null;
      }

      if(((uroURDialogIsOpen === false) && (uroSelectedURID === null)) || (uroURReclickAttempts > 0))
      {
         // user is editing a new UR

         // add our own click event handler to the Send button, so we can do stuff whenever a new comment is added
         if(document.getElementsByClassName('new-comment-form').length > 0)
         {
            if(document.getElementsByClassName('new-comment-form')[0].getElementsByClassName('send-button').length > 0)
            {
               document.getElementsByClassName('new-comment-form')[0].getElementsByClassName('send-button')[0].addEventListener("click", uroAddedComment, false);

               uroSelectedURID = thisSelectedURID;
               uroDBG.AddLog('user is editing UR '+uroSelectedURID);
               uroExpectedCommentCount = W.model.updateRequestSessions.objects[uroSelectedURID].attributes.comments.length;

               if((uroHoveredURID !== null) && (uroSelectedURID !== null) && (parseInt(uroHoveredURID) !== parseInt(uroSelectedURID)))
               {
                  if(uroURReclickAttempts === 0)
                  {
                     uroDBG.AddLog('DANGER, WILL ROBINSON!  You clicked on UR ID '+uroHoveredURID+' but WME has loaded the details for UR ID '+uroSelectedURID+' instead, attempting to fix...');
                  }
                  if(++uroURReclickAttempts < 3)
                  {
                     //uroRestackMarkers();
                     let urMarker = uroGetMarker(uroLayers.ID.UR,uroHoveredURID);
                     if(urMarker !== null)
                     {
                        let urMarkerAttributes = uroGetAttributes(uroLayers.ID.UR, uroHoveredURID);
                        if(urMarkerAttributes !== null)
                        {
                           urMarkerAttributes.geometry.x = urMarkerAttributes.geometry.realX;
                           urMarkerAttributes.geometry.y = urMarkerAttributes.geometry.realY;
                           uroOpenURDialog(uroHoveredURID);
                        }
                     }
                     return;
                  }
                  else
                  {
                     uroDBG.AddLog('Woe is me, attempting to open UR ID '+uroHoveredURID+' has failed...');
                     uroAlertBox.Show('fa-warning', 'URO+ Warning', 'WME may have opened the details panel for a different UR to the one you selected, proceed with caution', false, "OK", "", null, null);
                  }
               }
               uroURReclickAttempts = 0;
               uroFilterURs();
            }
         }
      }
   }
   else if(uroURDialogIsOpen === true)
   {
      // dialog was open and has now been closed
      uroSelectedURID = null;
      uroMarkers.clickedOnID = null;
      uroFilterURs();
   }
   uroURDialogIsOpen = URDialogIsOpen;

   if(((uroPendingCommentDataRefresh === true) || (uroWaitingCommentDataRefresh === true)) && (uroSelectedURID !== null))
   {
      uroDBG.AddLog('check completion of comment data refresh for UR '+uroSelectedURID+' ('+uroPendingCommentDataRefresh+','+uroWaitingCommentDataRefresh+')');
      uroGetSelectedURCommentCount();
   }

}
function uroClosureListHandler()
{
   // Handles adjustments to the closure list in the sidepanel...
   //
   if(uroUtils.GetCBChecked('_cbMasterEnable') === false)
   {
      return;
   }

   // List entry filtering
   let nEntries = document.querySelectorAll('.closure-item').length;
   if(nEntries > 0)
   {
      let filterExpired = uroUtils.GetCBChecked('_cbHideExpiredSidepanelRTCs');
      let filterCurrent = uroUtils.GetCBChecked('_cbHideSidepanelRTCs');
      let filterFuture = uroUtils.GetCBChecked('_cbHideFutureSidepanelRTCs');
      let filterUnknown = uroUtils.GetCBChecked('_cbHideUnknownSidepanelRTCs');
      for(let i = 0; i < nEntries; ++i)
      {
         let hide = false;
         let cElm = document.querySelectorAll('.closure-item')[i];
         let ciSrc = cElm.querySelector('img')?.src;
         if(ciSrc != undefined)
         {
            hide |= ((ciSrc.indexOf('finished') != -1) && (filterExpired == true));
            hide |= ((ciSrc.indexOf('active') != -1) && (filterCurrent == true));
            hide |= ((ciSrc.indexOf('not-started') != -1) && (filterFuture == true));
            hide |= ((ciSrc === "") && (filterUnknown == true));
         }

         if(hide == true)
         {
            cElm.style.display = "none";
         }
         else
         {
            cElm.style.display = "block";
         }
      }
   }

   // Closure cloning controls
   if((document.querySelectorAll('.closures-list').length > 0) && (document.querySelector('.closures-list').getAttribute('touchedbyuro') === null))
   {      
      let nClosures;
      let cLoop;
      let btnElm;

      // Cloning doesn't work with certain locales due to the way the date strings are formatted...
      if
      (
         (I18n.locale == "fa-IR") || 
         (I18n.locale == 'ar') ||
         (I18n.locale == 'zh') ||
         (I18n.locale == 'ko')
      )
      {
         // Sorry :-(
      }
      else
      {
         // for the others, are there any closures defined for all of the selected segment(s)...
         if(document.getElementsByClassName('full-closures').length > 0)
         {
            nClosures = document.getElementsByClassName('full-closures')[0].querySelectorAll('.closure-item.is-editable').length;
            if(nClosures > 0)
            {
               // Force a refresh of uroRTCObjs for the selected segment, as this is no longer guaranteed to have already occurred...
               let segID = W.selectionManager.getSelectedFeatureIds()[0];
               uroGetSelectedSegmentRTCs(segID);

               // and if so, have we already added the clone icon?
               for(cLoop = 0; cLoop < nClosures; cLoop++)
               {
                  btnElm = document.getElementsByClassName('full-closures')[0].querySelectorAll('.closure-item.is-editable')[cLoop].getElementsByClassName('closure-title')[0];
                  if((btnElm.innerHTML.indexOf('_uroCloneClosure-') == -1) && (uroGetRTCOrigin(uroRTCObjs[cLoop]) !== uroEnums.TRTC.UNKNOWN))
                  {
                     let newNode = document.createElement("div");
                     let anchorID1 = '_uroCloneClosure-1-'+cLoop;
                     let anchorID2 = '_uroCloneClosure-7-'+cLoop;
                     let newAnchor = '<a id="'+anchorID1+'" href="#">';
                     newAnchor += "<i style='font-size: 150%; cursor: copy' class='fa fa-copy'></i>";
                     newAnchor += "</a><sup>+1</sup>&nbsp;";
                     newAnchor += '<a id="'+anchorID2+'" href="#">';
                     newAnchor += "<i style='font-size: 150%; cursor: copy' class='fa fa-copy'></i>";
                     newAnchor += "</a><sup>+7</sup>";
                     newNode.innerHTML = uroUtils.ModifyHTML(newAnchor);
                     btnElm.prepend(newNode);
                     uroUtils.AddEventListener(anchorID1,"click",uroRTCClone.Clone,false);
                     uroUtils.AddEventListener(anchorID2,"click",uroRTCClone.Clone,false);

                     let chipElm = btnElm.querySelector("wz-image-chip");
                     chipElm.innerHTML = chipElm.innerHTML.split('>')[0] + '>';
                  }
               }
            }
         }
      }

      // if there's more than one closure (full or partial) listed, also add the delete all button if not already present
      nClosures = document.querySelectorAll('.closure-item.is-editable').length;
      if(nClosures > 0)
      {
         if(document.getElementById('_btnDeleteAllClosures') === null)
         {
            let daDiv = document.createElement('wz-button');
            daDiv.className = 'delete-all-button btn is-expanded'; //btn-primary';
            daDiv.id = '_btnDeleteAllClosures';

            let tHTML = '<i class="fa fa-trash"></i> '+I18n.lookup("closures.delete_confirm_no_reason");
            if(nClosures > 1)
            {
               tHTML += ' ('+I18n.lookup("closures.apply_to_all")+')';
            }
            daDiv.innerHTML = uroUtils.ModifyHTML(tHTML);
            daDiv.style.width = '100%';
            daDiv.style.marginBottom = '10px';

            let acBtn = document.getElementsByClassName('add-closure-button')[0];
            if(acBtn !== undefined)
            {
               acBtn.parentNode.insertBefore(daDiv, acBtn.nextSibling);
               uroUtils.AddEventListener('_btnDeleteAllClosures',"click", uroRTCClone.DeleteAll, false);
            }
         }
      }

      document.querySelector('.closures-list').setAttribute('touchedbyuro','true');
   }
}
function uroMiscUITweaksHandler()
{
   if(uroFilterPreamble())
   {
      // clickifies the ExtraInfo URL present in some MPs
      {
         if(document.getElementById('panel-container').getElementsByClassName('extraInfo').length > 0)
         {
            if(document.getElementById('panel-container').getElementsByClassName('extraInfo')[0].touchedByURO === undefined)
            {
               let tDesc = document.getElementById('panel-container').getElementsByClassName('extraInfo')[0].innerHTML;
               tDesc = uroUtils.Clickify(tDesc, '');
               document.getElementById('panel-container').getElementsByClassName('extraInfo')[0].innerHTML = uroUtils.ModifyHTML(tDesc);
               document.getElementById('panel-container').getElementsByClassName('extraInfo')[0].touchedByURO = true;
            }
         }
      }
   }
}
function uroMainTick()
{
   if(uroMTEMode) return;
   if(uroInit.setupListeners)
   {
      if(uroInit.finalisingListenerSetup === false)
      {
         if(W.loginManager.isLoggedIn())
         {
            if(document.getElementsByClassName('topbar').length === 0) return;
            uroInit.FinalizeListenerSetup();
            document.getElementsByClassName('topbar')[0].appendChild(uroAMList);
         }
      }
   }
   else
   {
      if(uroUtils.GetCBChecked('_cbMasterEnable') === true)
      {
         // do one maintick handler call in each 10ms cycle to minimise the time stuck within the maintick handler without
         // unduly affecting the overall response time for each individual handler

         if(uroMainTickStage === 0) uroTSTPopupHandler();
         if(uroMainTickStage == 1) uroTSTNextBtnHandler();
         if(uroMainTickStage == 2) uroTSTCommentAddedHandler();
         if(uroMainTickStage == 4) uroMiscUITweaksHandler();

         if(++uroMainTickStage == 6) uroMainTickStage = 0;
      }
   }
}
function uroNewLookCheckDetailsRequest()
{
   let thisurl = document.location.href;
   let doRetry = true;
   let urID;
   let endmarkerpos = thisurl.indexOf('&endshow');
   let showmarkerpos = thisurl.indexOf('&showturn=');

   if((endmarkerpos != -1) && (showmarkerpos != -1))
   {
      showmarkerpos += 10;
      uroDBG.AddLog('showturn tab opened');
      urID = thisurl.substr(showmarkerpos,endmarkerpos-showmarkerpos);
      uroDBG.AddLog(' turn problem ID = '+urID);

      try
      {
         uroGetMarker(uroLayers.ID.MP,urID).element.click();
         doRetry = false;
      }
      catch(err)
      {
         uroDBG.AddLog('problems not fully loaded, retrying...');
      }

      if(doRetry) window.setTimeout(uroNewLookCheckDetailsRequest,500);
   }
   else
   {
      showmarkerpos = thisurl.indexOf('&showpur=');
      if((endmarkerpos != -1) && (showmarkerpos != -1))
      {
         showmarkerpos += 9;
         uroDBG.AddLog('showPUR tab opened');
         urID = thisurl.substr(showmarkerpos,endmarkerpos-showmarkerpos);
         uroDBG.AddLog(' PUR ID = '+urID);

         try
         {
            uroGetMarker(uroLayers.ID.PUR, urID).element.click();
            doRetry = false;
         }
         catch(err)
         {
            uroDBG.AddLog('PURs not fully loaded, retrying...');
         }

         if(doRetry) window.setTimeout(uroNewLookCheckDetailsRequest,500);
      }

      else
      {
         showmarkerpos = thisurl.indexOf('&showppur=');
         if((endmarkerpos != -1) && (showmarkerpos != -1))
         {
            showmarkerpos += 10;
            uroDBG.AddLog('showPPUR tab opened');
            urID = thisurl.substr(showmarkerpos,endmarkerpos-showmarkerpos);
            uroDBG.AddLog(' PPUR ID = '+urID);

            try
            {
               uroGetMarker(uroLayers.ID.PPUR, urID).element.click();
               doRetry = false;
            }
            catch(err)
            {
               uroDBG.AddLog('PPURs not fully loaded, retrying...');
            }

            if(doRetry) window.setTimeout(uroNewLookCheckDetailsRequest,500);
         }
      }
   }

}
function uroUpdateVenueEditorLists()
{
   if(Object.keys(W.model.venues.objects).length === 0) return;

   // build the list of all userIDs contained in the currently loaded venue objects
   let selectedIdx = null;
   let listedIDs = [];
   let idx;
   for(idx in W.model.venues.objects)
   {
      if(W.model.venues.objects.hasOwnProperty(idx))
      {
         let obj = W.model.venues.objects[idx].attributes;
         let cbID = obj.createdBy;
         let ubID = obj.updatedBy;

         if((cbID !== null) && (listedIDs.indexOf(cbID) == -1))
         {
            listedIDs.push(cbID);
         }
         if((ubID !== null) && (ubID !== cbID) && (listedIDs.indexOf(ubID) == -1))
         {
            listedIDs.push(ubID);
         }
      }
   }

   // check for any previously selected userIDs in the two selector lists, then clear both lists
   // and repopulate using the newly gathered ID collection from above, and finally reselect the
   // previously selected user if they're still present in the new list...
   let selector;
   let selectedUser;
   let users = W.model.users.getByIds(listedIDs);
   let selectorEntry;

   for(let i=0; i<2; i++)
   {
      if(i === 0) selector = document.getElementById('_selectPlacesUserID');
      else selector = document.getElementById('_selectHidePlacesUserID');

      selectedUser = null;
      if(selector.selectedOptions[0] != null)
      {
         selectedUser = parseInt(selector.selectedOptions[0].value);
      }
      while(selector.options.length > 0)
      {
         selector.options.remove(0);
      }
      selector.options.add(new Option('<select a user>', null));
      if(listedIDs.length > 0)
      {
         selectorEntry = '';
         for(idx=0; idx<users.length; idx++)
         {
            if(users[idx].attributes.userName === undefined)
            {
               selectorEntry = users[idx].attributes.id;
            }
            else
            {
               selectorEntry = users[idx].attributes.userName;
            }
            selector.options.add(new Option(selectorEntry, users[idx].id));
            if(users[idx].attributes.id == selectedUser)
            {
               selectedIdx = idx+1;
            }
         }
      }

      if(selectedIdx !== null)
      {
         selector.selectedIndex = selectedIdx;
      }
   }
}
function uroPlacesEditorSelected()
{
   let selector = document.getElementById('_selectPlacesUserID');
   if(selector.selectedIndex > 0)
   {
      document.getElementById('_textPlacesEditor').value = document.getElementById('_selectPlacesUserID').selectedOptions[0].innerHTML;
   }
}
function uroHidePlacesEditorSelected()
{
   let selector = document.getElementById('_selectHidePlacesUserID');
   if(selector.selectedIndex > 0)
   {
      document.getElementById('_textHidePlacesEditor').value = document.getElementById('_selectHidePlacesUserID').selectedOptions[0].innerHTML;
   }
}
function uroCamEditorSelected()
{
   let selector = document.getElementById('_selectCameraUserID');
   if(selector.selectedIndex > 0)
   {
      document.getElementById('_textCameraEditor').value = document.getElementById('_selectCameraUserID').selectedOptions[0].innerHTML;
   }
}
function uroSetStyles(obj)
{
   obj.style.fontSize = '12px';
   obj.style.lineHeight = '100%';
   obj.style.flex = '1';
   obj.style.overflowY = 'auto';
}
function uroSetSectionTabStyles()
{
   for(let i =0; i < uroTabs.CtrlTabs.length; ++i)
   {
      uroSetStyles(uroTabs.CtrlTabs[i][uroTabs.FIELDS.TABBODY]);
   }
}
function uroPlacesGroupCEHandler(groupidx)
{
   if(uroPlacesGroupsCollapsed[groupidx] === false)
   {
      document.getElementById('_uroPlacesGroup-'+groupidx).style.display = "block";
      document.getElementById('_uroPlacesGroupState-'+groupidx).className = "fa fa-minus-square-o";
   }
   else
   {
      document.getElementById('_uroPlacesGroup-'+groupidx).style.display = "none";
      document.getElementById('_uroPlacesGroupState-'+groupidx).className = "fa fa-plus-square-o";
   }
}
function uroPlacesGroupCollapseExpand()
{
   let groupidx = this.id.substr(21);
   if(uroPlacesGroupsCollapsed[groupidx] === true) uroPlacesGroupsCollapsed[groupidx] = false;
   else uroPlacesGroupsCollapsed[groupidx] = true;
   uroPlacesGroupCEHandler(groupidx);
   return false;
}
function uroGetMarkerIDs(markerType)
{
   let idList = [];

   if(uroLayers.layers[markerType].mf !== null)
   {
      // Refresh .mf, as this is no longer guaranteed to have been set up as a reference to
      // whatever's in the current W.map.layers object...
      uroLayers.layers[markerType].mf = uroLayers.GetMarkersOrFeatures(markerType);
      for(let i = 0; i < uroLayers.layers[markerType].mf.length; ++i)
      {
         let dID = uroLayers.layers[markerType].mf[i]?.element?.attributes['data-id']?.value;
         if(dID === undefined)
         {
            dID = uroLayers.layers[markerType].mf[i]?.attributes?.wazeFeature?.id;
         }
         if(dID !== undefined)
         {
            idList.push(dID);
         }
      }
   }
   return idList;
}
function uroGetAttributes(markerType, markerID)
{
   if(markerType == uroLayers.ID.UR) return W.model.mapUpdateRequests.objects[markerID].attributes;
   if(markerType == uroLayers.ID.MP) return W.model.mapProblems.objects[markerID].attributes;
   return null;
}
function uroGetMarker(markerType, markerID)
{
   if(typeof(markerID) === 'number')
   {
      markerID = markerID.toString();
   }

   let retval = null;

   if(markerID !== null)
   {
      let mObj = null;
      if(markerType === uroLayers.ID.UR)
      {
         mObj = W.model.mapUpdateRequests.getObjectById(markerID);
      }
      else if(markerType === uroLayers.ID.MP)
      {
         mObj = W.model.mapProblems.getObjectById(markerID);
      }
      else if(markerType === uroLayers.ID.RTC)
      {
         mObj = W.model.roadClosures.getObjectById(markerID);
      }

      if(mObj !== null)
      {
         retval = W.userscripts.getMapElementByDataModel(mObj);
      }
      else
      {
         for(let i = 0; i < uroLayers.layers[markerType].mf.length; ++i)
         {
            if(uroLayers.layers[markerType].mf[i]?.attributes?.wazeFeature?.id === markerID)
            {
               let geoID = uroLayers.layers[markerType].mf[i].geometry.id;
               retval = document.getElementById(geoID);
               break;
            }
         }
      }
   }
   return retval;
}

uroInit.Initialise();