WME Address Point Helper

Creates point with same address

  1. // ==UserScript==
  2. // @name WME Address Point Helper
  3. // @description Creates point with same address
  4. // @version 2.5.7
  5. // @license MIT License
  6. // @author Andrei Pavlenko, Anton Shevchuk
  7. // @namespace https://greasyfork.org/ru/users/160654-waze-ukraine
  8. // @match https://*.waze.com/editor*
  9. // @match https://*.waze.com/*/editor*
  10. // @exclude https://*.waze.com/user/editor*
  11. // @icon data:image/png;base64,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
  12. // @grant none
  13. // @require https://update.greasyfork.org/scripts/389765/1090053/CommonUtils.js
  14. // @require https://update.greasyfork.org/scripts/450160/1218867/WME-Bootstrap.js
  15. // @require https://update.greasyfork.org/scripts/452563/1218878/WME.js
  16. // @require https://update.greasyfork.org/scripts/450221/1137043/WME-Base.js
  17. // @require https://update.greasyfork.org/scripts/450320/1555446/WME-UI.js
  18. // @require https://update.greasyfork.org/scripts/480123/1281900/WME-EntryPoint.js
  19.  
  20. // ==/UserScript==
  21.  
  22. /* jshint esversion: 8 */
  23. /* global require */
  24. /* global GM_info */
  25. /* global $, jQuery */
  26. /* global W, W.model */
  27. /* global I18n */
  28. /* global OpenLayers */
  29. /* global NavigationPoint */
  30. /* global WME, WMEBase, WMEUI, WMEUIHelper */
  31. /* global Container, Settings, SimpleCache, Tools */
  32.  
  33. (function () {
  34. 'use strict'
  35.  
  36. // Script name, uses as unique index
  37. const NAME = 'ADDRESS-POINT-HELPER'
  38.  
  39. const TRANSLATION = {
  40. 'en': {
  41. title: 'APH📍',
  42. description: 'Address Point Helper 📍',
  43. buttons: {
  44. createPoint: 'Clone to POI',
  45. createResidential: 'Clone to AT',
  46. newPoint: 'Create new point'
  47. },
  48. settings: {
  49. title: 'Options',
  50. addNavigationPoint: 'Add entry point',
  51. inheritNavigationPoint: 'Inherit parent\'s landmark entry point',
  52. autoSetHNToName: 'Copy house number into name',
  53. noDuplicates: 'Do not create duplicates',
  54. copyPOI: 'Copy the POI as point'
  55. }
  56. },
  57. 'uk': {
  58. title: 'APH📍',
  59. description: 'Address Point Helper 📍',
  60. buttons: {
  61. createPoint: 'Клон до POI',
  62. createResidential: 'Клон до АТ',
  63. newPoint: 'Створити нову точку POI'
  64. },
  65. settings: {
  66. title: 'Налаштування',
  67. addNavigationPoint: 'Додавати точку в\'їзду',
  68. inheritNavigationPoint: 'Наслідувати точку в\'їзду від POI',
  69. autoSetHNToName: 'Копіювати номер будинку в назву',
  70. noDuplicates: 'Не створювати дублікатів',
  71. copyPOI: 'Копіювати POI як точку',
  72. }
  73. },
  74. 'ru': {
  75. title: 'APH📍',
  76. description: 'Address Point Helper 📍',
  77. buttons: {
  78. createPoint: 'Клон в POI',
  79. createResidential: 'Клон в АТ',
  80. newPoint: 'Создать новую точку POI'
  81. },
  82. settings: {
  83. title: 'Настройки',
  84. addNavigationPoint: 'Создавать точку въезда',
  85. inheritNavigationPoint: 'Наследовать точку въезда от POI',
  86. autoSetHNToName: 'Копировать номер дома в название',
  87. noDuplicates: 'Не создавать дубликатов',
  88. copyPOI: 'Копировать POI как точку',
  89. }
  90. }
  91. }
  92.  
  93. const STYLE = '.address-point-helper legend { cursor:pointer; font-size: 12px; font-weight: bold; width: auto; text-align: right; border: 0; margin: 0; padding: 0 8px; }' +
  94. '.address-point-helper fieldset { border: 1px solid #ddd; padding: 4px; }' +
  95. '.address-point-helper fieldset div.controls label { white-space: normal; }' +
  96. 'button.address-point-helper { border: 1px solid #ddd; margin-right: 2px; }' +
  97. 'p.address-point-helper-info { border-top: 1px solid #ccc; color: #777; font-size: x-small; margin-top: 15px; padding-top: 10px; text-align: center; }'
  98.  
  99. WMEUI.addTranslation(NAME, TRANSLATION)
  100. WMEUI.addStyle(STYLE)
  101.  
  102. // Set shortcuts title
  103. WMEUIShortcut.setGroupTitle(NAME, I18n.t(NAME).description)
  104.  
  105. // default settings
  106. const SETTINGS = {
  107. addNavigationPoint: true,
  108. inheritNavigationPoint: true,
  109. autoSetHNToName: true,
  110. noDuplicates: true,
  111. copyPOI: false,
  112. }
  113.  
  114. const BUTTONS = {
  115. A: {
  116. title: '<i class="w-icon w-icon-node"></i> ' + I18n.t(NAME).buttons.createPoint,
  117. description: I18n.t(NAME).buttons.createPoint,
  118. shortcut: 'A+G',
  119. callback: () => createPoint()
  120. },
  121. B: {
  122. title: '<i class="fa fa-map-marker"></i> ' + I18n.t(NAME).buttons.createResidential,
  123. description: I18n.t(NAME).buttons.createResidential,
  124. shortcut: 'A+H',
  125. callback: () => createResidential()
  126. },
  127. }
  128.  
  129. let scriptSettings = new Settings(NAME, SETTINGS)
  130.  
  131. class APH extends WMEBase {
  132. constructor (name, settings) {
  133. super(name, settings)
  134.  
  135. this.helper = new WMEUIHelper(NAME)
  136.  
  137. // Create tab for settings
  138. this.tab = this.helper.createTab(
  139. I18n.t(NAME).title,
  140. {
  141. icon: 'home'
  142. }
  143. )
  144.  
  145. // Setup options
  146. let fieldsetSettings = this.helper.createFieldset(I18n.t(NAME).settings.title)
  147.  
  148. for (let item in settings.container) {
  149. if (settings.container.hasOwnProperty(item)) {
  150. fieldsetSettings.addCheckbox(
  151. item,
  152. I18n.t(NAME).settings[item],
  153. event => settings.set([item], event.target.checked),
  154. settings.get(item)
  155. )
  156. }
  157. }
  158. this.tab.addElement(fieldsetSettings)
  159.  
  160. this.tab.addText(
  161. 'info',
  162. '<a href="' + GM_info.scriptUpdateURL + '">' + GM_info.script.name + '</a> ' + GM_info.script.version
  163. )
  164.  
  165. this.tab.inject()
  166.  
  167. // Create a panel for POI
  168. this.panel = this.helper.createPanel(I18n.t(NAME).title)
  169. this.panel.addButtons(BUTTONS)
  170.  
  171. /* name, desc, group, title, shortcut, callback, scope */
  172. new WMEUIShortcut(
  173. this.name + '_new_point',
  174. I18n.t(NAME).buttons.newPoint,
  175. this.name,
  176. I18n.t(NAME).buttons.newPoint,
  177. '80', // P
  178. async () => {
  179. function waitForElement(selector, text, timeout = 3000) {
  180. return new Promise((resolve, reject) => {
  181. const interval = 50;
  182. let elapsed = 0;
  183. const timer = setInterval(() => {
  184. let elements = Array.from(document.querySelectorAll(selector));
  185. if (text) {
  186. elements = elements.filter(el => el.textContent.trim() === text);
  187. }
  188. if (elements.length > 0) {
  189. clearInterval(timer);
  190. resolve(elements[0]);
  191. }
  192. elapsed += interval;
  193. if (elapsed >= timeout) {
  194. clearInterval(timer);
  195. reject();
  196. }
  197. }, interval);
  198. });
  199. }
  200.  
  201. try {
  202. // 1. Click the plus icon to open the add menu
  203. const plusBtn = document.querySelector('.menuContainer--VNnFt .w-icon-plus');
  204. if (plusBtn) {
  205. plusBtn.closest('wz-button').click();
  206. } else {
  207. console.warn('Plus button not found!');
  208. return;
  209. }
  210.  
  211. // 2. Wait for the "Other" category to appear
  212. const otherRow = await waitForElement('.itemLabel--kXZjU','Other',3000);
  213. if (otherRow) {
  214. otherRow.scrollIntoView({ block: 'center' });
  215. } else {
  216. console.warn('"Other" category not found!');
  217. return;
  218. }
  219.  
  220. // 3. Click the "point" button in the "Other" row
  221. const wzMenuItem = otherRow.closest('wz-menu-item');
  222. if (!wzMenuItem) {
  223. console.warn('"Other" row menu item not found!');
  224. return;
  225. }
  226. const pointBtn = wzMenuItem.querySelector('wz-button.point');
  227. if (pointBtn) {
  228. pointBtn.click();
  229. } else {
  230. console.warn('"Point" button in "Other" row not found!');
  231. return;
  232. }
  233. } catch (err) {
  234. console.error('Error while creating new point:', err);
  235. }
  236. }
  237. ).register();
  238. }
  239.  
  240. /**
  241. * Handler for `venue.wme` event
  242. * @param {jQuery.Event} event
  243. * @param {HTMLElement} element
  244. * @param {W.model} model
  245. * @return {null|void}
  246. */
  247. onVenue (event, element, model) {
  248. if (!model.isGeometryEditable()) {
  249. return
  250. }
  251. if (element.querySelector('div.form-group.address-point-helper')) {
  252. return
  253. }
  254. element.prepend(this.panel.html())
  255.  
  256. $('button.address-point-helper-A').prop('disabled', !validateForPoint())
  257. $('button.address-point-helper-B').prop('disabled', !validateForResidential())
  258. }
  259.  
  260. /**
  261. * Handler for window `beforeunload` event
  262. * @param {jQuery.Event} event
  263. * @return {Null}
  264. */
  265. onBeforeUnload (event) {
  266. this.settings.save()
  267. }
  268. }
  269.  
  270. $(document).on('bootstrap.wme', () => {
  271. new APH(NAME, scriptSettings)
  272.  
  273. // Register handler for changes
  274. registerEventListeners()
  275. })
  276.  
  277. function createPoint (isResidential = false) {
  278. console.groupCollapsed(
  279. '%c' + NAME + ': 📍%c try to create ' + (isResidential ? 'residential ' : '') + 'point',
  280. 'color: #0DAD8D; font-weight: bold',
  281. 'color: dimgray; font-weight: normal'
  282. )
  283.  
  284. if ((!validateForPoint() && !isResidential)
  285. || (!validateForResidential() && isResidential)) {
  286. console.log('Invalid point')
  287. console.groupEnd()
  288. return
  289. }
  290.  
  291. let WazeFeatureVectorLandmark = require('Waze/Feature/Vector/Landmark')
  292. let WazeActionAddLandmark = require('Waze/Action/AddLandmark')
  293. let WazeActionUpdateObject = require('Waze/Action/UpdateObject')
  294. let WazeActionUpdateFeatureAddress = require('Waze/Action/UpdateFeatureAddress')
  295.  
  296. let { lat, lon } = getPointCoordinates()
  297. let address = getSelectedLandmarkAddress()
  298. let lockRank = getPointLockRank()
  299.  
  300. let pointGeometry = new OpenLayers.Geometry.Point(lon, lat)
  301.  
  302. let NewPoint = new WazeFeatureVectorLandmark({
  303. geoJSONGeometry: W.userscripts.toGeoJSONGeometry(pointGeometry)
  304. })
  305. NewPoint.attributes.categories.push('OTHER')
  306. NewPoint.attributes.lockRank = lockRank
  307. NewPoint.attributes.residential = isResidential
  308.  
  309. if (scriptSettings.get('addNavigationPoint')) {
  310. let newEntryPoint, parentEntryPoint = WME.getSelectedVenue().getAttributes().entryExitPoints[0]
  311. if (scriptSettings.get('inheritNavigationPoint') && parentEntryPoint !== undefined) {
  312. newEntryPoint = new entryPoint().with({primary: true, point: parentEntryPoint.getPoint()})
  313. } else {
  314. newEntryPoint = new entryPoint({primary: true, point: W.userscripts.toGeoJSONGeometry(pointGeometry.clone())})
  315. }
  316. NewPoint.attributes.entryExitPoints.push(newEntryPoint)
  317. }
  318.  
  319. // Modified: If no house number is present, use the POI name + " copy" as the new point's name.
  320. // This ensures every new point has a meaningful name, improving clarity and usability.
  321. if (!!address.attributes.houseNumber) {
  322. NewPoint.attributes.name = address.attributes.houseNumber;
  323. NewPoint.attributes.houseNumber = address.attributes.houseNumber;
  324. } else {
  325. const poiName = WME.getSelectedVenue().attributes.name;
  326. if (poiName && poiName.trim() !== "") {
  327. NewPoint.attributes.name = poiName + " copy";
  328. }
  329. }
  330.  
  331. let newAddressAttributes = {
  332. streetName: address.getStreetName(),
  333. emptyStreet: false,
  334. stateID: address.getState().getID(),
  335. countryID: address.getCountry().getID(),
  336. }
  337.  
  338. if (address.getCity().getID() === 55344
  339. || address.getCityName() === 'поза НП') {
  340. newAddressAttributes.cityName = ''
  341. newAddressAttributes.emptyCity = true
  342. } else {
  343. newAddressAttributes.cityName = address.getCityName()
  344. newAddressAttributes.emptyCity = false
  345. }
  346.  
  347. if (scriptSettings.get('noDuplicates') && hasDuplicate(NewPoint, newAddressAttributes, isResidential)) {
  348. console.log('This point already exists.')
  349. console.groupEnd()
  350. return
  351. }
  352.  
  353. W.selectionManager.unselectAll()
  354. let addedLandmark = new WazeActionAddLandmark(NewPoint)
  355. W.model.actionManager.add(addedLandmark)
  356. W.model.actionManager.add(new WazeActionUpdateFeatureAddress(NewPoint, newAddressAttributes))
  357. if (!!address.attributes.houseNumber) {
  358. W.model.actionManager.add(new WazeActionUpdateObject(NewPoint, { houseNumber: address.attributes.houseNumber }))
  359. }
  360. W.selectionManager.setSelectedModels([addedLandmark.venue])
  361. console.log('The point was created.')
  362. console.groupEnd()
  363. }
  364.  
  365. function createResidential () {
  366. createPoint(true)
  367. }
  368.  
  369. // 2. Checks if a POI can be cloned as a point: always true if "CopyPOI" is enabled, otherwise requires a house number.
  370. function validateForPoint () {
  371. if (scriptSettings.get('copyPOI')) return true;
  372. if (!WME.getSelectedVenue()) return false
  373. let selectedPoiHN = getSelectedLandmarkAddress().attributes.houseNumber
  374. return /\d+/.test(selectedPoiHN)
  375. }
  376.  
  377. function validateForResidential () {
  378. if (!WME.getSelectedVenue()) return false
  379. let selectedPoiHN = getSelectedLandmarkAddress().attributes.houseNumber
  380. return /^\d+[А-ЯЇІЄ]{0,3}$/i.test(selectedPoiHN)
  381. }
  382.  
  383. function getSelectedLandmarkAddress () {
  384. return WME.getSelectedVenue().getAddress(W.model)
  385. }
  386.  
  387. function getPointLockRank () {
  388. let selectedLandmark = WME.getSelectedVenue()
  389. let userRank = W.loginManager.user.attributes.rank
  390. let parentFeatureLockRank = selectedLandmark.getLockRank()
  391.  
  392. if (userRank >= parentFeatureLockRank) {
  393. return parentFeatureLockRank
  394. } else if (userRank >= 1) {
  395. return 1
  396. } else {
  397. return 0
  398. }
  399. }
  400.  
  401. function getPointCoordinates () {
  402. let selectedLandmark = WME.getSelectedVenue()
  403. let selectedLandmarkGeometry = selectedLandmark.getOLGeometry()
  404.  
  405. let coords
  406. if (/polygon/i.test(selectedLandmarkGeometry.id)) {
  407. let polygonCenteroid = selectedLandmarkGeometry.components[0].getCentroid()
  408. let geometryComponents = selectedLandmarkGeometry.components[0].components
  409. let flatComponentsCoords = []
  410. geometryComponents.forEach(c => flatComponentsCoords.push(c.x, c.y))
  411. let interiorPoint = getInteriorPointOfArray(
  412. flatComponentsCoords,
  413. 2, [polygonCenteroid.x, polygonCenteroid.y]
  414. )
  415. coords = {
  416. lon: interiorPoint[0],
  417. lat: interiorPoint[1]
  418. }
  419. } else {
  420. coords = {
  421. lon: selectedLandmarkGeometry.x,
  422. lat: selectedLandmarkGeometry.y
  423. }
  424. }
  425.  
  426. coords.lon += 4 // shift by X
  427. coords.lat += 5 // shift by Y
  428. return coords
  429. }
  430.  
  431. function hasDuplicate (poi, addr, isResidential) {
  432. const venues = W.model.venues.getObjectArray()
  433.  
  434. for (let key in venues) {
  435. if (!venues.hasOwnProperty(key)) continue
  436. const currentVenue = venues[key]
  437. const currentAddress = currentVenue.getAddress(W.model)
  438.  
  439. let equalNames = true // or empty for residential
  440. if (!isResidential && !!currentVenue.attributes.name && !!poi.attributes.name) {
  441. if (currentVenue.attributes.name !== poi.attributes.name) {
  442. equalNames = false
  443. }
  444. }
  445. if (
  446. equalNames
  447. && poi.attributes.houseNumber === currentVenue.attributes.houseNumber
  448. && poi.attributes.residential === currentVenue.attributes.residential
  449. && addr.streetName === currentAddress.getStreetName()
  450. && addr.cityName === currentAddress.getCityName()
  451. && addr.countryID === currentAddress.getCountry().getID()
  452. ) {
  453. return true
  454. }
  455. }
  456. return false
  457. }
  458.  
  459. function registerEventListeners () {
  460. let WazeActionUpdateObject = require('Waze/Action/UpdateObject')
  461.  
  462. W.model.actionManager.events.register('afteraction', null, action => {
  463. // Задаем номер дома в название, если нужно. Пока не нашел более лаконичного способа определить что
  464. // произошло именно изменение адреса. Можно тестить регуляркой поле _description, но будут проблемы с
  465. // нюансами содержания этого поля на разных языках
  466. if (scriptSettings.get('autoSetHNToName')) {
  467. try {
  468. let subAction = action.action.subActions[0]
  469. let houseNumber = subAction.attributes.houseNumber
  470. let feature = subAction.feature
  471. if (feature.attributes.categories.includes('OTHER') && feature.attributes.name === '') {
  472. W.model.actionManager.add(new WazeActionUpdateObject(feature, { name: houseNumber }))
  473. }
  474.  
  475. $('button.address-point-helper-A').prop('disabled', !validateForPoint())
  476. $('button.address-point-helper-B').prop('disabled', !validateForResidential())
  477.  
  478. } catch (e) { /* Do nothing */ }
  479. }
  480. })
  481. }
  482.  
  483. /**
  484. * @link https://github.com/openlayers/openlayers
  485. */
  486. function getInteriorPointOfArray (flatCoordinates, stride, flatCenters) {
  487. let offset = 0
  488. let flatCentersOffset = 0
  489. let ends = [flatCoordinates.length]
  490. let i, ii, x, x1, x2, y1, y2
  491. const y = flatCenters[flatCentersOffset + 1]
  492. const intersections = []
  493. // Calculate intersections with the horizontal line
  494. for (let r = 0, rr = ends.length; r < rr; ++r) {
  495. const end = ends[r]
  496. x1 = flatCoordinates[end - stride]
  497. y1 = flatCoordinates[end - stride + 1]
  498. for (i = offset; i < end; i += stride) {
  499. x2 = flatCoordinates[i]
  500. y2 = flatCoordinates[i + 1]
  501. if ((y <= y1 && y2 <= y) || (y1 <= y && y <= y2)) {
  502. x = (y - y1) / (y2 - y1) * (x2 - x1) + x1
  503. intersections.push(x)
  504. }
  505. x1 = x2
  506. y1 = y2
  507. }
  508. }
  509. // Find the longest segment of the horizontal line that has its center point
  510. // inside the linear ring.
  511. let pointX = NaN
  512. let maxSegmentLength = -Infinity
  513. intersections.sort(numberSafeCompareFunction)
  514. x1 = intersections[0]
  515. for (i = 1, ii = intersections.length; i < ii; ++i) {
  516. x2 = intersections[i]
  517. const segmentLength = Math.abs(x2 - x1)
  518. if (segmentLength > maxSegmentLength) {
  519. x = (x1 + x2) / 2
  520. if (linearRingsContainsXY(flatCoordinates, offset, ends, stride, x, y)) {
  521. pointX = x
  522. maxSegmentLength = segmentLength
  523. }
  524. }
  525. x1 = x2
  526. }
  527. if (isNaN(pointX)) {
  528. // There is no horizontal line that has its center point inside the linear
  529. // ring. Use the center of the the linear ring's extent.
  530. pointX = flatCenters[flatCentersOffset]
  531. }
  532.  
  533. return [pointX, y, maxSegmentLength]
  534. }
  535.  
  536. function numberSafeCompareFunction (a, b) {
  537. return a > b ? 1 : a < b ? -1 : 0
  538. }
  539.  
  540. function linearRingContainsXY (flatCoordinates, offset, end, stride, x, y) {
  541. // http://geomalgorithms.com/a03-_inclusion.html
  542. // Copyright 2000 softSurfer, 2012 Dan Sunday
  543. // This code may be freely used and modified for any purpose
  544. // providing that this copyright notice is included with it.
  545. // SoftSurfer makes no warranty for this code, and cannot be held
  546. // liable for any real or imagined damage resulting from its use.
  547. // Users of this code must verify correctness for their application.
  548. let wn = 0
  549. let x1 = flatCoordinates[end - stride]
  550. let y1 = flatCoordinates[end - stride + 1]
  551. for (; offset < end; offset += stride) {
  552. const x2 = flatCoordinates[offset]
  553. const y2 = flatCoordinates[offset + 1]
  554. if (y1 <= y) {
  555. if (y2 > y && ((x2 - x1) * (y - y1)) - ((x - x1) * (y2 - y1)) > 0) {
  556. wn++
  557. }
  558. } else if (y2 <= y && ((x2 - x1) * (y - y1)) - ((x - x1) * (y2 - y1)) < 0) {
  559. wn--
  560. }
  561. x1 = x2
  562. y1 = y2
  563. }
  564. return wn !== 0
  565. }
  566.  
  567. function linearRingsContainsXY (flatCoordinates, offset, ends, stride, x, y) {
  568. if (ends.length === 0) {
  569. return false
  570. }
  571. if (!linearRingContainsXY(flatCoordinates, offset, ends[0], stride, x, y)) {
  572. return false
  573. }
  574. for (let i = 1, ii = ends.length; i < ii; ++i) {
  575. if (linearRingContainsXY(flatCoordinates, ends[i - 1], ends[i], stride, x, y)) {
  576. return false
  577. }
  578. }
  579. return true
  580. }
  581. })()