WME Straighten Up!

Straighten selected WME segment(s) by aligning along straight line between two end points and removing geometry nodes.

  1. // ==UserScript==
  2. // @name WME Straighten Up!
  3. // @namespace https://greasyfork.org/users/166843
  4. // @version 2024.01.31.01
  5. // @description Straighten selected WME segment(s) by aligning along straight line between two end points and removing geometry nodes.
  6. // @author dBsooner
  7. // @match http*://*.waze.com/*editor*
  8. // @exclude http*://*.waze.com/user/editor*
  9. // @require https://greasyfork.org/scripts/24851-wazewrap/code/WazeWrap.js
  10. // @grant GM_xmlhttpRequest
  11. // @connect greasyfork.org
  12. // @license GPLv3
  13. // ==/UserScript==
  14.  
  15. // Original credit to jonny3D and impulse200
  16.  
  17. /* global I18n, GM_info, GM_xmlhttpRequest, W, WazeWrap */
  18.  
  19. (function () {
  20. 'use strict';
  21.  
  22. // eslint-disable-next-line no-nested-ternary
  23. const _SCRIPT_SHORT_NAME = `WME SU!${(/beta/.test(GM_info.script.name) ? ' β' : /\(DEV\)/i.test(GM_info.script.name) ? ' Ω' : '')}`,
  24. _SCRIPT_LONG_NAME = GM_info.script.name,
  25. _IS_ALPHA_VERSION = /[Ω]/.test(_SCRIPT_SHORT_NAME),
  26. _IS_BETA_VERSION = /[β]/.test(_SCRIPT_SHORT_NAME),
  27. // SCRIPT_AUTHOR = GM_info.script.author,
  28. _PROD_DL_URL = 'https://greasyfork.org/scripts/388349-wme-straighten-up/code/WME%20Straighten%20Up!.user.js',
  29. _FORUM_URL = 'https://www.waze.com/forum/viewtopic.php?f=819&t=289116',
  30. _SETTINGS_STORE_NAME = 'WMESU',
  31. _BETA_DL_URL = 'YUhSMGNITTZMeTluY21WaGMzbG1iM0pyTG05eVp5OXpZM0pwY0hSekx6TTRPRE0xTUMxM2JXVXRjM1J5WVdsbmFIUmxiaTExY0MxaVpYUmhMMk52WkdVdlYwMUZKVEl3VTNSeVlXbG5hSFJsYmlVeU1GVndJU1V5TUNoaVpYUmhLUzUxYzJWeUxtcHo=',
  32. _ALERT_UPDATE = true,
  33. _SCRIPT_VERSION = GM_info.script.version.toString(),
  34. _SCRIPT_VERSION_CHANGES = ['BUGFIX: Check for micro dog leg (mDL)'],
  35. _DEBUG = /[βΩ]/.test(_SCRIPT_SHORT_NAME),
  36. _LOAD_BEGIN_TIME = performance.now(),
  37. _elems = {
  38. b: document.createElement('b'),
  39. br: document.createElement('br'),
  40. div: document.createElement('div'),
  41. li: document.createElement('li'),
  42. ol: document.createElement('ol'),
  43. option: document.createElement('option'),
  44. p: document.createElement('p'),
  45. select: document.createElement('select'),
  46. 'wz-button': document.createElement('wz-button'),
  47. 'wz-card': document.createElement('wz-card')
  48. },
  49. _timeouts = { onWmeReady: undefined, saveSettingsToStorage: undefined };
  50. let _settings = {};
  51.  
  52. function log(message, data = '') { console.log(`${_SCRIPT_SHORT_NAME}:`, message, data); }
  53. function logError(message, data = '') { console.error(`${_SCRIPT_SHORT_NAME}:`, new Error(message), data); }
  54. function logWarning(message, data = '') { console.warn(`${_SCRIPT_SHORT_NAME}:`, message, data); }
  55. function logDebug(message, data = '') {
  56. if (_DEBUG)
  57. log(message, data);
  58. }
  59.  
  60. function $extend(...args) {
  61. const extended = {},
  62. deep = Object.prototype.toString.call(args[0]) === '[object Boolean]' ? args[0] : false,
  63. merge = function (obj) {
  64. Object.keys(obj).forEach((prop) => {
  65. if (Object.prototype.hasOwnProperty.call(obj, prop)) {
  66. if (deep && Object.prototype.toString.call(obj[prop]) === '[object Object]')
  67. extended[prop] = $extend(true, extended[prop], obj[prop]);
  68. else if ((obj[prop] !== undefined) && (obj[prop] !== null))
  69. extended[prop] = obj[prop];
  70. }
  71. });
  72. };
  73. for (let i = deep ? 1 : 0, { length } = args; i < length; i++) {
  74. if (args[i])
  75. merge(args[i]);
  76. }
  77. return extended;
  78. }
  79.  
  80. function createElem(type = '', attrs = {}, eventListener = []) {
  81. const el = _elems[type]?.cloneNode(false) || _elems.div.cloneNode(false),
  82. applyEventListeners = function ([evt, cb]) {
  83. return this.addEventListener(evt, cb);
  84. };
  85. Object.keys(attrs).forEach((attr) => {
  86. if ((attrs[attr] !== undefined) && (attrs[attr] !== 'undefined') && (attrs[attr] !== null) && (attrs[attr] !== 'null')) {
  87. if ((attr === 'disabled') || (attr === 'checked') || (attr === 'selected') || (attr === 'textContent') || (attr === 'innerHTML'))
  88. el[attr] = attrs[attr];
  89. else
  90. el.setAttribute(attr, attrs[attr]);
  91. }
  92. });
  93. if (eventListener.length > 0) {
  94. eventListener.forEach((obj) => {
  95. Object.entries(obj).map(applyEventListeners.bind(el));
  96. });
  97. }
  98. return el;
  99. }
  100.  
  101. function createTextNode(str = '') {
  102. return document.createTextNode(str);
  103. }
  104.  
  105. function dec(s = '') {
  106. return atob(atob(s));
  107. }
  108.  
  109. function checkTimeout(obj) {
  110. if (obj.toIndex) {
  111. if (_timeouts[obj.timeout]?.[obj.toIndex]) {
  112. window.clearTimeout(_timeouts[obj.timeout][obj.toIndex]);
  113. delete (_timeouts[obj.timeout][obj.toIndex]);
  114. }
  115. }
  116. else {
  117. if (_timeouts[obj.timeout])
  118. window.clearTimeout(_timeouts[obj.timeout]);
  119. _timeouts[obj.timeout] = undefined;
  120. }
  121. }
  122.  
  123. async function loadSettingsFromStorage() {
  124. const defaultSettings = {
  125. conflictingNames: 'warning',
  126. longJnMove: 'warning',
  127. microDogLegs: 'warning',
  128. nonContinuousSelection: 'warning',
  129. sanityCheck: 'warning',
  130. runStraightenUpShortcut: '',
  131. lastSaved: 0,
  132. lastVersion: undefined
  133. },
  134. loadedSettings = JSON.parse(localStorage.getItem(_SETTINGS_STORE_NAME));
  135. _settings = $extend(true, {}, defaultSettings, loadedSettings);
  136. const serverSettings = await WazeWrap.Remote.RetrieveSettings(_SETTINGS_STORE_NAME);
  137. if (serverSettings?.lastSaved > _settings.lastSaved)
  138. $extend(_settings, serverSettings);
  139. _timeouts.saveSettingsToStorage = window.setTimeout(saveSettingsToStorage, 5000);
  140. return Promise.resolve();
  141. }
  142.  
  143. function saveSettingsToStorage() {
  144. checkTimeout({ timeout: 'saveSettingsToStorage' });
  145. if (localStorage) {
  146. _settings.lastVersion = _SCRIPT_VERSION;
  147. _settings.lastSaved = Date.now();
  148. localStorage.setItem(_SETTINGS_STORE_NAME, JSON.stringify(_settings));
  149. WazeWrap.Remote.SaveSettings(_SETTINGS_STORE_NAME, _settings);
  150. logDebug('Settings saved.');
  151. }
  152. }
  153.  
  154. function checkShortcutChanged() {
  155. let keys = '';
  156. const { shortcut } = W.accelerators.Actions.runStraightenUpShortcut;
  157. if (shortcut) {
  158. if (shortcut.altKey)
  159. keys += 'A';
  160. if (shortcut.shiftKey)
  161. keys += 'S';
  162. if (shortcut.ctrlKey)
  163. keys += 'C';
  164. if (keys !== '')
  165. keys += '+';
  166. if (shortcut.keyCode)
  167. keys += shortcut.keyCode;
  168. }
  169. else {
  170. keys = '';
  171. }
  172. if (_settings.runStraightenUpShortcut !== keys) {
  173. _settings.runStraightenUpShortcut = keys;
  174. saveSettingsToStorage();
  175. }
  176. }
  177.  
  178. function showScriptInfoAlert() {
  179. if (_ALERT_UPDATE && (_SCRIPT_VERSION !== _settings.lastVersion)) {
  180. const divElemRoot = createElem('div');
  181. divElemRoot.appendChild(createElem('p', { textContent: 'What\'s New:' }));
  182. const ulElem = createElem('ul');
  183. if (_SCRIPT_VERSION_CHANGES.length > 0) {
  184. for (let idx = 0, { length } = _SCRIPT_VERSION_CHANGES; idx < length; idx++)
  185. ulElem.appendChild(createElem('li', { innerHTML: _SCRIPT_VERSION_CHANGES[idx] }));
  186. }
  187. else {
  188. ulElem.appendChild(createElem('li', { textContent: 'Nothing major.' }));
  189. }
  190. divElemRoot.appendChild(ulElem);
  191. WazeWrap.Interface.ShowScriptUpdate(_SCRIPT_SHORT_NAME, _SCRIPT_VERSION, divElemRoot.innerHTML, (_IS_BETA_VERSION ? dec(_BETA_DL_URL) : _PROD_DL_URL).replace(/code\/.*\.js/, ''), _FORUM_URL);
  192. }
  193. }
  194.  
  195. // рассчитаем пересчечение перпендикуляра точки с наклонной прямой
  196. // Calculate the intersection of the perpendicular point with an inclined line
  197. function getIntersectCoord(a, b, c, d) {
  198. // второй вариант по-проще: http://rsdn.ru/forum/alg/2589531.hot
  199. const r = [2];
  200. // eslint-disable-next-line no-mixed-operators
  201. r[1] = -1.0 * (c * b - a * d) / (a * a + b * b);
  202. r[0] = (-r[1] * (b + a) - c + d) / (a - b);
  203. return { x: r[0], y: r[1] };
  204. }
  205.  
  206. // определим направляющие
  207. // Define guides
  208. function getDeltaDirect(a, b) {
  209. let d = 0.0;
  210. if (a < b)
  211. d = 1.0;
  212. else if (a > b)
  213. d = -1.0;
  214. return d;
  215. }
  216.  
  217. function checkNameContinuity(segmentSelectionArr = []) {
  218. const streetIds = [],
  219. streetIdsForEach = (streetId) => { streetIds.push(streetId); };
  220. for (let idx = 0, { length } = segmentSelectionArr; idx < length; idx++) {
  221. if (idx > 0) {
  222. if ((segmentSelectionArr[idx].getPrimaryStreetID() > 0) && streetIds.includes(segmentSelectionArr[idx].getPrimaryStreetID()))
  223. // eslint-disable-next-line no-continue
  224. continue;
  225. if (segmentSelectionArr[idx].getAttribute('streetIDs').length > 0) {
  226. let included = false;
  227. for (let idx2 = 0, len = segmentSelectionArr[idx].getAttribute('streetIDs').length; idx2 < len; idx2++) {
  228. included = streetIds.includes(segmentSelectionArr[idx].getAttribute('streetIDs')[idx2]);
  229. if (included)
  230. break;
  231. }
  232. if (included === true)
  233. // eslint-disable-next-line no-continue
  234. continue;
  235. else
  236. return false;
  237. }
  238. return false;
  239. }
  240. if (idx === 0) {
  241. if (segmentSelectionArr[idx].getPrimaryStreetID() > 0)
  242. streetIds.push(segmentSelectionArr[idx].getPrimaryStreetID());
  243. if (segmentSelectionArr[idx].getAttribute('streetIDs').length > 0)
  244. segmentSelectionArr[idx].getAttribute('streetIDs').forEach(streetIdsForEach);
  245. }
  246. }
  247. return true;
  248. }
  249.  
  250. function distanceBetweenPoints(lon1, lat1, lon2, lat2, measurement) {
  251. // eslint-disable-next-line no-nested-ternary
  252. const multiplier = measurement === 'meters' ? 1000 : measurement === 'miles' ? 0.621371192237334 : measurement === 'feet' ? 3280.8398950131 : 1;
  253. const R = 6371; // KM
  254. const φ1 = lat1 * (Math.PI / 180);
  255. const φ2 = lat2 * (Math.PI / 180);
  256. const Δφ = (lat2 - lat1) * (Math.PI / 180);
  257. const Δλ = (lon2 - lon1) * (Math.PI / 180);
  258. const a = Math.sin(Δφ / 2) * Math.sin(Δφ / 2) + Math.cos1) * Math.cos2) * Math.sin(Δλ / 2) * Math.sin(Δλ / 2);
  259. const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
  260. const d = R * c;
  261. return d * multiplier;
  262. }
  263.  
  264. function checkForMicroDogLegs(distinctNodes, singleSegmentId) {
  265. if (!distinctNodes || (distinctNodes.length < 1))
  266. return false;
  267. const nodesChecked = [],
  268. nodesObjArr = W.model.nodes.getByIds(distinctNodes);
  269. if (!nodesObjArr || (nodesObjArr.length < 1))
  270. return false;
  271. const checkGeoComp = function (geoComp) {
  272. const testNode4326 = { lon: geoComp[0], lat: geoComp[1] };
  273. if ((this.lon !== testNode4326.lon) || (this.lat !== testNode4326.lat)) {
  274. if (distanceBetweenPoints(this.lon, this.lat, testNode4326.lon, testNode4326.lat, 'meters') < 2)
  275. return false;
  276. }
  277. return true;
  278. };
  279. for (let idx = 0, { length } = nodesObjArr; idx < length; idx++) {
  280. if (!nodesChecked.includes(nodesObjArr[idx])) {
  281. nodesChecked.push(nodesObjArr[idx]);
  282. const segmentsObjArr = W.model.segments.getByIds(nodesObjArr[idx].getSegmentIds()) || [],
  283. node4326 = {
  284. lon: nodesObjArr[idx].getGeometry().coordinates[0],
  285. lat: nodesObjArr[idx].getGeometry().coordinates[1]
  286. };
  287. for (let idx2 = 0, len = segmentsObjArr.length; idx2 < len; idx2++) {
  288. const segObj = segmentsObjArr[idx2];
  289. if (!singleSegmentId
  290. || (singleSegmentId && (segObj.getID() === singleSegmentId))) {
  291. if (!segObj.getGeometry().coordinates.every(checkGeoComp.bind(node4326)))
  292. return true;
  293. }
  294. }
  295. }
  296. }
  297. return false;
  298. }
  299.  
  300. function doStraightenSegments(sanityContinue, nonContinuousContinue, conflictingNamesContinue, microDogLegsContinue, longJnMoveContinue, passedObj) {
  301. const segmentSelection = W.selectionManager.getSegmentSelection();
  302. if (longJnMoveContinue && passedObj) {
  303. const {
  304. segmentsToRemoveGeometryArr, nodesToMoveArr, distinctNodes, endPointNodeIds
  305. } = passedObj;
  306. logDebug(`${I18n.t('wmesu.log.StraighteningSegments')}: ${distinctNodes.join(', ')} (${distinctNodes.length})`);
  307. logDebug(`${I18n.t('wmesu.log.EndPoints')}: ${endPointNodeIds.join(' & ')}`);
  308. if (segmentsToRemoveGeometryArr?.length > 0) {
  309. const UpdateSegmentGeometry = require('Waze/Action/UpdateSegmentGeometry');
  310. segmentsToRemoveGeometryArr.forEach((obj) => {
  311. W.model.actionManager.add(new UpdateSegmentGeometry(obj.segment, obj.geometry, obj.newGeo));
  312. logDebug(`${I18n.t('wmesu.log.RemovedGeometryNodes')} # ${obj.segment.getID()}`);
  313. });
  314. }
  315. if (nodesToMoveArr?.length > 0) {
  316. const MoveNode = require('Waze/Action/MoveNode');
  317. let straightened = false;
  318. nodesToMoveArr.forEach((node) => {
  319. if ((Math.abs(node.geometry.coordinates[0] - node.nodeGeo.coordinates[0]) > 0.00000001) || (Math.abs(node.geometry.coordinates[1] - node.nodeGeo.coordinates[1]) > 0.00000001)) {
  320. logDebug(`${I18n.t('wmesu.log.MovingJunctionNode')} # ${node.node.getID()} `
  321. + `- ${I18n.t('wmesu.common.From')}: ${node.geometry.coordinates[0]},${node.geometry.coordinates[1]} - `
  322. + `${I18n.t('wmesu.common.To')}: ${node.nodeGeo.coordinates[0]},${node.nodeGeo.coordinates[1]}`);
  323. W.model.actionManager.add(new MoveNode(node.node, node.geometry, node.nodeGeo, node.connectedSegObjs, {}));
  324. straightened = true;
  325. }
  326. });
  327. if (!straightened) {
  328. logDebug(I18n.t('wmesu.log.AllNodesStraight'));
  329. WazeWrap.Alerts.info(_SCRIPT_SHORT_NAME, I18n.t('wmesu.log.AllNodesStraight'));
  330. }
  331. }
  332. }
  333. else if (segmentSelection.segments.length > 1) {
  334. const segmentsToRemoveGeometryArr = [],
  335. nodesToMoveArr = [];
  336. if ((segmentSelection.segments.length > 10) && !sanityContinue) {
  337. if (_settings.sanityCheck === 'error') {
  338. WazeWrap.Alerts.error(_SCRIPT_SHORT_NAME, I18n.t('wmesu.error.TooManySegments'));
  339. return;
  340. }
  341. if (_settings.sanityCheck === 'warning') {
  342. WazeWrap.Alerts.confirm(
  343. _SCRIPT_SHORT_NAME,
  344. I18n.t('wmesu.prompts.SanityCheckConfirm'),
  345. () => { doStraightenSegments(true, false, false, false, false, undefined); },
  346. () => { },
  347. I18n.t('wmesu.common.Yes'),
  348. I18n.t('wmesu.common.No')
  349. );
  350. return;
  351. }
  352. }
  353. sanityContinue = true;
  354. if ((segmentSelection.multipleConnectedComponents === true) && !nonContinuousContinue) {
  355. if (_settings.nonContinuousSelection === 'error') {
  356. WazeWrap.Alerts.error(_SCRIPT_SHORT_NAME, I18n.t('wmesu.error.NonContinuous'));
  357. return;
  358. }
  359. if (_settings.nonContinuousSelection === 'warning') {
  360. WazeWrap.Alerts.confirm(
  361. _SCRIPT_SHORT_NAME,
  362. I18n.t('wmesu.prompts.NonContinuousConfirm'),
  363. () => { doStraightenSegments(sanityContinue, true, false, false, false, undefined); },
  364. () => { },
  365. I18n.t('wmesu.common.Yes'),
  366. I18n.t('wmesu.common.No')
  367. );
  368. return;
  369. }
  370. }
  371. nonContinuousContinue = true;
  372. if (_settings.conflictingNames !== 'nowarning') {
  373. const continuousNames = checkNameContinuity(segmentSelection.segments);
  374. if (!continuousNames && !conflictingNamesContinue && (_settings.conflictingNames === 'error')) {
  375. WazeWrap.Alerts.error(_SCRIPT_SHORT_NAME, I18n.t('wmesu.error.ConflictingNames'));
  376. return;
  377. }
  378. if (!continuousNames && !conflictingNamesContinue && (_settings.conflictingNames === 'warning')) {
  379. WazeWrap.Alerts.confirm(
  380. _SCRIPT_SHORT_NAME,
  381. I18n.t('wmesu.prompts.ConflictingNamesConfirm'),
  382. () => { doStraightenSegments(sanityContinue, nonContinuousContinue, true, false, false, undefined); },
  383. () => { },
  384. I18n.t('wmesu.common.Yes'),
  385. I18n.t('wmesu.common.No')
  386. );
  387. return;
  388. }
  389. }
  390. conflictingNamesContinue = true;
  391. const allNodeIds = [],
  392. dupNodeIds = [];
  393. let endPointNodeIds,
  394. longMove = false;
  395. for (let idx = 0, { length } = segmentSelection.segments; idx < length; idx++) {
  396. allNodeIds.push(segmentSelection.segments[idx].getFromNode().getID());
  397. allNodeIds.push(segmentSelection.segments[idx].getToNode().getID());
  398. if (segmentSelection.segments[idx].type === 'segment') {
  399. const newGeo = structuredClone(segmentSelection.segments[idx].getGeometry());
  400. // Remove the geometry nodes
  401. if (newGeo.coordinates.length > 2) {
  402. newGeo.coordinates.splice(1, newGeo.coordinates.length - 2);
  403. segmentsToRemoveGeometryArr.push({ segment: segmentSelection.segments[idx], geometry: segmentSelection.segments[idx].getGeometry(), newGeo });
  404. }
  405. }
  406. }
  407. allNodeIds.forEach((nodeId, idx) => {
  408. if (allNodeIds.indexOf(nodeId, idx + 1) > -1) {
  409. if (!dupNodeIds.includes(nodeId))
  410. dupNodeIds.push(nodeId);
  411. }
  412. });
  413. const distinctNodes = [...new Set(allNodeIds)];
  414. if (!microDogLegsContinue && (checkForMicroDogLegs(distinctNodes, undefined) === true)) {
  415. if (_settings.microDogLegs === 'error') {
  416. WazeWrap.Alerts.error(_SCRIPT_SHORT_NAME, I18n.t('wmesu.error.MicroDogLegs'));
  417. return;
  418. }
  419. if (_settings.microDogLegs === 'warning') {
  420. WazeWrap.Alerts.confirm(
  421. _SCRIPT_SHORT_NAME,
  422. I18n.t('wmesu.prompts.MicroDogLegsConfirm'),
  423. () => { doStraightenSegments(sanityContinue, nonContinuousContinue, conflictingNamesContinue, true, false, undefined); },
  424. () => { },
  425. I18n.t('wmesu.common.Yes'),
  426. I18n.t('wmesu.common.No')
  427. );
  428. return;
  429. }
  430. }
  431. microDogLegsContinue = true;
  432. if (segmentSelection.multipleConnectedComponents === false)
  433. endPointNodeIds = distinctNodes.filter((nodeId) => !dupNodeIds.includes(nodeId));
  434. else
  435. endPointNodeIds = [segmentSelection.segments[0].getFromNode().getID(), segmentSelection.segments[(segmentSelection.segments.length - 1)].getToNode().getID()];
  436. const endPointNodeObjs = W.model.nodes.getByIds(endPointNodeIds),
  437. endPointNode1Geo = structuredClone(endPointNodeObjs[0].getGeometry()),
  438. endPointNode2Geo = structuredClone(endPointNodeObjs[1].getGeometry());
  439. if (getDeltaDirect(endPointNode1Geo.coordinates[0], endPointNode2Geo.coordinates[0]) < 0) {
  440. let [t] = endPointNode1Geo.coordinates;
  441. [endPointNode1Geo.coordinates[0]] = endPointNode2Geo.coordinates;
  442. endPointNode2Geo.coordinates[0] = t;
  443. [, t] = endPointNode1Geo.coordinates;
  444. [, endPointNode1Geo.coordinates[1]] = endPointNode2Geo.coordinates;
  445. endPointNode2Geo.coordinates[1] = t;
  446. endPointNodeIds.push(endPointNodeIds[0]);
  447. endPointNodeIds.splice(0, 1);
  448. endPointNodeObjs.push(endPointNodeObjs[0]);
  449. endPointNodeObjs.splice(0, 1);
  450. }
  451. const a = endPointNode2Geo.coordinates[1] - endPointNode1Geo.coordinates[1],
  452. b = endPointNode1Geo.coordinates[0] - endPointNode2Geo.coordinates[0],
  453. c = endPointNode2Geo.coordinates[0] * endPointNode1Geo.coordinates[1] - endPointNode1Geo.coordinates[0] * endPointNode2Geo.coordinates[1];
  454. distinctNodes.forEach((nodeId) => {
  455. if (!endPointNodeIds.includes(nodeId)) {
  456. const node = W.model.nodes.getObjectById(nodeId),
  457. nodeGeo = structuredClone(node.getGeometry());
  458. const d = nodeGeo.coordinates[1] * a - nodeGeo.coordinates[0] * b,
  459. r1 = getIntersectCoord(a, b, c, d);
  460. nodeGeo.coordinates[0] = r1.x;
  461. nodeGeo.coordinates[1] = r1.y;
  462. const connectedSegObjs = {};
  463. for (let idx = 0, { length } = node.getAttribute('segIDs'); idx < length; idx++) {
  464. const segId = node.getAttribute('segIDs')[idx];
  465. connectedSegObjs[segId] = structuredClone(W.model.segments.getObjectById(segId).getGeometry());
  466. }
  467. const fromNodeLonLat = { x: node.getGeometry().coordinates[0], y: node.getGeometry().coordinates[1] },
  468. toNodeLonLat = r1;
  469. if (distanceBetweenPoints(fromNodeLonLat.x, fromNodeLonLat.y, toNodeLonLat.x, toNodeLonLat.y, 'meters') > 10)
  470. longMove = true;
  471. nodesToMoveArr.push({
  472. node, geometry: node.getGeometry(), nodeGeo, connectedSegObjs
  473. });
  474. }
  475. });
  476. if (longMove && (_settings.longJnMove === 'error')) {
  477. WazeWrap.Alerts.error(_SCRIPT_SHORT_NAME, I18n.t('wmesu.error.LongJnMove'));
  478. return;
  479. }
  480. if (longMove && (_settings.longJnMove === 'warning')) {
  481. WazeWrap.Alerts.confirm(
  482. _SCRIPT_SHORT_NAME,
  483. I18n.t('wmesu.prompts.LongJnMoveConfirm'),
  484. () => {
  485. doStraightenSegments(sanityContinue, nonContinuousContinue, conflictingNamesContinue, microDogLegsContinue, true, {
  486. segmentsToRemoveGeometryArr, nodesToMoveArr, distinctNodes, endPointNodeIds
  487. });
  488. },
  489. () => { },
  490. I18n.t('wmesu.common.Yes'),
  491. I18n.t('wmesu.common.No')
  492. );
  493. return;
  494. }
  495. doStraightenSegments(sanityContinue, nonContinuousContinue, conflictingNamesContinue, microDogLegsContinue, true, {
  496. segmentsToRemoveGeometryArr, nodesToMoveArr, distinctNodes, endPointNodeIds
  497. });
  498. }
  499. else if (segmentSelection.segments.length === 1) {
  500. const seg = segmentSelection.segments[0];
  501. if (seg.type === 'segment') {
  502. if (!microDogLegsContinue && (checkForMicroDogLegs([seg.getFromNode().getID(), seg.getToNode().getID()], seg.getID()) === true)) {
  503. if (_settings.microDogLegs === 'error') {
  504. WazeWrap.Alerts.error(_SCRIPT_SHORT_NAME, I18n.t('wmesu.error.MicroDogLegs'));
  505. return;
  506. }
  507. if (_settings.microDogLegs === 'warning') {
  508. WazeWrap.Alerts.confirm(
  509. _SCRIPT_SHORT_NAME,
  510. I18n.t('wmesu.prompts.MicroDogLegsConfirm'),
  511. () => { doStraightenSegments(sanityContinue, nonContinuousContinue, conflictingNamesContinue, true, false, undefined); },
  512. () => { },
  513. I18n.t('wmesu.common.Yes'),
  514. I18n.t('wmesu.common.No')
  515. );
  516. return;
  517. }
  518. }
  519. microDogLegsContinue = true;
  520. const newGeo = structuredClone(seg.getGeometry());
  521. // Remove the geometry nodes
  522. if (newGeo.coordinates.length > 2) {
  523. const UpdateSegmentGeometry = require('Waze/Action/UpdateSegmentGeometry');
  524. newGeo.coordinates.splice(1, newGeo.coordinates.length - 2);
  525. W.model.actionManager.add(new UpdateSegmentGeometry(seg, seg.getGeometry(), newGeo, { createNodes: true, snappedFeatures: undefined }));
  526. logDebug(`${I18n.t('wmesu.log.RemovedGeometryNodes')} # ${seg.getID()}`);
  527. }
  528. }
  529. }
  530. else {
  531. logWarning(I18n.t('wmesu.log.NoSegmentsSelected'));
  532. }
  533. }
  534.  
  535. function insertSimplifyStreetGeometryButtons() {
  536. const wmeSuDiv = document.getElementById('WME-SU-div'),
  537. elem = document.getElementById('segment-edit-general');
  538. if (!elem)
  539. return;
  540. const docFrags = document.createDocumentFragment();
  541. if (!wmeSuDiv) {
  542. const contentDiv = createElem('div', { style: 'align-items:center; cursor:pointer; display:flex; font-size:13px; gap:8px; justify-content:flex-start;', textContent: I18n.t('wmesu.StraightenUp') });
  543. contentDiv.appendChild(createElem('wz-button', {
  544. id: 'WME-SU', color: 'secondary', size: 'xs', textContent: I18n.t('wmesu.common.DoIt'), title: I18n.t('wmesu.StraightenUpTitle')
  545. }, [{ click: doStraightenSegments }]));
  546. const wzCard = createElem('wz-card', { style: '--wz-card-padding:4px 8px; --wz-card-margin:0; --wz-card-width:auto; display:block; margin-bottom:8px;' });
  547. wzCard.appendChild(contentDiv);
  548. const divElemRoot = createElem('div', { id: 'WME-SU-div' });
  549. divElemRoot.appendChild(wzCard);
  550. docFrags.appendChild(divElemRoot);
  551. }
  552. if (docFrags.firstChild)
  553. elem.insertBefore(docFrags, elem.firstChild);
  554. }
  555.  
  556. function loadTranslations() {
  557. return new Promise((resolve) => {
  558. const translations = {
  559. en: {
  560. StraightenUp: 'Straighten Up!',
  561. StraightenUpTitle: 'Click here to straighten the selected segment(s) by removing geometry nodes and moving junction nodes as needed.',
  562. common: {
  563. DoIt: 'Do It',
  564. From: 'from',
  565. Help: 'Help',
  566. No: 'No',
  567. Note: 'Note',
  568. NothingMajor: 'Nothing major.',
  569. To: 'to',
  570. Warning: 'Warning',
  571. WhatsNew: 'What\'s new',
  572. Yes: 'Yes'
  573. },
  574. error: {
  575. ConflictingNames: 'You selected segments that do not share at least one name in common amongst all the segments and have the conflicting names setting set to error. '
  576. + 'Segments not straightened.',
  577. LongJnMove: 'One or more of the junction nodes that were to be moved would have been moved further than 10m and you have the long junction node move setting set to '
  578. + 'give error. Segments not straightened.',
  579. MicroDogLegs: 'One or more of the junctions nodes in the selection have a geonode within 2 meters. This is usually the sign of a micro dog leg (mDL).<br><br>'
  580. + 'You have the setting for possibe micro doglegs set to give error. Segments not straightened.',
  581. NonContinuous: 'You selected segments that are not all connected and have the non-continuous selected segments setting set to give error. Segments not straightened.',
  582. TooManySegments: 'You selected too many segments and have the sanity check setting set to give error. Segments not straightened.'
  583. },
  584. help: {
  585. Note01: 'This script uses the action manager, so changes can be undone before saving.',
  586. Warning01: 'Enabling (Give warning, No warning) any of these settings can cause unexpected results. Use with caution!',
  587. Step01: 'Select the starting segment.',
  588. Step02: 'ALT+click the ending segment.',
  589. Step02note: 'If the segments you wanted to straighten are not all selected, unselect them and start over using CTRL+click to select each segment instead.',
  590. Step03: 'Click "Straighten up!" button in the sidebar.'
  591. },
  592. log: {
  593. AllNodesStraight: 'All junction nodes that would be moved are already considered \'straight\'. No junction nodes were moved.',
  594. EndPoints: 'End points',
  595. MovingJunctionNode: 'Moving junction node',
  596. NoSegmentsSelected: 'No segments selected.',
  597. RemovedGeometryNodes: 'Removed geometry nodes for segment',
  598. Segment: I18n.t('objects.segment.name'),
  599. StraighteningSegments: 'Straightening segments'
  600. },
  601. prompts: {
  602. ConflictingNamesConfirm: 'You selected segments that do not share at least one name in common amongst all the segments. Are you sure you wish to continue straightening?',
  603. LongJnMoveConfirm: 'One or more of the junction nodes that are to be moved would be moved further than 10m. Are you sure you wish to continue straightening?',
  604. MicroDogLegsConfirm: 'One or more of the junction nodes in the selection have a geonode within 2 meters. This is usually the sign of a micro dog leg (mDL).<br>'
  605. + 'This geonode could exist on any segment connected to the junction nodes, not just the segments you selected.<br><br>'
  606. + '<b>You should not continue until you are certain there are no micro dog legs.<b><br><br>'
  607. + 'Are you sure you wish to continue straightening?',
  608. NonContinuousConfirm: 'You selected segments that do not all connect. Are you sure you wish to continue straightening?',
  609. SanityCheckConfirm: 'You selected many segments. Are you sure you wish to continue straightening?'
  610. },
  611. settings: {
  612. GiveError: 'Give error',
  613. GiveWarning: 'Give warning',
  614. NoWarning: 'No warning',
  615. ConflictingNames: 'Segments with conflicting names',
  616. ConflictingNamesTitle: 'Select what to do if the selected segments do not share at least one name among their primary and alternate names (based on name, city and state).',
  617. LongJnMove: 'Long junction node moves',
  618. LongJnMoveTitle: 'Select what to do if one or more of the junction nodes would move further than 10m.',
  619. MicroDogLegs: 'Possible micro doglegs (mDL)',
  620. MicroDogLegsTitle: 'Select what to do if one or more of the junction nodes in the selection have a geometry node within 2m of itself, which is a possible micro dogleg (mDL).',
  621. NonContinuousSelection: 'Non-continuous selected segments',
  622. NonContinuousSelectionTitle: 'Select what to do if the selected segments are not continuous.',
  623. SanityCheck: 'Sanity check',
  624. SanityCheckTitle: 'Select what to do if you selected a many segments.'
  625. }
  626. },
  627. ru: {
  628. StraightenUp: 'Выпрямить сегменты!',
  629. StraightenUpTitle: 'Нажмите, чтобы выпрямить выбранные сегменты, удалив лишние геометрические точки и переместив узлы перекрёстков в ровную линию.',
  630. common: {
  631. DoIt: 'Сделай это',
  632. From: 'с',
  633. Help: 'Помощь',
  634. No: 'Нет',
  635. Note: 'Примечание',
  636. NothingMajor: 'Не критично.',
  637. To: 'до',
  638. Warning: 'Предупреждение',
  639. WhatsNew: 'Что нового',
  640. Yes: 'Да'
  641. },
  642. error: {
  643. ConflictingNames: 'Вы выбрали сегменты, которые не имеют хотя бы одного общего названия улицы среди выделенных.'
  644. + 'Сегменты не были выпрямлены.',
  645. LongJnMove: 'Для выпрямления сегментов, их узлы должны быть перемещены более чем на 10 м, но в настройках у вас установлено ограничение перемещения на такое большое '
  646. + 'расстояние. Сегменты не были выпрямлены.',
  647. MicroDogLegs: 'Один или несколько узлов выбранных сегментов имеют точку в пределах 2 метров. Обычно это признак “<a href=”https://wazeopedia.waze.com/wiki/Benelux/Junction_Arrows” target=”blank”>микроискривления</a>”.<br><br>'
  648. + 'В настройках для возможных микроискривлений у вас выставлено ограничение, чтобы выдать ошибку. Сегменты не были выпрямлены.',
  649. NonContinuous: 'Вы выбрали сегменты, которые не соединены между собой, но в настройках у вас установлено ограничение для работы с такими сегментами. Сегменты не были '
  650. + 'выпрямлены.',
  651. TooManySegments: 'Вы выбрали слишком много сегментов, но в настройках у вас включено ограничение на количество одновременно обрабатываемых сегментов. Сегменты не были '
  652. + 'выпрямлены.'
  653. },
  654. help: {
  655. Note01: 'Этот скрипт использует историю действий, поэтому перед их сохранением изменения можно отменить.',
  656. Warning01: 'Настройка любого из этих параметров в положение (Выдать предупреждение, Не предупреждать) может привести к неожиданным результатам. Используйте с осторожностью!',
  657. Step01: 'Выделите начальный сегмент.',
  658. Step02: 'При помощи Alt-кнопки, выделите конечный сегмент.',
  659. Step02note: 'Если выделены не все нужные вам сегменты, при помощи Ctrl-кнопки можно дополнительно выделить или снять выделения сегментов.',
  660. Step03: 'Нажмите ‘Выпрямить сегменты!’ на левой панели.'
  661. },
  662. log: {
  663. AllNodesStraight: 'Все узлы, которые нужно было выпрямить, уже выровнены в линию. Сегменты оставлены без изменений.',
  664. EndPoints: 'конечные точки',
  665. MovingJunctionNode: 'Перемещение узла',
  666. NoSegmentsSelected: 'Сегменты не выделены.',
  667. RemovedGeometryNodes: 'Удалены лишние точки сегмента',
  668. Segment: I18n.t('objects.segment.name'),
  669. StraighteningSegments: 'Выпрямление сегментов'
  670. },
  671. prompts: {
  672. ConflictingNamesConfirm: 'Вы выбрали сегменты, которые не имеют хотя бы одного общего названия среди всех сегментов. Вы уверены, что хотите продолжить выпрямление?',
  673. LongJnMoveConfirm: 'Один или несколько узлов будут перемещены более, чем на 10 метров. Вы уверены, что хотите продолжить выпрямление?',
  674. MicroDogLegsConfirm: 'Один или несколько узлов выбранных сегментов имеют точки в пределах 2 метров. Обычно это признак “<a href=”https://wazeopedia.waze.com/wiki/Benelux/Junction_Arrows” target=”blank”>микроискривления</a>”.<br>'
  675. + 'Такая точка может находиться в любом сегменте, соединенном с выбранными вами сегментами и узлами, а не только на них самих.<br><br>'
  676. + '<b>Вы не должны продолжать до тех пор, пока не убедитесь, что у вас нет “микроискривлений”.<b><br><br>'
  677. + 'Вы уверены,что готовы продолжать выпрямление?',
  678. NonContinuousConfirm: 'Вы выбрали сегменты, которые не соединяются друг с другом. Вы уверены, что хотите продолжить выпрямление?',
  679. SanityCheckConfirm: 'Вы выбрали слишком много сегментов. Вы уверены, что хотите продолжить выпрямление?'
  680. },
  681. settings: {
  682. GiveError: 'Выдать ошибку',
  683. GiveWarning: 'Выдать предупреждение',
  684. NoWarning: 'Не предупреждать',
  685. ConflictingNames: 'Сегменты с разными названиями',
  686. ConflictingNamesTitle: 'Выберите, что делать, если выбранные сегменты не содержат хотя бы одно название среди своих основных и альтернативных названий (на основе улицы, '
  687. + 'города и района).',
  688. LongJnMove: 'Перемещение узлов на большие расстояния',
  689. LongJnMoveTitle: 'Выберите, что делать, если один или несколько узлов будут перемещаться дальше, чем на 10 метров.',
  690. MicroDogLegs: 'Допускать “<a href=”https://wazeopedia.waze.com/wiki/Benelux/Junction_Arrows” target=”blank”>микроискривления</a>”',
  691. MicroDogLegsTitle: 'Выберите, что делать, если один или несколько узлов соединения в выделении имеют точку в пределах 2 м от себя, что является возможным “микроискривлением”.',
  692. NonContinuous: 'Не соединённые сегменты',
  693. NonContinuousTitle: 'Выберите, что делать, если выбранные сегменты не соединены друг с другом.',
  694. SanityCheck: 'Ограничение нагрузки',
  695. SanityCheckTitle: 'Выберите, что делать, если вы выбрали слишком много сегментов.'
  696. }
  697. }
  698. },
  699. locale = I18n.currentLocale();
  700. I18n.translations[locale].wmesu = translations.en;
  701. translations['en-US'] = { ...translations.en };
  702. I18n.translations[locale].wmesu = $extend(true, {}, translations.en, translations[locale]);
  703. resolve();
  704. });
  705. }
  706.  
  707. function checkSuVersion() {
  708. if (_IS_ALPHA_VERSION)
  709. return;
  710. let updateMonitor;
  711. try {
  712. updateMonitor = new WazeWrap.Alerts.ScriptUpdateMonitor(_SCRIPT_LONG_NAME, _SCRIPT_VERSION, (_IS_BETA_VERSION ? dec(_BETA_DL_URL) : _PROD_DL_URL), GM_xmlhttpRequest);
  713. updateMonitor.start();
  714. }
  715. catch (err) {
  716. logError('Upgrade version check:', err);
  717. }
  718. }
  719.  
  720. async function onWazeWrapReady() {
  721. log('Initializing.');
  722. checkSuVersion();
  723. if (W.loginManager.getUserRank() < 2)
  724. return;
  725. await loadSettingsFromStorage();
  726. await loadTranslations();
  727. const onSelectionChange = function () {
  728. const setting = this.id.substr(6);
  729. if (this.value.toLowerCase() !== _settings[setting]) {
  730. _settings[setting] = this.value.toLowerCase();
  731. saveSettingsToStorage();
  732. }
  733. },
  734. buildSelections = (selected) => {
  735. const docFrags = document.createDocumentFragment();
  736. docFrags.appendChild(createElem('option', { value: 'nowarning', selected: selected === 'nowarning', textContent: I18n.t('wmesu.settings.NoWarning') }));
  737. docFrags.appendChild(createElem('option', { value: 'warning', selected: selected === 'warning', textContent: I18n.t('wmesu.settings.GiveWarning') }));
  738. docFrags.appendChild(createElem('option', { value: 'error', selected: selected === 'error', textContent: I18n.t('wmesu.settings.GiveError') }));
  739. return docFrags;
  740. },
  741. buildSection = (section) => {
  742. const selectElem = createElem('select', {
  743. id: `WMESU-${section}`,
  744. style: 'font-size:11px;height:22px;',
  745. title: I18n.t(`wmesu.settings.${section.charAt(0).toUpperCase()}${section.slice(1)}Title`)
  746. }, [{ change: onSelectionChange }]);
  747. selectElem.appendChild(buildSelections(_settings[section]));
  748. const divElemDiv = createElem('div', { id: `WMESU-div-${section}`, class: 'controls-container' });
  749. divElemDiv.appendChild(selectElem);
  750. const divElemDivDiv = createElem('div', { style: 'display:inline-block;font-size:11px;', textContent: I18n.t(`wmesu.settings.${section.charAt(0).toUpperCase()}${section.slice(1)}`) });
  751. divElemDiv.appendChild(divElemDivDiv);
  752. return divElemDiv;
  753. },
  754. tabContent = () => {
  755. const docFrags = document.createDocumentFragment();
  756. docFrags.appendChild(createElem('div', { style: 'margin-bottom:0px;font-size:13px;font-weight:600;', textContent: _SCRIPT_SHORT_NAME }));
  757. docFrags.appendChild(createElem('div', { style: 'margin-top:0px;font-size:11px;font-weight:600;color:#aaa;', textContent: _SCRIPT_VERSION }));
  758. docFrags.appendChild(buildSection('conflictingNames'));
  759. docFrags.appendChild(buildSection('longJnMove'));
  760. docFrags.appendChild(buildSection('microDogLegs'));
  761. docFrags.appendChild(buildSection('nonContinuousSelection'));
  762. docFrags.appendChild(buildSection('sanityCheck'));
  763. const divElemDiv = createElem('div', { style: 'margin-top:20px;' });
  764. divElemDiv.appendChild(createElem('div', { style: 'font-size:14px;font-weight:600;', textContent: I18n.t('wmesu.common.Help') }));
  765. let liElem = createElem('li');
  766. liElem.appendChild(createElem('p', { style: 'font-weight:100;margin-bottom:0px;', textContent: I18n.t('wmesu.help.Step01') }));
  767. const olElem = createElem('ol', { style: 'font-weight:600;' });
  768. olElem.appendChild(liElem);
  769. const pElem = createElem('p', { style: 'font-weight:100;margin-bottom:0px;' });
  770. pElem.appendChild(createTextNode(I18n.t('wmesu.help.Step02')));
  771. pElem.appendChild(createElem('br'));
  772. pElem.appendChild(createElem('b', { textContent: `${I18n.t('wmesu.common.Note')}:` }));
  773. pElem.appendChild(createTextNode(` ${I18n.t('wmesu.help.Step02note')}`));
  774. liElem = createElem('li');
  775. liElem.appendChild(pElem);
  776. olElem.appendChild(liElem);
  777. liElem = createElem('li');
  778. liElem.appendChild(createElem('p', { style: 'font-weight:100;margin-bottom:0px;', textContent: I18n.t('wmesu.help.Step03') }));
  779. olElem.appendChild(liElem);
  780. const divElemDivDiv = createElem('div');
  781. divElemDivDiv.appendChild(olElem);
  782. divElemDiv.appendChild(divElemDivDiv);
  783. divElemDiv.appendChild(createElem('b', { textContent: `${I18n.t('wmesu.common.Warning')}:` }));
  784. divElemDiv.appendChild(createTextNode(` ${I18n.t('wmesu.help.Warning01')}`));
  785. divElemDiv.appendChild(createElem('br'));
  786. divElemDiv.appendChild(createElem('br'));
  787. divElemDiv.appendChild(createElem('b', { textContent: `${I18n.t('wmesu.common.Note')}:` }));
  788. divElemDiv.appendChild(createTextNode(` ${I18n.t('wmesu.help.Note01')}`));
  789. docFrags.appendChild(divElemDiv);
  790. return docFrags;
  791. };
  792. const { tabLabel, tabPane } = W.userscripts.registerSidebarTab('SU!');
  793. tabLabel.textContent = 'SU!';
  794. tabLabel.title = _SCRIPT_LONG_NAME;
  795. tabPane.appendChild(tabContent());
  796. tabPane.id = 'WMESUSettings';
  797. await W.userscripts.waitForElementConnected(tabPane);
  798. logDebug('Enabling MOs.');
  799. W.selectionManager.events.register('selectionchanged', null, insertSimplifyStreetGeometryButtons);
  800. if (W.selectionManager.getSegmentSelection().segments.length > 0)
  801. insertSimplifyStreetGeometryButtons();
  802. window.addEventListener('beforeunload', () => { checkShortcutChanged(); }, false);
  803. new WazeWrap.Interface.Shortcut(
  804. 'runStraightenUpShortcut',
  805. 'Run straighten up',
  806. 'editing',
  807. 'Straighten Up',
  808. _settings.runStraightenUpShortcut,
  809. () => document.getElementById('WME-SU')?.dispatchEvent(new MouseEvent('click', { bubbles: true })),
  810. null
  811. ).add();
  812. showScriptInfoAlert();
  813. log(`Fully initialized in ${Math.round(performance.now() - _LOAD_BEGIN_TIME)} ms.`);
  814. setTimeout(checkShortcutChanged, 10000);
  815. }
  816.  
  817. function onWmeReady(tries = 1) {
  818. if (typeof tries === 'object')
  819. tries = 1;
  820. checkTimeout({ timeout: 'onWmeReady' });
  821. if (WazeWrap?.Ready) {
  822. logDebug('WazeWrap is ready. Proceeding with initialization.');
  823. onWazeWrapReady();
  824. }
  825. else if (tries < 1000) {
  826. logDebug(`WazeWrap is not in Ready state. Retrying ${tries} of 1000.`);
  827. _timeouts.onWmeReady = window.setTimeout(onWmeReady, 200, ++tries);
  828. }
  829. else {
  830. logError('onWmeReady timed out waiting for WazeWrap Ready state.');
  831. }
  832. }
  833.  
  834. function onWmeInitialized() {
  835. if (W.userscripts?.state?.isReady) {
  836. logDebug('W is ready and already in "wme-ready" state. Proceeding with initialization.');
  837. onWmeReady();
  838. }
  839. else {
  840. logDebug('W is ready, but state is not "wme-ready". Adding event listener.');
  841. document.addEventListener('wme-ready', onWmeReady, { once: true });
  842. }
  843. }
  844.  
  845. function bootstrap() {
  846. if (!W) {
  847. logDebug('W is not available. Adding event listener.');
  848. document.addEventListener('wme-initialized', onWmeInitialized, { once: true });
  849. }
  850. else {
  851. onWmeInitialized();
  852. }
  853. }
  854.  
  855. bootstrap();
  856. }
  857. )();