WME History Enhancer

Enhances map object history entries

// ==UserScript==
// @name                WME History Enhancer
// @namespace           http://greasemonkey.chizzum.com
// @description         Enhances map object history entries
// @include             https://*.waze.com/*editor*
// @include             https://editor-beta.waze.com/*
// @include             https://beta.waze.com/*
// @exclude             https://www.waze.com/user/*editor/*
// @exclude             https://www.waze.com/*/user/*editor/*
// @grant               none
// @version             1.6
// ==/UserScript==

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


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

*/

/* JSHint Directives */
/* globals W: true */
/* globals I18n: */
/* globals trustedTypes: */
/* jshint bitwise: false */
/* jshint eqnull: true */
/* jshint esversion: 11 */
/* jshint undef: true */
/* jshint unused: true */

const Release =
{
   version : "1.6",
   date : "20250504",
   changes : 
   [
      "Compatibility updates",
      "Reformats venue category and service changes to clearly show what they are",
      "Hide closures/rejected updates setting is now persistent"
   ]
};

const AlertBox =
{
   stack: [],
   tickAction: null,
   crossAction: null,
   inUse: false,
   ab: null,
   abID: null,
   fnMH: null,
   
   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()
   {
      let abElm = document.getElementById(AlertBox.abID);
      abElm.childNodes[0].innerHTML = AlertBox.fnMH('');
      abElm.childNodes[1].innerHTML = AlertBox.fnMH('');
      abElm.querySelector('#tickBtnCaption').innerHTML = AlertBox.fnMH('');
      abElm.querySelector('#crossBtnCaption').innerHTML = AlertBox.fnMH('');
      AlertBox.tickAction = null;
      AlertBox.crossAction = null;
      abElm.style.visibility = "hidden";
      abElm.querySelector('#crossBtn').style.visibility = "hidden";
      AlertBox.inUse = false;
      if(AlertBox.stack.length > 0)
      {
         AlertBox.BuildFromStack();
      }
   },
   CloseWithTick: function()
   {
      if(typeof AlertBox.tickAction === 'function')
      {
         AlertBox.tickAction();
      }
      AlertBox.Close();
   },
   CloseWithCross: function()
   {
      if(typeof AlertBox.crossAction === 'function')
      {
         AlertBox.crossAction();
      }
      AlertBox.Close();
   },
   Show: function(headericon, title, content, hasCross, tickText, crossText, tickAction, crossAction)
   {
      AlertBox.stack.push(new AlertBox.ABObj(headericon, title, content, hasCross, tickText, crossText, tickAction, crossAction));
      if(AlertBox.inUse === false)
      {
         AlertBox.BuildFromStack();
      }
   },
   BuildFromStack: function()
   {
      AlertBox.inUse = true;
      AlertBox.tickAction = null;
      AlertBox.crossAction = null;
      let titleContent = '<span style="font-size:14px;padding:2px;">';
      titleContent += '<i class="fa '+AlertBox.stack[0].headericon+'"> </i>&nbsp;';
      titleContent += AlertBox.stack[0].title;
      titleContent += '</span>';
      let abElm = document.getElementById(AlertBox.abID);
      abElm.childNodes[0].innerHTML = AlertBox.fnMH(titleContent);
      abElm.childNodes[1].innerHTML = AlertBox.fnMH(AlertBox.stack[0].content);
      abElm.querySelector('#tickBtnCaption').innerHTML = AlertBox.fnMH(AlertBox.stack[0].tickText);
      if(AlertBox.stack[0].hasCross)
      {
         abElm.querySelector('#crossBtnCaption').innerHTML = AlertBox.fnMH(AlertBox.stack[0].crossText);
         abElm.querySelector('#crossBtn').style.visibility = "visible";
         if(typeof AlertBox.stack[0].crossAction === "function")
         {
            AlertBox.crossAction = AlertBox.stack[0].crossAction;
         }
      }
      else
      {
         abElm.querySelector('#crossBtn').style.visibility = "hidden";
      }
      if(typeof AlertBox.stack[0].tickAction === "function")
      {
         AlertBox.tickAction = AlertBox.stack[0].tickAction;
      }
      abElm.style.visibility = "";
      AlertBox.stack.shift();
   },
   Init: function(abID, hdCol, bgCol, fnModHTML)
   {
      AlertBox.abID = abID;
      AlertBox.fnMH = fnModHTML;

      // create a new div to display script alerts
      AlertBox.ab = document.createElement('div');
      AlertBox.ab.id = abID;
      AlertBox.ab.style.position = 'fixed';
      AlertBox.ab.style.visibility = 'hidden';
      AlertBox.ab.style.top = '50%';
      AlertBox.ab.style.left = '50%';
      AlertBox.ab.style.zIndex = 10000;
      AlertBox.ab.style.backgroundColor = bgCol;
      AlertBox.ab.style.borderWidth = '3px';
      AlertBox.ab.style.borderStyle = 'solid';
      AlertBox.ab.style.borderRadius = '10px';
      AlertBox.ab.style.boxShadow = '5px 5px 10px Silver';
      AlertBox.ab.style.padding = '4px';
      AlertBox.ab.style.webkitTransform = "translate(-50%, -50%)";
      AlertBox.ab.style.transform = "translate(-50%, -50%)";
   
      let alertsHTML = '<div id="header" style="padding: 4px; background-color:'+hdCol+'; 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="tickBtn" 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="tickBtnCaption" style="font-weight: bold;"></span>';
      alertsHTML += '</span>';
      alertsHTML += '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;';
      alertsHTML += '<span id="crossBtn" 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="crossBtnCaption" style="font-weight: bold;"></span>';
      alertsHTML += '</span>';
      alertsHTML += '</div>';
      AlertBox.ab.innerHTML = fnModHTML(alertsHTML);
      document.body.appendChild(AlertBox.ab);

      window.setTimeout(AlertBox.Init2, 100);
   },
   Init2: function()
   {
      let abElm = document.getElementById(AlertBox.abID);
      abElm.querySelector('#tickBtn').addEventListener('click', AlertBox.CloseWithTick, true);
      abElm.querySelector('#crossBtn').addEventListener('click', AlertBox.CloseWithCross, true);   
   }
};

