WME ClickSaver

Various UI changes to make editing faster and easier.

目前为 2022-12-10 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name WME ClickSaver
  3. // @namespace https://gf.qytechs.cn/users/45389
  4. // @version 2022.11.09.002
  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. // @contributionURL https://github.com/WazeDev/Thank-The-Authors
  11. // @grant GM_xmlhttpRequest
  12. // @grant GM_addElement
  13. // @require https://gf.qytechs.cn/scripts/24851-wazewrap/code/WazeWrap.js
  14. // ==/UserScript==
  15.  
  16. /* global GM_info */
  17. /* global W */
  18. /* global I18n */
  19. /* global OL */
  20. /* global $ */
  21. /* global WazeWrap */
  22.  
  23. (function() {
  24. 'use strict';
  25.  
  26. const UPDATE_MESSAGE = '';
  27.  
  28. const SCRIPT_NAME = GM_info.script.name;
  29. const SCRIPT_VERSION = GM_info.script.version;
  30. const FORUM_URL = 'https://www.waze.com/forum/viewtopic.php?f=819&t=199894';
  31. const TRANSLATIONS_URL = 'https://sheets.googleapis.com/v4/spreadsheets/1ZlE9yhNncP9iZrPzFFa-FCtYuK58wNOEcmKqng4sH1M/values/ClickSaver';
  32. const API_KEY = 'YTJWNVBVRkplbUZUZVVGMFl6aFVjMjVOTW0wNU5GaG5kVE40TUZoNWJVZEhWbU5rUjNacVdtdFlWUT09';
  33. const DEC = s => atob(atob(s));
  34. const EXTERNAL_SETTINGS = {
  35. toggleTwoWaySegDrawingShortcut: null
  36. };
  37. const EXTERNAL_SETTINGS_NAME = 'clicksaver_settings_ext';
  38.  
  39. // This function is injected into the page.
  40. function main(argsObject) {
  41. /* eslint-disable object-curly-newline */
  42. const ROAD_TYPE_DROPDOWN_SELECTOR = 'wz-select[name="roadType"]';
  43. const ROAD_TYPE_CHIP_SELECTOR = 'wz-chip-select[class="road-type-chip-select"]';
  44. const PARKING_SPACES_DROPDOWN_SELECTOR = 'select[name="estimatedNumberOfSpots"]';
  45. const PARKING_COST_DROPDOWN_SELECTOR = 'select[name="costType"]';
  46. const SETTINGS_STORE_NAME = 'clicksaver_settings';
  47. const DEFAULT_TRANSLATION = {
  48. roadTypeButtons: {
  49. St: { text: 'St' },
  50. PS: { text: 'PS' },
  51. mH: { text: 'mH' },
  52. MH: { text: 'MH' },
  53. Fw: { text: 'Fw' },
  54. Rmp: { text: 'Rmp' },
  55. OR: { text: 'OR' },
  56. PLR: { text: 'PLR' },
  57. PR: { text: 'PR' },
  58. Fer: { text: 'Fer' },
  59. WT: { text: 'WT' },
  60. PB: { text: 'PB' },
  61. Sw: { text: 'Sw' },
  62. RR: { text: 'RR' },
  63. RT: { text: 'RT' },
  64. Pw: { text: 'Pw' }
  65. },
  66. groundButtonText: 'Ground',
  67. multiLockLevelWarning: 'Multiple lock levels selected!',
  68. prefs: {
  69. dropdownHelperGroup: 'DROPDOWN HELPERS',
  70. roadTypeButtons: 'Add road type buttons',
  71. useOldRoadColors: 'Use old road colors (requires refresh)',
  72. setStreetCityToNone: 'Set Street/City to None (new seg\'s only)',
  73. setStreetCityToNone_Title: 'NOTE: Only works if connected directly or indirectly'
  74. + ' to a segment with State / Country already set.',
  75. setCityToConnectedSegCity: 'Set City to connected segment\'s City',
  76. parkingCostButtons: 'Add PLA cost buttons',
  77. parkingSpacesButtons: 'Add PLA estimated spaces buttons',
  78. spaceSaversGroup: 'SPACE SAVERS',
  79. discussionForumLinkText: 'Discussion Forum'
  80. }
  81. };
  82. const ROAD_TYPES = {
  83. St: { val: 1, wmeColor: '#ffffeb', svColor: '#ffffff', category: 'streets', visible: true },
  84. PS: { val: 2, wmeColor: '#f0ea58', svColor: '#cba12e', category: 'streets', visible: true },
  85. Pw: { val: 22, wmeColor: '#64799a', svColor: '#64799a', category: 'streets', visible: false },
  86. mH: { val: 7, wmeColor: '#69bf88', svColor: '#ece589', category: 'highways', visible: true },
  87. MH: { val: 6, wmeColor: '#45b8d1', svColor: '#c13040', category: 'highways', visible: true },
  88. Fw: { val: 3, wmeColor: '#c577d2', svColor: '#387fb8', category: 'highways', visible: false },
  89. Rmp: { val: 4, wmeColor: '#b3bfb3', svColor: '#58c53b', category: 'highways', visible: false },
  90. OR: { val: 8, wmeColor: '#867342', svColor: '#82614a', category: 'otherDrivable', visible: false },
  91. PLR: { val: 20, wmeColor: '#ababab', svColor: '#2282ab', category: 'otherDrivable', visible: true },
  92. PR: { val: 17, wmeColor: '#beba6c', svColor: '#00ffb3', category: 'otherDrivable', visible: true },
  93. Fer: { val: 15, wmeColor: '#d7d8f8', svColor: '#ff8000', category: 'otherDrivable', visible: false },
  94. RR: { val: 18, wmeColor: '#c62925', svColor: '#ffffff', category: 'nonDrivable', visible: false },
  95. RT: { val: 19, wmeColor: '#ffffff', svColor: '#00ff00', category: 'nonDrivable', visible: false },
  96. WT: { val: 5, wmeColor: '#b0a790', svColor: '#00ff00', category: 'pedestrian', visible: false },
  97. PB: { val: 10, wmeColor: '#9a9a9a', svColor: '#0000ff', category: 'pedestrian', visible: false },
  98. Sw: { val: 16, wmeColor: '#999999', svColor: '#b700ff', category: 'pedestrian', visible: false }
  99. };
  100. /* eslint-enable object-curly-newline */
  101. let _settings = {};
  102. let _trans; // Translation object
  103.  
  104. // Do not make these const values. They may get assigned before require() is defined. Trust me. Don't do it.
  105. let UpdateObject;
  106. let UpdateFeatureAddress;
  107. let MultiAction;
  108. let AddSeg;
  109. let Segment;
  110. let DelSeg;
  111.  
  112. // function log(message) {
  113. // console.log('ClickSaver:', message);
  114. // }
  115.  
  116. function logDebug(message) {
  117. console.debug('ClickSaver:', message);
  118. }
  119.  
  120. // function logWarning(message) {
  121. // console.warn('ClickSaver:', message);
  122. // }
  123.  
  124. // function logError(message) {
  125. // console.error('ClickSaver:', message);
  126. // }
  127.  
  128. function isChecked(checkboxId) {
  129. return $(`#${checkboxId}`).is(':checked');
  130. }
  131.  
  132. function isSwapPedestrianPermitted() {
  133. const { user } = W.loginManager;
  134. const rank = user.rank + 1;
  135. return rank >= 4 || (rank === 3 && user.isAreaManager);
  136. }
  137.  
  138. function setChecked(checkboxId, checked) {
  139. $(`#${checkboxId}`).prop('checked', checked);
  140. }
  141. function loadSettingsFromStorage() {
  142. const loadedSettings = $.parseJSON(localStorage.getItem(SETTINGS_STORE_NAME));
  143. const defaultSettings = {
  144. lastVersion: null,
  145. roadButtons: true,
  146. roadTypeButtons: ['St', 'PS', 'mH', 'MH', 'Fw', 'Rmp', 'PLR', 'PR', 'PB'],
  147. parkingCostButtons: true,
  148. parkingSpacesButtons: true,
  149. setNewPLRStreetToNone: true,
  150. setNewPLRCity: true,
  151. setNewPRStreetToNone: false,
  152. setNewPRCity: false,
  153. setNewRRStreetToNone: true, // added by jm6087
  154. setNewRRCity: false, // added by jm6087
  155. setNewPBStreetToNone: true, // added by jm6087
  156. setNewPBCity: true, // added by jm6087
  157. setNewORStreetToNone: false,
  158. setNewORCity: false,
  159. addAltCityButton: true,
  160. addSwapPedestrianButton: false,
  161. useOldRoadColors: false,
  162. warnOnPedestrianTypeSwap: true
  163. };
  164. _settings = loadedSettings || defaultSettings;
  165. Object.keys(defaultSettings).forEach(prop => {
  166. if (!_settings.hasOwnProperty(prop)) {
  167. _settings[prop] = defaultSettings[prop];
  168. }
  169. });
  170.  
  171. setChecked('csRoadTypeButtonsCheckBox', _settings.roadButtons);
  172. if (_settings.roadTypeButtons) {
  173. Object.keys(ROAD_TYPES).forEach(roadTypeAbbr1 => {
  174. setChecked(`cs${roadTypeAbbr1}CheckBox`, _settings.roadTypeButtons.indexOf(roadTypeAbbr1) !== -1);
  175. });
  176. }
  177.  
  178. if (_settings.roadButtons) {
  179. $('.csRoadTypeButtonsCheckBoxContainer').show();
  180. } else {
  181. $('.csRoadTypeButtonsCheckBoxContainer').hide();
  182. }
  183. setChecked('csParkingSpacesButtonsCheckBox', _settings.parkingSpacesButtons);
  184. setChecked('csParkingCostButtonsCheckBox', _settings.parkingCostButtons);
  185. setChecked('csSetNewPLRCityCheckBox', _settings.setNewPLRCity);
  186. setChecked('csClearNewPLRCheckBox', _settings.setNewPLRStreetToNone);
  187. setChecked('csSetNewPRCityCheckBox', _settings.setNewPRCity);
  188. setChecked('csClearNewPRCheckBox', _settings.setNewPRStreetToNone);
  189. setChecked('csSetNewRRCityCheckBox', _settings.setNewRRCity);
  190. setChecked('csClearNewRRCheckBox', _settings.setNewRRStreetToNone); // added by jm6087
  191. setChecked('csSetNewPBCityCheckBox', _settings.setNewPBCity);
  192. setChecked('csClearNewPBCheckBox', _settings.setNewPBStreetToNone); // added by jm6087
  193. setChecked('csSetNewORCityCheckBox', _settings.setNewORCity);
  194. setChecked('csClearNewORCheckBox', _settings.setNewORStreetToNone);
  195. setChecked('csUseOldRoadColorsCheckBox', _settings.useOldRoadColors);
  196. setChecked('csAddAltCityButtonCheckBox', _settings.addAltCityButton);
  197. setChecked('csAddSwapPedestrianButtonCheckBox', _settings.addSwapPedestrianButton);
  198. }
  199.  
  200. function saveSettingsToStorage() {
  201. if (localStorage) {
  202. const settings = {
  203. lastVersion: argsObject.scriptVersion,
  204. roadButtons: _settings.roadButtons,
  205. parkingCostButtons: _settings.parkingCostButtons,
  206. parkingSpacesButtons: _settings.parkingSpacesButtons,
  207. setNewPLRCity: _settings.setNewPLRCity,
  208. setNewPLRStreetToNone: _settings.setNewPLRStreetToNone,
  209. setNewPRCity: _settings.setNewPRCity,
  210. setNewPRStreetToNone: _settings.setNewPRStreetToNone,
  211. setNewRRCity: _settings.setNewRRCity,
  212. setNewRRStreetToNone: _settings.setNewRRStreetToNone, // added by jm6087
  213. setNewPBCity: _settings.setNewPBCity,
  214. setNewPBStreetToNone: _settings.setNewPBStreetToNone, // added by jm6087
  215. setNewORCity: _settings.setNewORCity,
  216. setNewORStreetToNone: _settings.setNewORStreetToNone,
  217. useOldRoadColors: _settings.useOldRoadColors,
  218. addAltCityButton: _settings.addAltCityButton,
  219. addSwapPedestrianButton: _settings.addSwapPedestrianButton,
  220. warnOnPedestrianTypeSwap: _settings.warnOnPedestrianTypeSwap
  221. };
  222. settings.roadTypeButtons = [];
  223. Object.keys(ROAD_TYPES).forEach(roadTypeAbbr => {
  224. if (_settings.roadTypeButtons.indexOf(roadTypeAbbr) !== -1) {
  225. settings.roadTypeButtons.push(roadTypeAbbr);
  226. }
  227. });
  228. localStorage.setItem(SETTINGS_STORE_NAME, JSON.stringify(settings));
  229. logDebug('Settings saved');
  230. }
  231. }
  232.  
  233. function isPedestrianTypeSegment(segment) {
  234. return [5, 10, 16].includes(segment.attributes.roadType);
  235. }
  236.  
  237. function getConnectedSegmentIDs(segmentID) {
  238. const IDs = [];
  239. const segment = W.model.segments.getObjectById(segmentID);
  240. [
  241. W.model.nodes.getObjectById(segment.attributes.fromNodeID),
  242. W.model.nodes.getObjectById(segment.attributes.toNodeID)
  243. ].forEach(node => {
  244. if (node) {
  245. node.attributes.segIDs.forEach(segID => {
  246. if (segID !== segmentID) { IDs.push(segID); }
  247. });
  248. }
  249. });
  250. return IDs;
  251. }
  252.  
  253. function getFirstConnectedSegmentAddress(startSegment) {
  254. const nonMatches = [];
  255. const segmentIDsToSearch = [startSegment.getID()];
  256. while (segmentIDsToSearch.length > 0) {
  257. const startSegmentID = segmentIDsToSearch.pop();
  258. startSegment = W.model.segments.getObjectById(startSegmentID);
  259. const connectedSegmentIDs = getConnectedSegmentIDs(startSegmentID);
  260. for (let i = 0; i < connectedSegmentIDs.length; i++) {
  261. const addr = W.model.segments.getObjectById(connectedSegmentIDs[i]).getAddress();
  262. if (!addr.isEmpty()) {
  263. return addr;
  264. }
  265. }
  266.  
  267. nonMatches.push(startSegmentID);
  268. connectedSegmentIDs.forEach(segmentID => {
  269. if (nonMatches.indexOf(segmentID) === -1 && segmentIDsToSearch.indexOf(segmentID) === -1) {
  270. segmentIDsToSearch.push(segmentID);
  271. }
  272. });
  273. }
  274. return undefined;
  275. }
  276.  
  277. function setStreetAndCity(setCity) {
  278. const segments = W.selectionManager.getSelectedFeatures();
  279. if (segments.length === 0 || segments[0].model.type !== 'segment') {
  280. return;
  281. }
  282.  
  283. const mAction = new MultiAction();
  284. mAction.setModel(W.model);
  285. segments.forEach(segment => {
  286. const segModel = segment.model;
  287. if (segModel.attributes.primaryStreetID === null) {
  288. const addr = getFirstConnectedSegmentAddress(segModel);
  289. if (addr && !addr.isEmpty()) {
  290. const cityNameToSet = setCity && !addr.getCity().isEmpty() ? addr.getCityName() : '';
  291. const action = new UpdateFeatureAddress(segModel, {
  292. countryID: addr.getCountry().id,
  293. stateID: addr.getState().id,
  294. cityName: cityNameToSet,
  295. emptyStreet: true,
  296. emptyCity: !setCity
  297. }, { streetIDField: 'primaryStreetID' });
  298. mAction.doSubAction(action);
  299. }
  300. }
  301. });
  302. const count = mAction.subActions.length;
  303. if (count) {
  304. mAction._description = `Updated address on ${count} segment${count > 1 ? 's' : ''}`;
  305. W.model.actionManager.add(mAction);
  306. }
  307. }
  308.  
  309. function waitForElem(selector, callback) {
  310. const elem = document.querySelector(selector);
  311. setTimeout(() => {
  312. if (!elem) {
  313. waitForElem(selector, callback);
  314. } else {
  315. callback(elem);
  316. }
  317. }, 10);
  318. }
  319.  
  320. function waitForShadowElem(parentElemSelector, shadowElemSelector, callback) {
  321. setTimeout(() => {
  322. const parentElem = document.querySelector(parentElemSelector);
  323. const sRoot = parentElem ? parentElem.shadowRoot : null;
  324. const shadowElem = sRoot ? sRoot.querySelector(shadowElemSelector) : null;
  325. if (!shadowElem) {
  326. waitForShadowElem(parentElemSelector, shadowElemSelector, callback);
  327. } else {
  328. callback(shadowElem, parentElem);
  329. }
  330. }, 10);
  331. }
  332.  
  333. // eslint-disable-next-line no-unused-vars
  334. function onAddAltCityButtonClick() {
  335. const streetID = W.selectionManager.getSelectedFeatures()[0].model.attributes.primaryStreetID;
  336. $('wz-button[class="add-alt-street-btn"]').click();
  337. waitForElem('wz-autocomplete.alt-street-name', elem => {
  338. elem.focus();
  339. waitForShadowElem('wz-autocomplete.alt-street-name', `wz-menu-item[item-id="${streetID}"]`, shadowElem => {
  340. shadowElem.click();
  341. const emptyCityCheckbox = $('wz-checkbox.empty-city');
  342. if (emptyCityCheckbox[0].checked) { emptyCityCheckbox.click(); }
  343. waitForShadowElem('wz-autocomplete.alt-city-name', 'wz-text-input', (cityTextElem, cityAutocompleteElem) => {
  344. cityTextElem.value = null;
  345. cityAutocompleteElem.focus();
  346. });
  347. });
  348. });
  349. }
  350.  
  351. function onRoadTypeButtonClick(roadTypeAbbr) {
  352. const segments = W.selectionManager.getSelectedFeatures();
  353. const roadTypeVal = ROAD_TYPES[roadTypeAbbr].val;
  354. let action;
  355. if (segments.length > 1) {
  356. action = new MultiAction();
  357. action.setModel(W.model);
  358. segments.forEach(segment => {
  359. const subAction = new UpdateObject(segment.model, { roadType: roadTypeVal });
  360. action.doSubAction(subAction);
  361. });
  362. action._description = I18n.t(
  363. 'save.changes_log.actions.UpdateObject.changed',
  364. {
  365. propertyName: I18n.t('objects.segment.fields.roadType'),
  366. objectsString: I18n.t('objects.segment.multi', { count: segments.length }),
  367. value: I18n.t('segment.road_types')[roadTypeVal]
  368. }
  369. );
  370. } else {
  371. action = new UpdateObject(segments[0].model, { roadType: roadTypeVal });
  372. }
  373. W.model.actionManager.add(action);
  374.  
  375. if (roadTypeAbbr === 'PLR' && isChecked('csClearNewPLRCheckBox') && typeof require !== 'undefined') {
  376. setStreetAndCity(isChecked('csSetNewPLRCityCheckBox'));
  377. } else if (roadTypeAbbr === 'PR' && isChecked('csClearNewPRCheckBox') && typeof require !== 'undefined') {
  378. setStreetAndCity(isChecked('csSetNewPRCityCheckBox'));
  379. } else if (roadTypeAbbr === 'RR' && isChecked('csClearNewRRCheckBox') && typeof require !== 'undefined') { // added by jm6087
  380. setStreetAndCity(isChecked('csSetNewRRCityCheckBox')); // added by jm6087
  381. } else if (roadTypeAbbr === 'PB' && isChecked('csClearNewPBCheckBox') && typeof require !== 'undefined') { // added by jm6087
  382. setStreetAndCity(isChecked('csSetNewPBCityCheckBox')); // added by jm6087
  383. } else if (roadTypeAbbr === 'OR' && isChecked('csClearNewORCheckBox') && typeof require !== 'undefined') {
  384. setStreetAndCity(isChecked('csSetNewORCityCheckBox'));
  385. }
  386. }
  387.  
  388. function addRoadTypeButtons() {
  389. const seg = W.selectionManager.getSelectedFeatures()[0].model;
  390. if (seg.type !== 'segment') return;
  391. const isPed = isPedestrianTypeSegment(seg);
  392. const $dropDown = $(ROAD_TYPE_DROPDOWN_SELECTOR);
  393. $('#csRoadTypeButtonsContainer').remove();
  394. const $container = $('<div>', { id: 'csRoadTypeButtonsContainer', class: 'cs-rt-buttons-container' });
  395. const $street = $('<div>', { id: 'csStreetButtonContainer', class: 'cs-rt-buttons-group' });
  396. const $highway = $('<div>', { id: 'csHighwayButtonContainer', class: 'cs-rt-buttons-group' });
  397. const $otherDrivable = $('<div>', { id: 'csOtherDrivableButtonContainer', class: 'cs-rt-buttons-group' });
  398. const $nonDrivable = $('<div>', { id: 'csNonDrivableButtonContainer', class: 'cs-rt-buttons-group' });
  399. const $pedestrian = $('<div>', { id: 'csPedestrianButtonContainer', class: 'cs-rt-buttons-group' });
  400. const divs = {
  401. streets: $street,
  402. highways: $highway,
  403. otherDrivable: $otherDrivable,
  404. nonDrivable: $nonDrivable,
  405. pedestrian: $pedestrian
  406. };
  407. Object.keys(ROAD_TYPES).forEach(roadTypeKey => {
  408. if (_settings.roadTypeButtons.indexOf(roadTypeKey) !== -1) {
  409. const roadType = ROAD_TYPES[roadTypeKey];
  410. const isDisabled = $dropDown[0].hasAttribute('disabled') && $dropDown[0].getAttribute('disabled') === 'true';
  411. if (!isDisabled && ((roadType.category === 'pedestrian' && isPed) || (roadType.category !== 'pedestrian' && !isPed))) {
  412. const $div = divs[roadType.category];
  413. $div.append(
  414. $('<div>', {
  415. class: `btn cs-rt-button cs-rt-button-${roadTypeKey} btn-positive`,
  416. title: I18n.t('segment.road_types')[roadType.val]
  417. })
  418. .text(_trans.roadTypeButtons[roadTypeKey].text)
  419. .prop('checked', roadType.visible)
  420. .data('key', roadTypeKey)
  421. // TODO: change onRoadTypeButtonClick handler to work with element rather than data
  422. .click(function rtbClick() { onRoadTypeButtonClick($(this).data('key')); })
  423. );
  424. }
  425. }
  426. });
  427. if (isPed) {
  428. $container.append($pedestrian);
  429. } else {
  430. $container.append($street).append($highway).append($otherDrivable).append($nonDrivable);
  431. }
  432. $dropDown.before($container);
  433. }
  434.  
  435. //Function to add an event listener to the chip select for the road type in compact mode
  436. function addCompactRoadTypeChangeEvents() {
  437. const chipSelect = document.getElementsByClassName('road-type-chip-select')[0];
  438. chipSelect.addEventListener('chipSelected', function() {
  439. const chipNodes = chipSelect.getElementsByTagName('wz-checkable-chip');
  440.  
  441. //Identifies which road type is now selected then calls the onRoadTypeButtonClick
  442. //function with the road type abbreviation from the chip.
  443. for (let i = 0; i < chipNodes.length; i++) {
  444. const chip = chipNodes[i];
  445. if (chip.checked) {
  446. onRoadTypeButtonClick(chip.innerText);
  447. break;
  448. }
  449. }
  450. });
  451. }
  452.  
  453. function isPLA(item) {
  454. return (item.model.type === 'venue') && item.model.attributes.categories.includes('PARKING_LOT');
  455. }
  456.  
  457. function addParkingSpacesButtons() {
  458. const $dropDown = $(PARKING_SPACES_DROPDOWN_SELECTOR);
  459. const selItems = W.selectionManager.getSelectedFeatures();
  460. const item = selItems[0];
  461.  
  462. // If it's not a PLA, exit.
  463. if (!isPLA(item)) return;
  464.  
  465. $('#csParkingSpacesContainer').remove();
  466. const $div = $('<div>', { id: 'csParkingSpacesContainer' });
  467. const dropdownDisabled = $dropDown.attr('disabled') === 'disabled';
  468. const optionNodes = $(`${PARKING_SPACES_DROPDOWN_SELECTOR} option`);
  469.  
  470. for (let i = 0; i < optionNodes.length; i++) {
  471. const $option = $(optionNodes[i]);
  472. const text = $option.text();
  473. const selected = $option.val() === $dropDown.val();
  474. $div.append(
  475. // TODO css
  476. $('<div>', {
  477. class: `btn waze-btn waze-btn-white${selected ? ' waze-btn-blue' : ''}${dropdownDisabled ? ' disabled' : ''}`,
  478. style: 'margin-bottom: 5px; height: 22px; padding: 2px 8px 0px 8px; margin-right: 3px;'
  479. })
  480. .text(text)
  481. .data('val', $option.val())
  482. // eslint-disable-next-line func-names
  483. .hover(() => { })
  484. .click(function onParkingSpacesButtonClick() {
  485. if (!dropdownDisabled) {
  486. $(PARKING_SPACES_DROPDOWN_SELECTOR).val($(this).data('val')).change();
  487. addParkingSpacesButtons();
  488. }
  489. })
  490. );
  491. }
  492.  
  493. $dropDown.before($div);
  494. $dropDown.hide();
  495. }
  496.  
  497. function addParkingCostButtons() {
  498. const $dropDown = $(PARKING_COST_DROPDOWN_SELECTOR);
  499. const selItems = W.selectionManager.getSelectedFeatures();
  500. const item = selItems[0];
  501.  
  502. // If it's not a PLA, exit.
  503. if (!isPLA(item)) return;
  504.  
  505. $('#csParkingCostContainer').remove();
  506. const $div = $('<div>', { id: 'csParkingCostContainer' });
  507. const dropdownDisabled = $dropDown.attr('disabled') === 'disabled';
  508. const optionNodes = $(`${PARKING_COST_DROPDOWN_SELECTOR} option`);
  509. for (let i = 0; i < optionNodes.length; i++) {
  510. const $option = $(optionNodes[i]);
  511. const text = $option.text();
  512. const selected = $option.val() === $dropDown.val();
  513. $div.append(
  514. $('<div>', {
  515. class: `btn waze-btn waze-btn-white${selected ? ' waze-btn-blue' : ''}${dropdownDisabled ? ' disabled' : ''}`,
  516. // TODO css
  517. style: 'margin-bottom: 5px; height: 22px; padding: 2px 8px 0px 8px; margin-right: 4px;'
  518. })
  519. .text(text !== '' ? text : '?')
  520. .data('val', $option.val())
  521. // eslint-disable-next-line func-names
  522. .hover(() => { })
  523. .click(function onParkingCostButtonClick() {
  524. if (!dropdownDisabled) {
  525. $(PARKING_COST_DROPDOWN_SELECTOR).val($(this).data('val')).change();
  526. addParkingCostButtons();
  527. }
  528. })
  529. );
  530. }
  531.  
  532. $dropDown.before($div);
  533. $dropDown.hide();
  534. }
  535.  
  536. function addAddAltCityButton() {
  537. const selFeatures = W.selectionManager.getSelectedFeatures();
  538. const streetID = selFeatures[0].model.attributes.primaryStreetID;
  539. // Only show the button if every segment has the same primary city and street.
  540. if (selFeatures.length > 1 && !selFeatures.every(f => f.model.attributes.primaryStreetID === streetID)) return;
  541.  
  542. const id = 'csAddAltCityButton';
  543. if (selFeatures[0].model.type === 'segment' && $(`#${id}`).length === 0) {
  544. $('div.address-edit').prev('wz-label').append(
  545. $('<a>', {
  546. href: '#',
  547. // TODO css
  548. style: 'float: right;text-transform: none;'
  549. + 'font-family: "Helvetica Neue", Helvetica, "Open Sans", sans-serif;color: #26bae8;'
  550. + 'font-weight: normal;'
  551. }).text('Add alt city').click(onAddAltCityButtonClick)
  552. );
  553. }
  554. }
  555.  
  556. function addSwapPedestrianButton(displayMode) { //Added displayMode argument to identify compact vs. regular mode.
  557. const id = 'csSwapPedestrianContainer';
  558. $(`#${id}`).remove();
  559. const selectedFeatures = W.selectionManager.getSelectedFeatures();
  560. if (selectedFeatures.length === 1 && selectedFeatures[0].model.type === 'segment') {
  561. // TODO css
  562. const $container = $('<div>', { id, style: 'white-space: nowrap;float: right;display: inline;' });
  563. const $button = $('<div>', {
  564. id: 'csBtnSwapPedestrianRoadType',
  565. title: '',
  566. // TODO css
  567. style: 'display:inline-block;cursor:pointer;'
  568. });
  569. $button.append('<span class="fa fa-arrows-h" style="font-size:20px; color:#e84545;"></span>')
  570. .attr({
  571. title: 'Swap between driving-type and walking-type segments.\nWARNING!'
  572. + ' This will DELETE and recreate the segment. Nodes may need to be reconnected.'
  573. });
  574. $container.append($button);
  575.  
  576. //Inser swap button in the correct location based on display mode.
  577. if (displayMode === 'compact') {
  578. const $label = $('wz-chip-select[class="road-type-chip-select"]').parent().find('wz-label');
  579. $label.css({ display: 'inline' }).before($container);
  580. }
  581. else {
  582. const $label = $('wz-select[name="roadType"]').closest('form').find('wz-label').first();
  583. $label.css({ display: 'inline' }).after($container);
  584. }
  585. // TODO css
  586.  
  587. $('#csBtnSwapPedestrianRoadType').click(() => {
  588. if (_settings.warnOnPedestrianTypeSwap) {
  589. _settings.warnOnPedestrianTypeSwap = false;
  590. saveSettingsToStorage();
  591. if (!confirm('This will DELETE the segment and recreate it. Any speed data will be lost,'
  592. + ' and nodes will need to be reconnected (if applicable).'
  593. + ' This message will only be displayed once. Continue?')) {
  594. return;
  595. }
  596. }
  597.  
  598. //Check for paths before deleting.
  599. let segment = W.selectionManager.getSelectedFeatures()[0];
  600. if (segment.model.hasPaths()) {
  601. WazeWrap.Alerts.error('Clicksaver', 'Paths must be removed from segment before changing between road and pedestrian.');
  602. return;
  603. }
  604.  
  605. // delete the selected segment
  606. const oldGeom = segment.geometry.clone();
  607. W.model.actionManager.add(new DelSeg(segment.model));
  608.  
  609. // create the replacement segment in the other segment type (pedestrian -> road & vice versa)
  610. // Note: this doesn't work in a MultiAction for some reason.
  611. const newRoadType = isPedestrianTypeSegment(segment.model) ? 1 : 5;
  612. segment = new Segment({ geometry: oldGeom, roadType: newRoadType });
  613. segment.state = OL.State.INSERT;
  614. W.model.actionManager.add(new AddSeg(segment, {
  615. createNodes: !0,
  616. openAllTurns: W.prefs.get('enableTurnsByDefault'),
  617. createTwoWay: W.prefs.get('twoWaySegmentsByDefault'),
  618. snappedFeatures: [null, null]
  619. }));
  620. const newId = W.model.repos.segments.idGenerator.lastValue;
  621. const newSegment = W.model.segments.getObjectById(newId);
  622. W.selectionManager.setSelectedModels([newSegment]);
  623. });
  624. }
  625. }
  626.  
  627. /* eslint-disable no-bitwise, no-mixed-operators */
  628. function shadeColor2(color, percent) {
  629. const f = parseInt(color.slice(1), 16);
  630. const t = percent < 0 ? 0 : 255;
  631. const p = percent < 0 ? percent * -1 : percent;
  632. const R = f >> 16;
  633. const G = f >> 8 & 0x00FF;
  634. const B = f & 0x0000FF;
  635. return `#${(0x1000000 + (Math.round((t - R) * p) + R) * 0x10000 + (Math.round((t - G) * p) + G)
  636. * 0x100 + (Math.round((t - B) * p) + B)).toString(16).slice(1)}`;
  637. }
  638. /* eslint-enable no-bitwise, no-mixed-operators */
  639.  
  640. function buildRoadTypeButtonCss() {
  641. const lines = [];
  642. const useOldColors = _settings.useOldRoadColors;
  643. Object.keys(ROAD_TYPES).forEach(roadTypeAbbr => {
  644. const roadType = ROAD_TYPES[roadTypeAbbr];
  645. const bgColor = useOldColors ? roadType.svColor : roadType.wmeColor;
  646. let output = `.cs-rt-buttons-container .cs-rt-button-${roadTypeAbbr} {background-color:${
  647. bgColor};box-shadow:0 2px ${shadeColor2(bgColor, -0.5)};border-color:${shadeColor2(bgColor, -0.15)};}`;
  648. output += ` .cs-rt-buttons-container .cs-rt-button-${roadTypeAbbr}:hover {background-color:${
  649. shadeColor2(bgColor, 0.2)}}`;
  650. lines.push(output);
  651. });
  652. return lines.join(' ');
  653. }
  654.  
  655. function injectCss() {
  656. const css = [
  657. // Road type button formatting
  658. '.csRoadTypeButtonsCheckBoxContainer {margin-left:15px;}',
  659. '.cs-rt-buttons-container {margin-bottom:5px;height:21px;}',
  660. '.cs-rt-buttons-container .cs-rt-button {font-size:11px;line-height:20px;color:black;padding:0px 4px;height:20px;'
  661. + 'margin-right:2px;border-style:solid;border-width:1px;}',
  662. buildRoadTypeButtonCss(),
  663. '.btn.cs-rt-button:active {box-shadow:none;transform:translateY(2px)}',
  664. 'div .cs-rt-buttons-group {float:left; margin: 0px 5px 5px 0px;}',
  665. '#sidepanel-clicksaver .controls-container {padding:0px;}',
  666. '#sidepanel-clicksaver .controls-container label {white-space: normal;}',
  667.  
  668. // Lock button formatting
  669. '.cs-group-label {font-size: 11px; width: 100%; font-family: Poppins, sans-serif;'
  670. + ' text-transform: uppercase; font-weight: 700; color: #354148; margin-bottom: 6px;}'
  671. ].join(' ');
  672. $(`<style type="text/css">${css}</style>`).appendTo('head');
  673. }
  674.  
  675. function createSettingsCheckbox(id, settingName, labelText, titleText, divCss, labelCss, optionalAttributes) {
  676. const $container = $('<div>', { class: 'controls-container' });
  677. const $input = $('<input>', {
  678. type: 'checkbox', class: 'csSettingsCheckBox', name: id, id, 'data-setting-name': settingName
  679. }).appendTo($container);
  680. const $label = $('<label>', { for: id }).text(labelText).appendTo($container);
  681. // TODO css
  682. if (divCss) $container.css(divCss);
  683. // TODO css
  684. if (labelCss) $label.css(labelCss);
  685. if (titleText) $container.attr({ title: titleText });
  686. if (optionalAttributes) $input.attr(optionalAttributes);
  687. return $container;
  688. }
  689.  
  690. function initUserPanel() {
  691. const $roadTypesDiv = $('<div>', { class: 'csRoadTypeButtonsCheckBoxContainer' });
  692. $roadTypesDiv.append(
  693. createSettingsCheckbox('csUseOldRoadColorsCheckBox', 'useOldRoadColors', _trans.prefs.useOldRoadColors)
  694. );
  695. Object.keys(ROAD_TYPES).forEach(roadTypeAbbr => {
  696. const roadType = ROAD_TYPES[roadTypeAbbr];
  697. const id = `cs${roadTypeAbbr}CheckBox`;
  698. const title = I18n.t('segment.road_types')[roadType.val];
  699. $roadTypesDiv.append(
  700. createSettingsCheckbox(id, 'roadType', title, null, null, null, {
  701. 'data-road-type': roadTypeAbbr
  702. })
  703. );
  704. if (['PLR', 'PR', 'RR', 'PB', 'OR'].includes(roadTypeAbbr)) { // added RR & PB by jm6087
  705. $roadTypesDiv.append(
  706. // TODO css
  707. createSettingsCheckbox(`csClearNew${roadTypeAbbr}CheckBox`, `setNew${roadTypeAbbr}StreetToNone`,
  708. _trans.prefs.setStreetCityToNone, _trans.prefs.setStreetCityToNone_Title,
  709. { paddingLeft: '20px', marginRight: '4px' }, { fontStyle: 'italic' }),
  710. createSettingsCheckbox(`csSetNew${roadTypeAbbr}CityCheckBox`, `setNew${roadTypeAbbr}City`,
  711. _trans.prefs.setCityToConnectedSegCity, '',
  712. { paddingLeft: '30px', marginRight: '4px' }, { fontStyle: 'italic' })
  713. );
  714. }
  715. });
  716.  
  717. const $tab = $('<li>', { title: argsObject.scriptName }).append(
  718. $('<a>', { 'data-toggle': 'tab', href: '#sidepanel-clicksaver' }).append($('<span>').text('CS'))
  719. );
  720.  
  721. const $panel = $('<div>', { class: 'tab-pane', id: 'sidepanel-clicksaver' }).append(
  722. $('<div>', { class: 'side-panel-section>' }).append(
  723. // TODO css
  724. $('<div>', { style: 'margin-bottom:8px;' }).append(
  725. $('<div>', { class: 'form-group' }).append(
  726. $('<label>', { class: 'cs-group-label' }).text(_trans.prefs.dropdownHelperGroup),
  727. $('<div>').append(
  728. createSettingsCheckbox('csRoadTypeButtonsCheckBox', 'roadButtons',
  729. _trans.prefs.roadTypeButtons)
  730. ).append($roadTypesDiv),
  731. createSettingsCheckbox('csParkingCostButtonsCheckBox', 'parkingCostButtons',
  732. _trans.prefs.parkingCostButtons),
  733. createSettingsCheckbox('csParkingSpacesButtonsCheckBox', 'parkingSpacesButtons',
  734. _trans.prefs.parkingSpacesButtons)
  735. ),
  736. $('<label>', { class: 'cs-group-label' }).text('Time Savers'),
  737. $('<div>', { style: 'margin-bottom:8px;' }).append(
  738. // THIS IS CURRENTLY DISABLED
  739. createSettingsCheckbox('csAddAltCityButtonCheckBox', 'addAltCityButton',
  740. 'Show "Add alt city" button'),
  741. isSwapPedestrianPermitted() ? createSettingsCheckbox('csAddSwapPedestrianButtonCheckBox',
  742. 'addSwapPedestrianButton', 'Show "Swap driving<->walking segment type" button') : ''
  743. )
  744. )
  745. )
  746. );
  747.  
  748. $panel.append(
  749. // TODO css
  750. $('<div>', { style: 'margin-top:20px;font-size:10px;color:#999999;' }).append(
  751. $('<div>').text(`version ${argsObject.scriptVersion}${argsObject.scriptName.toLowerCase().includes('beta') ? ' beta' : ''}`),
  752. $('<div>').append(
  753. $('<a>', { href: argsObject.forumUrl, target: '__blank' }).text(_trans.prefs.discussionForumLinkText)
  754. )
  755. )
  756. );
  757.  
  758. $('#user-tabs > .nav-tabs').append($tab);
  759. $('#user-info > .flex-parent > .tab-content').append($panel);
  760.  
  761. // Add change events
  762. $('#csRoadTypeButtonsCheckBox').change(function onRoadTypeButtonCheckChanged() {
  763. if (this.checked) {
  764. $('.csRoadTypeButtonsCheckBoxContainer').show();
  765. } else {
  766. $('.csRoadTypeButtonsCheckBoxContainer').hide();
  767. }
  768. saveSettingsToStorage();
  769. });
  770. $('.csSettingsCheckBox').change(function onSettingsCheckChanged() {
  771. const { checked } = this;
  772. const settingName = $(this).data('setting-name');
  773. if (settingName === 'roadType') {
  774. const roadType = $(this).data('road-type');
  775. const array = _settings.roadTypeButtons;
  776. const index = array.indexOf(roadType);
  777. if (checked && index === -1) {
  778. array.push(roadType);
  779. } else if (!checked && index !== -1) {
  780. array.splice(index, 1);
  781. }
  782. } else {
  783. _settings[settingName] = checked;
  784. }
  785. saveSettingsToStorage();
  786. });
  787. }
  788.  
  789. function updateControls() {
  790. if ($(ROAD_TYPE_DROPDOWN_SELECTOR).length > 0) {
  791. if (isChecked('csRoadTypeButtonsCheckBox')) addRoadTypeButtons();
  792. }
  793. if ($(PARKING_SPACES_DROPDOWN_SELECTOR).length > 0 && isChecked('csParkingSpacesButtonsCheckBox')) {
  794. addParkingSpacesButtons(); // TODO - add option setting
  795. }
  796. if ($(PARKING_COST_DROPDOWN_SELECTOR).length > 0 && isChecked('csParkingCostButtonsCheckBox')) {
  797. addParkingCostButtons(); // TODO - add option setting
  798. }
  799. }
  800.  
  801. function replaceWord(target, searchWord, replaceWithWord) {
  802. return target.replace(new RegExp(`\\b${searchWord}\\b`, 'g'), replaceWithWord);
  803. }
  804.  
  805. function titleCase(word) {
  806. return word.charAt(0).toUpperCase() + word.substring(1).toLowerCase();
  807. }
  808. function mcCase(word) {
  809. return word.charAt(0).toUpperCase() + word.charAt(1).toLowerCase()
  810. + word.charAt(2).toUpperCase() + word.substring(3).toLowerCase();
  811. }
  812. function upperCase(word) {
  813. return word.toUpperCase();
  814. }
  815.  
  816. function processSubstring(target, substringRegex, processFunction) {
  817. const substrings = target.match(substringRegex);
  818. if (substrings) {
  819. for (let idx = 0; idx < substrings.length; idx++) {
  820. const substring = substrings[idx];
  821. const newSubstring = processFunction(substring);
  822. target = replaceWord(target, substring, newSubstring);
  823. }
  824. }
  825. return target;
  826. }
  827.  
  828. function onPaste(e) {
  829. const targetNode = e.target;
  830. if (targetNode.name === 'streetName' || targetNode.className.includes('street-name')) {
  831. // Get the text that's being pasted.
  832. let pastedText = e.clipboardData.getData('text/plain');
  833.  
  834. // If pasting text in ALL CAPS...
  835. if (/^[^a-z]*$/.test(pastedText)) {
  836. [
  837. // Title case all words first.
  838. [/\b[a-zA-Z]+(?:'S)?\b/g, titleCase],
  839.  
  840. // Then process special cases.
  841. [/\bMC\w+\b/ig, mcCase], // e.g. McCaulley
  842. [/\b(?:I|US|SH|SR|CH|CR|CS|PR|PS)\s*-?\s*\d+\w*\b/ig, upperCase], // e.g. US-25, US25
  843. /* eslint-disable-next-line max-len */
  844. [/\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
  845. [/\b(?:NE|NW|SE|SW)\b/ig, upperCase]
  846. ].forEach(item => {
  847. pastedText = processSubstring(pastedText, item[0], item[1]);
  848. });
  849.  
  850. // Insert new text in the focused node.
  851. document.execCommand('insertText', false, pastedText);
  852.  
  853. // Prevent the default paste behavior.
  854. e.preventDefault();
  855. return false;
  856. }
  857. }
  858. return true;
  859. }
  860.  
  861. function getTranslationObject() {
  862. if (argsObject.useDefaultTranslation) {
  863. return DEFAULT_TRANSLATION;
  864. }
  865. let locale = I18n.currentLocale().toLowerCase();
  866. if (!argsObject.translations.hasOwnProperty(locale)) {
  867. locale = 'en-us';
  868. }
  869. return argsObject.translations[locale];
  870. }
  871.  
  872. function errorHandler(callback) {
  873. try {
  874. callback();
  875. } catch (ex) {
  876. console.error(`${argsObject.scriptName}:`, ex);
  877. }
  878. }
  879.  
  880. function init() {
  881. UpdateObject = require('Waze/Action/UpdateObject');
  882. UpdateFeatureAddress = require('Waze/Action/UpdateFeatureAddress');
  883. MultiAction = require('Waze/Action/MultiAction');
  884. AddSeg = require('Waze/Action/AddSegment');
  885. Segment = require('Waze/Feature/Vector/Segment');
  886. DelSeg = require('Waze/Action/DeleteSegment');
  887.  
  888. _trans = getTranslationObject();
  889. Object.keys(ROAD_TYPES).forEach(rtName => {
  890. ROAD_TYPES[rtName].text = _trans.roadTypeButtons[rtName].text;
  891. });
  892.  
  893. document.addEventListener('paste', onPaste);
  894.  
  895. // check for changes in the edit-panel
  896. const observer = new MutationObserver(mutations => {
  897. mutations.forEach(mutation => {
  898. for (let i = 0; i < mutation.addedNodes.length; i++) {
  899. const addedNode = mutation.addedNodes[i];
  900.  
  901. if (addedNode.nodeType === Node.ELEMENT_NODE) {
  902. //Checks to identify if this is a segment in regular display mode.
  903. if (addedNode.querySelector(ROAD_TYPE_DROPDOWN_SELECTOR)) {
  904. if (isChecked('csRoadTypeButtonsCheckBox')) addRoadTypeButtons();
  905. if (isSwapPedestrianPermitted() && isChecked('csAddSwapPedestrianButtonCheckBox')) {
  906. addSwapPedestrianButton('regular');
  907. }
  908. }
  909. //Checks to identify if this is a segment in compact display mode.
  910. if (addedNode.querySelector(ROAD_TYPE_CHIP_SELECTOR)) {
  911. if (isChecked('csRoadTypeButtonsCheckBox')) addCompactRoadTypeChangeEvents();
  912. if (isSwapPedestrianPermitted() && isChecked('csAddSwapPedestrianButtonCheckBox')) {
  913. addSwapPedestrianButton('compact');
  914. }
  915. }
  916. if (addedNode.querySelector(PARKING_SPACES_DROPDOWN_SELECTOR) && isChecked('csParkingSpacesButtonsCheckBox')) {
  917. addParkingSpacesButtons();
  918. }
  919. if (addedNode.querySelector(PARKING_COST_DROPDOWN_SELECTOR)
  920. && isChecked('csParkingCostButtonsCheckBox')) {
  921. addParkingCostButtons();
  922. }
  923. if (addedNode.querySelector('.side-panel-section') && isChecked('csAddAltCityButtonCheckBox')) {
  924. addAddAltCityButton();
  925. }
  926. }
  927. }
  928. });
  929. });
  930.  
  931. observer.observe(document.getElementById('edit-panel'), { childList: true, subtree: true });
  932. initUserPanel();
  933. loadSettingsFromStorage();
  934. injectCss();
  935. // W.prefs.on('change:isImperial', () => errorHandler(() => { initUserPanel(); loadSettingsFromStorage(); }));
  936. updateControls(); // In case of PL w/ segments selected.
  937. W.selectionManager.events.register('selectionchanged', null, () => errorHandler(updateControls));
  938.  
  939. logDebug('Initialized');
  940. }
  941.  
  942. function bootstrap() {
  943. if (typeof require !== 'undefined' && W && W.loginManager && W.loginManager.events.register && W.map && W.loginManager.user) {
  944. logDebug('Initializing...');
  945. init();
  946. } else {
  947. logDebug('Bootstrap failed. Trying again...');
  948. setTimeout(bootstrap, 250);
  949. }
  950. }
  951.  
  952. // Not sure if the document.ready is necessary but I'm leaving it because of some random errors
  953. // that people were having with "require is not defined". I tried several things to fix it and
  954. // I'm leaving those things, though all may not be needed.
  955. $(document).ready(() => {
  956. logDebug('Bootstrap...');
  957. bootstrap();
  958. });
  959. } // END Main function (code to be injected)
  960.  
  961. function injectMain(argsObject) {
  962. if (typeof require !== 'undefined' && typeof $ !== 'undefined') {
  963. // const scriptElem = document.createElement('script');
  964. // scriptElem.textContent = `(function(){${main.toString()}\n main(${JSON.stringify(argsObject).replace('\'', '\\\'')})})();`;
  965. // scriptElem.setAttribute('type', 'application/javascript');
  966. // document.body.appendChild(scriptElem);
  967.  
  968. GM_addElement('script', {
  969. textContent: `(function(){${main.toString()}\n main(${JSON.stringify(argsObject).replace('\'', '\\\'')})})();`
  970. });
  971. } else {
  972. setTimeout(() => injectMain(argsObject), 250);
  973. }
  974. }
  975.  
  976. function setValue(object, path, value) {
  977. const pathParts = path.split('.');
  978. for (let i = 0; i < pathParts.length - 1; i++) {
  979. const pathPart = pathParts[i];
  980. if (pathPart in object) {
  981. object = object[pathPart];
  982. } else {
  983. object[pathPart] = {};
  984. object = object[pathPart];
  985. }
  986. }
  987. object[pathParts[pathParts.length - 1]] = value;
  988. }
  989.  
  990. function convertTranslationsArrayToObject(arrayIn) {
  991. const translations = {};
  992. let iRow;
  993. let iCol;
  994. const languages = arrayIn[0].map(lang => lang.toLowerCase());
  995. for (iCol = 1; iCol < languages.length; iCol++) {
  996. translations[languages[iCol]] = {};
  997. }
  998. for (iRow = 1; iRow < arrayIn.length; iRow++) {
  999. const row = arrayIn[iRow];
  1000. const propertyPath = row[0];
  1001. for (iCol = 1; iCol < row.length; iCol++) {
  1002. setValue(translations[languages[iCol]], propertyPath, row[iCol]);
  1003. }
  1004. }
  1005. return translations;
  1006. }
  1007.  
  1008. function loadTranslations() {
  1009. if (typeof $ === 'undefined') {
  1010. setTimeout(loadTranslations, 250);
  1011. console.debug('ClickSaver:', 'jQuery not ready. Retry loading translations...');
  1012. } else {
  1013. // This call retrieves the data from the translations spreadsheet and then injects
  1014. // the main code into the page. If the spreadsheet call fails, the default English
  1015. // translation is used.
  1016. const args = {
  1017. scriptName: SCRIPT_NAME,
  1018. scriptVersion: SCRIPT_VERSION,
  1019. forumUrl: FORUM_URL
  1020. };
  1021. $.getJSON(`${TRANSLATIONS_URL}?${DEC(API_KEY)}`).then(res => {
  1022. args.translations = convertTranslationsArrayToObject(res.values);
  1023. console.debug('ClickSaver:', 'Translations loaded.');
  1024. }).fail(() => {
  1025. console.error('ClickSaver: Error loading translations spreadsheet. Using default translation (English).');
  1026. args.useDefaultTranslation = true;
  1027. }).always(() => {
  1028. // Leave this document.ready function. Some people randomly get a "require is not defined" error unless the injectMain function
  1029. // is called late enough. Even with a "typeof require !== 'undefined'" check.
  1030. $(document).ready(() => {
  1031. injectMain(args);
  1032. });
  1033. });
  1034. }
  1035. }
  1036.  
  1037. // This function requires WazeWrap so it must be called outside of the injected code, as
  1038. // WazeWrap is not guaranteed to be available in the page's scope.
  1039. function addToggleDrawNewRoadsAsTwoWayShortcut() {
  1040. new WazeWrap.Interface.Shortcut('ToggleTwoWayNewSeg', 'Toggle new segment two-way drawing',
  1041. 'editing', 'editToggleNewSegTwoWayDrawing', EXTERNAL_SETTINGS.toggleTwoWaySegDrawingShortcut,
  1042. () => { $('wz-checkbox[name="twoWaySegmentsByDefault"]').click(); }, null).add();
  1043. }
  1044.  
  1045. function sandboxLoadSettings() {
  1046. const loadedSettings = JSON.parse(localStorage.getItem(EXTERNAL_SETTINGS_NAME)) || {};
  1047. EXTERNAL_SETTINGS.toggleTwoWaySegDrawingShortcut = loadedSettings.toggleTwoWaySegDrawingShortcut || '';
  1048. addToggleDrawNewRoadsAsTwoWayShortcut();
  1049. $(window).on('beforeunload', () => sandboxSaveSettings());
  1050. }
  1051.  
  1052. function sandboxSaveSettings() {
  1053. let keys = '';
  1054. const { shortcut } = W.accelerators.Actions.ToggleTwoWayNewSeg;
  1055. if (shortcut) {
  1056. if (shortcut.altKey) keys += 'A';
  1057. if (shortcut.shiftKey) keys += 'S';
  1058. if (shortcut.ctrlKey) keys += 'C';
  1059. if (keys.length) keys += '+';
  1060. if (shortcut.keyCode) keys += shortcut.keyCode;
  1061. }
  1062. EXTERNAL_SETTINGS.toggleTwoWaySegDrawingShortcut = keys;
  1063. localStorage.setItem(EXTERNAL_SETTINGS_NAME, JSON.stringify(EXTERNAL_SETTINGS));
  1064. }
  1065.  
  1066. function sandboxBootstrap() {
  1067. if (WazeWrap && WazeWrap.Ready) {
  1068. WazeWrap.Interface.ShowScriptUpdate(SCRIPT_NAME, SCRIPT_VERSION, UPDATE_MESSAGE, FORUM_URL);
  1069. sandboxLoadSettings();
  1070. } else {
  1071. setTimeout(sandboxBootstrap, 250);
  1072. }
  1073. }
  1074.  
  1075. // Go ahead and start loading translations, and inject the main code into the page.
  1076. loadTranslations();
  1077.  
  1078. // Start the "sandboxed" code.
  1079. sandboxBootstrap();
  1080. })();

QingJ © 2025

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