// ==UserScript== // @name WME Segment Shift Utility // @namespace https://github.com/kid4rm90s/Segment-Shift-Utility // @version 2025.03.22.01 // @description Utility for shifting street segments in WME without disconnecting nodes // @include /^https:\/\/(www|beta)\.waze\.com\/(?!user\/)(.{2,6}\/)?editor\/?.*$/* // @author kid4rm90s // @connect raw.githubusercontent.com // @connect github.com // @grant GM_xmlhttpRequest // @require https://greasyfork.org/scripts/24851-wazewrap/code/WazeWrap.js // @license MIT // @downloadURL none // ==/UserScript== /*Scripts modified from WME RA Util (https://greasyfork.org/en/scripts/23616-wme-ra-util) orgianl author: JustinS83 Waze*/ (function() { let sdkVersion = ""; unsafeWindow.SDK_INITIALIZED.then(() => { let sdk = unsafeWindow.getWmeSdk({ scriptId: "wme-ss-util", scriptName: "WME Segment Shift Utility", }); sdkVersion = sdk.getSDKVersion() }); var SSUtilWindow = null; var UpdateSegmentGeometry; var MoveNode, MultiAction; var drc_layer; let wEvents; const SSUTIL_VERSION = `${GM_info.script.version}`; //const SCRIPT_NAME = GM_info.script.name; const GF_LINK = 'https://github.com/kid4rm90s/Segment-Shift-Utility/blob/master/WME-Segment-Shift-Utility.user.js'; const DOWNLOAD_URL = 'https://raw.githubusercontent.com/kid4rm90s/Segment-Shift-Utility/master/WME-Segment-Shift-Utility.user.js'; //var totalActions = 0; var _settings; const updateMessage = "Minor changes:

Now it is able to alert the distance when the segment is shifted.

Thanks for the update!"; function bootstrap(tries = 1) { if (W.map && W.model && require && WazeWrap.Ready){ startScriptUpdateMonitor(); init(); } else if (tries < 1000) setTimeout(function () {bootstrap(++tries);}, 200); } bootstrap(); function startScriptUpdateMonitor() { let updateMonitor; try { updateMonitor = new WazeWrap.Alerts.ScriptUpdateMonitor(GM_info.script.name, GM_info.script.version, DOWNLOAD_URL, GM_xmlhttpRequest, DOWNLOAD_URL); updateMonitor.start(); } catch (ex) { // Report, but don't stop if ScriptUpdateMonitor fails. console.error('WME SSUtil:', ex); } } function init(){ injectCss(); UpdateSegmentGeometry = require('Waze/Action/UpdateSegmentGeometry'); MoveNode = require("Waze/Action/MoveNode"); MultiAction = require("Waze/Action/MultiAction"); console.log("SS UTIL"); console.log(GM_info.script); if(W.map.events) wEvents = W.map.events; else wEvents = W.map.getMapEventsListener(); SSUtilWindow = document.createElement('div'); SSUtilWindow.id = "SSUtilWindow"; SSUtilWindow.style.position = 'fixed'; SSUtilWindow.style.visibility = 'hidden'; SSUtilWindow.style.top = '15%'; SSUtilWindow.style.left = '25%'; SSUtilWindow.style.width = '250px'; SSUtilWindow.style.zIndex = 100; SSUtilWindow.style.backgroundColor = '#FFFFFE'; SSUtilWindow.style.borderWidth = '0px'; SSUtilWindow.style.borderStyle = 'solid'; SSUtilWindow.style.borderRadius = '10px'; SSUtilWindow.style.boxShadow = '5px 5px 10px Silver'; SSUtilWindow.style.padding = '4px'; var alertsHTML = ''; // start collapse // I put it al the beginning alertsHTML += '
'; //***************** Disconnect Nodes Checkbox ************************** alertsHTML += '

 Disconnect Nodes

