您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
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.2 // ==/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].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) { for(let j = 0; j < vObj.changedVenue.categories.length; ++j) { tHTML += '<br> '+vObj.changedVenue.categories[j]; } } } if(vObj.changedVenue.entryExitPoints !== undefined) { tHTML += '<br>Entry/exit point change'; } if(vObj.changedVenue.description !== undefined) { tHTML += '<br>Description change'; if(showExtendedDetails === true) { } } if(vObj.changedVenue.name !== undefined) { tHTML += '<br>Name change'; if(showExtendedDetails === true) { } } 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) { } } 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) { } } } } 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() { if((document.querySelector('.toggleHistory') != null) && (document.querySelector('#wheHideClosures') == null)) { let hcToggle = document.createElement('label'); let thBCR = document.querySelector('.toggleHistory').getBoundingClientRect(); if(thBCR.width != 0) { hcToggle.id = "wheHideClosures"; hcToggle.style.position = "relative"; hcToggle.style.left = thBCR.width + 16 + "px"; hcToggle.style.width = "50%"; hcToggle.innerHTML = "<input type='checkbox' id='whe_cbHideClosures' />Hide closures?"; document.querySelector(".additional-attributes").insertBefore(hcToggle, null); // Once the hide closures toggle has been added to the DOM, we can adjust its vertical position based on how // its insertion has affected the size/position of its container element as well as the view history button // we want to align against... let aaBCR = document.querySelector(".additional-attributes").getBoundingClientRect(); let srBCR = document.querySelector(".toggleHistory").shadowRoot.querySelector('.wz-button').getBoundingClientRect(); hcToggle.style.top = srBCR.top - aaBCR.bottom + 26 + "px"; document.querySelector('#whe_cbHideClosures').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.getSelectedFeatures().length === 1) { WHE.enhanceHistoryItemID = W.selectionManager.getSelectedFeatures()[0].model.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 = heContainer.getElementsByClassName('tx-item').length; WHE.AddLog("found "+historyLength+" history entries"); let tHTML; let hideClosures = document.querySelector('#whe_cbHideClosures').checked; 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(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); } 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 foundCard = false let pObj = listEntries[listEntryIdx]; while(foundCard === false) { pObj = pObj.parentElement; foundCard = (pObj.tagName == "WZ-CARD"); } if(hideClosures === true) { pObj.style.display="none"; } else { pObj.style.display=""; } } } ++listEntryIdx; } } } } } } } }, 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(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();