Greasy Fork is available in English.

WME Address Point Helper

Creates point with same address

Fra og med 14.12.2023. Se den nyeste version.

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