You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

598 lines
16 KiB

6 years ago
6 years ago
7 years ago
7 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
  1. /* global showDetails, openstreetbrowserPrefix */
  2. /* eslint camelcase: 0 */
  3. var OpenStreetBrowserLoader = require('./OpenStreetBrowserLoader')
  4. var OverpassLayer = require('overpass-layer')
  5. var OverpassLayerList = require('overpass-layer').List
  6. var queryString = require('query-string')
  7. var CategoryBase = require('./CategoryBase')
  8. var state = require('./state')
  9. var tabs = require('modulekit-tabs')
  10. var markers = require('./markers')
  11. var maki = require('./maki')
  12. var qs = require('sheet-router/qs')
  13. const showMore = require('./showMore')
  14. var defaultValues = {
  15. feature: {
  16. title: "{{ localizedTag(tags, 'name') |default(localizedTag(tags, 'operator')) | default(localizedTag(tags, 'ref')) | default(trans('unnamed')) }}",
  17. markerSign: '',
  18. 'style:selected': {
  19. color: '#3f3f3f',
  20. width: 3,
  21. opacity: 1,
  22. radius: 12,
  23. pane: 'selected'
  24. },
  25. markerSymbol: '{{ markerPointer({})|raw }}',
  26. listMarkerSymbol: '{{ markerCircle({})|raw }}',
  27. preferredZoom: 16
  28. },
  29. queryOptions: {
  30. }
  31. }
  32. CategoryOverpass.prototype = Object.create(CategoryBase.prototype)
  33. CategoryOverpass.prototype.constructor = CategoryOverpass
  34. function CategoryOverpass (options, data, repository) {
  35. var p
  36. CategoryBase.call(this, options, data, repository)
  37. data.id = this.id
  38. // set undefined data properties from defaultValues
  39. for (var k1 in defaultValues) {
  40. if (!(k1 in data)) {
  41. data[k1] = JSON.parse(JSON.stringify(defaultValues[k1]))
  42. } else if (typeof defaultValues[k1] === 'object') {
  43. for (var k2 in defaultValues[k1]) {
  44. if (!(k2 in data[k1])) {
  45. data[k1][k2] = JSON.parse(JSON.stringify(defaultValues[k1][k2]))
  46. } else if (typeof defaultValues[k1][k2] === 'object') {
  47. for (var k3 in defaultValues[k1][k2]) {
  48. if (!(k3 in data[k1][k2])) {
  49. data[k1][k2][k3] = JSON.parse(JSON.stringify(defaultValues[k1][k2][k3]))
  50. }
  51. }
  52. }
  53. }
  54. }
  55. }
  56. // get minZoom
  57. if ('minZoom' in data) {
  58. // has minZoom
  59. } else if (typeof data.query === 'object') {
  60. data.minZoom = Object.keys(data.query)[0]
  61. } else {
  62. data.minZoom = 14
  63. }
  64. data.feature.appUrl = '#' + this.id + '/{{ id }}'
  65. data.styleNoBindPopup = [ 'selected' ]
  66. data.stylesNoAutoShow = [ 'selected' ]
  67. data.updateAssets = this.updateAssets.bind(this)
  68. this.layer = new OverpassLayer(data)
  69. this.layer.onLoadStart = function (ev) {
  70. this.dom.classList.add('loading')
  71. if (this.parentCategory) {
  72. this.parentCategory.notifyChildLoadStart(this)
  73. }
  74. }.bind(this)
  75. this.layer.onLoadEnd = function (ev) {
  76. this.dom.classList.remove('loading')
  77. if (this.parentCategory) {
  78. this.parentCategory.notifyChildLoadEnd(this)
  79. }
  80. if (ev.error) {
  81. alert('Error loading data from Overpass API: ' + ev.error)
  82. }
  83. }.bind(this)
  84. this.layer.on('update', function (object, ob) {
  85. if (!ob.popup || !ob.popup._contentNode || map._popup !== ob.popup) {
  86. return
  87. }
  88. this.updatePopupContent(ob, ob.popup)
  89. if (document.getElementById('content').className === 'details open') {
  90. showDetails(ob, this)
  91. }
  92. this.emit('update', object, ob)
  93. }.bind(this))
  94. this.layer.on('add', (ob, data) => this.emit('add', ob, data))
  95. this.layer.on('remove', (ob, data) => this.emit('remove', ob, data))
  96. this.layer.on('zoomChange', (ob, data) => this.emit('remove', ob, data))
  97. this.layer.on('twigData',
  98. (ob, data, result) => {
  99. result.user = global.options
  100. global.currentCategory = this
  101. }
  102. )
  103. call_hooks('category-overpass-init', this)
  104. var p = document.createElement('div')
  105. p.className = 'loadingIndicator'
  106. p.innerHTML = '<i class="fa fa-spinner fa-pulse fa-fw"></i><span class="sr-only">' + lang('loading') + '</span>'
  107. this.dom.appendChild(p)
  108. this.domStatus = document.createElement('div')
  109. this.domStatus.className = 'status'
  110. if (this.data.lists) {
  111. this.dom.insertBefore(this.domStatus, this.domHeader.nextSibling)
  112. } else {
  113. p = document.createElement('div')
  114. p.className = 'loadingIndicator2'
  115. p.innerHTML = '<div class="bounce1"></div><div class="bounce2"></div><div class="bounce3"></div>'
  116. this.dom.appendChild(p)
  117. this.dom.appendChild(this.domStatus)
  118. }
  119. register_hook('state-get', function (state) {
  120. if (this.isOpen) {
  121. if (state.categories) {
  122. state.categories += ','
  123. } else {
  124. state.categories = ''
  125. }
  126. let id = this.id
  127. let param = {}
  128. this.emit('stateGet', param)
  129. for (var k in param) {
  130. if (!param[k]) {
  131. delete param[k]
  132. }
  133. }
  134. if (param && Object.keys(param).length) {
  135. id += '[' + queryString.stringify(param) + ']'
  136. }
  137. state.categories += id
  138. }
  139. }.bind(this))
  140. register_hook('state-apply', function (state) {
  141. if (!('categories' in state)) {
  142. return
  143. }
  144. let list = state.categories.split(',')
  145. let found = list.filter(id => {
  146. let m = id.match(/^([0-9A-Z_-]+)(\[(.*)\])/i)
  147. if (m) {
  148. id = m[1]
  149. }
  150. return id === this.id
  151. }).length
  152. if (!found) {
  153. this.close()
  154. }
  155. // opening categories is handled by src/categories.js
  156. }.bind(this))
  157. }
  158. CategoryOverpass.prototype.setParam = function (param) {
  159. this.emit('setParam', param)
  160. this._applyParam(param)
  161. }
  162. CategoryOverpass.prototype._applyParam = function (param) {
  163. this.emit('applyParam', param)
  164. }
  165. CategoryOverpass.prototype.updateAssets = function (div) {
  166. var imgs = div.getElementsByTagName('img')
  167. for (var i = 0; i < imgs.length; i++) {
  168. let img = imgs[i]
  169. // TODO: 'src' is deprecated, use only data-src
  170. var src = img.getAttribute('src') || img.getAttribute('data-src')
  171. if (src === null) {
  172. } else if (src.match(/^(maki|temaki):.*/)) {
  173. let m = src.match(/^(maki|temaki):([a-z0-9-_]*)(?:\?(.*))?$/)
  174. if (m) {
  175. let span = document.createElement('span')
  176. img.parentNode.insertBefore(span, img)
  177. img.parentNode.removeChild(img)
  178. i--
  179. maki(m[1], m[2], m[3] ? qs(m[3]) : {}, function (err, result) {
  180. if (err === null) {
  181. span.innerHTML = result
  182. }
  183. })
  184. }
  185. } else if (src.match(/^(marker):.*/)) {
  186. let m = src.match(/^(marker):([a-z0-9-_]*)(?:\?(.*))?$/)
  187. if (m) {
  188. let span = document.createElement('span')
  189. img.parentNode.insertBefore(span, img)
  190. img.parentNode.removeChild(img)
  191. i--
  192. let param = m[3] ? qs(m[3]) : {}
  193. if (param.styles) {
  194. let newParam = { styles: param.styles }
  195. for (let k in param) {
  196. let m = k.match(/^(style|style:.*)?:([^:]*)$/)
  197. if (m) {
  198. if (!(m[1] in newParam)) {
  199. newParam[m[1]] = {}
  200. }
  201. newParam[m[1]][m[2]] = param[k]
  202. }
  203. }
  204. param = newParam
  205. }
  206. console.log(param)
  207. span.innerHTML = markers[m[2]](param)
  208. }
  209. } else if (!src.match(/^(https?:|data:|\.|\/)/)) {
  210. img.setAttribute('src', (typeof openstreetbrowserPrefix === 'undefined' ? './' : openstreetbrowserPrefix) +
  211. 'asset.php?repo=' + this.options.repositoryId + '&file=' + encodeURIComponent(img.getAttribute('data-src') || img.getAttribute('src')))
  212. }
  213. }
  214. }
  215. CategoryOverpass.prototype.load = function (callback) {
  216. OpenStreetBrowserLoader.getTemplate('popupBody', this.options, function (err, template) {
  217. if (err) {
  218. console.log("can't load popupBody.html")
  219. } else {
  220. this.popupBodyTemplate = template
  221. }
  222. callback(null)
  223. }.bind(this))
  224. }
  225. CategoryOverpass.prototype.setParentDom = function (parentDom) {
  226. CategoryBase.prototype.setParentDom.call(this, parentDom)
  227. }
  228. CategoryOverpass.prototype.setMap = function (map) {
  229. CategoryBase.prototype.setMap.call(this, map)
  230. this.map.on('zoomend', function () {
  231. this.updateStatus()
  232. this.updateInfo()
  233. }.bind(this))
  234. this.updateStatus()
  235. this.updateInfo()
  236. }
  237. CategoryOverpass.prototype.updateStatus = function () {
  238. this.domStatus.innerHTML = ''
  239. if (typeof this.data.query === 'object') {
  240. var highestZoom = Object.keys(this.data.query).reverse()[0]
  241. if (this.map.getZoom() < highestZoom) {
  242. this.domStatus.innerHTML = lang('zoom_in_more')
  243. }
  244. }
  245. if ('minZoom' in this.data && this.map.getZoom() < this.data.minZoom) {
  246. this.domStatus.innerHTML = lang('zoom_in_appear')
  247. }
  248. }
  249. CategoryOverpass.prototype._getMarker = function (origGetMarker, origList, ob) {
  250. if (ob.data[origList.options.prefix + 'MarkerSymbol'].trim() === 'line') {
  251. let div = document.createElement('div')
  252. div.className = 'marker'
  253. div.innerHTML = markers.line(ob.data)
  254. return div
  255. } else if (ob.data[origList.options.prefix + 'MarkerSymbol'].trim() === 'polygon') {
  256. let div = document.createElement('div')
  257. div.className = 'marker'
  258. div.innerHTML = markers.polygon(ob.data)
  259. return div
  260. }
  261. return origGetMarker.call(origList, ob)
  262. }
  263. CategoryOverpass.prototype.open = function () {
  264. if (this.isOpen) {
  265. return
  266. }
  267. CategoryBase.prototype.open.call(this)
  268. this.layer.addTo(this.map)
  269. if (!this.lists) {
  270. this.lists = []
  271. this.listsDom = []
  272. if (this.data.lists) {
  273. let wrapper = document.createElement('div')
  274. wrapper.className = 'categoryWrapper'
  275. this.domContent.appendChild(wrapper)
  276. for (let k in this.data.lists) {
  277. let listData = this.data.lists[k]
  278. let list = new OverpassLayerList(this.layer, listData)
  279. this.lists.push(list)
  280. let dom = document.createElement('div')
  281. dom.className = 'category category-list'
  282. this.listsDom.push(dom)
  283. wrapper.appendChild(dom)
  284. let domHeader = document.createElement('header')
  285. dom.appendChild(domHeader)
  286. let p = document.createElement('div')
  287. p.className = 'loadingIndicator'
  288. p.innerHTML = '<i class="fa fa-spinner fa-pulse fa-fw"></i><span class="sr-only">' + lang('loading') + '</span>'
  289. dom.appendChild(p)
  290. let name
  291. if (typeof listData.name === 'undefined') {
  292. name = k
  293. } else if (typeof listData.name === 'object') {
  294. name = lang(listData.name)
  295. } else {
  296. name = listData.name
  297. }
  298. let a = document.createElement('a')
  299. a.appendChild(document.createTextNode(name))
  300. a.href = '#'
  301. domHeader.appendChild(a)
  302. a.onclick = () => {
  303. dom.classList.toggle('open')
  304. return false
  305. }
  306. let domContent = document.createElement('div')
  307. domContent.className = 'content'
  308. dom.appendChild(domContent)
  309. list.addTo(domContent)
  310. showMore(this, domContent)
  311. p = document.createElement('div')
  312. p.className = 'loadingIndicator2'
  313. p.innerHTML = '<div class="bounce1"></div><div class="bounce2"></div><div class="bounce3"></div>'
  314. dom.appendChild(p)
  315. }
  316. } else {
  317. let list = new OverpassLayerList(this.layer, {})
  318. this.lists.push(list)
  319. list.addTo(this.domContent)
  320. this.listsDom.push(this.domContent)
  321. showMore(this, this.domContent)
  322. }
  323. this.lists.forEach(list => {
  324. let origGetMarker = list._getMarker
  325. list._getMarker = this._getMarker.bind(this, origGetMarker, list)
  326. })
  327. }
  328. this.listsDom.forEach(dom => dom.classList.add('open'))
  329. this.isOpen = true
  330. state.update()
  331. if ('info' in this.data) {
  332. if (!this.tabInfo) {
  333. this.tabInfo = new tabs.Tab({
  334. id: 'info'
  335. })
  336. this.tools.add(this.tabInfo)
  337. this.tabInfo.header.innerHTML = '<i class="fa fa-info-circle" aria-hidden="true"></i>'
  338. this.tabInfo.header.title = lang('category-info-tooltip')
  339. this.domInfo = this.tabInfo.content
  340. this.domInfo.classList.add('info')
  341. this.templateInfo = OverpassLayer.twig.twig({ data: this.data.info, autoescape: true })
  342. }
  343. this.updateInfo()
  344. }
  345. this.emit('open')
  346. }
  347. CategoryOverpass.prototype.updateInfo = function () {
  348. if (!this.tabInfo) {
  349. return
  350. }
  351. global.currentCategory = this
  352. var data = {
  353. layer_id: this.id,
  354. 'const': this.data.const
  355. }
  356. if (this.map) {
  357. data.map = {
  358. zoom: this.map.getZoom(),
  359. metersPerPixel: this.map.getMetersPerPixel()
  360. }
  361. }
  362. this.domInfo.innerHTML = this.templateInfo.render(data)
  363. this.updateAssets(this.domInfo)
  364. global.currentCategory = null
  365. }
  366. CategoryOverpass.prototype.recalc = function () {
  367. this.layer.recalc()
  368. }
  369. CategoryOverpass.prototype.close = function () {
  370. if (!this.isOpen) {
  371. return
  372. }
  373. CategoryBase.prototype.close.call(this)
  374. this.layer.remove()
  375. this.lists.forEach(list => list.remove())
  376. state.update()
  377. }
  378. CategoryOverpass.prototype.get = function (id, callback) {
  379. this.layer.get(id, callback)
  380. }
  381. CategoryOverpass.prototype.show = function (id, options, callback) {
  382. if (this.currentDetails) {
  383. this.currentDetails.hide()
  384. }
  385. let layerOptions = {
  386. styles: [ 'selected' ],
  387. flags: [ 'selected' ]
  388. }
  389. let idParts = id.split(/:/)
  390. switch (idParts.length) {
  391. case 2:
  392. id = idParts[1]
  393. layerOptions.sublayer_id = idParts[0]
  394. break
  395. case 1:
  396. break
  397. default:
  398. return callback(new Error('too many id parts! ' + id))
  399. }
  400. this.currentDetails = this.layer.show(id, layerOptions,
  401. function (err, ob, data) {
  402. if (!err) {
  403. if (options.showDetails && !options.hasLocation) {
  404. var preferredZoom = data.data.preferredZoom || 16
  405. var maxZoom = this.map.getZoom()
  406. maxZoom = maxZoom > preferredZoom ? maxZoom : preferredZoom
  407. this.map.flyToBounds(data.object.bounds.toLeaflet({ shiftWorld: this.layer.getShiftWorld() }), {
  408. maxZoom: maxZoom
  409. })
  410. }
  411. }
  412. callback(err, data)
  413. }.bind(this)
  414. )
  415. }
  416. CategoryOverpass.prototype.notifyPopupOpen = function (object, popup) {
  417. if (this.currentSelected) {
  418. this.currentSelected.hide()
  419. }
  420. let layerOptions = {
  421. styles: [ 'selected' ],
  422. flags: [ 'selected' ],
  423. sublayer_id: object.sublayer_id
  424. }
  425. this.updatePopupContent(object, popup)
  426. this.currentSelected = this.layer.show(object.id, layerOptions, function () {})
  427. }
  428. CategoryOverpass.prototype.notifyPopupClose = function (object, popup) {
  429. if (this.currentSelected) {
  430. this.currentSelected.hide()
  431. this.currentSelected = null
  432. }
  433. if (this.currentDetails) {
  434. this.currentDetails.hide()
  435. this.currentDetails = null
  436. }
  437. }
  438. CategoryOverpass.prototype.updatePopupContent = function (object, popup) {
  439. if (this.popupBodyTemplate) {
  440. var popupBody = popup._contentNode.getElementsByClassName('popupBody')
  441. if (!popupBody.length) {
  442. popupBody = document.createElement('div')
  443. popupBody.className = 'popupBody'
  444. popup._contentNode.appendChild(popupBody)
  445. }
  446. let html = this.popupBodyTemplate.render(object.twigData)
  447. if (popupBody.currentHTML !== html) {
  448. popupBody.innerHTML = html
  449. this.updateAssets(popup._contentNode)
  450. }
  451. popupBody.currentHTML = html
  452. }
  453. let id_with_sublayer = (object.sublayer_id === 'main' ? '' : object.sublayer_id + ':') + object.id
  454. var footer = popup._contentNode.getElementsByClassName('popup-footer')
  455. if (!footer.length) {
  456. footer = document.createElement('ul')
  457. popup._contentNode.appendChild(footer)
  458. footer.className = 'popup-footer'
  459. call_hooks_callback('show-popup', object, this, popup._contentNode,
  460. function (err) {
  461. if (err.length) {
  462. console.log('show-popup produced errors:', err)
  463. }
  464. }
  465. )
  466. }
  467. var footerContent = '<li><a class="showDetails" href="#' + this.id + '/' + id_with_sublayer + '/details">' + lang('show details') + '</a></li>'
  468. footerContent += '<li><a target="_blank" class="editLink" href="' + config.urlOpenStreetMap + '/edit?editor=id&' + object.object.type + '=' + object.object.osm_id + '">' + lang('edit') + '</a></li>'
  469. footer.innerHTML = footerContent
  470. }
  471. CategoryOverpass.prototype.renderTemplate = function (object, templateId, callback) {
  472. OpenStreetBrowserLoader.getTemplate(templateId, this.options, function (err, template) {
  473. if (err) {
  474. err = "can't load " + templateId + ': ' + err
  475. return callback(err, null)
  476. }
  477. var result = template.render(object.twigData)
  478. callback(null, result)
  479. })
  480. }
  481. CategoryOverpass.prototype.allMapFeatures = function (callback) {
  482. if (!this.isOpen) {
  483. return callback(null, [])
  484. }
  485. callback(null, Object.values(this.layer.mainlayer.visibleFeatures))
  486. }
  487. CategoryOverpass.defaultValues = defaultValues
  488. OpenStreetBrowserLoader.registerType('overpass', CategoryOverpass)
  489. module.exports = CategoryOverpass