const WHE =
{
   showDebugOutput: true,
   enhanceHistoryItemID: null,
   enhanceHistoryItemType: null,
   itemHistoryDetails: null,
   itemHistoryLoaded: false,
   prevWazeBitsPresent: null,
   wazeBitsPresent: 0,
   initAttempts: 10,
   settings: {lastVer: null, hideBits: null},
   listType: {categories: "categories", services: "services"},

   ModifyHTML: function(htmlIn)
   {
      if(typeof trustedTypes === "undefined")
      {
         return htmlIn;
      }
      else
      {
         const escapeHTMLPolicy = trustedTypes.createPolicy("forceInner", {createHTML: (to_escape) => to_escape});
         return escapeHTMLPolicy.createHTML(htmlIn);
      }
   },
   AddLog: function(logtext)
   {
      if(WHE.showDebugOutput) console.log('WHE: '+Date()+' '+logtext);
   },
   GetRestrictionLanes: function(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;
   },
   GetRestrictionLaneType: function(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;
   },
   GetDirectionString: function(isForward)
   {
      if(isForward === true)
      {
         return 'A-B';
      }
      else
      {
         return 'B-A';
      }
   },
   GetVehicleDescription: function(vehicleType)
   {
      let retval = null;
      let i18nLookup = null;
      if(vehicleType === 0) i18nLookup = "TRUCK";
      else if(vehicleType === 256) i18nLookup = "PUBLIC_TRANSPORTATION";
      else if(vehicleType === 272) i18nLookup = "TAXI";
      else if(vehicleType === 288) i18nLookup = "BUS";
      else if(vehicleType === 512) i18nLookup = "RV";
      else if(vehicleType === 768) i18nLookup = "TOWING_VEHICLE";
      else if(vehicleType === 1024) i18nLookup = "MOTORCYCLE";
      else if(vehicleType === 1280) i18nLookup = "PRIVATE";
      else if(vehicleType === 1536) i18nLookup = "HAZARDOUS_MATERIALS";
      else if(vehicleType === 1792) i18nLookup = "CAV";
      else if(vehicleType === 1808) i18nLookup = "EV";
      else if(vehicleType === 1824) i18nLookup = "HYBRID";
      else if(vehicleType === 1840) i18nLookup = "CLEAN_FUEL";
      if(i18nLookup !== null)
      {
         retval = I18n.lookup("restrictions.vehicle_types."+i18nLookup);
      }
      return retval;
   },
   FormatTBR: function(tbrObj)
   {
      let retval = '';
      if(tbrObj.description !== null)
      {
         retval += '&nbsp;&nbsp;Reason: ' + tbrObj.description + '<br>';
      }

      if(tbrObj.timeFrames.length > 0)
      {
         retval += '&nbsp;&nbsp;Dates: ';
         if(tbrObj.timeFrames[0].startDate === null)
         {
            retval += 'all dates';
         }
         else
         {
            retval += tbrObj.timeFrames[0].startDate + ' to ' + tbrObj.timeFrames[0].endDate;
         }
         retval += '<br>';

         retval += '&nbsp;&nbsp;Days: ';
         if(tbrObj.timeFrames[0].weekdays & (1<<0)) retval += 'M';
         else retval += '-';
         if(tbrObj.timeFrames[0].weekdays & (1<<1)) retval += 'T';
         else retval += '-';
         if(tbrObj.timeFrames[0].weekdays & (1<<2)) retval += 'W';
         else retval += '-';
         if(tbrObj.timeFrames[0].weekdays & (1<<3)) retval += 'T';
         else retval += '-';
         if(tbrObj.timeFrames[0].weekdays & (1<<4)) retval += 'F';
         else retval += '-';
         if(tbrObj.timeFrames[0].weekdays & (1<<5)) retval += 'S';
         else retval += '-';
         if(tbrObj.timeFrames[0].weekdays & (1<<6)) retval += 'S';
         else retval += '-';
         retval += '<br>';

         retval += '&nbsp;Timespan: ';
         if(tbrObj.timeFrames[0].fromTime === null)
         {
            retval += 'all day';
         }
         else
         {
            retval += tbrObj.timeFrames[0].fromTime + ' to ' + tbrObj.timeFrames[0].toTime;
         }
         retval += '<br>';
      }

      let vtLength = 0;
      if(tbrObj.driveProfiles.BLOCKED !== undefined)
      {
         vtLength = tbrObj.driveProfiles.BLOCKED[0].vehicleTypes.length;
         if(vtLength > 0)
         {
            retval += '&nbsp;Vehicle types prohibited:<br>';
            for(let i=0; i<vtLength; i++)
            {
               retval += '&nbsp;&nbsp;'+WHE.GetVehicleDescription(tbrObj.driveProfiles.BLOCKED[0].vehicleTypes[i])+'<br>';
            }
         }
      }
      else if(tbrObj.driveProfiles.FREE !== undefined)
      {
         vtLength = tbrObj.driveProfiles.FREE[0].vehicleTypes.length;
         if(vtLength > 0)
         {
            retval += '&nbsp;Vehicle types allowed:<br>';
            for(let i=0; i<vtLength; i++)
            {
               retval += '&nbsp;&nbsp;'+WHE.GetVehicleDescription(tbrObj.driveProfiles.FREE[0].vehicleTypes[i])+'<br>';
            }
         }
      }
      else if(tbrObj.defaultType === "BLOCKED")
      {
         retval += '&nbsp;Blocked for all vehicle types<br>';
      }

      if(tbrObj.defaultType === "DIFFICULT")
      {
         retval += '&nbsp;Difficult Turn<br>';
      }
      return retval;
   },
   // IsObject and CompareTBRs modified from original code at
   // https://dmitripavlutin.com/how-to-compare-objects-in-javascript/
   IsObject: function(object) 
   {
      return object != null && typeof object === 'object';
   },
   CompareTBRs: function(tbr1, tbr2)
   {
      let retval = true;
      const keys1 = Object.keys(tbr1);
      const keys2 = Object.keys(tbr2);
      if (keys1.length !== keys2.length) 
      {
         retval = false;
      }
      else
      {
         for (const key of keys1) 
         {
            const val1 = tbr1[key];
            const val2 = tbr2[key];
            const areObjects = WHE.IsObject(val1) && WHE.IsObject(val2);
            if (areObjects && !WHE.CompareTBRs(val1, val2) || !areObjects && val1 !== val2) 
            {
               retval = false;
               break;
            }
         }
      }
      return retval;
   },
   FormatTBRDetails: function(tbrObj)
   {
      let retval = '';

      let hasOld = ((tbrObj.oldValue !== undefined) && (tbrObj.oldValue.restrictions !== undefined) && (tbrObj.oldValue.restrictions.length > 0));
      let hasNew = ((tbrObj.newValue !== undefined) && (tbrObj.newValue.restrictions !== undefined) && (tbrObj.newValue.restrictions.length > 0));

      if((hasOld === true) || (hasNew === true))
      {
         retval += '<i>TBR ';
         if(hasOld === false)
         {
            retval += 'Added:<br>';
            for(let i = 0; i < tbrObj.newValue.restrictions.length; ++i)
            {
               if(i > 0)
               {
                  retval += '<br>';
               }
               retval += WHE.FormatTBR(tbrObj.newValue.restrictions[i]);
            }
         }
         else if (hasNew === false)
         {
            retval += 'Deleted:<br>';
            for(let i = 0; i < tbrObj.oldValue.restrictions.length; ++i)
            {
               if(i > 0)
               {
                  retval += '<br>';
               }
               retval += WHE.FormatTBR(tbrObj.oldValue.restrictions[i]);
            }
         }
         else
         {
            retval += 'Changed:<br>';

            let oldStillPresent = [];
            let newStillPresent = [];
            for(let i = 0; i < tbrObj.oldValue.restrictions.length; ++i)
            {
               for(let j = 0; j < tbrObj.newValue.restrictions.length; ++j)
               {
                  if(WHE.CompareTBRs(tbrObj.oldValue.restrictions[i], tbrObj.newValue.restrictions[j]) == true)
                  {
                     oldStillPresent.push(i);
                     newStillPresent.push(j);
                  }
               }
            }

            let tbrsShown = 0;
            for(let i = 0; i < tbrObj.oldValue.restrictions.length; ++i)
            {
               if(oldStillPresent.indexOf(i) == -1)
               {
                  if(tbrsShown == 0)
                  {
                     retval += 'Removed:<br>';
                  }
                  else
                  {
                     retval += '<br>';
                  }
                  retval += WHE.FormatTBR(tbrObj.oldValue.restrictions[i]);
                  ++tbrsShown;
               }
            }
            if(tbrsShown > 0)
            {
               retval += '<br>';
            }
            tbrsShown = 0;
            for(let i = 0; i < tbrObj.newValue.restrictions.length; ++i)
            {
               if(newStillPresent.indexOf(i) == -1)
               {
                  if(tbrsShown == 0)
                  {
                     retval += 'Added:<br>';
                  }
                  else
                  {
                     retval += '<br>';
                  }
                  retval += WHE.FormatTBR(tbrObj.newValue.restrictions[i]);
                  ++tbrsShown;
               }
            }
         }
         retval += '</i>';
      }
      else
      {
         // not a TBR history entry...
      }

      return retval;
   },
   FormatClosureReason: function(rData)
   {
      let retval = "";
      if(rData == null)
      {
         retval = "<i>not provided</i>";
      }
      else
      {
         retval = rData;
      }
      return retval;
   },
   FormatClosureMTE: function(mData)
   {
      let retval = "";
      if(mData == null)
      {
         retval = "<i>not provided</i>";
      }
      else if(W.model.majorTrafficEvents.objects[mData] === undefined)
      {
         retval = "<i>data not available</i>";
      }
      else
      {
         retval = W.model.majorTrafficEvents.objects[mData].attributes.names[0].value;
      }
      return retval;
   },
   FormatClosureDetails: function(cObjA, cObjB)
   {
      let retval = '';
      if(cObjB === null)
      {
         retval += 'Reason: ' + WHE.FormatClosureReason(cObjA.reason) + '<br>';
         retval += 'MTE: ' + WHE.FormatClosureMTE(cObjA.eventId) + '<br>';
         retval += 'From: ' + cObjA.startDate + '<br>';
         retval += 'To: ' + cObjA.endDate + '<br>';
         retval += 'Direction: ' + WHE.GetDirectionString(cObjA.forward) + '<br>';
         retval += 'Ignore traffic: ' + cObjA.permanent;
      }
      else
      {
         if(cObjA.reason !== cObjB.reason)
         {
            retval += 'Reason: ' + WHE.FormatClosureReason(cObjA.reason);
            retval += ' <i>\>\>\> ' + WHE.FormatClosureReason(cObjB.reason) + '</i><br>';
         }
         if(cObjA.eventId !== cObjB.eventId)
         {
            retval += 'MTE: ' + WHE.FormatClosureMTE(cObjA.eventId);
            retval += ' <i>\>\>\> ' + WHE.FormatClosureMTE(cObjB.eventId) + '</i><br>';
         }
         if(cObjA.startDate !== cObjB.startDate)
         {
            retval += 'From: ' + cObjA.startDate;
            retval += ' <i>\>\>\> ' + cObjB.startDate + '</i><br>';
         }
         if(cObjA.endDate !== cObjB.endDate)
         {
            retval += 'To: ' + cObjA.endDate;
            retval += ' <i>\>\>\> ' + cObjB.endDate + '<i><br>';
         }
         if(cObjA.forward !== cObjB.forward)
         {
            retval += 'Direction: ' + WHE.GetDirectionString(cObjA.forward);
            retval += ' <i>\>\>\> ' + WHE.GetDirectionString(cObjB.forward) + '</i><br>';
         }
         if(cObjA.permanent !== cObjB.permanent)
         {
            retval += 'Ignore traffic: ' + cObjA.permanent;
            retval += ' <i>\>\>\> ' + cObjB.permanent + '</i><br>';
         }
      }
      return retval;
   },
   GetTIOString: function(tioValue)
   {
      let retval = I18n.lookup("turn_tooltip.instruction_override.no_opcode");
      if(tioValue !== null)
      {
         retval = I18n.lookup("turn_tooltip.instruction_override.opcodes." + tioValue);
      }
      return retval;
   },
   SegmentHistoryNameString: function(segID)
   {
      let retval = '';
      if(W.model.segments.objects[segID] !== undefined)
      {
         if(W.model.segments.objects[segID].attributes.primaryStreetID !== undefined)
         {
            const sID = W.model.segments.objects[segID].attributes.primaryStreetID;
            const sName = W.model.streets.objects[sID].attributes.name;
            if((sName === null) || (sName === ''))
            {
               retval += 'unnamed segment';
            }
            else
            {
               retval += sName;
            }
         }
         else
         {
            retval += 'unnamed segment';
         }
      }
      else
      {
         retval += 'unknown segment';
      }
      retval += ' (ID ' + segID + ')';
      return retval;
   },
   VenueHistoryFormatChanges: function(vObj, showExtendedDetails)
   {
      let tHTML = '';
      if(vObj.type === "IMAGE")
      {
         tHTML += '<br>Image update';
      }
      else if(vObj.type === "REQUEST")
      {
         if(vObj.subType === "UPDATE")
         {
            if(vObj.changedVenue !== undefined)
            {
               if(vObj.changedVenue.categories !== undefined)
               {
                  tHTML += '<br>Category update';

                  if(showExtendedDetails === true)
                  {
                     tHTML += "<ul>";
                     for(let j = 0; j < vObj.changedVenue.categories.length; ++j)
                     {
                        tHTML += '<li>'+vObj.changedVenue.categories[j];
                     }
                     tHTML += "</ul>";
                  }
               }
               if(vObj.changedVenue.entryExitPoints !== undefined)
               {
                  tHTML += '<br>Entry/exit point change';
               }
               if(vObj.changedVenue.description !== undefined)
               {
                  tHTML += '<br>Description change';
                  if(showExtendedDetails === true)
                  {
                     tHTML += '<ul><li>' + vObj.changedVenue.description + "</ul>";
                  }
               }
               if(vObj.changedVenue.name !== undefined)
               {
                  tHTML += '<br>Name change';
                  if(showExtendedDetails === true)
                  {
                     tHTML += '<ul><li>' + vObj.changedVenue.name + "</ul>";
                  }
               }
               if(vObj.changedVenue.openingHours !== undefined)
               {
                  tHTML += '<br>Opening hours change';
                  if(showExtendedDetails === true)
                  {
                  }
               }
               if(vObj.changedVenue.url !== undefined)
               {
                  tHTML += '<br>URL change';
                  if(showExtendedDetails === true)
                  {
                  }
               }
               if(vObj.changedVenue.services !== undefined)
               {
                  tHTML += '<br>Services change';
                  if(showExtendedDetails === true)
                  {
                     tHTML += '<ul>';
                     for(let i = 0; i < vObj.changedVenue.services.length; ++i)
                     {
                        tHTML += '<li>' + I18n.lookup('venues.services')[vObj.changedVenue.services[i]];
                     }
                     tHTML += '</ul>';
                  }
               }
               if(vObj.changedVenue.phone !== undefined)
               {
                  tHTML += '<br>Phone number change';
                  if(showExtendedDetails === true)
                  {
                  }
               }
               if(vObj.changedVenue.aliases !== undefined)
               {
                  tHTML += '<br>Alternate name change';
                  if(showExtendedDetails === true)
                  {
                     tHTML += '<ul>';
                     for(let i = 0; i < vObj.changedVenue.aliases.length; ++i)
                     {
                        tHTML += '<li>' + vObj.changedVenue.aliases[i];
                     }
                     tHTML += '</ul>';
                  }
               }
               if(vObj.changedVenue.brand !== undefined)
               {
                  tHTML += '<br>Brand change';
                  if(showExtendedDetails === true)
                  {
                  }
               }
            }
         }
         else if(vObj.subType === "FLAG")
         {
            tHTML += '<br>Flagged place';
         }
      }

      if(tHTML === '')
      {
         tHTML += '<br>No details';
      }
      return tHTML;
   },
   TranslateListType: function(orig, listType)
   {
      return I18n.lookup("venues."+listType)[orig];
   },
   GenerateChangeList: function(oldOnes, newOnes, listType)
   {
      if(newOnes === undefined)
      {
         newOnes = [''];
      }
      if(oldOnes === undefined)
      {
         oldOnes = [''];
      }

      let tHTML = "";
      let addHeader = true;
      for(let i = 0; i < newOnes.length; ++i)
      {
         let nC = newOnes[i];
         if(oldOnes.indexOf(nC) === -1)
         {
            if(addHeader === true)
            {
               tHTML += '<br>Added:<ul>';
               addHeader = false;
            }
            tHTML += '<li>' + WHE.TranslateListType(nC, listType);
         }
      }
      if(addHeader === false)
      {
         tHTML += '</ul>';
      }

      addHeader = true;
      for(let i = 0; i < oldOnes.length; ++i)
      {
         let oC = oldOnes[i];
         if(newOnes.indexOf(oC) === -1)
         {
            if(addHeader === true)
            {
               tHTML += '<br>Removed:<ul>';
               addHeader = false;
            }
            tHTML += '<li>' + WHE.TranslateListType(oC, listType);
         }
      }
      if(addHeader === false)
      {
         tHTML += '</ul>';
      }

      return tHTML;
   },
   ParseHistoryObject_Venue: function(tObj)
   {
      let tHTML = '';
      let aType = tObj.actionType;

      if(aType === "UPDATE")
      {
         // categories
         tHTML += WHE.GenerateChangeList(tObj.oldValue.categories, tObj.newValue.categories, WHE.listType.categories);

         // services
         tHTML += WHE.GenerateChangeList(tObj.oldValue.services, tObj.newValue.services, WHE.listType.services);
      }

      return tHTML;
   },
   ParseHistoryObject_VenueUpdateRequest: function(tObj)
   {
      let tHTML = '';
      let aType = tObj.actionType;

      if(aType === "DELETE")
      {
         if(tObj.oldValue.approve === true)
         {
            tHTML += '<b>Approved:</b>';
            tHTML += WHE.VenueHistoryFormatChanges(tObj.oldValue, false);
         }
         else if(tObj.oldValue.approve === false)
         {
            tHTML += '<b>Rejected:</b>';
            tHTML += WHE.VenueHistoryFormatChanges(tObj.oldValue, true);
         }
         else
         {
            tHTML += '<b>Closed, no further details</b>';
            // older venueUpdateRequest objects don't have fully populated oldValues...
         }
      }
      else if(aType === "ADD")
      {
         tHTML += '<b>Editor change pending approval:</b> ';
         tHTML += WHE.VenueHistoryFormatChanges(tObj.newValue, true);
      }

      return tHTML;
   },
   GetStreetBits: function(segID)
   {
      let retval = null;

      if(segID != undefined)
      {
         let sIdx = -1;
         let cIdx = -1;
         let stIdx = -1;
         for(let i = 0; i < WHE.itemHistoryDetails.streets.length; ++i)
         {
            if(WHE.itemHistoryDetails.streets[i].id == segID)
            {
               sIdx = i;
               break;
            }
         }
         
         const cityID = WHE.itemHistoryDetails.streets[sIdx].cityID;
         for(let i = 0; i < WHE.itemHistoryDetails.cities.length; ++i)
         {
            if(WHE.itemHistoryDetails.cities[i].id == cityID)
            {
               cIdx = i;
               break;
            }
         }

         const stateID = WHE.itemHistoryDetails.cities[cIdx].stateID;
         for(let i = 0; i < WHE.itemHistoryDetails.states.length; ++i)
         {
            if(WHE.itemHistoryDetails.states[i].id == stateID)
            {
               stIdx = i;
               break;
            }
         }

         let streetName = "";
         if(sIdx != -1)
         {
            streetName = WHE.itemHistoryDetails.streets[sIdx].name;
         }
         if(streetName == "")
         {
            streetName = "(none)";
         }

         let cityName = "";
         if(cIdx != -1)
         {
            cityName = WHE.itemHistoryDetails.cities[cIdx].name;
         }
         if(cityName == "")
         {
            cityName = "(none)";
         }
         let stateName = "";
         if(stIdx != -1)
         {
            stateName = WHE.itemHistoryDetails.states[stIdx].name;
         }
         if(stateName == "")
         {
            stateName = "(none)";
         }

         retval = [];
         retval.push(streetName);
         retval.push(cityName);
         retval.push(stateName);
      }

      return retval;
   },
   FormatSegmentNameDetails: function(tObj)
   {
      let oldStreetBits = null;
      let newStreetBits = null;

      let retval = "";
      const bitIDs = ["Street: ", "City: ", "County: "];

      if(tObj.oldValue != undefined)
      {
         oldStreetBits = WHE.GetStreetBits(tObj.oldValue.primaryStreetID);
      }
      if(tObj.newValue != undefined)
      {
         newStreetBits = WHE.GetStreetBits(tObj.newValue.primaryStreetID);
      }

      if(oldStreetBits != newStreetBits)
      {
         if(oldStreetBits == null)
         {
            retval += "Added:<br>";
            for(let i = 0; i < 3; ++i)
            {
               if(newStreetBits[i] != "(none)")
               {
                  retval += "&nbsp;";
                  retval += bitIDs[i] + newStreetBits[i]+"<br>";
               }
            }
         }
         else if(newStreetBits == null)
         {
            retval += "Deleted: "+oldStreetBits[0]+', '+oldStreetBits[1]+', '+oldStreetBits[2];
         }
         else
         {
            retval += "Changed:<br>";
            for(let i = 0; i < 3; ++i)
            {
               if(oldStreetBits[i] != newStreetBits[i])
               {
                  retval += "&nbsp;";
                  retval += bitIDs[i] + oldStreetBits[i]+" >> "+newStreetBits[i]+"<br>";
               }
            }
         }
      }
      return retval;
   },
   ParseHistoryObject_Segment: function(tObj)
   {
      let tHTML = '';
      let aType = tObj.actionType;
      
      if(aType === "UPDATE")
      {
         tHTML += WHE.FormatSegmentNameDetails(tObj);
         tHTML += WHE.FormatTBRDetails(tObj);
      }

      return tHTML;
   },
   ParseHistoryObject_RoadClosure: function(tObj)
   {
      let tHTML = '';
      let aType = tObj.actionType;

      let cObjA = null;
      let cObjB = null;

      tHTML += '<b>Road closure:</b> ';
      if(aType === "ADD")
      {
         tHTML += 'added<br>';
         cObjA = tObj.newValue;
      }
      else if(aType === "DELETE")
      {
         tHTML += 'deleted<br>';
         cObjA = tObj.oldValue;
      }
      else if(aType === "UPDATE")
      {
         tHTML += 'edited<br>';
         cObjA = tObj.oldValue;
         cObjB = tObj.newValue;
      }
      tHTML += 'ID: ' + tObj.objectID + '<br>';
      tHTML += WHE.FormatClosureDetails(cObjA, cObjB);

      return tHTML;
   },
   GetTurnAngleString: function(angle)
   {
      let retval = I18n.lookup('lanes.override.angles')[angle];
      if(retval == undefined)
      {
         retval = 'unknown angle';
      }
      return retval;
   },
   GetLanesString: function(lanesFrom, lanesTo)
   {
      let retval = '';
      if(lanesFrom == lanesTo)
      {
         retval = 'lane '+lanesFrom;
      }
      else
      {
         retval = 'lanes '+lanesFrom+'-'+lanesTo;
      }
      return retval;
   },
   GetGuidanceModeString: function(gMode)
   {
      let retval;
      if(gMode == 0)
      {
         retval = "Waze Selected";
      }
      else if(gMode == 1)
      {
         retval = "View Only";
      }
      else if(gMode == 2)
      {
         retval = "View and Hear";
      }
      else
      {
         retval = "";
      }
      return retval;
   },
   GetLaneGuidanceUpdateString: function(tObj)
   {
      let tHTML = '<br><i>';
      if(tObj.oldValue.lanes == undefined)
      {
         tHTML += 'Lane guidance added: ';
         let lanesFrom = tObj.newValue.lanes.fromLaneIndex + 1;
         let lanesTo = tObj.newValue.lanes.toLaneIndex + 1;
         tHTML += WHE.GetLanesString(lanesFrom, lanesTo);
         let turnAngle = tObj.newValue.lanes.laneArrowAngle;
         if(tObj.newValue.lanes.angleOverride != undefined)
         {
            turnAngle = tObj.newValue.lanes.angleOverride;
         }
         tHTML += ' '+WHE.GetTurnAngleString(turnAngle);
      }
      else if(tObj.newValue.lanes == undefined)
      {
         tHTML += 'Lane guidance removed';
      }
      else
      {
         tHTML += 'Lane guidance changed: ';

         let lanesFromNew = tObj.newValue.lanes.fromLaneIndex + 1;
         let lanesToNew = tObj.newValue.lanes.toLaneIndex + 1;
         let lanesFromOld = tObj.oldValue.lanes.fromLaneIndex + 1;
         let lanesToOld = tObj.oldValue.lanes.toLaneIndex + 1;
         tHTML += WHE.GetLanesString(lanesFromOld, lanesToOld);
         let lanesSame = ((lanesFromNew == lanesFromOld) && (lanesToNew == lanesToOld));

         let turnAngleNew = tObj.newValue.lanes.laneArrowAngle;
         if(tObj.newValue.lanes.angleOverride != undefined)
         {
            turnAngleNew = tObj.newValue.lanes.angleOverride;
         }
         let turnAngleOld = tObj.oldValue.lanes.laneArrowAngle;
         if(tObj.oldValue.lanes.angleOverride != undefined)
         {
            turnAngleOld = tObj.oldValue.lanes.angleOverride;
         }
         if((turnAngleOld != turnAngleNew) || (lanesSame == false))
         {
            tHTML += ' '+WHE.GetTurnAngleString(turnAngleOld)+' > ';
            if(lanesSame == false)
            {
               tHTML += WHE.GetLanesString(lanesFromNew, lanesToNew);
               tHTML += ' ';
            }
            tHTML += WHE.GetTurnAngleString(turnAngleNew);
         }

         let gModeNew = tObj.newValue.lanes.guidanceMode;
         let gModeOld = tObj.oldValue.lanes.guidanceMode;
         if(gModeOld != gModeNew)
         {
            tHTML += ' '+WHE.GetGuidanceModeString(gModeOld)+' > '+WHE.GetGuidanceModeString(gModeNew);
         }
      }
      tHTML += '</i>';

      return tHTML;
   },
   ParseHistoryObject_NodeConnection: function(tObj)
   {
      let tHTML = '';
      let aType = tObj.actionType;

      let outboundTR = (tObj.objectID.fromSegID === WHE.enhanceHistoryItemID);
      if(outboundTR === true)
      {
         tHTML += '<b>Outbound turn:</b> ';
      }
      else
      {
         tHTML += '<b>Inbound turn:</b> ';
      }
      if(aType == "DELETE") tHTML += 'disabled';
      else if(aType == "ADD") tHTML += 'enabled';
      tHTML += ' from ';

      if(outboundTR === true)
      {
         tHTML += 'node ';
         if(tObj.objectID.fromSegFwd === true)
         {
            tHTML += 'B';
         }
         else
         {
            tHTML += 'A';
         }
         tHTML += ' to ';
         tHTML += WHE.SegmentHistoryNameString(tObj.objectID.toSegID);
      }
      else
      {
         tHTML += WHE.SegmentHistoryNameString(tObj.objectID.fromSegID);
         tHTML += ' to node ';
         if(tObj.objectID.toSegFwd === true)
         {
            tHTML += 'A';
         }
         else
         {
            tHTML += 'B';
         }
      }
      tHTML += '<br>';

      if(aType === "UPDATE")
      {
         if((tObj.oldValue !== undefined) && (tObj.newValue !== undefined))
         {
            if(tObj.oldValue.instructionOpCode !== tObj.newValue.instructionOpCode)
            {
               tHTML += '<i>Instruction Override changed from '+WHE.GetTIOString(tObj.oldValue.instructionOpCode)+' to '+WHE.GetTIOString(tObj.newValue.instructionOpCode)+'</i><br>';
            }
            if(tObj.oldValue.lanes !== tObj.newValue.lanes)
            {
               tHTML += WHE.GetLaneGuidanceUpdateString(tObj);
            }
            if((tObj.oldValue.turnGuidance != null) && (tObj.newValue.turnGuidance == null))
            {
               tHTML += '<i>Turn guidance deleted</i><br>';
            }
         }
      }
      else if(aType === "ADD")
      {
         if(tObj.newValue !== undefined)
         {
            if(tObj.newValue.instructionOpCode !== null)
            {
               tHTML += '<i>Instruction Override set to ' + WHE.GetTIOString(tObj.newValue.instructionOpCode)+'</i><br>';
            }
         }
      }
      else if(aType === "DELETE")
      {
         if(tObj.oldValue !== undefined)
         {
            if(tObj.oldValue.instructionOpCode !== null)
            {
            }
         }
      }
      if(aType === "UPDATE")
      {
         tHTML += WHE.FormatTBRDetails(tObj);
         if((tObj.oldValue.navigable !== null) && (tObj.oldValue.navigable !== undefined))
         {
            if((tObj.newValue.navigable !== null) && (tObj.newValue.navigable !== undefined))
            {
               if((tObj.oldValue.navigable === false) && (tObj.newValue.navigable === true))
               {
                  tHTML += '<br><i>Turn enabled</i>';
               }
               else if((tObj.oldValue.navigable === true) && (tObj.newValue.navigable === false))
               {
                  tHTML += '<br><i>Turn disabled</i>';
               }
            }
         }

      }
      else if(aType === "ADD")
      {
         tHTML += WHE.FormatTBRDetails(tObj);
      }

      return tHTML;  
   },
   HistoryEntryToAdjust: function(lObj)
   {
      let retval;

      if
      (
         (lObj.getElementsByClassName('ca-geometry').length > 0) ||
         (lObj.getElementsByClassName('ca-roadType').length > 0) ||
         (lObj.getElementsByClassName('ca-fwdLaneCount').length > 0) ||
         (lObj.getElementsByClassName('ca-revLaneCount').length > 0)
      )
      {
         // For any history entry with one of these classes, the native details are sufficient, so don't touch them at all...
         retval = null;
      }
      else if (lObj.getElementsByClassName('turn-preview').length > 0)
      {
         // For turn previews, edit just the name so that it more clearly indicates which turn the preview relates to...
         retval = lObj.getElementsByClassName('ro-name')[0];
      }
      else
      {
         // For all other history entries, nuke the whole thing and replace with our own details...
         retval = lObj;
      }

      return retval;
   },
   EditPanelChanged: function()
   {
      let objType = W.selectionManager.getSelectedDataModelObjects()[0]?.type;
      if((objType == "segment") || (objType == "venue"))
      {
         if((document.querySelector('.toggleHistory') != null) && (document.querySelector('#wheHideHistoryBits') == null))
         {
            document.querySelector('.elementHistoryContainer').style.display='block';
            let hhbToggle = document.createElement('label');
            hhbToggle.id = "wheHideHistoryBits";
            let hhbText = "";
            if(objType == "segment")
            {
               hhbText = "Hide closures?";
            }
            else if(objType == "venue")
            {
               hhbText = "Hide rejected updates?";
            }

            let iHTML = "<input type='checkbox' id='whe_cbHideHistoryBits'";
            if(WHE.settings.hideBits === true)
            {
               iHTML += " checked";
            }
            iHTML += " />" + hhbText;
            hhbToggle.innerHTML = iHTML;
            document.querySelector(".elementHistoryContainer").insertBefore(hhbToggle, null);

            document.querySelector('#whe_cbHideHistoryBits').addEventListener('click', WHE.UpdateHistoryEntries, true);
            document.querySelector('.toggleHistory').addEventListener('click', WHE.WaitHistoryShown, true);
         }
      }
   },
   WaitHistoryShown: function()
   {
      if(document.querySelector('.toggleHistory').innerText == "View History")
      {
         window.setTimeout(WHE.WaitHistoryShown, 100);
      }
      else
      {
         WHE.UpdateHistoryEntries();
      }
   },
   ShowUpgradeNotes: function()
   {
      WHE.AddLog('let users know what\'s new in this release');

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

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

      AlertBox.Init("wheReleaseNotes", "wheat", "aliceblue", WHE.ModifyHTML);
      AlertBox.Show('fa-info-circle', 'WHE Release Notes', releaseNotes, false, "OK", "", null, null);
   },
   LoadSettings: function()
   {
      if(localStorage.WHESettings !== undefined)
      {
         WHE.settings = JSON.parse(localStorage.WHESettings);
      }
   },
   SaveSettings: function()
   {
      localStorage.WHESettings = JSON.stringify(WHE.settings);
   },
   FinaliseInit: function()
   {
      WHE.AddLog('Finalise init');
      let MO_EditPanel = new MutationObserver(WHE.EditPanelChanged);
      MO_EditPanel.observe(document.querySelector('#edit-panel'),{childList: true, subtree: true});

      WHE.LoadSettings();
      window.addEventListener("beforeunload", WHE.SaveSettings, false);

      WHE.AddInterceptor();
      if(Release.version != WHE.settings.lastVer)
      {
         WHE.ShowUpgradeNotes();
         WHE.settings.lastVer = Release.version;
      }

      ////WHE.showDebugOutput = false;
   },
   ArrayPushUnique: function(arr, obj)
   {
      let doPush = true;
      let sObj = JSON.stringify(obj);

      for(const i of arr)
      {
         if(JSON.stringify(i) == sObj)
         {
            doPush = false;
            break;
         }
      }
      if(doPush === true)
      {
         arr.push(obj);
      }
      return arr;
   },
   ParseHistoryResponse: function(body)
   {
      WHE.AddLog('history response received...');

      if(W.selectionManager.getSelectedDataModelObjects().length === 1)
      {
         WHE.enhanceHistoryItemID = W.selectionManager.getSelectedDataModelObjects()[0].attributes.id;
         WHE.AddLog('itemID = '+WHE.enhanceHistoryItemID);

         for(const t of body.streets.objects)
         {
            WHE.ArrayPushUnique(WHE.itemHistoryDetails.streets, t);
         }
         for(const t of body.cities.objects)
         {
            WHE.ArrayPushUnique(WHE.itemHistoryDetails.cities, t);
         }
         for(const t of body.states.objects)
         {
            WHE.ArrayPushUnique(WHE.itemHistoryDetails.states, t);
         }
         for(const t of body.countries.objects)
         {
            WHE.ArrayPushUnique(WHE.itemHistoryDetails.countries, t);
         }
         for(const t of body.users.objects)
         {
            WHE.ArrayPushUnique(WHE.itemHistoryDetails.users, t);
         }      
         for(const t of body.transactions.objects)
         {
            WHE.itemHistoryDetails.transactions.push(t);
         }
         WHE.UpdateHistoryEntries();
      }
      else
      {
         WHE.AddLog('selected item count != 1, which is odd...');
         WHE.enhanceHistoryItemID = null;
      }
   },
   HandleListEntry: function(className)
   {
      const handledClasses = ['ca-images', 'ca-categories', 'ca-services', 'segment-related-objects'];
      let retval = false;

      for(let i = 0; i < handledClasses.length; ++ i)
      {
         if(className.indexOf(handledClasses[i]) !== -1)
         {
            retval = true;
            break;
         }
      }
      return retval;
   },
   ProcessNativeHistoryEntry: function(lObj, listEntries)
   {
      let TCA = lObj.querySelectorAll('.tx-changed-attribute');
      if(TCA.length > 0)
      {
         for(let i = 0; i < TCA.length; ++i)
         {
            let taObj = WHE.HistoryEntryToAdjust(TCA[i]);
            if(taObj !== null)
            {
               let cn = taObj.childNodes[1];
               if(WHE.HandleListEntry(cn.className) === true)
               {
                  listEntries.push(cn);
               }
            }
         }
      }
      else
      {
         let newLObj = WHE.HistoryEntryToAdjust(lObj);
         if(newLObj !== null)
         {
            if(newLObj.querySelectorAll('.ca-name').length > 0)
            {
               // Keep the caption part of any entry where it's stored within the tx-changed
               // element rather than outside of it - this seems to be done for anything
               // where the caption is something other than "Allowed" or "Disallowed"
               let taObj = WHE.HistoryEntryToAdjust(newLObj);
               if(taObj !== null)
               {
                  let cn = newLObj.childNodes[1];
                  if(WHE.HandleListEntry(cn.className) === true)
                  {
                     listEntries.push(cn);
                  }
               }
            }
            else
            {
               // For entries where the caption is outside the element, we can overwrite the
               // whole thing...
               if(WHE.HandleListEntry(newLObj.className) === true)
               {
                  listEntries.push(newLObj);
               }
            }
         }
      }
   },
   UpdateHistoryEntries: function()
   {
      let heContainer = document.querySelector('.historyContent');
      if(heContainer !== null)
      {
         if(heContainer.querySelector('.history-loader') !== null)
         {
            window.setTimeout(WHE.UpdateHistoryEntries, 10);
            return;
         }

         if(heContainer.style.display === "")
         {
            let historyLength = WHE.itemHistoryDetails.transactions.length;
            WHE.AddLog("found "+historyLength+" history entries");
            let tHTML;
            let hideBits = document.querySelector('#whe_cbHideHistoryBits').checked;
            if(hideBits !== WHE.settings.hideBits)
            {
               WHE.settings.hideBits = hideBits;
               WHE.SaveSettings();
            }
            let objType = W.selectionManager.getSelectedDataModelObjects()[0]?.type;

            let goAround = false;
            for(let i = 0; i < historyLength; ++i)
            {
               let heEntry = heContainer.querySelectorAll('.tx-item')[i];
               let heToggle = heEntry.querySelector('.tx-item-toggle-icon');
               if(heToggle !== null)
               {
                  if(heEntry.classList.contains('tx-item-expanded') === false)
                  {
                     heToggle.click();
                     goAround = true;
                  }
               }
            }
            if(goAround === true)
            {
               window.setTimeout(WHE.UpdateHistoryEntries, 10);
               return;
            }

            for(let i = 0; i < historyLength; ++i)
            {
               let heEntry = heContainer.querySelectorAll('.tx-item')[i];
               let historyEntry = heEntry.querySelector('.tx-item-content');
               if(historyEntry !== null)
               {
                  let listEntries = [];
                  let listEntryIdx = 0;

                  if((objType === 'segment') || (objType === 'venue'))
                  {
                     let lObj = historyEntry.querySelector('.main-changes-list');
                     if(lObj !== null)
                     {
                        WHE.ProcessNativeHistoryEntry(lObj, listEntries);
                     }
                     lObj = historyEntry.querySelector('.segment-related-objects');
                     if(lObj !== null)
                     {
                        WHE.ProcessNativeHistoryEntry(lObj, listEntries);
                     }
                  }

                  for(let rol of historyEntry.querySelectorAll('.related-objects-list'))
                  {
                     for(let lObj of rol.getElementsByTagName('wz-caption'))
                     {
                        WHE.ProcessNativeHistoryEntry(lObj, listEntries);
                     }
                  }

                  let hObj = WHE.itemHistoryDetails.transactions[i];
                  let uIdx = -1;
                  let aIdx = -1;
                  let dIdx = -1;
                  let s;
                  for(s = 0; s < hObj.objects.length; ++s)
                  {
                     if((uIdx == -1) && (hObj.objects[s].actionType == "UPDATE"))
                     {
                        uIdx = s;
                     }
                     if((aIdx == -1) && (hObj.objects[s].actionType == "ADD"))
                     {
                        aIdx = s;
                     }
                     if((dIdx == -1) && (hObj.objects[s].actionType == "DELETE"))
                     {
                        dIdx = s;
                     }
                  }
                  let tList = [];
                  if(uIdx != -1)
                  {
                     for(s = 0; s < hObj.objects.length; ++s)
                     {
                        if(hObj.objects[s].actionType == "UPDATE")
                        {
                           tList.push(hObj.objects[s]);
                        }
                     }
                  }
                  if((aIdx != -1) && (aIdx < dIdx))
                  {
                     for(s = 0; s < hObj.objects.length; ++s)
                     {
                        if(hObj.objects[s].actionType == "ADD")
                        {
                           tList.push(hObj.objects[s]);
                        }
                     }
                     for(s = 0; s < hObj.objects.length; ++s)
                     {
                        if(hObj.objects[s].actionType == "DELETE")
                        {
                           tList.push(hObj.objects[s]);
                        }
                     }
                  }
                  else
                  {
                     for(s = 0; s < hObj.objects.length; ++s)
                     {
                        if(hObj.objects[s].actionType == "DELETE")
                        {
                           tList.push(hObj.objects[s]);
                        }
                     }
                     for(s = 0; s < hObj.objects.length; ++s)
                     {
                        if(hObj.objects[s].actionType == "ADD")
                        {
                           tList.push(hObj.objects[s]);
                        }
                     }
                  }

                  let hiddenBits = 0;
                  for(let k = 0; k < tList.length; ++k)
                  {
                     tHTML = '';
                     let tObj = tList[k];
                     let oType = tObj.objectType;

                     if(oType === "nodeConnection")
                     {
                        tHTML += WHE.ParseHistoryObject_NodeConnection(tObj);
                     }
                     else if(oType === "roadClosure")
                     {
                        tHTML += WHE.ParseHistoryObject_RoadClosure(tObj);
                     }
                     else if(oType === "venue")
                     {
                        tHTML += WHE.ParseHistoryObject_Venue(tObj);
                     }
                     else if(oType === "venueUpdateRequest")
                     {
                        tHTML += WHE.ParseHistoryObject_VenueUpdateRequest(tObj);
                     }
                     else if(oType === "segment")
                     {
                        tHTML += WHE.ParseHistoryObject_Segment(tObj);
                     }

                     if(listEntries.length > listEntryIdx)
                     {
                        if(tHTML !== '')
                        {
                           if(listEntries[listEntryIdx] !== undefined)
                           {
                              listEntries[listEntryIdx].innerHTML = WHE.ModifyHTML(tHTML + '<br>');
                              
                              if((oType === 'roadClosure') || (oType === 'venue'))
                              {
                                 let pObj = listEntries[listEntryIdx];
                                 WHE.FindCard(pObj, hideBits);
                              }
                              else if(oType === 'venueUpdateRequest')
                              {
                                 // Hide/unhide the individual transaction entries for rejected PURs
                                 if(tObj?.oldValue?.approve === false)
                                 {
                                    let pObj = listEntries[listEntryIdx];   
                                    if(hideBits === true)
                                    {
                                       pObj.style.display = "none";
                                    }
                                    else
                                    {
                                       pObj.style.display = "";
                                    }
                                    ++hiddenBits;
                                 }
                              }
                           }
                           ++listEntryIdx;
                        }
                     }
                  }

                  // Venue entry cards may contain multiple transactions, so we only want to hide/restore
                  // them if all of the transaction entries have also been hidden/restored (i.e. whenever
                  // tList.length = hiddenBits)... 
                  if(objType === "venue")
                  {
                     if(tList.length === hiddenBits)
                     {
                        // -1 as we've already incremented listEntryIdx...
                        let pObj = listEntries[listEntryIdx - 1];
                        WHE.FindCard(pObj, hideBits);
                     }
                  }
               }
            }
         }
      }
   },
   FindCard: function(pObj, hideBits)
   {
      let foundCard = false;
      while(foundCard === false)
      {
         pObj = pObj.parentElement;
         foundCard = (pObj.tagName == "WZ-CARD");
      }
      if(hideBits === true)
      {
         pObj.style.display = "none";
      }
      else
      {
         pObj.style.display = "";
      }
   },
   AddInterceptor: function()
   {
      WHE.AddLog('Adding interceptor functions...');

      // intercept fetch() so we can detect when requests are made for object history details, and
      // grab copies of the responses - this both enables us to know when the history is being
      // viewed (requests are only made when the user clicks View History...), and avoids the need
      // to perform our own requests to get the same data (as URO+ used to do in the original
      // iteration of this code...)

      // https://stackoverflow.com/questions/45425169/intercept-fetch-api-requests-and-responses-in-javascript  
      const origFetch = window.fetch;
      window.fetch = async (...args) => 
      {
         let [resource, config ] = args;

         // as we're handling everything that goes via fetch(), we let all requests through as-is,
         // except for the ones related to history fetches - these requests always include a
         // reference to "ElementHistory" in their URLs...
         if(resource.url !== undefined)
         {
            if(resource.url.indexOf('ElementHistory') != -1)
            {
               WHE.AddLog('object history being viewed...');
               if(resource.url.indexOf('&till=') == -1)
               {
                  WHE.AddLog('first history entries requested, resetting tracking vars...');
                  WHE.enhanceHistoryItemID = null;
                  WHE.itemHistoryDetails = {streets: [], cities: [], states: [], countries: [], users: [], transactions: []};
               }
            }
         }

         const response = await origFetch(resource, config);

         // we also let all responses through as-is, except for the ones related to history fetches...
         if(response.url !== undefined)
         {
            if(response.url.indexOf('ElementHistory') != -1)
            {
               response
               .clone()
               .json()
               .then(body => WHE.ParseHistoryResponse(body));
            }
         }

         return response;
      };
   },
   Initialise: function()
   {
      if(document.getElementsByClassName("sandbox").length > 0)
      {
         WHE.AddLog('WME practice mode detected, script is disabled...');
         return;
      }

      if(document.location.href.indexOf('user/') !== -1)
      {
         WHE.AddLog('User profile page detected, script is disabled...');
         return;
      }

      if ((typeof W === 'object') && (W.userscripts?.state?.isReady))
      {
         WHE.AddLog('Initialising now...');
         WHE.FinaliseInit();
      } 
      else 
      {
         WHE.AddLog('Initialising on wme-ready...');
         document.addEventListener("wme-ready", WHE.FinaliseInit, {once: true});
      }
   }
};
WHE.Initialise();