WME Segment City Highlighter

Highlighter to help out with cities on WME road segments

目前为 2024-09-08 提交的版本。查看 最新版本

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

QingJ © 2025

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