WME ClickSaver

Various UI changes to make editing faster and easier.

目前为 2025-03-14 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name WME ClickSaver
  3. // @namespace https://gf.qytechs.cn/users/45389
  4. // @version 2025.03.14.000
  5. // @description Various UI changes to make editing faster and easier.
  6. // @author MapOMatic
  7. // @include /^https:\/\/(www|beta)\.waze\.com\/(?!user\/)(.{2,6}\/)?editor\/?.*$/
  8. // @license GNU GPLv3
  9. // @connect sheets.googleapis.com
  10. // @connect gf.qytechs.cn
  11. // @contributionURL https://github.com/WazeDev/Thank-The-Authors
  12. // @grant GM_xmlhttpRequest
  13. // @grant GM_addElement
  14. // @require https://gf.qytechs.cn/scripts/24851-wazewrap/code/WazeWrap.js
  15. // @require https://update.gf.qytechs.cn/scripts/509664/WME%20Utils%20-%20Bootstrap.js
  16. // ==/UserScript==
  17.  
  18. /* global I18n */
  19. /* global WazeWrap */
  20. /* global bootstrap */
  21.  
  22. /* eslint-disable max-classes-per-file */
  23.  
  24. (function main() {
  25. 'use strict';
  26.  
  27. const updateMessage = '';
  28. const scriptName = GM_info.script.name;
  29. const scriptVersion = GM_info.script.version;
  30. const downloadUrl = 'https://gf.qytechs.cn/scripts/369629-wme-clicksaver/code/WME%20ClickSaver.user.js';
  31. const forumUrl = 'https://www.waze.com/forum/viewtopic.php?f=819&t=199894';
  32. const translationsUrl = 'https://sheets.googleapis.com/v4/spreadsheets/1ZlE9yhNncP9iZrPzFFa-FCtYuK58wNOEcmKqng4sH1M/values/ClickSaver';
  33. const apiKey = 'YTJWNVBVRkplbUZUZVVGMFl6aFVjMjVOTW0wNU5GaG5kVE40TUZoNWJVZEhWbU5rUjNacVdtdFlWUT09';
  34. const DEC = s => atob(atob(s));
  35. let sdk;
  36.  
  37. // This function is injected into the page.
  38. async function clicksaver(argsObject) {
  39. /* eslint-disable object-curly-newline */
  40. const roadTypeDropdownSelector = 'wz-select[name="roadType"]';
  41. const roadTypeChipSelector = 'wz-chip-select[class="road-type-chip-select"]';
  42. // const PARKING_SPACES_DROPDOWN_SELECTOR = 'select[name="estimatedNumberOfSpots"]';
  43. // const PARKING_COST_DROPDOWN_SELECTOR = 'select[name="costType"]';
  44. const settingsStoreName = 'clicksaver_settings';
  45. const defaultTranslation = {
  46. roadTypeButtons: {
  47. St: { text: 'St' },
  48. PS: { text: 'PS' },
  49. mH: { text: 'mH' },
  50. MH: { text: 'MH' },
  51. Fw: { text: 'Fw' },
  52. Rmp: { text: 'Rmp' },
  53. OR: { text: 'OR' },
  54. PLR: { text: 'PLR' },
  55. PR: { text: 'PR' },
  56. Fer: { text: 'Fer' },
  57. WT: { text: 'WT' },
  58. PB: { text: 'PB' },
  59. Sw: { text: 'Sw' },
  60. RR: { text: 'RR' },
  61. RT: { text: 'RT' },
  62. Pw: { text: 'Pw' }
  63. },
  64. prefs: {
  65. dropdownHelperGroup: 'DROPDOWN HELPERS',
  66. roadTypeButtons: 'Add road type buttons',
  67. useOldRoadColors: 'Use old road colors (requires refresh)',
  68. setStreetCityToNone: 'Set Street/City to None (new seg\'s only)',
  69. // eslint-disable-next-line camelcase
  70. setStreetCityToNone_Title: 'NOTE: Only works if connected directly or indirectly'
  71. + ' to a segment with State / Country already set.',
  72. setCityToConnectedSegCity: 'Set City to connected segment\'s City',
  73. parkingCostButtons: 'Add PLA cost buttons',
  74. parkingSpacesButtons: 'Add PLA estimated spaces buttons',
  75. timeSaversGroup: 'TIME SAVERS',
  76. discussionForumLinkText: 'Discussion Forum',
  77. showAddAltCityButton: 'Show "Add alt city" button',
  78. showSwapDrivingWalkingButton: 'Show "Swap driving<->walking segment type" button',
  79. // eslint-disable-next-line camelcase
  80. showSwapDrivingWalkingButton_Title: 'Swap between driving-type and walking-type segments. WARNING! This will DELETE and recreate the segment. Nodes may need to be reconnected.',
  81. showSwitchStreetNamesButton: 'Show swap primary and alternative street name button',
  82. addCompactColors: 'Add colors to compact mode road type buttons'
  83. },
  84. swapSegmentTypeWarning: 'This will DELETE the segment and recreate it. Any speed data will be lost, and nodes will need to be reconnected. This message will only be displayed once. Continue?',
  85. // eslint-disable-next-line camelcase
  86. swapSegmentTypeError_Paths: 'Paths must be removed from segment before changing between driving and pedestrian road type.',
  87. addAltCityButtonText: 'Add alt city'
  88. };
  89.  
  90. // Road types defined in the WME SDK documentation
  91. const wmeRoadType = {
  92. ALLEY: 22,
  93. FERRY: 15,
  94. FREEWAY: 3,
  95. MAJOR_HIGHWAY: 6,
  96. MINOR_HIGHWAY: 7,
  97. OFF_ROAD: 8,
  98. PARKING_LOT_ROAD: 20,
  99. PEDESTRIAN_BOARDWALK: 10,
  100. PRIMARY_STREET: 2,
  101. PRIVATE_ROAD: 17,
  102. RAILROAD: 18,
  103. RAMP: 4,
  104. RUNWAY_TAXIWAY: 19,
  105. STAIRWAY: 16,
  106. STREET: 1,
  107. WALKING_TRAIL: 5,
  108. WALKWAY: 9
  109. };
  110. const roadTypeSettings = {
  111. St: { id: wmeRoadType.STREET, wmeColor: '#ffffeb', svColor: '#ffffff', category: 'streets', visible: true },
  112. PS: { id: wmeRoadType.PRIMARY_STREET, wmeColor: '#f0ea58', svColor: '#cba12e', category: 'streets', visible: true },
  113. Pw: { id: wmeRoadType.ALLEY, wmeColor: '#64799a', svColor: '#64799a', category: 'streets', visible: false },
  114. mH: { id: wmeRoadType.MINOR_HIGHWAY, wmeColor: '#69bf88', svColor: '#ece589', category: 'highways', visible: true },
  115. MH: { id: wmeRoadType.MAJOR_HIGHWAY, wmeColor: '#45b8d1', svColor: '#c13040', category: 'highways', visible: true },
  116. Fw: { id: wmeRoadType.FREEWAY, wmeColor: '#c577d2', svColor: '#387fb8', category: 'highways', visible: false },
  117. Rmp: { id: wmeRoadType.RAMP, wmeColor: '#b3bfb3', svColor: '#58c53b', category: 'highways', visible: false },
  118. OR: { id: wmeRoadType.OFF_ROAD, wmeColor: '#867342', svColor: '#82614a', category: 'otherDrivable', visible: false },
  119. PLR: { id: wmeRoadType.PARKING_LOT_ROAD, wmeColor: '#ababab', svColor: '#2282ab', category: 'otherDrivable', visible: true },
  120. PR: { id: wmeRoadType.PRIVATE_ROAD, wmeColor: '#beba6c', svColor: '#00ffb3', category: 'otherDrivable', visible: true },
  121. Fer: { id: wmeRoadType.FERRY, wmeColor: '#d7d8f8', svColor: '#ff8000', category: 'otherDrivable', visible: false },
  122. RR: { id: wmeRoadType.RAILROAD, wmeColor: '#c62925', svColor: '#ffffff', category: 'nonDrivable', visible: false },
  123. RT: { id: wmeRoadType.RUNWAY_TAXIWAY, wmeColor: '#ffffff', svColor: '#00ff00', category: 'nonDrivable', visible: false },
  124. WT: { id: wmeRoadType.WALKING_TRAIL, wmeColor: '#b0a790', svColor: '#00ff00', category: 'pedestrian', visible: false },
  125. PB: { id: wmeRoadType.PEDESTRIAN_BOARDWALK, wmeColor: '#9a9a9a', svColor: '#0000ff', category: 'pedestrian', visible: false },
  126. Sw: { id: wmeRoadType.STAIRWAY, wmeColor: '#999999', svColor: '#b700ff', category: 'pedestrian', visible: false }
  127. };
  128.  
  129. /* eslint-enable object-curly-newline */
  130. let _settings = {};
  131. let trans; // Translation object
  132.  
  133. // function log(message) {
  134. // console.log('ClickSaver:', message);
  135. // }
  136.  
  137. function logDebug(message) {
  138. console.debug('ClickSaver:', message);
  139. }
  140.  
  141. // function logWarning(message) {
  142. // console.warn('ClickSaver:', message);
  143. // }
  144.  
  145. // function logError(message) {
  146. // console.error('ClickSaver:', message);
  147. // }
  148.  
  149. function isChecked(checkboxId) {
  150. return $(`#${checkboxId}`).is(':checked');
  151. }
  152.  
  153. function isSwapPedestrianPermitted() {
  154. const userInfo = sdk.State.getUserInfo();
  155. const rank = userInfo.rank + 1;
  156. return rank >= 4 || (rank === 3 && userInfo.isAreaManager);
  157. }
  158.  
  159. function setChecked(checkboxId, checked) {
  160. $(`#${checkboxId}`).prop('checked', checked);
  161. }
  162. function loadSettingsFromStorage() {
  163. const loadedSettings = $.parseJSON(localStorage.getItem(settingsStoreName));
  164. const defaultSettings = {
  165. lastVersion: null,
  166. roadButtons: true,
  167. roadTypeButtons: ['St', 'PS', 'mH', 'MH', 'Fw', 'Rmp', 'PLR', 'PR', 'PB'],
  168. parkingCostButtons: true,
  169. parkingSpacesButtons: true,
  170. setNewPLRStreetToNone: true,
  171. setNewPLRCity: true,
  172. setNewPRStreetToNone: false,
  173. setNewPRCity: false,
  174. setNewRRStreetToNone: true, // added by jm6087
  175. setNewRRCity: false, // added by jm6087
  176. setNewPBStreetToNone: true, // added by jm6087
  177. setNewPBCity: true, // added by jm6087
  178. setNewORStreetToNone: false,
  179. setNewORCity: false,
  180. addAltCityButton: true,
  181. addSwapPedestrianButton: false,
  182. useOldRoadColors: false,
  183. warnOnPedestrianTypeSwap: true,
  184. addCompactColors: true,
  185. addSwitchPrimaryNameButton: false,
  186. shortcuts: {}
  187. };
  188. _settings = { ...defaultSettings, ...loadedSettings };
  189.  
  190. setChecked('csRoadTypeButtonsCheckBox', _settings.roadButtons);
  191. if (_settings.roadTypeButtons) {
  192. Object.keys(roadTypeSettings).forEach(roadTypeAbbr1 => {
  193. setChecked(`cs${roadTypeAbbr1}CheckBox`, _settings.roadTypeButtons.indexOf(roadTypeAbbr1) !== -1);
  194. });
  195. }
  196.  
  197. if (_settings.roadButtons) {
  198. $('.csRoadTypeButtonsCheckBoxContainer').show();
  199. } else {
  200. $('.csRoadTypeButtonsCheckBoxContainer').hide();
  201. }
  202. // setChecked('csParkingSpacesButtonsCheckBox', _settings.parkingSpacesButtons);
  203. // setChecked('csParkingCostButtonsCheckBox', _settings.parkingCostButtons);
  204. setChecked('csSetNewPLRCityCheckBox', _settings.setNewPLRCity);
  205. setChecked('csClearNewPLRCheckBox', _settings.setNewPLRStreetToNone);
  206. setChecked('csSetNewPRCityCheckBox', _settings.setNewPRCity);
  207. setChecked('csClearNewPRCheckBox', _settings.setNewPRStreetToNone);
  208. setChecked('csSetNewRRCityCheckBox', _settings.setNewRRCity);
  209. setChecked('csClearNewRRCheckBox', _settings.setNewRRStreetToNone); // added by jm6087
  210. setChecked('csSetNewPBCityCheckBox', _settings.setNewPBCity);
  211. setChecked('csClearNewPBCheckBox', _settings.setNewPBStreetToNone); // added by jm6087
  212. setChecked('csSetNewORCityCheckBox', _settings.setNewORCity);
  213. setChecked('csClearNewORCheckBox', _settings.setNewORStreetToNone);
  214. setChecked('csUseOldRoadColorsCheckBox', _settings.useOldRoadColors);
  215. setChecked('csAddAltCityButtonCheckBox', _settings.addAltCityButton);
  216. setChecked('csAddSwapPedestrianButtonCheckBox', _settings.addSwapPedestrianButton);
  217. setChecked('csAddCompactColorsCheckBox', _settings.addCompactColors);
  218. setChecked('csAddSwitchPrimaryNameCheckBox', _settings.addSwitchPrimaryNameButton);
  219. }
  220.  
  221. function saveSettingsToStorage() {
  222. const settings = {
  223. lastVersion: argsObject.scriptVersion,
  224. roadButtons: _settings.roadButtons,
  225. parkingCostButtons: _settings.parkingCostButtons,
  226. parkingSpacesButtons: _settings.parkingSpacesButtons,
  227. setNewPLRCity: _settings.setNewPLRCity,
  228. setNewPLRStreetToNone: _settings.setNewPLRStreetToNone,
  229. setNewPRCity: _settings.setNewPRCity,
  230. setNewPRStreetToNone: _settings.setNewPRStreetToNone,
  231. setNewRRCity: _settings.setNewRRCity,
  232. setNewRRStreetToNone: _settings.setNewRRStreetToNone,
  233. setNewPBCity: _settings.setNewPBCity,
  234. setNewPBStreetToNone: _settings.setNewPBStreetToNone,
  235. setNewORCity: _settings.setNewORCity,
  236. setNewORStreetToNone: _settings.setNewORStreetToNone,
  237. useOldRoadColors: _settings.useOldRoadColors,
  238. addAltCityButton: _settings.addAltCityButton,
  239. addSwapPedestrianButton: _settings.addSwapPedestrianButton,
  240. warnOnPedestrianTypeSwap: _settings.warnOnPedestrianTypeSwap,
  241. addCompactColors: _settings.addCompactColors,
  242. addSwitchPrimaryNameButton: _settings.addSwitchPrimaryNameButton,
  243. shortcuts: {}
  244. };
  245. sdk.Shortcuts.getAllShortcuts().forEach(shortcut => {
  246. settings.shortcuts[shortcut.shortcutId] = shortcut.shortcutKeys;
  247. });
  248. settings.roadTypeButtons = [];
  249. Object.keys(roadTypeSettings).forEach(roadTypeAbbr => {
  250. if (_settings.roadTypeButtons.indexOf(roadTypeAbbr) !== -1) {
  251. settings.roadTypeButtons.push(roadTypeAbbr);
  252. }
  253. });
  254. localStorage.setItem(settingsStoreName, JSON.stringify(settings));
  255. logDebug('Settings saved');
  256. }
  257.  
  258. function isPedestrianTypeSegment(segment) {
  259. const pedRoadTypes = Object.values(roadTypeSettings)
  260. .filter(roadType => roadType.category === 'pedestrian')
  261. .map(roadType => roadType.id);
  262. return pedRoadTypes.includes(segment.roadType);
  263. }
  264.  
  265. function getConnectedSegmentIDs(segmentId) {
  266. return [
  267. ...sdk.DataModel.Segments.getConnectedSegments({ segmentId, reverseDirection: false }),
  268. ...sdk.DataModel.Segments.getConnectedSegments({ segmentId, reverseDirection: true })
  269. ].map(segment => segment.id);
  270. }
  271.  
  272. function getFirstConnectedSegmentAddress(segmentId) {
  273. const nonMatches = [];
  274. const segmentIDsToSearch = [segmentId];
  275. const hasAddress = id => !sdk.DataModel.Segments.getAddress({ segmentId: id }).isEmpty;
  276. while (segmentIDsToSearch.length > 0) {
  277. const startSegmentID = segmentIDsToSearch.pop();
  278. const connectedSegmentIDs = getConnectedSegmentIDs(startSegmentID);
  279. const hasAddrSegmentId = connectedSegmentIDs.find(hasAddress);
  280. if (hasAddrSegmentId) return sdk.DataModel.Segments.getAddress({ segmentId: hasAddrSegmentId });
  281.  
  282. nonMatches.push(startSegmentID);
  283. connectedSegmentIDs.forEach(segmentID => {
  284. if (nonMatches.indexOf(segmentID) === -1 && segmentIDsToSearch.indexOf(segmentID) === -1) {
  285. segmentIDsToSearch.push(segmentID);
  286. }
  287. });
  288. }
  289. return null;
  290. }
  291.  
  292. function setStreetAndCity(setCity) {
  293. const selection = sdk.Editing.getSelection();
  294.  
  295. selection?.ids.forEach(segmentId => {
  296. if (sdk.DataModel.Segments.getAddress({ segmentId }).isEmpty) {
  297. const addr = getFirstConnectedSegmentAddress(segmentId);
  298. if (addr) {
  299. // Process the city
  300. const newCityProperties = {
  301. cityName: setCity && !addr.city?.isEmpty ? addr.city.name : '',
  302. countryId: addr.country.id,
  303. stateId: addr.state.id
  304. };
  305. let newCityId = sdk.DataModel.Cities.getCity(newCityProperties)?.id;
  306. if (newCityId == null) {
  307. newCityId = sdk.DataModel.Cities.addCity(newCityProperties);
  308. }
  309.  
  310. // Process the street
  311. const newPrimaryStreetId = getOrCreateStreet('', newCityId).id;
  312.  
  313. // Update the segment with the new street
  314. sdk.DataModel.Segments.updateAddress({ segmentId, primaryStreetId: newPrimaryStreetId });
  315. }
  316. }
  317. });
  318. }
  319.  
  320. class WaitForElementError extends Error { }
  321.  
  322. function waitForElem(selector) {
  323. return new Promise((resolve, reject) => {
  324. function checkIt(tries = 0) {
  325. if (tries < 150) { // try for about 3 seconds;
  326. const elem = document.querySelector(selector);
  327. setTimeout(() => {
  328. if (!elem) {
  329. checkIt(++tries);
  330. } else {
  331. resolve(elem);
  332. }
  333. }, 20);
  334. } else {
  335. reject(new WaitForElementError(`Element was not found within 3 seconds: ${selector}`));
  336. }
  337. }
  338. checkIt();
  339. });
  340. }
  341.  
  342. async function waitForShadowElem(parentElemSelector, shadowElemSelectors) {
  343. const parentElem = await waitForElem(parentElemSelector);
  344. return new Promise((resolve, reject) => {
  345. shadowElemSelectors.forEach((shadowElemSelector, idx) => {
  346. function checkIt(parent, tries = 0) {
  347. if (tries < 150) { // try for about 3 seconds;
  348. const shadowElem = parent.shadowRoot.querySelector(shadowElemSelector);
  349. setTimeout(() => {
  350. if (!shadowElem) {
  351. checkIt(parent, ++tries);
  352. } else if (idx === shadowElemSelectors.length - 1) {
  353. resolve({ shadowElem, parentElem });
  354. } else {
  355. checkIt(shadowElem, 0);
  356. }
  357. }, 20);
  358. } else {
  359. reject(new WaitForElementError(`Shadow element was not found within 3 seconds: ${shadowElemSelector}`));
  360. }
  361. }
  362. checkIt(parentElem);
  363. });
  364. });
  365. }
  366.  
  367. async function onAddAltCityButtonClick() {
  368. const segmentId = sdk.Editing.getSelection().ids[0];
  369. const addr = sdk.DataModel.Segments.getAddress({ segmentId });
  370.  
  371. $('wz-button[class="add-alt-street-btn"]').click();
  372. await waitForElem('wz-autocomplete.alt-street-name');
  373.  
  374. // Set the street name field
  375. let result = await waitForShadowElem('wz-autocomplete.alt-street-name', ['wz-text-input']);
  376. result.shadowElem.focus();
  377. result.shadowElem.value = addr?.street?.name ?? '';
  378.  
  379. // Clear the city name field
  380. result = await waitForShadowElem('wz-autocomplete.alt-city-name', ['wz-text-input']);
  381. result.shadowElem.focus();
  382. result.shadowElem.value = null;
  383. }
  384.  
  385. function onRoadTypeButtonClick(roadType) {
  386. const selection = sdk.Editing.getSelection();
  387.  
  388. // Temporarily remove this while bugs are worked out.
  389. // WS.SDKMultiActionHack.groupActions(() => {
  390. selection?.ids.forEach(segmentId => {
  391. // Check for same roadType is necessary to prevent an error.
  392. if (sdk.DataModel.Segments.getById({ segmentId }).roadType !== roadType) {
  393. sdk.DataModel.Segments.updateSegment({ segmentId, roadType });
  394. }
  395. });
  396.  
  397. if (roadType === roadTypeSettings.PLR.id && isChecked('csClearNewPLRCheckBox')) {
  398. setStreetAndCity(isChecked('csSetNewPLRCityCheckBox'));
  399. } else if (roadType === roadTypeSettings.PR.id && isChecked('csClearNewPRCheckBox')) {
  400. setStreetAndCity(isChecked('csSetNewPRCityCheckBox'));
  401. } else if (roadType === roadTypeSettings.RR.id && isChecked('csClearNewRRCheckBox')) {
  402. setStreetAndCity(isChecked('csSetNewRRCityCheckBox'));
  403. } else if (roadType === roadTypeSettings.PB && isChecked('csClearNewPBCheckBox')) {
  404. setStreetAndCity(isChecked('csSetNewPBCityCheckBox'));
  405. } else if (roadType === roadTypeSettings.OR.id && isChecked('csClearNewORCheckBox')) {
  406. setStreetAndCity(isChecked('csSetNewORCityCheckBox'));
  407. }
  408. // });
  409. }
  410.  
  411. function addRoadTypeButtons() {
  412. const segmentId = sdk.Editing.getSelection()?.ids[0];
  413. if (segmentId == null) return;
  414. const sdkSeg = sdk.DataModel.Segments.getById({ segmentId });
  415. if (!sdkSeg) return;
  416. const isPed = isPedestrianTypeSegment(sdkSeg);
  417. const $dropDown = $(roadTypeDropdownSelector);
  418. $('#csRoadTypeButtonsContainer').remove();
  419. const $container = $('<div>', { id: 'csRoadTypeButtonsContainer', class: 'cs-rt-buttons-container', style: 'display: inline-table;' });
  420. const $street = $('<div>', { id: 'csStreetButtonContainer', class: 'cs-rt-buttons-group' });
  421. const $highway = $('<div>', { id: 'csHighwayButtonContainer', class: 'cs-rt-buttons-group' });
  422. const $otherDrivable = $('<div>', { id: 'csOtherDrivableButtonContainer', class: 'cs-rt-buttons-group' });
  423. const $nonDrivable = $('<div>', { id: 'csNonDrivableButtonContainer', class: 'cs-rt-buttons-group' });
  424. const $pedestrian = $('<div>', { id: 'csPedestrianButtonContainer', class: 'cs-rt-buttons-group' });
  425. const divs = {
  426. streets: $street,
  427. highways: $highway,
  428. otherDrivable: $otherDrivable,
  429. nonDrivable: $nonDrivable,
  430. pedestrian: $pedestrian
  431. };
  432. Object.keys(roadTypeSettings).forEach(roadTypeKey => {
  433. if (_settings.roadTypeButtons.includes(roadTypeKey)) {
  434. const roadTypeSetting = roadTypeSettings[roadTypeKey];
  435. const isDisabled = $dropDown[0].hasAttribute('disabled') && $dropDown[0].getAttribute('disabled') === 'true';
  436. if (!isDisabled && ((roadTypeSetting.category === 'pedestrian' && isPed) || (roadTypeSetting.category !== 'pedestrian' && !isPed))) {
  437. const $div = divs[roadTypeSetting.category];
  438. $div.append(
  439. $('<div>', {
  440. class: `btn cs-rt-button cs-rt-button-${roadTypeKey} btn-positive`,
  441. title: I18n.t('segment.road_types')[roadTypeSetting.id]
  442. })
  443. .text(trans.roadTypeButtons[roadTypeKey].text)
  444. .prop('checked', roadTypeSetting.visible)
  445. .data('rtId', roadTypeSetting.id)
  446. .click(function rtbClick() { onRoadTypeButtonClick($(this).data('rtId')); })
  447. );
  448. }
  449. }
  450. });
  451. if (isPed) {
  452. $container.append($pedestrian);
  453. } else {
  454. $container.append($street).append($highway).append($otherDrivable).append($nonDrivable);
  455. }
  456. $dropDown.before($container);
  457. }
  458.  
  459. // Function to add an event listener to the chip select for the road type in compact mode
  460. function addCompactRoadTypeChangeEvents() {
  461. const chipSelect = document.getElementsByClassName('road-type-chip-select')[0];
  462. chipSelect.addEventListener('chipSelected', evt => {
  463. const rtValue = evt.detail.value;
  464. onRoadTypeButtonClick(rtValue);
  465. });
  466. }
  467.  
  468. // Function to add road type colors to the chips in compact mode
  469. async function addCompactRoadTypeColors() {
  470. // TODO: Clean this up. Was combined from two functions.
  471. try {
  472. if (sdk.Settings.getUserSettings().isCompactMode
  473. && isChecked('csAddCompactColorsCheckBox')
  474. && sdk.Editing.getSelection()) {
  475. const useOldColors = _settings.useOldRoadColors;
  476. await waitForElem('.road-type-chip-select wz-checkable-chip');
  477. $('.road-type-chip-select wz-checkable-chip').addClass('cs-compact-button');
  478. Object.values(roadTypeSettings).forEach(roadType => {
  479. const bgColor = useOldColors ? roadType.svColor : roadType.wmeColor;
  480. const rtChip = $(`.road-type-chip-select wz-checkable-chip[value=${roadType.id}]`);
  481. if (rtChip.length !== 1) return;
  482. waitForShadowElem(`.road-type-chip-select wz-checkable-chip[value='${roadType.id}']`, ['div']).then(result => {
  483. const $elem = $(result.shadowElem);
  484. const padding = $elem.hasClass('checked') ? '0px 3px' : '0px 4px';
  485. $elem.css({ backgroundColor: bgColor, padding, color: 'black' });
  486. });
  487. });
  488.  
  489. const result = await waitForShadowElem('.road-type-chip-select wz-checkable-chip[checked=""]', ['div']);
  490. $(result.shadowElem).css({ border: 'black 2px solid', padding: '0px 3px' });
  491.  
  492. $('.road-type-chip-select wz-checkable-chip').each(function updateRoadTypeChip() {
  493. const style = {};
  494. if (this.getAttribute('checked') === 'false') {
  495. style.border = '';
  496. style.padding = '0px 4px';
  497. } else {
  498. style.border = 'black 2px solid';
  499. style.padding = '0px 3px';
  500. }
  501. $(this.shadowRoot.querySelector('div')).css(style);
  502. });
  503. }
  504. } catch (ex) {
  505. if (ex instanceof WaitForElementError) {
  506. // waitForElem will throw an error if Undo causes a deselection. Ignore it.
  507. } else {
  508. throw ex;
  509. }
  510. }
  511. }
  512.  
  513. // function isPLA(item) {
  514. // return (item.model.type === 'venue') && item.model.attributes.categories.includes('PARKING_LOT');
  515. // }
  516.  
  517. // function addParkingSpacesButtons() {
  518. // const $dropDown = $(PARKING_SPACES_DROPDOWN_SELECTOR);
  519. // const selItems = W.selectionManager.getSelectedFeatures();
  520. // const item = selItems[0];
  521.  
  522. // // If it's not a PLA, exit.
  523. // if (!isPLA(item)) return;
  524.  
  525. // $('#csParkingSpacesContainer').remove();
  526. // const $div = $('<div>', { id: 'csParkingSpacesContainer' });
  527. // const dropdownDisabled = $dropDown.attr('disabled') === 'disabled';
  528. // const optionNodes = $(`${PARKING_SPACES_DROPDOWN_SELECTOR} option`);
  529.  
  530. // for (let i = 0; i < optionNodes.length; i++) {
  531. // const $option = $(optionNodes[i]);
  532. // const text = $option.text();
  533. // const selected = $option.val() === $dropDown.val();
  534. // $div.append(
  535. // // TODO css
  536. // $('<div>', {
  537. // class: `btn waze-btn waze-btn-white${selected ? ' waze-btn-blue' : ''}${dropdownDisabled ? ' disabled' : ''}`,
  538. // style: 'margin-bottom: 5px; height: 22px; padding: 2px 8px 0px 8px; margin-right: 3px;'
  539. // })
  540. // .text(text)
  541. // .data('val', $option.val())
  542. // // eslint-disable-next-line func-names
  543. // .hover(() => { })
  544. // .click(function onParkingSpacesButtonClick() {
  545. // if (!dropdownDisabled) {
  546. // $(PARKING_SPACES_DROPDOWN_SELECTOR).val($(this).data('val')).change();
  547. // addParkingSpacesButtons();
  548. // }
  549. // })
  550. // );
  551. // }
  552.  
  553. // $dropDown.before($div);
  554. // $dropDown.hide();
  555. // }
  556.  
  557. // function addParkingCostButtons() {
  558. // const $dropDown = $(PARKING_COST_DROPDOWN_SELECTOR);
  559. // const selItems = W.selectionManager.getSelectedFeatures();
  560. // const item = selItems[0];
  561.  
  562. // // If it's not a PLA, exit.
  563. // if (!isPLA(item)) return;
  564.  
  565. // $('#csParkingCostContainer').remove();
  566. // const $div = $('<div>', { id: 'csParkingCostContainer' });
  567. // const dropdownDisabled = $dropDown.attr('disabled') === 'disabled';
  568. // const optionNodes = $(`${PARKING_COST_DROPDOWN_SELECTOR} option`);
  569. // for (let i = 0; i < optionNodes.length; i++) {
  570. // const $option = $(optionNodes[i]);
  571. // const text = $option.text();
  572. // const selected = $option.val() === $dropDown.val();
  573. // $div.append(
  574. // $('<div>', {
  575. // class: `btn waze-btn waze-btn-white${selected ? ' waze-btn-blue' : ''}${dropdownDisabled ? ' disabled' : ''}`,
  576. // // TODO css
  577. // style: 'margin-bottom: 5px; height: 22px; padding: 2px 8px 0px 8px; margin-right: 4px;'
  578. // })
  579. // .text(text !== '' ? text : '?')
  580. // .data('val', $option.val())
  581. // // eslint-disable-next-line func-names
  582. // .hover(() => { })
  583. // .click(function onParkingCostButtonClick() {
  584. // if (!dropdownDisabled) {
  585. // $(PARKING_COST_DROPDOWN_SELECTOR).val($(this).data('val')).change();
  586. // addParkingCostButtons();
  587. // }
  588. // })
  589. // );
  590. // }
  591.  
  592. // $dropDown.before($div);
  593. // $dropDown.hide();
  594. // }
  595.  
  596. function addAddAltCityButton() {
  597. // Only show the button if every segment has the same primary city and street.
  598. if (!selectedPrimaryStreetsAreEqual()) {
  599. return;
  600. }
  601.  
  602. const id = 'csAddAltCityButton';
  603. if ($(`#${id}`).length === 0) {
  604. $('div.address-edit').prev('wz-label').append(
  605. $('<a>', {
  606. href: '#',
  607. // TODO css
  608. style: 'float: right;text-transform: none;'
  609. + 'font-family: "Helvetica Neue", Helvetica, "Open Sans", sans-serif;color: #26bae8;'
  610. + 'font-weight: normal;'
  611. }).text(trans.addAltCityButtonText).click(onAddAltCityButtonClick)
  612. );
  613. }
  614. }
  615.  
  616. async function addSwitchPrimaryNameButton() {
  617. if (!isChecked('csAddSwitchPrimaryNameCheckBox')) {
  618. return;
  619. }
  620. if (!selectedPrimaryStreetsAreEqual() || !selectedAltStreetsAreEqual()) {
  621. return;
  622. }
  623.  
  624. await waitForElem('.alt-streets-control');
  625.  
  626. // eslint-disable-next-line func-names
  627. $('span.alt-street-preview').each(function() {
  628. const id = 'csAddSwitchPrimaryName';
  629. const altStreetId = Number($(this).attr('data-id'));
  630. const switchingIconElement = $(this).find(`#${id}`);
  631.  
  632. if (streetEqualsPrimaryStreetName(altStreetId)) {
  633. switchingIconElement.remove();
  634. return;
  635. }
  636.  
  637. const switchingIconExists = switchingIconElement.length > 0;
  638. if (switchingIconExists) {
  639. return;
  640. }
  641. const switchStreetNameButton = $('<i>', {
  642. id,
  643. class: 'w-icon w-icon-arrow-up alt-edit-button'
  644. });
  645.  
  646. $(this).append(switchStreetNameButton);
  647. switchStreetNameButton.click(onSwitchStreetNamesClick);
  648. });
  649. }
  650.  
  651. function onSwitchStreetNamesClick() {
  652. const selectedSegments = getSelectedSegments();
  653. const currentPrimaryStreet = sdk.DataModel.Segments.getAddress({ segmentId: selectedSegments[0] });
  654. const currentAltStreets = currentPrimaryStreet.altStreets.map(street => street.street);
  655. const selectedStreetId = Number($(this).parent().attr('data-id'));
  656. const newPrimary = currentAltStreets
  657. .find(street => street.id === selectedStreetId);
  658.  
  659. // WS.SDKMultiActionHack.groupActions(() => {
  660. const newPrimaryStreet = getOrCreateStreet(newPrimary.name, currentPrimaryStreet.city.id);
  661. const primaryToAltStreet = getOrCreateStreet(currentPrimaryStreet.street.name, newPrimary.cityId);
  662.  
  663. const newAltStreetsIds = currentAltStreets
  664. .map(alt => alt.id)
  665. .filter(id => id !== selectedStreetId);
  666. newAltStreetsIds.push(primaryToAltStreet.id);
  667. selectedSegments.forEach(segmentId => sdk.DataModel.Segments.updateAddress({
  668. segmentId,
  669. primaryStreetId: newPrimaryStreet.id,
  670. alternateStreetIds: newAltStreetsIds
  671. }));
  672. // });
  673. }
  674.  
  675. function addSwapPedestrianButton() { // Added displayMode argument to identify compact vs. regular mode.
  676. const id = 'csSwapPedestrianContainer';
  677. $(`#${id}`).remove();
  678. const selection = sdk.Editing.getSelection();
  679. if (selection?.ids.length === 1 && selection.objectType === 'segment') {
  680. // TODO css
  681. const $container = $('<div>', { id, style: 'white-space: nowrap;float: right;display: inline;' });
  682. const $button = $('<div>', {
  683. id: 'csBtnSwapPedestrianRoadType',
  684. title: '',
  685. // TODO css
  686. style: 'display:inline-block;cursor:pointer;'
  687. });
  688. $button.append('<i class="w-icon w-icon-streetview w-icon-lg"></i><i class="fa fa-arrows-h fa-lg" style="color: #e84545;vertical-align: top;"></i><i class="w-icon w-icon-car w-icon-lg"></i>')
  689. .attr({
  690. title: trans.prefs.showSwapDrivingWalkingButton_Title
  691. });
  692. $container.append($button);
  693.  
  694. // Insert swap button in the correct location based on display mode.
  695. const $label = $('#segment-edit-general > form > div > div.road-type-control > wz-label');
  696. $label.css({ display: 'inline' }).append($container);
  697.  
  698. $('#csBtnSwapPedestrianRoadType').click(onSwapPedestrianButtonClick);
  699. }
  700. }
  701.  
  702. function onSwapPedestrianButtonClick() {
  703. if (_settings.warnOnPedestrianTypeSwap) {
  704. _settings.warnOnPedestrianTypeSwap = false;
  705. saveSettingsToStorage();
  706. if (!confirm(trans.swapSegmentTypeWarning)) {
  707. return;
  708. }
  709. }
  710.  
  711. const originalSegment = sdk.DataModel.Segments.getById({ segmentId: sdk.Editing.getSelection().ids[0] });
  712.  
  713. // Copy the selected segment geometry and attributes, then delete it.
  714. const oldPrimaryStreetId = originalSegment.primaryStreetId;
  715. const oldAltStreetIds = originalSegment.alternateStreetIds;
  716.  
  717. // WS.SDKMultiActionHack.groupActions(() => {
  718. const newRoadType = isPedestrianTypeSegment(originalSegment) ? wmeRoadType.STREET : wmeRoadType.WALKING_TRAIL;
  719. try {
  720. sdk.DataModel.Segments.deleteSegment({ segmentId: originalSegment.id });
  721. } catch (ex) {
  722. if (ex instanceof sdk.Errors.InvalidStateError) {
  723. WazeWrap.Alerts.error(scriptName, 'Something prevents this segment from being deleted.');
  724. return;
  725. }
  726. }
  727.  
  728. // create the replacement segment in the other segment type (pedestrian -> road & vice versa)
  729.  
  730. const newSegmentId = sdk.DataModel.Segments.addSegment({ geometry: originalSegment.geometry, roadType: newRoadType });
  731.  
  732. sdk.DataModel.Segments.updateAddress({
  733. segmentId: newSegmentId,
  734. primaryStreetId: oldPrimaryStreetId,
  735. alternateStreetIds: oldAltStreetIds
  736. });
  737.  
  738. sdk.Editing.setSelection({ selection: { ids: [newSegmentId], objectType: 'segment' } });
  739. // });
  740. }
  741.  
  742. function getSelectedSegments() {
  743. const selection = sdk.Editing.getSelection();
  744. if (selection?.objectType !== 'segment') {
  745. return null;
  746. }
  747. return selection.ids;
  748. }
  749.  
  750. function selectedPrimaryStreetsAreEqual() {
  751. const selection = getSelectedSegments();
  752. if (!selection) {
  753. return false;
  754. }
  755. if (selection.length === 1) {
  756. return true;
  757. }
  758.  
  759. const firstStreetId = sdk.DataModel.Segments.getAddress({ segmentId: selection[0] })?.street?.id;
  760. return selection
  761. .map(segmentId => sdk.DataModel.Segments.getAddress({ segmentId }))
  762. .every(addr => addr.street?.id === firstStreetId);
  763. }
  764.  
  765. function selectedAltStreetsAreEqual() {
  766. const selection = getSelectedSegments();
  767. if (!selection) {
  768. return false;
  769. }
  770. const addresses = selection.map(segmentId => sdk.DataModel.Segments.getAddress({ segmentId }))
  771. .map(street => street.altStreets.map(altStreet => altStreet.street.id))
  772. .map(addr => new Set(addr));
  773.  
  774. const firstAltAddresses = addresses[0];
  775. return addresses
  776. .every(address => address.size === firstAltAddresses.size && Array.from(address).every(value => firstAltAddresses.has(value)));
  777. }
  778.  
  779. function getOrCreateStreet(streetName, cityId) {
  780. return sdk.DataModel.Streets.getStreet({ streetName, cityId })
  781. ?? sdk.DataModel.Streets.addStreet({ streetName, cityId });
  782. }
  783.  
  784. function streetEqualsPrimaryStreetName(altStreetId) {
  785. const selection = getSelectedSegments();
  786. const primaryStreetName = selection
  787. .map(segmentId => sdk.DataModel.Segments.getAddress({ segmentId }))[0].street?.name;
  788. const selectedStreetName = sdk.DataModel.Streets.getById({ streetId: altStreetId })?.name;
  789. return primaryStreetName === selectedStreetName;
  790. }
  791.  
  792. /* eslint-disable no-bitwise, no-mixed-operators */
  793. function shadeColor2(color, percent) {
  794. const f = parseInt(color.slice(1), 16);
  795. const t = percent < 0 ? 0 : 255;
  796. const p = percent < 0 ? percent * -1 : percent;
  797. const R = f >> 16;
  798. const G = f >> 8 & 0x00FF;
  799. const B = f & 0x0000FF;
  800. return `#${(0x1000000 + (Math.round((t - R) * p) + R) * 0x10000 + (Math.round((t - G) * p) + G)
  801. * 0x100 + (Math.round((t - B) * p) + B)).toString(16).slice(1)}`;
  802. }
  803. /* eslint-enable no-bitwise, no-mixed-operators */
  804.  
  805. function buildRoadTypeButtonCss() {
  806. const lines = [];
  807. const useOldColors = _settings.useOldRoadColors;
  808. Object.keys(roadTypeSettings).forEach(roadTypeAbbr => {
  809. const roadType = roadTypeSettings[roadTypeAbbr];
  810. const bgColor = useOldColors ? roadType.svColor : roadType.wmeColor;
  811. let output = `.cs-rt-buttons-container .cs-rt-button-${roadTypeAbbr} {background-color:${
  812. bgColor};box-shadow:0 2px ${shadeColor2(bgColor, -0.5)};border-color:${shadeColor2(bgColor, -0.15)};}`;
  813. output += ` .cs-rt-buttons-container .cs-rt-button-${roadTypeAbbr}:hover {background-color:${
  814. shadeColor2(bgColor, 0.2)}}`;
  815. lines.push(output);
  816. });
  817. return lines.join(' ');
  818. }
  819.  
  820. function injectCss() {
  821. const css = [
  822. // Road type button formatting
  823. '.csRoadTypeButtonsCheckBoxContainer {margin-left:15px;}',
  824. '.cs-rt-buttons-container {margin-bottom:5px;height:21px;}',
  825. '.cs-rt-buttons-container .cs-rt-button {font-size:11px;line-height:20px;color:black;padding:0px 4px;height:20px;'
  826. + 'margin-right:2px;border-style:solid;border-width:1px;}',
  827. buildRoadTypeButtonCss(),
  828. '.btn.cs-rt-button:active {box-shadow:none;transform:translateY(2px)}',
  829. 'div .cs-rt-buttons-group {float:left; margin: 0px 5px 5px 0px;}',
  830. '#sidepanel-clicksaver .controls-container {padding:0px;}',
  831. '#sidepanel-clicksaver .controls-container label {white-space: normal;}',
  832. '#sidepanel-clicksaver {font-size:13px;}',
  833.  
  834. // Compact moad road type button formatting.
  835. '.cs-compact-button[checked="false"] {opacity: 0.65;}',
  836.  
  837. // Lock button formatting
  838. '.cs-group-label {font-size: 11px; width: 100%; font-family: Poppins, sans-serif;'
  839. + ' text-transform: uppercase; font-weight: 700; color: #354148; margin-bottom: 6px;}'
  840. ].join(' ');
  841. $(`<style type="text/css">${css}</style>`).appendTo('head');
  842. }
  843.  
  844. function createSettingsCheckbox(id, settingName, labelText, titleText, divCss, labelCss, optionalAttributes) {
  845. const $container = $('<div>', { class: 'controls-container' });
  846. const $input = $('<input>', {
  847. type: 'checkbox', class: 'csSettingsCheckBox', name: id, id, 'data-setting-name': settingName
  848. }).appendTo($container);
  849. const $label = $('<label>', { for: id }).text(labelText).appendTo($container);
  850. // TODO css
  851. if (divCss) $container.css(divCss);
  852. // TODO css
  853. if (labelCss) $label.css(labelCss);
  854. if (titleText) $container.attr({ title: titleText });
  855. if (optionalAttributes) $input.attr(optionalAttributes);
  856. return $container;
  857. }
  858.  
  859. async function initUserPanel() {
  860. const $roadTypesDiv = $('<div>', { class: 'csRoadTypeButtonsCheckBoxContainer' });
  861. $roadTypesDiv.append(
  862. createSettingsCheckbox('csUseOldRoadColorsCheckBox', 'useOldRoadColors', trans.prefs.useOldRoadColors)
  863. );
  864. Object.keys(roadTypeSettings).forEach(roadTypeAbbr => {
  865. const roadType = roadTypeSettings[roadTypeAbbr];
  866. const id = `cs${roadTypeAbbr}CheckBox`;
  867. const title = I18n.t('segment.road_types')[roadType.id];
  868. $roadTypesDiv.append(
  869. createSettingsCheckbox(id, 'roadType', title, null, null, null, {
  870. 'data-road-type': roadTypeAbbr
  871. })
  872. );
  873. if (['PLR', 'PR', 'RR', 'PB', 'OR'].includes(roadTypeAbbr)) { // added RR & PB by jm6087
  874. $roadTypesDiv.append(
  875. // TODO css
  876. createSettingsCheckbox(
  877. `csClearNew${roadTypeAbbr}CheckBox`,
  878. `setNew${roadTypeAbbr}StreetToNone`,
  879. trans.prefs.setStreetCityToNone,
  880. trans.prefs.setStreetCityToNone_Title,
  881. { paddingLeft: '20px', marginRight: '4px' },
  882. { fontStyle: 'italic' }
  883. ),
  884. createSettingsCheckbox(
  885. `csSetNew${roadTypeAbbr}CityCheckBox`,
  886. `setNew${roadTypeAbbr}City`,
  887. trans.prefs.setCityToConnectedSegCity,
  888. '',
  889. { paddingLeft: '30px', marginRight: '4px' },
  890. { fontStyle: 'italic' }
  891. )
  892. );
  893. }
  894. });
  895.  
  896. const $panel = $('<div>', { id: 'sidepanel-clicksaver' }).append(
  897. $('<div>', { class: 'side-panel-section>' }).append(
  898. // TODO css
  899. $('<div>', { style: 'margin-bottom:8px;' }).append(
  900. $('<div>', { class: 'form-group' }).append(
  901. $('<label>', { class: 'cs-group-label' }).text(trans.prefs.dropdownHelperGroup),
  902. $('<div>').append(
  903. createSettingsCheckbox(
  904. 'csRoadTypeButtonsCheckBox',
  905. 'roadButtons',
  906. trans.prefs.roadTypeButtons
  907. )
  908. ).append($roadTypesDiv),
  909. createSettingsCheckbox(
  910. 'csAddCompactColorsCheckBox',
  911. 'addCompactColors',
  912. trans.prefs.addCompactColors
  913. )
  914. ),
  915. $('<label>', { class: 'cs-group-label' }).text(trans.prefs.timeSaversGroup),
  916. $('<div>', { style: 'margin-bottom:8px;' }).append(
  917. createSettingsCheckbox(
  918. 'csAddAltCityButtonCheckBox',
  919. 'addAltCityButton',
  920. trans.prefs.showAddAltCityButton
  921. ),
  922. isSwapPedestrianPermitted() ? createSettingsCheckbox(
  923. 'csAddSwapPedestrianButtonCheckBox',
  924. 'addSwapPedestrianButton',
  925. trans.prefs.showSwapDrivingWalkingButton
  926. ) : '',
  927. createSettingsCheckbox(
  928. 'csAddSwitchPrimaryNameCheckBox',
  929. 'addSwitchPrimaryNameButton',
  930. trans.prefs.showSwitchStreetNamesButton
  931. )
  932. )
  933. )
  934. )
  935. );
  936.  
  937. $panel.append(
  938. // TODO css
  939. $('<div>', { style: 'margin-top:20px;font-size:10px;color:#999999;' }).append(
  940. $('<div>').text(`v. ${argsObject.scriptVersion}${argsObject.scriptName.toLowerCase().includes('beta') ? ' beta' : ''}`),
  941. $('<div>').append(
  942. $('<a>', { href: argsObject.forumUrl, target: '__blank' }).text(trans.prefs.discussionForumLinkText)
  943. )
  944. )
  945. );
  946.  
  947. const { tabLabel, tabPane } = await sdk.Sidebar.registerScriptTab();
  948. $(tabLabel).text('CS');
  949. $(tabPane).append($panel);
  950. // Decrease spacing around the tab contents.
  951. $(tabPane).parent().css({ 'padding-top': '0px', 'padding-left': '8px' });
  952.  
  953. // Add change events
  954. $('#csRoadTypeButtonsCheckBox').change(function onRoadTypeButtonCheckChanged() {
  955. if (this.checked) {
  956. $('.csRoadTypeButtonsCheckBoxContainer').show();
  957. } else {
  958. $('.csRoadTypeButtonsCheckBoxContainer').hide();
  959. }
  960. saveSettingsToStorage();
  961. });
  962. $('.csSettingsCheckBox').change(function onSettingsCheckChanged() {
  963. const { checked } = this;
  964. const settingName = $(this).data('setting-name');
  965. if (settingName === 'roadType') {
  966. const roadType = $(this).data('road-type');
  967. const array = _settings.roadTypeButtons;
  968. const index = array.indexOf(roadType);
  969. if (checked && index === -1) {
  970. array.push(roadType);
  971. } else if (!checked && index !== -1) {
  972. array.splice(index, 1);
  973. }
  974. } else {
  975. _settings[settingName] = checked;
  976. }
  977. saveSettingsToStorage();
  978. });
  979. }
  980.  
  981. function updateControls() {
  982. if ($(roadTypeDropdownSelector).length > 0) {
  983. if (isChecked('csRoadTypeButtonsCheckBox')) addRoadTypeButtons();
  984. }
  985. addCompactRoadTypeColors();
  986. if (isSwapPedestrianPermitted() && isChecked('csAddSwapPedestrianButtonCheckBox')) {
  987. addSwapPedestrianButton();
  988. }
  989. // if ($(PARKING_SPACES_DROPDOWN_SELECTOR).length > 0 && isChecked('csParkingSpacesButtonsCheckBox')) {
  990. // addParkingSpacesButtons(); // TODO - add option setting
  991. // }
  992. // if ($(PARKING_COST_DROPDOWN_SELECTOR).length > 0 && isChecked('csParkingCostButtonsCheckBox')) {
  993. // addParkingCostButtons(); // TODO - add option setting
  994. // }
  995. }
  996.  
  997. function replaceWord(target, searchWord, replaceWithWord) {
  998. return target.replace(new RegExp(`\\b${searchWord}\\b`, 'g'), replaceWithWord);
  999. }
  1000.  
  1001. function titleCase(word) {
  1002. return word.charAt(0).toUpperCase() + word.substring(1).toLowerCase();
  1003. }
  1004. function mcCase(word) {
  1005. return word.charAt(0).toUpperCase() + word.charAt(1).toLowerCase()
  1006. + word.charAt(2).toUpperCase() + word.substring(3).toLowerCase();
  1007. }
  1008. function upperCase(word) {
  1009. return word.toUpperCase();
  1010. }
  1011.  
  1012. function processSubstring(target, substringRegex, processFunction) {
  1013. const substrings = target.match(substringRegex);
  1014. if (substrings) {
  1015. for (let idx = 0; idx < substrings.length; idx++) {
  1016. const substring = substrings[idx];
  1017. const newSubstring = processFunction(substring);
  1018. target = replaceWord(target, substring, newSubstring);
  1019. }
  1020. }
  1021. return target;
  1022. }
  1023.  
  1024. function onPaste(e) {
  1025. const targetNode = e.target;
  1026. if (targetNode.name === 'streetName' || targetNode.className.includes('street-name')) {
  1027. // Get the text that's being pasted.
  1028. let pastedText = e.clipboardData.getData('text/plain');
  1029.  
  1030. // If pasting text in ALL CAPS...
  1031. if (/^[^a-z]*$/.test(pastedText)) {
  1032. [
  1033. // Title case all words first.
  1034. [/\b[a-zA-Z]+(?:'S)?\b/g, titleCase],
  1035.  
  1036. // Then process special cases.
  1037. [/\bMC\w+\b/ig, mcCase], // e.g. McCaulley
  1038. [/\b(?:I|US|SH|SR|CH|CR|CS|PR|PS)\s*-?\s*\d+\w*\b/ig, upperCase], // e.g. US-25, US25
  1039. /* eslint-disable-next-line max-len */
  1040. [/\b(?:AL|AK|AS|AZ|AR|CA|CO|CT|DE|DC|FM|FL|GA|GU|HI|ID|IL|IN|IA|KS|KY|LA|ME|MH|MD|MA|MI|MN|MS|MO|MT|NE|NV|NH|NJ|NM|NY|NC|ND|MP|OH|OK|OR|PW|PA|PR|RI|SC|SD|TN|TX|UT|VT|VI|VA|WA|WV|WI|WY)\s*-?\s*\d+\w*\b/ig, upperCase], // e.g. WV-52
  1041. [/\b(?:NE|NW|SE|SW)\b/ig, upperCase]
  1042. ].forEach(item => {
  1043. pastedText = processSubstring(pastedText, item[0], item[1]);
  1044. });
  1045.  
  1046. // Insert new text in the focused node.
  1047. document.execCommand('insertText', false, pastedText);
  1048.  
  1049. // Prevent the default paste behavior.
  1050. e.preventDefault();
  1051. return false;
  1052. }
  1053. }
  1054. return true;
  1055. }
  1056.  
  1057. function getTranslationObject() {
  1058. if (argsObject.useDefaultTranslation) {
  1059. return defaultTranslation;
  1060. }
  1061. let locale = I18n.currentLocale().toLowerCase();
  1062. if (!argsObject.translations.hasOwnProperty(locale)) {
  1063. locale = 'en-us';
  1064. }
  1065. return argsObject.translations[locale];
  1066. }
  1067.  
  1068. function errorHandler(callback) {
  1069. try {
  1070. callback();
  1071. } catch (ex) {
  1072. console.error(`${argsObject.scriptName}:`, ex);
  1073. }
  1074. }
  1075.  
  1076. /**
  1077. * This event handler is needed in the following scenarios:
  1078. * 1. When the user changes the selected compact road type chip to adjust its styling.
  1079. * 2. When the switch alternative name button is clicked.
  1080. */
  1081. function onSegmentsChanged() {
  1082. addCompactRoadTypeColors();
  1083. addSwitchPrimaryNameButton();
  1084. }
  1085.  
  1086. async function onCopyCoordinatesShortcut() {
  1087. try {
  1088. const center = sdk.Map.getMapCenter();
  1089. const output = `${center.lat.toFixed(5)}, ${center.lon.toFixed(5)}`;
  1090. await navigator.clipboard.writeText(output);
  1091. WazeWrap.Alerts.info('WME ClickSaver', `Map center coordinate copied to clipboard:\n${output}`, false, false, 2000);
  1092. // console.debug('Map coordinates copied to clipboard:', center);
  1093. } catch (err) {
  1094. console.error('Failed to copy map center coordinates to clipboard: ', err);
  1095. }
  1096. }
  1097.  
  1098. function onToggleDrawNewRoadsAsTwoWayShortcut() {
  1099. const options = sdk.Settings.getUserSettings();
  1100. options.isCreateRoadsAsTwoWay = !options.isCreateRoadsAsTwoWay;
  1101. sdk.Settings.setUserSettings(options);
  1102. WazeWrap.Alerts.info('WME ClickSaver', `New segments will be drawn as <b>${options.isCreateRoadsAsTwoWay ? 'two-way' : 'one-way'}</b>.`, false, false, 2000);
  1103. }
  1104.  
  1105. function createShortcut(shortcutId, description, callback) {
  1106. let shortcutKeys = _settings.shortcuts?.[shortcutId] ?? null;
  1107. if (shortcutKeys && sdk.Shortcuts.areShortcutKeysInUse({ shortcutKeys })) {
  1108. shortcutKeys = null;
  1109. }
  1110. sdk.Shortcuts.createShortcut({
  1111. shortcutId,
  1112. shortcutKeys,
  1113. description,
  1114. callback
  1115. });
  1116. }
  1117.  
  1118. async function init() {
  1119. logDebug('Initializing...');
  1120.  
  1121. trans = getTranslationObject();
  1122. Object.keys(roadTypeSettings).forEach(rtName => {
  1123. roadTypeSettings[rtName].text = trans.roadTypeButtons[rtName].text;
  1124. });
  1125.  
  1126. document.addEventListener('paste', onPaste);
  1127.  
  1128. sdk.Events.trackDataModelEvents({ dataModelName: 'segments' });
  1129. sdk.Events.on({
  1130. eventName: 'wme-data-model-objects-changed',
  1131. eventHandler: () => errorHandler(onSegmentsChanged)
  1132. });
  1133. sdk.Events.on({
  1134. eventName: 'wme-selection-changed',
  1135. eventHandler: () => errorHandler(updateControls)
  1136. });
  1137.  
  1138. // check for changes in the edit-panel
  1139. const observer = new MutationObserver(mutations => {
  1140. mutations.forEach(mutation => {
  1141. for (let i = 0; i < mutation.addedNodes.length; i++) {
  1142. const addedNode = mutation.addedNodes[i];
  1143.  
  1144. if (addedNode.nodeType === Node.ELEMENT_NODE) {
  1145. // Checks to identify if this is a segment in regular display mode.
  1146. if (addedNode.querySelector(roadTypeDropdownSelector)) {
  1147. if (isChecked('csRoadTypeButtonsCheckBox')) addRoadTypeButtons();
  1148. if (isSwapPedestrianPermitted() && isChecked('csAddSwapPedestrianButtonCheckBox')) {
  1149. addSwapPedestrianButton();
  1150. }
  1151. }
  1152. // Checks to identify if this is a segment in compact display mode.
  1153. if (addedNode.querySelector(roadTypeChipSelector)) {
  1154. if (isChecked('csRoadTypeButtonsCheckBox')) addCompactRoadTypeChangeEvents();
  1155. if (isSwapPedestrianPermitted() && isChecked('csAddSwapPedestrianButtonCheckBox')) {
  1156. addSwapPedestrianButton();
  1157. }
  1158. }
  1159. // if (addedNode.querySelector(PARKING_SPACES_DROPDOWN_SELECTOR) && isChecked('csParkingSpacesButtonsCheckBox')) {
  1160. // addParkingSpacesButtons();
  1161. // }
  1162. // if (addedNode.querySelector(PARKING_COST_DROPDOWN_SELECTOR)
  1163. // && isChecked('csParkingCostButtonsCheckBox')) {
  1164. // addParkingCostButtons();
  1165. // }
  1166. if (addedNode.querySelector('.side-panel-section') && isChecked('csAddAltCityButtonCheckBox')) {
  1167. addAddAltCityButton();
  1168. }
  1169. if (addedNode.querySelector('.alt-streets') && isChecked('csAddSwitchPrimaryNameCheckBox')) {
  1170. // Cancel button doesn't change the datamodel so re-add the switch arrow on cancel click
  1171. // eslint-disable-next-line func-names
  1172. addedNode.addEventListener('click', event => {
  1173. if (event.target.classList.contains('alt-address-cancel-button')) {
  1174. addSwitchPrimaryNameButton();
  1175. }
  1176. });
  1177. addSwitchPrimaryNameButton();
  1178. }
  1179. }
  1180. }
  1181. });
  1182. });
  1183.  
  1184. observer.observe(document.getElementById('edit-panel'), { childList: true, subtree: true });
  1185. await initUserPanel();
  1186. loadSettingsFromStorage();
  1187. createShortcut('toggleTwoWaySegDrawingShortcut', 'Toggle new segment two-way drawing', onToggleDrawNewRoadsAsTwoWayShortcut);
  1188. createShortcut('copyCoordinatesShortcut', 'Copy map center coordinates', onCopyCoordinatesShortcut);
  1189. window.addEventListener('beforeunload', saveSettingsToStorage, false);
  1190. injectCss();
  1191. updateControls(); // In case of PL w/ segments selected.
  1192.  
  1193. logDebug('Initialized');
  1194. }
  1195.  
  1196. function skipLoginDialog(tries = 0) {
  1197. if (sdk || tries === 1000) return;
  1198. if ($('wz-button.do-login').length) {
  1199. $('wz-button.do-login').click();
  1200. return;
  1201. }
  1202. setTimeout(skipLoginDialog, 100, ++tries);
  1203. }
  1204. skipLoginDialog();
  1205.  
  1206. sdk = await bootstrap({ scriptUpdateMonitor: { downloadUrl } });
  1207.  
  1208. init();
  1209. } // END clicksaver function (used to be injected, now just runs as a function)
  1210.  
  1211. // function exists(...objects) {
  1212. // return objects.every(object => typeof object !== 'undefined' && object !== null);
  1213. // }
  1214.  
  1215. function injectScript(argsObject) {
  1216. // 3/31/2023 - removing script injection due to loading errors that I can't track down ("require is not defined").
  1217. // Not sure if injection is needed anymore. I believe it was to get around an issue with Greasemonkey / Firefox.
  1218. clicksaver(argsObject);
  1219. // if (exists(require, $)) {
  1220. // GM_addElement('script', {
  1221. // textContent: `(function(){${clicksaver.toString()}\n clicksaver(${JSON.stringify(argsObject).replace('\'', '\\\'')})})();`
  1222. // });
  1223. // } else {
  1224. // setTimeout(() => injectScript(argsObject), 250);
  1225. // }
  1226. }
  1227.  
  1228. function setValue(object, path, value) {
  1229. const pathParts = path.split('.');
  1230. for (let i = 0; i < pathParts.length - 1; i++) {
  1231. const pathPart = pathParts[i];
  1232. if (pathPart in object) {
  1233. object = object[pathPart];
  1234. } else {
  1235. object[pathPart] = {};
  1236. object = object[pathPart];
  1237. }
  1238. }
  1239. object[pathParts[pathParts.length - 1]] = value;
  1240. }
  1241.  
  1242. function convertTranslationsArrayToObject(arrayIn) {
  1243. const translations = {};
  1244. let iRow;
  1245. let iCol;
  1246. const languages = arrayIn[0].map(lang => lang.toLowerCase());
  1247. for (iCol = 1; iCol < languages.length; iCol++) {
  1248. translations[languages[iCol]] = {};
  1249. }
  1250. for (iRow = 1; iRow < arrayIn.length; iRow++) {
  1251. const row = arrayIn[iRow];
  1252. const propertyPath = row[0];
  1253. for (iCol = 1; iCol < row.length; iCol++) {
  1254. setValue(translations[languages[iCol]], propertyPath, row[iCol]);
  1255. }
  1256. }
  1257. return translations;
  1258. }
  1259.  
  1260. function loadTranslations() {
  1261. if (typeof $ === 'undefined') {
  1262. setTimeout(loadTranslations, 250);
  1263. console.debug('ClickSaver:', 'jQuery not ready. Retry loading translations...');
  1264. } else {
  1265. // This call retrieves the data from the translations spreadsheet and then injects
  1266. // the main code into the page. If the spreadsheet call fails, the default English
  1267. // translation is used.
  1268. const args = {
  1269. scriptName,
  1270. scriptVersion,
  1271. forumUrl
  1272. };
  1273. $.getJSON(`${translationsUrl}?${DEC(apiKey)}`).then(res => {
  1274. args.translations = convertTranslationsArrayToObject(res.values);
  1275. console.debug('ClickSaver:', 'Translations loaded.');
  1276. }).fail(() => {
  1277. console.error('ClickSaver: Error loading translations spreadsheet. Using default translation (English).');
  1278. args.useDefaultTranslation = true;
  1279. }).always(() => {
  1280. // Leave this document.ready function. Some people randomly get a "require is not defined" error unless the injectMain function
  1281. // is called late enough. Even with a "typeof require !== 'undefined'" check.
  1282. $(document).ready(() => {
  1283. injectScript(args);
  1284. });
  1285. });
  1286. }
  1287. }
  1288.  
  1289. function sandboxBootstrap() {
  1290. if (WazeWrap?.Ready) {
  1291. WazeWrap.Interface.ShowScriptUpdate(scriptName, scriptVersion, updateMessage, forumUrl);
  1292. } else {
  1293. setTimeout(sandboxBootstrap, 250);
  1294. }
  1295. }
  1296.  
  1297. // Go ahead and start loading translations, and inject the main code into the page.
  1298. loadTranslations();
  1299.  
  1300. // Start the "sandboxed" code.
  1301. sandboxBootstrap();
  1302. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址