UROverview Plus (URO+)

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

Terá de instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

Terá de instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Terá de instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Terá de instalar uma extensão como Tampermonkey para instalar este script.

Terá de instalar uma extensão de gestão de scripts de utilizador para instalar este script.

(Já tenho um gestor de scripts de utilizador, deixe-me instalá-lo!)

Terá de instalar uma extensão como Stylus para instalar este estilo.

Terá de instalar uma extensão como Stylus para instalar este estilo.

Terá de instalar uma extensão como Stylus para instalar este estilo.

Terá de instalar uma extensão de gestão de estilos de utilizador para instalar este estilo.

Terá de instalar uma extensão de gestão de estilos de utilizador para instalar este estilo.

Terá de instalar uma extensão de gestão de estilos de utilizador para instalar este estilo.

(Já tenho um gestor de estilos de utilizador, deixe-me instalá-lo!)

// ==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             5.0
// ==/UserScript==

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

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

*/

const uroRelease =
{
   version : "5.0",
   date : "20260807",
   changes : 
   [
      "SDK migration sufficiently complete to release, though note some things remain broken..."
   ]
};

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,
      APP: 4,
      PROBLEM_IMPORT: 5,
      PARTNER_EDITOR: 6
   },
   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]'];

let uroURDupes = [];
let uroFID = -1;
let uroShownFID = -1;
let uroInhibitSave = true;
let uroConfirmIntercepted = false;
let uroMCLayer = null;
let uroVenueLayer = null;
let uro_uFP = [];
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 uroHidePopupOnPanelOpen = false;
let uroUserName = null;
let uroMTEMode = false;
let uroDiv = null;
let uroAlerts = null;
let uroControls = null;
let uroCtrlHides = null;
let uroAMList = [];
let uroManagedAreas = [];
let uroIgnoreAreasUserName = undefined;
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 sdk = null;

let rtcClasses = 
{
   marker:  
   {
      prefix:     'roadClosure--',
      suffix:     'map-marker',
      active:     'statusActive--',
      finished:   'statusFinished--', 
      notStarted: 'statusNotStarted--'
   }, 
   arrow:
   {
      generic:    'arrow--',
      ab:         'forward',
      ba:         'backward',
      active:     'active--',
      finished:   'inactive--', 
      notStarted: 'inactive--'
   }
};


