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.

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