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.

409 lines
10 KiB

  1. const tabs = require('modulekit-tabs')
  2. const yaml = require('js-yaml')
  3. const md5 = require('md5')
  4. const OverpassLayer = require('overpass-layer')
  5. const jsonMultilineStrings = require('json-multiline-strings')
  6. const Window = require('./Window')
  7. const OpenStreetBrowserLoader = require('./OpenStreetBrowserLoader')
  8. const editors = []
  9. class CustomCategoryRepository {
  10. constructor () {
  11. this.clearCache()
  12. }
  13. load (callback) {
  14. callback(null)
  15. }
  16. clearCache () {
  17. this.cache = {}
  18. }
  19. listCategories (options, callback) {
  20. fetch('customCategory.php?action=list')
  21. .then(res => res.json())
  22. .then(result => callback(null, result))
  23. }
  24. getCategory (id, options, callback) {
  25. if (id in this.cache) {
  26. return callback(null, yaml.load(this.cache[id]), this.cache[id])
  27. }
  28. fetch('customCategory.php?id=' + id)
  29. .then(res => res.text())
  30. .then(content => {
  31. let data
  32. this.cache[id] = content
  33. try {
  34. data = yaml.load(content)
  35. }
  36. catch (e) {
  37. return global.alert(e)
  38. }
  39. if (data && typeof data !== 'object') {
  40. callback(new Error('Data can not be parsed into an object'))
  41. }
  42. if (!data.name) {
  43. data.name = 'Custom ' + id.substr(0, 6)
  44. }
  45. callback(null, data, content)
  46. })
  47. }
  48. saveCategory (body, options, callback) {
  49. const id = md5(body)
  50. this.cache[id] = body
  51. fetch('customCategory.php?action=save', {
  52. method: 'POST',
  53. body
  54. })
  55. }
  56. getTemplate (id, options, callback) {
  57. callback(null, '')
  58. }
  59. }
  60. class CustomCategoryEditor {
  61. constructor (repository) {
  62. this.repository = repository
  63. editors.push(this)
  64. }
  65. load (id, callback) {
  66. this.repository.getCategory(id, {},
  67. (err, category, content) => {
  68. this.content = content
  69. callback(err, content)
  70. })
  71. }
  72. edit () {
  73. if (this.window) {
  74. this.window.focused = true
  75. return
  76. }
  77. this.window = new Window({
  78. title: 'Custom Category'
  79. })
  80. this.window.on('close', () => {
  81. this.window = null
  82. })
  83. this.textarea = document.createElement('textarea')
  84. this.textarea.spellcheck = false
  85. this.window.content.appendChild(this.textarea)
  86. if (this.content !== undefined) {
  87. this.textarea.value = this.content
  88. }
  89. const controls = document.createElement('div')
  90. controls.className = 'controls'
  91. this.window.content.appendChild(controls)
  92. const input = document.createElement('input')
  93. input.type = 'submit'
  94. input.value = lang('apply')
  95. controls.appendChild(input)
  96. const inputClose = document.createElement('input')
  97. inputClose.type = 'button'
  98. inputClose.value = lang('close')
  99. inputClose.onclick = () => this.window.close()
  100. controls.appendChild(inputClose)
  101. const tutorial = document.createElement('span')
  102. tutorial.className = 'tip-tutorial'
  103. let text = lang('tip-tutorial')
  104. text = text.replace('[', '<a target="_blank" href="https://github.com/plepe/OpenStreetBrowser/blob/master/doc/CategoryAsYAML.md">')
  105. text = text.replace(']', '</a>')
  106. tutorial.innerHTML = text
  107. controls.appendChild(tutorial)
  108. input.onclick = (e) => {
  109. const err = customCategoryTest(this.textarea.value)
  110. if (err) {
  111. return global.alert(err)
  112. }
  113. this.applyContent(this.textarea.value)
  114. e.preventDefault()
  115. }
  116. this.window.show()
  117. }
  118. applyContent (content) {
  119. this.content = content
  120. this.repository.saveCategory(this.content, {}, () => {})
  121. if (this.textarea) {
  122. this.textarea.value = content
  123. }
  124. const id = md5(content)
  125. this.id = id
  126. if (this.category) {
  127. this.category.remove()
  128. this.category = null
  129. }
  130. OpenStreetBrowserLoader.getCategory('custom/' + id, {}, (err, category) => {
  131. if (err) {
  132. return global.alert(err)
  133. }
  134. this.category = category
  135. this.category.setParentDom(document.getElementById('contentListAddCategories'))
  136. this.category.open()
  137. })
  138. }
  139. }
  140. function editCustomCategory (id, category) {
  141. let done = editors.filter(editor => {
  142. if (editor.id === id) {
  143. editor.edit()
  144. return true
  145. }
  146. })
  147. if (!done.length) {
  148. const editor = new CustomCategoryEditor(repository)
  149. editor.load(id, (err) => {
  150. if (err) { return global.alert(err) }
  151. editor.category = category
  152. editor.edit()
  153. })
  154. }
  155. }
  156. hooks.register('browser-more-categories', (browser, parameters) => {
  157. const content = browser.dom
  158. if (!Object.keys(parameters).length) {
  159. let block = document.createElement('div')
  160. block.setAttribute('weight', 0)
  161. content.appendChild(block)
  162. let header = document.createElement('h4')
  163. header.innerHTML = lang('customCategory:header')
  164. block.appendChild(header)
  165. let ul = document.createElement('ul')
  166. let li = document.createElement('li')
  167. let a = document.createElement('a')
  168. a.innerHTML = lang('customCategory:create')
  169. a.href = '#'
  170. a.onclick = (e) => {
  171. const editor = new CustomCategoryEditor(repository)
  172. editor.edit()
  173. browser.close()
  174. e.preventDefault()
  175. }
  176. li.appendChild(a)
  177. ul.appendChild(li)
  178. li = document.createElement('li')
  179. a = document.createElement('a')
  180. a.innerHTML = lang('customCategory:list')
  181. a.href = '#more-categories?custom=list'
  182. li.appendChild(a)
  183. ul.appendChild(li)
  184. block.appendChild(ul)
  185. browser.catchLinks()
  186. }
  187. else if (parameters.custom === 'list') {
  188. customCategoriesList(browser, parameters)
  189. }
  190. })
  191. function customCategoriesList (browser, options) {
  192. browser.dom.innerHTML = '<i class="fa fa-spinner fa-pulse fa-fw"></i> ' + lang('loading')
  193. repository.listCategories({},
  194. (err, result) => {
  195. browser.dom.innerHTML = ''
  196. const ul = document.createElement('ul')
  197. browser.dom.appendChild(ul)
  198. result.forEach(cat => {
  199. const li = document.createElement('li')
  200. const a = document.createElement('a')
  201. a.href = '#categories=custom/' + cat.id
  202. a.appendChild(document.createTextNode(cat.name))
  203. li.appendChild(a)
  204. const edit = document.createElement('a')
  205. edit.onclick = (e) => {
  206. editCustomCategory(cat.id)
  207. e.preventDefault()
  208. }
  209. edit.innerHTML = ' <i class="fa fa-pen"></i>'
  210. li.appendChild(edit)
  211. ul.appendChild(li)
  212. })
  213. browser.catchLinks()
  214. })
  215. }
  216. const repository = new CustomCategoryRepository()
  217. hooks.register('init', () => {
  218. OpenStreetBrowserLoader.registerRepository('custom', repository)
  219. })
  220. hooks.register('category-overpass-init', (category) => {
  221. const m = category.id.match(/^custom\/(.*)$/)
  222. if (m) {
  223. const id = m[1]
  224. if (category.tabEdit) {
  225. category.tools.remove(this.category.tabEdit)
  226. }
  227. category.tabEdit = new tabs.Tab({
  228. id: 'edit',
  229. weight: 9
  230. })
  231. category.tools.add(category.tabEdit)
  232. category.tabEdit.header.innerHTML = '<i class="fa fa-pen"></i>'
  233. category.tabEdit.header.title = lang('edit')
  234. category.tabEdit.on('select', () => {
  235. category.tabEdit.unselect()
  236. editCustomCategory(id, category)
  237. })
  238. if (!category.tabShare) {
  239. const url = location.origin + location.pathname + '#categories=custom/' + id
  240. category.tabShare = new tabs.Tab({
  241. id: 'share',
  242. weight: 10
  243. })
  244. category.tools.add(category.tabShare)
  245. category.shareLink = document.createElement('a')
  246. category.shareLink.href = url
  247. category.shareLink.innerHTML = '<i class="fa fa-share-alt"></i>'
  248. category.tabShare.header.appendChild(category.shareLink)
  249. category.tabShare.header.className = 'share-button'
  250. category.tabShare.on('select', () => {
  251. category.tabShare.unselect()
  252. navigator.clipboard.writeText(url)
  253. const notify = document.createElement('div')
  254. notify.className = 'notify'
  255. notify.innerHTML = lang('copied-clipboard')
  256. category.tabShare.header.appendChild(notify)
  257. global.setTimeout(() => category.tabShare.header.removeChild(notify), 2000)
  258. })
  259. }
  260. } else {
  261. if (category.tabClone) {
  262. category.tools.remove(this.category.tabClone)
  263. }
  264. category.tabClone = new tabs.Tab({
  265. id: 'clone',
  266. weight: 9
  267. })
  268. category.tools.add(category.tabClone)
  269. category.tabClone.header.innerHTML = '<i class="fa fa-clone"></i>'
  270. category.tabClone.header.title = lang('customCategory:clone')
  271. category.tabClone.on('select', () => {
  272. category.tabClone.unselect()
  273. const clone = new CustomCategoryEditor(repository)
  274. clone.edit()
  275. category.repository.file_get_contents(category.data.fileName, {},
  276. (err, content) => {
  277. if (category.data.format === 'json') {
  278. content = JSON.parse(content)
  279. content = jsonMultilineStrings.join(content, { exclude: [ [ 'const' ], [ 'filter' ] ] })
  280. content = yaml.dump(content, {
  281. lineWidth: 9999
  282. })
  283. }
  284. clone.applyContent(content)
  285. category.close()
  286. }
  287. )
  288. })
  289. }
  290. })
  291. function customCategoryTest (value) {
  292. if (!value) {
  293. return new Error('Empty category')
  294. }
  295. let data
  296. try {
  297. data = yaml.load(value)
  298. }
  299. catch (e) {
  300. return e
  301. }
  302. if (!data || typeof data !== 'object') {
  303. return new Error('Data can not be parsed into an object')
  304. }
  305. const fields = ['feature', 'memberFeature']
  306. for (let i1 = 0; i1 < fields.length; i1++) {
  307. const k1 = fields[i1]
  308. if (data[k1]) {
  309. for (k2 in data[k1]) {
  310. const err = customCategoryTestCompile(data[k1][k2])
  311. if (err) {
  312. return new Error('Compiling /' + k1 + '/' + k2 + ': ' + err.message)
  313. }
  314. if (k2 === 'style' || k2.match(/^style:/)) {
  315. for (const k3 in data[k1][k2]) {
  316. const err = customCategoryTestCompile(data[k1][k2][k3])
  317. if (err) {
  318. return new Error('Compiling /' + k1 + '/' + k2 + '/' + k3 + ': ' + err.message)
  319. }
  320. }
  321. }
  322. }
  323. }
  324. }
  325. }
  326. function customCategoryTestCompile (data) {
  327. if (typeof data !== 'string' || data.search('{') === -1) {
  328. return
  329. }
  330. try {
  331. OverpassLayer.twig.twig({ data })
  332. }
  333. catch (e) {
  334. return e
  335. }
  336. }