WME ClickSaver

Various UI changes to make editing faster and easier.

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