WME E40

Setup POI geometry properties in one click

当前为 2019-08-08 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name WME E40
  3. // @version 0.0.2
  4. // @description Setup POI geometry properties in one click
  5. // @author Anton Shevchuk
  6. // @license MIT License
  7. // @include https://www.waze.com/editor*
  8. // @include https://www.waze.com/*/editor*
  9. // @include https://beta.waze.com/editor*
  10. // @include https://beta.waze.com/*/editor*
  11. // @exclude https://www.waze.com/user/editor*
  12. // @exclude https://beta.waze.com/user/editor*
  13. // @grant none
  14. // @require https://greasyfork.org/scripts/24851-wazewrap/code/WazeWrap.js
  15. // @supportURL https://github.com/AntonShevchuk/wme-e40/issues
  16. // @namespace https://greasyfork.org/users/227648
  17. // ==/UserScript==
  18.  
  19. /* jshint esversion: 6 */
  20. /* global require, window, WazeWrap, OL */
  21. (function ($, WazeApi, I18n) {
  22. 'use strict';
  23.  
  24. // Script name, uses as unique index
  25. const NAME = 'E40';
  26.  
  27. // Translations
  28. const LOCALE = I18n.currentLocale();
  29. const translation = {
  30. 'en': {
  31. title: 'Geometry',
  32. orthogonalize: 'Orthogonalize',
  33. simplify: 'Simplify',
  34. scale: 'Scale',
  35. },
  36. 'uk': {
  37. title: 'Геометрія',
  38. orthogonalize: 'Вирівняти',
  39. simplify: 'Спростити',
  40. scale: 'Масштабувати',
  41. },
  42. 'ru': {
  43. title: 'Геометрия',
  44. orthogonalize: 'Выровнять',
  45. simplify: 'Упростить',
  46. scale: 'Масштабировать',
  47. }
  48. };
  49.  
  50. const buttons = {
  51. A: {
  52. title: '🔲',
  53. shortcut: 'S+49',
  54. callback: () => orthogonalize()
  55. },
  56. B: {
  57. title: '〽️',
  58. shortcut: 'S+50',
  59. callback: () => simplify()
  60. },
  61. C: {
  62. title: '500m²',
  63. shortcut: 'S+51',
  64. callback: () => scaleSelected(500)
  65. },
  66. D: {
  67. title: '650m²',
  68. shortcut: 'S+52',
  69. callback: () => scaleSelected(650)
  70. },
  71. E: {
  72. title: '>650',
  73. shortcut: 'S+53',
  74. callback: () => scaleSelected(650, true)
  75. }
  76. };
  77.  
  78. let WazeActionUpdateFeatureGeometry = require('Waze/Action/UpdateFeatureGeometry');
  79.  
  80. // Scale place to X m²
  81. function scaleSelected(x, orMore = false) {
  82. if (!WazeWrap.hasPlaceSelected()) {
  83. return;
  84. }
  85. let selected = WazeApi.selectionManager.getSelectedFeatures().map((x) => x.model);
  86. scaleArray(selected, x, orMore);
  87. return false;
  88. }
  89. function scaleAll(x = 650) {
  90. scaleArray(WazeApi.model.venues.getObjectArray(), x, true);
  91. return false;
  92. }
  93. function scaleArray(elements, x, orMore = false) {
  94. for (let i = 0; i < elements.length; i++) {
  95. let selected = elements[i];
  96. if (!selected.isGeometryEditable() || selected.isPoint()) {
  97. continue;
  98. }
  99. try {
  100. let oldGeometry = selected.geometry.clone();
  101. let newGeometry = selected.geometry.clone();
  102.  
  103. let scale = Math.sqrt((x + 5) / oldGeometry.getGeodesicArea(WazeApi.map.getProjectionObject()));
  104. if (scale < 1 && orMore) {
  105. continue;
  106. }
  107. newGeometry.resize(scale, newGeometry.getCentroid());
  108.  
  109. let action = new WazeActionUpdateFeatureGeometry(selected, WazeApi.model.venues, oldGeometry, newGeometry);
  110. WazeApi.model.actionManager.add(action);
  111.  
  112. } catch (e) {
  113. log('skipped');
  114. }
  115. }
  116. }
  117. // Orthogonalize place
  118. function orthogonalize() {
  119. if (!WazeWrap.hasPlaceSelected()) {
  120. return;
  121. }
  122.  
  123. let selected = WazeApi.selectionManager.getSelectedFeatures().map((x) => x.model);
  124. orthogonalizeArray(selected);
  125. return false;
  126. }
  127. function orthogonalizeAll() {
  128. let selected = WazeApi.model.venues.getObjectArray();
  129. // skip parking, natural and outdoors
  130. // TODO: make options for filters
  131. selected = selected.filter(model => model.getMainCategory() !== 'OUTDOORS');
  132. selected = selected.filter(model => model.getMainCategory() !== 'PARKING_LOT');
  133. selected = selected.filter(model => model.getMainCategory() !== 'NATURAL_FEATURES');
  134. orthogonalizeArray(selected);
  135. return false;
  136. }
  137. function orthogonalizeArray(elements) {
  138. for (let i = 0; i < elements.length; i++) {
  139. let selected = elements[i];
  140. if (!selected.isGeometryEditable() || selected.isPoint()) {
  141. continue;
  142. }
  143.  
  144. try {
  145. let oldGeometry = selected.geometry.clone();
  146. let newGeometry = WazeWrap.Util.OrthogonalizeGeometry(selected.geometry.clone().components[0].components);
  147.  
  148. if (!compare(oldGeometry.components[0].components, newGeometry)) {
  149. selected.geometry.components[0].components = [].concat(newGeometry);
  150. selected.geometry.components[0].clearBounds();
  151.  
  152. let action = new WazeActionUpdateFeatureGeometry(selected, WazeApi.model.venues, oldGeometry, selected.geometry);
  153. WazeApi.model.actionManager.add(action);
  154. }
  155. } catch (e) {
  156. log('skipped');
  157. }
  158. }
  159. return false;
  160. }
  161. // Simplify place
  162. function simplify(factor = 8) {
  163. if (!WazeWrap.hasPlaceSelected()) {
  164. return;
  165. }
  166.  
  167. let selected = WazeApi.selectionManager.getSelectedFeatures().map((x) => x.model);
  168. simplifyArray(selected, factor);
  169. return false;
  170. }
  171. function simplifyAll() {
  172. simplifyArray(WazeApi.model.venues.getObjectArray());
  173. return false;
  174. }
  175. function simplifyArray(elements, factor = 8) {
  176. for (let i = 0; i < elements.length; i++) {
  177. let selected = elements[i];
  178. if (!selected.isGeometryEditable() || selected.isPoint()) {
  179. continue;
  180. }
  181.  
  182. try {
  183. let oldGeometry = selected.geometry.clone();
  184. let ls = new OL.Geometry.LineString(oldGeometry.components[0].components);
  185. ls = ls.simplify(factor);
  186. let newGeometry = new OL.Geometry.Polygon(new OL.Geometry.LinearRing(ls.components));
  187.  
  188. if (newGeometry.components[0].components.length < oldGeometry.components[0].components.length) {
  189. WazeApi.model.actionManager.add(new WazeActionUpdateFeatureGeometry(selected, WazeApi.model.venues, oldGeometry, newGeometry));
  190. }
  191. } catch (e) {
  192. log('skipped');
  193. }
  194. }
  195. return false;
  196. }
  197. // Compare two polygons point-by-point
  198. function compare(geo1, geo2) {
  199. if (geo1.length !== geo2.length) {
  200. return false;
  201. }
  202. for (let i = 0; i < geo1.length; i++) {
  203. if (Math.abs(geo1[i].x - geo2[i].x) > .1
  204. || Math.abs(geo1[i].y - geo2[i].y) > .1) {
  205. return false;
  206. }
  207. }
  208. return true;
  209. }
  210. // Bootstrap plugin
  211. function bootstrap(tries = 1) {
  212. log('attempt ' + tries);
  213. if (WazeApi &&
  214. WazeApi.map &&
  215. WazeApi.model &&
  216. WazeApi.loginManager.user &&
  217. WazeWrap.Ready) {
  218. log('was initialized');
  219. init();
  220. } else if (tries < 100) {
  221. tries++;
  222. setTimeout(() => bootstrap(tries), 500);
  223. } else {
  224. console.error('initialization failed');
  225. }
  226. }
  227.  
  228. function init() {
  229. // Initial Mutation Observer
  230. initObserver();
  231. // Initial Translation
  232. initTranslation();
  233. // Initial Tab
  234. initUI();
  235. // Initial Handlers
  236. initHandlers();
  237. // Initial Shortcuts
  238. initShortcuts();
  239. // Apply CSS styles
  240. appendStyle(
  241. 'button.waze-btn.E40 { margin: 0 4px 4px 0; padding: 2px; width: 42px; } ' +
  242. 'button.waze-btn.E40:hover { box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.1), inset 0 0 100px 100px rgba(255, 255, 255, 0.3); } '
  243. );
  244. }
  245.  
  246. // Initial Mutation Observer
  247. // #segment-edit-general - for segment tab
  248. // #landmark-edit-general - for POI tab
  249. function initObserver() {
  250. // Check for changes in the edit-panel
  251. let speedLimitsObserver = new MutationObserver(function (mutations) {
  252. mutations.forEach(function (mutation) {
  253. for (let i = 0, total = mutation.addedNodes.length; i < total; i++) {
  254. let node = mutation.addedNodes[i];
  255. // Only fire up if it's a node
  256. if (node.nodeType === Node.ELEMENT_NODE &&
  257. node.querySelector('div.selection') &&
  258. (node.querySelector('#landmark-edit-general') || node.querySelector('#mergeLandmarksCollection')) && // segment or landmark tab
  259. !node.querySelector('div.form-group.' + NAME)) {
  260. createUI();
  261. }
  262. }
  263. });
  264. });
  265.  
  266. speedLimitsObserver.observe(document.getElementById('edit-panel'), {childList: true, subtree: true});
  267. log('observer was run');
  268. }
  269. // Create UI controls everytime when updated DOM of sidebar
  270. // Uses native JS function for better performance
  271. function createUI() {
  272. // Container for buttons
  273. let controls = document.createElement('div');
  274. controls.className = 'controls';
  275. // Create buttons and append it to panel
  276. // Create buttons
  277. for (let btn in buttons) {
  278. let button = document.createElement('button');
  279. button.className = 'waze-btn waze-btn-small ' + NAME + ' ' + NAME + '-' + btn;
  280. button.innerHTML = buttons[btn].title;
  281. button.title = buttons[btn].title;
  282. button.dataset[NAME] = btn;
  283. controls.appendChild(button);
  284. }
  285.  
  286. let label = document.createElement('label');
  287. label.className = 'control-label';
  288. label.innerHTML = I18n.translate(NAME).title;
  289.  
  290. let group = document.createElement('div');
  291. group.className = 'form-group ' + NAME;
  292. group.appendChild(label);
  293. group.appendChild(controls);
  294.  
  295. if (document.getElementById('landmark-edit-general')) {
  296. document.getElementById('landmark-edit-general').prepend(group)
  297. }
  298.  
  299. if (document.getElementById('mergeLandmarksCollection')) {
  300. document.getElementById('mergeLandmarksCollection').prepend(group)
  301. }
  302. }
  303. // Initial Translation for UI and Shortcuts
  304. function initTranslation() {
  305. I18n.translations[LOCALE][NAME] = translation[LOCALE] || translation.en;
  306.  
  307. // Translation for Shortcuts
  308. I18n.translations[LOCALE].keyboard_shortcuts.groups[NAME] = [];
  309. I18n.translations[LOCALE].keyboard_shortcuts.groups[NAME].description = NAME;
  310. I18n.translations[LOCALE].keyboard_shortcuts.groups[NAME].members = [];
  311. }
  312. // Initial UI
  313. function initUI() {
  314. let html =
  315. '<div class="form-group">'+
  316. '<label class="control-label">'+ NAME +'</label>' +
  317. '<div class="button-toolbar">' +
  318. '<p><button type="button" id="E40-orthogonalize" class="btn btn-default">🔲</button> ' + I18n.t(NAME +'.orthogonalize') + '</p>' +
  319. '<p><button type="button" id="E40-simplify" class="btn btn-default">〽️</button> ' + I18n.t(NAME +'.simplify') + '</p>' +
  320. '<p><button type="button" id="E40-650" class="btn btn-default">&gt;650m²</button> ' + I18n.t(NAME +'.scale') + '</p>' +
  321. '</div>' +
  322. '</div>'
  323. ;
  324.  
  325. new WazeWrap.Interface.Tab(NAME, html, function() {
  326. log('tab');
  327. });
  328. }
  329. // Initial button handlers, init it once
  330. function initHandlers() {
  331. $('#edit-panel').on('click', 'button.'+NAME, function() {
  332. let btn = $(this).data(NAME);
  333. return buttons[btn].callback();
  334. });
  335. $('#E40-orthogonalize').on('click', orthogonalizeAll);
  336. $('#E40-simplify').on('click', simplifyAll);
  337. $('#E40-650').on('click', () => scaleAll(650));
  338. }
  339. // Initial shortcuts
  340. function initShortcuts() {
  341. WazeApi.accelerators.Groups[NAME] = [];
  342. WazeApi.accelerators.Groups[NAME].members = [];
  343.  
  344. for (let btn in buttons) {
  345. let name = NAME + 'Button' + buttons[btn].title;
  346. WazeApi.accelerators.addAction(name, { group: NAME });
  347. WazeApi.accelerators.events.register(name, null, buttons[btn].callback);
  348. WazeApi.accelerators.registerShortcut(buttons[btn].shortcut, name);
  349. }
  350. }
  351. // Apply CSS styles
  352. function appendStyle(css) {
  353. let style = document.createElement('style');
  354. style.type = 'text/css';
  355. style.innerHTML = css;
  356. document.getElementsByTagName('head')[0].appendChild(style);
  357. }
  358. // Simple console.log wrapper
  359. function log(message) {
  360. console.log(NAME + ': ' + message);
  361. }
  362. log('initialization');
  363. bootstrap();
  364. })(window.jQuery, window.W, window.I18n);