const uroUtils =  // utility functions
{
   userCache: [],
   escapeHTMLPolicy: null,

   FindCachedUserByID: function(id)
   {
      let idx = null;
      for(let i = 0; i < uroUtils.userCache.length; ++i)
      {
         if(uroUtils.userCache[i].id === id)
         {
            idx = i;
            break;
         }
      }
      return idx;
   },
   GetUserLevelFromName: function(userName)
   {
      let level = null;
      for(let i = 0; i < uroUtils.userCache.length; ++i)
      {
         if(uroUtils.userCache[i].userName === userName)
         {
            level = uroUtils.userCache[i].rank + 1;
            break;
         }
      }
      return level;
   },
   AddUserToCache: function(userObj)
   {
      let idx = uroUtils.FindCachedUserByID(userObj.id);
      if(idx === null)
      {
         userObj.ts = Date.now();
         uroUtils.userCache.push(userObj);
      }
      else
      {
         uroUtils.userCache[idx].ts = Date.now();
      }
   },
   CleanUserCache: function()
   {
      const MAX_AGE = 3600000;
      let tsNow = Date.now();
      let idx = 0;
      while(idx < uroUtils.userCache.length)
      {
         if((tsNow - uroUtils.userCache[idx].ts) > MAX_AGE)
         {
            uroUtils.userCache.splice(idx, 1);
         }
         else
         {
            ++idx;
         }
      }
   },
   ExtentContains: function(geo)
   {
      let retval = false;
      let extent = sdk.Map.getMapExtent();

      retval = true; //// TBC...

      return retval;
   },
   ModifyHTML: function(htmlIn)
   {
      if(typeof trustedTypes === "undefined")
      {
         return htmlIn;
      }
      else
      {
         if(this.escapeHTMLPolicy === null)
         {
            this.escapeHTMLPolicy = trustedTypes.createPolicy("forceInner", {createHTML: (to_escape) => to_escape});
         }
         return this.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(mcModData, ageType, getRaw)
   {
      let retval = {raw: null, days: null};
      if(ageType === 0)
      {
         if((mcModData.createdOn !== null)&&(mcModData.createdOn !== 0))
         {
            retval.raw = mcModData.createdOn;
            retval.days = uroUtils.DateToDays(mcModData.createdOn);
         }
      }
      else if(ageType === 1)
      {
         if((mcModData.updatedOn !== null)&&(mcModData.updatedOn !== 0))
         {
            retval.raw = mcModData.updatedOn;
            retval.days = uroUtils.DateToDays(mcModData.updatedOn);
         }
      }
      else if(ageType === 2)
      {
         if((mcModData.endDate !== null)&&(mcModData.endDate !== 0))
         {
            let tDate = new Date(mcModData.endDate);
            retval.raw = tDate;
            retval.days = uroUtils.DateToDays(tDate);
         }
      }
      else
      {
         // do nothing
      }
      return retval;
   },
   GetPURAge: function(purObj)
   {
      if(purObj.attributes.venueUpdateRequests[0].attributes.dateAdded !== null)
      {
         return uroUtils.DateToDays(purObj.attributes.venueUpdateRequests[0].attributes.dateAdded);
      }
      else
      {
         return -1;
      }
   },
   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 country = null;
   
         if(sdk.DataModel.Countries.getTopCountry() !== null)
         {
            country = sdk.DataModel.Countries.getTopCountry().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;
            }
         }
   
         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 '';
      }
   },
   GetUserNameAndRank: function(userName)
   {
      let profileLink = sdk.DataModel.Users.getUserProfileLink({userName: userName});
      let userLevel = uroUtils.GetUserLevelFromName(userName);

      let retval = '<a href="' + profileLink + '" target="_blank">' + userName + '</a>';
      if(userLevel !== null)
      {
         retval += ' (' + userLevel + ')';
      }
      return retval;
   },
   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.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.slice(-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;
   },
   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 sel = sdk.Editing.getSelection();
      let retval = [];

      if(sel !== null)
      {
         if(sel.objectType == "segment")
         {
            retval = sel.ids;
         }
      }

      return retval;
   },
   IsFilteringEnabled: function (ignoreZoom)
   {
      let retval = false;
      if
      (
         (uroUtils.GetCBChecked('_cbMasterEnable') === true) && 
         (
            (ignoreZoom === true) || 
            (sdk.Map.getZoomLevel() <= uroUtils.GetElmValue('_inputFilterMinZoomLevel'))
         )
      )
      {
         retval = true;
      }
      return retval;
   },
   GetUserID: function(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;
   },
   GetLengthString: function(length)
   {
      let retval = '';
      if(length == null)
      {
         retval = "Default";
      }
      else if(sdk.Settings.getUserSettings().isImperial == true)
      {
         retval = length + "ft";
      }
      else
      {
         retval = length + "m";
      }

      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,
      ////MISC: 7
      MISC: 6
   },
   FIELDS:
   {
      TABHEADER: 0,
      TABBODY: 1,
      LINKID: 2,
      TABTITLE: 3,
      SHOWFN: 4,
      CLICKFN: 5,
      STORAGE: 6,
      POPULATEFN: 7
   },
   CtrlTabs: [],

   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 mainCategories = sdk.DataModel.Venues.getVenueMainCategories();
      let subCategories = sdk.DataModel.Venues.getVenueSubCategories();
      let nCategories = mainCategories.length;
      let i;
      if(uroPlacesGroupsCollapsed.length != nCategories)
      {
         for(i = 0; i < nCategories; i++)
         {
            uroPlacesGroupsCollapsed.push(false);
         }
      }

      for(i = 0; i < nCategories; i++)
      {
         let parentCategory = mainCategories[i];

         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.id+'"><b>'+parentCategory.localizedName+'</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 < subCategories.length; ii++)
         {
            let subCategory = subCategories[ii];
            if(subCategory.categoryId === parentCategory.id)
            {
               tHTML += '&nbsp;&nbsp;&nbsp;&nbsp;<input type="checkbox" id="_cbPlacesFilter-'+subCategory.subCategoryId+'">'+subCategory.localizedName+'</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 += '<br><b>Filter by attribute:</b><br>';

      iHTML += '&nbsp;&nbsp;<i>Client OS</i><br>';
      iHTML += '&nbsp;&nbsp;<input type="checkbox" id="_cbFilterOS_IOS">IOS</input><br>';
      iHTML += '&nbsp;&nbsp;<input type="checkbox" id="_cbFilterOS_Android">Android</input><br>';
      iHTML += '&nbsp;&nbsp;<input type="checkbox" id="_cbFilterOS_AndroidAuto">Android Auto</input><br>';
      iHTML += '&nbsp;&nbsp;<input type="checkbox" id="_cbFilterOS_CarPlay">Car Play</input><br>';
      iHTML += '&nbsp;&nbsp;<input type="checkbox" id="_cbFilterOS_Unknown">Unknown</input><br>';

      iHTML += '&nbsp;&nbsp;<i>Client Type</i><br>';
      iHTML += '&nbsp;&nbsp;<input type="checkbox" id="_cbFilterSource_MobileClient">App</input><br>';
      iHTML += '&nbsp;&nbsp;<input type="checkbox" id="_cbFilterSource_Web">Web</input><br>';
      iHTML += '&nbsp;&nbsp;<input type="checkbox" id="_cbFilterSource_MobileWeb">Mobile Web</input><br>';
      iHTML += '&nbsp;&nbsp;<input type="checkbox" id="_cbFilterSource_Unknown">Unknown</input><br>';

      iHTML += '&nbsp;&nbsp;<i>EV</i><br>';
      iHTML += '&nbsp;&nbsp;<input type="checkbox" id="_cbFilterIsEV_True">Yes</input><br>';
      iHTML += '&nbsp;&nbsp;<input type="checkbox" id="_cbFilterIsEV_False">No</input><br>';
      iHTML += '&nbsp;&nbsp;<input type="checkbox" id="_cbFilterIsEV_Unknown">Unknown</input><br>';

      iHTML += '&nbsp;&nbsp;<i>Vehicle Type</i><br>';
      iHTML += '&nbsp;&nbsp;<input type="checkbox" id="_cbFilterVehicle_Private">Car</input><br>';
      iHTML += '&nbsp;&nbsp;<input type="checkbox" id="_cbFilterVehicle_Taxi">Taxi</input><br>';
      iHTML += '&nbsp;&nbsp;<input type="checkbox" id="_cbFilterVehicle_Motorcycle">Motorbike</input><br>';
      iHTML += '&nbsp;&nbsp;<input type="checkbox" id="_cbFilterVehicle_Unknown">Unknown</input><br>';
      
      iHTML += '<br><input type="checkbox" id="_cbInvertURAttributesFilter">Invert operation of attributes filters?</input><br>';      

      
      iHTML += '<br><b>Filter 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;
   },
   PopulateRTC: function()
   {
      let iHTML = '';

      let rtcClassActive = rtcClasses.marker.prefix + ' ' + rtcClasses.marker.active + ' ' + rtcClasses.marker.suffix;
      let rtcClassFuture = rtcClasses.marker.prefix + ' ' + rtcClasses.marker.notStarted + ' ' + rtcClasses.marker.suffix;
      let rtcClassPast = rtcClasses.marker.prefix + ' ' + rtcClasses.marker.finished + ' ' + rtcClasses.marker.suffix;

      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="' + rtcClassPast + '" style="margin-left:0px;margin-top:0px;" /></td><td><div class="' + rtcClassActive + '" style="margin-left:0px;margin-top:0px;" /></td><td><div class="' + rtcClassFuture + '" 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>From App</td><td><input type="checkbox" id="_cbHideExpiredAppRTCs" /></td><td><input type="checkbox" id="_cbHideAppRTCs" /></td><td><input type="checkbox" id="_cbHideFutureAppRTCs" /></td><td><input type="checkbox" id="_cbHideUnknownAppRTCs" /></td></tr>';
      iHTML += '<tr><td>From Problem Import</td><td><input type="checkbox" id="_cbHideExpiredProbImpRTCs" /></td><td><input type="checkbox" id="_cbHideProbImpRTCs" /></td><td><input type="checkbox" id="_cbHideFutureProbImpRTCs" /></td><td><input type="checkbox" id="_cbHideUnknownProbImpRTCs" /></td></tr>';
      iHTML += '<tr><td>From Partner Editor</td><td><input type="checkbox" id="_cbHideExpiredPartEditRTCs" /></td><td><input type="checkbox" id="_cbHidePartEditRTCs" /></td><td><input type="checkbox" id="_cbHideFuturePartEditRTCs" /></td><td><input type="checkbox" id="_cbHideUnknownPartEditRTCs" /></td></tr>';
      iHTML += '<tr><td>Unknown source</td><td><input type="checkbox" id="_cbHideExpiredUnknownRTCs" /></td><td><input type="checkbox" id="_cbHideUnknownRTCs" /></td><td><input type="checkbox" id="_cbHideFutureUnknownRTCs" /></td><td><input type="checkbox" id="_cbHideUnknownUnknownRTCs" /></td></tr>';
      iHTML += '<tr><td colspan=5 style="text-align:left;">Invert above filters: <input type="checkbox" id="_cbInvertRTCFilters" /></td></tr>';
      iHTML += '<tr><td colspan=5><hr style="margin-top:2px;margin-bottom:8px;"></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 += '<tr><td>In Popup</td><td><input type="checkbox" id="_cbHideExpiredPopupRTCs" /></td><td><input type="checkbox" id="_cbHidePopupRTCs" /></td><td><input type="checkbox" id="_cbHideFuturePopupRTCs" /></td><td><input type="checkbox" id="_cbHideUnknownPopupRTCs" /></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="_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;
   },
   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;
   },
   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();
   },
   ClickMisc: function()
   {
      uroFilterItems();
      uroUITweaks.ChangeMapBGColour();
      uroUITweaks.HideAMLayer();
      uroUITweaks.HideSegments();
      uroUITweaks.ChangeClustering();
   },
   ClickRTCs: function()
   {
      uroRTCs.Filter();
      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],
         ['_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;
   },
   GetServer: function()
   {
      let retval = null;

      let pl = sdk.Map.getPermalink();
      if(pl.indexOf('env=row' !== -1))
      {
         retval = 'row';
      }
      else if(pl.indexOf('env=il' !== -1))
      {
         retval = 'il';
      }
      else if(pl.indexOf('env=usa' !== -1))
      {
         retval = 'usa';
      }

      return retval;
   },
   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 = uroAFN.GetServer();
      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.substring(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') === null)
      {
         return;
      }
   
      if(document.getElementById('sidepanel-areas').getElementsByClassName('list-item-card-layout').length === 0)
      {
         return;
      }
   
      if(replaceAfterNameChange === false)
      {
         if(document.getElementById('sidepanel-areas').getElementsByClassName('list-item-card-layout')[0].id == "friendlyNamed")
         {
            return;
         }
      }
   
      let panelRootObj = document.getElementById('sidepanel-areas').getElementsByClassName('list-item-card')[0].parentElement;
      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 == uroAFN.GetServer()))
                  {
                     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('list-item-card-layout')[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,
   pEntryTimeout : null,
   inhibitSetCenter : false,
   clickedOnCenter : null,
   clickedOnID : null,

   EntryTimeout: function()
   {
      uroMarkers.armHover = false;
      if(uroMarkers.lastOver !== null)
      {
         if((uroMarkers.type == uroLayersFT.ID.UR) || (uroMarkers.type == uroLayersFT.ID.MP))
         {
            if(uroMarkers.type == uroLayersFT.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)
      {
         if(uroMarkers.pEntryTimeout !== null)
         {
            window.clearTimeout(uroMarkers.pEntryTimeout);
         }
         let eto = uroUtils.GetElmValue('_inputPopupEntryTimeout') * 100;
         if(uroStackType !== null)
         {
            eto = 1;
         }
         uroMarkers.pEntryTimeout = window.setTimeout(uroMarkers.EntryTimeout, eto);
      }
   },
   TranslateType: function(layer)
   {
      let retval = null;
      for(let i = 0; i < uroLayers.layers.length; ++i)
      {
         if(uroLayers.layers[i].name == layer)
         {
            retval = uroLayers.layers[i].ft;
            break;
         }
      }
      return retval;
   },
   GetObject: function(id, layer)
   {
      let retval = null;
      for(let i = 0; i < uroLayers.layers.length; ++i)
      {
         if(uroLayers.layers[i].name == layer)
         {
            let ul = uroLayers.layers[i];
            ////if(ul.l.features === undefined)
            {
               let dm = ul.dm;
               let gbi = ul.gbi;
               if((dm !== null) && (gbi !== null))
               {
                  retval = sdk.DataModel[dm].getById({[gbi]: id});
                  break;
               }
            }
            /*
            else
            {
               for(let j = 0; j < ul.l.features.length; ++j)
               {
                  if(ul.l.features[j].attributes.wazeFeature.id === id)
                  {
                     retval = document.querySelector('#' + ul.l.features[j].geometry.id);
                     break;
                  }
               }
            }
            */
         }
      }
      return retval;
   },
   MouseOver: function(e)
   {
      let obj = uroMarkers.GetObject(e.featureId, e.layerName);

      if(obj !== null)
      {
         // single marker...
         uroMarkers.elm = sdk.Map.getFeatureDomElement({featureId: e.featureId, layerName: e.layerName});
         uroMarkers.obj = obj;
         uroMarkers.id = obj.id;
         uroMarkers.type = uroMarkers.TranslateType(e.layerName);
         uroMarkers.lastOver = obj.id;
         uroMarkers.armHover = true;

         uroMarkers.AddMarkerEventHandler();
      }
   },
   RestackAfterMouseOut: function()
   {
      if(uroMarkers.lastOver === null)
      {
         uroRestackMarkers();
         uroFilterURs();
         uroFilterProblems();
         uroFilterPlaces();
      }
   },
   MouseOut: function()
   {
      uroMarkers.lastOver = null;
      if(uroMarkers.type !== null)
      {
         uroDBG.AddLog('hover off '+uroMarkers.type+' ID '+uroMarkers.id);
         uroHoveredURID = null;

         uroFID = -1;
         if(uroStackType !== null)
         {
            window.setTimeout(uroMarkers.RestackAfterMouseOut, 100);
         }

         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 = sdk.Map.getMapCenter();
   
         if(uroMarkers.inhibitSetCenter === false)
         {
            if(uroMarkers.Decentre() === true)
            {
               uroMarkers.inhibitSetCenter = true;
            }
         }
         
      }
   },
   Decentre: function()
   {
      let inhibit = false;
   
      inhibit = inhibit || ((uroMarkers.type == uroLayersFT.ID.UR) && (uroUtils.GetCBChecked("_cbInhibitURCentering")));
      inhibit = inhibit || ((uroMarkers.type == uroLayersFT.ID.MP) && (uroUtils.GetCBChecked("_cbInhibitMPCentering")));
      inhibit = inhibit || ((uroMarkers.type == uroLayersFT.ID.VPUR) && (uroUtils.GetCBChecked("_cbInhibitPURCentering")));
      inhibit = inhibit || ((uroMarkers.type == uroLayersFT.ID.VPUR) && (uroUtils.GetCBChecked("_cbInhibitPPURCentering")));
      inhibit = inhibit || ((uroMarkers.type == uroLayersFT.ID.VPUR) && (uroUtils.GetCBChecked("_cbInhibitRPURCentering")));
   
      return inhibit;
   },
   RegisterEvents: function()
   {
      let tracking = false;
      for(let i = 0; i < uroLayers.layers.length; ++i)
      {     
         if(uroLayers.layers[i].regEvt === true)
         {
            sdk.Events.trackLayerEvents({layerName: uroLayers.layers[i].name});
            tracking = true;
         }
      }
      if(tracking == true)
      {
         sdk.Events.on({eventName: "wme-layer-feature-mouse-enter", eventHandler: uroMarkers.MouseOver});
         sdk.Events.on({eventName: "wme-layer-feature-mouse-leave", eventHandler: uroMarkers.MouseOut});
      }
   },
   AddMarkerEventHandler: function()
   {
      if
      (
         (uroMarkers.type === uroLayersFT.ID.UR) ||
         (uroMarkers.type === uroLayersFT.ID.MP) ||
         (uroMarkers.type === uroLayersFT.ID.VPUR)
      )
      {
         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 uroLayersFT = // local IDs for all layers URO interacts with
{
   ID :
   {
      VPUR: -1,   // used to indicate any type of PUR marker - not directly associated with any of the above layer definitions
      UR: 0,
      MP: 1,
      PUR: 2,
      PPUR: 3,
      RPUR: 4,
      RTC: 5,
      node: 6,
      seg: 7,
      venue: 8,
      MC: 9,
      RTCnode: 10,
      TRTCnode: 11,
      SegSug: 12,
      EditSug: 13
   }
};
const uroLayers = // layer functions
{
   // -----------------------------------------------------------------
   // NOTE CAREFULLY!
   // The entries in layers and in the uroLayersFT.ID list MUST, MUST, MUST, remain
   // in sync at all times...

   // For each entry in layers, the following attributes are required:
   //
   // name = layer name as used by W.map and the SDK
   // l = set to null, populated elsewhere
   // mf = set to null, populated elsewhere
   // MO = set to null, populated elsewhere
   // regEvt = true if mouseover/leave handlers are required for this layer, false if not
   // dm = name of the SDK datamodel section for the features on this layer, or null if SDK does not provide access
   // gbi = name of the id parameter required by the getById() function, or null if sDK does not provide this
   // ft = feature type value for the features on this layer, or null if not required

   layers :
   [
      {
         name: "update_requests",
         l: null,
         mf: null,
         MO: null,
         regEvt: true,
         dm: "MapUpdateRequests",
         gbi: "mapUpdateRequestId",
         ft: uroLayersFT.ID.UR,
         getMF: true
      },
      {
         name: "mapProblems",
         l: null,
         mf: null,
         MO: null,
         regEvt: true,
         dm: "MapProblems",
         gbi: "mapProblemId",
         ft: uroLayersFT.ID.MP,
         getMF: true
      },
      {
         name: "place_updates",
         l: null,
         mf: null,
         MO: null,
         regEvt: true,
         dm: null,
         ft: uroLayersFT.ID.VPUR,
         getMF: true
      },
      {
         name: "PARKING_PLACE_UPDATES",
         l: null,
         mf: null,
         MO: null,
         regEvt: true,
         dm: null,
         gbi: null,
         ft: null,
         getMF: true
      },
      {
         name: "RESIDENTIAL_PLACE_UPDATES",
         l: null,
         mf: null,
         MO: null,
         regEvt: true,
         dm: null,
         gbi: null,
         ft: null,
         getMF: true
      },
      {
         name: "closures",
         l: null,
         mf: null,
         MO: null,
         regEvt: false,
         dm: null,
         gbi: null,
         ft: null,
         getMF: true
      },
      {
         name: "nodes",
         l: null,
         mf: null,
         MO: null,
         regEvt: true,
         dm: "Nodes",
         gbi: "nodeId",
         ft: "node",
         getMF: false
      },
      {
         name: "segments",
         l: null,
         mf: null,
         MO: null,
         regEvt: true,
         dm: "Segments",
         gbi: "segmentId",
         ft: "segment",
         getMF: false
      },
      {
         name: "venues",
         l: null,
         mf: null,
         MO: null,
         regEvt: true,
         dm: "Venues",
         gbi: "venueId",
         ft: "venue",
         getMF: false
      },
      {
         name: "mapComments",
         l: null,
         mf: null,
         MO: null,
         regEvt: true,
         dm: "MapComments",
         gbi: "mapCommentId",
         ft: "comment",
         getMF: false
      },
      {
         name: "closure_nodes",
         l: null,
         mf: null,
         MO: null,
         regEvt: false,
         dm: null,
         gbi: null,
         ft: null,
         getMF: true
      },
      {
         name: "turn_closure",
         l: null,
         mf: null,
         MO: null,
         regEvt: false,
         dm: null,
         gbi: null,
         ft: null,
         getMF: true
      },
      {
         name: "segment_suggestions_markers",
         l: null,
         mf: null,
         MO: null,
         regEvt: true,
         dm: null,
         gbi: null,
         ft: uroLayersFT.ID.SegSug,
         getMF: true
      },
      {
         name: "edit_suggestions_markers",
         l: null,
         mf: null,
         MO: null,
         regEvt: true,
         dm: "EditSuggestions",
         gbi: "editSuggestionId",
         ft: uroLayersFT.ID.EditSug,
         getMF: true
      }
   ],
   // -----------------------------------------------------------------

   LayerVisible: function(name)
   {
      let retval = null;
      try
      {
         retval = sdk.Map.isLayerVisible({layerName: name});
      }
      catch
      {
      }
      return retval;
   },
   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')
         {
            let mcObj = sdk.DataModel.MapComments.getById({mapCommentId: blobID});
            if(mcObj !== null)
            {
               let geoID = mcObj.geometry.id;
               if(mcObj.geometry.type === 'Point')
               {
                  // reapply visibility mods
                  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;
      let selected = sdk.Editing.getSelection();
      if(selected !== null)
      {
         if(selected.objectType === "mapComment")
         {
            retval = true;
         }
      }
      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++)
            {
               mcModel = uroMCLayer.features[mObj];
               {
                  if(mcModel.renderIntent !== "select")
                  {
                     let mcBlobID = mcModel.geometry.id;
                     let mcID = mcModel.attributes.wazeFeature.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[uroLayersFT.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[uroLayersFT.ID.UR].MO.observe(uroLayers.layers[uroLayersFT.ID.UR].l.renderer.vectorRoot,{childList: true, attributes : true, characterData : true, subtree: true});
   },
   URLayerChanged: function()
   {
      // Ignore any changes caused by unstacking markers...
      if(uroStackType === null)
      {
         uroDBG.AddLog('UR layer change detected');
         uroLayers.layers[uroLayersFT.ID.UR].MO.disconnect();
         uroURSessions.Refresh();
         uroLayers.Observe_URLayer();
      }
   },
   PURLayerChanged: function()
   {
      uroDBG.AddLog('PUR layer change detected');
      uroLayers.layers[uroLayersFT.ID.PUR].MO.disconnect();
      uroFilterProblems();
      uroLayers.Observe_PURLayer();
   },
   Observe_PURLayer: function()
   {
      uroLayers.layers[uroLayersFT.ID.PUR].MO.observe(uroLayers.layers[uroLayersFT.ID.PUR].l.div,{childList: true, attributes : true, characterData : true, subtree: true});
   },
   PPURLayerChanged: function()
   {
      uroDBG.AddLog('PPUR layer change detected');
      uroLayers.layers[uroLayersFT.ID.PPUR].MO.disconnect();
      uroFilterProblems();
      uroLayers.Observe_PPURLayer();
   },
   Observe_PPURLayer: function()
   {
      uroLayers.layers[uroLayersFT.ID.PPUR].MO.observe(uroLayers.layers[uroLayersFT.ID.PPUR].l.div,{childList: true, attributes : true, characterData : true, subtree: true});
   },
   RPURLayerChanged: function()
   {
      uroDBG.AddLog('RPUR layer change detected');
      uroLayers.layers[uroLayersFT.ID.RPUR].MO.disconnect();
      uroFilterProblems();
      uroLayers.Observe_RPURLayer();
   },
   Observe_RPURLayer: function()
   {
      uroLayers.layers[uroLayersFT.ID.RPUR].MO.observe(uroLayers.layers[uroLayersFT.ID.RPUR].l.div,{childList: true, attributes : true, characterData : true, subtree: true});
   },

   RTCLayerChanged: function()
   {
      uroDBG.AddLog('reapplying closures filter');
      uroLayers.layers[uroLayersFT.ID.RTC].MO.disconnect();
      uroRTCs.Filter();
      uroLayers.Observe_RTCLayer();
   },
   Observe_RTCLayer: function()
   {
      uroLayers.layers[uroLayersFT.ID.RTC].MO.observe(uroLayers.layers[uroLayersFT.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[uroLayersFT.ID.UR].MO = new MutationObserver(uroLayers.URLayerChanged);
      uroLayers.layers[uroLayersFT.ID.PUR].MO = new MutationObserver(uroLayers.PURLayerChanged);
      uroLayers.layers[uroLayersFT.ID.PPUR].MO = new MutationObserver(uroLayers.PPURLayerChanged);
      uroLayers.layers[uroLayersFT.ID.RPUR].MO = new MutationObserver(uroLayers.RPURLayerChanged);
      uroLayers.layers[uroLayersFT.ID.venue].MO = new MutationObserver(uroLayers.VenueLayerChanged);
      uroLayers.layers[uroLayersFT.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;
   },
   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.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...');
         }
      }
   },
   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;
      }
   },
   RemoveOldSettings: function()
   {
      let oldies = [
         "UROverviewFeedFilterOptions",
         "UROverviewCWLGroups",
         "UROverviewCamWatchList",
         "UROverviewPlaceWatchList",
         "UROverviewSegWatchList"
      ];

      for(let i = 0; i < oldies.length; ++ i)
      {
         if(localStorage[oldies[i]] != undefined)
         {
            localStorage.removeItem(oldies[i]);
         }
      }
   },
   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.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...');
      }

      uroConfig.RemoveOldSettings();

      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 += '[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 += '[UROverviewRAOptions][len=178]:_cbShowSpecificRA,false:_cbRAEditorIDFilter,false:_cbEnableRAAgeFilterLessThan,false:_inputFilterRAAgeLessThan,39:_cbEnableRAAgeFilterMoreThan,false:_inputFilterRAAgeMoreThan,38[END]';
      defaultSettings += '[UROverviewCurrentVersion][len=3]4.5[END]';      
      defaultSettings += '[UROverviewFriendlyAreaNames][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].substring(aPos+1, bPos);
            subText[i] = subText[i].substring(bPos+1);
            bPos = subText[i].indexOf(']');
            if(bPos != -1)
            {
               let settingLength = subText[i].substring(5, bPos);
               subText[i] = subText[i].substring(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,
   Reason : null,
   Event : null,
   Direction : null,
   StartDate : null,
   StartTime : null,
   EndDate : null,
   EndTime : null,
   IgnoreTraffic : null,
   ClosedNodes : null,
   PendingClone : -1,
   PendingCloneIncrement : 0,

   SetReact: function(elmDesc, newValue) 
   {
      let elm = document.querySelector(elmDesc);
      if(elm !== null)
      {
         // Some (all?) of the closure panel elements need to be focussed before we can successfully change
         // their values...
         elm.focus();
         // https://stackoverflow.com/questions/23892547/what-is-the-best-way-to-trigger-change-or-input-event-in-react-js#46012210
         let nativeInputValueSetter = Object.getOwnPropertyDescriptor(elm.__proto__, "value").set;
         nativeInputValueSetter.call(elm, newValue);
         let event = new Event('input', { bubbles: true });
         elm.dispatchEvent(event);         
      }
   },
   Complete: function()
   {
      let loop;
      
      if(document.getElementsByClassName('edit-closure').length === 0)
      {
         window.setTimeout(uroRTCClone.Complete,100);
         return;
      }

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

      if(uroRTCClone.Reason !== null)
      {
         uroRTCClone.SetReact('wz-text-input#closure_reason', uroRTCClone.Reason);
      }
      if(uroRTCClone.Direction !== null)
      {
         uroRTCClone.SetReact('wz-select#closure_direction', uroRTCClone.Direction);
      }
      if(uroRTCClone.StartDate !== null)
      {
         uroRTCClone.SetReact('wz-text-input#closure_startDate', uroRTCClone.StartDate);
      }
      if(uroRTCClone.StartTime !== null)
      {
         uroRTCClone.SetReact('div.form-group.start-date-form-group wz-text-input.time-picker-input', uroRTCClone.StartTime);
      }
      if(uroRTCClone.IgnoreTraffic !== null)
      {
         uroRTCClone.SetReact('wz-checkbox#closure_permanent', uroRTCClone.IgnoreTraffic);
      }
      if(uroRTCClone.EndDate !== null)
      {
         uroRTCClone.SetReact('wz-text-input#closure_endDate', uroRTCClone.EndDate);
      }
      if(uroRTCClone.EndTime !== null)
      {
         uroRTCClone.SetReact('div.form-group.end-date-form-group wz-text-input.time-picker-input', uroRTCClone.EndTime);
      }

      // 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 form = $('#edit-panel .closures .edit-closure form');
         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]);
   },
   GetDeletableClosures: function()
   {
      let dElms = [];

      let entryElms = document.querySelector('.closures-list')?.querySelectorAll('wz-card');
      if(entryElms !== undefined)
      {
         let nEntries = entryElms.length;

         for(let i = 0; i < nEntries; ++i)
         {
            let iSrc = entryElms[i].querySelector('.closure-status-chip').imageSrc;
            if((iSrc.indexOf('active') !== -1) || (iSrc.indexOf('not-started') !== -1))
            {
               dElms.push(entryElms[i]);
            }
         }
      }
      return dElms;
   },
   WaitForClosureDeletion: function()
   {
      let deleted = false;
      let statusElm = document.querySelector('.closures-list')?.querySelector('.closures-status-message');
      if(statusElm !== undefined)
      {
         let statusText = I18n.lookup('closures.success.delete');
         if(statusElm.innerText === statusText)
         {
            deleted = true;
         }
      }

      let nDCs = uroRTCClone.GetDeletableClosures().length;
      if(nDCs > 0)
      {
         if(deleted === false)
         {
            window.setTimeout(uroRTCClone.WaitForClosureDeletion, 100);
         }
         else
         {
            uroRTCClone.DeleteNextOnList();
         }
      }
      else
      {
         uroRTCClone.ConfirmDelete = true;
      }
   },
   DeleteNextOnList: function()
   {
      let dElms = uroRTCClone.GetDeletableClosures();
      let nClosures = dElms.length;

      if(nClosures > 0)
      {
         let menuText = I18n.lookup('closures.delete');
         let menuEntries = dElms[0].querySelectorAll('wz-menu-item');
         for(let i = 0; i < menuEntries.length; ++i)
         {
            if(menuEntries[i].innerText === menuText)
            {
               menuEntries[i].click();
               window.setTimeout(uroRTCClone.WaitForClosureDeletion, 100);
               break;
            }
         }
      }
      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 dElms = uroRTCClone.GetDeletableClosures();
      if(dElms.length > 0)
      {
         uroRTCClone.DeleteNextOnList();
      }
      else
      {
         uroRTCClone.ConfirmDelete = true;
      }
   }   
};
const uroRTCs = // Other RTC related stuff...
{
   rtcTotal : null,
   rtcSegIDs : null,
   prTO : null,
   mteDropDown : null,
   lhdArrows : false,
   GetDuration: function(rcObj)
   {
      //// https://issuetracker.google.com/issues/478409089
      let rcObj_notSDK = W.model.roadClosures.getObjectById(rcObj.id);
      let duration = new Date(rcObj_notSDK.attributes.endDate) - new Date(rcObj_notSDK.attributes.startDate);
      return Math.floor(duration / 86400000);
   },
   GetOffset: function(rcDate)
   {
      let dateObj = new Date(rcDate);
      return (0 - uroUtils.DateToDays(dateObj));
   },
   GetOrigin: function(rcObj)
   {
      //// https://issuetracker.google.com/issues/478409089
      let rcObj_notSDK = W.model.roadClosures.getObjectById(rcObj.id);
      if(rcObj_notSDK !== undefined)
      {
         if(rcObj_notSDK.attributes.attributions === null)
         {
            if(rcObj_notSDK.attributes.createdBy == -5)
            {
               retval = uroEnums.TRTC.WAZEFEED;
            }
            else if((W.model.users.objects[rcObj_notSDK.attributes.createdBy] !== undefined) && (W.model.users.objects[rcObj_notSDK.attributes.createdBy].attributes.rank == 6))
            {
               retval = uroEnums.TRTC.WAZEOTHER;
            }
            else if((rcObj_notSDK.attributes.provider === 'Map Editors Community') || (rcObj_notSDK.attributes.closureType === 'SEGMENT'))
            {
               retval = uroEnums.TRTC.WME;
            }
            else
            {
               retval = uroEnums.TRTC.APP;
            }
         }
         else
         {
            for(let i = 0; i < rcObj_notSDK.attributes.attributions.length; ++i)
            {
               let ch = rcObj_notSDK.attributes.attributions[i].channel;
               let uc = rcObj_notSDK.attributes.attributions[i].userChannel;
               if(ch === 'WME_COMMUNITY_EDITOR')
               {
                  retval = uroEnums.TRTC.WME;
               }
               else if((ch === 'FEED') || (uc === 'SYSTEM'))
               {
                  retval = uroEnums.TRTC.WAZEFEED;
               }
               else if(ch === 'WME_OPS_EDITOR')
               {
                  retval = uroEnums.TRTC.WAZEOTHER;
               }
               else if(ch === 'WME_ROAD_CLOSURE_PROBLEM_IMPORT')
               {
                  retval = uroEnums.TRTC.PROBLEM_IMPORT;
               }
               else if(ch === 'WME_PARTNER_EDITOR')
               {
                  retval = uroEnums.TRTC.PARTNER_EDITOR;
               }
               else
               {
                  uroDBG.Add('RTC with unknown attribution: ' + ch);
                  console.debug(rcObj_notSDK);
               }
            }
         }
      }
      return retval;
   },
   GetState: function(rcObj)
   {
      let retval = uroEnums.SRTC.UNKNOWN;
      let rcStatus = rcObj.status;

      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;
   },
   GetStateText: function(rcObj)
   {
      let retval = "---";
      let i18 = I18n.lookup("closures.statuses")[rcObj.status];
      if(i18 !== undefined)
      {
         retval = i18;
      }
      return retval;
   },
   GetForSelectedSegment: function(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...
         selection = sdk.Editing.getSelection();
         if(selection.objectType === 'segment')
         {
            selectedSegs = selection.ids;
         }
      }

      if((selectedSegs.length > 0) || (segID !== null))
      {
         let filterExpired = uroUtils.GetCBChecked('_cbHideExpiredPopupRTCs');
         let filterCurrent = uroUtils.GetCBChecked('_cbHidePopupRTCs');
         let filterFuture = uroUtils.GetCBChecked('_cbHideFuturePopupRTCs');
         let filterUnknown = uroUtils.GetCBChecked('_cbHideUnknownPopupRTCs');

         let rcObjs = sdk.DataModel.RoadClosures.getAll();
         for(let roadClosure in rcObjs)
         {
            let rcObj = rcObjs[roadClosure];
            let rcState = uroRTCs.GetState(rcObj);
            let ignore = ((rcState === uroEnums.SRTC.EXPIRED) && (filterExpired === true));
            ignore = ignore || ((rcState === uroEnums.SRTC.ACTIVE) && (filterCurrent === true));
            ignore = ignore || ((rcState === uroEnums.SRTC.FUTURE) && (filterFuture === true));
            ignore = ignore || ((rcState === uroEnums.SRTC.UNKNOWN) && (filterUnknown === true));

            if(ignore === false)
            {
               rcObj.segIDs = [rcObj.segmentId]; // 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.isForward === 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.segmentId == segID)
                  {
                     RTCObjs.push(rcObj);
                  }
               }
               else
               {
                  for(let i = 0; i < selectedSegs.length; ++i)
                  {
                     if(rcObj.segmentID == selectedSegs[i].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.startDate === b.startDate)
            {
               if(a.direction == uroEnums.DRTC.SEG_AB) return -1;
               return 1;
            }
            if(a.startDate > b.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].modificationData.createdBy == RTCObjs[i+1].modificationData.createdBy) &&
                  (RTCObjs[i].endDate == RTCObjs[i+1].endDate) &&
                  (RTCObjs[i].trafficEventId == RTCObjs[i+1].trafficEventId) &&
                  (RTCObjs[i].description == RTCObjs[i+1].description) &&
                  (RTCObjs[i].startDate == RTCObjs[i+1].startDate) &&
                  (RTCObjs[i].direction == RTCObjs[i+1].direction)
               )
               {
                  RTCObjs[i].segIDs.push(RTCObjs[i+1].segmentId);
                  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].modificationData.createdBy == RTCObjs[i+1].modificationData.createdBy) &&
                  (RTCObjs[i].endDate == RTCObjs[i+1].endDate) &&
                  (RTCObjs[i].trafficEventId == RTCObjs[i+1].trafficEventId) &&
                  (RTCObjs[i].description == RTCObjs[i+1].description) &&
                  (RTCObjs[i].startDate == RTCObjs[i+1].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...
         let turnClosures = sdk.DataModel.TurnClosures.getAll();
         for(let tc in turnClosures)
         {
            let tcObj = turnClosures[tc];
            
            if(tcObj.fromSegmentId === segID)
            {
               tcObj.direction = uroEnums.DRTC.TURN_OUT;
               RTTCObjs.push(tcObj);
            }
            else if(tcObj.toSegmentId === segID)
            {
               tcObj.direction = uroEnums.DRTC.TURN_IN;
               RTTCObjs.push(tcObj);
            }
         }

         if(RTTCObjs.length > 1)
         {
            RTTCObjs = RTTCObjs.sort(function(a,b)
            {
               if(a.startDate === b.startDate)
               {
                  if(a.direction == uroEnums.DRTC.TURN_OUT) return -1;
                  return 1;
               }
               if(a.startDate > b.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;
   },
   ScrollToEndOfClosures: function()
   {
      let cItems = document.querySelectorAll('.closure-item');
      let nClosures = cItems.length;
      let segIDs = uroUtils.GetSelectedSegmentIDs();
      let sameSegs = false;
      if((segIDs !== null) && (uroRTCs.rtcSegIDs !== null))
      {
         if(segIDs.length == uroRTCs.rtcSegIDs.length)
         {
            sameSegs = true;
            for(let i = 0; i < segIDs.length; ++i)
            {
               if(segIDs[i] != uroRTCs.rtcSegIDs[i])
               {
                  sameSegs = false;
                  break;
               }
            }
         }
      }
      if(sameSegs === false)
      {
         uroRTCs.rtcTotal = null;
         uroRTCs.rtcSegIDs = segIDs;
      }
      if(nClosures > uroRTCs.rtcTotal)
      {
         if(cItems[0].getBoundingClientRect().height != 0)
         {
            uroRTCs.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();
            }
         }
      }
   },
   ClosureEditUIChanged: function()
   {
      let mteDropDown = document.getElementById('closure_eventId');
      if(mteDropDown !== null)
      {
         // 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;
         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
            {
               uroRTCs.FixMTEDropDown(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();
      }
   },
   PanelRendered: function()
   {
      uroRTCs.prTO = null;
      uroRTCs.CompleteMTEDropDownFix();
   },
   FixMTEDropDown: function(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...

      // As the closure UI is rendered in such a way as to generate a myriad of change events, we can't simply use any one
      // of those events as a way to indicate when the UI actually has finished rendering and it's therefore safe to start
      // altering the MTE dropdown.  In the absence of any other way to figure out when this can be done, we therefore use a
      // simple delay-based approach...
      //
      // First store the reference to the MTE dropdown
      uroRTCs.mteDropDown = mteDropDown;
      // Cancel our previous timeout if it's still running - this means another change event has occurred on the closure panel
      // within 250ms of the last one, so we don't want to let the timeout handler complete as we now know the panel is still
      // in a state of flux...
      if(uroRTCs.prTO !== null)
      {
         window.clearTimeout(uroRTCs.prTO);
      }
      // Set up a new 250ms timeout - this will only expire once all of the change events have taken place on the panel, and
      // can therefore be used to call the one-time handling required to fix the MTE dropdown
      uroRTCs.prTO = window.setTimeout(uroRTCs.PanelRendered, 250);
   },
   CompleteMTEDropDownFix: function()
   {  
      // Make sure the closure event list is available, and that we haven't already messed with it.
      if((uroRTCs.mteDropDown !== null) && (uroRTCs.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 = uroRTCs.mteDropDown.shadowRoot.querySelectorAll('.selected-value')[0];
         if(shadowElm !== undefined)
         {
            // 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...
            let eventText = uroRTCs.mteDropDown.shadowRoot.querySelectorAll('.selected-value')[0].innerText;
            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.
                  let returnFocusTo = document.activeElement;
                  uroRTCs.mteDropDown.children[0].click();
                  returnFocusTo.focus();
               }
               // Tag the event list to prevent further processing attempts whilst the edit UI for this closure remains open.
               uroRTCs.mteDropDown.tag = "touchedByURO";

               let closureReason = document.querySelector('#closure_reason');
               if(closureReason !== undefined)
               {
                  closureReason.click();
               }
            }
         }
      }
   },
   MarkerInfo: function(rtcID, isVisible)
   {
      let rtcModel = sdk.DataModel.RoadClosures.getById({roadClosureId: rtcID});
      let mObj = sdk.Map.getFeatureDomElement({featureId: rtcID, layerName: "closures"});

      this.isVisible = isVisible;
      this.status = rtcModel.status;
      this.segmentId = rtcModel.segmentId;
      this.isAB = (rtcModel.isForward === true);
      this.isBA = (rtcModel.isForward === false);

      this.fwdAngle = null;
      this.revAngle = null;
      this.pos = {left: null, top: null};
      if(mObj !== null)
      {
         // Default all RTC markers to hidden - any we want to keep visible will then be set
         // as such once we've applied the latest filtering settings
         mObj.style.visibility = "hidden";
         let tStr = mObj.firstChild?.style?.transform;
         // Check to see if the LHD arrow fix has already been applied by FUME, so we can
         // avoid applying the -180 angle correction again here which would then undo the fix
         // when we come to render our own markers...
         let lhdFixApplied = (tStr.indexOf("scaleX(-1)") !== -1);
         let angle = parseFloat(tStr.split('(')[1].split('deg')[0]);
         if(lhdFixApplied === true)
         {
            angle -= 180.0;
         }
         if(rtcModel.isForward === true)
         {
            this.fwdAngle = angle % 360;
            this.revAngle = (angle - 180.0) % 360;
         }
         else
         {
            this.revAngle = angle % 360;
            this.fwdAngle = (angle - 180.0) % 360;
         }
         this.pos.left = mObj.style.left;
         this.pos.top = mObj.style.top;        
      }
   },
   CheckAndAddRTCObj: function(mI, rO)
   {
      // First see if the segment this RTC object relates to is already listed in markerInfo
      let idx;
      let sameSeg = false;
      for(idx = 0; idx < mI.length; ++idx)
      {
         if(mI[idx].segmentId === rO.segmentId)
         {
            sameSeg = true;
            break;
         }
      }

      if(sameSeg === true)
      {
         // Segment already listed, so now we need to see if the closure described by this
         // RTC object ought to replace the one already listed.  The intention here is that
         // by the time we've checked all the RTCs, markerInfo will give us a clear
         // indication as to whether or not we need to render ANY RTC marker for the segment
         // and, if so, what type it should be and which directional arrows it requires

         // If this object contains position or arrow rotation info, update our segment
         // entry accordingly so we can make use of it later
         if(rO.pos.left !== null)
         {
            mI[idx].pos = rO.pos;
         }
         if(rO.fwdAngle !== null)
         {
            mI[idx].fwdAngle = rO.fwdAngle;
         }
         if(rO.revAngle !== null)
         {
            mI[idx].revAngle = rO.revAngle;
         }

         // If the current RTC object is for an unfiltered closure of a higher priority
         // than the one already in markerInfo, update the stored priority
         if(rO.isVisible === true)
         {           
            newStatus = null;
            // Active always takes priority over everything else
            if(rO.status === "ACTIVE")
            {
               newStatus = "ACTIVE";
            }
            // Future closures are second priority
            else if((rO.status === "NOT_STARTED") && (mI[idx].status !== "ACTIVE"))
            {
               newStatus = "NOT_STARTED";
            }
            // Ended closures are at the bottom of the pile, so use only if we haven't
            // already identified a higher priority RTC to show for this segment...
            else if((rO.status !== null) && (mI[idx].status !== "ACTIVE") && (mI[idx].status !== "NOT_STARTED"))
            {
               newStatus = rO.status;
            }

            // If this object is the same or higher priority as the one already in markerInfo,
            // update the priority of the latter, and also adjust the directionality tracking.
            // If we're changing the marker priority, we need to first dump the existing
            // tracking info so we don't end up showing the wrong directionality for the higher
            // priority marker we are showing, whereas if the priority is the same then we OR
            // the existing info with the new, to account for the way WME uses two seperate
            // marker objects for bidirectional closures
            if(newStatus !== null)
            {
               if(mI[idx].status !== newStatus)
               {
                  mI[idx].isAB = false;
                  mI[idx].isBA = false;
               }
               mI[idx].status = newStatus;
               mI[idx].isAB ||= rO.isAB;
               mI[idx].isBA ||= rO.isBA;
            }
         }

         // If any of the RTCs in a stack are visible, we need to keep the RTC marker
         // visible, so logically-ORing the individual RTC visibility flags means we'd only
         // hide the marker if we've filtered all of the RTCs for that segment
         mI[idx].isVisible ||= rO.isVisible;
      }
      else
      {
         // Segment isn't already in markerInfo, so provided this is a visible RTC object, then
         // we can simply use it to initialise the entry for the segment.  If it's not visible,
         // we still need to use it to initialise the entry, but we first need to make a few
         // tweaks to prevent it affecting how the visible objects are handled
         if(rO.isVisible === false)
         {
            rO.status = null;
         }
         mI.push(rO);            
      }

      return mI;
   },
   GenerateArrowDiv: function(isFwd, mI)
   {
      let aElm = document.createElement('div');
      aElm.className = rtcClasses.arrow.generic + " ";
      if(isFwd === true)
      {
         aElm.className += rtcClasses.arrow.ab + " ";
      }
      else
      {
         aElm.className += rtcClasses.arrow.ba + " ";
      }
      if(mI.status === "ACTIVE")
      {
         aElm.className += rtcClasses.arrow.active;
      }
      else if(mI.status === "NOT_STARTED")
      {
         aElm.className += rtcClasses.arrow.notStarted;
      }
      else
      {
         aElm.className += rtcClasses.arrow.finished;
      }
      aElm.touchedByURO = true;
      aElm.className += " uroRTCMarkers";
      let transform = "rotate(";
      let angle = null;
      if(isFwd === true)
      {
         angle = mI.fwdAngle;
      }
      else
      {
         angle = mI.revAngle;
      }
      if(uroRTCs.lhdArrows === true)
      {
         angle += 180.0;
      }
      transform += angle + "deg)";
      if(uroRTCs.lhdArrows === true)
      {
         transform += " scaleX(-1)";
      }
      aElm.style.transform = transform;

      return aElm;
   },
   Filter: function()
   {
      let pmTStart = performance.now();
      let pmFunction = "uroRTCs.Filter";

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

      if(uroLayers.LayerVisible("closures") !== true) return;

      let rtcFilters = 
      [
         [false, false, false, false],   // UNKNOWN
         [false, false, false, false],   // WME
         [false, false, false, false],   // WAZEFEED
         [false, false, false, false],   // WAZEOTHER
         [false, false, false, false],   // APP
         [false, false, false, false],   // PROBLEM_IMPORT
         [false, false, false, false]    // PARTNER_EDITOR
      ];

      rtcFilters[uroEnums.TRTC.UNKNOWN][uroEnums.SRTC.UNKNOWN] = uroUtils.GetCBChecked('_cbHideUnknownUnknownRTCs');
      rtcFilters[uroEnums.TRTC.UNKNOWN][uroEnums.SRTC.EXPIRED] = uroUtils.GetCBChecked('_cbHideExpiredUnknownRTCs');
      rtcFilters[uroEnums.TRTC.UNKNOWN][uroEnums.SRTC.ACTIVE] = uroUtils.GetCBChecked('_cbHideUnknownRTCs');
      rtcFilters[uroEnums.TRTC.UNKNOWN][uroEnums.SRTC.FUTURE] = uroUtils.GetCBChecked('_cbHideFutureUnknownRTCs');
      rtcFilters[uroEnums.TRTC.WME][uroEnums.SRTC.UNKNOWN] = uroUtils.GetCBChecked('_cbHideUnknownEditorRTCs');
      rtcFilters[uroEnums.TRTC.WME][uroEnums.SRTC.EXPIRED] = uroUtils.GetCBChecked('_cbHideExpiredEditorRTCs');
      rtcFilters[uroEnums.TRTC.WME][uroEnums.SRTC.ACTIVE] = uroUtils.GetCBChecked('_cbHideEditorRTCs');
      rtcFilters[uroEnums.TRTC.WME][uroEnums.SRTC.FUTURE] = uroUtils.GetCBChecked('_cbHideFutureEditorRTCs');
      rtcFilters[uroEnums.TRTC.WAZEFEED][uroEnums.SRTC.UNKNOWN] = uroUtils.GetCBChecked('_cbHideUnknownWazeFeedRTCs');
      rtcFilters[uroEnums.TRTC.WAZEFEED][uroEnums.SRTC.EXPIRED] = uroUtils.GetCBChecked('_cbHideExpiredWazeFeedRTCs');
      rtcFilters[uroEnums.TRTC.WAZEFEED][uroEnums.SRTC.ACTIVE] = uroUtils.GetCBChecked('_cbHideWazeFeedRTCs');
      rtcFilters[uroEnums.TRTC.WAZEFEED][uroEnums.SRTC.FUTURE] = uroUtils.GetCBChecked('_cbHideFutureWazeFeedRTCs');
      rtcFilters[uroEnums.TRTC.WAZEOTHER][uroEnums.SRTC.UNKNOWN] = uroUtils.GetCBChecked('_cbHideUnknownWazeRTCs');
      rtcFilters[uroEnums.TRTC.WAZEOTHER][uroEnums.SRTC.EXPIRED] = uroUtils.GetCBChecked('_cbHideExpiredWazeRTCs');
      rtcFilters[uroEnums.TRTC.WAZEOTHER][uroEnums.SRTC.ACTIVE] = uroUtils.GetCBChecked('_cbHideWazeRTCs');
      rtcFilters[uroEnums.TRTC.WAZEOTHER][uroEnums.SRTC.FUTURE] = uroUtils.GetCBChecked('_cbHideFutureWazeRTCs');
      rtcFilters[uroEnums.TRTC.APP][uroEnums.SRTC.UNKNOWN] = uroUtils.GetCBChecked('_cbHideUnknownAppRTCs');
      rtcFilters[uroEnums.TRTC.APP][uroEnums.SRTC.EXPIRED] = uroUtils.GetCBChecked('_cbHideExpiredAppRTCs');
      rtcFilters[uroEnums.TRTC.APP][uroEnums.SRTC.ACTIVE] = uroUtils.GetCBChecked('_cbHideAppRTCs');
      rtcFilters[uroEnums.TRTC.APP][uroEnums.SRTC.FUTURE] = uroUtils.GetCBChecked('_cbHideFutureAppRTCs');
      rtcFilters[uroEnums.TRTC.PROBLEM_IMPORT][uroEnums.SRTC.UNKNOWN] = uroUtils.GetCBChecked('_cbHideUnknownProbImpRTCs');
      rtcFilters[uroEnums.TRTC.PROBLEM_IMPORT][uroEnums.SRTC.EXPIRED] = uroUtils.GetCBChecked('_cbHideExpiredProbImpRTCs');
      rtcFilters[uroEnums.TRTC.PROBLEM_IMPORT][uroEnums.SRTC.ACTIVE] = uroUtils.GetCBChecked('_cbHideProbImpRTCs');
      rtcFilters[uroEnums.TRTC.PROBLEM_IMPORT][uroEnums.SRTC.FUTURE] = uroUtils.GetCBChecked('_cbHideFutureProbImpRTCs');
      rtcFilters[uroEnums.TRTC.PARTNER_EDITOR][uroEnums.SRTC.UNKNOWN] = uroUtils.GetCBChecked('_cbHideUnknownPartEditRTCs');
      rtcFilters[uroEnums.TRTC.PARTNER_EDITOR][uroEnums.SRTC.EXPIRED] = uroUtils.GetCBChecked('_cbHideExpiredPartEditRTCs');
      rtcFilters[uroEnums.TRTC.PARTNER_EDITOR][uroEnums.SRTC.ACTIVE] = uroUtils.GetCBChecked('_cbHidePartEditRTCs');
      rtcFilters[uroEnums.TRTC.PARTNER_EDITOR][uroEnums.SRTC.FUTURE] = uroUtils.GetCBChecked('_cbHideFuturePartEditRTCs');
      let uFR_invertFilters = uroUtils.GetCBChecked('_cbInvertRTCFilters');

      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 = uroUtils.IsFilteringEnabled(false);
      let markerInfo = [];
      let markerDiv = null;

      // Build a list of all closures which, based on the filtering settings, need to 
      // be shown
      let rtcObjs = sdk.DataModel.RoadClosures.getAll();
      for (let r in rtcObjs)
      {
         let isVisible = true;
         let rtcModel = rtcObjs[r];

         if(uFR_masterEnable === true)
         {   
            // First we delete any markers we'd generated on a previous filtering pass so we're starting
            // with a clean slate
            let myMarkers = document.querySelectorAll('.uroRTCMarkers');
            for(let i = 0; i < myMarkers.length; ++i)
            {
               myMarkers[i].remove();
            }

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

            let rtcType = uroRTCs.GetOrigin(rtcModel);
            let rtcState = uroRTCs.GetState(rtcModel);
            if(rtcFilters[rtcType][rtcState] === true)
            {
               isVisible = false;
            }
            if(uFR_invertFilters === true)
            {
               isVisible = !isVisible;
            }
            let rtcDuration = uroRTCs.GetDuration(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.startDate).getTime();
               let endTS = new Date(rtcModel.endDate).getTime();
               if(uFR_filterShowForTS === true)
               {
                  if((filterTS < startTS) || (filterTS > endTS))
                  {
                     isVisible = false;
                  }
               }
               if (uFR_filterHideForTS === true)
               {
                  if((filterTS >= startTS) && (filterTS <= endTS))
                  {
                     isVisible = false;
                  }            
               }
            }
         }

         let rtcObj = new uroRTCs.MarkerInfo(rtcModel.id, isVisible);
         if((rtcObj.mObj !== null) && (markerDiv === null))
         {
            let mObj = sdk.Map.getFeatureDomElement({featureId: rtcModel.id, layerName: "closures"});
            if(mObj !== null)
            {
               markerDiv = mObj.parentElement;
            }
         }
         markerInfo = uroRTCs.CheckAndAddRTCObj(markerInfo, rtcObj);
      }
   
      ////console.debug(rtcObjs);   
      ////console.debug(markerInfo);

      // markerInfo[] now contains all the information we need to be able to render the correct
      // RTC marker and directionality arrows for all still-visible RTCs.  As we've already hidden
      // all of the native markers whilst building the markerInfo list, and as we can't rely on WME
      // having rendered the correct directionality arrows for the marker we want to show on a
      // segment, we need to generate our own markers...
      if(markerDiv !== null)
      {
         let tc = sdk.DataModel.Countries.getTopCountry();
         if(tc !== null)
         {
            uroRTCs.lhdArrows = tc.isLeftHandTraffic;
         }

         for(let i = 0; i < markerInfo.length; ++i)
         {
            if(markerInfo[i].isVisible === true)
            {
               let mElm = document.createElement('div');
               mElm.className = rtcClasses.marker.prefix + " ";
               if(markerInfo[i].status === "ACTIVE")
               {
                  mElm.className += rtcClasses.marker.active;
               }
               else if(markerInfo[i].status === "NOT_STARTED")
               {
                  mElm.className += rtcClasses.marker.notStarted;
               }
               else
               {
                  mElm.className += rtcClasses.marker.finished;
               }
               mElm.className += " " + rtcClasses.marker.suffix;
               mElm.className += " uroRTCMarkers";
               mElm.style.left = markerInfo[i].pos.left;
               mElm.style.top = markerInfo[i].pos.top;
               mElm.style.position = "absolute";
               mElm.touchedByURO = true;

               if(markerInfo[i].isAB === true)
               {
                  mElm.appendChild(uroRTCs.GenerateArrowDiv(true, markerInfo[i]));
               }
               if(markerInfo[i].isBA === true)
               {
                  mElm.appendChild(uroRTCs.GenerateArrowDiv(false, markerInfo[i]));
               }
               markerDiv.appendChild(mElm);
            }
         }
      }

      uroDBG.PerfMon(pmFunction, pmTStart);
   }
};
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,
   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 = '';

      // check for stacking...
      if(uroShownFID != uroMarkers.id)
      {
         uroCheckStacking(uroLayersFT.ID.UR,uroMarkers.id);
      }

      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>';
               }
            }

            let sdObj = uroURSessions.GetSessionData(uroMarkers.id);
            if(sdObj != null)
            {
               let hasMyComments = uroURHasMyComments(uroMarkers.id);
               let nComments = sdObj.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(sdObj.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 = '';

      // check for stacking...
      if(uroShownFID != uroMarkers.id)
      {
         uroCheckStacking(uroLayersFT.ID.MP,uroMarkers.id);
      }

      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()
   {
      let result = '';

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

      if(uroUtils.GetCBChecked('_cbInhibitPUPopup') === false)
      {
         let ureq = sdk.DataModel.Venues.getById({venueId: 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 == uroLayersFT.ID.VPUR)
         {
            uroDBG.AddLog('building popup for placeUpdate '+uroMarkers.id);
         }

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

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

         if(ureq.isResidential === 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.ExtentContains(uroMarkers.obj.geometry) === true)
               {
                  if(uroMousedOverMapComment !== null)
                  {
                     uroDBG.AddLog('setting uroMousedOverOtherObjectWithinMapComment for place highlight');
                     uroMousedOverOtherObjectWithinMapComment = true;
                  }

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

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

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

                     result += '<hr>';
                     let venueAddr = sdk.DataModel.Venues.getAddress({venueId: uroFID});
                     result += uroGetAddress(venueAddr.street.id, venueAddr.houseNumber, false, false);
                     result += '<ul>';
                     for(let idx = 0; idx < uroMarkers.obj.categories.length; idx++)
                     {
                        result += '<li>' + I18n.lookup("venues.categories." + uroMarkers.obj.categories[idx]);
                     }
                     result += '</ul>';

                     let npLink = document.location.href;
                     let npLayers = '';
                     npLink = npLink.substring(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;

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

                     uroPopup.result += result;
                     uroPopup.Show();
                  }
                  else
                  {
                     let otherID = uroMarkers.obj.id;
                     uroDBG.AddLog('venue '+otherID+' is also highlighted');
                  }
               }
               else
               {
                  uroDBG.AddLog('landmark '+uroFID+' has renderIntent==highlight but is offscreen... blocking popup');
               }
            }
         }
      }
   },
   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;
               if(uroUtils.ExtentContains(uroMarkers.obj.geometry) === true)
               {
                  if(uroPopup.newPopupType === null)
                  {
                     if((uroMousedOverMapComment === moAttrs.id) && (uroMousedOverOtherObjectWithinMapComment === true))
                     {
                        uroDBG.AddLog('inhibit popup for map comment '+uroMousedOverMapComment);
                     }
                     else
                     {
                        uroMousedOverOtherObjectWithinMapComment = false;
                        if(moAttrs.isPoint === true)
                        {
                           // 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 mcAgeData = uroUtils.GetMCAge(moAttrs.modificationData, 0);
                        if(mcAgeData.raw != null)
                        {
                           mcAgeData.raw = uroUtils.GetDateTimeString(mcAgeData.raw);
                        }
                        if(mcAgeData.days != null)
                        {
                           result += '<i>Submitted ' + uroUtils.ParseDaysAgo(mcAgeData.days) + ' ';
                           if(mcAgeData.raw != null) result += '(' + mcAgeData.raw + ') ';
                           if(moAttrs.modificationData.createdBy != null)
                           {
                              result += ' by '+uroUtils.GetUserNameAndRank(moAttrs.modificationData.createdBy);
                           }
                           result += '</i><br>';
                        }
                        mcAgeData = uroUtils.GetMCAge(moAttrs.modificationData, 1);
                        if(mcAgeData.raw != null)
                        {
                           mcAgeData.raw = uroUtils.GetDateTimeString(mcAgeData.raw);
                        }
                        if(mcAgeData.days != null)
                        {
                           result += '<i>Updated ' + uroUtils.ParseDaysAgo(mcAgeData.days) + ' ';
                           if(mcAgeData.raw != null) result += '(' + mcAgeData.raw + ') ';
                           if(moAttrs.modificationData.createdBy != null)
                           {
                              result += ' by '+uroUtils.GetUserNameAndRank(moAttrs.modificationData.updatedBy);
                           }
                           result += '</i><br>';
                        }

                        mcAgeData = uroUtils.GetMCAge(moAttrs, 2);
                        if(mcAgeData.days != null)
                        {
                           result += '<i>Expires ' + uroUtils.ParseDaysToGo(mcAgeData.days) + ' ';
                           result += '(' + uroUtils.GetDateTimeString(mcAgeData.raw) +')</i><br>';
                        }

                        let mcHasMyComments = false;
                        let mcNComments = moAttrs.conversation.length;
                        if(mcNComments > 0)
                        {
                           for(let j=0; j<mcNComments; j++)
                           {
                              if(moAttrs.conversation[j].userName == uroUserName)
                              {
                                 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.startDate;
      let endDate = "unknown";
      if(rcObj.endDate !== null)
      {
         endDate = rcObj.endDate;
      }
      let provider = "---";

      //// https://issuetracker.google.com/issues/473096295
      let rcObj_notSDK = W.model.roadClosures.getObjectById(rcObj.id);
      if(rcObj_notSDK.attributes.provider !== null)
      {
         provider = rcObj_notSDK.attributes.provider;
      }
      else if(rcObj.modificationData.createdBy !== null)
      {
         provider = uroUtils.GetUserNameAndRank(rcObj.modificationData.createdBy);
      }
      let reason = "---";
      if(rcObj.description !== null)
      {
         reason = rcObj.description;
      }
      let mte = "---";
      
      //// https://issuetracker.google.com/issues/540020379
      let mteID = null;
      if(rcObj.majorTrafficEventId !== undefined)
      {
         mteID = rcObj.majorTrafficEventId;
      }
      else
      {
         mteID = rcObj.trafficEventId;
      }

      if(mteID !== null)
      {
         let mteObj = sdk.DataModel.MajorTrafficEvents.getById({majorTrafficEventId: mteID});
         if(mteObj !== null)
         {
            mte = mteObj.names[0].value;
         }
      }
   
      let startOffset = uroRTCs.GetOffset(rcObj.startDate);
      let duration = uroRTCs.GetDuration(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 = uroRTCs.GetState(rcObj);
      let status = uroRTCs.GetStateText(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 borderStyle = 'border:1px solid #888;';
      let tdTag = '<td nowrap style="' + borderStyle + '">';      
      let result = '';
      result += '<tr bgcolor="' + bgCol + '">';
      result += tdTag + status + '</td>';
      result += tdTag + startDate + ' to ' + endDate + ' (' + durationStr + ')</td>';
      result += tdTag + provider + '</td>';
      result += tdTag + reason + '</td>';
      result += tdTag + 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()
   {
      let borderStyle = 'border:1px solid #888;';
      if(uroUtils.GetCBChecked('_cbInhibitSegPopup') === false)
      {
         {
            if(uroUtils.ExtentContains(uroMarkers.obj.geometry) === true)
            {
               let result = '';
               let doPopUp = false;
               let restObj;

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

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

                     let isToll = sdk.DataModel.Segments.isTollSegment({segmentId: segID});
                     let streetID = uroMarkers.obj.primaryStreetId;
                     result += uroGetAddress(streetID, null, true, isToll);

                     if(uroMarkers.obj.alternateStreetIds.length > 0)
                     {
                        // list any alternate names
                        result += '<br>Alternate names:<br>';
                        for(let i = 0; i < uroMarkers.obj.alternateStreetIds.length; ++i)
                        {
                           let streetObj = sdk.DataModel.Streets.getById({streetId: uroMarkers.obj.alternateStreetIds[i]});
                           if(streetObj !== null)
                           {
                              result += '&nbsp;<i>' + streetObj.name + ', ';
                              let cityName = "";
                              let cityObj = sdk.DataModel.Cities.getById({cityId: streetObj.cityId});
                              if(cityObj !== null)
                              {
                                 cityName = cityObj.name;
                              }
                              if(cityName != "")
                              {
                                 result += cityName;
                              }
                              else
                              {
                                 result += ' no city';
                              }
                              result += '</i><br>';
                           }
                        }
                        result += '<br>';
                     }

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

                     let level = uroMarkers.obj.elevationLevel;
                     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.modificationData.updatedBy;
                     let leOn = uroMarkers.obj.modificationData.updatedOn;
                     if(leOn == null)
                     {
                        leBy = uroMarkers.obj.modificationData.createdBy;
                        leOn = uroMarkers.obj.modificationData.createdOn;
                     }
                     result += "<b>Last edit by</b> " + uroUtils.GetUserNameAndRank(leBy) + " <b>on</b> " + uroUtils.GetDateTimeString(leOn) + "<br><br>";

                     let fwdSpeed = uroMarkers.obj.fwdSpeedLimit;
                     let revSpeed = uroMarkers.obj.revSpeedLimit;
                     let fwdLanes = 'Not set';
                     let revLanes = 'Not set';
                     let fwdWidth = 'Not set';
                     let revWidth = 'Not set';
                     if(uroMarkers.obj.fromLanesInfo != null)
                     {                     
                        fwdLanes = uroMarkers.obj.fromLanesInfo.numberOfLanes;
                        fwdWidth = uroUtils.GetLengthString(uroMarkers.obj.fromLanesInfo.laneWidth);
                     }
                     if(uroMarkers.obj.toLanesInfo != null)
                     {
                        revLanes = uroMarkers.obj.toLanesInfo.numberOfLanes;
                        revWidth = uroUtils.GetLengthString(uroMarkers.obj.toLanesInfo.laneWidth);
                     }
                     let fwdUnverified = uroMarkers.obj.isFwdSpeedLimitUnverified;
                     let revUnverified = uroMarkers.obj.isRevSpeedLimitUnverified;
                     let fwdASC = (uroMarkers.obj.flagAttributes.fwdSpeedCamera === true);
                     let revASC = (uroMarkers.obj.flagAttributes.revSpeedCamera === true);

                     let thTag = '<th style="' + borderStyle + '">';
                     let tdTag = '<td style="' + borderStyle + '">';
                     result += '<table border=1>';
                     result += '<tr>';
                     result += thTag + 'Dir</th>';
                     result += thTag + 'Speed</th>';
                     result += thTag + 'ASC</th>';
                     result += thTag + 'Lanes</th>';
                     result += thTag + 'Width</th>';
                     result += '</tr>';
                     if(uroMarkers.obj.isAtoB || uroMarkers.obj.isTwoWay)
                     {
                        result += '<tr>' + tdTag + '<b>A-B</b></td>';
                        result += tdTag + uroUtils.GetLocalisedSpeedString(fwdSpeed) + '</td>';
                        result += tdTag;
                        if(fwdASC == true)
                        {
                           result += 'Yes';
                        }
                        else
                        {
                           result += 'No';
                        }
                        result += '</td>';
                        result += tdTag;
                        if(fwdLanes > 0)
                        {
                           result += fwdLanes;
                        }
                        else
                        {
                           result += 'Not set';
                        }
                        result += '</td>';
                        result += tdTag + fwdWidth + '</td></tr>';
                     }
                     if(uroMarkers.obj.isBtoA || uroMarkers.obj.isTwoWay)
                     {
                        result += '<tr>' + tdTag + '<b>B-A</b></td>';
                        result += tdTag + uroUtils.GetLocalisedSpeedString(revSpeed) + '</td>';
                        result += tdTag;
                        if(revASC == true)
                        {
                           result += 'Yes';
                        }
                        else
                        {
                           result += 'No';
                        }
                        result += '</td>';
                        result += tdTag;
                        if(revLanes > 0)
                        {
                           result += revLanes;
                        }
                        else
                        {
                           result += 'Not set';
                        }
                        result += '</td>';
                        result += tdTag + revWidth + '</td></tr>';
                     }
                     result += '</table>';

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

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

                     let nABRestrictions = 0;
                     let nBARestrictions = 0;
                     let nBothRestrictions = 0;

                     //// https://issuetracker.google.com/issues/448433097
                     let tRestrictions = W.model.segments.objects[segID].attributes.restrictions;
                     for(let idx = 0; idx < tRestrictions.length; idx++)
                     {
                        restObj = tRestrictions[idx];
                        if(restObj._direction === "FWD")
                        {
                           nABRestrictions++;
                           fwdResult += uroFormatRestriction(restObj, borderStyle);
                        }
                        else if(restObj._direction === "REV")
                        {
                           nBARestrictions++;
                           revResult += uroFormatRestriction(restObj, borderStyle);
                        }
                        else if(restObj._direction === "BOTH")
                        {
                           nBothRestrictions++;
                           bothResult += uroFormatRestriction(restObj, borderStyle);
                        }
                        else
                        {
                           uroDBG.AddLog("unknown restriction direction...");
                        }
                     }
                     if(nABRestrictions > 0)
                     {
                        result += fwdResult;
                     }
                     if(nBARestrictions > 0)
                     {
                        result += revResult;
                     }
                     if(nBothRestrictions > 0)
                     {
                        result += bothResult;
                     }
                     result += '</table>';
                  }
                  
                  if(sdk.Map.isLayerVisible({layerName: uroLayers.layers[uroLayersFT.ID.RTC].name}) === true)
                  {
                     let closureTypes = uroRTCs.GetForSelectedSegment(uroMarkers.obj.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.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 nodeObj = sdk.DataModel.Nodes.getById({nodeId: uroMarkers.id});
         if(nodeObj === null)
         {
            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 < nodeObj.connectedSegmentIds.length; k++)
            {
               let nodeSegID = nodeObj.connectedSegmentIds[k];
               let nodeSegObj = sdk.DataModel.Segments.getById({segmentId: nodeSegID});
               if(nodeSegObj !== null)
               {
                  let nodeStreetID = nodeSegObj.primaryStreetId;
                  result += uroGetAddress(nodeStreetID, null, false, false);
               }
               uroRTCs.GetForSelectedSegment(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.substring(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) urLink += '&venueUpdateRequest='+uroMarkers.id;

      let targetTab = "_uroTab_" + Math.round(Math.random()*1000000);
      uroPopup.result += '<hr>Open in: <a href="'+urLink+'" id="_openInNewTab" target="'+targetTab+'">new tab</a>';
      uroPopup.hasOpenInNewTabLink = 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.substring(0, zpos);
         lmLink += '?zoom=17&lat='+urPos.lat+'&lon='+urPos.lon+'&layers=BTTTT';
         uroPopup.result += ' | <a href="'+lmLink+'" target="_lmTab">new livemap tab</a>';
      }
      if(!uroPopup.isPlaceUpdate)
      {
         // add "ignore for this session" link
         uroPopup.result += '<br><a href="#" id="_addtoignore">Hide for this session</a>';
         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.LayerVisible(uroLayers.layers[uroLayersFT.ID.UR].name) === null)
         {
            if(uroNullURLayer === false)
            {
               uroDBG.AddLog('caught null UR layer');
               uroNullURLayer = true;
            }
            retval = false;
         }
         else
         {
            uroNullURLayer = false;
         }
      }

      if(retval === true)
      {
         if(uroLayers.LayerVisible(uroLayers.layers[uroLayersFT.ID.MP].name) === 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.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);   //// TBC

         uroInit.WazeBits();

         let mouseLonLat = sdk.Map.getLonLatFromPixel({x: uroPopup.mouseX, y: uroPopup.mouseY});
         let mousePoint = new OpenLayers.Geometry.Point(mouseLonLat.lon, mouseLonLat.lat);
         if(uroMousedOverMapComment !== null)
         {
            let mcObj = uroMapComments.GetMCFeatureByID(uroMousedOverMapComment);
            if(mcObj === null)
            {
               uroDBG.AddLog('clearing uroMousedOverMapComment: object no longer exists in current map view');
               uroMousedOverMapComment = null;
            }
            else if(mcObj.renderIntent === "default")
            {
               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.hasOpenInNewTabLink === true)
            {
               uroUtils.AddEventListener('_openInNewTab','mouseup', uroOpenNewTab, 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 === uroLayersFT.ID.UR) uroPopup.UR();
         else if(uroMarkers.type === uroLayersFT.ID.MP) uroPopup.MP();
         else if(uroMarkers.type === uroLayersFT.ID.VPUR) uroPopup.PUR();
         else if(uroMarkers.type === "venue") uroPopup.Venue();
         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 unhandled 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));
   },
   RemoveCommentCounts: function()
   {
      let toRemove = document.querySelectorAll('#uroCommentCount');
      let trCount = toRemove.length;
      while(trCount > 0)
      {
         --trCount;
         toRemove[trCount].remove();
      }
   },
   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...
      uroURExtras.RemoveCommentCounts();
   
      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(uroLayersFT.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,

   GetClassName: function(rText, nameFragment)
   {
      let retval = null;

      let fragPos = rText.indexOf('.' + nameFragment);
      if(fragPos !== -1)
      {
         endPos = rText.indexOf('{', fragPos);
         if(endPos !== -1)
         {
            retval = rText.substring(fragPos + 1, endPos);
         }
      }

      return retval;
   },
   GetRTCStyles: async function()
   {
      // CORS prevents us simply reading the CSS info directly from the document, so we bypass this
      // by finding any of the CSS-related URLs in <head> which suggest they'll contain the styles
      // relating to RTC markers, then fetching them directly to extract the style names...
      let headLinks = document.head.getElementsByTagName("link");
      for(let i = 0; i < headLinks.length; ++i)
      {
         let href = headLinks[i].href
         if(href.indexOf("css/app") !== -1)
         {
            let response = await window.fetch(href);
            if(response.status === 200)
            {
               let rText = await response.text();
               if(rText.indexOf('.'+rtcClasses.marker.prefix) !== -1)
               {              
                  rtcClasses.marker.prefix = (uroInit.GetClassName(rText, rtcClasses.marker.prefix));
                  rtcClasses.marker.active = (uroInit.GetClassName(rText, rtcClasses.marker.active));
                  rtcClasses.marker.finished = (uroInit.GetClassName(rText, rtcClasses.marker.finished));
                  rtcClasses.marker.notStarted = (uroInit.GetClassName(rText, rtcClasses.marker.notStarted));
                  rtcClasses.arrow.generic = (uroInit.GetClassName(rText, rtcClasses.arrow.generic));
                  rtcClasses.arrow.active = (uroInit.GetClassName(rText, rtcClasses.arrow.active));
                  rtcClasses.arrow.finished = (uroInit.GetClassName(rText, rtcClasses.arrow.finished));
                  rtcClasses.arrow.notStarted = (uroInit.GetClassName(rText, rtcClasses.arrow.notStarted));
                  break;
               }
            }
         }
      }

      // 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();
      }      
   },
   WazeBitsAvailable: function()
   {
      uroDBG.AddLog('All WazeBits present and correct...');

      W.prefs.on('change:isImperial', uroInit.Initialise);
   
      uroInit.GetRTCStyles();
      uroLayers.Init();
   },
   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
      const _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);
         }
      };
      
      // add interceptor function for window.fetch(), so we can workaround the present lack of SDK support for
      // finding the rank of other users, by building up our own local cache of user details as they come in
      // from the server each time WME itself asks for them...
      const _fetch = window.fetch;
      window.fetch = async (...args) => 
      {
         let [resource, config ] = args;
         const response = await _fetch(resource, config);

         // Intercept any response which might include user details
         if(response.url !== undefined)
         {
            if(response.url.indexOf('Features') != -1)
            {
               response
               .clone()
               .json()
               .then(body => uroInit.ParseFeaturesResponse(body));
            }
         }

         // This is a transparent interception, so all responses are passed back to WME without modification
         return response;
      };

      uroConfirmIntercepted = true;
   },
   ParseFeaturesResponse: function(body)
   {
      // Add any user details to our cache if they're not already present...
      if(body?.users?.objects !== undefined)
      {
         for(let i  = 0; i < body.users.objects.length; ++i)
         {
            uroUtils.AddUserToCache(body.users.objects[i]);
         }
         uroUtils.CleanUserCache();
      }
   },
   Initialise: function()
   {
      uroInit.initialised = false;
      uroInit.setupListeners = true;
      uroInit.finalisingListenerSetup = false;

      if(typeof getWmeSdk !== 'function')
      {
         window.setTimeout(uroInit.Initialise, 100);
      }
      else
      {
         sdk = getWmeSdk({scriptId:"uro+",scriptName:"URO+"});
         sdk.Events.once({eventName: 'wme-ready'}).then(uroInit.WazeBitsAvailable);
      }
   },
   WaitForControlsContainer: function()
   {
      if(document.getElementById('uroControlsContainer') === null)
      {
         window.setTimeout(uroInit.WaitForControlsContainer,500);
      }
      else
      {
         let updateURL = 'https://greasyfork.org/scripts/1952-uroverview-plus-uro';
   
         uroDBG.AddLog('adding controls to sidebar container...');
         let tabbyHTML = '<div id="uroTabHeader"><b><a href="'+updateURL+'" target="_blank">UROverview Plus</a></b> <label id="_uroVersion">'+uroRelease.version+'</label>';
         tabbyHTML += '<label id="_uroDebugMode">(dbg)</label>';
         tabbyHTML += '&nbsp;<input type="checkbox" id="_cbMasterEnable" checked>Enabled</input>';
         tabbyHTML += uroTabs.CreateTabHeaders();
         tabbyHTML += '</div>';
         document.getElementById('uroControlsContainer').innerHTML = uroUtils.ModifyHTML(tabbyHTML);
   
         uroTabs.CreateTabBodies();
   
         // other sidebar elements
         uroAMList = document.createElement('div');
         uroAMList.style.color = "#ffff00";
         uroCtrlHides = document.createElement('div');
   
         // 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.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;
      let layers = W.map.getLayers();

      for(let i=0; i < layers.length; i++)
      {
         if(layers[i].name == 'mapComments') uroMCLayer = layers[i];
         if(layers[i].name == 'venues') uroVenueLayer = 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.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('_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('_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 < sdk.DataModel.Venues.getVenueMainCategories().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 name...');
      uroUserName = sdk.State.getUserInfo();
      if(uroUserName !== null)
      {
         uroUserName = uroUserName.userName;
         uroDBG.AddLog('...name is '+uroUserName);
      }
      else
      {
         uroDBG.AddLog('...user is not logged in yet :-/');
      }
      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(uroLayersFT.ID.UR, uroUtils.GetCBChecked('_cbInhibitURClusters'));
      uroUITweaks.SetClusteringFor(uroLayersFT.ID.MP, uroUtils.GetCBChecked('_cbInhibitMPClusters'));
      uroUITweaks.SetClusteringFor(uroLayersFT.ID.PUR, uroUtils.GetCBChecked('_cbInhibitPUClusters'));
      uroUITweaks.SetClusteringFor(uroLayersFT.ID.PPUR, uroUtils.GetCBChecked('_cbInhibitPUClusters'));
      uroUITweaks.SetClusteringFor(uroLayersFT.ID.RPUR, uroUtils.GetCBChecked('_cbInhibitPUClusters'));
      uroUITweaks.SetClusteringFor(uroLayersFT.ID.SegSug, uroUtils.GetCBChecked('_cbInhibitESClusters'));      
      uroUITweaks.SetClusteringFor(uroLayersFT.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(sdk.Map.getZoomLevel() < 16)
      {
         sdk.Map.zoomIn();
         sdk.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;
            sdk.Map.setMapCenter()(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)
      {
         uroRTCs.FixMTEDropDown(mteDropDown);
      }
   },
   CheckForClosurePanel: function()
   {
      let cl = document.querySelector('.closures-list');
      let ct = document.querySelector('wz-tab.closures-tab');
      if((cl !== null) && (ct !== null))
      {
         // Closure panel active
         let uroMO_ClosureUI = new MutationObserver(uroRTCs.ClosureEditUIChanged);
         uroMO_ClosureUI.disconnect();
         uroMO_ClosureUI.observe(ct ,{subtree: true, attributes: true});
         uroRTCs.ClosureEditUIChanged();
         let uroRO_ClosureUI = new ResizeObserver(uroRTCs.ScrollToEndOfClosures);
         uroRO_ClosureUI.disconnect();
         uroRO_ClosureUI.observe(cl);
         uroRTCs.ScrollToEndOfClosures();
      }
   },
   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 = sdk.Map.getMapCenter();
      let z = sdk.Map.getZoomLevel();
      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();
         uroFilterURs();
         uroFilterRAs();
         uroFilterMapComments();

         uroMiscUITweaksHandler();
      
         uroLayers.MCLayerChanged();
   
         this.lat = mc.lat;
         this.lon = mc.lon;
         this.zoom = z;
      }
   }
};
const uroURSessions =   // handlers for UR session data retrieval
{
   pendingIDs : [],
   fetchTotal : 0,
   inProgress : false,
   sessionData : [],

   FindSessionData : function(urID)
   {
      return this.sessionData.findIndex(({id}) => id === urID);
   },
   GetSessionData : function(urID)
   {
      let retval = null;
      let idx = uroURSessions.FindSessionData(urID);
      
      if(idx != -1)
      {
         retval = this.sessionData[idx];
      }

      return retval;
   },
   GetSessionDataLength : function()
   {
      return this.sessionData.length;
   },
   Refresh : function()
   {
      if(this.inProgress === false)
      {
         this.pendingIDs = [];
         this.fetchTotal = 0;

         // Start by clearing the keep flags for any sessionData entries already cached, so we know which ones
         // to discard...
         for (let i in this.sessionData)
         {
            this.sessionData[i].keep = false;
         }

         // Now, for each UR currently loaded, see if we already have a corresponding sessionData entry, set the
         // sessionData keep flag if we do, or  add its ID to the sessionData fetch queue if we don't
         for (let urID in W.model.mapUpdateRequests.objects)
         {
            if(W.model.mapUpdateRequests.objects.hasOwnProperty(urID))
            {
               urID = parseInt(urID);
               let sdIdx = uroURSessions.FindSessionData(urID);
               if(sdIdx == -1)
               {
                  this.pendingIDs.push(urID);
                  ++this.fetchTotal;
               }
               else
               {
                  this.sessionData[sdIdx].keep = true;
               }
            }
         }

         // Next, delete any sessionData entries for URs no longer present
         let sdIdx = this.sessionData.findIndex(({keep}) => keep === false);
         while(sdIdx !== -1)
         {
            this.sessionData.splice(sdIdx, 1);
            sdIdx = this.sessionData.findIndex(({keep}) => keep === false);
         }

         if(this.fetchTotal > 0)
         {
            // If the fetch queue isn't empty, set the lockout flag and start requesting
            // details from the server...
            this.inProgress = true;
            uroDBG.AddLog('grabbing session data for ' + this.fetchTotal + ' URs, please wait...');
            document.querySelector('#WazeMap').style.cursor = "progress";
            this.NextRequest();
         }
         else
         {
            // Once the fetch queue is empty, apply filtering
            uroDBG.AddLog('Completed UR details fetch');
            document.querySelector('#WazeMap').style.cursor = "";
            uroFilterURs();
         }
      }
   },
   NextRequest : function()
   {
      if(uroURSessions.pendingIDs.length > 0)
      {
         let nextID = uroURSessions.pendingIDs.shift();
         sdk.DataModel.MapUpdateRequests.getUpdateRequestDetails({mapUpdateRequestId: nextID}).then(uroURSessions.SessionDataHandler).finally(uroURSessions.PromiseHandler).catch(uroURSessions.PromiseHandler);
      }
      else
      {
         uroURSessions.PromiseHandler();
      }
   },
   SessionDataHandler : function(sd)
   {
      sd.keep = true;
      uroURSessions.sessionData.push(sd);
   },
   PromiseHandler : function()
   {
      --uroURSessions.fetchTotal;
      if(uroURSessions.fetchTotal > 0)
      {
         uroURSessions.NextRequest();
      }
      else
      {
         // Once we've finished receiving all the ones in the current queue, clear the lockout flag so
         // we can go around again to check for any further URs requiring a details fetch...
         uroURSessions.inProgress = false;
         uroURSessions.Refresh();
      }
   }
};
const uroMapComments =  // map comment/note stuff
{
   GetMCFeatureByID : function (mcID)
   {
      let retval = null;
      for(let i = 0; i < uroMCLayer.features.length; ++i)
      {
         if(uroMCLayer.features[i].attributes.wazeFeature.id === mcID)
         {
            retval = uroMCLayer.features[i];
            break;
         }
      }
      return retval;
   }
};
// ================================================================================================
// Here be the unfactored wilderness...
// ================================================================================================

function uroGetAddress(streetID, houseNumber, formatForSegmentPopup, showAsToll)
{
   let result = '';
   if((houseNumber !== undefined) && (houseNumber !== null))
   {
      result += houseNumber + ' ';
   }

   if((streetID === null) || (streetID === undefined))
   {
      result += '<b>Segment name not set...</b>';
   }
   else
   {
      let streetName = I18n.lookup('edit.address.no_street');
      let doesStreetIDExist = true;
      let streetObj = sdk.DataModel.Streets.getById({streetId: streetID});
      if(streetObj === null)
      {
         streetName = 'non-existent streetID';
         doesStreetIDExist = false;
      }
      else
      {
         if((streetName !== null) && (streetObj.isEmpty === false))
         {
            streetName = streetObj.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 = sdk.DataModel.Streets.getById({streetId: streetID}).cityId;
         let cityObj = sdk.DataModel.Cities.getById({cityId: cityID});
         if(cityObj === null)
         {
            cityName = 'non-existent cityID';
            doesCityIDExist = false;
         }
         else
         {
            if(cityObj.name !== "")
            {
               cityName = cityObj.name;
            }
         }
         result += cityName + ', ';

         if(doesCityIDExist === true)
         {
            let stateID = cityObj.stateId;
            let stateObj = sdk.DataModel.States.getById({stateId: stateID});
            if(stateObj === null)
            {
               result += 'non-existent stateID';
            }
            else
            {
               result += stateObj.name;
            }
         }
      }
   }
   result += '<br>';

   return result;
}
function uroURHasMyComments(fid)
{
   if(uroUserName === null)
   {
      return false;
   }

   let sdObj = uroURSessions.GetSessionData(fid);
   if(sdObj !== null)
   {
      let nComments = sdObj.comments.length;
      if(nComments === 0)
      {
         return false;
      }

      for(let cidx = 0; cidx < nComments; cidx++)
      {
         if(sdObj.comments[cidx].userName == uroUserName)
         {
            return true;
         }
      }
   }

   return false;
}
function uroUpdateMTEList()
{
   let mtes = sdk.DataModel.MajorTrafficEvents.getAll();
   if(mtes.length === 0) return;

   let selectedIdx = null;
   let idx;
   let mteNames = [];
   let mteIDs = [];
   for(idx in mtes)
   {
      let name = mtes[idx].names[0]?.value;
      if(mteNames.indexOf(name) == -1)
      {
         mteNames.push(name);
         mteIDs.push(mtes[idx].id);
      }
   }
   // 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 uroUpdateRAList()
{
   let rdas = sdk.DataModel.RestrictedDrivingAreas.getAll();
   if(rdas.length === 0) return;

   let selectedIdx = null;
   let idx;
   let raNames = [];
   
   for(idx in rdas)
   {
      let name = rdas[idx].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 = selector.selectedOptions[0].value;
   }

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

   let selectedIdx = null;
   let listedUsers = [];
   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((listedUsers.indexOf(userID) == -1) && (userID != -1))
                  {
                     listedUsers.push(userID);                     
                  }
               }
            }
         }
         else
         {
            obj = modelObj[idx].modificationData;
            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) && (listedUsers.indexOf(cbID) == -1))
            {
               listedUsers.push(cbID);
            }
            if((ubID !== null) && (listedUsers.indexOf(ubID) == -1))
            {
               listedUsers.push(ubID);
            }
            if((rbID !== null) && (listedUsers.indexOf(rbID) == -1))
            {
               listedUsers.push(rbID);
            }
         }
      }
   }

   selector.options.add(new Option('<select a user>', null));
   if(listedUsers.length > 0)
   {
      let selectorEntry = '';
      for(idx=0; idx<listedUsers.length; idx++)
      {
         /*
         if(listedUsers.indexOf(users[idx].id) != -1)
         {
            listedUsers.splice(listedUsers.indexOf(users[idx]), 1);
         }
         */
         

         selectorEntry = listedUsers[idx];
         selector.options.add(new Option(selectorEntry, selectorEntry));
         if(selectorEntry == selectedUser)
         {
            selectedIdx = idx+1;
         }
      }
   }

   if(selectedIdx !== null)
   {
      selector.selectedIndex = selectedIdx;
   }
}
function uroFilterRAs()
{
   let pmTStart = performance.now();
   let pmFunction = "uroFilterRAs";

   if(uroFilterPreamble() === false) return;
   let uFURs_masterEnable = uroUtils.IsFilteringEnabled(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 RAs = sdk.DataModel.RestrictedDrivingAreas.getAll();
   let filterName = null;
   if(filterByLastEditor == true)
   {
      uroUpdateEditorList(RAs, '_selectRAEditorID', true, true, false, false);
      let selector = document.getElementById('_selectRAEditorID');
      if(selector.selectedIndex > 0)
      {
         filterName = document.getElementById('_selectRAEditorID').selectedOptions[0].value;
      }
   }
   
   for (let raIdx = 0; raIdx < RAs.length; raIdx++)
   {
      let raStyle = 'visible';
      if(uFURs_masterEnable === true)
      {
         if(shownRA !== null)
         {
            if(RAs[raIdx].name != shownRA) raStyle = 'hidden';
         }
         
         if(filterName !== null)
         {
            if((RAs[raIdx].modificationData.createdBy != filterName) && (RAs[raIdx].modificationData.updatedBy != filterName))
            {
               raStyle = 'hidden';
            }
         }
      
         let raAge = uroUtils.DateToDays(RAs[raIdx].modificationData.updatedOn);
         if(filterByMinAge == true)
         {
            if(raAge < thresholdMinAge) raStyle = 'hidden';
         }
         if(filterByMaxAge == true)
         {
            if(raAge > thresholdMaxAge) raStyle = 'hidden';
         }
      }
      
      let raID = RAs[raIdx].id;
      let raFeatures = W.map.restrictedDrivingAreaLayer.features;
      for (let fIdx = 0; fIdx < raFeatures.length; ++fIdx)
      {
         if(raFeatures[fIdx].attributes.wazeFeature.id === raID)
         {
            let geoID = raFeatures[fIdx].geometry.id;
            if(document.getElementById(geoID) !== null)
            {
               document.getElementById(geoID).style.visibility = raStyle;
            }
         }
      }
   }      
   uroDBG.PerfMon(pmFunction, pmTStart);
}
function uroFilterPURMarker(markerType, pu, placeStyle)
{
   let vObj = sdk.DataModel.Venues.getById({venueId: pu});
   let mObj = uroGetMarker(markerType, pu);
   mObj.style.visibility = placeStyle;

   let urEntries = vObj.venueUpdateRequests;
   if(urEntries !== undefined)
   {
      if (urEntries.length > 1)
      {
         // 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;
         }
      }
   }
}
function uroFilterPlaceMarker(markerType, pu, uFP_masterEnable)
{
   let vObj = sdk.DataModel.Venues.getById({venueId: pu});

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

   if(uFP_masterEnable === true)
   {
      if(uro_uFP[uroEnums.FP_OPTS.filterInsideManagedAreas] === true)
      {
         let tPt = [];
         tPt.push(vObj.geometry.coordinates[0]);
         tPt.push(vObj.geometry.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';
      }
   }

   uroFilterPURMarker(markerType, pu, 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 gs = sdk.Editing.getSelection();
   if(gs !== null)
   {
      if(gs.objectType == "venue")
      {
         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 = uroUtils.GetUserID(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 = uroUtils.GetUserID(filterHideNameID, tbHideUserName);

   let filterCats = [];
   let mainCategories = sdk.DataModel.Venues.getVenueMainCategories();
   let subCategories = sdk.DataModel.Venues.getVenueSubCategories();
   for(let i = 0; i < mainCategories.length; i++)
   {
      let parentCategory = mainCategories[i];
      let subCategory;

      if(uroUtils.GetCBChecked('_cbPlacesFilter-'+parentCategory.id) === true)
      {
         filterCats.push(parentCategory.id);
         for(let i1=0; i1 < subCategories.length; i1++)
         {
            subCategory = subCategories[i1];
            if(subCategory.categoryId === parentCategory.id)
            {
               filterCats.push(subCategory.subCategoryId);
            }
         }
      }
      else
      {
         for(let i2 = 0; i2 < subCategories.length; i2++)
         {
            subCategory = subCategories[i2];
            if(subCategory.categoryId === parentCategory.id)
            {
               if(uroUtils.GetCBChecked('_cbPlacesFilter-'+subCategory.subCategoryId) === true)
               {
                  filterCats.push(subCategory.subCategoryId);
               }
            }
         }
      }
   }

   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 = uroUtils.IsFilteringEnabled(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 lmFeature = uroVenueLayer.features[v];
         let lmObj = sdk.DataModel.Venues.getById({venueId: lmFeature.attributes.wazeFeature.id});

         // 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 !== null)
         {
            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.type === "Polygon")
               {
                  placeStyle = 'hidden';
               }
            }
            if(uFP_filterPointPlaces)
            {
               if(lmObj.geometry.type === "Point")
               {
                  placeStyle = 'hidden';
               }
            }


            if(placeStyle == 'visible')
            {
               if((uFP_filterEditedLessThan) || (uFP_filterEditedMoreThan))
               {
                  let editDate = lmObj.modificationData.updatedOn;
                  if(editDate === undefined)
                  {
                     // where a place has never been edited since its creation, use the creation date instead...
                     editDate = lmObj.modificationData.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.isAdLocked) 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].isApproved) 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.isResidential === 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.modificationData.createdBy) placeStyle = 'hidden';
                  }
                  if(uFP_filterEditedBy === true)
                  {
                     if(filterNameID != lmObj.modificationData.updatedBy) placeStyle = 'hidden';
                  }
               }
            }
            if(placeStyle == 'visible')
            {
               if(filterHideNameID != null)
               {
                  if(uFP_filterHideCreatedBy === true)
                  {
                     if(filterHideNameID == lmObj.modificationData.createdBy) placeStyle = 'hidden';
                  }
                  if(uFP_filterHideEditedBy === true)
                  {
                     if(filterHideNameID == lmObj.modificationData.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] = sdk.State.isLoggedIn();
   uro_uFP[uroEnums.FP_OPTS.userRank] = sdk.State.getUserInfo().rank;

   uro_uFP[uroEnums.FP_OPTS.filterInsideManagedAreas] = uro_uFP[uroEnums.FP_OPTS.filterInsideManagedAreas] && (uroGetManagedAreas() !== 0);
   if(uroUtils.GetCBChecked('_cbPURExcludeUserArea') == true)
   {
      uroIgnoreAreasUserName = sdk.State.getUserInfo()?.userName;
   }

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

   uroDBG.PerfMon(pmFunction, pmTStart);
}
function uroPrepForFilterPlaceMarker(markerType, masterEnable)
{
   if(uroLayers.LayerVisible(uroLayers.layers[markerType].name) === true)
   {
      let idList = uroGetMarkerIDs(markerType);
      for(let pu of idList)
      {
         let mObj = uroGetMarker(markerType, pu);
         if(mObj !== null)
         {
            uroFilterPlaceMarker(markerType, pu, masterEnable);
         }
      }
   }
}
function uroFilterMapComments()
{
   let pmTStart = performance.now();
   let pmFunction = "uroFilterMapComments";

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

   let uFURs_masterEnable = uroUtils.IsFilteringEnabled(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(sdk.DataModel.MapComments.getAll(), '_selectMCCreatorID', true, false, false, false);
      }
      if(selectorCreator.selectedOptions[0] != null)
      {
         creatorUser = selectorCreator.selectedOptions[0].value;
      }
   }

   let MCs = sdk.DataModel.MapComments.getAll();
   for (let mcIdx = 0; mcIdx < MCs.length; mcIdx++)
   {
      {
         let desc = '';
         if(MCs[mcIdx].subject !== null) desc += MCs[mcIdx].subject.replace(/<\/?[^>]+(>|$)/g, "");
         if(MCs[mcIdx].body !== null) desc += MCs[mcIdx].body.replace(/<\/?[^>]+(>|$)/g, "");
         let nComments = MCs[mcIdx].conversation.length;
         if(nComments > 0)
         {
            for(let cIdx=0; cIdx < nComments; cIdx++)
            {
               desc += MCs[mcIdx].conversation[cIdx].text.replace(/<\/?[^>]+(>|$)/g, "");
            }
         }

         let mcStyle = 'visible';
         let mcID = MCs[mcIdx].id;
         if(uroIgnore.IsOnList(mcID)) 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 = MCs[mcIdx].lockRank;
            let expiry = MCs[mcIdx].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(MCs[mcIdx].isFollowing === true)
               {
                  if(filterMyFollowed === true) mcStyle = 'hidden';
               }
               else
               {
                  if(filterMyUnfollowed === true) mcStyle = 'hidden';
               }
            }

            if(mcStyle == 'visible')
            {
               if(creatorUser !== null)
               {
                  if(MCs[mcIdx].modificationData.createdBy != creatorUser) mcStyle = 'hidden';
               }
               
               if(filterWRCMC === true)
               {
                  if(MCs[mcIdx].modificationData.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 mcFeatureObj = uroMapComments.GetMCFeatureByID(mcID);
         if(mcFeatureObj !== null)
         {
            let geoID = mcFeatureObj.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 = [];
   uroIgnoreAreasUserName = undefined;

   let managedAreas = sdk.DataModel.ManagedAreas.getAll();
   for(let maObj in managedAreas)
   {
      uroManagedAreas.push(managedAreas[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(uroIgnoreAreasUserName !== undefined)
   {
      for(let uma = 0; uma < uroManagedAreas.length; ++uma)
      {
         if(uroIgnoreAreasUserName == uroManagedAreas[uma].userName)
         {
            ignoreUserMA = uroContainsPoint(uroManagedAreas[uma].geometry.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(uroIgnoreAreasUserName != uroManagedAreas[ma].userName)
         {
            retval = uroContainsPoint(uroManagedAreas[ma].geometry.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 sdObj = uroURSessions.GetSessionData(ureqID);
         if((sdObj !== null) && (sdObj.driveGeometry !== null))
         {
            let cPairs = [];
            for(let i = 0; i < sdObj.driveGeometry.coordinates.length; ++i)
            {
               for(let j = 0; j < sdObj.driveGeometry.coordinates[i].length; ++j)
               {
                  if((i === 0) || (j > 0))
                  {
                     let coords = sdObj.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(uroUserName === null) 
   {
      return;
   }

   // compatibility fix for URComments - based on code supplied by RickZabel
   let hasActiveURFilters = false;
   if(uroUtils.IsFilteringEnabled(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;
   
   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(uroURSessions.GetSessionDataLength() === 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(uroURSessions.sessionData, '_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 = uroUtils.IsFilteringEnabled(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 invertAttributesFilters = uroUtils.GetCBChecked('_cbInvertURAttributesFilter');
   let filterOS_IOS = uroUtils.GetCBChecked('_cbFilterOS_IOS');
   let filterOS_Android = uroUtils.GetCBChecked('_cbFilterOS_Android');
   let filterOS_AndroidAuto = uroUtils.GetCBChecked('_cbFilterOS_AndroidAuto');
   let filterOS_CarPlay = uroUtils.GetCBChecked('_cbFilterOS_CarPlay');
   let filterOS_Unknown = uroUtils.GetCBChecked('_cbFilterOS_Unknown');
   let filterSource_MobileClient = uroUtils.GetCBChecked('_cbFilterSource_MobileClient');
   let filterSource_Web = uroUtils.GetCBChecked('_cbFilterSource_Web');
   let filterSource_MobileWeb = uroUtils.GetCBChecked('_cbFilterSource_MobileWeb');
   let filterSource_Unknown = uroUtils.GetCBChecked('_cbFilterSource_Unknown');
   let filterIsEV_True = uroUtils.GetCBChecked('_cbFilterIsEV_True');
   let filterIsEV_False = uroUtils.GetCBChecked('_cbFilterIsEV_False');
   let filterIsEV_Unknown = uroUtils.GetCBChecked('_cbFilterIsEV_Unknown');
   let filterVehicle_Private = uroUtils.GetCBChecked('_cbFilterVehicle_Private');
   let filterVehicle_Taxi = uroUtils.GetCBChecked('_cbFilterVehicle_Taxi');
   let filterVehicle_Motorcycle = uroUtils.GetCBChecked('_cbFilterVehicle_Motorcycle');
   let filterVehicle_Unknown = uroUtils.GetCBChecked('_cbFilterVehicle_Unknown');

   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)
   {
      uroIgnoreAreasUserName = sdk.State.getUserInfo()?.userName;
   }

   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, uroLayersFT.ID.UR, desc);
         let ageLastComment = null;

         let sdObj = uroURSessions.GetSessionData(ureqID);
         if(sdObj !== null)
         {
            nComments = sdObj.comments.length;
            if(nComments != 0)
            {
               ageLastComment = uroUtils.GetCommentAge(sdObj.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';
            }

            // attribute filtering
            let hideOnAttributes = false;
            let attrVal = ureq.attributes.os;
            hideOnAttributes ||= ((filterOS_IOS === true) && (attrVal === 'IOS'));
            hideOnAttributes ||= ((filterOS_Android === true) && (attrVal === 'ANDROID_MOBILE'));
            hideOnAttributes ||= ((filterOS_CarPlay === true) && (attrVal === 'CARPLAY'));
            hideOnAttributes ||= ((filterOS_AndroidAuto === true) && (attrVal === 'ANDROID_AUTO'));
            hideOnAttributes ||= ((filterOS_Unknown === true) && ((attrVal === null) || (attrVal === undefined)));
            attrVal = ureq.attributes.source;
            hideOnAttributes ||= ((filterSource_MobileClient === true) && (attrVal === 'MOBILE_CLIENT'));
            hideOnAttributes ||= ((filterSource_Web === true) && (attrVal === 'WEB'));
            hideOnAttributes ||= ((filterSource_MobileWeb === true) && (attrVal === 'MOBILE_WEB'));
            hideOnAttributes ||= ((filterSource_Unknown === true) && ((attrVal === null) || (attrVal === undefined)));
            attrVal = ureq.attributes.hasEv;
            hideOnAttributes ||= ((filterIsEV_True === true) && (attrVal === true));
            hideOnAttributes ||= ((filterIsEV_False === true) && (attrVal === false));
            hideOnAttributes ||= ((filterIsEV_Unknown === true) && ((attrVal === null) || (attrVal === undefined)));
            attrVal = ureq.attributes.vehicleType;
            hideOnAttributes ||= ((filterVehicle_Private === true) && (attrVal === 'PRIVATE'));
            hideOnAttributes ||= ((filterVehicle_Taxi === true) && (attrVal === 'TAXI'));
            hideOnAttributes ||= ((filterVehicle_Motorcycle === true) && (attrVal === 'MOTORCYCLE'));
            hideOnAttributes ||= ((filterVehicle_Unknown === true) && ((attrVal === null) || (attrVal === undefined)));
            if
            (
               ((hideOnAttributes === true) && (invertAttributesFilters === false)) || 
               ((hideOnAttributes === false) && (invertAttributesFilters === true))
            )
            {
               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
                  let sdObj = uroURSessions.GetSessionData(ureqID);
                  if(sdObj != null)
                  {
                     nComments = sdObj.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(sdObj.comments[nComments-1].userName == "") 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(sdObj.comments[nComments-1].userName == uroUserName) 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 = sdObj.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(sdObj.comments[cidx].userID == commenterUser)
                              {
                                 urStyle = 'visible';
                                 break;
                              }
                           }
                        }

                        let commentText = '';
                        for(cidx=0; cidx<nComments; cidx++)
                        {
                           commentText += sdObj.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(ureq.attributes.isStarred === 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';
               }
            }

            // state-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(uroLayersFT.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 = uroUtils.IsFilteringEnabled(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();

   let mpObjs = sdk.DataModel.MapProblems.getAll();
   for (let urobj in mpObjs)
   {
      let problem = mpObjs[urobj];

      if(problem.origGeo === undefined)
      {
         // Store a copy of the original marker position if we haven't already done so
         problem.origGeo = uroUtils.CloneObject(problem.geometry);
      }
      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.geometry = uroUtils.CloneObject(problem.origGeo);
      }


      let ureqID = problem.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.isEditable === false)
            {
               problemStyle = 'hidden';
            }
         }

         /*
         //// https://issuetracker.google.com/issues/478409089
         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 domElm = sdk.Map.getFeatureDomElement({featureId: problem.id, layerName: "mapProblems"})
         if(domElm !== null)
         {
            let problem_marker_img = domElm.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.isOpen === false)
            {
               problemStyle = 'hidden';
            }
         }

         /*
         //// https://issuetracker.google.com/issues/478409089
         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 = null; ////problem.attributes.subType;  https://issuetracker.google.com/issues/478409089
            let desc = '';
            /*
            https://issuetracker.google.com/issues/478409089
            if(problem.attributes.description != null)
            {
               desc = problem.attributes.description;
            } 
            */     
            let customType = uroGetCustomType(ureqID, uroLayersFT.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.isOpen === true) && (problem.resolvedOn !== null))
               {
                  problemStyle = 'hidden';
               }
            }


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


            if(problem.severity == "low")
            {
               if(filter_LowSeverity === true) problemStyle = 'hidden';
            }
            else if(problem.severity == "medium")
            {
               if(filter_MediumSeverity === true) problemStyle = 'hidden';
            }
            else if(filter_HighSeverity === true) problemStyle = 'hidden';
         }
      }

      let marker = uroGetMarker(uroLayersFT.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.geometry.coordinates[0] = 0;
            problem.geometry.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();
   uroFilterURs();
   uroRTCs.Filter();
   uroFilterRAs();
   uroFilterMapComments();
}
function uroFilterItemsOnMove()
{
   W.map.events.unregister('mousemove',null,uroFilterItemsOnMove);
   uroFilterItems();
}
function uroCheckCommentsForTag(idSrc)
{
   let sdObj = uroURSessions.GetSessionData(idSrc);
   if(sdObj === null) return -1;
   if(sdObj.comments.length === 0) return -1;

   for(let idx = sdObj.comments.length-1; idx >= 0; idx--)
   {
      for(let tag = 0; tag < uroCustomURTags.length; tag++)
      {
         let keyword = uroCustomURTags[tag];
         if(sdObj.comments[idx].text.indexOf(keyword) != -1)
         {
            return tag;
         }
      }
   }
   return -1;
}
function uroGetCustomType(idSrc, markerType, desc)
{
   let provider = '';
   if(desc === null) desc = '';
   if(markerType == uroLayersFT.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 == uroLayersFT.ID.MP)
   {
      let mp = W.model.mapProblems.objects[idSrc];
      if(mp.attributes.provider != null)
      {
         provider = mp.attributes.provider;
      }
   }

   if(desc !== '')
   {
      if((markerType == uroLayersFT.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 == uroLayersFT.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 == uroLayersFT.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, borderStyle)
{
   let retval = '';

   if(restObj._defaultType == "DIFFICULT")
   {
      retval = '<tr><td colspan=13 ' + borderStyle + '>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;
      }

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

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

      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>' + tdTag;

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

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

      retval += uroGetRestrictionLanes(restObj._disposition);

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

      retval += uroGetRestrictionLaneType(restObj._laneType);

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

      // 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 style="' + borderStyle + '">';
      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 uroDoesLayerHaveMF(markerType)
{
   let mtList = uroGetMarkerTypeList(markerType);
   let mfAvailable = false;
   while(mtList.length > 0)
   {
      let mt = mtList.pop();
      if(uroLayers.layers[mt].mf !== null)
      {
         mfAvailable = true;
         break;
      }
   }
   return mfAvailable;
}
function uroRestackMarkers()
{
   if(uroStackList.length === 0) return;

   if(uroDoesLayerHaveMF(uroStackType) === true)
   {
      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 urM = uroGetMarker(uroStackType, uroStackList[idx]);
         if(urM != null)
         {
            urM.style.transform = "";
         }
      }
      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] == idSrc) return true;
   }
   return false;
}
function uroHideOtherMarkers(stackType)
{
   const stackTypes = [uroLayersFT.ID.VPUR, uroLayersFT.ID.UR, uroLayersFT.ID.MP];

   for(let st of stackTypes)
   {
      let listIDs = uroGetMarkerIDs(st);
      for(let mID of listIDs)
      {
         let toHideMarker = uroGetMarker(st, mID);
         if(toHideMarker !== null)
         {
            if((uroIsIDAlreadyUnstacked(mID) === false) || (st !== stackType))
            {
               if(st === uroLayersFT.ID.VPUR)
               {
                  uroFilterPURMarker(st, mID, 'hidden');
               }
               else
               {
                  toHideMarker.style.visibility = 'hidden';
               }
            }
         }
      }
   }
}
function uroCheckStacking(stackType, masterID)
{
   if(typeof(masterID) === 'number')
   {
      masterID = masterID.toString();
   }

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

   // Do the uroGetMarkerIDs call here, as this refreshes the .mf entries in uroLayers,
   // which is needed for uroGetMarker to be able to find the marker if the layer markers
   // have been updated by WME...
   let idList = uroGetMarkerIDs(stackType);

   let masterObj = uroGetMarker(stackType, masterID);
   if(masterObj === 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 bcr = masterObj.getBoundingClientRect();
   let unstackedX = (bcr.left + bcr.right) / 2;
   let unstackedY = (bcr.top + bcr.bottom) / 2;

   let offset = 0.000000001;
   if(uroDoesLayerHaveMF(stackType) === true)
   {
      let showOpen = true;
      let showClosed = false;
      let showTypes = null;
      if(stackType === uroLayersFT.ID.UR)
      {
         showTypes = W?.issueTrackerController?.app?.attributes?.issueTrackerFilter?.attributes?.mapUpdateRequestsFilter?.attributes?.status;
      }
      else if(stackType === uroLayersFT.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(let 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.style.visibility != 'hidden');
            let isClosed = (testMarkerAttributes.open === false);
            includeInStack = includeInStack && ((isClosed && showClosed) || (!isClosed && showOpen));
            if(includeInStack)
            {
               let tmID = testMarkerAttributes.id;
               if(typeof(tmID) === 'number')
               {
                  tmID = tmID.toString();
               }               
               if(tmID != masterID)
               {
                  let bcr = testMarkerObj.getBoundingClientRect();
                  let tmx = (bcr.left + bcr.right) / 2;
                  let tmy = (bcr.top + bcr.bottom) / 2;
                  let xdiff = unstackedX - tmx;
                  let ydiff = unstackedY - tmy;
                  let distSquared = ((xdiff * xdiff) + (ydiff * ydiff));
                  
                  if(distSquared < threshSquared)
                  {
                     stackList.push(tmID);
                  }
               }
            }
         }
      }
   }

   let inhibitUnstacking = (sdk.Map.getZoomLevel() < 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(masterID);

         let offsetX = 0;
         let offsetY = 0;
         for(let shoveIdx=0; shoveIdx < stackList.length; shoveIdx++)
         {
            let fid = stackList[shoveIdx];
            let stackMarker = uroGetMarker(stackType, fid);
            if(stackMarker !== null)
            {
               let bcr = stackMarker.getBoundingClientRect();
               let smx = (bcr.left + bcr.right) / 2;
               let smy = (bcr.top + bcr.bottom) / 2;
               let newX = unstackedX - smx + offsetX;
               let newY = unstackedY - smy - offsetY;
               
               uroStackList.push(fid);
               stackMarker.style.transform = "translate("+newX+"px, "+newY+"px)";
               offsetX += 10;
               offsetY += 25;
            }
         }


         // hide other markers to prevent confusion with the unstacked markers
         uroHideOtherMarkers(stackType);
         // also hide the comment counts we add to URs, both so that they don't hang around
         // after we've hidden the respective UR markers not in the current stack, and also
         // because even for the ones that are in the stack, the counts would get in the
         // way unless we repositioned them, which seems like extra effort that isn't really
         // worthwhile given that once a user is into the realms of unstacking a UR cluster,
         // they've probably already gleaned all the info they need from the counts and can
         // do without them whilst handling the stack...
         uroURExtras.RemoveCommentCounts();
      }
   }
   else
   {
      uroRestackMarkers();
   }
}
function uroGetCentroidish(geo)
{
   let retval = null;
   if(geo.type === "Point")
   {
      retval = geo.coordinates;
   }
   else if(geo.coordinates.length > 0)
   {
      let clon = 0;
      let clat = 0;
      let n = 0;
      for(let i = 0; i < geo.coordinates.length; ++i)
      {
         for(let j = 0; j < geo.coordinates[i].length; ++j)
         {
            clon += geo.coordinates[i][j][0];
            clat += geo.coordinates[i][j][1];
            ++n;
         }
      }
      retval = [(clon / n), (clat / n)];
   }
   return retval
}
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 = sdk.DataModel.Venues.getById({venueId: uroFID});
   if(vObj !== undefined)
   {
      if(vObj.navigationPoints.length > 0)
      {
         // if the venue has any navpoints defined, use the position of the first one
         let tPoint = vObj.navigationPoints[0].point;
         retval.lon = tPoint.coordinates[0];
         retval.lat = tPoint.coordinates[1];
      }
      else
      {
         // otherwise use the centrepoint of the venue point or polygon
         let tPoint = uroGetCentroidish(vObj.geometry);
         if(tPoint !== null)
         {
            retval.lon = tPoint[0];
            retval.lat = tPoint[1];
         }
      }
   }
   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 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;
   }

   let mapBCR = document.getElementById('map').getBoundingClientRect();
   let mouseX = e.pageX - mapBCR.left;
   let mouseY = e.pageY - mapBCR.top;
   if((isNaN(mouseX) == true) || (isNaN(mouseY) == true))
   {
      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(sdk.Map.isLayerVisible({layerName: 'area_managers'}) === true)
   {
      let mousePixel = sdk.Map.getLonLatFromPixel({x: mouseX, y: 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");

      let managedAreas = sdk.DataModel.ManagedAreas.getAll();
      for(let amObj in managedAreas)
      {
         let ama = managedAreas[amObj];
         let userName = ama.userName;
         let userLevel = uroUtils.GetUserLevelFromName(userName);  //// https://issuetracker.google.com/issues/473067424

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

         /*
         //// https://issuetracker.google.com/issues/484308800
         if((hideNewAreas === true) || (hideOldAreas === true))
         {
            let amaDaysAgo = uroUtils.DateToDays(ama.attributes.createdOn);
            hideArea = hideArea || ((hideNewAreas === true) && (amaDaysAgo < areaMinAge));
            hideArea = hideArea || ((hideOldAreas === true) && (amaDaysAgo > areaMaxAge));
         }
         */

         if(hideArea === false)
         {
            let nc = ama.geometry.coordinates.length;
            for(let i = 0; i < nc; ++i)
            {
               let geo = ama.geometry.coordinates[i];
               if(uroContainsPoint(geo, mousePoint) === true)
               {
                  if(amList.indexOf(userName) === -1)
                  {
                     if(amList !== '') amList += ', ';
                     tName = uroUtils.GetUserNameAndRank(userName);
                     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 mPos = uroUtils.ConvertMercatorToWGS84(sdk.Map.getLonLatFromPixel({x: x, y: y}));
   let nZoom = sdk.Map.getZoomLevel();
   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);
   }
}
function uroGetSelectedURCommentCount()
{
   let sdObj = uroURSessions.GetSessionData(uroSelectedURID);
   if(sdObj !== null)
   {
      let cachedCommentCount = sdObj.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 = sdObj.comments[cachedCommentCount-1].id;
            }
            else
            {
               uroCachedLastCommentID = null;
            }
            uroDBG.AddLog('session data 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');
            }
            sdk.DataModel.updateRequestSessions.getUpdateRequestDetails({mapUpdateRequestId: parseInt(uroSelectedURID)});

            // the refresh process has started so we're no longer pending, but we are now waiting for the request to return...
            uroPendingCommentDataRefresh = false;
            uroWaitingCommentDataRefresh = true;
         }
         else
         {
            if(cachedCommentCount > 0)
            {
               let currentLastCommentID = sdObj.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

   let sdObj = uroURSessions.GetSessionData(uroSelectedURID);
   if(sdObj !== null)
   {
      uroExpectedCommentCount = sdObj.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;
}
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);

               let sdObj = uroURSessions.GetSessionData(uroSelectedURID);
               if(sdObj !== null)
               {
                  uroExpectedCommentCount = sdObj.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(uroLayersFT.ID.UR,uroHoveredURID);
                     if(urMarker !== null)
                     {
                        let urMarkerAttributes = uroGetAttributes(uroLayersFT.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 uroClosureListFiltering()
{
   // List entry filtering
   let entryElms = document.querySelector('.closures-list')?.querySelectorAll('wz-card');
   if(entryElms !== undefined)
   {
      let nEntries = entryElms.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 = entryElms[i];
            let ciSrc = cElm.querySelector('wz-image-chip')?.imageSrc;
            if(ciSrc != undefined)
            {
               hide = 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.parentElement.style.display = "none";
            }
            else
            {
               cElm.parentElement.style.display = "block";
            }
         }
      }
   }
}
function uroClosureListAddCloneCtrls()
{
   // Closure cloning controls
   if((document.querySelectorAll('.closures-list').length > 0) && (document.querySelector('.closures-list').getAttribute('touchedbyuro_cloning') === 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)
               {
                  uroRTCs.GetForSelectedSegment(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) && (uroRTCs.GetOrigin(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] + '>';
                     }
                  }
               }
            }
         }
      }
      document.querySelector('.closures-list').setAttribute('touchedbyuro_cloning','true');
   }
}
function uroClosureListAddDeleteAll()
{
   // Add a "Delete All" button if there's more than one closure in the list that can be deleted...

   if(document.querySelector('.closures-list')?.getAttribute('touchedbyuro_deleteall') !== null)
   {
      return;
   }

   let deletableElms = uroRTCClone.GetDeletableClosures();
   if(deletableElms.length < 2)
   {
      return;
   }

   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") + ' ('+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_deleteall','true');
}
function uroClosureListHandler()
{
   // Handles adjustments to the closure list in the sidepanel...
   if(uroUtils.GetCBChecked('_cbMasterEnable') === false)
   {
      return;
   }

   uroClosureListFiltering();
   ////uroClosureListAddCloneCtrls();
   uroClosureListAddDeleteAll();
}
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(sdk.State.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.substring(showmarkerpos, endmarkerpos);
      uroDBG.AddLog(' turn problem ID = '+urID);

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

      if(doRetry) window.setTimeout(uroNewLookCheckDetailsRequest,500);
   }
}
function uroUpdateVenueEditorLists()
{
   let vObjs = sdk.DataModel.Venues.getAll();
   if(vObjs.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 vObjs)
   {
      let obj = vObjs[idx];
      let cbID = obj.modificationData.createdBy;
      let ubID = obj.modificationData.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 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.slice(21);
   if(uroPlacesGroupsCollapsed[groupidx] === true) uroPlacesGroupsCollapsed[groupidx] = false;
   else uroPlacesGroupsCollapsed[groupidx] = true;
   uroPlacesGroupCEHandler(groupidx);
   return false;
}
function uroGetMarkerIDs(markerType)
{
   let idList = [];
   let mtList = uroGetMarkerTypeList(markerType);

   while(mtList.length > 0)
   {
      let mt = mtList.pop();
      if(uroLayers.layers[mt].mf !== null)
      {
         // Refresh .mf, as this is no longer guaranteed to have been set up as a reference to
         // whatever's in the current layer stack...
         uroLayers.layers[mt].mf = uroLayers.GetMarkersOrFeatures(mt);         
         for(let i = 0; i < uroLayers.layers[mt].mf.length; ++i)
         {
            let dID = uroLayers.layers[mt].mf[i]?.element?.attributes['data-id']?.value;
            if(dID === undefined)
            {
               dID = uroLayers.layers[mt].mf[i]?.attributes?.wazeFeature?.id;
            }
            if(dID !== undefined)
            {
               if(typeof dID === "string")
               {
                  dID = parseInt(dID);
               }
               idList.push(dID);
            }
         }
      }
   }
   return idList;
}
function uroGetAttributes(markerType, markerID)
{
   if(markerType == uroLayersFT.ID.UR) return sdk.DataModel.MapUpdateRequests.getById({mapUpdateRequestId: markerID});
   if(markerType == uroLayersFT.ID.MP) return sdk.DataModel.MapProblems.getById({mapProblemId: markerID});
   if(markerType == uroLayersFT.ID.VPUR) return sdk.DataModel.Venues.getById({venueId: markerID});
   if(markerType == uroLayersFT.ID.SegSug) return sdk.DataModel.EditSuggestions.getById({editSuggestionId: markerID});
   return null;
}
function uroGetMarker(markerType, markerID)
{
   if(typeof(markerID) === 'string')
   {
      markerID = parseInt(markerID);
   }

   let retval = null;

   if(markerID !== null)
   {
      retval = uroFindMarkerOnLayer(markerType, markerID);
   }
   return retval;
}
function uroGetMarkerTypeList(markerType)
{
   let mtList = [];
   if(markerType === uroLayersFT.ID.VPUR)
   {
      mtList.push(uroLayersFT.ID.PUR);
      mtList.push(uroLayersFT.ID.PPUR);
      mtList.push(uroLayersFT.ID.RPUR);
   }
   else
   {
      mtList.push(markerType);
   }
   return mtList;
}
function uroFindMarkerOnLayer(markerType, markerID)
{
   retval = null;
   let mtList = uroGetMarkerTypeList(markerType);

   while(mtList.length > 0)
   {
      let mt = mtList.pop();
      retval = sdk.Map.getFeatureDomElement({featureId: markerID, layerName: uroLayers.layers[mt].name});
      if(retval !== null)
      {
         break;
      }
   }
   return retval;
}

uroInit.Initialise();