WME Segment City Highlighter

Highlighter to help out with cities on WME road segments

目前為 2024-08-19 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name WME Segment City Highlighter
  3. // @namespace WazeDev
  4. // @version 2024.08.19.000
  5. // @description Highlighter to help out with cities on WME road segments
  6. // @author MapOMatic
  7. // @include /^https:\/\/(www|beta)\.waze\.com\/(?!user\/)(.{2,6}\/)?editor\/?.*$/
  8. // @grant none
  9. // @license GNU GPLv3
  10. // ==/UserScript==
  11.  
  12. /* global W */
  13. /* global _ */
  14. /* global OpenLayers */
  15.  
  16. (function main() {
  17. 'use strict';
  18.  
  19. const SCRIPT_STORE = 'wme-sc-highlighter';
  20. const SCRIPT_NAME = GM_info.script.name;
  21. const NO_CITY_NAME = '<No city>';
  22. const CSS = `
  23. input.wmesch-city-input { height: 22px; }
  24. .wmesch-clear-text { height: 22px; vertical-align: bottom; }
  25. .wmesch-btn { height: 22px; float: right; }
  26. #wmesch-container {border: #bbb 1px solid; border-radius: 4px; margin: 2px 10px; padding: 4px;}
  27. #wmesch-container table { width: 100%; }
  28. #wmesch-container td { vertical-align:top; }
  29. #wmesch-container .header-label { float: right; }
  30. #wmesch-container .header-cell { width: 90px; }
  31. #wmesch-container .wmesch-city-input { width: 160px; }
  32. .wmesch-preview { float: left; }
  33. `;
  34. const _lastValues = {};
  35. let LAYER_Z_INDEX;
  36.  
  37. let _mapLayer;
  38. let _$previewCheckbox;
  39. let _$primaryCityText;
  40. let _$altCityText;
  41.  
  42. function log(msg) {
  43. console.log('WME SCH:', msg);
  44. }
  45.  
  46. function _id(name) {
  47. return `wmesch-${name}`;
  48. }
  49.  
  50. function saveSettings() {
  51. localStorage.setItem(SCRIPT_STORE, JSON.stringify({
  52. primaryCity: _$primaryCityText.val(),
  53. altCity: _$altCityText.val(),
  54. preview: _$previewCheckbox.prop('checked')
  55. }));
  56. }
  57.  
  58. function loadSettings() {
  59. const settings = $.parseJSON(localStorage.getItem(SCRIPT_STORE) || '{}');
  60.  
  61. _$primaryCityText.val(settings.primaryCity || '');
  62. _$altCityText.val(settings.altCity || '');
  63. _$previewCheckbox.prop('checked', settings.preview || false);
  64. }
  65.  
  66. function updateCityLists() {
  67. const cities = W.model.cities.getObjectArray()
  68. .map(city => city.attributes.name)
  69. .filter(name => name.length)
  70. .sort()
  71. .map(name => `<option value="${name}">`);
  72. $(`#${_id('alt-city-datalist')}`).empty().append(cities);
  73. cities.push(`<option value="${NO_CITY_NAME}">`);
  74. $(`#${_id('primary-city-datalist')}`).empty().append(cities);
  75. }
  76.  
  77. function getStreetInfo(streetID, isPrimary = false) {
  78. const street = W.model.streets.getObjectById(streetID);
  79. if (!street) {
  80. return { ignore: true };
  81. }
  82. const city = W.model.cities.getObjectById(street.attributes.cityID);
  83. if (!city) return { ignore: true };
  84. const state = W.model.states.getObjectById(city.attributes.stateID);
  85. const country = W.model.countries.getObjectById(city.getCountryID());
  86. // If country is not found, it will be assumed the city is not a valid city and will be treated the
  87. // same as a no - city segment. i.e. it wll be removed if primary or any alts have a city with the same street name.
  88. return {
  89. id: streetID,
  90. streetName: street.attributes.name,
  91. cityName: country ? W.model.cities.getObjectById(street.attributes.cityID).attributes.name : '',
  92. stateID: state.attributes.id,
  93. countryID: country ? country.attributes.id : -1,
  94. isPrimary
  95. };
  96. }
  97.  
  98. function processSegments(segments) {
  99. const roadTypesToIgnore = [18];
  100. segments = segments.filter(s => roadTypesToIgnore.indexOf(s.attributes.roadType) === -1);
  101. const newPrimaryCityName = $(`#${_id('primary-city')}`).val().trim();
  102. const newAltCityName = $(`#${_id('alt-city')}`).val().trim();
  103. const removeOtherAltCities = $(`#${_id('remove-other-alts')}`).prop('checked');
  104. const result = { actions: [], affectedSegments: [], altIdsToRemove: [] };
  105.  
  106. segments.forEach(segment => {
  107. const segmentAttr = segment.attributes;
  108. let isSegmentEdited = false;
  109.  
  110. if (segmentAttr.primaryStreetID) {
  111. const primaryStreetInfo = getStreetInfo(segmentAttr.primaryStreetID, true);
  112. const noPrimaryCity = newPrimaryCityName === NO_CITY_NAME;
  113. if (newPrimaryCityName && ((!noPrimaryCity && primaryStreetInfo.cityName !== newPrimaryCityName)
  114. || (noPrimaryCity && !!primaryStreetInfo.cityName))) {
  115. isSegmentEdited = true;
  116. }
  117.  
  118. let streetInfos = [primaryStreetInfo];
  119. if (noPrimaryCity) {
  120. primaryStreetInfo.cityName = '';
  121. } else if (newPrimaryCityName) {
  122. primaryStreetInfo.cityName = newPrimaryCityName;
  123. }
  124.  
  125. const altStreetInfos = segmentAttr.streetIDs.map(streetID => getStreetInfo(streetID));
  126. streetInfos = streetInfos.concat(altStreetInfos);
  127. if (!streetInfos.some(streetInfo => streetInfo.ignore)) {
  128. let cityNames = _.uniq(streetInfos.map(streetInfo => streetInfo.cityName).filter(cityName => !!cityName));
  129. if (newAltCityName && cityNames.indexOf(newAltCityName) === -1) cityNames.push(newAltCityName);
  130. const streetNames = _.uniq(streetInfos.map(streetInfo => streetInfo.streetName).filter(streetName => !!streetName));
  131. if (removeOtherAltCities) {
  132. cityNames = cityNames.filter(cityName => cityName === newPrimaryCityName || cityName === newAltCityName);
  133. }
  134. cityNames.forEach(cityName => {
  135. streetNames.forEach(streetName => {
  136. if (!streetInfos.some(streetInfo => streetInfo.streetName === streetName && streetInfo.cityName === cityName)) {
  137. isSegmentEdited = true;
  138. streetInfos.push({ streetID: -999, streetName, cityName });
  139. }
  140. });
  141. });
  142. if (cityNames.length) {
  143. const altIdsToRemove = altStreetInfos.filter(altStreetInfo => {
  144. if (newPrimaryCityName && newPrimaryCityName === altStreetInfo.cityName
  145. && primaryStreetInfo.streetName === altStreetInfo.streetName) {
  146. return true;
  147. } if (!altStreetInfo.cityName) {
  148. return true;
  149. }
  150. return false;
  151. }).map(altStreetInfo => altStreetInfo.id);
  152. if (altIdsToRemove.length) {
  153. result.altIdsToRemove.push({
  154. segment,
  155. altIds: altIdsToRemove
  156. });
  157. isSegmentEdited = true;
  158. }
  159. }
  160. }
  161. }
  162. if (isSegmentEdited) result.affectedSegments.push(segment);
  163. });
  164.  
  165. return result;
  166. }
  167.  
  168. function highlightSegments() {
  169. if (!_$previewCheckbox.prop('checked')) return;
  170. _mapLayer.removeAllFeatures();
  171. const result = processSegments(W.model.segments.getObjectArray(), true);
  172. const features = W.map.segmentLayer.features.filter(f => result.affectedSegments.indexOf(f.attributes.wazeFeature._wmeObject) > -1).map(f => {
  173. const geometry = f.geometry.clone();
  174. const style = {
  175. strokeColor: '#ff0',
  176. strokeDashstyle: 'solid',
  177. strokeWidth: 30
  178. };
  179. return new OpenLayers.Feature.Vector(geometry, null, style);
  180. });
  181. _mapLayer.addFeatures(features);
  182. }
  183.  
  184. function onSegmentsAdded() {
  185. highlightSegments();
  186. }
  187.  
  188. function onCitiesAddedToModel() {
  189. updateCityLists();
  190. }
  191.  
  192. function onCityTextChange() {
  193. const id = $(this).attr('id');
  194. if (id) {
  195. _lastValues[id] = $(this).val();
  196. }
  197. saveSettings();
  198. highlightSegments();
  199. }
  200.  
  201. function onClearTextClick() {
  202. $(`#${_id($(this).attr('for'))}`).val(null).change();
  203. }
  204.  
  205. function onPreviewChanged() {
  206. saveSettings();
  207. if (_$previewCheckbox.prop('checked')) {
  208. highlightSegments();
  209. W.model.segments.on('objectsadded', onSegmentsAdded);
  210. W.model.segments.on('objectschanged', onSegmentsAdded);
  211. } else {
  212. _mapLayer.removeAllFeatures();
  213. W.model.segments.off('objectsadded', onSegmentsAdded);
  214. W.model.segments.off('objectschanged', onSegmentsAdded);
  215. }
  216. onSelectionChanged();
  217. }
  218.  
  219. function onSelectionChanged() {
  220. try {
  221. const selected = W.selectionManager.getSelectedDataModelObjects()[0];
  222. const isSegment = selected?.type === 'segment';
  223. $(`#${_id('container')}`).css({ display: isSegment ? '' : 'none' });
  224. } catch (ex) {
  225. console.error(SCRIPT_NAME, ex);
  226. }
  227. }
  228.  
  229. function initGui() {
  230. _$previewCheckbox = $('<input>', {
  231. id: _id('preview'),
  232. type: 'checkbox',
  233. class: _id('preview')
  234. });
  235. _$primaryCityText = $('<input>', {
  236. id: _id('primary-city'),
  237. type: 'text',
  238. class: _id('city-input'),
  239. list: _id('primary-city-datalist'),
  240. autocomplete: 'off' // helps prevent password manager from displaying a popup list
  241. });
  242. _$altCityText = $('<input>', {
  243. id: _id('alt-city'),
  244. type: 'text',
  245. class: _id('city-input'),
  246. list: _id('alt-city-datalist'),
  247. autocomplete: 'off' // helps prevent password manager from displaying a popup list
  248. });
  249.  
  250. // TODO: 2022-11-22 - This is temporary to determine which parent element to add the div to, depending on beta or production WME.
  251. // Remove once new side panel is pushed to production.
  252. const $parent = $('#edit-panel .contents');
  253. $parent.prepend(
  254. $('<div>', { id: _id('container') }).append(
  255. $('<table>').append(
  256. $('<tr>').append(
  257. $('<td>', { class: 'header-cell' }).append($('<label>', { class: 'header-label' }).text('Primary city')),
  258. $('<td>').append(
  259. _$primaryCityText,
  260. $('<button>', { class: _id('clear-text'), for: 'primary-city' }).text('x')
  261. )
  262. ),
  263. $('<tr>').append(
  264. $('<td>', { class: 'header-cell' }).append($('<label>', { class: 'header-label' }).text('Alt city')),
  265. $('<td>').append(
  266. _$altCityText,
  267. $('<button>', { class: _id('clear-text'), for: 'alt-city' }).text('x')
  268. )
  269. ),
  270. $('<tr>').append($('<td>', { colspan: '2', class: _id('run-button-container') }).append(
  271. $('<div>').append(
  272. $('<div>', { class: `controls-container ${_id('preview')}` }).append(
  273. _$previewCheckbox.change(onPreviewChanged),
  274. $('<label>', { for: _id('preview') }).text('Preview')
  275. )
  276. )
  277. ))
  278. ),
  279. $('<datalist>', { id: _id('primary-city-datalist') }),
  280. $('<datalist>', { id: _id('alt-city-datalist') })
  281. )
  282. );
  283. $(`.${_id('clear-text')}`).click(onClearTextClick);
  284. $(`.${_id('city-input')}`).each2((idx, obj) => {
  285. const [{ id }] = obj;
  286. const lastVal = _lastValues[id];
  287. if (lastVal) obj.val(lastVal);
  288. }).change(onCityTextChange);
  289.  
  290. updateCityLists();
  291. loadSettings();
  292. onPreviewChanged();
  293. }
  294.  
  295. function initLayer() {
  296. _mapLayer = new OpenLayers.Layer.Vector('WME Segment City Highlighter', { uniqueName: '__wmeSegmentCityHighlighter' });
  297. W.map.addLayer(_mapLayer);
  298.  
  299. // W.map.setLayerIndex(_mapLayer, W.map.getLayerIndex(W.map.roadLayers[0])-2);
  300. // HACK to get around conflict with URO+. If URO+ is fixed, this can be replaced with the setLayerIndex line above.
  301. LAYER_Z_INDEX = W.map.roadLayer.getZIndex() - 2;
  302. _mapLayer.setZIndex(LAYER_Z_INDEX);
  303. const checkLayerZIndex = () => { if (_mapLayer.getZIndex() !== LAYER_Z_INDEX) _mapLayer.setZIndex(LAYER_Z_INDEX); };
  304. setInterval(() => { checkLayerZIndex(); }, 100);
  305. // END HACK
  306.  
  307. _mapLayer.setOpacity(0.6);
  308. _mapLayer.setVisibility(true);
  309. }
  310.  
  311. function init() {
  312. $(`<style type="text/css">${CSS}</style>`).appendTo('head');
  313. W.model.cities.on('objectsadded', onCitiesAddedToModel);
  314. W.selectionManager.events.register('selectionchanged', null, onSelectionChanged);
  315. initLayer();
  316. initGui();
  317. }
  318.  
  319. function bootstrap(tries = 1) {
  320. if (W && W.loginManager && W.loginManager.user && $('#sidebar').length) {
  321. init();
  322. } else if (tries > 200) {
  323. log('Bootstrap has failed too many times. Exiting script.');
  324. } else {
  325. if (tries % 20 === 0) log('Bootstrap failed. Trying again...');
  326. setTimeout(() => bootstrap(++tries), 250);
  327. }
  328. }
  329.  
  330. bootstrap();
  331. })();

QingJ © 2025

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