const tabs = require('modulekit-tabs') const yaml = require('js-yaml') const md5 = require('md5') const OverpassLayer = require('overpass-layer') const OverpassFrontendFilter = require('overpass-frontend/src/Filter') const jsonMultilineStrings = require('json-multiline-strings') const Window = require('./Window') const OpenStreetBrowserLoader = require('./OpenStreetBrowserLoader') const editors = [] class CustomCategoryRepository { constructor () { this.clearCache() } load (callback) { callback(null) } clearCache () { this.cache = {} } listCategories (options, callback) { fetch('customCategory.php?action=list') .then(res => res.json()) .then(result => callback(null, result)) } getCategory (id, options, callback) { if (id in this.cache) { const data = this.parseCategory(id, this.cache[id]) return callback(null, data, this.cache[id]) } fetch('customCategory.php?id=' + id) .then(res => res.text()) .then(content => { this.cache[id] = content const data = this.parseCategory(id, content) callback(null, data, content) }) } parseCategory (id, content) { let data try { data = yaml.load(content) } catch (e) { return global.alert(e) } if (data && typeof data !== 'object') { return new Error('Data can not be parsed into an object') } if (!data.name) { data.name = 'Custom ' + id.substr(0, 6) } return data } saveCategory (body, options, callback) { const id = md5(body) this.cache[id] = body fetch('customCategory.php?action=save', { method: 'POST', body }) } getTemplate (id, options, callback) { callback(null, '') } } class CustomCategoryEditor { constructor (repository) { this.repository = repository editors.push(this) } load (id, callback) { this.repository.getCategory(id, {}, (err, category, content) => { this.content = content callback(err, content) }) } edit () { if (this.window) { this.window.focused = true return } this.window = new Window({ title: 'Custom Category' }) this.window.on('close', () => { this.window = null }) this.textarea = document.createElement('textarea') this.textarea.spellcheck = false this.window.content.appendChild(this.textarea) if (this.content !== undefined) { this.textarea.value = this.content } const controls = document.createElement('div') controls.className = 'controls' this.window.content.appendChild(controls) const input = document.createElement('input') input.type = 'submit' input.value = lang('apply-keep') controls.appendChild(input) const inputClose = document.createElement('input') inputClose.type = 'button' inputClose.value = lang('apply-close') inputClose.onclick = () => { if (this.submit()) { this.window.close() } } controls.appendChild(inputClose) const icons = document.createElement('div') icons.className = 'actions' controls.appendChild(icons) this.inputDownload = document.createElement('a') this.textarea.onchange = () => this.updateDownload() this.updateDownload() this.inputDownload.title = lang('download') this.inputDownload.innerHTML = '' icons.appendChild(this.inputDownload) const tutorial = document.createElement('span') tutorial.className = 'tip-tutorial' let text = lang('tip-tutorial') text = text.replace('[', '') text = text.replace(']', '') tutorial.innerHTML = text controls.appendChild(tutorial) input.onclick = (e) => { this.submit() e.preventDefault() } this.window.show() } submit () { const err = customCategoryTest(this.textarea.value) if (err) { global.alert(err) return false } this.applyContent(this.textarea.value) return true } applyContent (content) { this.content = content this.repository.saveCategory(this.content, {}, () => {}) if (this.textarea) { this.textarea.value = content this.updateDownload() } const id = md5(content) this.id = id if (this.category) { this.category.remove() this.category = null } OpenStreetBrowserLoader.getCategory('custom/' + id, {}, (err, category) => { if (err) { return global.alert(err) } this.category = category this.category.setParentDom(document.getElementById('contentListAddCategories')) this.category.open() }) } updateDownload () { const file = new Blob([this.textarea.value], { type: 'application/yaml' }) this.inputDownload.href = URL.createObjectURL(file) this.inputDownload.download = md5(this.textarea.value) + '.yaml' } } function editCustomCategory (id, category) { const done = editors.filter(editor => { if (editor.id === id) { editor.edit() return true } }) if (!done.length) { const editor = new CustomCategoryEditor(repository) editor.load(id, (err) => { if (err) { return global.alert(err) } editor.category = category editor.edit() }) } } hooks.register('browser-more-categories', (browser, parameters) => { const content = browser.dom if (!Object.keys(parameters).length) { const block = document.createElement('div') block.setAttribute('weight', 0) content.appendChild(block) const header = document.createElement('h4') header.innerHTML = lang('customCategory:header') block.appendChild(header) const ul = document.createElement('ul') let li = document.createElement('li') let a = document.createElement('a') a.innerHTML = lang('customCategory:create') a.href = '#' a.onclick = (e) => { const editor = new CustomCategoryEditor(repository) editor.edit() browser.close() e.preventDefault() } li.appendChild(a) ul.appendChild(li) li = document.createElement('li') a = document.createElement('a') a.innerHTML = lang('customCategory:list') a.href = '#more-categories?custom=list' li.appendChild(a) ul.appendChild(li) block.appendChild(ul) browser.catchLinks() } else if (parameters.custom === 'list') { customCategoriesList(browser, parameters) } }) function customCategoriesList (browser, options) { browser.dom.innerHTML = ' ' + lang('loading') repository.listCategories({}, (err, result) => { browser.dom.innerHTML = '' const ul = document.createElement('ul') browser.dom.appendChild(ul) result.forEach(cat => { const li = document.createElement('li') const a = document.createElement('a') a.href = '#categories=custom/' + cat.id a.appendChild(document.createTextNode(cat.name)) li.appendChild(a) const edit = document.createElement('a') edit.onclick = (e) => { editCustomCategory(cat.id) e.preventDefault() } edit.innerHTML = ' ' li.appendChild(edit) ul.appendChild(li) }) browser.catchLinks() }) } const repository = new CustomCategoryRepository() hooks.register('init', () => { OpenStreetBrowserLoader.registerRepository('custom', repository) }) hooks.register('category-overpass-init', (category) => { const m = category.id.match(/^custom\/(.*)$/) if (m) { const id = m[1] if (category.tabEdit) { category.tools.remove(this.category.tabEdit) } category.tabEdit = new tabs.Tab({ id: 'edit', weight: 9 }) category.tools.add(category.tabEdit) category.tabEdit.header.innerHTML = '' category.tabEdit.header.title = lang('edit') category.tabEdit.on('select', () => { category.tabEdit.unselect() editCustomCategory(id, category) }) if (!category.tabShare) { const url = location.origin + location.pathname + '#categories=custom/' + id category.tabShare = new tabs.Tab({ id: 'share', weight: 10 }) category.tools.add(category.tabShare) category.shareLink = document.createElement('a') category.shareLink.href = url category.shareLink.innerHTML = '' category.tabShare.header.appendChild(category.shareLink) category.tabShare.header.className = 'share-button' category.tabShare.on('select', () => { category.tabShare.unselect() navigator.clipboard.writeText(url) const notify = document.createElement('div') notify.className = 'notify' notify.innerHTML = lang('copied-clipboard') category.tabShare.header.appendChild(notify) global.setTimeout(() => category.tabShare.header.removeChild(notify), 2000) }) } } else { if (category.tabClone) { category.tools.remove(this.category.tabClone) } category.tabClone = new tabs.Tab({ id: 'clone', weight: 9 }) category.tools.add(category.tabClone) category.tabClone.header.innerHTML = '' category.tabClone.header.title = lang('customCategory:clone') category.tabClone.on('select', () => { category.tabClone.unselect() const clone = new CustomCategoryEditor(repository) clone.edit() category.repository.file_get_contents(category.data.fileName, {}, (err, content) => { if (err) { console.error(err) return global.alert(err) } if (category.data.format === 'json') { content = JSON.parse(content) content = jsonMultilineStrings.join(content, { exclude: [['const'], ['filter']] }) content = yaml.dump(content, { lineWidth: 9999 }) } clone.applyContent(content) category.close() } ) }) } }) function customCategoryTest (value) { if (!value) { return new Error('Empty category') } let data try { data = yaml.load(value) } catch (e) { return e } if (!data || typeof data !== 'object') { return new Error('Data can not be parsed into an object') } if (!('query' in data)) { return new Error('No "query" defined!') } if (typeof data.query === 'string') { const r = customCategoryTestQuery(data.query) if (r) { return r } } else if (data.query === null) { return new Error('No "query" defined!') } else if (Object.values(data.query).length) { for (const z in data.query) { const r = customCategoryTestQuery(data.query[z]) if (r) { return new Error('Query z' + z + ': ' + r) } } } else { return new Error('"query" can not be parsed!') } const testPaths = [ [ /^(memberF|f)eature$/, /./ ], [ /^(memberF|f)eature$/, /^style(:.*)$/, /./ ], [ 'filter', /./, /(name|query|values|placeholder|valueName)/ ], [ 'filter', /./, 'values', /./ , 'name'], [ 'info' ] ] try { testPaths.forEach(path => customCategoryTestCompilePath(data, path, '')) } catch (err) { return err } } function customCategoryTestCompilePath (data, subPaths, path) { if (subPaths.length) { const p = subPaths[0] for (const k in data) { if (typeof p === 'string' ? k === p : k.match(p)) { customCategoryTestCompilePath(data[k], subPaths.slice(1), path + '/' + k) } } } const err = customCategoryTestCompile(data) if (err) { throw new Error('Compiling ' + path + ': ' + err.message) } } function customCategoryTestCompile (data) { if (typeof data !== 'string' || data.search('{') === -1) { return } let template try { template = OverpassLayer.twig.twig({ data, rethrow: true }) } catch (e) { return e } const fakeOb = { id: 'n1', sublayer_id: 'main', osm_id: 1, type: 'node', tags: { foo: 'bar' }, map: { zoom: 15, metersPerPixel: 0.8 } } try { template.render(fakeOb) } catch (e) { return e } } function customCategoryTestQuery (str) { if (typeof str !== 'string') { return 'Query is not a string!' } // make sure the request ends with ';' if (!str.match(/;\s*$/)) { str += ';' } try { new OverpassFrontendFilter(str) } catch (e) { return e } }