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.

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