WME Segment Shift Utility

Utility for shifting street segments in WME without disconnecting nodes

当前为 2025-05-25 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name WME Segment Shift Utility
  3. // @namespace https://github.com/kid4rm90s/Segment-Shift-Utility
  4. // @version 2025.03.22.01
  5. // @description Utility for shifting street segments in WME without disconnecting nodes
  6. // @include /^https:\/\/(www|beta)\.waze\.com\/(?!user\/)(.{2,6}\/)?editor\/?.*$/*
  7. // @author kid4rm90s
  8. // @connect raw.githubusercontent.com
  9. // @connect github.com
  10. // @grant GM_xmlhttpRequest
  11. // @require https://greasyfork.org/scripts/24851-wazewrap/code/WazeWrap.js
  12. // @license MIT
  13. // ==/UserScript==
  14.  
  15. /*Scripts modified from WME RA Util (https://greasyfork.org/en/scripts/23616-wme-ra-util)
  16. orgianl author: JustinS83 Waze*/
  17. (function() {
  18. let sdkVersion = "";
  19. unsafeWindow.SDK_INITIALIZED.then(() => {
  20. let sdk = unsafeWindow.getWmeSdk({
  21. scriptId: "wme-ss-util",
  22. scriptName: "WME Segment Shift Utility",
  23. });
  24. sdkVersion = sdk.getSDKVersion()
  25. });
  26. var SSUtilWindow = null;
  27. var UpdateSegmentGeometry;
  28. var MoveNode, MultiAction;
  29. var drc_layer;
  30. let wEvents;
  31. const SSUTIL_VERSION = `${GM_info.script.version}`;
  32. //const SCRIPT_NAME = GM_info.script.name;
  33. const GF_LINK = 'https://github.com/kid4rm90s/Segment-Shift-Utility/blob/master/WME-Segment-Shift-Utility.user.js';
  34. const DOWNLOAD_URL = 'https://raw.githubusercontent.com/kid4rm90s/Segment-Shift-Utility/master/WME-Segment-Shift-Utility.user.js';
  35. //var totalActions = 0;
  36. var _settings;
  37. const updateMessage = "Minor changes:<br><br>Now it is able to alert the distance when the segment is shifted.<br><br>Thanks for the update!";
  38.  
  39. function bootstrap(tries = 1) {
  40.  
  41. if (W.map && W.model && require && WazeWrap.Ready){
  42. startScriptUpdateMonitor();
  43. init();
  44. }
  45. else if (tries < 1000)
  46. setTimeout(function () {bootstrap(++tries);}, 200);
  47. }
  48.  
  49. bootstrap();
  50.  
  51. function startScriptUpdateMonitor() {
  52. let updateMonitor;
  53. try {
  54. updateMonitor = new WazeWrap.Alerts.ScriptUpdateMonitor(GM_info.script.name, GM_info.script.version, DOWNLOAD_URL, GM_xmlhttpRequest, DOWNLOAD_URL);
  55. updateMonitor.start();
  56. } catch (ex) {
  57. // Report, but don't stop if ScriptUpdateMonitor fails.
  58. console.error('WME SSUtil:', ex);
  59. }
  60. }
  61.  
  62. function init(){
  63. injectCss();
  64. UpdateSegmentGeometry = require('Waze/Action/UpdateSegmentGeometry');
  65. MoveNode = require("Waze/Action/MoveNode");
  66. MultiAction = require("Waze/Action/MultiAction");
  67.  
  68. console.log("SS UTIL");
  69. console.log(GM_info.script);
  70. if(W.map.events)
  71. wEvents = W.map.events;
  72. else
  73. wEvents = W.map.getMapEventsListener();
  74.  
  75. SSUtilWindow = document.createElement('div');
  76. SSUtilWindow.id = "SSUtilWindow";
  77. SSUtilWindow.style.position = 'fixed';
  78. SSUtilWindow.style.visibility = 'hidden';
  79. SSUtilWindow.style.top = '15%';
  80. SSUtilWindow.style.left = '25%';
  81. SSUtilWindow.style.width = '250px';
  82. SSUtilWindow.style.zIndex = 100;
  83. SSUtilWindow.style.backgroundColor = '#FFFFFE';
  84. SSUtilWindow.style.borderWidth = '0px';
  85. SSUtilWindow.style.borderStyle = 'solid';
  86. SSUtilWindow.style.borderRadius = '10px';
  87. SSUtilWindow.style.boxShadow = '5px 5px 10px Silver';
  88. SSUtilWindow.style.padding = '4px';
  89.  
  90. var alertsHTML = '<div id="header" style="padding: 4px; background-color:#92C3D3; border-radius: 5px;-moz-border-radius: 5px;-webkit-border-radius: 5px; color: white; font-weight: bold; text-align:center; letter-spacing: 1px;text-shadow: black 0.1em 0.1em 0.2em;"><img src="https://storage.googleapis.com/wazeopedia-files/1/1e/RA_Util.png" style="float:left"></img> Segment Shift Utility <a data-toggle="collapse" href="#divWrappers1" id="collapserLink1" style="float:right"><span id="collapser1" style="cursor:pointer;padding:2px;color:white;" class="fa fa-caret-square-o-up"></a></span></div>';
  91. // start collapse // I put it al the beginning
  92. alertsHTML += '<div id="divWrappers1" class="collapse in">';
  93. //***************** Disconnect Nodes Checkbox **************************
  94. alertsHTML += '<p style="margin: 10px 0px 0px 20px;"><input type="checkbox" id="chkDisconnectNodes"> Disconnect Nodes</p>';
  95. //***************** Shift Amount **************************
  96. // Define BOX
  97. alertsHTML += '<div id="contentShift" style="text-align:center;float:left; width: 120px;max-width: 49%;height: 170px;margin: 1em 5px 0px 0px;opacity:1;border-radius: 2px;-moz-border-radius: 2px;-webkit-border-radius: 4px;border-width:1px;border-style:solid;border-color:#92C3D3;padding:2px;}">';
  98. alertsHTML += '<b>Shift amount</b></br><input type="text" name="shiftAmount" id="shiftAmount" size="1" style="float: left; text-align: center;font: inherit; line-height: normal; width: 30px; height: 20px; margin: 5px 4px; box-sizing: border-box; display: block; padding-left: 0; border-bottom-color: rgba(black,.3); background: transparent; outline: none; color: black;" value="1"/> <div style="margin: 5px 4px;">Metre(s)';
  99. // Shift amount controls
  100. alertsHTML += '<div id="controls" style="text-align:center; padding:06px 4px;width=100px; height=100px;margin: 5px 0px;border-style:solid; border-width: 2px;border-radius: 50%;-moz-border-radius: 50%;-webkit-border-radius: 50%;box-shadow: inset 0px 0px 50px -14px rgba(0,0,0,1);-moz-box-shadow: inset 0px 0px 50px -14px rgba(0,0,0,1);-webkit-box-shadow: inset 0px 0px 50px -14px rgba(0,0,0,1); background:#92C3D3;align:center;">';
  101. //Single Shift Up Button
  102. alertsHTML += '<span id="SSShiftUpBtn" style="cursor:pointer;font-size:14px;">';
  103. alertsHTML += '<i class="fa fa-angle-double-up fa-2x" style="color: white; text-shadow: black 0.1em 0.1em 0.2em; vertical-align: top;"> </i>';
  104. alertsHTML += '<span id="UpBtnCaption" style="font-weight: bold;"></span>';
  105. alertsHTML += '</span><br>';
  106. //Single Shift Left Button
  107. alertsHTML += '<span id="SSShiftLeftBtn" style="cursor:pointer;font-size:14px;margin-left:-40px;">';
  108. alertsHTML += '<i class="fa fa-angle-double-left fa-2x" style="color: white; text-shadow: black 0.1em 0.1em 0.2em; vertical-align: middle"> </i>';
  109. alertsHTML += '<span id="LeftBtnCaption" style="font-weight: bold;"></span>';
  110. alertsHTML += '</span>';
  111. //Single Shift Right Button
  112. alertsHTML += '<span id="SSShiftRightBtn" style="float: right;cursor:pointer;font-size:14px;margin-right:5px;">';
  113. alertsHTML += '<i class="fa fa-angle-double-right fa-2x" style="color: white;text-shadow: black 0.1em 0.1em 0.2em; vertical-align: middle"> </i>';
  114. alertsHTML += '<span id="RightBtnCaption" style="font-weight: bold;"></span>';
  115. alertsHTML += '</span><br>';
  116. //Single Shift Down Button
  117. alertsHTML += '<span id="SSShiftDownBtn" style="cursor:pointer;font-size:14px;margin-top:0px;">';
  118. alertsHTML += '<i class="fa fa-angle-double-down fa-2x" style="color: white;text-shadow: black 0.1em 0.1em 0.2em; vertical-align: middle"> </i>';
  119. alertsHTML += '<span id="DownBtnCaption" style="font-weight: bold;"></span>';
  120. alertsHTML += '</span>';
  121. alertsHTML += '</div></div></div>';
  122.  
  123. SSUtilWindow.innerHTML = alertsHTML;
  124. document.body.appendChild(SSUtilWindow);
  125.  
  126. $('#SSShiftLeftBtn').click(SSShiftLeftBtnClick);
  127. $('#SSShiftRightBtn').click(SSShiftRightBtnClick);
  128. $('#SSShiftUpBtn').click(SSShiftUpBtnClick);
  129. $('#SSShiftDownBtn').click(SSShiftDownBtnClick);
  130.  
  131. $('#shiftAmount').keypress(function(event) {
  132. if ((event.which != 46 || $(this).val().indexOf('.') != -1) && (event.which < 48 || event.which > 57))
  133. event.preventDefault();
  134. });
  135.  
  136. $('#collapserLink1').click(function(){
  137. $("#divWrappers1").slideToggle("fast");
  138. if($('#collapser1').attr('class') == "fa fa-caret-square-o-down"){
  139. $("#collapser1").removeClass("fa-caret-square-o-down");
  140. $("#collapser1").addClass("fa-caret-square-o-up");
  141. }
  142. else{
  143. $("#collapser1").removeClass("fa-caret-square-o-up");
  144. $("#collapser1").addClass("fa-caret-square-o-down");
  145. }
  146. saveSettingsToStorage();
  147. });
  148.  
  149. W.selectionManager.events.register("selectionchanged", null, checkDisplayTool);
  150.  
  151. var loadedSettings = $.parseJSON(localStorage.getItem("WME_SSUtil"));
  152. var defaultSettings = {
  153. divTop: "15%",
  154. divLeft: "25%",
  155. Expanded: true,
  156. DisconnectNodes: false // default to false (normal behavior)
  157. };
  158. _settings = loadedSettings ? loadedSettings : defaultSettings;
  159.  
  160. $('#SSUtilWindow').css('left', _settings.divLeft);
  161. $('#SSUtilWindow').css('top', _settings.divTop);
  162. $('#chkDisconnectNodes').prop('checked', _settings.DisconnectNodes); // Set checkbox state from settings
  163.  
  164. if(!_settings.Expanded){
  165. // $("#divWrappers1").removeClass("in");
  166. // $("#divWrappers1").addClass("collapse");
  167. $("#divWrappers1").hide();
  168. $("#collapser1").removeClass("fa-caret-square-o-up");
  169. $("#collapser1").addClass("fa-caret-square-o-down");
  170. }
  171.  
  172. 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");
  173. }
  174.  
  175. function saveSettingsToStorage() {
  176. if (localStorage) {
  177. var settings = {
  178. divTop: "15%",
  179. divLeft: "25%",
  180. Expanded: true,
  181. DisconnectNodes: false // default value
  182. };
  183.  
  184. settings.divLeft = $('#SSUtilWindow').css('left');
  185. settings.divTop = $('#SSUtilWindow').css('top');
  186. settings.Expanded = $("#collapser1").attr('class').indexOf("fa-caret-square-o-up") > -1;
  187. settings.DisconnectNodes = $('#chkDisconnectNodes').is(':checked'); // Save checkbox state
  188. localStorage.setItem("WME_SSUtil", JSON.stringify(settings));
  189. }
  190. }
  191.  
  192. function checkDisplayTool(){
  193. if(WazeWrap.hasSelectedFeatures() && WazeWrap.getSelectedFeatures()[0].WW.getType() === 'segment'){
  194. if(WazeWrap.getSelectedFeatures().length === 0)
  195. $('#SSUtilWindow').css({'visibility': 'hidden'});
  196. else{
  197. $('#SSUtilWindow').css({'visibility': 'visible'});
  198. if(typeof jQuery.ui !== 'undefined')
  199. $('#SSUtilWindow' ).draggable({ //Gotta nuke the height setting the dragging inserts otherwise the panel cannot collapse
  200. stop: function(event, ui) {
  201. $('#SSUtilWindow').css("height", "");
  202. saveSettingsToStorage();
  203. }
  204. });
  205. }
  206. }
  207. else{
  208. $('#SSUtilWindow').css({'visibility': 'hidden'});
  209. if(typeof jQuery.ui !== 'undefined')
  210. $('#SSUtilWindow' ).draggable({
  211. stop: function(event, ui) {
  212. $('#SSUtilWindow').css("height", "");
  213. saveSettingsToStorage();
  214. }
  215. });
  216. }
  217. }
  218.  
  219. function ShiftSegmentNodesLat(latOffset) {
  220. var multiaction = new MultiAction();
  221. var selectedFeatures = WazeWrap.getSelectedFeatures();
  222. var disconnectNodes = $('#chkDisconnectNodes').is(':checked'); // Checkbox state
  223.  
  224. if (!disconnectNodes) {
  225. // Normal behavior: Shift segments and connected nodes
  226.  
  227. var uniqueNodes = new Set();
  228.  
  229. // 1. Collect Unique Nodes from Selected Segments
  230. for (let i = 0; i < selectedFeatures.length; i++) {
  231. var segObj = W.model.segments.getObjectById(selectedFeatures[i].WW.getObjectModel().attributes.id);
  232. if (!segObj) continue;
  233. uniqueNodes.add(segObj.attributes.fromNodeID);
  234. uniqueNodes.add(segObj.attributes.toNodeID);
  235. }
  236.  
  237. // 2. Shift Unique Nodes
  238. for (let nodeId of uniqueNodes) {
  239. var node = W.model.nodes.objects[nodeId];
  240. if (!node) continue;
  241.  
  242. var newNodeGeometry = structuredClone(node.attributes.geoJSONGeometry);
  243. newNodeGeometry.coordinates[1] += latOffset;
  244.  
  245. var connectedSegObjs = {};
  246. var emptyObj = {};
  247. for (let j = 0; j < node.attributes.segIDs.length; j++) {
  248. var segid = node.attributes.segIDs[j];
  249. connectedSegObjs[segid] = structuredClone(W.model.segments.getObjectById(segid).attributes.geoJSONGeometry);
  250. }
  251. multiaction.doSubAction(W.model, new MoveNode(node, node.attributes.geoJSONGeometry, newNodeGeometry, connectedSegObjs, emptyObj));
  252. }
  253. } // else - if disconnectNodes is checked, we skip node shifting
  254.  
  255. // 3. Update Segment Geometries (always update segment geometry)
  256. for (let i = 0; i < selectedFeatures.length; i++) {
  257. var segObj = W.model.segments.getObjectById(selectedFeatures[i].WW.getObjectModel().attributes.id);
  258. if (!segObj) continue;
  259. var newGeometry = structuredClone(segObj.attributes.geoJSONGeometry);
  260. var originalLength = segObj.attributes.geoJSONGeometry.coordinates.length;
  261.  
  262. if (disconnectNodes) {
  263. // Shift all points when disconnecting
  264. for (let j = 0; j < originalLength; j++) {
  265. newGeometry.coordinates[j][1] += latOffset;
  266. }
  267. } else {
  268. // Shift only inner points when not disconnecting (normal behavior)
  269. for (let j = 1; j < originalLength - 1; j++) {
  270. newGeometry.coordinates[j][1] += latOffset;
  271. }
  272. }
  273. multiaction.doSubAction(W.model, new UpdateSegmentGeometry(segObj, segObj.attributes.geoJSONGeometry, newGeometry));
  274. }
  275.  
  276.  
  277. W.model.actionManager.add(multiaction);
  278. }
  279.  
  280.  
  281. function ShiftSegmentsNodesLong(longOffset) {
  282. var multiaction = new MultiAction();
  283. var selectedFeatures = WazeWrap.getSelectedFeatures();
  284. var disconnectNodes = $('#chkDisconnectNodes').is(':checked'); // Checkbox state
  285.  
  286. if (!disconnectNodes) {
  287. // Normal behavior: Shift segments and connected nodes
  288.  
  289. var uniqueNodes = new Set();
  290.  
  291. // 1. Collect Unique Nodes from Selected Segments
  292. for (let i = 0; i < selectedFeatures.length; i++) {
  293. var segObj = W.model.segments.getObjectById(selectedFeatures[i].WW.getObjectModel().attributes.id);
  294. if (!segObj) continue;
  295. uniqueNodes.add(segObj.attributes.fromNodeID);
  296. uniqueNodes.add(segObj.attributes.toNodeID);
  297. }
  298.  
  299. // 2. Shift Unique Nodes
  300. for (let nodeId of uniqueNodes) {
  301. var node = W.model.nodes.objects[nodeId];
  302. if (!node) continue;
  303.  
  304. var newNodeGeometry = structuredClone(node.attributes.geoJSONGeometry);
  305. newNodeGeometry.coordinates[0] += longOffset;
  306.  
  307. var connectedSegObjs = {};
  308. var emptyObj = {};
  309. for (let j = 0; j < node.attributes.segIDs.length; j++) {
  310. var segid = node.attributes.segIDs[j];
  311. connectedSegObjs[segid] = structuredClone(W.model.segments.getObjectById(segid).attributes.geoJSONGeometry);
  312. }
  313. multiaction.doSubAction(W.model, new MoveNode(node, node.attributes.geoJSONGeometry, newNodeGeometry, connectedSegObjs, emptyObj));
  314. }
  315. } // else - if disconnectNodes is checked, we skip node shifting
  316.  
  317.  
  318. // 3. Update Segment Geometries (always update segment geometry)
  319. for (let i = 0; i < selectedFeatures.length; i++) {
  320. var segObj = W.model.segments.getObjectById(selectedFeatures[i].WW.getObjectModel().attributes.id);
  321. if (!segObj) continue;
  322.  
  323. var newGeometry = structuredClone(segObj.attributes.geoJSONGeometry);
  324. var originalLength = segObj.attributes.geoJSONGeometry.coordinates.length;
  325.  
  326. if (disconnectNodes) {
  327. // Shift all points when disconnecting
  328. for (let j = 0; j < originalLength; j++) {
  329. newGeometry.coordinates[j][0] += longOffset;
  330. }
  331. } else {
  332. // Shift only inner points when not disconnecting (normal behavior)
  333. for (let j = 1; j < originalLength - 1; j++) {
  334. newGeometry.coordinates[j][0] += longOffset;
  335. }
  336. }
  337. multiaction.doSubAction(W.model, new UpdateSegmentGeometry(segObj, segObj.attributes.geoJSONGeometry, newGeometry));
  338. }
  339.  
  340. W.model.actionManager.add(multiaction);
  341. }
  342.  
  343.  
  344. //Left
  345. function SSShiftLeftBtnClick(e){
  346. e.stopPropagation();
  347. var segObj = WazeWrap.getSelectedFeatures()[0];
  348. if (!segObj) return;
  349. var convertedCoords = WazeWrap.Geometry.ConvertTo4326(segObj.WW.getAttributes().geoJSONGeometry.coordinates[0][0], segObj.WW.getAttributes().geoJSONGeometry.coordinates[0][1]);
  350. var gpsOffsetAmount = WazeWrap.Geometry.CalculateLongOffsetGPS(-$('#shiftAmount').val(), convertedCoords.lon, convertedCoords.lat);
  351. ShiftSegmentsNodesLong(gpsOffsetAmount);
  352. WazeWrap.Alerts.info('WME Segment Shift Utility', `The segments are shifted by <b>${$('#shiftAmount').val()} Metres</b> to the left.`, false, false, 2000);
  353. }
  354. //Right
  355. function SSShiftRightBtnClick(e){
  356. e.stopPropagation();
  357. var segObj = WazeWrap.getSelectedFeatures()[0];
  358. if (!segObj) return;
  359. var convertedCoords = WazeWrap.Geometry.ConvertTo4326(segObj.WW.getAttributes().geoJSONGeometry.coordinates[0][0], segObj.WW.getAttributes().geoJSONGeometry.coordinates[0][1]);
  360. var gpsOffsetAmount = WazeWrap.Geometry.CalculateLongOffsetGPS($('#shiftAmount').val(), convertedCoords.lon, convertedCoords.lat);
  361. ShiftSegmentsNodesLong(gpsOffsetAmount);
  362. WazeWrap.Alerts.info('WME Segment Shift Utility', `The segments are shifted by <b>${$('#shiftAmount').val()} Metres</b> to the right.`, false, false, 2000);
  363. }
  364. //Up
  365. function SSShiftUpBtnClick(e){
  366. e.stopPropagation();
  367. var segObj = WazeWrap.getSelectedFeatures()[0];
  368. if (!segObj) return;
  369. var gpsOffsetAmount = WazeWrap.Geometry.CalculateLatOffsetGPS($('#shiftAmount').val(), WazeWrap.Geometry.ConvertTo4326(segObj.WW.getAttributes().geoJSONGeometry.coordinates[0][0], segObj.WW.getAttributes().geoJSONGeometry.coordinates[0][1]));
  370. ShiftSegmentNodesLat(gpsOffsetAmount);
  371. WazeWrap.Alerts.info('WME Segment Shift Utility', `The segments are shifted by <b>${$('#shiftAmount').val()} Metres</b> to the up.`, false, false, 2000);
  372. }
  373. //Down
  374. function SSShiftDownBtnClick(e){
  375. e.stopPropagation();
  376. var segObj = WazeWrap.getSelectedFeatures()[0];
  377. if (!segObj) return;
  378. var gpsOffsetAmount = WazeWrap.Geometry.CalculateLatOffsetGPS(-$('#shiftAmount').val(), WazeWrap.Geometry.ConvertTo4326(segObj.WW.getAttributes().geoJSONGeometry.coordinates[0][0], segObj.WW.getAttributes().geoJSONGeometry.coordinates[0][1]));
  379. ShiftSegmentNodesLat(gpsOffsetAmount);
  380. WazeWrap.Alerts.info('WME Segment Shift Utility', `The segments are shifted by <b>${$('#shiftAmount').val()} Metres</b> to the down.`, false, false, 2000);
  381. }
  382.  
  383. function injectCss() {
  384. var css = [
  385. '.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);}',
  386. '.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);}'
  387. ].join(' ');
  388. $('<style type="text/css">' + css + '</style>').appendTo('head');
  389. }
  390.  
  391. })();