Greasy Fork is available in English.

WME E40 Geometry

A script that allows aligning, scaling, and copying POI geometry

  1. // ==UserScript==
  2. // @name WME E40 Geometry
  3. // @name:uk WME 🇺🇦 E40 Geometry
  4. // @version 0.7.4
  5. // @description A script that allows aligning, scaling, and copying POI geometry
  6. // @description:uk За допомогою цього скрипта ви можете легко змінювати площу та вирівнювати POI
  7. // @license MIT License
  8. // @author Anton Shevchuk
  9. // @namespace https://greatest.deepsurf.us/users/227648-anton-shevchuk
  10. // @supportURL https://github.com/AntonShevchuk/wme-e40/issues
  11. // @match https://*.waze.com/editor*
  12. // @match https://*.waze.com/*/editor*
  13. // @exclude https://*.waze.com/user/editor*
  14. // @icon data:image/png;base64,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
  15. // @grant none
  16. // @require https://update.greatest.deepsurf.us/scripts/450160/1218867/WME-Bootstrap.js
  17. // @require https://update.greatest.deepsurf.us/scripts/452563/1218878/WME.js
  18. // @require https://update.greatest.deepsurf.us/scripts/450221/1137043/WME-Base.js
  19. // @require https://update.greatest.deepsurf.us/scripts/450320/1555446/WME-UI.js
  20. // ==/UserScript==
  21.  
  22. /* jshint esversion: 8 */
  23. /* global require */
  24. /* global $, jQuery */
  25. /* global W */
  26. /* global I18n */
  27. /* global OpenLayers */
  28. /* global WME, WMEBase, WMEUI, WMEUIHelper, WMEUIShortcut */
  29. /* global Container, Settings, SimpleCache, Tools */
  30.  
  31. (function () {
  32. 'use strict'
  33.  
  34. // Script name, uses as unique index
  35. const NAME = 'E40'
  36.  
  37. // User level required for apply geometry for all entities in the view area
  38. const REQUIRED_LEVEL = 2
  39.  
  40. // Translations
  41. const TRANSLATION = {
  42. 'en': {
  43. title: 'POI Geometry',
  44. description: 'Change geometry in the current view area',
  45. warning: '⚠️ This option is available for editors with a rank higher than ' + REQUIRED_LEVEL,
  46. orthogonalize: 'Orthogonalize',
  47. simplify: 'Simplify',
  48. scale: 'Scale',
  49. copy: 'Copy',
  50. about: '<a href="https://greatest.deepsurf.us/uk/scripts/388271-wme-e40-geometry">WME E40 Geometry</a>',
  51. },
  52. 'uk': {
  53. title: 'Геометрія POI',
  54. description: 'Змінити геометрію об’єктів у поточному розташуванні',
  55. warning: '⚠️ Ця опція доступна лише для редакторів з рангом вищім ніж ' + REQUIRED_LEVEL,
  56. orthogonalize: 'Вирівняти',
  57. simplify: 'Спростити',
  58. scale: 'Масштабувати',
  59. copy: 'Копіювати',
  60. about: '<a href="https://greatest.deepsurf.us/uk/scripts/388271-wme-e40-geometry">WME E40 Geometry</a>',
  61. },
  62. 'ru': {
  63. title: 'Геометрия POI',
  64. description: 'Изменить геометрию объектов в текущем расположении',
  65. warning: '⚠️ Эта опция доступна для редакторов с рангов выше ' + REQUIRED_LEVEL,
  66. orthogonalize: 'Выровнять',
  67. simplify: 'Упростить',
  68. scale: 'Масштабировать',
  69. copy: 'Копировать',
  70. about: '<a href="https://greatest.deepsurf.us/uk/scripts/388271-wme-e40-geometry">WME E40 Geometry</a>',
  71. }
  72. }
  73.  
  74. const STYLE =
  75. 'button.waze-btn.e40 { margin: 0 4px 4px 0; padding: 2px; width: 45px; border: 1px solid #ddd; } ' +
  76. 'p.e40-info { border-top: 1px solid #ccc; color: #777; font-size: x-small; margin-top: 15px; padding-top: 10px; text-align: center; }' +
  77. 'p.e40-warning { color: #f77 }'
  78.  
  79. WMEUI.addTranslation(NAME, TRANSLATION)
  80. WMEUI.addStyle(STYLE)
  81.  
  82. // Set shortcuts title
  83. WMEUIShortcut.setGroupTitle(NAME, I18n.t(NAME).title)
  84.  
  85. const panelButtons = {
  86. A: {
  87. title: '🔲',
  88. description: I18n.t(NAME).orthogonalize,
  89. shortcut: 'S+49',
  90. callback: () => orthogonalize()
  91. },
  92. B: {
  93. title: '〽️',
  94. description: I18n.t(NAME).simplify,
  95. shortcut: 'S+50',
  96. callback: () => simplify()
  97. },
  98. C: {
  99. title: '500m²',
  100. description: I18n.t(NAME).scale + ' 500m²',
  101. shortcut: 'S+51',
  102. callback: () => scaleSelected(500)
  103. },
  104. D: {
  105. title: '650m²',
  106. description: I18n.t(NAME).scale + ' 650m²',
  107. shortcut: 'S+52',
  108. callback: () => scaleSelected(650)
  109. },
  110. E: {
  111. title: '650+',
  112. description: I18n.t(NAME).scale + ' 650+',
  113. shortcut: 'S+53',
  114. callback: () => scaleSelected(650, true)
  115. },
  116. F: {
  117. title: '<i class="fa fa-clone" aria-hidden="true"></i>',
  118. description: I18n.t(NAME).copy,
  119. shortcut: 'S+54',
  120. callback: () => copyPlaces()
  121. }
  122. }
  123.  
  124. const tabButtons = {
  125. A: {
  126. title: '🔲',
  127. description: I18n.t(NAME).orthogonalize,
  128. shortcut: null,
  129. callback: () => orthogonalizeAll()
  130. },
  131. B: {
  132. title: '〽️',
  133. description: I18n.t(NAME).simplify,
  134. shortcut: null,
  135. callback: () => simplifyAll()
  136. },
  137. C: {
  138. title: '500+',
  139. description: I18n.t(NAME).scale + ' 500m²+',
  140. shortcut: null,
  141. callback: () => scaleAll(500, true)
  142. }
  143. }
  144.  
  145. let WazeActionUpdateFeatureGeometry
  146. let WazeActionUpdateFeatureAddress
  147. let WazeFeatureVectorLandmark
  148. let WazeActionAddLandmark
  149.  
  150. class E40 extends WMEBase {
  151. constructor (name) {
  152. super(name)
  153.  
  154. this.helper = new WMEUIHelper(name)
  155.  
  156. this.panel = this.helper.createPanel(I18n.t(name).title)
  157. this.panel.addButtons(panelButtons)
  158.  
  159. let tab = this.helper.createTab(
  160. I18n.t(name).title,
  161. {
  162. image: GM_info.script.icon
  163. }
  164. )
  165. tab.addText('description', I18n.t(name).description)
  166. if (W.loginManager.user.getRank() > REQUIRED_LEVEL) {
  167. tab.addButtons(tabButtons)
  168. } else {
  169. tab.addText('warning', I18n.t(name).warning)
  170. }
  171. tab.addText(
  172. 'info',
  173. '<a href="' + GM_info.scriptUpdateURL + '">' + GM_info.script.name + '</a> ' + GM_info.script.version
  174. )
  175. tab.inject()
  176. }
  177.  
  178. /**
  179. * Handler for `place.wme` event
  180. * @param {jQuery.Event} event
  181. * @param {HTMLElement} element
  182. * @param {W.model} model
  183. */
  184. onPlace (event, element, model) {
  185. if (!model.isGeometryEditable()) {
  186. return
  187. }
  188. this.createPanel(event, element)
  189. }
  190.  
  191. /**
  192. * Handler for `venues.wme` event
  193. * @param {jQuery.Event} event
  194. * @param {HTMLElement} element
  195. * @param {Array} models
  196. * @return {Null}
  197. */
  198. onVenues (event, element, models) {
  199. models = models.filter(el => !el.isPoint() && el.isGeometryEditable())
  200. if (models.length > 0) {
  201. this.createPanel(event, element)
  202. }
  203. }
  204.  
  205. /**
  206. * Create panel with buttons
  207. * @param event
  208. * @param element
  209. */
  210. createPanel (event, element) {
  211. if (element.querySelector('div.form-group.e40')) {
  212. return
  213. }
  214.  
  215. element.prepend(this.panel.html())
  216. this.updateLabel()
  217. }
  218.  
  219. /**
  220. * Updated label
  221. */
  222. updateLabel () {
  223. let places = getSelectedPlaces()
  224. if (places.length === 0) {
  225. return
  226. }
  227. let info = []
  228. for (let i = 0; i < places.length; i++) {
  229. let selected = places[i]
  230. info.push(Math.round(selected.getOLGeometry().getGeodesicArea(W.map.getProjectionObject())) + 'm²')
  231. }
  232. let label = I18n.t(NAME).title
  233. if (info.length) {
  234. label += ' (' + info.join(', ') + ')'
  235. }
  236.  
  237. let elm = document.querySelector('div.form-group.e40 label')
  238. if (elm) elm.innerText = label
  239. }
  240. }
  241.  
  242. $(document).on('bootstrap.wme', () => {
  243. // Require Waze components
  244. WazeActionUpdateFeatureGeometry = require('Waze/Action/UpdateFeatureGeometry')
  245. WazeActionUpdateFeatureAddress = require('Waze/Action/UpdateFeatureAddress')
  246. WazeFeatureVectorLandmark = require('Waze/Feature/Vector/Landmark')
  247. WazeActionAddLandmark = require('Waze/Action/AddLandmark')
  248.  
  249. let E40Instance = new E40(NAME)
  250.  
  251. W.model.actionManager.events.register('afterundoaction', null, E40Instance.updateLabel)
  252. W.model.actionManager.events.register('afterclearactions', null, E40Instance.updateLabel)
  253. W.model.actionManager.events.register('afteraction', null, E40Instance.updateLabel)
  254. })
  255.  
  256. /**
  257. * Get selected Area POI
  258. * @return {Array}
  259. */
  260. function getSelectedPlaces () {
  261. let selected
  262. selected = WME.getSelectedVenues()
  263. selected = selected.filter(el => !el.isPoint())
  264. return selected
  265. }
  266.  
  267. // Scale selected place(s) to X m²
  268. function scaleSelected (x, orMore = false) {
  269. scaleArray(getSelectedPlaces(), x, orMore)
  270. return false
  271. }
  272.  
  273. // Scale all places in the editor area to X m²
  274. function scaleAll (x = 650, orMore = true) {
  275. scaleArray(WME.getVenues().filter(el => !el.isPoint()), x, orMore)
  276. return false
  277. }
  278.  
  279. function scaleArray (elements, x, orMore = false) {
  280. console.groupCollapsed(
  281. '%c' + NAME + ': 📏 %c try to scale ' + (elements.length) + ' element(s) to ' + x + 'm²',
  282. 'color: #0DAD8D; font-weight: bold',
  283. 'color: dimgray; font-weight: normal'
  284. )
  285. let total = 0
  286. for (let i = 0; i < elements.length; i++) {
  287. let selected = elements[i]
  288. try {
  289. let oldOLGeometry = selected.getOLGeometry().clone()
  290. let newOLGeometry = selected.getOLGeometry().clone()
  291.  
  292. let scale = Math.sqrt((x + 5) / oldOLGeometry.getGeodesicArea(W.map.getProjectionObject()))
  293. if (scale < 1 && orMore) {
  294. continue
  295. }
  296. newOLGeometry.resize(scale, newOLGeometry.getCentroid())
  297.  
  298. let action = new WazeActionUpdateFeatureGeometry(
  299. selected,
  300. W.model.venues,
  301. W.userscripts.toGeoJSONGeometry(oldOLGeometry),
  302. W.userscripts.toGeoJSONGeometry(newOLGeometry)
  303. )
  304. W.model.actionManager.add(action)
  305. total++
  306. } catch (e) {
  307. console.log('skipped', e)
  308. }
  309. }
  310. console.log(total + ' element(s) was scaled')
  311. console.groupEnd()
  312. }
  313.  
  314. // Orthogonalize selected place(s)
  315. function orthogonalize () {
  316. orthogonalizeArray(getSelectedPlaces())
  317. return false
  318. }
  319.  
  320. // Orthogonalize all places in the editor area
  321. function orthogonalizeAll () {
  322. // skip parking, natural and outdoors
  323. // TODO: make options for filters
  324. orthogonalizeArray(WME.getVenues(['OUTDOORS', 'PARKING_LOT', 'NATURAL_FEATURES']).filter(el => !el.isPoint()))
  325. return false
  326. }
  327.  
  328. function orthogonalizeArray (elements) {
  329. console.groupCollapsed(
  330. '%c' + NAME + ': 🔲 %c try to orthogonalize ' + (elements.length) + ' element(s)',
  331. 'color: #0DAD8D; font-weight: bold',
  332. 'color: dimgray; font-weight: normal'
  333. )
  334. let total = 0
  335. // skip points
  336. for (let i = 0; i < elements.length; i++) {
  337. let selected = elements[i]
  338. try {
  339.  
  340. let oldGeometry = { ...selected.getGeometry() }
  341. let currentOLGeometry = selected.getOLGeometry()
  342.  
  343. let oldNodes = currentOLGeometry.clone().components[0].components
  344. let newNodes = orthogonalizeGeometry(selected.getOLGeometry().clone().components[0].components)
  345.  
  346.  
  347. if (!compare(oldNodes, newNodes)) {
  348. currentOLGeometry.components[0].components = [].concat(newNodes)
  349. currentOLGeometry.components[0].clearBounds()
  350.  
  351. selected.setOLGeometry(currentOLGeometry)
  352.  
  353. let action = new WazeActionUpdateFeatureGeometry(selected, W.model.venues, oldGeometry, selected.getGeometry())
  354. W.model.actionManager.add(action)
  355. total++
  356. }
  357. } catch (e) {
  358. console.log('skipped', e)
  359. }
  360. }
  361. console.log(total + ' element(s) was orthogonalized')
  362. console.groupEnd()
  363. }
  364.  
  365. /**
  366. * Clone OL Geometry and orthogonalize it
  367. * @param nodes
  368. * @param threshold
  369. * @return {*}
  370. */
  371. function orthogonalizeGeometry (nodes, threshold = 12) {
  372.  
  373. let nomthreshold = threshold, // degrees within right or straight to alter
  374. lowerThreshold = Math.cos((90 - nomthreshold) * Math.PI / 180),
  375. upperThreshold = Math.cos(nomthreshold * Math.PI / 180)
  376.  
  377. function Orthogonalize (nodes) {
  378. let points = nodes.slice(0, -1).map(function (n) {
  379. let p = n.clone().transform(new OpenLayers.Projection('EPSG:900913'), new OpenLayers.Projection('EPSG:4326'))
  380. p.y = lat2latp(p.y)
  381. return p
  382. }),
  383. corner = { i: 0, dotp: 1 },
  384. epsilon = 1e-4,
  385. i, j, score, motions
  386.  
  387. // Triangle
  388. if (nodes.length === 4) {
  389. for (i = 0; i < 1000; i++) {
  390. motions = points.map(calcMotion)
  391.  
  392. let tmp = addPoints(points[corner.i], motions[corner.i])
  393. points[corner.i].x = tmp.x
  394. points[corner.i].y = tmp.y
  395.  
  396. score = corner.dotp
  397. if (score < epsilon) {
  398. break
  399. }
  400. }
  401.  
  402. let n = points[corner.i]
  403. n.y = latp2lat(n.y)
  404. let pp = n.transform(new OpenLayers.Projection('EPSG:4326'), new OpenLayers.Projection('EPSG:900913'))
  405.  
  406. let id = nodes[corner.i].id
  407. for (i = 0; i < nodes.length; i++) {
  408. if (nodes[i].id !== id) {
  409. continue
  410. }
  411.  
  412. nodes[i].x = pp.x
  413. nodes[i].y = pp.y
  414. }
  415.  
  416. return nodes
  417. } else {
  418. let best,
  419. originalPoints = nodes.slice(0, -1).map(function (n) {
  420. let p = n.clone().transform(new OpenLayers.Projection('EPSG:900913'), new OpenLayers.Projection('EPSG:4326'))
  421. p.y = lat2latp(p.y)
  422. return p
  423. })
  424. score = Infinity
  425.  
  426. for (i = 0; i < 1000; i++) {
  427. motions = points.map(calcMotion)
  428. for (j = 0; j < motions.length; j++) {
  429. let tmp = addPoints(points[j], motions[j])
  430. points[j].x = tmp.x
  431. points[j].y = tmp.y
  432. }
  433. let newScore = squareness(points)
  434. if (newScore < score) {
  435. best = [].concat(points)
  436. score = newScore
  437. }
  438. if (score < epsilon) {
  439. break
  440. }
  441. }
  442.  
  443. points = best
  444.  
  445. for (i = 0; i < points.length; i++) {
  446. // only move the points that actually moved
  447. if (originalPoints[i].x !== points[i].x || originalPoints[i].y !== points[i].y) {
  448. let n = points[i]
  449. n.y = latp2lat(n.y)
  450. let pp = n.transform(new OpenLayers.Projection('EPSG:4326'), new OpenLayers.Projection('EPSG:900913'))
  451.  
  452. let id = nodes[i].id
  453. for (j = 0; j < nodes.length; j++) {
  454. if (nodes[j].id !== id) {
  455. continue
  456. }
  457.  
  458. nodes[j].x = pp.x
  459. nodes[j].y = pp.y
  460. }
  461. }
  462. }
  463.  
  464. // remove empty nodes on straight sections
  465. for (i = 0; i < points.length; i++) {
  466. let dotp = normalizedDotProduct(i, points)
  467. if (dotp < -1 + epsilon) {
  468. let id = nodes[i].id
  469. for (j = 0; j < nodes.length; j++) {
  470. if (nodes[j].id !== id) {
  471. continue
  472. }
  473.  
  474. nodes[j] = false
  475. }
  476. }
  477. }
  478.  
  479. return nodes.filter(item => item !== false)
  480. }
  481.  
  482. function calcMotion (b, i, array) {
  483. let a = array[(i - 1 + array.length) % array.length],
  484. c = array[(i + 1) % array.length],
  485. p = subtractPoints(a, b),
  486. q = subtractPoints(c, b),
  487. scale, dotp
  488.  
  489. scale = 2 * Math.min(euclideanDistance(p, { x: 0, y: 0 }), euclideanDistance(q, { x: 0, y: 0 }))
  490. p = normalizePoint(p, 1.0)
  491. q = normalizePoint(q, 1.0)
  492.  
  493. dotp = filterDotProduct(p.x * q.x + p.y * q.y)
  494.  
  495. // nasty hack to deal with almost-straight segments (angle is closer to 180 than to 90/270).
  496. if (array.length > 3) {
  497. if (dotp < -0.707106781186547) {
  498. dotp += 1.0
  499. }
  500. } else if (dotp && Math.abs(dotp) < corner.dotp) {
  501. corner.i = i
  502. corner.dotp = Math.abs(dotp)
  503. }
  504.  
  505. return normalizePoint(addPoints(p, q), 0.1 * dotp * scale)
  506. }
  507. }
  508.  
  509. function lat2latp (lat) {
  510. return 180 / Math.PI * Math.log(Math.tan(Math.PI / 4 + lat * (Math.PI / 180) / 2))
  511. }
  512.  
  513. function latp2lat (a) {
  514. return 180 / Math.PI * (2 * Math.atan(Math.exp(a * Math.PI / 180)) - Math.PI / 2)
  515. }
  516.  
  517. function squareness (points) {
  518. return points.reduce(function (sum, val, i, array) {
  519. let dotp = normalizedDotProduct(i, array)
  520.  
  521. dotp = filterDotProduct(dotp)
  522. return sum + 2.0 * Math.min(Math.abs(dotp - 1.0), Math.min(Math.abs(dotp), Math.abs(dotp + 1)))
  523. }, 0)
  524. }
  525.  
  526. function normalizedDotProduct (i, points) {
  527. let a = points[(i - 1 + points.length) % points.length],
  528. b = points[i],
  529. c = points[(i + 1) % points.length],
  530. p = subtractPoints(a, b),
  531. q = subtractPoints(c, b)
  532.  
  533. p = normalizePoint(p, 1.0)
  534. q = normalizePoint(q, 1.0)
  535.  
  536. return p.x * q.x + p.y * q.y
  537. }
  538.  
  539. function subtractPoints (a, b) {
  540. return { x: a.x - b.x, y: a.y - b.y }
  541. }
  542.  
  543. function addPoints (a, b) {
  544. return { x: a.x + b.x, y: a.y + b.y }
  545. }
  546.  
  547. function euclideanDistance (a, b) {
  548. let x = a.x - b.x, y = a.y - b.y
  549. return Math.sqrt((x * x) + (y * y))
  550. }
  551.  
  552. function normalizePoint (point, scale) {
  553. let vector = { x: 0, y: 0 }
  554. let length = Math.sqrt(point.x * point.x + point.y * point.y)
  555. if (length !== 0) {
  556. vector.x = point.x / length
  557. vector.y = point.y / length
  558. }
  559.  
  560. vector.x *= scale
  561. vector.y *= scale
  562.  
  563. return vector
  564. }
  565.  
  566. function filterDotProduct (dotp) {
  567. if (lowerThreshold > Math.abs(dotp) || Math.abs(dotp) > upperThreshold) {
  568. return dotp
  569. }
  570.  
  571. return 0
  572. }
  573.  
  574. return Orthogonalize(nodes)
  575. }
  576.  
  577. // Simplify selected place(s)
  578. function simplify (factor = 8) {
  579. simplifyArray(getSelectedPlaces(), factor)
  580. return false
  581. }
  582.  
  583. // Simplify all places in the editor area
  584. function simplifyAll () {
  585. // skip parking, natural and outdoors
  586. // TODO: make options for filters
  587. simplifyArray(WME.getVenues(['OUTDOORS', 'PARKING_LOT', 'NATURAL_FEATURES']).filter(el => !el.isPoint()))
  588. return false
  589. }
  590.  
  591. function simplifyArray (elements, factor = 8) {
  592. console.groupCollapsed(
  593. '%c' + NAME + ': 〽️ %c try to simplify ' + (elements.length) + ' element(s)',
  594. 'color: #0DAD8D; font-weight: bold',
  595. 'color: dimgray; font-weight: normal'
  596. )
  597. let total = 0
  598. for (let i = 0; i < elements.length; i++) {
  599. let selected = elements[i]
  600. try {
  601. let oldOLGeometry = selected.getOLGeometry().clone()
  602. let ls = new OpenLayers.Geometry.LineString(oldOLGeometry.components[0].components)
  603. ls = ls.simplify(factor)
  604. let newOLGeometry = new OpenLayers.Geometry.Polygon(new OpenLayers.Geometry.LinearRing(ls.components))
  605.  
  606. if (newOLGeometry.components[0].components.length < oldOLGeometry.components[0].components.length) {
  607. W.model.actionManager.add(
  608. new WazeActionUpdateFeatureGeometry(
  609. selected,
  610. W.model.venues,
  611. W.userscripts.toGeoJSONGeometry(oldOLGeometry),
  612. W.userscripts.toGeoJSONGeometry(newOLGeometry)
  613. )
  614. )
  615. total++
  616. }
  617. } catch (e) {
  618. console.log('skipped', e)
  619. }
  620. }
  621. console.log(total + ' element(s) was simplified')
  622. console.groupEnd()
  623. }
  624.  
  625. /**
  626. * Compare two polygons point-by-point
  627. *
  628. * @return boolean
  629. */
  630. function compare (geo1, geo2) {
  631. if (geo1.length !== geo2.length) {
  632. return false
  633. }
  634. for (let i = 0; i < geo1.length; i++) {
  635. if (Math.abs(geo1[i].x - geo2[i].x) > .1
  636. || Math.abs(geo1[i].y - geo2[i].y) > .1) {
  637. return false
  638. }
  639. }
  640. return true
  641. }
  642.  
  643. /**
  644. * Copy selected places
  645. * Last of them will be chosen
  646. */
  647. function copyPlaces () {
  648. let venues = getSelectedPlaces()
  649.  
  650. for (let i = 0; i < venues.length; i++) {
  651. copyPlace(venues[i])
  652. }
  653. }
  654.  
  655. /**
  656. * Create copy for place
  657. * @param oldPlace
  658. */
  659. function copyPlace (oldPlace) {
  660. console.log(
  661. '%c' + NAME + ': %c created a copy of the POI ' + oldPlace.attributes.name,
  662. 'color: #0DAD8D; font-weight: bold',
  663. 'color: dimgray; font-weight: normal'
  664. )
  665.  
  666. // copy all attributes of the old place
  667. // maybe we should except something in the feature
  668. let newPlace = new WazeFeatureVectorLandmark({ ...oldPlace.attributes})
  669.  
  670. newPlace.setAttribute('name', oldPlace.getAttribute('name') + ' (copy)')
  671.  
  672. let geometry = { ... oldPlace.getGeometry()}
  673.  
  674. // little move for new POI, uses geoJSON
  675. for (let i = 0; i < geometry.coordinates[0].length; i++) {
  676. geometry.coordinates[0][i][0] += 0.0001
  677. geometry.coordinates[0][i][1] += 0.00005
  678. }
  679.  
  680. newPlace.setGeometry(geometry)
  681.  
  682. // add new POI
  683. W.model.actionManager.add(new WazeActionAddLandmark(newPlace))
  684.  
  685. // update address of new POI
  686. // set the same Country/State/Street and skip the house number
  687. let address = {
  688. countryID: oldPlace.getAddress().getCountry().getID(),
  689. stateID: oldPlace.getAddress().getState().getID(),
  690. cityName: oldPlace.getAddress().getCityName(),
  691. streetName: oldPlace.getAddress().getStreetName()
  692. }
  693. W.model.actionManager.add(new WazeActionUpdateFeatureAddress(newPlace, address))
  694.  
  695. W.selectionManager.setSelectedModels(newPlace)
  696. }
  697.  
  698. })()