// ==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.5
// ==/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: 8 */
const WHE =
{
showDebugOutput: false,
enhanceHistoryItemID: null,
enhanceHistoryItemType: null,
itemHistoryDetails: null,
itemHistoryLoaded: false,
prevWazeBitsPresent: null,
wazeBitsPresent: 0,
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 += ' Reason: ' + tbrObj.description + '<br>';
}
if(tbrObj.timeFrames.length > 0)
{
retval += ' Dates: ';
if(tbrObj.timeFrames[0].startDate === null)
{
retval += 'all dates';
}
else
{
retval += tbrObj.timeFrames[0].startDate + ' to ' + tbrObj.timeFrames[0].endDate;
}
retval += '<br>';
retval += ' Days: ';
if(tbrObj.timeFrames[0].weekdays & (1<<0)) retval += 'S';
else retval += '-';
if(tbrObj.timeFrames[0].weekdays & (1<<1)) retval += 'M';
else retval += '-';
if(tbrObj.timeFrames[0].weekdays & (1<<2)) retval += 'T';
else retval += '-';
if(tbrObj.timeFrames[0].weekdays & (1<<3)) retval += 'W';
else retval += '-';
if(tbrObj.timeFrames[0].weekdays & (1<<4)) retval += 'T';
else retval += '-';
if(tbrObj.timeFrames[0].weekdays & (1<<5)) retval += 'F';
else retval += '-';
if(tbrObj.timeFrames[0].weekdays & (1<<6)) retval += 'S';
else retval += '-';
retval += '<br>';
retval += ' 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 += ' Vehicle types prohibited:<br>';
for(let i=0; i<vtLength; i++)
{
retval += ' '+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 += ' Vehicle types allowed:<br>';
for(let i=0; i<vtLength; i++)
{
retval += ' '+WHE.GetVehicleDescription(tbrObj.driveProfiles.FREE[0].vehicleTypes[i])+'<br>';
}
}
}
else if(tbrObj.defaultType === "BLOCKED")
{
retval += ' Blocked for all vehicle types<br>';
}
if(tbrObj.defaultType === "DIFFICULT")
{
retval += ' 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;
},
ParseHistoryObject_Venue: function(tObj)
{
let tHTML = '';
//// Placeholder for now...
//let aType = tObj.actionType;
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 += " ";
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 += " ";
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?"
}
hhbToggle.innerHTML = "<input type='checkbox' id='whe_cbHideHistoryBits' />" + hhbText;
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();
}
},
FinaliseInit: function()
{
let MO_EditPanel = new MutationObserver(WHE.EditPanelChanged);
MO_EditPanel.observe(document.querySelector('#edit-panel'),{childList: true, subtree: true});
WHE.AddInterceptor();
},
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;
}
},
ProcessNativeHistoryEntry: function(lObj, listEntries)
{
let newLObj = WHE.HistoryEntryToAdjust(lObj);
if(newLObj !== null)
{
if(newLObj.getElementsByClassName('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"
listEntries.push(newLObj.childNodes[1]);
}
else
{
// For entries where the caption is outside the element, we can overwrite the
// whole thing...
listEntries.push(newLObj);
}
}
},
UpdateHistoryEntries: function()
{
if(document.getElementsByClassName('historyContent').length === 1)
{
let heContainer = document.getElementsByClassName('historyContent')[0];
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;
let objType = W.selectionManager.getSelectedDataModelObjects()[0]?.type;
for(let i = 0; i < historyLength; ++i)
{
let heEntry = heContainer.getElementsByClassName('tx-item')[i];
if(heEntry.getElementsByClassName('tx-item-header')[0].getElementsByClassName('tx-item-toggle-icon').length == 1)
{
if(heEntry.classList.contains('tx-item-expanded') === false)
{
heEntry.getElementsByClassName('tx-item-header')[0].getElementsByClassName('tx-item-toggle-icon')[0].click();
}
let historyEntry = heEntry.getElementsByClassName('tx-item-content')[0];
let listEntries = [];
let listEntryIdx = 0;
if(objType === 'segment')
{
if(historyEntry.getElementsByClassName('main-changes-list').length > 0)
{
let lObj = historyEntry.getElementsByClassName('main-changes-list')[0];
WHE.ProcessNativeHistoryEntry(lObj, listEntries);
}
}
if(historyEntry.getElementsByClassName('related-objects-list').length > 0)
{
for(let rol of historyEntry.getElementsByClassName('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]);
}
}
}
if(WHE.showDebugOutput === true)
{
console.debug(tList);
console.debug(listEntries);
}
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')
{
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 === 'undefined')
{
window.setTimeout(WHE.Initialise, 100);
return;
}
if (W.userscripts?.state?.isReady)
{
WHE.FinaliseInit();
}
else
{
document.addEventListener("wme-ready", WHE.FinaliseInit, {once: true});
}
}
};
WHE.Initialise();