WME BDP Check (beta)

Check for possible BDP routes between two selected segments.

目前为 2023-03-15 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name WME BDP Check (beta)
  3. // @namespace https://greasyfork.org/users/166843
  4. // @version 2023.03.15.01
  5. // @description Check for possible BDP routes between two selected segments.
  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 none
  11. // @license GPLv3
  12. // ==/UserScript==
  13.  
  14. /* global $, GM_info, W, WazeWrap */
  15.  
  16. (function () {
  17. 'use strict';
  18.  
  19. const ALERT_UPDATE = true,
  20. DEBUG = true,
  21. LOAD_BEGIN_TIME = performance.now(),
  22. SCRIPT_AUTHOR = GM_info.script.author,
  23. SCRIPT_FORUM_URL = 'https://www.waze.com/forum/viewtopic.php?f=819&t=294789',
  24. SCRIPT_GF_URL = 'https://greasyfork.org/en/scripts/393407-wme-bdp-check',
  25. SCRIPT_NAME = GM_info.script.name.replace('(beta)', 'β'),
  26. SCRIPT_VERSION = GM_info.script.version,
  27. SCRIPT_VERSION_CHANGES = ['<b>CHANGE:</b> New bootstrap routine.',
  28. '<b>CHANGE:</b> Updated code to use optional chaining.',
  29. '<b>CHANGE:</b> Code structure with new linter options.',
  30. '<b>CHANGE:</b> Code cleanup.',
  31. '<b>CHANGE:</b> Utilize @match instead of @include in userscript headers.'
  32. ],
  33. SETTINGS_STORE_NAME = 'WMEBDPC',
  34. sleep = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)),
  35. _timeouts = { onWmeReady: undefined, saveSettingsToStorage: undefined },
  36. _editPanelObserver = new MutationObserver((mutations) => {
  37. if (W.selectionManager.getSegmentSelection().segments.length === 0)
  38. return;
  39. mutations.forEach((mutation) => {
  40. for (let i = 0; i < mutation.addedNodes.length; i++) {
  41. const addedNode = mutation.addedNodes[i];
  42. if (addedNode.nodeType === Node.ELEMENT_NODE) {
  43. if (addedNode.querySelector('#segment-edit-general .form-group.more-actions')) {
  44. if (W.selectionManager.getSegmentSelection().segments.length < 2) {
  45. if ($('#WME-BDPC-WME').length > 0)
  46. $('#WME-BDPC-WME').remove();
  47. if ($('#WME-BDPC-LM').length > 0)
  48. $('#WME-BDPC-LM').remove();
  49. }
  50. else if (W.selectionManager.getSegmentSelection().segments.length > 1) {
  51. insertCheckBDPButton();
  52. }
  53. }
  54. }
  55. }
  56. });
  57. });
  58. let _settings = {},
  59. _pathEndSegId,
  60. _restoreZoomLevel,
  61. _restoreMapCenter;
  62.  
  63. function log(message) { console.log('WME-BDPC:', message); }
  64. function logError(message) { console.error('WME-BDPC:', message); }
  65. function logWarning(message) { console.warn('WME-BDPC:', message); }
  66. function logDebug(message) {
  67. if (DEBUG)
  68. console.log('WME-BDPC:', message);
  69. }
  70.  
  71. async function loadSettingsFromStorage() {
  72. const defaultSettings = {
  73. lastSaved: 0,
  74. lastVersion: undefined
  75. },
  76. loadedSettings = $.parseJSON(localStorage.getItem(SETTINGS_STORE_NAME)),
  77. serverSettings = await WazeWrap.Remote.RetrieveSettings(SETTINGS_STORE_NAME);
  78. _settings = $.extend({}, defaultSettings, loadedSettings);
  79. if (serverSettings?.lastSaved > _settings.lastSaved)
  80. $.extend(_settings, serverSettings);
  81. _timeouts.saveSettingsToStorage = window.setTimeout(saveSettingsToStorage, 5000);
  82. return Promise.resolve();
  83. }
  84.  
  85. function saveSettingsToStorage() {
  86. checkTimeout({ timeout: 'saveSettingsToStorage' });
  87. if (localStorage) {
  88. _settings.lastVersion = SCRIPT_VERSION;
  89. _settings.lastSaved = Date.now();
  90. localStorage.setItem(SETTINGS_STORE_NAME, JSON.stringify(_settings));
  91. WazeWrap.Remote.SaveSettings(SETTINGS_STORE_NAME, _settings);
  92. logDebug('Settings saved.');
  93. }
  94. }
  95.  
  96. function showScriptInfoAlert() {
  97. if (ALERT_UPDATE && (SCRIPT_VERSION !== _settings.lastVersion)) {
  98. let releaseNotes = '';
  99. releaseNotes += '<p>What\'s new:</p>';
  100. if (SCRIPT_VERSION_CHANGES.length > 0) {
  101. releaseNotes += '<ul>';
  102. for (let idx = 0; idx < SCRIPT_VERSION_CHANGES.length; idx++)
  103. releaseNotes += `<li>${SCRIPT_VERSION_CHANGES[idx]}`;
  104. releaseNotes += '</ul>';
  105. }
  106. else {
  107. releaseNotes += '<ul><li>Nothing major.</ul>';
  108. }
  109. WazeWrap.Interface.ShowScriptUpdate(SCRIPT_NAME, SCRIPT_VERSION, releaseNotes, SCRIPT_GF_URL, SCRIPT_FORUM_URL);
  110. }
  111. }
  112.  
  113. function checkTimeout(obj) {
  114. if (obj.toIndex) {
  115. if (_timeouts[obj.timeout]?.[obj.toIndex]) {
  116. window.clearTimeout(_timeouts[obj.timeout][obj.toIndex]);
  117. delete (_timeouts[obj.timeout][obj.toIndex]);
  118. }
  119. }
  120. else {
  121. if (_timeouts[obj.timeout])
  122. window.clearTimeout(_timeouts[obj.timeout]);
  123. _timeouts[obj.timeout] = undefined;
  124. }
  125. }
  126.  
  127. function getMidpoint(startSeg, endSeg) {
  128. let startCenter = startSeg.getCenter(),
  129. endCenter = endSeg.getCenter();
  130. startCenter = WazeWrap.Geometry.ConvertTo4326(startCenter.x, startCenter.y);
  131. endCenter = WazeWrap.Geometry.ConvertTo4326(endCenter.x, endCenter.y);
  132. let lon1 = startCenter.lon,
  133. lat1 = startCenter.lat,
  134. lat2 = endCenter.lat;
  135. const piDiv = Math.PI / 180,
  136. divPi = 180 / Math.PI,
  137. lon2 = endCenter.lon,
  138. dLon = ((lon2 - lon1) * piDiv);
  139. lat1 *= piDiv;
  140. lat2 *= piDiv;
  141. lon1 *= piDiv;
  142. const bX = Math.cos(lat2) * Math.cos(dLon),
  143. bY = Math.cos(lat2) * Math.sin(dLon),
  144. lat3 = (Math.atan2(Math.sin(lat1) + Math.sin(lat2), Math.sqrt((Math.cos(lat1) + bX) * (Math.cos(lat1) + bX) + bY * bY))) * divPi,
  145. lon3 = (lon1 + Math.atan2(bY, Math.cos(lat1) + bX)) * divPi;
  146. return WazeWrap.Geometry.ConvertTo900913(lon3, lat3);
  147. }
  148.  
  149. async function doZoom(restore = false, zoom = -1, coordObj = {}) {
  150. if ((zoom === -1) || (Object.entries(coordObj).length === 0))
  151. return Promise.resolve();
  152. W.map.setCenter(coordObj);
  153. if (W.map.getZoom() !== zoom)
  154. W.map.getOLMap().zoomTo(zoom);
  155. if (restore) {
  156. _restoreZoomLevel = null;
  157. _restoreMapCenter = undefined;
  158. }
  159. else {
  160. WazeWrap.Alerts.info(SCRIPT_NAME, 'Waiting for WME to populate after zoom level change.<br>Proceeding in 2 seconds...');
  161. await sleep(2000);
  162. $('#toast-container-wazedev > .toast-info').find('.toast-close-button').click();
  163. }
  164. return Promise.resolve();
  165. }
  166.  
  167. function rtgContinuityCheck([...segs] = []) {
  168. if (segs.length < 2)
  169. return false;
  170. const rtg = { 7: 'mH', 6: 'MHFW', 3: 'MHFW' },
  171. seg1rtg = rtg[segs[0].attributes.roadType];
  172. segs.splice(0, 1);
  173. return segs.every((el) => seg1rtg === rtg[el.attributes.roadType]);
  174. }
  175.  
  176. function nameContinuityCheck([...segs] = []) {
  177. if (segs.length < 2)
  178. return false;
  179. const bs1StreetNames = [],
  180. bs2StreetNames = [],
  181. streetNames = [];
  182. let street;
  183. if (segs[0].attributes.primaryStreetID) {
  184. street = W.model.streets.getObjectById(segs[0].attributes.primaryStreetID);
  185. if (street?.name?.length > 0) {
  186. if (segs.length === 2)
  187. streetNames.push(street.name);
  188. else
  189. bs1StreetNames.push(street.name);
  190. }
  191. }
  192. if (segs[0].attributes.streetIDs.length > 0) {
  193. for (let i = 0; i < segs[0].attributes.streetIDs.length; i++) {
  194. street = W.model.streets.getObjectById(segs[0].attributes.streetIDs[i]);
  195. if (street?.name?.length > 0) {
  196. if (segs.length === 2)
  197. streetNames.push(street.name);
  198. else
  199. bs1StreetNames.push(street.name);
  200. }
  201. }
  202. }
  203. if (((segs.length === 2) && (streetNames.length === 0))
  204. || ((segs.length > 2) && (bs1StreetNames.length === 0)))
  205. return false;
  206. if (segs.length === 2) {
  207. if (segs[1].attributes.primaryStreetID) {
  208. street = W.model.streets.getObjectById(segs[1].attributes.primaryStreetID);
  209. if (street?.name && streetNames.includes(street.name))
  210. return true;
  211. }
  212. if (segs[1].attributes.streetIDs.length > 0) {
  213. for (let i = 0; i < segs[1].attributes.streetIDs.length; i++) {
  214. street = W.model.streets.getObjectById(segs[1].attributes.streetIDs[i]);
  215. if (street?.name && streetNames.includes(street.name))
  216. return true;
  217. }
  218. }
  219. }
  220. else {
  221. segs.splice(0, 1);
  222. const lastIdx = segs.length - 1;
  223. if (segs[lastIdx].attributes.primaryStreetID) {
  224. street = W.model.streets.getObjectById(segs[lastIdx].attributes.primaryStreetID);
  225. if (street?.name && (street.name.length > 0))
  226. bs2StreetNames.push(street.name);
  227. }
  228. if (segs[lastIdx].attributes.streetIDs.length > 0) {
  229. for (let i = 0; i < segs[lastIdx].attributes.streetIDs.length; i++) {
  230. street = W.model.streets.getObjectById(segs[lastIdx].attributes.streetIDs[i]);
  231. if (street?.name && (street.name.length > 0))
  232. bs2StreetNames.push(street.name);
  233. }
  234. }
  235. if (bs2StreetNames.length === 0)
  236. return false;
  237. segs.splice(-1, 1);
  238. return segs.every((el) => {
  239. if (el.attributes.primaryStreetID) {
  240. street = W.model.streets.getObjectById(el.attributes.primaryStreetID);
  241. if (street?.name && (bs1StreetNames.includes(street.name) || bs2StreetNames.includes(street.name)))
  242. return true;
  243. }
  244. if (el.attributes.streetIDs.length > 0) {
  245. for (let i = 0; i < el.attributes.streetIDs.length; i++) {
  246. street = W.model.streets.getObjectById(el.attributes.streetIDs[i]);
  247. if (street?.name && (bs1StreetNames.includes(street.name) || bs2StreetNames.includes(street.name)))
  248. return true;
  249. }
  250. }
  251. return false;
  252. });
  253. }
  254. return false;
  255. }
  256.  
  257. async function findLiveMapRoutes(startSeg, endSeg, maxLength) {
  258. const start900913center = startSeg.getCenter(),
  259. end900913center = endSeg.getCenter(),
  260. start4326Center = WazeWrap.Geometry.ConvertTo4326(start900913center.x, start900913center.y),
  261. end4326Center = WazeWrap.Geometry.ConvertTo4326(end900913center.x, end900913center.y),
  262. // eslint-disable-next-line no-nested-ternary
  263. url = (W.model.countries.getObjectById(235) || W.model.countries.getObjectById(40) || W.model.countries.getObjectById(182))
  264. ? '/RoutingManager/routingRequest'
  265. : W.model.countries.getObjectById(106)
  266. ? '/il-RoutingManager/routingRequest'
  267. : '/row-RoutingManager/routingRequest',
  268. data = {
  269. from: `x:${start4326Center.lon} y:${start4326Center.lat}`,
  270. to: `x:${end4326Center.lon} y:${end4326Center.lat}`,
  271. returnJSON: true,
  272. returnGeometries: true,
  273. returnInstructions: false,
  274. timeout: 60000,
  275. type: 'HISTORIC_TIME',
  276. nPaths: 6,
  277. clientVersion: '4.0.0',
  278. vehType: 'PRIVATE',
  279. options: 'AVOID_TOLL_ROADS:f,AVOID_PRIMARIES:f,AVOID_DANGEROUS_TURNS:f,AVOID_FERRIES:f,ALLOW_UTURNS:t'
  280. },
  281. returnRoutes = [];
  282. let jsonData = { error: false };
  283. try {
  284. jsonData = await $.ajax({
  285. dataType: 'JSON',
  286. cache: false,
  287. url,
  288. data,
  289. traditional: true,
  290. dataFilter: (retData) => retData.replace(/NaN/g, '0')
  291. }).fail((response, textStatus, errorThrown) => {
  292. logWarning(`Route request failed ${(textStatus ? `with ${textStatus}` : '')}\r\n${errorThrown}!\r\nResponse: ${response}`);
  293. });
  294. }
  295. catch (error) {
  296. logWarning(JSON.stringify(error));
  297. jsonData = { error };
  298. }
  299. if (!jsonData) {
  300. logWarning('No data returned.');
  301. }
  302. else if (jsonData.error) {
  303. logWarning(((typeof jsonData.error === 'object') ? $.parseJSON(jsonData.error) : jsonData.error.replace('|', '\r\n')));
  304. }
  305. else {
  306. let routes = jsonData.coords ? [jsonData] : [];
  307. if (jsonData.alternatives)
  308. routes = routes.concat(jsonData.alternatives);
  309. routes.forEach((route) => {
  310. const fullRouteSegIds = route.response.results.map((result) => result.path.segmentId),
  311. fullRouteSegs = W.model.segments.getByIds(fullRouteSegIds);
  312. if (nameContinuityCheck(fullRouteSegs) && rtgContinuityCheck(fullRouteSegs)) {
  313. const routeDistance = route.response.results.map((result) => result.length).slice(1, -1).reduce((a, b) => a + b);
  314. if (routeDistance < maxLength)
  315. returnRoutes.push(route.response.results.map((result) => result.path.segmentId));
  316. }
  317. });
  318. }
  319. return new Promise((resolve) => resolve(returnRoutes));
  320. }
  321.  
  322. function findDirectRoute(obj = {}) {
  323. const {
  324. maxLength, startSeg, startNode, endSeg, endNodeIds
  325. } = obj,
  326. processedSegs = [],
  327. sOutIds = startNode.attributes.segIDs.filter((segId) => segId !== startSeg.attributes.id),
  328. segIdsFilter = (nextSegIds, alreadyProcessed) => nextSegIds.filter((value) => alreadyProcessed.indexOf(value) === -1),
  329. getNextSegs = (nextSegIds, curSeg, nextNode) => {
  330. const rObj = { addPossibleRouteSegments: [] };
  331. for (let i = 0; i < nextSegIds.length; i++) {
  332. const nextSeg = W.model.segments.getObjectById(nextSegIds[i]);
  333. if ((nextNode.isTurnAllowedBySegDirections(curSeg, nextSeg) || curSeg.isTurnAllowed(nextSeg, nextNode))
  334. && nameContinuityCheck([curSeg, nextSeg])
  335. && (nameContinuityCheck([startSeg, nextSeg]) || nameContinuityCheck([endSeg, nextSeg]))
  336. ) {
  337. if (!processedSegs.some((o) => (o.fromSegId === curSeg.attributes.id) && (o.toSegId === nextSegIds[i]))) {
  338. rObj.addPossibleRouteSegments.push({ nextSegStartNode: nextNode, nextSeg });
  339. break;
  340. }
  341. }
  342. }
  343. return rObj;
  344. },
  345. returnRoutes = [];
  346. for (let i = 0, len = sOutIds.length; i < len; i++) {
  347. const sOut = W.model.segments.getObjectById(sOutIds[i]);
  348. if ((startNode.isTurnAllowedBySegDirections(startSeg, sOut) || startSeg.isTurnAllowed(sOut, startNode)) && nameContinuityCheck([startSeg, sOut])) {
  349. const possibleRouteSegments = [{
  350. curSeg: startSeg,
  351. nextSegStartNode: startNode,
  352. nextSeg: sOut
  353. }];
  354. let curLength = 0;
  355. while (possibleRouteSegments.length > 0) {
  356. const idx = possibleRouteSegments.length - 1,
  357. curSeg = possibleRouteSegments[idx].nextSeg,
  358. curSegStartNode = possibleRouteSegments[idx].nextSegStartNode,
  359. curSegEndNode = curSeg.getOtherNode(curSegStartNode),
  360. curSegEndNodeSOutIds = segIdsFilter(curSegEndNode.attributes.segIDs, possibleRouteSegments.map((routeSeg) => routeSeg.nextSeg.attributes.id));
  361. if ((endNodeIds.indexOf(curSegEndNode.attributes.id) > -1) && (curSegEndNode.isTurnAllowedBySegDirections(curSeg, endSeg) || curSeg.isTurnAllowed(endSeg, curSegEndNode))) {
  362. returnRoutes.push([startSeg.attributes.id].concat(possibleRouteSegments.map((routeSeg) => routeSeg.nextSeg.attributes.id), [endSeg.attributes.id]));
  363. possibleRouteSegments.splice(idx, 1);
  364. }
  365. else if ((curLength + curSeg.attributes.length) > maxLength) {
  366. possibleRouteSegments.splice(idx, 1);
  367. curLength -= curSeg.attributes.length;
  368. }
  369. else {
  370. const nextSegObj = getNextSegs(curSegEndNodeSOutIds, curSeg, curSegEndNode);
  371. if (nextSegObj.addPossibleRouteSegments.length > 0) {
  372. curLength += curSeg.attributes.length;
  373. possibleRouteSegments.push(nextSegObj.addPossibleRouteSegments[0]);
  374. processedSegs.push({ fromSegId: curSeg.attributes.id, toSegId: nextSegObj.addPossibleRouteSegments[0].nextSeg.attributes.id });
  375. }
  376. else {
  377. curLength -= curSeg.attributes.length;
  378. possibleRouteSegments.splice(idx, 1);
  379. }
  380. }
  381. }
  382. if (returnRoutes.length > 0)
  383. break;
  384. }
  385. else {
  386. processedSegs.push({ fromSegId: startSeg.attributes.id, toSegId: sOut.attributes.id });
  387. }
  388. }
  389. return returnRoutes;
  390. }
  391.  
  392. async function doCheckBDP(viaLM = false) {
  393. const selectedFeatures = W.selectionManager.getSelectedFeatures(),
  394. segmentSelection = W.selectionManager.getSegmentSelection(),
  395. numSelectedFeatureSegments = selectedFeatures.filter((feature) => feature.model.type === 'segment').length;
  396. let startSeg,
  397. endSeg,
  398. directRoutes = [];
  399. if ((segmentSelection.segments.length < 2) || (numSelectedFeatureSegments < 2)) {
  400. insertCheckBDPButton(true);
  401. WazeWrap.Alerts.error(SCRIPT_NAME, 'You must select either the two <i>bracketing segments</i> or an entire detour route with <i>bracketing segments</i>.');
  402. return;
  403. }
  404. if (segmentSelection.multipleConnectedComponents && ((segmentSelection.segments.length > 2) || (numSelectedFeatureSegments > 2))) {
  405. insertCheckBDPButton(true);
  406. WazeWrap.Alerts.error(
  407. SCRIPT_NAME,
  408. 'If you select more than 2 segments, the selection of segments must be continuous.<br><br>'
  409. + 'Either select just the two bracketing segments or an entire detour route with bracketing segments.'
  410. );
  411. return;
  412. }
  413. if (!segmentSelection.multipleConnectedComponents && (segmentSelection.segments.length === 2)) {
  414. insertCheckBDPButton(true);
  415. WazeWrap.Alerts.error(SCRIPT_NAME, 'You selected only two segments and they connect to each other. There are no alternate routes.');
  416. return;
  417. }
  418. if (segmentSelection.segments.length === 2) {
  419. [startSeg, endSeg] = segmentSelection.segments;
  420. }
  421. else if (_pathEndSegId) {
  422. if (segmentSelection.segments[0].attributes.id === _pathEndSegId) {
  423. [endSeg] = segmentSelection.segments;
  424. startSeg = segmentSelection.segments[segmentSelection.segments.length - 1];
  425. }
  426. else {
  427. [startSeg] = segmentSelection.segments;
  428. endSeg = segmentSelection.segments[segmentSelection.segments.length - 1];
  429. }
  430. const routeNodeIds = segmentSelection.segments.slice(1, -1).flatMap((segment) => [segment.attributes.toNodeID, segment.attributes.fromNodeID]);
  431. if (routeNodeIds.some((nodeId) => endSeg.attributes.fromNodeID === nodeId))
  432. endSeg.attributes.bdpcheck = { routeFarEndNodeId: endSeg.attributes.toNodeID };
  433. else
  434. endSeg.attributes.bdpcheck = { routeFarEndNodeId: endSeg.attributes.fromNodeID };
  435. }
  436. else {
  437. [startSeg] = segmentSelection.segments;
  438. endSeg = segmentSelection.segments[segmentSelection.segments.length - 1];
  439. const routeNodeIds = segmentSelection.segments.slice(1, -1).flatMap((segment) => [segment.attributes.toNodeID, segment.attributes.fromNodeID]);
  440. if (routeNodeIds.some((nodeId) => endSeg.attributes.fromNodeID === nodeId))
  441. endSeg.attributes.bdpcheck = { routeFarEndNodeId: endSeg.attributes.toNodeID };
  442. else
  443. endSeg.attributes.bdpcheck = { routeFarEndNodeId: endSeg.attributes.fromNodeID };
  444. }
  445. if ((startSeg.attributes.roadType < 3) || (startSeg.attributes.roadType === 4) || (startSeg.attributes.roadType === 5) || (startSeg.attributes.roadType > 7)
  446. || (endSeg.attributes.roadType < 3) || (endSeg.attributes.roadType === 4) || (endSeg.attributes.roadType === 5) || (endSeg.attributes.roadType > 7)
  447. ) {
  448. insertCheckBDPButton(true);
  449. WazeWrap.Alerts.info(SCRIPT_NAME, 'At least one of the bracketing selected segments is not in the correct road type group for BDP.');
  450. return;
  451. }
  452. if (!rtgContinuityCheck([startSeg, endSeg])) {
  453. insertCheckBDPButton(true);
  454. WazeWrap.Alerts.info(SCRIPT_NAME, 'One bracketing segment is a minor highway while the other is not. BDP only applies when bracketing segments are in the same road type group.');
  455. return;
  456. }
  457. if (!nameContinuityCheck([startSeg, endSeg])) {
  458. insertCheckBDPButton(true);
  459. WazeWrap.Alerts.info(SCRIPT_NAME, 'The bracketing segments do not share a street name. BDP will not be applied to any route.');
  460. return;
  461. }
  462. const maxLength = (startSeg.attributes.roadType === 7) ? 5000 : 50000;
  463. if (segmentSelection.segments.length === 2) {
  464. if (((startSeg.attributes.roadType === 7) && (W.map.getZoom() > 16))
  465. || ((startSeg.attributes.roadType !== 7) && (W.map.getZoom() > 15))) {
  466. _restoreZoomLevel = W.map.getZoom();
  467. _restoreMapCenter = W.map.getCenter();
  468. await doZoom(false, (startSeg.attributes.roadType === 7) ? 16 : 15, getMidpoint(startSeg, endSeg));
  469. }
  470. if (viaLM) {
  471. directRoutes = directRoutes.concat(await findLiveMapRoutes(startSeg, endSeg, maxLength));
  472. }
  473. else {
  474. const startSegDirection = startSeg.getDirection(),
  475. endSegDirection = endSeg.getDirection();
  476. const startNodeObjs = [],
  477. endNodeObjs = [];
  478. if ((startSegDirection !== 2) && startSeg.getToNode())
  479. startNodeObjs.push(startSeg.getToNode());
  480. if ((startSegDirection !== 1) && startSeg.getFromNode())
  481. startNodeObjs.push(startSeg.getFromNode());
  482. if ((endSegDirection !== 2) && endSeg.getFromNode())
  483. endNodeObjs.push(endSeg.getFromNode());
  484. if ((endSegDirection !== 1) && endSeg.getToNode())
  485. endNodeObjs.push(endSeg.getToNode());
  486. for (let i = 0; i < startNodeObjs.length; i++) {
  487. const startNode = startNodeObjs[i];
  488. directRoutes = findDirectRoute({
  489. maxLength, startSeg, startNode, endSeg, endNodeIds: endNodeObjs.map((nodeObj) => nodeObj?.attributes.id)
  490. });
  491. if (directRoutes.length > 0)
  492. break;
  493. }
  494. }
  495. }
  496. else {
  497. const routeSegIds = W.selectionManager.getSegmentSelection().getSelectedSegments()
  498. .map((segment) => segment.attributes.id)
  499. .filter((segId) => (segId !== endSeg.attributes.id) && (segId !== startSeg.attributes.id)),
  500. endNodeObj = endSeg.getOtherNode(W.model.nodes.getObjectById(endSeg.attributes.bdpcheck.routeFarEndNodeId)),
  501. startSegDirection = startSeg.getDirection(),
  502. startNodeObjs = [],
  503. lastDetourSegId = routeSegIds.filter((el) => endNodeObj.attributes.segIDs.includes(el));
  504. let lastDetourSeg;
  505. if (lastDetourSegId.length === 1) {
  506. lastDetourSeg = W.model.segments.getObjectById(lastDetourSegId);
  507. }
  508. else {
  509. const oneWayTest = W.model.segments.getByIds(lastDetourSegId).filter(
  510. (seg) => seg.isOneWay() && (endNodeObj.isTurnAllowedBySegDirections(endSeg, seg) || seg.isTurnAllowed(endSeg, endNodeObj))
  511. );
  512. if (oneWayTest.length === 1) {
  513. [lastDetourSeg] = oneWayTest;
  514. }
  515. else {
  516. insertCheckBDPButton(true);
  517. WazeWrap.Alerts.info(SCRIPT_NAME, `Could not determine the last detour segment. Please send ${SCRIPT_AUTHOR} a message with a PL describing this issue. Thank you!`);
  518. return;
  519. }
  520. }
  521. const detourSegs = segmentSelection.segments.slice(1, -1),
  522. detourSegTypes = [...new Set(detourSegs.map((segment) => segment.attributes.roadType))];
  523. if ([9, 10, 16, 18, 19, 22].some((type) => detourSegTypes.indexOf(type) > -1)) {
  524. insertCheckBDPButton(true);
  525. WazeWrap.Alerts.info(SCRIPT_NAME, 'Your selection contains one more more segments with an unrouteable road type. The selected route is not a valid route.');
  526. return;
  527. }
  528. if (![1].some((type) => detourSegTypes.indexOf(type) > -1)) {
  529. if (((startSeg.attributes.roadType === 7) && (W.map.getZoom() > 16))
  530. || ((startSeg.attributes.roadType !== 7) && (W.map.getZoom() > 15))) {
  531. _restoreZoomLevel = W.map.getZoom();
  532. _restoreMapCenter = W.map.getCenter();
  533. await doZoom(false, (startSeg.attributes.roadType === 7) ? 16 : 15, getMidpoint(startSeg, endSeg));
  534. }
  535. }
  536. if ((startSegDirection !== 2) && startSeg.getToNode())
  537. startNodeObjs.push(startSeg.getToNode());
  538. if ((startSegDirection !== 1) && startSeg.getFromNode())
  539. startNodeObjs.push(startSeg.getFromNode());
  540. if (nameContinuityCheck([lastDetourSeg, endSeg])) {
  541. insertCheckBDPButton(true);
  542. WazeWrap.Alerts.info(SCRIPT_NAME, 'BDP will not be applied to this detour route because the last detour segment and the second bracketing segment share a common street name.');
  543. doZoom(true, _restoreZoomLevel, _restoreMapCenter);
  544. return;
  545. }
  546. if (rtgContinuityCheck([lastDetourSeg, endSeg])) {
  547. insertCheckBDPButton(true);
  548. WazeWrap.Alerts.info(SCRIPT_NAME, 'BDP will not be applied to this detour route because the last detour segment and the second bracketing segment are in the same road type group.');
  549. doZoom(true, _restoreZoomLevel, _restoreMapCenter);
  550. return;
  551. }
  552. if (detourSegs.length < 2) {
  553. insertCheckBDPButton(true);
  554. WazeWrap.Alerts.info(SCRIPT_NAME, 'BDP will not be applied to this detour route because it is less than 2 segments long.');
  555. doZoom(true, _restoreZoomLevel, _restoreMapCenter);
  556. return;
  557. }
  558. if (detourSegs.map((seg) => seg.attributes.length).reduce((a, b) => a + b) > ((startSeg.attributes.roadType === 7) ? 500 : 5000)) {
  559. insertCheckBDPButton(true);
  560. WazeWrap.Alerts.info(SCRIPT_NAME, `BDP will not be applied to this detour route because it is longer than ${((startSeg.attributes.roadType === 7) ? '500m' : '5km')}.`);
  561. doZoom(true, _restoreZoomLevel, _restoreMapCenter);
  562. return;
  563. }
  564. if (viaLM) {
  565. directRoutes = directRoutes.concat(await findLiveMapRoutes(startSeg, endSeg, maxLength));
  566. }
  567. else {
  568. for (let i = 0; i < startNodeObjs.length; i++) {
  569. const startNode = startNodeObjs[i];
  570. directRoutes = findDirectRoute({
  571. maxLength, startSeg, startNode, endSeg, endNodeIds: [endNodeObj.attributes.id]
  572. });
  573. if (directRoutes.length > 0)
  574. break;
  575. }
  576. }
  577. }
  578. if (directRoutes.length > 0) {
  579. WazeWrap.Alerts.confirm(
  580. SCRIPT_NAME,
  581. 'A <b>direct route</b> was found! Would you like to select the direct route?',
  582. () => {
  583. const segments = [];
  584. for (let i = 0; i < directRoutes[0].length; i++) {
  585. const seg = W.model.segments.getObjectById(directRoutes[0][i]);
  586. if (seg !== 'undefined')
  587. segments.push(seg);
  588. }
  589. W.selectionManager.setSelectedModels(segments);
  590. doZoom(true, _restoreZoomLevel, _restoreMapCenter);
  591. },
  592. () => {
  593. doZoom(true, _restoreZoomLevel, _restoreMapCenter);
  594. insertCheckBDPButton(true);
  595. },
  596. 'Yes',
  597. 'No'
  598. );
  599. }
  600. else if (segmentSelection.segments.length === 2) {
  601. insertCheckBDPButton(true);
  602. WazeWrap.Alerts.info(
  603. SCRIPT_NAME,
  604. 'No direct routes found between the two selected segments. A BDP penalty <b>will not</b> be applied to any routes.'
  605. + '<br><b>Note:</b> This could also be caused by the distance between the two selected segments is longer than than the allowed distance for detours.'
  606. );
  607. doZoom(true, _restoreZoomLevel, _restoreMapCenter);
  608. }
  609. else {
  610. insertCheckBDPButton(true);
  611. WazeWrap.Alerts.info(
  612. SCRIPT_NAME,
  613. 'No direct routes found between the possible detour bracketing segments. A BDP penalty <b>will not</b> be applied to the selected route.'
  614. + '<br><b>Note:</b> This could also be because any possible direct routes are very long, which would take longer to travel than taking the selected route (even with penalty).'
  615. );
  616. doZoom(true, _restoreZoomLevel, _restoreMapCenter);
  617. }
  618. }
  619.  
  620. function insertCheckBDPButton(recreate = false) {
  621. const $wmeButton = $('#WME-BDPC-WME'),
  622. $lmButton = $('#WME-BDPC-LM'),
  623. $elem = $('#segment-edit-general .form-group.more-actions');
  624. if (recreate) {
  625. if ($wmeButton.length > 0)
  626. $wmeButton.remove();
  627. if ($lmButton.length > 0)
  628. $lmButton.remove();
  629. }
  630. if ($elem.length === 0)
  631. return;
  632. const buildElem = (id) => {
  633. if ($elem.find('wz-button').length > 0)
  634. return $('<wz-button>', { id, color: 'secondary', size: 'sm' });
  635. return $('<button>', { id, class: 'waze-btn waze-btn-small waze-btn-white' });
  636. };
  637. if ($('#WME-BDPC-WME').length === 0) {
  638. $elem.append(buildElem('WME-BDPC-WME', $elem).text('BDP Check (WME)').attr('title', 'Check BDP of selected segments, via WME.').click((e) => {
  639. e.preventDefault();
  640. doCheckBDP(false);
  641. }));
  642. }
  643. if ($('#WME-BDPC-LM').length === 0) {
  644. $elem.append(buildElem('WME-BDPC-LM', $elem).text('BDP Check (LM)').attr('title', 'Check BDP of selected segments, via LM.').click((e) => {
  645. e.preventDefault();
  646. doCheckBDP(true);
  647. }));
  648. }
  649. }
  650.  
  651. function pathSelected(evt) {
  652. if (evt?.feature?.model?.type === 'segment')
  653. _pathEndSegId = evt.feature.model.attributes.id;
  654. }
  655.  
  656. async function onWazeWrapReady() {
  657. log('Initializing.');
  658. await loadSettingsFromStorage();
  659. _editPanelObserver.observe(document.querySelector('#edit-panel'), {
  660. childList: true, attributes: false, attributeOldValue: false, characterData: false, characterDataOldValue: false, subtree: true
  661. });
  662. W.selectionManager.selectionMediator.on('map:selection:pathSelect', pathSelected);
  663. W.selectionManager.selectionMediator.on('map:selection:featureClick', () => { _pathEndSegId = undefined; });
  664. W.selectionManager.selectionMediator.on('map:selection:clickOut', () => { _pathEndSegId = undefined; });
  665. W.selectionManager.selectionMediator.on('map:selection:deselectKey', () => { _pathEndSegId = undefined; });
  666. W.selectionManager.selectionMediator.on('map:selection:featureBoxSelection', () => { _pathEndSegId = undefined; });
  667. showScriptInfoAlert();
  668. log(`Fully initialized in ${Math.round(performance.now() - LOAD_BEGIN_TIME)} ms.`);
  669. }
  670.  
  671. function onWmeReady(tries = 1) {
  672. if (typeof tries === 'object')
  673. tries = 1;
  674. checkTimeout({ timeout: 'onWmeReady' });
  675. if (WazeWrap?.Ready) {
  676. logDebug('WazeWrap is ready. Proceeding with initialization.');
  677. onWazeWrapReady();
  678. }
  679. else if (tries < 1000) {
  680. logDebug(`WazeWrap is not in Ready state. Retrying ${tries} of 1000.`);
  681. _timeouts.onWmeReady = window.setTimeout(onWmeReady, 200, ++tries);
  682. }
  683. else {
  684. logError(new Error('onWmeReady timed out waiting for WazeWrap Ready state.'));
  685. }
  686. }
  687.  
  688. function onWmeInitialized() {
  689. if (W.userscripts?.state?.isReady) {
  690. logDebug('W is ready and already in "wme-ready" state. Proceeding with initialization.');
  691. onWmeReady(1);
  692. }
  693. else {
  694. logDebug('W is ready, but state is not "wme-ready". Adding event listener.');
  695. document.addEventListener('wme-ready', onWmeReady, { once: true });
  696. }
  697. }
  698.  
  699. function bootstrap() {
  700. if (!W) {
  701. logDebug('W is not available. Adding event listener.');
  702. document.addEventListener('wme-initialized', onWmeInitialized, { once: true });
  703. }
  704. else {
  705. onWmeInitialized();
  706. }
  707. }
  708.  
  709. bootstrap();
  710. }
  711. )();