UROverview Plus (URO+)

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

目前為 2025-01-02 提交的版本,檢視 最新版本

// ==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.11
// ==/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.11",
   date : "20250102",
   changes : 
   [
      "AM topbar list is now filterable by AM rank and the age of their area",
      "Topbar black background expands as required if AM list spans more than one line",
      "AM list no longer ignores 'complex' area geometries...",
      "Config handling tweaks"
   ]
};

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,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YzlkIj8+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;
   },
   GetUserLevelFromID: function(userID)
   {
      let userLevel = undefined;
      if(W.model.users.objects[userID] != null)
      {
         userLevel = W.model.users.objects[userID].attributes.rank + 1;
      }
      return userLevel;
   },
   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"));
   },
   GetSelectedSegmentIDs: function()
   {
      let selDMOs = W.selectionManager.getSelectedDataModelObjects();
      let retval = [];

      if(selDMOs.length > 0)
      {
         for(let i = 0; i < selDMOs.length; ++i)
         {
            if(selDMOs[i].type === "segment")
            {
               retval.push(selDMOs[i].attributes.id);
            }
         }
      }

      return retval;
   }
};
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 += 'Hide areas for AM levels:<br>'
      iHTML += '&nbsp;&nbsp;&nbsp;<input type="checkbox" id="_cbHideAMA-L1">L1</input>';
      iHTML += '&nbsp;&nbsp;&nbsp;<input type="checkbox" id="_cbHideAMA-L2">L2</input>';
      iHTML += '&nbsp;&nbsp;&nbsp;<input type="checkbox" id="_cbHideAMA-L3">L3</input>';
      iHTML += '&nbsp;&nbsp;&nbsp;<input type="checkbox" id="_cbHideAMA-L4">L4</input>';
      iHTML += '&nbsp;&nbsp;&nbsp;<input type="checkbox" id="_cbHideAMA-L5">L5</input>';
      iHTML += '&nbsp;&nbsp;&nbsp;<input type="checkbox" id="_cbHideAMA-L6">L6</input>';
      iHTML += '&nbsp;&nbsp;&nbsp;<input type="checkbox" id="_cbHideAMA-L7">L7</input><br>';
      iHTML += 'Hide areas by age:<br>'
      iHTML += '&nbsp;&nbsp;&nbsp;<input type="checkbox" id="_cbEnableAMAMinAgeFilter">Less than </input>';
      iHTML += '<input type="number" min="1" size="3" style="width:50px;line-height:14px;height:22px;margin-bottom:4px;" id="_inputFilterAMA-MinDays"> days old<br>';
      iHTML += '&nbsp;&nbsp;&nbsp;<input type="checkbox" id="_cbEnableAMAMaxAgeFilter">More than </input>';
      iHTML += '<input type="number" min="1" size="3" style="width:50px;line-height:14px;height:22px;margin-bottom:4px;" id="_inputFilterAMA-MaxDays"> days old<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
         {
            let masterEnable = uroUtils.GetCBChecked('_cbMasterEnable');
            if(masterEnable !== null)
            {
               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 = masterEnable;
               localStorage.UROverviewCurrentVersion = uroRelease.version;

               uroDBG.AddLog('settings saved');
            }
            else
            {
               uroDBG.AddLog('master enable state not known when trying to save settings...');
            }
         }
         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)
         {
            uroDBG.AddLog('master enable not found, not applying settings...');
            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];
               }
            }
            uroDBG.AddLog('settings applied...');
         }
      }
   },
   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://gf.qytechs.cn/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 !== undefined)
   {
      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('topbar')[0].style.height="auto";
   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);

      let hideL1Areas = uroUtils.GetCBChecked("_cbHideAMA-L1");
      let hideL2Areas = uroUtils.GetCBChecked("_cbHideAMA-L2");
      let hideL3Areas = uroUtils.GetCBChecked("_cbHideAMA-L3");
      let hideL4Areas = uroUtils.GetCBChecked("_cbHideAMA-L4");
      let hideL5Areas = uroUtils.GetCBChecked("_cbHideAMA-L5");
      let hideL6Areas = uroUtils.GetCBChecked("_cbHideAMA-L6");
      let hideL7Areas = uroUtils.GetCBChecked("_cbHideAMA-L7");

      let hideNewAreas = uroUtils.GetCBChecked("_cbEnableAMAMinAgeFilter");
      let areaMinAge = uroUtils.GetElmValue("_inputFilterAMA-MinDays");
      let hideOldAreas = uroUtils.GetCBChecked("_cbEnableAMAMaxAgeFilter");
      let areaMaxAge = uroUtils.GetElmValue("_inputFilterAMA-MaxDays");

      for(let amObj in W.model.managedAreas.objects)
      {
         let ama = W.model.managedAreas.objects[amObj];
         let userID = ama.attributes.userID;
         let userLevel = uroUtils.GetUserLevelFromID(userID);

         let hideArea = false;
         hideArea ||= ((hideL1Areas === true) && (userLevel === 1));
         hideArea ||= ((hideL2Areas === true) && (userLevel === 2));
         hideArea ||= ((hideL3Areas === true) && (userLevel === 3));
         hideArea ||= ((hideL4Areas === true) && (userLevel === 4));
         hideArea ||= ((hideL5Areas === true) && (userLevel === 5));
         hideArea ||= ((hideL6Areas === true) && (userLevel === 6));
         hideArea ||= ((hideL7Areas === true) && (userLevel === 7));

         if((hideNewAreas === true) || (hideOldAreas === true))
         {
            let amaDaysAgo = uroUtils.DateToDays(ama.attributes.createdOn);
            hideArea ||= ((hideNewAreas === true) && (amaDaysAgo < areaMinAge));
            hideArea ||= ((hideOldAreas === true) && (amaDaysAgo > areaMaxAge));
         }

         if(hideArea === false)
         {
            let nc = ama.attributes.geoJSONGeometry.coordinates.length;
            for(let i = 0; i < nc; ++i)
            {
               let geo = ama.attributes.geoJSONGeometry.coordinates[i];
               if(uroContainsPoint(geo, mousePoint) === true)
               {
                  let amName = uroUtils.GetUserNameFromID(ama.attributes.userID);
                  if(amList.indexOf(amName) === -1)
                  {
                     if(amList !== '') amList += ', ';
                     tName = uroUtils.GetUserNameAndRank(ama.attributes.userID);
                     if(tName.indexOf('a href') !== -1)
                     {
                        tName = tName.replace('a href', 'a style="color:#c0c0ff;" href');
                     }
                     amList += tName;
                     break;
                  }
               }
            }
         }
      }
      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 = uroUtils.GetSelectedSegmentIDs();
   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)



   let reportPanel = document.querySelector('#panel-container');

   if(reportPanel.childElementCount > 0)
   {
      let nurButton = reportPanel.getElementsByClassName('next')[0];
      if(nurButton === undefined)
      {
         nurButton = reportPanel.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 = reportPanel.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 selectedSegIDs = uroUtils.GetSelectedSegmentIDs();
               if(selectedSegIDs.length > 0)
               {
                  uroGetSelectedSegmentRTCs(selectedSegIDs[0]);

                  // 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();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址