'; //***************** Shift Amount ************************** // Define BOX alertsHTML += '
'; alertsHTML += 'Shift amount
Metre(s)'; // Shift amount controls alertsHTML += '
'; //Single Shift Up Button alertsHTML += ''; alertsHTML += ' '; alertsHTML += ''; alertsHTML += '
'; //Single Shift Left Button alertsHTML += ''; alertsHTML += ' '; alertsHTML += ''; alertsHTML += ''; //Single Shift Right Button alertsHTML += ''; alertsHTML += ' '; alertsHTML += ''; alertsHTML += '
'; //Single Shift Down Button alertsHTML += ''; alertsHTML += ' '; alertsHTML += ''; alertsHTML += ''; alertsHTML += '
'; SSUtilWindow.innerHTML = alertsHTML; document.body.appendChild(SSUtilWindow); $('#SSShiftLeftBtn').click(SSShiftLeftBtnClick); $('#SSShiftRightBtn').click(SSShiftRightBtnClick); $('#SSShiftUpBtn').click(SSShiftUpBtnClick); $('#SSShiftDownBtn').click(SSShiftDownBtnClick); $('#shiftAmount').keypress(function(event) { if ((event.which != 46 || $(this).val().indexOf('.') != -1) && (event.which < 48 || event.which > 57)) event.preventDefault(); }); $('#collapserLink1').click(function(){ $("#divWrappers1").slideToggle("fast"); if($('#collapser1').attr('class') == "fa fa-caret-square-o-down"){ $("#collapser1").removeClass("fa-caret-square-o-down"); $("#collapser1").addClass("fa-caret-square-o-up"); } else{ $("#collapser1").removeClass("fa-caret-square-o-up"); $("#collapser1").addClass("fa-caret-square-o-down"); } saveSettingsToStorage(); }); W.selectionManager.events.register("selectionchanged", null, checkDisplayTool); var loadedSettings = $.parseJSON(localStorage.getItem("WME_SSUtil")); var defaultSettings = { divTop: "15%", divLeft: "25%", Expanded: true, DisconnectNodes: false // default to false (normal behavior) }; _settings = loadedSettings ? loadedSettings : defaultSettings; $('#SSUtilWindow').css('left', _settings.divLeft); $('#SSUtilWindow').css('top', _settings.divTop); $('#chkDisconnectNodes').prop('checked', _settings.DisconnectNodes); // Set checkbox state from settings if(!_settings.Expanded){ // $("#divWrappers1").removeClass("in"); // $("#divWrappers1").addClass("collapse"); $("#divWrappers1").hide(); $("#collapser1").removeClass("fa-caret-square-o-up"); $("#collapser1").addClass("fa-caret-square-o-down"); } WazeWrap.Interface.ShowScriptUpdate("WME SS Util", GM_info.script.version, updateMessage, "https://raw.githubusercontent.com/kid4rm90s/Segment-Shift-Utility/main/WME-Segment-Shift-Utility.user.js", "https://github.com/kid4rm90s/Segment-Shift-Utility"); } function saveSettingsToStorage() { if (localStorage) { var settings = { divTop: "15%", divLeft: "25%", Expanded: true, DisconnectNodes: false // default value }; settings.divLeft = $('#SSUtilWindow').css('left'); settings.divTop = $('#SSUtilWindow').css('top'); settings.Expanded = $("#collapser1").attr('class').indexOf("fa-caret-square-o-up") > -1; settings.DisconnectNodes = $('#chkDisconnectNodes').is(':checked'); // Save checkbox state localStorage.setItem("WME_SSUtil", JSON.stringify(settings)); } } function checkDisplayTool(){ if(WazeWrap.hasSelectedFeatures() && WazeWrap.getSelectedFeatures()[0].WW.getType() === 'segment'){ if(WazeWrap.getSelectedFeatures().length === 0) $('#SSUtilWindow').css({'visibility': 'hidden'}); else{ $('#SSUtilWindow').css({'visibility': 'visible'}); if(typeof jQuery.ui !== 'undefined') $('#SSUtilWindow' ).draggable({ //Gotta nuke the height setting the dragging inserts otherwise the panel cannot collapse stop: function(event, ui) { $('#SSUtilWindow').css("height", ""); saveSettingsToStorage(); } }); } } else{ $('#SSUtilWindow').css({'visibility': 'hidden'}); if(typeof jQuery.ui !== 'undefined') $('#SSUtilWindow' ).draggable({ stop: function(event, ui) { $('#SSUtilWindow').css("height", ""); saveSettingsToStorage(); } }); } } function ShiftSegmentNodesLat(latOffset) { var multiaction = new MultiAction(); var selectedFeatures = WazeWrap.getSelectedFeatures(); var disconnectNodes = $('#chkDisconnectNodes').is(':checked'); // Checkbox state if (!disconnectNodes) { // Normal behavior: Shift segments and connected nodes var uniqueNodes = new Set(); // 1. Collect Unique Nodes from Selected Segments for (let i = 0; i < selectedFeatures.length; i++) { var segObj = W.model.segments.getObjectById(selectedFeatures[i].WW.getObjectModel().attributes.id); if (!segObj) continue; uniqueNodes.add(segObj.attributes.fromNodeID); uniqueNodes.add(segObj.attributes.toNodeID); } // 2. Shift Unique Nodes for (let nodeId of uniqueNodes) { var node = W.model.nodes.objects[nodeId]; if (!node) continue; var newNodeGeometry = structuredClone(node.attributes.geoJSONGeometry); newNodeGeometry.coordinates[1] += latOffset; var connectedSegObjs = {}; var emptyObj = {}; for (let j = 0; j < node.attributes.segIDs.length; j++) { var segid = node.attributes.segIDs[j]; connectedSegObjs[segid] = structuredClone(W.model.segments.getObjectById(segid).attributes.geoJSONGeometry); } multiaction.doSubAction(W.model, new MoveNode(node, node.attributes.geoJSONGeometry, newNodeGeometry, connectedSegObjs, emptyObj)); } } // else - if disconnectNodes is checked, we skip node shifting // 3. Update Segment Geometries (always update segment geometry) for (let i = 0; i < selectedFeatures.length; i++) { var segObj = W.model.segments.getObjectById(selectedFeatures[i].WW.getObjectModel().attributes.id); if (!segObj) continue; var newGeometry = structuredClone(segObj.attributes.geoJSONGeometry); var originalLength = segObj.attributes.geoJSONGeometry.coordinates.length; if (disconnectNodes) { // Shift all points when disconnecting for (let j = 0; j < originalLength; j++) { newGeometry.coordinates[j][1] += latOffset; } } else { // Shift only inner points when not disconnecting (normal behavior) for (let j = 1; j < originalLength - 1; j++) { newGeometry.coordinates[j][1] += latOffset; } } multiaction.doSubAction(W.model, new UpdateSegmentGeometry(segObj, segObj.attributes.geoJSONGeometry, newGeometry)); } W.model.actionManager.add(multiaction); } function ShiftSegmentsNodesLong(longOffset) { var multiaction = new MultiAction(); var selectedFeatures = WazeWrap.getSelectedFeatures(); var disconnectNodes = $('#chkDisconnectNodes').is(':checked'); // Checkbox state if (!disconnectNodes) { // Normal behavior: Shift segments and connected nodes var uniqueNodes = new Set(); // 1. Collect Unique Nodes from Selected Segments for (let i = 0; i < selectedFeatures.length; i++) { var segObj = W.model.segments.getObjectById(selectedFeatures[i].WW.getObjectModel().attributes.id); if (!segObj) continue; uniqueNodes.add(segObj.attributes.fromNodeID); uniqueNodes.add(segObj.attributes.toNodeID); } // 2. Shift Unique Nodes for (let nodeId of uniqueNodes) { var node = W.model.nodes.objects[nodeId]; if (!node) continue; var newNodeGeometry = structuredClone(node.attributes.geoJSONGeometry); newNodeGeometry.coordinates[0] += longOffset; var connectedSegObjs = {}; var emptyObj = {}; for (let j = 0; j < node.attributes.segIDs.length; j++) { var segid = node.attributes.segIDs[j]; connectedSegObjs[segid] = structuredClone(W.model.segments.getObjectById(segid).attributes.geoJSONGeometry); } multiaction.doSubAction(W.model, new MoveNode(node, node.attributes.geoJSONGeometry, newNodeGeometry, connectedSegObjs, emptyObj)); } } // else - if disconnectNodes is checked, we skip node shifting // 3. Update Segment Geometries (always update segment geometry) for (let i = 0; i < selectedFeatures.length; i++) { var segObj = W.model.segments.getObjectById(selectedFeatures[i].WW.getObjectModel().attributes.id); if (!segObj) continue; var newGeometry = structuredClone(segObj.attributes.geoJSONGeometry); var originalLength = segObj.attributes.geoJSONGeometry.coordinates.length; if (disconnectNodes) { // Shift all points when disconnecting for (let j = 0; j < originalLength; j++) { newGeometry.coordinates[j][0] += longOffset; } } else { // Shift only inner points when not disconnecting (normal behavior) for (let j = 1; j < originalLength - 1; j++) { newGeometry.coordinates[j][0] += longOffset; } } multiaction.doSubAction(W.model, new UpdateSegmentGeometry(segObj, segObj.attributes.geoJSONGeometry, newGeometry)); } W.model.actionManager.add(multiaction); } //Left function SSShiftLeftBtnClick(e){ e.stopPropagation(); var segObj = WazeWrap.getSelectedFeatures()[0]; if (!segObj) return; var convertedCoords = WazeWrap.Geometry.ConvertTo4326(segObj.WW.getAttributes().geoJSONGeometry.coordinates[0][0], segObj.WW.getAttributes().geoJSONGeometry.coordinates[0][1]); var gpsOffsetAmount = WazeWrap.Geometry.CalculateLongOffsetGPS(-$('#shiftAmount').val(), convertedCoords.lon, convertedCoords.lat); ShiftSegmentsNodesLong(gpsOffsetAmount); WazeWrap.Alerts.info('WME Segment Shift Utility', `The segments are shifted by ${$('#shiftAmount').val()} Metres to the left.`, false, false, 2000); } //Right function SSShiftRightBtnClick(e){ e.stopPropagation(); var segObj = WazeWrap.getSelectedFeatures()[0]; if (!segObj) return; var convertedCoords = WazeWrap.Geometry.ConvertTo4326(segObj.WW.getAttributes().geoJSONGeometry.coordinates[0][0], segObj.WW.getAttributes().geoJSONGeometry.coordinates[0][1]); var gpsOffsetAmount = WazeWrap.Geometry.CalculateLongOffsetGPS($('#shiftAmount').val(), convertedCoords.lon, convertedCoords.lat); ShiftSegmentsNodesLong(gpsOffsetAmount); WazeWrap.Alerts.info('WME Segment Shift Utility', `The segments are shifted by ${$('#shiftAmount').val()} Metres to the right.`, false, false, 2000); } //Up function SSShiftUpBtnClick(e){ e.stopPropagation(); var segObj = WazeWrap.getSelectedFeatures()[0]; if (!segObj) return; var gpsOffsetAmount = WazeWrap.Geometry.CalculateLatOffsetGPS($('#shiftAmount').val(), WazeWrap.Geometry.ConvertTo4326(segObj.WW.getAttributes().geoJSONGeometry.coordinates[0][0], segObj.WW.getAttributes().geoJSONGeometry.coordinates[0][1])); ShiftSegmentNodesLat(gpsOffsetAmount); WazeWrap.Alerts.info('WME Segment Shift Utility', `The segments are shifted by ${$('#shiftAmount').val()} Metres to the up.`, false, false, 2000); } //Down function SSShiftDownBtnClick(e){ e.stopPropagation(); var segObj = WazeWrap.getSelectedFeatures()[0]; if (!segObj) return; var gpsOffsetAmount = WazeWrap.Geometry.CalculateLatOffsetGPS(-$('#shiftAmount').val(), WazeWrap.Geometry.ConvertTo4326(segObj.WW.getAttributes().geoJSONGeometry.coordinates[0][0], segObj.WW.getAttributes().geoJSONGeometry.coordinates[0][1])); ShiftSegmentNodesLat(gpsOffsetAmount); WazeWrap.Alerts.info('WME Segment Shift Utility', `The segments are shifted by ${$('#shiftAmount').val()} Metres to the down.`, false, false, 2000); } function injectCss() { var css = [ '.btnMoveNode {width=25px; height=25px; background-color:#92C3D3; cursor:pointer; padding:5px; font-size:14px; border:thin outset black; border-style:solid; border-width: 1px;border-radius:50%; -moz-border-radius:50%; -webkit-border-radius:50%; box-shadow:inset 0px 0px 20px -14px rgba(0,0,0,1); -moz-box-shadow:inset 0px 0px 20px -14px rgba(0,0,0,1); -webkit-box-shadow: inset 0px 0px 20px -14px rgba(0,0,0,1);}', '.btnRotate { width=45px; height=45px; background-color:#92C3D3; cursor:pointer; padding: 5px; font-size:14px; border:thin outset black; border-style:solid; border-width: 1px;border-radius: 50%;-moz-border-radius: 50%;-webkit-border-radius: 50%;box-shadow: inset 0px 0px 20px -14px rgba(0,0,0,1);-moz-box-shadow: inset 0px 0px 20px -14px rgba(0,0,0,1);-webkit-box-shadow: inset 0px 0px 20px -14px rgba(0,0,0,1);}' ].join(' '); $('').appendTo('head'); } })();