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.6
  5. // @license MIT License
  6. // @author Andrei Pavlenko, Anton Shevchuk
  7. // @namespace https://greatest.deepsurf.us/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.greatest.deepsurf.us/scripts/389765/1090053/CommonUtils.js
  14. // @require https://update.greatest.deepsurf.us/scripts/450160/1218867/WME-Bootstrap.js
  15. // @require https://update.greatest.deepsurf.us/scripts/452563/1218878/WME.js
  16. // @require https://update.greatest.deepsurf.us/scripts/450221/1137043/WME-Base.js
  17. // @require https://update.greatest.deepsurf.us/scripts/450320/1555446/WME-UI.js
  18. // @require https://update.greatest.deepsurf.us/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. }
  55. },
  56. 'uk': {
  57. title: 'APH📍',
  58. description: 'Address Point Helper 📍',
  59. buttons: {
  60. createPoint: 'Клон до POI',
  61. createResidential: 'Клон до АТ',
  62. newPoint: 'Створити нову точку POI'
  63. },
  64. settings: {
  65. title: 'Налаштування',
  66. addNavigationPoint: 'Додавати точку в\'їзду',
  67. inheritNavigationPoint: 'Наслідувати точку в\'їзду від POI',
  68. autoSetHNToName: 'Копіювати номер будинку в назву',
  69. noDuplicates: 'Не створювати дублікатів'
  70. }
  71. },
  72. 'ru': {
  73. title: 'APH📍',
  74. description: 'Address Point Helper 📍',
  75. buttons: {
  76. createPoint: 'Клон в POI',
  77. createResidential: 'Клон в АТ',
  78. newPoint: 'Создать новую точку POI'
  79. },
  80. settings: {
  81. title: 'Настройки',
  82. addNavigationPoint: 'Создавать точку въезда',
  83. inheritNavigationPoint: 'Наследовать точку въезда от POI',
  84. autoSetHNToName: 'Копировать номер дома в название',
  85. noDuplicates: 'Не создавать дубликатов'
  86. }
  87. }
  88. }
  89.  
  90. 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; }' +
  91. '.address-point-helper fieldset { border: 1px solid #ddd; padding: 4px; }' +
  92. '.address-point-helper fieldset div.controls label { white-space: normal; }' +
  93. 'button.address-point-helper { border: 1px solid #ddd; margin-right: 2px; }' +
  94. '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; }'
  95.  
  96. WMEUI.addTranslation(NAME, TRANSLATION)
  97. WMEUI.addStyle(STYLE)
  98.  
  99. // Set shortcuts title
  100. WMEUIShortcut.setGroupTitle(NAME, I18n.t(NAME).description)
  101.  
  102. // default settings
  103. const SETTINGS = {
  104. addNavigationPoint: false,
  105. inheritNavigationPoint: false,
  106. autoSetHNToName: false,
  107. noDuplicates: false
  108. }
  109.  
  110. const BUTTONS = {
  111. A: {
  112. title: '<i class="w-icon w-icon-node"></i> ' + I18n.t(NAME).buttons.createPoint,
  113. description: I18n.t(NAME).buttons.createPoint,
  114. shortcut: 'A+G',
  115. callback: () => createPoint()
  116. },
  117. B: {
  118. title: '<i class="fa fa-map-marker"></i> ' + I18n.t(NAME).buttons.createResidential,
  119. description: I18n.t(NAME).buttons.createResidential,
  120. shortcut: 'A+H',
  121. callback: () => createResidential()
  122. },
  123. }
  124.  
  125. let scriptSettings = new Settings(NAME, SETTINGS)
  126.  
  127. class APH extends WMEBase {
  128. constructor (name, settings) {
  129. super(name, settings)
  130.  
  131. this.helper = new WMEUIHelper(NAME)
  132.  
  133. // Create tab for settings
  134. this.tab = this.helper.createTab(
  135. I18n.t(NAME).title,
  136. {
  137. icon: 'home'
  138. }
  139. )
  140.  
  141. // Setup options
  142. let fieldsetSettings = this.helper.createFieldset(I18n.t(NAME).settings.title)
  143.  
  144. for (let item in settings.container) {
  145. if (settings.container.hasOwnProperty(item)) {
  146. fieldsetSettings.addCheckbox(
  147. item,
  148. I18n.t(NAME).settings[item],
  149. event => settings.set([item], event.target.checked),
  150. settings.get(item)
  151. )
  152. }
  153. }
  154. this.tab.addElement(fieldsetSettings)
  155.  
  156. this.tab.addText(
  157. 'info',
  158. '<a href="' + GM_info.scriptUpdateURL + '">' + GM_info.script.name + '</a> ' + GM_info.script.version
  159. )
  160.  
  161. this.tab.inject()
  162.  
  163. // Create a panel for POI
  164. this.panel = this.helper.createPanel(I18n.t(NAME).title)
  165. this.panel.addButtons(BUTTONS)
  166.  
  167. /* name, desc, group, title, shortcut, callback, scope */
  168. new WMEUIShortcut(
  169. this.name + '_new_point',
  170. I18n.t(NAME).buttons.newPoint,
  171. this.name,
  172. I18n.t(NAME).buttons.newPoint,
  173. '80', // P
  174. () => $('.toolbar-group-item.other').find('wz-button.point').click()
  175. ).register()
  176. }
  177.  
  178. /**
  179. * Handler for `venue.wme` event
  180. * @param {jQuery.Event} event
  181. * @param {HTMLElement} element
  182. * @param {W.model} model
  183. * @return {null|void}
  184. */
  185. onVenue (event, element, model) {
  186. if (!model.isGeometryEditable()) {
  187. return
  188. }
  189. if (element.querySelector('div.form-group.address-point-helper')) {
  190. return
  191. }
  192. element.prepend(this.panel.html())
  193.  
  194. $('button.address-point-helper-A').prop('disabled', !validateForPoint())
  195. $('button.address-point-helper-B').prop('disabled', !validateForResidential())
  196. }
  197.  
  198. /**
  199. * Handler for window `beforeunload` event
  200. * @param {jQuery.Event} event
  201. * @return {Null}
  202. */
  203. onBeforeUnload (event) {
  204. this.settings.save()
  205. }
  206. }
  207.  
  208. $(document).on('bootstrap.wme', () => {
  209. new APH(NAME, scriptSettings)
  210.  
  211. // Register handler for changes
  212. registerEventListeners()
  213. })
  214.  
  215. function createPoint (isResidential = false) {
  216. console.groupCollapsed(
  217. '%c' + NAME + ': 📍%c try to create ' + (isResidential ? 'residential ' : '') + 'point',
  218. 'color: #0DAD8D; font-weight: bold',
  219. 'color: dimgray; font-weight: normal'
  220. )
  221.  
  222. if ((!validateForPoint() && !isResidential)
  223. || (!validateForResidential() && isResidential)) {
  224. console.log('Invalid point')
  225. console.groupEnd()
  226. return
  227. }
  228.  
  229. let WazeFeatureVectorLandmark = require('Waze/Feature/Vector/Landmark')
  230. let WazeActionAddLandmark = require('Waze/Action/AddLandmark')
  231. let WazeActionUpdateObject = require('Waze/Action/UpdateObject')
  232. let WazeActionUpdateFeatureAddress = require('Waze/Action/UpdateFeatureAddress')
  233.  
  234. let { lat, lon } = getPointCoordinates()
  235. let address = getSelectedLandmarkAddress()
  236. let lockRank = getPointLockRank()
  237.  
  238. let pointGeometry = new OpenLayers.Geometry.Point(lon, lat)
  239.  
  240. let NewPoint = new WazeFeatureVectorLandmark({
  241. geoJSONGeometry: W.userscripts.toGeoJSONGeometry(pointGeometry)
  242. })
  243. NewPoint.attributes.categories.push('OTHER')
  244. NewPoint.attributes.lockRank = lockRank
  245. NewPoint.attributes.residential = isResidential
  246.  
  247. if (scriptSettings.get('addNavigationPoint')) {
  248. let newEntryPoint, parentEntryPoint = WME.getSelectedVenue().getAttributes().entryExitPoints[0]
  249. if (scriptSettings.get('inheritNavigationPoint') && parentEntryPoint !== undefined) {
  250. newEntryPoint = new entryPoint().with({primary: true, point: parentEntryPoint.getPoint()})
  251. } else {
  252. newEntryPoint = new entryPoint({primary: true, point: W.userscripts.toGeoJSONGeometry(pointGeometry.clone())})
  253. }
  254. NewPoint.attributes.entryExitPoints.push(newEntryPoint)
  255. }
  256.  
  257. if (!!address.attributes.houseNumber) {
  258. NewPoint.attributes.name = address.attributes.houseNumber
  259. NewPoint.attributes.houseNumber = address.attributes.houseNumber
  260. }
  261.  
  262. let newAddressAttributes = {
  263. streetName: address.getStreetName(),
  264. emptyStreet: false,
  265. stateID: address.getState().getID(),
  266. countryID: address.getCountry().getID(),
  267. }
  268.  
  269. if (address.getCity().getID() === 55344
  270. || address.getCityName() === 'поза НП') {
  271. newAddressAttributes.cityName = ''
  272. newAddressAttributes.emptyCity = true
  273. } else {
  274. newAddressAttributes.cityName = address.getCityName()
  275. newAddressAttributes.emptyCity = false
  276. }
  277.  
  278. if (scriptSettings.get('noDuplicates') && hasDuplicate(NewPoint, newAddressAttributes, isResidential)) {
  279. console.log('This point already exists.')
  280. console.groupEnd()
  281. return
  282. }
  283.  
  284. W.selectionManager.unselectAll()
  285. let addedLandmark = new WazeActionAddLandmark(NewPoint)
  286. W.model.actionManager.add(addedLandmark)
  287. W.model.actionManager.add(new WazeActionUpdateFeatureAddress(NewPoint, newAddressAttributes))
  288. if (!!address.attributes.houseNumber) {
  289. W.model.actionManager.add(new WazeActionUpdateObject(NewPoint, { houseNumber: address.attributes.houseNumber }))
  290. }
  291. W.selectionManager.setSelectedModels([addedLandmark.venue])
  292. console.log('The point was created.')
  293. console.groupEnd()
  294. }
  295.  
  296. function createResidential () {
  297. createPoint(true)
  298. }
  299.  
  300. function validateForPoint () {
  301. if (!WME.getSelectedVenue()) return false
  302. let selectedPoiHN = getSelectedLandmarkAddress().attributes.houseNumber
  303. return /\d+/.test(selectedPoiHN)
  304. }
  305.  
  306. function validateForResidential () {
  307. if (!WME.getSelectedVenue()) return false
  308. let selectedPoiHN = getSelectedLandmarkAddress().attributes.houseNumber
  309. return /^\d+[А-ЯЇІЄ]{0,3}$/i.test(selectedPoiHN)
  310. }
  311.  
  312. function getSelectedLandmarkAddress () {
  313. return WME.getSelectedVenue().getAddress(W.model)
  314. }
  315.  
  316. function getPointLockRank () {
  317. let selectedLandmark = WME.getSelectedVenue()
  318. let userRank = W.loginManager.user.attributes.rank
  319. let parentFeatureLockRank = selectedLandmark.getLockRank()
  320.  
  321. if (userRank >= parentFeatureLockRank) {
  322. return parentFeatureLockRank
  323. } else if (userRank >= 1) {
  324. return 1
  325. } else {
  326. return 0
  327. }
  328. }
  329.  
  330. function getPointCoordinates () {
  331. let selectedLandmark = WME.getSelectedVenue()
  332. let selectedLandmarkGeometry = selectedLandmark.getOLGeometry()
  333.  
  334. let coords
  335. if (/polygon/i.test(selectedLandmarkGeometry.id)) {
  336. let polygonCenteroid = selectedLandmarkGeometry.components[0].getCentroid()
  337. let geometryComponents = selectedLandmarkGeometry.components[0].components
  338. let flatComponentsCoords = []
  339. geometryComponents.forEach(c => flatComponentsCoords.push(c.x, c.y))
  340. let interiorPoint = getInteriorPointOfArray(
  341. flatComponentsCoords,
  342. 2, [polygonCenteroid.x, polygonCenteroid.y]
  343. )
  344. coords = {
  345. lon: interiorPoint[0],
  346. lat: interiorPoint[1]
  347. }
  348. } else {
  349. coords = {
  350. lon: selectedLandmarkGeometry.x,
  351. lat: selectedLandmarkGeometry.y
  352. }
  353. }
  354.  
  355. coords.lon += 4 // shift by X
  356. coords.lat += 5 // shift by Y
  357. return coords
  358. }
  359.  
  360. function hasDuplicate (poi, addr, isResidential) {
  361. const venues = W.model.venues.getObjectArray()
  362.  
  363. for (let key in venues) {
  364. if (!venues.hasOwnProperty(key)) continue
  365. const currentVenue = venues[key]
  366. const currentAddress = currentVenue.getAddress(W.model)
  367.  
  368. let equalNames = true // or empty for residential
  369. if (!isResidential && !!currentVenue.attributes.name && !!poi.attributes.name) {
  370. if (currentVenue.attributes.name !== poi.attributes.name) {
  371. equalNames = false
  372. }
  373. }
  374. if (
  375. equalNames
  376. && poi.attributes.houseNumber === currentVenue.attributes.houseNumber
  377. && poi.attributes.residential === currentVenue.attributes.residential
  378. && addr.streetName === currentAddress.getStreetName()
  379. && addr.cityName === currentAddress.getCityName()
  380. && addr.countryID === currentAddress.getCountry().getID()
  381. ) {
  382. return true
  383. }
  384. }
  385. return false
  386. }
  387.  
  388. function registerEventListeners () {
  389. let WazeActionUpdateObject = require('Waze/Action/UpdateObject')
  390.  
  391. W.model.actionManager.events.register('afteraction', null, action => {
  392. // Задаем номер дома в название, если нужно. Пока не нашел более лаконичного способа определить что
  393. // произошло именно изменение адреса. Можно тестить регуляркой поле _description, но будут проблемы с
  394. // нюансами содержания этого поля на разных языках
  395. if (scriptSettings.get('autoSetHNToName')) {
  396. try {
  397. let subAction = action.action.subActions[0]
  398. let houseNumber = subAction.attributes.houseNumber
  399. let feature = subAction.feature
  400. if (feature.attributes.categories.includes('OTHER') && feature.attributes.name === '') {
  401. W.model.actionManager.add(new WazeActionUpdateObject(feature, { name: houseNumber }))
  402. }
  403.  
  404. $('button.address-point-helper-A').prop('disabled', !validateForPoint())
  405. $('button.address-point-helper-B').prop('disabled', !validateForResidential())
  406.  
  407. } catch (e) { /* Do nothing */ }
  408. }
  409. })
  410. }
  411.  
  412. /**
  413. * @link https://github.com/openlayers/openlayers
  414. */
  415. function getInteriorPointOfArray (flatCoordinates, stride, flatCenters) {
  416. let offset = 0
  417. let flatCentersOffset = 0
  418. let ends = [flatCoordinates.length]
  419. let i, ii, x, x1, x2, y1, y2
  420. const y = flatCenters[flatCentersOffset + 1]
  421. const intersections = []
  422. // Calculate intersections with the horizontal line
  423. for (let r = 0, rr = ends.length; r < rr; ++r) {
  424. const end = ends[r]
  425. x1 = flatCoordinates[end - stride]
  426. y1 = flatCoordinates[end - stride + 1]
  427. for (i = offset; i < end; i += stride) {
  428. x2 = flatCoordinates[i]
  429. y2 = flatCoordinates[i + 1]
  430. if ((y <= y1 && y2 <= y) || (y1 <= y && y <= y2)) {
  431. x = (y - y1) / (y2 - y1) * (x2 - x1) + x1
  432. intersections.push(x)
  433. }
  434. x1 = x2
  435. y1 = y2
  436. }
  437. }
  438. // Find the longest segment of the horizontal line that has its center point
  439. // inside the linear ring.
  440. let pointX = NaN
  441. let maxSegmentLength = -Infinity
  442. intersections.sort(numberSafeCompareFunction)
  443. x1 = intersections[0]
  444. for (i = 1, ii = intersections.length; i < ii; ++i) {
  445. x2 = intersections[i]
  446. const segmentLength = Math.abs(x2 - x1)
  447. if (segmentLength > maxSegmentLength) {
  448. x = (x1 + x2) / 2
  449. if (linearRingsContainsXY(flatCoordinates, offset, ends, stride, x, y)) {
  450. pointX = x
  451. maxSegmentLength = segmentLength
  452. }
  453. }
  454. x1 = x2
  455. }
  456. if (isNaN(pointX)) {
  457. // There is no horizontal line that has its center point inside the linear
  458. // ring. Use the center of the the linear ring's extent.
  459. pointX = flatCenters[flatCentersOffset]
  460. }
  461.  
  462. return [pointX, y, maxSegmentLength]
  463. }
  464.  
  465. function numberSafeCompareFunction (a, b) {
  466. return a > b ? 1 : a < b ? -1 : 0
  467. }
  468.  
  469. function linearRingContainsXY (flatCoordinates, offset, end, stride, x, y) {
  470. // http://geomalgorithms.com/a03-_inclusion.html
  471. // Copyright 2000 softSurfer, 2012 Dan Sunday
  472. // This code may be freely used and modified for any purpose
  473. // providing that this copyright notice is included with it.
  474. // SoftSurfer makes no warranty for this code, and cannot be held
  475. // liable for any real or imagined damage resulting from its use.
  476. // Users of this code must verify correctness for their application.
  477. let wn = 0
  478. let x1 = flatCoordinates[end - stride]
  479. let y1 = flatCoordinates[end - stride + 1]
  480. for (; offset < end; offset += stride) {
  481. const x2 = flatCoordinates[offset]
  482. const y2 = flatCoordinates[offset + 1]
  483. if (y1 <= y) {
  484. if (y2 > y && ((x2 - x1) * (y - y1)) - ((x - x1) * (y2 - y1)) > 0) {
  485. wn++
  486. }
  487. } else if (y2 <= y && ((x2 - x1) * (y - y1)) - ((x - x1) * (y2 - y1)) < 0) {
  488. wn--
  489. }
  490. x1 = x2
  491. y1 = y2
  492. }
  493. return wn !== 0
  494. }
  495.  
  496. function linearRingsContainsXY (flatCoordinates, offset, ends, stride, x, y) {
  497. if (ends.length === 0) {
  498. return false
  499. }
  500. if (!linearRingContainsXY(flatCoordinates, offset, ends[0], stride, x, y)) {
  501. return false
  502. }
  503. for (let i = 1, ii = ends.length; i < ii; ++i) {
  504. if (linearRingContainsXY(flatCoordinates, ends[i - 1], ends[i], stride, x, y)) {
  505. return false
  506. }
  507. }
  508. return true
  509. }
  510. })()