WME E40 Geometry

Setup POI geometry properties in one click

当前为 2022-12-14 提交的版本,查看 最新版本

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