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.

403 lines
9.9 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 tutorial = document.createElement('span')
  97. tutorial.className = 'tip-tutorial'
  98. let text = lang('tip-tutorial')
  99. text = text.replace('[', '<a target="_blank" href="https://github.com/plepe/OpenStreetBrowser/blob/master/doc/CategoryAsYAML.md">')
  100. text = text.replace(']', '</a>')
  101. tutorial.innerHTML = text
  102. controls.appendChild(tutorial)
  103. input.onclick = (e) => {
  104. const err = customCategoryTest(this.textarea.value)
  105. if (err) {
  106. return global.alert(err)
  107. }
  108. this.applyContent(this.textarea.value)
  109. e.preventDefault()
  110. }
  111. this.window.show()
  112. }
  113. applyContent (content) {
  114. this.content = content
  115. this.repository.saveCategory(this.content, {}, () => {})
  116. if (this.textarea) {
  117. this.textarea.value = content
  118. }
  119. const id = md5(content)
  120. this.id = id
  121. if (this.category) {
  122. this.category.remove()
  123. this.category = null
  124. }
  125. OpenStreetBrowserLoader.getCategory('custom/' + id, {}, (err, category) => {
  126. if (err) {
  127. return global.alert(err)
  128. }
  129. this.category = category
  130. this.category.setParentDom(document.getElementById('contentListAddCategories'))
  131. this.category.open()
  132. })
  133. }
  134. }
  135. function editCustomCategory (id, category) {
  136. let done = editors.filter(editor => {
  137. if (editor.id === id) {
  138. editor.edit()
  139. return true
  140. }
  141. })
  142. if (!done.length) {
  143. const editor = new CustomCategoryEditor(repository)
  144. editor.load(id, (err) => {
  145. if (err) { return global.alert(err) }
  146. editor.category = category
  147. editor.edit()
  148. })
  149. }
  150. }
  151. hooks.register('browser-more-categories', (browser, parameters) => {
  152. const content = browser.dom
  153. if (!Object.keys(parameters).length) {
  154. let block = document.createElement('div')
  155. block.setAttribute('weight', 0)
  156. content.appendChild(block)
  157. let header = document.createElement('h4')
  158. header.innerHTML = lang('customCategory:header')
  159. block.appendChild(header)
  160. let ul = document.createElement('ul')
  161. let li = document.createElement('li')
  162. let a = document.createElement('a')
  163. a.innerHTML = lang('customCategory:create')
  164. a.href = '#'
  165. a.onclick = (e) => {
  166. const editor = new CustomCategoryEditor(repository)
  167. editor.edit()
  168. browser.close()
  169. e.preventDefault()
  170. }
  171. li.appendChild(a)
  172. ul.appendChild(li)
  173. li = document.createElement('li')
  174. a = document.createElement('a')
  175. a.innerHTML = lang('customCategory:list')
  176. a.href = '#more-categories?custom=list'
  177. li.appendChild(a)
  178. ul.appendChild(li)
  179. block.appendChild(ul)
  180. browser.catchLinks()
  181. }
  182. else if (parameters.custom === 'list') {
  183. customCategoriesList(browser, parameters)
  184. }
  185. })
  186. function customCategoriesList (browser, options) {
  187. browser.dom.innerHTML = '<i class="fa fa-spinner fa-pulse fa-fw"></i> ' + lang('loading')
  188. repository.listCategories({},
  189. (err, result) => {
  190. browser.dom.innerHTML = ''
  191. const ul = document.createElement('ul')
  192. browser.dom.appendChild(ul)
  193. result.forEach(cat => {
  194. const li = document.createElement('li')
  195. const a = document.createElement('a')
  196. a.href = '#categories=custom/' + cat.id
  197. a.appendChild(document.createTextNode(cat.name))
  198. li.appendChild(a)
  199. const edit = document.createElement('a')
  200. edit.onclick = (e) => {
  201. editCustomCategory(cat.id)
  202. e.preventDefault()
  203. }
  204. edit.innerHTML = ' <i class="fa fa-pen"></i>'
  205. li.appendChild(edit)
  206. ul.appendChild(li)
  207. })
  208. browser.catchLinks()
  209. })
  210. }
  211. const repository = new CustomCategoryRepository()
  212. hooks.register('init', () => {
  213. OpenStreetBrowserLoader.registerRepository('custom', repository)
  214. })
  215. hooks.register('category-overpass-init', (category) => {
  216. const m = category.id.match(/^custom\/(.*)$/)
  217. if (m) {
  218. const id = m[1]
  219. if (category.tabEdit) {
  220. category.tools.remove(this.category.tabEdit)
  221. }
  222. category.tabEdit = new tabs.Tab({
  223. id: 'edit',
  224. weight: 9
  225. })
  226. category.tools.add(category.tabEdit)
  227. category.tabEdit.header.innerHTML = '<i class="fa fa-pen"></i>'
  228. category.tabEdit.header.title = lang('edit')
  229. category.tabEdit.on('select', () => {
  230. category.tabEdit.unselect()
  231. editCustomCategory(id, category)
  232. })
  233. if (!category.tabShare) {
  234. const url = location.origin + location.pathname + '#categories=custom/' + id
  235. category.tabShare = new tabs.Tab({
  236. id: 'share',
  237. weight: 10
  238. })
  239. category.tools.add(category.tabShare)
  240. category.shareLink = document.createElement('a')
  241. category.shareLink.href = url
  242. category.shareLink.innerHTML = '<i class="fa fa-share-alt"></i>'
  243. category.tabShare.header.appendChild(category.shareLink)
  244. category.tabShare.header.className = 'share-button'
  245. category.tabShare.on('select', () => {
  246. category.tabShare.unselect()
  247. navigator.clipboard.writeText(url)
  248. const notify = document.createElement('div')
  249. notify.className = 'notify'
  250. notify.innerHTML = lang('copied-clipboard')
  251. category.tabShare.header.appendChild(notify)
  252. global.setTimeout(() => category.tabShare.header.removeChild(notify), 2000)
  253. })
  254. }
  255. } else {
  256. if (category.tabClone) {
  257. category.tools.remove(this.category.tabClone)
  258. }
  259. category.tabClone = new tabs.Tab({
  260. id: 'clone',
  261. weight: 9
  262. })
  263. category.tools.add(category.tabClone)
  264. category.tabClone.header.innerHTML = '<i class="fa fa-clone"></i>'
  265. category.tabClone.header.title = lang('customCategory:clone')
  266. category.tabClone.on('select', () => {
  267. category.tabClone.unselect()
  268. const clone = new CustomCategoryEditor(repository)
  269. clone.edit()
  270. category.repository.file_get_contents(category.data.fileName, {},
  271. (err, content) => {
  272. if (category.data.format === 'json') {
  273. content = JSON.parse(content)
  274. content = jsonMultilineStrings.join(content, { exclude: [ [ 'const' ], [ 'filter' ] ] })
  275. content = yaml.dump(content, {
  276. lineWidth: 9999
  277. })
  278. }
  279. clone.applyContent(content)
  280. category.close()
  281. }
  282. )
  283. })
  284. }
  285. })
  286. function customCategoryTest (value) {
  287. if (!value) {
  288. return new Error('Empty category')
  289. }
  290. let data
  291. try {
  292. data = yaml.load(value)
  293. }
  294. catch (e) {
  295. return e
  296. }
  297. if (!data || typeof data !== 'object') {
  298. return new Error('Data can not be parsed into an object')
  299. }
  300. const fields = ['feature', 'memberFeature']
  301. for (let i1 = 0; i1 < fields.length; i1++) {
  302. const k1 = fields[i1]
  303. if (data[k1]) {
  304. for (k2 in data[k1]) {
  305. const err = customCategoryTestCompile(data[k1][k2])
  306. if (err) {
  307. return new Error('Compiling /' + k1 + '/' + k2 + ': ' + err.message)
  308. }
  309. if (k2 === 'style' || k2.match(/^style:/)) {
  310. for (const k3 in data[k1][k2]) {
  311. const err = customCategoryTestCompile(data[k1][k2][k3])
  312. if (err) {
  313. return new Error('Compiling /' + k1 + '/' + k2 + '/' + k3 + ': ' + err.message)
  314. }
  315. }
  316. }
  317. }
  318. }
  319. }
  320. }
  321. function customCategoryTestCompile (data) {
  322. if (typeof data !== 'string' || data.search('{') === -1) {
  323. return
  324. }
  325. try {
  326. OverpassLayer.twig.twig({ data })
  327. }
  328. catch (e) {
  329. return e
  330. }
  331. }