// ==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');
}
})();