From 3f18c48d370b72192bad7634836cd1f30dcc6ac1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Tue, 2 Aug 2022 21:12:43 +0200 Subject: [PATCH 01/76] CustomCategory: create new, edit loaded --- lang/en.json | 2 + package-lock.json | 9 ++-- package.json | 4 +- src/addCategories.js | 3 ++ src/customCategory.js | 103 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 117 insertions(+), 4 deletions(-) create mode 100644 src/customCategory.js diff --git a/lang/en.json b/lang/en.json index a02708b6..7cdf7ca8 100644 --- a/lang/en.json +++ b/lang/en.json @@ -8,6 +8,8 @@ "category-info-tooltip": "Info & Map key", "closed": "closed", "default": "default", + "apply": "Apply", + "customCategory:create": "Create custom category", "edit": "edit", "editor:id": "iD (in-browser editor)", "editor:remote": "Remote Control (JOSM or Merkaator)", diff --git a/package-lock.json b/package-lock.json index d5b06e8f..018ffe57 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4819,7 +4819,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, "requires": { "argparse": "^2.0.1" }, @@ -4827,8 +4826,7 @@ "argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" } } }, @@ -7737,6 +7735,11 @@ "is-typed-array": "^1.1.6" } }, + "window-modal": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/window-modal/-/window-modal-1.0.5.tgz", + "integrity": "sha512-DbpgUeFeYLUoq/ZLCR2IESFxcf5koyKdtRgncumPCu9l/iu7lzGLOJon/XJZ/zRA1XEZv5Ga/rtOpldqF4bEjg==" + }, "word-wrap": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", diff --git a/package.json b/package.json index df68799e..41a61605 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "formatcoords": "^1.1.3", "i18next-client": "^1.11.4", "ip-location": "^1.0.1", + "js-yaml": "^4.1.0", "json-multiline-strings": "^0.1.0", "leaflet": "^1.7.1", "leaflet-geosearch": "^3.6.1", @@ -47,7 +48,8 @@ "query-string": "^6.13.8", "sheet-router": "^4.2.3", "sprintf-js": "^1.1.2", - "weight-sort": "^1.3.1" + "weight-sort": "^1.3.1", + "window-modal": "^1.0.5" }, "scripts": { "test": "mocha --bail", diff --git a/src/addCategories.js b/src/addCategories.js index 89dc7287..840e0b85 100644 --- a/src/addCategories.js +++ b/src/addCategories.js @@ -5,6 +5,7 @@ const tabs = require('modulekit-tabs') const weightSort = require('weight-sort') const OpenStreetBrowserLoader = require('./OpenStreetBrowserLoader') +const customCategory = require('./customCategory') let tab @@ -37,6 +38,8 @@ function addCategoriesShow (repo, options={}) { var list = {} + customCategory(content) + if (repo) { var backLink = document.createElement('a') backLink.className = 'back' diff --git a/src/customCategory.js b/src/customCategory.js new file mode 100644 index 00000000..a2c830ff --- /dev/null +++ b/src/customCategory.js @@ -0,0 +1,103 @@ +const ModalWindow = require('window-modal') +const tabs = require('modulekit-tabs') +const yaml = require('js-yaml') +const md5 = require('md5') + +const OpenStreetBrowserLoader = require('./OpenStreetBrowserLoader') + +class CustomCategory { + constructor () { + } + + edit () { + if (this.modal) { + this.modal.focused = true + return + } + + this.modal = new ModalWindow({ + title: 'Custom Category', + hideMinimize: true, + zIndex: '99999' + }) + + this.modal.addEventListener('close', () => { + this.modal = null + }) + + this.textarea = document.createElement('textarea') + this.modal.content.element.appendChild(this.textarea) + if (this.content !== undefined) { + this.textarea.value = this.content + } + + const input = document.createElement('input') + input.type = 'submit' + input.value = lang('apply') + this.modal.content.element.appendChild(input) + + input.onclick = () => { + this.applyContent(this.textarea.value) + return true + } + } + + applyContent (content) { + this.content = content + + const id = 'custom:' + md5(content) + const data = yaml.load(content) + + if (this.category) { + this.category.remove() + this.category = null + } + + OpenStreetBrowserLoader.getCategoryFromData(id, {}, data, (err, category) => { + if (err) { + return global.alert(err) + } + + this.category = category + this.category.setParentDom(document.getElementById('contentListAddCategories')) + this.category.setMap(global.map) + + if (this.category.tabEdit) { + this.category.tools.remove(this.category.tabEdit) + } + + this.category.tabEdit = new tabs.Tab({ + id: 'edit' + }) + this.category.tools.add(this.category.tabEdit) + this.category.tabEdit.header.innerHTML = '' + this.category.tabEdit.on('select', () => { + this.category.tabEdit.unselect() + this.edit() + }) + + this.category.open() + }) + } +} + + +function createCustomCategory () { + let category + + category = new CustomCategory() + category.edit() + + return false +} + +module.exports = function customCategory (content) { + let div = document.createElement('div') + + let a = document.createElement('a') + a.innerHTML = lang('customCategory:create') + a.href = '#' + a.onclick = createCustomCategory + div.appendChild(a) + content.appendChild(div) +} From e44958606da353b55a07a038b956ee5e3e4c2566 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Tue, 2 Aug 2022 22:54:10 +0200 Subject: [PATCH 02/76] CustomCategory: save categories to a database --- conf.php-dist | 8 ++++++++ init.sql | 7 +++++++ modulekit.php | 2 ++ src/customCategory.js | 1 + src/customCategory.php | 39 +++++++++++++++++++++++++++++++++++++++ src/database.php | 9 +++++++++ 6 files changed, 66 insertions(+) create mode 100644 init.sql create mode 100644 src/customCategory.php create mode 100644 src/database.php diff --git a/conf.php-dist b/conf.php-dist index 9dc2457d..148c121e 100644 --- a/conf.php-dist +++ b/conf.php-dist @@ -94,6 +94,14 @@ $config['baseMaps'] = array( ), ); +// customCategory needs a database +$db_conf = [ + //'dsn' => 'mysql:host=localhost;dbname=openstreetbrowser', + 'dsn' => 'sqlite:data/db.sqlite', + 'username' => 'USERNAME', + 'password' => 'PASSWORD', +]; + // List of available user interface languages $languages = array( "en", // English diff --git a/init.sql b/init.sql new file mode 100644 index 00000000..5476b34f --- /dev/null +++ b/init.sql @@ -0,0 +1,7 @@ +create table customCategory ( + id char(32) not null, + content mediumtext not null, + created datetime not null default CURRENT_TIMESTAMP, + lastAccess datetime not null default CURRENT_TIMESTAMP, + primary key(id) +); diff --git a/modulekit.php b/modulekit.php index 6bbce73a..59b1d8e8 100644 --- a/modulekit.php +++ b/modulekit.php @@ -12,6 +12,7 @@ $depend = array( $include = array( 'php' => array( 'src/defaults.php', + 'src/database.php', 'src/options.php', 'src/language.php', 'src/ip-location.php', @@ -22,6 +23,7 @@ $include = array( 'src/RepositoryDir.php', 'src/RepositoryGit.php', 'src/repositories.php', + 'src/customCategory.php', ), 'css' => array( 'style.css', diff --git a/src/customCategory.js b/src/customCategory.js index a2c830ff..d20e3a2d 100644 --- a/src/customCategory.js +++ b/src/customCategory.js @@ -38,6 +38,7 @@ class CustomCategory { input.onclick = () => { this.applyContent(this.textarea.value) + ajax('customCategory', { content: this.textarea.value }, (result) => {}) return true } } diff --git a/src/customCategory.php b/src/customCategory.php new file mode 100644 index 00000000..cb3083ca --- /dev/null +++ b/src/customCategory.php @@ -0,0 +1,39 @@ +prepare("select content from customCategory where id=:id"); + $stmt->bindValue(':id', $param['id'], PDO::PARAM_STR); + if ($stmt->execute()) { + $row = $stmt->fetch(PDO::FETCH_ASSOC); + $result = $row['content']; + $stmt->closeCursor(); + + $stmt = $db->prepare("update customCategory set lastAccess=:now where id=:id"); + $stmt->bindValue(':id', $param['id']); + $stmt->bindValue(':now', (new DateTime())->format('Y-m-d H:i:s'), PDO::PARAM_STR); + $stmt->execute(); + + return $result; + } + + return false; + } + + if ($param['content']) { + $id = md5($param['content']); + + //$stmt = $db->prepare("insert into customCategory (id, content) values (:id, :content) on duplicate key update lastAccess=:now"); + $stmt = $db->prepare("insert into customCategory (id, content) values (:id, :content) on conflict(id) do update set lastAccess=:now"); + $stmt->bindValue(':id', $id, PDO::PARAM_STR); + $stmt->bindValue(':content', $param['content'], PDO::PARAM_STR); + $stmt->bindValue(':now', (new DateTime())->format('Y-m-d H:i:s'), PDO::PARAM_STR); + $result = $stmt->execute(); + return $result; + } +} diff --git a/src/database.php b/src/database.php new file mode 100644 index 00000000..c5b013b8 --- /dev/null +++ b/src/database.php @@ -0,0 +1,9 @@ + Date: Wed, 3 Aug 2022 18:36:23 +0200 Subject: [PATCH 03/76] OpenStreetBrowserLoader: convert to a ES6 class --- src/OpenStreetBrowserLoader.js | 400 +++++++++++++++++---------------- 1 file changed, 201 insertions(+), 199 deletions(-) diff --git a/src/OpenStreetBrowserLoader.js b/src/OpenStreetBrowserLoader.js index e8659ed9..4b163a1b 100644 --- a/src/OpenStreetBrowserLoader.js +++ b/src/OpenStreetBrowserLoader.js @@ -2,268 +2,270 @@ var OverpassLayer = require('overpass-layer') const Repository = require('./Repository') -function OpenStreetBrowserLoader () { - this.types = {} - this.categories = {} - this.repoCache = {} - this.repositories = {} - this.templates = {} - this._loadClash = {} // if a category is being loaded multiple times, collect callbacks -} - -OpenStreetBrowserLoader.prototype.setMap = function (map) { - this.map = map -} - -/** - * @param string id ID of the category - * @param [object] options Options. - * @waram {boolean} [options.force=false] Whether repository should be reload or not. - * @param function callback Callback which will be called with (err, category) - */ -OpenStreetBrowserLoader.prototype.getCategory = function (id, options, callback) { - if (typeof options === 'function') { - callback = options - options = {} +class OpenStreetBrowserLoader { + constructor () { + this.types = {} + this.categories = {} + this.repoCache = {} + this.repositories = {} + this.templates = {} + this._loadClash = {} // if a category is being loaded multiple times, collect callbacks } - var ids = this.getFullId(id, options) - if (ids === null) { - return callback(new Error('invalid id'), null) + setMap (map) { + this.map = map } - if (options.force) { - delete this.categories[ids.fullId] - } + /** + * @param string id ID of the category + * @param [object] options Options. + * @waram {boolean} [options.force=false] Whether repository should be reload or not. + * @param function callback Callback which will be called with (err, category) + */ + getCategory (id, options, callback) { + if (typeof options === 'function') { + callback = options + options = {} + } - if (ids.fullId in this.categories) { - return callback(null, this.categories[ids.fullId]) - } + var ids = this.getFullId(id, options) + if (ids === null) { + return callback(new Error('invalid id'), null) + } - var opt = JSON.parse(JSON.stringify(options)) - opt.categoryId = ids.entityId - opt.repositoryId = ids.repositoryId + if (options.force) { + delete this.categories[ids.fullId] + } - this.getRepo(ids.repositoryId, opt, function (err, repoData) { - // maybe loaded in the meantime? if (ids.fullId in this.categories) { return callback(null, this.categories[ids.fullId]) } - if (err) { - return callback(err, null) - } + var opt = JSON.parse(JSON.stringify(options)) + opt.categoryId = ids.entityId + opt.repositoryId = ids.repositoryId - if (!(ids.entityId in repoData.categories)) { - return callback(new Error('category "' + ids.entityId + '" not defined'), null) - } + this.getRepo(ids.repositoryId, opt, function (err, repoData) { + // maybe loaded in the meantime? + if (ids.fullId in this.categories) { + return callback(null, this.categories[ids.fullId]) + } - this.getCategoryFromData(ids.id, opt, repoData.categories[ids.entityId], function (err, category) { - if (category) { - category.setMap(this.map) + if (err) { + return callback(err, null) } - callback(err, category) - }.bind(this)) - }.bind(this)) -} + if (!(ids.entityId in repoData.categories)) { + return callback(new Error('category "' + ids.entityId + '" not defined'), null) + } -/** - * @param string repo ID of the repository - * @parapm [object] options Options. - * @waram {boolean} [options.force=false] Whether repository should be reload or not. - * @param function callback Callback which will be called with (err, repoData) - */ -OpenStreetBrowserLoader.prototype.getRepo = function (repo, options, callback) { - if (options.force) { - delete this.repoCache[repo] - } + this.getCategoryFromData(ids.id, opt, repoData.categories[ids.entityId], function (err, category) { + if (category) { + category.setMap(this.map) + } - if (repo in this.repoCache) { - return callback.apply(this, this.repoCache[repo]) + callback(err, category) + }.bind(this)) + }.bind(this)) } - if (repo in this._loadClash) { - this._loadClash[repo].push(callback) - return - } + /** + * @param string repo ID of the repository + * @parapm [object] options Options. + * @waram {boolean} [options.force=false] Whether repository should be reload or not. + * @param function callback Callback which will be called with (err, repoData) + */ + getRepo (repo, options, callback) { + if (options.force) { + delete this.repoCache[repo] + } - this._loadClash[repo] = [ callback ] + if (repo in this.repoCache) { + return callback.apply(this, this.repoCache[repo]) + } - function reqListener (req) { - if (req.status !== 200) { - console.log('http error when loading repository', req) - this.repoCache[repo] = [ req.statusText, null ] - } else { - try { - let repoData = JSON.parse(req.responseText) - this.repositories[repo] = new Repository(repo, repoData) - this.repoCache[repo] = [ null, repoData ] - } catch (err) { - console.log('couldn\'t parse repository', req.responseText) - this.repoCache[repo] = [ 'couldn\t parse repository', null ] - } + if (repo in this._loadClash) { + this._loadClash[repo].push(callback) + return } - var todo = this._loadClash[repo] - delete this._loadClash[repo] + this._loadClash[repo] = [ callback ] + + function reqListener (req) { + if (req.status !== 200) { + console.log('http error when loading repository', req) + this.repoCache[repo] = [ req.statusText, null ] + } else { + try { + let repoData = JSON.parse(req.responseText) + this.repositories[repo] = new Repository(repo, repoData) + this.repoCache[repo] = [ null, repoData ] + } catch (err) { + console.log('couldn\'t parse repository', req.responseText) + this.repoCache[repo] = [ 'couldn\t parse repository', null ] + } + } - todo.forEach(function (callback) { - callback.apply(this, this.repoCache[repo]) - }.bind(this)) - } + var todo = this._loadClash[repo] + delete this._loadClash[repo] - var param = [] - if (repo) { - param.push('repo=' + encodeURIComponent(repo)) - } - param.push('lang=' + encodeURIComponent(ui_lang)) - param.push(config.categoriesRev) - param = param.length ? '?' + param.join('&') : '' - - var req = new XMLHttpRequest() - req.addEventListener('load', reqListener.bind(this, req)) - req.open('GET', 'repo.php' + param) - req.send() -} + todo.forEach(function (callback) { + callback.apply(this, this.repoCache[repo]) + }.bind(this)) + } -OpenStreetBrowserLoader.prototype.getRepository = function (id, options, callback) { - if (id in this.repositories) { - return callback(null, this.repositories[id]) + var param = [] + if (repo) { + param.push('repo=' + encodeURIComponent(repo)) + } + param.push('lang=' + encodeURIComponent(ui_lang)) + param.push(config.categoriesRev) + param = param.length ? '?' + param.join('&') : '' + + var req = new XMLHttpRequest() + req.addEventListener('load', reqListener.bind(this, req)) + req.open('GET', 'repo.php' + param) + req.send() } - this.getRepo(id, options, (err, repoData) => { - if (err) { - return callback(err) + getRepository (id, options, callback) { + if (id in this.repositories) { + return callback(null, this.repositories[id]) } - callback(null, this.repositories[id]) - }) -} + this.getRepo(id, options, (err, repoData) => { + if (err) { + return callback(err) + } -/** - * @param string id ID of the template - * @parapm [object] options Options. - * @waram {boolean} [options.force=false] Whether repository should be reload or not. - * @param function callback Callback which will be called with (err, template) - */ -OpenStreetBrowserLoader.prototype.getTemplate = function (id, options, callback) { - if (typeof options === 'function') { - callback = options - options = {} + callback(null, this.repositories[id]) + }) } - var ids = this.getFullId(id, options) + /** + * @param string id ID of the template + * @parapm [object] options Options. + * @waram {boolean} [options.force=false] Whether repository should be reload or not. + * @param function callback Callback which will be called with (err, template) + */ + getTemplate (id, options, callback) { + if (typeof options === 'function') { + callback = options + options = {} + } - if (options.force) { - delete this.templates[ids.fullId] - } + var ids = this.getFullId(id, options) - if (ids.fullId in this.templates) { - return callback(null, this.templates[ids.fullId]) - } - - var opt = JSON.parse(JSON.stringify(options)) - opt.templateId = ids.entityId - opt.repositoryId = ids.repositoryId + if (options.force) { + delete this.templates[ids.fullId] + } - this.getRepo(ids.repositoryId, opt, function (err, repoData) { - // maybe loaded in the meantime? if (ids.fullId in this.templates) { return callback(null, this.templates[ids.fullId]) } - if (err) { - return callback(err, null) - } + var opt = JSON.parse(JSON.stringify(options)) + opt.templateId = ids.entityId + opt.repositoryId = ids.repositoryId - if (!repoData.templates || !(ids.entityId in repoData.templates)) { - return callback(new Error('template not defined'), null) - } + this.getRepo(ids.repositoryId, opt, function (err, repoData) { + // maybe loaded in the meantime? + if (ids.fullId in this.templates) { + return callback(null, this.templates[ids.fullId]) + } - this.templates[ids.fullId] = OverpassLayer.twig.twig({ data: repoData.templates[ids.entityId], autoescape: true }) + if (err) { + return callback(err, null) + } - callback(null, this.templates[ids.fullId]) - }.bind(this)) -} + if (!repoData.templates || !(ids.entityId in repoData.templates)) { + return callback(new Error('template not defined'), null) + } -OpenStreetBrowserLoader.prototype.getCategoryFromData = function (id, options, data, callback) { - var ids = this.getFullId(id, options) + this.templates[ids.fullId] = OverpassLayer.twig.twig({ data: repoData.templates[ids.entityId], autoescape: true }) - if (ids.fullId in this.categories) { - return callback(null, this.categories[ids.fullId]) + callback(null, this.templates[ids.fullId]) + }.bind(this)) } - if (!data.type) { - return callback(new Error('no type defined'), null) - } + getCategoryFromData (id, options, data, callback) { + var ids = this.getFullId(id, options) - if (!(data.type in this.types)) { - return callback(new Error('unknown type'), null) - } + if (ids.fullId in this.categories) { + return callback(null, this.categories[ids.fullId]) + } - let repository = this.repositories[ids.repositoryId] + if (!data.type) { + return callback(new Error('no type defined'), null) + } - var opt = JSON.parse(JSON.stringify(options)) - opt.id = ids.id - var layer = new this.types[data.type](opt, data, repository) + if (!(data.type in this.types)) { + return callback(new Error('unknown type'), null) + } - layer.setMap(this.map) + let repository = this.repositories[ids.repositoryId] - this.categories[ids.fullId] = layer + var opt = JSON.parse(JSON.stringify(options)) + opt.id = ids.id + var layer = new this.types[data.type](opt, data, repository) - if ('load' in layer) { - layer.load(function (err) { - callback(err, layer) - }) - } else { - callback(null, layer) - } -} + layer.setMap(this.map) -OpenStreetBrowserLoader.prototype.getFullId = function (id, options) { - var result = {} + this.categories[ids.fullId] = layer - if (!id) { - return null + if ('load' in layer) { + layer.load(function (err) { + callback(err, layer) + }) + } else { + callback(null, layer) + } } - let m = id.match(/^(.*)\/([^/]*)/) - if (m) { - result.id = id - result.repositoryId = m[1] - result.entityId = m[2] - } else if (options.repositoryId && options.repositoryId !== 'default') { - result.repositoryId = options.repositoryId - result.entityId = id - result.id = result.repositoryId + '/' + id - } else { - result.id = id - result.repositoryId = 'default' - result.entityId = id - } + getFullId (id, options) { + var result = {} - result.sublayerId = null - m = result.entityId.split(/:/) - if (m.length > 1) { - result.sublayerId = m[0] - result.entityId = m[1] - } + if (!id) { + return null + } - result.fullId = result.repositoryId + '/' + (result.sublayerId ? result.sublayerId + ':' : '') + result.entityId + let m = id.match(/^(.*)\/([^/]*)/) + if (m) { + result.id = id + result.repositoryId = m[1] + result.entityId = m[2] + } else if (options.repositoryId && options.repositoryId !== 'default') { + result.repositoryId = options.repositoryId + result.entityId = id + result.id = result.repositoryId + '/' + id + } else { + result.id = id + result.repositoryId = 'default' + result.entityId = id + } - return result -} + result.sublayerId = null + m = result.entityId.split(/:/) + if (m.length > 1) { + result.sublayerId = m[0] + result.entityId = m[1] + } -OpenStreetBrowserLoader.prototype.forget = function (id) { - var ids = this.getFullId(id, options) + result.fullId = result.repositoryId + '/' + (result.sublayerId ? result.sublayerId + ':' : '') + result.entityId - this.categories[ids.fullId].remove() - delete this.categories[ids.fullId] -} + return result + } + + forget (id) { + var ids = this.getFullId(id, options) -OpenStreetBrowserLoader.prototype.registerType = function (type, classObject) { - this.types[type] = classObject + this.categories[ids.fullId].remove() + delete this.categories[ids.fullId] + } + + registerType (type, classObject) { + this.types[type] = classObject + } } module.exports = new OpenStreetBrowserLoader() From da57880c2b670bccb76397ad898da9b0998df611 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Wed, 3 Aug 2022 19:14:10 +0200 Subject: [PATCH 04/76] OpenStreetBrowserLoader: do not use .getRepo() directly --- src/OpenStreetBrowserLoader.js | 3 ++- src/addCategories.js | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/OpenStreetBrowserLoader.js b/src/OpenStreetBrowserLoader.js index 4b163a1b..921fad08 100644 --- a/src/OpenStreetBrowserLoader.js +++ b/src/OpenStreetBrowserLoader.js @@ -45,7 +45,8 @@ class OpenStreetBrowserLoader { opt.categoryId = ids.entityId opt.repositoryId = ids.repositoryId - this.getRepo(ids.repositoryId, opt, function (err, repoData) { + this.getRepository(ids.repositoryId, opt, (err, repository) => { + const repoData = repository.data // maybe loaded in the meantime? if (ids.fullId in this.categories) { return callback(null, this.categories[ids.fullId]) diff --git a/src/addCategories.js b/src/addCategories.js index 840e0b85..a800f017 100644 --- a/src/addCategories.js +++ b/src/addCategories.js @@ -24,11 +24,13 @@ function addCategoriesShow (repo, options={}) { content.innerHTML = '

' + lang('more_categories') + '

' + ' ' + lang('loading') - OpenStreetBrowserLoader.getRepo(repo, options, function (err, repoData) { + OpenStreetBrowserLoader.getRepository(repo, options, function (err, repository) { if (err) { alert(err) } + const repoData = repository.data + content.innerHTML = '

' + lang('more_categories') + '

' var categoryUrl = null From 399298472f471297a124a5b286a19043a4d64a0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Wed, 3 Aug 2022 19:14:42 +0200 Subject: [PATCH 05/76] OpenStreetBrowserLoader.getCategory(), .getTemplate() -> call fun in Repository --- src/OpenStreetBrowserLoader.js | 55 +++++++++++++++++++++------------- src/Repository.js | 16 ++++++++++ 2 files changed, 51 insertions(+), 20 deletions(-) diff --git a/src/OpenStreetBrowserLoader.js b/src/OpenStreetBrowserLoader.js index 921fad08..dd937792 100644 --- a/src/OpenStreetBrowserLoader.js +++ b/src/OpenStreetBrowserLoader.js @@ -46,7 +46,6 @@ class OpenStreetBrowserLoader { opt.repositoryId = ids.repositoryId this.getRepository(ids.repositoryId, opt, (err, repository) => { - const repoData = repository.data // maybe loaded in the meantime? if (ids.fullId in this.categories) { return callback(null, this.categories[ids.fullId]) @@ -56,24 +55,29 @@ class OpenStreetBrowserLoader { return callback(err, null) } - if (!(ids.entityId in repoData.categories)) { - return callback(new Error('category "' + ids.entityId + '" not defined'), null) - } - - this.getCategoryFromData(ids.id, opt, repoData.categories[ids.entityId], function (err, category) { - if (category) { - category.setMap(this.map) + repository.getCategory(ids.entityId, opt, (err, data) => { + // maybe loaded in the meantime? + if (ids.fullId in this.categories) { + return callback(null, this.categories[ids.fullId]) } - callback(err, category) - }.bind(this)) - }.bind(this)) + if (err) { return callback(err) } + + this.getCategoryFromData(ids.id, opt, data, (err, category) =>{ + if (category) { + category.setMap(this.map) + } + + callback(err, category) + }) + }) + }) } /** * @param string repo ID of the repository - * @parapm [object] options Options. - * @waram {boolean} [options.force=false] Whether repository should be reload or not. + * @param [object] options Options. + * @param {boolean} [options.force=false] Whether repository should be reloaded or not. * @param function callback Callback which will be called with (err, repoData) */ getRepo (repo, options, callback) { @@ -129,6 +133,12 @@ class OpenStreetBrowserLoader { req.send() } + /** + * @param string repo ID of the repository + * @param [object] options Options. + * @param {boolean} [options.force=false] Whether repository should be reloaded or not. + * @param function callback Callback which will be called with (err, repository) + */ getRepository (id, options, callback) { if (id in this.repositories) { return callback(null, this.repositories[id]) @@ -169,7 +179,7 @@ class OpenStreetBrowserLoader { opt.templateId = ids.entityId opt.repositoryId = ids.repositoryId - this.getRepo(ids.repositoryId, opt, function (err, repoData) { + this.getRepository(ids.repositoryId, opt, (err, repository) => { // maybe loaded in the meantime? if (ids.fullId in this.templates) { return callback(null, this.templates[ids.fullId]) @@ -179,14 +189,19 @@ class OpenStreetBrowserLoader { return callback(err, null) } - if (!repoData.templates || !(ids.entityId in repoData.templates)) { - return callback(new Error('template not defined'), null) - } + repository.getTemplate(ids.entityId, opt, (err, data) => { + // maybe loaded in the meantime? + if (ids.fullId in this.templates) { + return callback(null, this.templates[ids.fullId]) + } + + if (err) { return callback(err) } - this.templates[ids.fullId] = OverpassLayer.twig.twig({ data: repoData.templates[ids.entityId], autoescape: true }) + this.templates[ids.fullId] = OverpassLayer.twig.twig({ data, autoescape: true }) - callback(null, this.templates[ids.fullId]) - }.bind(this)) + callback(null, this.templates[ids.fullId]) + }) + }) } getCategoryFromData (id, options, data, callback) { diff --git a/src/Repository.js b/src/Repository.js index c5e7de30..b819666d 100644 --- a/src/Repository.js +++ b/src/Repository.js @@ -5,4 +5,20 @@ module.exports = class Repository { this.lang = this.data.lang || {} } + + getCategory (id, options, callback) { + if (!(id in this.data.categories)) { + return callback(new Error('Repository ' + this.id + ': Category "' + id + '" not defined'), null) + } + + callback(null, this.data.categories[id]) + } + + getTemplate (id, options, callback) { + if (!(id in this.data.templates)) { + return callback(new Error('Repository ' + this.id + ': Template "' + id + '" not defined'), null) + } + + callback(null, this.data.templates[id]) + } } From 8d6069d11ad6aaa76947d523521f39d4af6fc3c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Wed, 3 Aug 2022 19:53:28 +0200 Subject: [PATCH 06/76] OpenStreetBrowserLoader: move loading to Repository, get rid of .getRepo() --- src/OpenStreetBrowserLoader.js | 83 +++++++--------------------------- src/Repository.js | 49 +++++++++++++++++++- 2 files changed, 64 insertions(+), 68 deletions(-) diff --git a/src/OpenStreetBrowserLoader.js b/src/OpenStreetBrowserLoader.js index dd937792..bc02949b 100644 --- a/src/OpenStreetBrowserLoader.js +++ b/src/OpenStreetBrowserLoader.js @@ -6,7 +6,6 @@ class OpenStreetBrowserLoader { constructor () { this.types = {} this.categories = {} - this.repoCache = {} this.repositories = {} this.templates = {} this._loadClash = {} // if a category is being loaded multiple times, collect callbacks @@ -78,79 +77,31 @@ class OpenStreetBrowserLoader { * @param string repo ID of the repository * @param [object] options Options. * @param {boolean} [options.force=false] Whether repository should be reloaded or not. - * @param function callback Callback which will be called with (err, repoData) + * @param function callback Callback which will be called with (err, repository) */ - getRepo (repo, options, callback) { - if (options.force) { - delete this.repoCache[repo] - } - - if (repo in this.repoCache) { - return callback.apply(this, this.repoCache[repo]) - } - - if (repo in this._loadClash) { - this._loadClash[repo].push(callback) - return - } + getRepository (id, options, callback) { + if (id in this.repositories) { + const repository = this.repositories[id] - this._loadClash[repo] = [ callback ] - - function reqListener (req) { - if (req.status !== 200) { - console.log('http error when loading repository', req) - this.repoCache[repo] = [ req.statusText, null ] - } else { - try { - let repoData = JSON.parse(req.responseText) - this.repositories[repo] = new Repository(repo, repoData) - this.repoCache[repo] = [ null, repoData ] - } catch (err) { - console.log('couldn\'t parse repository', req.responseText) - this.repoCache[repo] = [ 'couldn\t parse repository', null ] - } + if (repository.loadCallbacks) { + return repository.loadCallbacks.push((err) => callback(err, repository)) } - var todo = this._loadClash[repo] - delete this._loadClash[repo] + if (options.force) { + repository.clearCache() + return repository.load((err) => { + if (err) { return callback(err) } - todo.forEach(function (callback) { - callback.apply(this, this.repoCache[repo]) - }.bind(this)) - } - - var param = [] - if (repo) { - param.push('repo=' + encodeURIComponent(repo)) - } - param.push('lang=' + encodeURIComponent(ui_lang)) - param.push(config.categoriesRev) - param = param.length ? '?' + param.join('&') : '' - - var req = new XMLHttpRequest() - req.addEventListener('load', reqListener.bind(this, req)) - req.open('GET', 'repo.php' + param) - req.send() - } + options.force = false + callback(repository.err, repository) + }) + } - /** - * @param string repo ID of the repository - * @param [object] options Options. - * @param {boolean} [options.force=false] Whether repository should be reloaded or not. - * @param function callback Callback which will be called with (err, repository) - */ - getRepository (id, options, callback) { - if (id in this.repositories) { - return callback(null, this.repositories[id]) + return callback(repository.err, repository) } - this.getRepo(id, options, (err, repoData) => { - if (err) { - return callback(err) - } - - callback(null, this.repositories[id]) - }) + this.repositories[id] = new Repository(id) + this.repositories[id].load((err) => callback(err, this.repositories[id])) } /** diff --git a/src/Repository.js b/src/Repository.js index b819666d..bea94fa0 100644 --- a/src/Repository.js +++ b/src/Repository.js @@ -1,9 +1,54 @@ module.exports = class Repository { constructor (id, data) { this.id = id - this.data = data + this.isLoaded = false - this.lang = this.data.lang || {} + if (data) { + this.data = data + this.lang = this.data.lang || {} + this.loadCallbacks = null + } + } + + load (callback) { + if (this.loadCallbacks) { + return this.loadCallbacks.push(callback) + } + + this.loadCallbacks = [callback] + + var param = [] + + param.push('repo=' + encodeURIComponent(this.id)) + param.push('lang=' + encodeURIComponent(ui_lang)) + param.push(config.categoriesRev) + param = param.length ? '?' + param.join('&') : '' + + fetch('repo.php' + param) + .then(res => res.json()) + .then(data => { + this.data = data + this.lang = this.data.lang || {} + this.err = null + + global.setTimeout(() => { + const cbs = this.loadCallbacks + this.loadCallbacks = null + cbs.forEach(cb => cb(null)) + }, 0) + }) + .catch(err => { + this.err = err + global.setTimeout(() => { + const cbs = this.loadCallbacks + this.loadCallbacks = null + cbs.forEach(cb => cb(err)) + }, 0) + }) + } + + clearCache () { + this.data = null } getCategory (id, options, callback) { From 2bd803b16edbba1a3a00992752d87722052ab0ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Wed, 3 Aug 2022 19:53:59 +0200 Subject: [PATCH 07/76] addCategories: bugfix, return after error-alert --- src/addCategories.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/addCategories.js b/src/addCategories.js index a800f017..b87a238d 100644 --- a/src/addCategories.js +++ b/src/addCategories.js @@ -26,7 +26,7 @@ function addCategoriesShow (repo, options={}) { OpenStreetBrowserLoader.getRepository(repo, options, function (err, repository) { if (err) { - alert(err) + return global.alert(err) } const repoData = repository.data From 0d06bcbb56daf26d443a5c05faedc9ed04276f87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Wed, 3 Aug 2022 20:09:32 +0200 Subject: [PATCH 08/76] OpenStreetBrowserLoader.getRepositoryList() --- src/OpenStreetBrowserLoader.js | 41 ++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/src/OpenStreetBrowserLoader.js b/src/OpenStreetBrowserLoader.js index bc02949b..7a39467e 100644 --- a/src/OpenStreetBrowserLoader.js +++ b/src/OpenStreetBrowserLoader.js @@ -104,6 +104,47 @@ class OpenStreetBrowserLoader { this.repositories[id].load((err) => callback(err, this.repositories[id])) } + /** + * @param [object] options Options. + * @param {boolean} [options.force=false] Whether repository should be reloaded or not. + * @param function callback Callback which will be called with (err, list) + */ + getRepositoryList (options, callback) { + if (this.list) { + return callback(null, this.list) + } + + if (this.repositoryListCallbacks) { + return this.repositoryListCallbacks.push(callback) + } + + this.repositoryListCallbacks = [callback] + + var param = [] + param.push('lang=' + encodeURIComponent(ui_lang)) + param.push(config.categoriesRev) + param = param.length ? '?' + param.join('&') : '' + + fetch('repo.php' + param) + .then(res => res.json()) + .then(data => { + this.list = data + + global.setTimeout(() => { + const cbs = this.repositoryListCallbacks + this.repositoryListCallbacks = null + cbs.forEach(cb => cb(null, this.list)) + }, 0) + }) + .catch(err => { + global.setTimeout(() => { + const cbs = this.repositoryListCallbacks + this.repositoryListCallbacks = null + cbs.forEach(cb => cb(err)) + }, 0) + }) + } + /** * @param string id ID of the template * @parapm [object] options Options. From 724bb1cb7c233ba1e2cf63120babf5beb41c6a31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Wed, 3 Aug 2022 20:11:52 +0200 Subject: [PATCH 09/76] addCategories: split function in list/show --- src/addCategories.js | 190 +++++++++++++++++++++++++++---------------- 1 file changed, 122 insertions(+), 68 deletions(-) diff --git a/src/addCategories.js b/src/addCategories.js index b87a238d..c0c0fe18 100644 --- a/src/addCategories.js +++ b/src/addCategories.js @@ -9,14 +9,95 @@ const customCategory = require('./customCategory') let tab +function addCategoriesList (options = {}) { + let content = tab.content + + content.innerHTML = '

' + lang('more_categories') + '

' + ' ' + lang('loading') + + OpenStreetBrowserLoader.getRepositoryList(options, function (err, repoData) { + if (err) { + return global.alert(err) + } + + content.innerHTML = '

' + lang('more_categories') + '

' + + var categoryUrl = null + if (repoData.categoryUrl) { + categoryUrl = OverpassLayer.twig.twig({ data: repoData.categoryUrl, autoescape: true }) + } + + var list = {} + + customCategory(content) + + if (typeof repositoriesGitea === 'object' && repositoriesGitea.url) { + let a = document.createElement('a') + a.href = repositoriesGitea.url + a.target = '_blank' + a.innerHTML = lang('more_categories_gitea') + content.appendChild(a) + } + + list = weightSort(repoData, { + key: 'timestamp', + reverse: true + }) + + let menu = document.createElement('ul') + menu.className = 'menu' + content.appendChild(menu) + + let header = document.createElement('h3') + header.innerHTML = lang('repositories') + ':' + content.appendChild(header) + + var ul = document.createElement('ul') + + for (var id in list) { + var data = list[id] + + var repositoryUrl = null + if (data.repositoryUrl) { + repositoryUrl = OverpassLayer.twig.twig({ data: data.repositoryUrl, autoescape: true }) + } + + var li = document.createElement('li') + + let a = document.createElement('a') + a.href = '#' + a.onclick = function (id) { + addCategoriesShow(id) + return false + }.bind(this, id) + + li.appendChild(a) + a.appendChild(document.createTextNode('name' in data ? lang(data.name) : id)) + + var editLink = null + if (repositoryUrl) { + editLink = document.createElement('a') + editLink.href = repositoryUrl.render({ repositoryId: id }) + } + if (editLink) { + editLink.className = 'source-code' + editLink.title = 'Show source code' + editLink.target = '_blank' + editLink.innerHTML = '' + li.appendChild(document.createTextNode(' ')) + li.appendChild(editLink) + } + + ul.appendChild(li) + } + + content.appendChild(ul) + }) +} + function addCategoriesShow (repo, options={}) { let content = tab.content - let repoId - let branchId - if (repo) { - [ repoId, branchId ] = repo.split(/~/) - } + let [ repoId, branchId ] = repo.split(/~/) if (!branchId) { branchId = 'master' @@ -42,66 +123,47 @@ function addCategoriesShow (repo, options={}) { customCategory(content) - if (repo) { - var backLink = document.createElement('a') - backLink.className = 'back' - backLink.href = '#' - backLink.innerHTML = ' ' - backLink.appendChild(document.createTextNode(lang('back'))) + var backLink = document.createElement('a') + backLink.className = 'back' + backLink.href = '#' + backLink.innerHTML = ' ' + backLink.appendChild(document.createTextNode(lang('back'))) - backLink.onclick = function () { - addCategoriesShow() - return false - } - content.appendChild(backLink) - - let h = document.createElement('h2') - h.appendChild(document.createTextNode(repoId)) - content.appendChild(h) - - list = repoData.categories - } else { - if (typeof repositoriesGitea === 'object' && repositoriesGitea.url) { - let a = document.createElement('a') - a.href = repositoriesGitea.url - a.target = '_blank' - a.innerHTML = lang('more_categories_gitea') - content.appendChild(a) - } - - list = weightSort(repoData, { - key: 'timestamp', - reverse: true - }) + backLink.onclick = function () { + addCategoriesList() + return false } + content.appendChild(backLink) + + let h = document.createElement('h2') + h.appendChild(document.createTextNode(repoId)) + content.appendChild(h) + + list = repoData.categories let menu = document.createElement('ul') menu.className = 'menu' content.appendChild(menu) - if (repo) { - let li = document.createElement('li') - menu.appendChild(li) + let li = document.createElement('li') + menu.appendChild(li) - let text = document.createElement('a') - text.innerHTML = lang('repo-use-as-base') - text.href = '#repo=' + repo - text.onclick = addCategoriesHide - li.appendChild(text) - } + let text = document.createElement('a') + text.innerHTML = lang('repo-use-as-base') + text.href = '#repo=' + repo + text.onclick = addCategoriesHide + li.appendChild(text) - if (repo) { - let li = document.createElement('li') - menu.appendChild(li) + li = document.createElement('li') + menu.appendChild(li) - let text = document.createElement('a') - text.innerHTML = lang('reload') - text.href = '#' - text.onclick = () => { - addCategoriesShow(repo, { force: true }) - } - li.appendChild(text) + text = document.createElement('a') + text.innerHTML = lang('reload') + text.href = '#' + text.onclick = () => { + addCategoriesShow(repo, { force: true }) } + li.appendChild(text) if ('branches' in repoData) { let li = document.createElement('li') @@ -136,7 +198,7 @@ function addCategoriesShow (repo, options={}) { } let header = document.createElement('h3') - header.innerHTML = lang(repo ? 'categories' : 'repositories') + ':' + header.innerHTML = lang('categories') + ':' content.appendChild(header) var ul = document.createElement('ul') @@ -149,20 +211,12 @@ function addCategoriesShow (repo, options={}) { repositoryUrl = OverpassLayer.twig.twig({ data: data.repositoryUrl, autoescape: true }) } - var li = document.createElement('li') + li = document.createElement('li') let a = document.createElement('a') - if (repo) { - a.href = '#categories=' + (repo === 'default' ? '' : repo + '/') + id - a.onclick = function () { - addCategoriesHide() - } - } else { - a.href = '#' - a.onclick = function (id) { - addCategoriesShow(id) - return false - }.bind(this, id) + a.href = '#categories=' + (repo === 'default' ? '' : repo + '/') + id + a.onclick = function () { + addCategoriesHide() } li.appendChild(a) @@ -210,7 +264,7 @@ register_hook('init', function (callback) { tab.on('select', () => { if (!initialized) { - addCategoriesShow() + addCategoriesList() initialized = true } }) From c8da00335ac0bf5cdb1ddc7e6410d4c357f80b68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Wed, 3 Aug 2022 20:32:20 +0200 Subject: [PATCH 10/76] OpenStreetBrowserLoader: register repositories --- src/OpenStreetBrowserLoader.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/OpenStreetBrowserLoader.js b/src/OpenStreetBrowserLoader.js index 7a39467e..8442e15d 100644 --- a/src/OpenStreetBrowserLoader.js +++ b/src/OpenStreetBrowserLoader.js @@ -276,4 +276,8 @@ class OpenStreetBrowserLoader { } } +OpenStreetBrowserLoader.prototype.registerRepository = function (id, repository) { + this.repositories[id] = repository +} + module.exports = new OpenStreetBrowserLoader() From 085814b1f1a747ecc95cf055c1901317b9089e38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Wed, 3 Aug 2022 20:32:44 +0200 Subject: [PATCH 11/76] customCategory: load categories when requested --- src/customCategory.js | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/customCategory.js b/src/customCategory.js index d20e3a2d..b1e7bdee 100644 --- a/src/customCategory.js +++ b/src/customCategory.js @@ -5,6 +5,28 @@ const md5 = require('md5') const OpenStreetBrowserLoader = require('./OpenStreetBrowserLoader') +class CustomCategoryRepository { + constructor () { + } + + load (callback) { + callback(null) + } + + clearCache () { + } + + getCategory (id, options, callback) { + ajax('customCategory', { id }, (result) => { + callback(null, yaml.load(result)) + }) + } + + getTemplate (id, options, callback) { + callback(null, '') + } +} + class CustomCategory { constructor () { } @@ -46,7 +68,7 @@ class CustomCategory { applyContent (content) { this.content = content - const id = 'custom:' + md5(content) + const id = 'custom/' + md5(content) const data = yaml.load(content) if (this.category) { @@ -102,3 +124,7 @@ module.exports = function customCategory (content) { div.appendChild(a) content.appendChild(div) } + +hooks.register('init', () => { + OpenStreetBrowserLoader.registerRepository('custom', new CustomCategoryRepository()) +}) From c29f924cd6e3691e97f18c0cac14755d01c8e605 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Thu, 4 Aug 2022 19:21:29 +0200 Subject: [PATCH 12/76] CustomCategory: test yaml for correctness --- src/customCategory.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/customCategory.js b/src/customCategory.js index b1e7bdee..06ec155d 100644 --- a/src/customCategory.js +++ b/src/customCategory.js @@ -59,6 +59,13 @@ class CustomCategory { this.modal.content.element.appendChild(input) input.onclick = () => { + try { + yaml.load(this.textarea.value) + } + catch (e) { + return global.alert(e) + } + this.applyContent(this.textarea.value) ajax('customCategory', { content: this.textarea.value }, (result) => {}) return true From 045ec83d81705c7c4e0fd6fd86f7decd18e31ade Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Thu, 4 Aug 2022 20:41:57 +0200 Subject: [PATCH 13/76] CustomCategory: cache categories --- src/customCategory.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/customCategory.js b/src/customCategory.js index 06ec155d..361e2414 100644 --- a/src/customCategory.js +++ b/src/customCategory.js @@ -5,6 +5,8 @@ const md5 = require('md5') const OpenStreetBrowserLoader = require('./OpenStreetBrowserLoader') +const cache = {} + class CustomCategoryRepository { constructor () { } @@ -17,6 +19,10 @@ class CustomCategoryRepository { } getCategory (id, options, callback) { + if (id in cache) { + return callback(null, yaml.load(cache[id])) + } + ajax('customCategory', { id }, (result) => { callback(null, yaml.load(result)) }) @@ -75,15 +81,16 @@ class CustomCategory { applyContent (content) { this.content = content - const id = 'custom/' + md5(content) + const id = md5(content) const data = yaml.load(content) + cache[id] = content if (this.category) { this.category.remove() this.category = null } - OpenStreetBrowserLoader.getCategoryFromData(id, {}, data, (err, category) => { + OpenStreetBrowserLoader.getCategoryFromData('custom/' + id, {}, data, (err, category) => { if (err) { return global.alert(err) } From 96ce746929b3b62dca8e9115f4e681d3aa82be91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Thu, 4 Aug 2022 20:44:02 +0200 Subject: [PATCH 14/76] CustomCategory: use Loader.getCategory() --- src/customCategory.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/customCategory.js b/src/customCategory.js index 361e2414..68fe8f8c 100644 --- a/src/customCategory.js +++ b/src/customCategory.js @@ -82,7 +82,6 @@ class CustomCategory { this.content = content const id = md5(content) - const data = yaml.load(content) cache[id] = content if (this.category) { @@ -90,14 +89,14 @@ class CustomCategory { this.category = null } - OpenStreetBrowserLoader.getCategoryFromData('custom/' + id, {}, data, (err, category) => { + OpenStreetBrowserLoader.getCategory('custom/' + id, {}, (err, category) => { if (err) { return global.alert(err) } this.category = category this.category.setParentDom(document.getElementById('contentListAddCategories')) - this.category.setMap(global.map) + if (this.category.tabEdit) { this.category.tools.remove(this.category.tabEdit) From 8ccaa41e2fa16410224f3256a68712ed8796c337 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Thu, 4 Aug 2022 21:01:21 +0200 Subject: [PATCH 15/76] CustomCategory: edit categories, regardless of they were interactively created or loaded via link --- src/customCategory.js | 64 +++++++++++++++++++++++++++++++++---------- 1 file changed, 49 insertions(+), 15 deletions(-) diff --git a/src/customCategory.js b/src/customCategory.js index 68fe8f8c..e7783d7b 100644 --- a/src/customCategory.js +++ b/src/customCategory.js @@ -6,6 +6,7 @@ const md5 = require('md5') const OpenStreetBrowserLoader = require('./OpenStreetBrowserLoader') const cache = {} +const customCategories = [] class CustomCategoryRepository { constructor () { @@ -35,6 +36,15 @@ class CustomCategoryRepository { class CustomCategory { constructor () { + customCategories.push(this) + } + + load (id, callback) { + this.id = id + ajax('customCategory', { id }, (result) => { + this.content = result + callback(null, result) + }) } edit () { @@ -82,6 +92,7 @@ class CustomCategory { this.content = content const id = md5(content) + this.id = id cache[id] = content if (this.category) { @@ -97,21 +108,6 @@ class CustomCategory { this.category = category this.category.setParentDom(document.getElementById('contentListAddCategories')) - - if (this.category.tabEdit) { - this.category.tools.remove(this.category.tabEdit) - } - - this.category.tabEdit = new tabs.Tab({ - id: 'edit' - }) - this.category.tools.add(this.category.tabEdit) - this.category.tabEdit.header.innerHTML = '' - this.category.tabEdit.on('select', () => { - this.category.tabEdit.unselect() - this.edit() - }) - this.category.open() }) } @@ -127,6 +123,23 @@ function createCustomCategory () { return false } +function editCustomCategory (id) { + let done = customCategories.filter(customCategory => { + if (customCategory.id === id) { + customCategory.edit() + return true + } + }) + + if (!done.length) { + const customCategory = new CustomCategory() + customCategory.load(id, (err) => { + if (err) { return global.alert(err) } + customCategory.edit() + }) + } +} + module.exports = function customCategory (content) { let div = document.createElement('div') @@ -141,3 +154,24 @@ module.exports = function customCategory (content) { hooks.register('init', () => { OpenStreetBrowserLoader.registerRepository('custom', new CustomCategoryRepository()) }) +register_hook('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.on('select', () => { + category.tabEdit.unselect() + editCustomCategory(id) + }) + } +}) From 3032b2cb64fb18c34c446c4a70eb23ed0ff1c800 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Fri, 5 Aug 2022 14:52:24 +0200 Subject: [PATCH 16/76] CustomCategory: styling modal window --- src/customCategory.js | 6 +++++- style.css | 16 ++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/customCategory.js b/src/customCategory.js index e7783d7b..28f8d16a 100644 --- a/src/customCategory.js +++ b/src/customCategory.js @@ -69,10 +69,14 @@ class CustomCategory { this.textarea.value = this.content } + const controls = document.createElement('div') + controls.className = 'controls' + this.modal.content.element.appendChild(controls) + const input = document.createElement('input') input.type = 'submit' input.value = lang('apply') - this.modal.content.element.appendChild(input) + controls.appendChild(input) input.onclick = () => { try { diff --git a/style.css b/style.css index 11161578..f513cf8e 100644 --- a/style.css +++ b/style.css @@ -612,3 +612,19 @@ ul.overpass-layer-list > li > a > .content > .details { margin-bottom: 0.25em; background-color: #ffdfdf; } + +.WindowModal-content { + height: 100%; + display: flex; + flex-direction: column; + align-content: stretch; +} +.WindowModal-content textarea { + height: 100%; + width: 100%; + resize: none; + box-sizing: border-box; +} +.WindowModal-content .controls { + flex-grow: 0; +} From bf7084a94b607311b299b4a572658e2a1208669d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Fri, 5 Aug 2022 14:52:51 +0200 Subject: [PATCH 17/76] CustomCategory: when editing a category loaded by link, change this category after save --- src/customCategory.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/customCategory.js b/src/customCategory.js index 28f8d16a..899a8502 100644 --- a/src/customCategory.js +++ b/src/customCategory.js @@ -127,7 +127,7 @@ function createCustomCategory () { return false } -function editCustomCategory (id) { +function editCustomCategory (id, category) { let done = customCategories.filter(customCategory => { if (customCategory.id === id) { customCategory.edit() @@ -139,6 +139,7 @@ function editCustomCategory (id) { const customCategory = new CustomCategory() customCategory.load(id, (err) => { if (err) { return global.alert(err) } + customCategory.category = category customCategory.edit() }) } @@ -175,7 +176,7 @@ register_hook('category-overpass-init', (category) => { category.tabEdit.header.innerHTML = '' category.tabEdit.on('select', () => { category.tabEdit.unselect() - editCustomCategory(id) + editCustomCategory(id, category) }) } }) From a5b172bda7cba3cd2101e0c1ceb5a8160c5e9ad7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Fri, 5 Aug 2022 15:30:59 +0200 Subject: [PATCH 18/76] CategoryAsYAML: improve documentation --- doc/CategoryAsYAML.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/doc/CategoryAsYAML.md b/doc/CategoryAsYAML.md index e326fa49..5e6596d1 100644 --- a/doc/CategoryAsYAML.md +++ b/doc/CategoryAsYAML.md @@ -1,9 +1,9 @@ Categories can be created as YAML files. This is much simpler as JSON files, because you don't have to add all these quotes, you can use multi-line strings and allows adding comments. -A simple example ([Source](https://www.openstreetbrowser.org/dev/OpenStreetBrowser/examples/src/branch/master/example1.yaml)). It queries nodes, ways and relations with amenity=restaurant from OpenStreetMap (via Overpass API), starting from zoom level 15. `nwr` is short for `(node[amenity=restaurant];way[amenity=restaurant];relation[amenity=restaurant];)`: +A simple example ([Source](https://www.openstreetbrowser.org/dev/OpenStreetBrowser/examples/src/branch/master/example1.yaml)). It queries nodes, ways and relations with amenity=restaurant from OpenStreetMap (via Overpass API), starting from zoom level 15. `nwr` is short for `(node[amenity=restaurant];way[amenity=restaurant];relation[amenity=restaurant];)`. Please note, that only a subset of OverpassQL is available (see [overpass-frontend](https://github.com/plepe/overpass-frontend) for details). ```yaml -# This is necessary, it tells OSB that this uses OverpassQL for queries. +# This is necessary, it tells OSB that this category is of type 'overpass'. An alternative would be 'index' (for directories). type: overpass # From zoom level 15 on, load all node, ways and relations with amenity=restaurant. query: @@ -206,3 +206,5 @@ const: default: color: '#ffff00' ``` + +All scripts of a feature are processed in the order of their appearance. As they all use the same scope, Twig variables (set via `{% set varname = 'value' %}`) are available in all sub-sequent scripts. From 382dafa6364b92796d5703ba6c37a3ad8f612d66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Fri, 5 Aug 2022 17:45:45 +0200 Subject: [PATCH 19/76] Replace 'window-modal' by a self-written class (with some copy&paste) --- package.json | 3 +- src/Window.js | 79 +++++++++++++++++++++++++++++++++++++++++++ src/customCategory.js | 22 ++++++------ style.css | 53 ++++++++++++++++++++++++----- 4 files changed, 136 insertions(+), 21 deletions(-) create mode 100644 src/Window.js diff --git a/package.json b/package.json index 41a61605..a19a8344 100644 --- a/package.json +++ b/package.json @@ -48,8 +48,7 @@ "query-string": "^6.13.8", "sheet-router": "^4.2.3", "sprintf-js": "^1.1.2", - "weight-sort": "^1.3.1", - "window-modal": "^1.0.5" + "weight-sort": "^1.3.1" }, "scripts": { "test": "mocha --bail", diff --git a/src/Window.js b/src/Window.js new file mode 100644 index 00000000..09822f99 --- /dev/null +++ b/src/Window.js @@ -0,0 +1,79 @@ +const EventEmitter = require('events') + +module.exports = class Window extends EventEmitter { + constructor (options) { + super() + + this.dom = document.createElement('div') + this.dom.className = 'Window' + + this.header = document.createElement('div') + this.header.className = 'header' + this.header.innerHTML = options.title + this.dom.appendChild(this.header) + + this.closeBtn = document.createElement('div') + this.closeBtn.className = 'closeBtn' + this.closeBtn.onclick = () => this.close() + this.header.appendChild(this.closeBtn) + + this.content = document.createElement('div') + this.content.className = 'content' + this.dom.appendChild(this.content) + + dragElement(this.dom) + } + + show () { + document.body.appendChild(this.dom) + this.emit('show') + } + + close () { + document.body.removeChild(this.dom) + this.emit('close') + } +} + +// copied from https://www.w3schools.com/HOWTO/howto_js_draggable.asp +// Make the DIV element draggable: +function dragElement(elmnt) { + var pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0; + if (elmnt.firstChild) { + // if present, the header is where you move the DIV from: + elmnt.firstChild.onmousedown = dragMouseDown; + } else { + // otherwise, move the DIV from anywhere inside the DIV: + elmnt.onmousedown = dragMouseDown; + } + + function dragMouseDown(e) { + e = e || window.event; + e.preventDefault(); + // get the mouse cursor position at startup: + pos3 = e.clientX; + pos4 = e.clientY; + document.onmouseup = closeDragElement; + // call a function whenever the cursor moves: + document.onmousemove = elementDrag; + } + + function elementDrag(e) { + e = e || window.event; + e.preventDefault(); + // calculate the new cursor position: + pos1 = pos3 - e.clientX; + pos2 = pos4 - e.clientY; + pos3 = e.clientX; + pos4 = e.clientY; + // set the element's new position: + elmnt.style.top = (elmnt.offsetTop - pos2) + "px"; + elmnt.style.left = (elmnt.offsetLeft - pos1) + "px"; + } + + function closeDragElement() { + // stop moving when mouse button is released: + document.onmouseup = null; + document.onmousemove = null; + } +} diff --git a/src/customCategory.js b/src/customCategory.js index 899a8502..087b74e7 100644 --- a/src/customCategory.js +++ b/src/customCategory.js @@ -1,8 +1,8 @@ -const ModalWindow = require('window-modal') const tabs = require('modulekit-tabs') const yaml = require('js-yaml') const md5 = require('md5') +const Window = require('./Window') const OpenStreetBrowserLoader = require('./OpenStreetBrowserLoader') const cache = {} @@ -48,30 +48,28 @@ class CustomCategory { } edit () { - if (this.modal) { - this.modal.focused = true + if (this.window) { + this.window.focused = true return } - this.modal = new ModalWindow({ - title: 'Custom Category', - hideMinimize: true, - zIndex: '99999' + this.window = new Window({ + title: 'Custom Category' }) - this.modal.addEventListener('close', () => { - this.modal = null + this.window.on('close', () => { + this.window = null }) this.textarea = document.createElement('textarea') - this.modal.content.element.appendChild(this.textarea) + this.window.content.appendChild(this.textarea) if (this.content !== undefined) { this.textarea.value = this.content } const controls = document.createElement('div') controls.className = 'controls' - this.modal.content.element.appendChild(controls) + this.window.content.appendChild(controls) const input = document.createElement('input') input.type = 'submit' @@ -90,6 +88,8 @@ class CustomCategory { ajax('customCategory', { content: this.textarea.value }, (result) => {}) return true } + + this.window.show() } applyContent (content) { diff --git a/style.css b/style.css index f513cf8e..a98e0520 100644 --- a/style.css +++ b/style.css @@ -613,18 +613,55 @@ ul.overpass-layer-list > li > a > .content > .details { background-color: #ffdfdf; } -.WindowModal-content { - height: 100%; - display: flex; - flex-direction: column; - align-content: stretch; -} -.WindowModal-content textarea { +.Window > .content textarea { height: 100%; width: 100%; resize: none; box-sizing: border-box; } -.WindowModal-content .controls { +.Window > .content .controls { + flex-grow: 0; +} + +/* Window */ +.Window { + position: absolute; + z-index: 99999; + background-color: #f1f1f1; + border: 1px solid #000000; + resize: both; + overflow: hidden; + width: min(60em, 80%); + height: min(30em, 60%); + left: 10%; + top: 10%; + + display: flex; + flex-direction: column; + align-content: stretch; +} +.Window > .header { + padding: 0.25em; + font-weight: bold; + cursor: move; + z-index: 100000; + background-color: #dfdfdf; + color: #000000; flex-grow: 0; + position: relative; +} +.Window > .header > .closeBtn { +} +.Window > .header > .closeBtn::before { + font-family: "Font Awesome 5 Free"; + content: "\f00d"; + position: absolute; + right: 0.25em; + top: 0.25em; +} +.Window > .content { + height: 100%; + display: flex; + flex-direction: column; + align-content: stretch; } From c630de8f1ed3979e1b1f20e9ec9b35a34c383e19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Fri, 5 Aug 2022 23:17:40 +0200 Subject: [PATCH 20/76] CustomCategory: record each access to a category (once per day and session) --- init.sql | 7 ++++++- src/customCategory.php | 31 ++++++++++++++++++++++++------- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/init.sql b/init.sql index 5476b34f..5731376c 100644 --- a/init.sql +++ b/init.sql @@ -2,6 +2,11 @@ create table customCategory ( id char(32) not null, content mediumtext not null, created datetime not null default CURRENT_TIMESTAMP, - lastAccess datetime not null default CURRENT_TIMESTAMP, primary key(id) ); + +create table customCategoryAccess ( + id char(32) not null, + ts datetime not null default CURRENT_TIMESTAMP, + foreign key(id) references customCategory(id) on delete cascade +); diff --git a/src/customCategory.php b/src/customCategory.php index cb3083ca..7b4b8ff6 100644 --- a/src/customCategory.php +++ b/src/customCategory.php @@ -14,10 +14,7 @@ function ajax_customCategory ($param) { $result = $row['content']; $stmt->closeCursor(); - $stmt = $db->prepare("update customCategory set lastAccess=:now where id=:id"); - $stmt->bindValue(':id', $param['id']); - $stmt->bindValue(':now', (new DateTime())->format('Y-m-d H:i:s'), PDO::PARAM_STR); - $stmt->execute(); + customCategoryUpdateAccess($param['id']); return $result; } @@ -28,12 +25,32 @@ function ajax_customCategory ($param) { if ($param['content']) { $id = md5($param['content']); - //$stmt = $db->prepare("insert into customCategory (id, content) values (:id, :content) on duplicate key update lastAccess=:now"); - $stmt = $db->prepare("insert into customCategory (id, content) values (:id, :content) on conflict(id) do update set lastAccess=:now"); + $stmt = $db->prepare("insert or ignore into customCategory (id, content) values (:id, :content)"); $stmt->bindValue(':id', $id, PDO::PARAM_STR); $stmt->bindValue(':content', $param['content'], PDO::PARAM_STR); - $stmt->bindValue(':now', (new DateTime())->format('Y-m-d H:i:s'), PDO::PARAM_STR); $result = $stmt->execute(); + + customCategoryUpdateAccess($id); + return $result; } } + +function customCategoryUpdateAccess ($id) { + global $db; + + if (!isset($_SESSION['customCategoryAccess'])) { + $_SESSION['customCategoryAccess'] = []; + } + + // update access per session only once a day + if (array_key_exists($id, $_SESSION['customCategoryAccess']) && $_SESSION['customCategoryAccess'][$id] > time() - 86400) { + return; + } + + $_SESSION['customCategoryAccess'][$id] = time(); + + $stmt = $db->prepare("insert into customCategoryAccess (id) values (:id)"); + $stmt->bindValue(':id', $id); + $stmt->execute(); +} From 8c151f88499da21e7a4bb5787ce0b2673fec691c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Fri, 5 Aug 2022 23:36:22 +0200 Subject: [PATCH 21/76] CustomCategory: add a share button which copies the link to the clipboard --- lang/en.json | 1 + src/customCategory.js | 21 +++++++++++++++++++++ style.css | 14 ++++++++++++++ 3 files changed, 36 insertions(+) diff --git a/lang/en.json b/lang/en.json index 7cdf7ca8..a63fc9c2 100644 --- a/lang/en.json +++ b/lang/en.json @@ -10,6 +10,7 @@ "default": "default", "apply": "Apply", "customCategory:create": "Create custom category", + "copied-clipboard": "Copied to clipboard", "edit": "edit", "editor:id": "iD (in-browser editor)", "editor:remote": "Remote Control (JOSM or Merkaator)", diff --git a/src/customCategory.js b/src/customCategory.js index 087b74e7..6dfbf82b 100644 --- a/src/customCategory.js +++ b/src/customCategory.js @@ -178,5 +178,26 @@ register_hook('category-overpass-init', (category) => { category.tabEdit.unselect() editCustomCategory(id, category) }) + + if (!category.tabShare) { + category.tabShare = new tabs.Tab({ + id: 'share', + weight: 10 + }) + category.tools.add(category.tabShare) + category.tabShare.header.innerHTML = '' + category.tabShare.header.className = 'share-button' + category.tabShare.on('select', () => { + category.tabShare.unselect() + const url = location.origin + location.pathname + '#categories=custom/' + id + 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) + }) + } } }) diff --git a/style.css b/style.css index a98e0520..ef60076f 100644 --- a/style.css +++ b/style.css @@ -665,3 +665,17 @@ ul.overpass-layer-list > li > a > .content > .details { flex-direction: column; align-content: stretch; } + +/* Copy to clipboard */ +.share-button { + position: relative; +} +.share-button > .notify { + position: absolute; + background: white; + border: 1px solid black; + border-radius: 0.25em; + width: 8em; + text-align: center; + z-index: 1; +} From d25f16329a1e2a8bce006ee79b5aef537825d332 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Sat, 6 Aug 2022 11:42:08 +0200 Subject: [PATCH 22/76] CustomCategory: print list of popular categories --- src/customCategory.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/customCategory.php b/src/customCategory.php index 7b4b8ff6..864a2fd3 100644 --- a/src/customCategory.php +++ b/src/customCategory.php @@ -6,6 +6,14 @@ function ajax_customCategory ($param) { return null; } + if (isset($param['list'])) { + $stmt = $db->prepare("select * from customCategory left join (select id, count(id) accessCount, max(ts) lastAccess from customCategoryAccess group by id) t on customCategory.id=t.id order by accessCount desc, created desc limit 25"); + $stmt->execute(); + $data = $stmt->fetchAll(PDO::FETCH_ASSOC); + $stmt->closeCursor(); + return $data; + } + if ($param['id']) { $stmt = $db->prepare("select content from customCategory where id=:id"); $stmt->bindValue(':id', $param['id'], PDO::PARAM_STR); From 785038e623537b71c5c94f849c0659a7e1322d11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Sun, 7 Aug 2022 07:40:42 +0200 Subject: [PATCH 23/76] repositoriesGitea: move code from repositories.php to separate module --- modulekit.php | 1 + src/repositories.php | 40 +----------------------------------- src/repositoriesGitea.php | 43 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 39 deletions(-) create mode 100644 src/repositoriesGitea.php diff --git a/modulekit.php b/modulekit.php index 59b1d8e8..6b3cf35f 100644 --- a/modulekit.php +++ b/modulekit.php @@ -23,6 +23,7 @@ $include = array( 'src/RepositoryDir.php', 'src/RepositoryGit.php', 'src/repositories.php', + 'src/repositoriesGitea.php', 'src/customCategory.php', ), 'css' => array( diff --git a/src/repositories.php b/src/repositories.php index 2a56b83f..40f83dce 100644 --- a/src/repositories.php +++ b/src/repositories.php @@ -1,7 +1,6 @@ "{$repositoriesGitea['path']}/{$f1}/{$f2}", - 'type' => 'git', - ); - - if (array_key_exists('url', $repositoriesGitea)) { - $r['repositoryUrl'] = "{$repositoriesGitea['url']}/{{ repositoryId }}"; - $r['categoryUrl'] = "{$repositoriesGitea['url']}/{{ repositoryId }}/src/branch/{{ branchId }}/{{ categoryId }}.json"; - } - - $result["{$f1}/{$f2id}"] = $r; - } - } - closedir($d2); - } - } - closedir($d1); - } + call_hooks("get-repositories", $result); return $result; } @@ -57,14 +30,3 @@ function getRepo ($repoId, $repoData) { return $repo; } - -register_hook('init', function () { - global $repositoriesGitea; - - if (isset($repositoriesGitea) && array_key_exists('url', $repositoriesGitea)) { - $d = array('repositoriesGitea' => array( - 'url' => $repositoriesGitea['url'], - )); - html_export_var($d); - } -}); diff --git a/src/repositoriesGitea.php b/src/repositoriesGitea.php new file mode 100644 index 00000000..81e94d4c --- /dev/null +++ b/src/repositoriesGitea.php @@ -0,0 +1,43 @@ + "{$repositoriesGitea['path']}/{$f1}/{$f2}", + 'type' => 'git', + ); + + if (array_key_exists('url', $repositoriesGitea)) { + $r['repositoryUrl'] = "{$repositoriesGitea['url']}/{{ repositoryId }}"; + $r['categoryUrl'] = "{$repositoriesGitea['url']}/{{ repositoryId }}/src/branch/{{ branchId }}/{{ categoryId }}.json"; + } + + $result["{$f1}/{$f2id}"] = $r; + } + } + closedir($d2); + } + } + closedir($d1); + } +}); + +register_hook('init', function () { + global $repositoriesGitea; + + if (isset($repositoriesGitea) && array_key_exists('url', $repositoriesGitea)) { + $d = array('repositoriesGitea' => array( + 'url' => $repositoriesGitea['url'], + )); + html_export_var($d); + } +}); From 2ce395b2af39f9cd9841bac58fee6717d05cb93e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Sun, 7 Aug 2022 07:52:12 +0200 Subject: [PATCH 24/76] Repositories: add a 'group' property (default|gitea) --- repo.php | 1 + src/repositoriesGitea.php | 1 + 2 files changed, 2 insertions(+) diff --git a/repo.php b/repo.php index 38f1b82f..f40a680a 100644 --- a/repo.php +++ b/repo.php @@ -26,6 +26,7 @@ if (!isset($_REQUEST['repo'])) { if (isset($repoData['categoryUrl'])) { $info['categoryUrl'] = $repoData['categoryUrl']; } + $info['group'] = $repoData['group'] ?? 'default'; print json_encode($info, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_FORCE_OBJECT); } diff --git a/src/repositoriesGitea.php b/src/repositoriesGitea.php index 81e94d4c..497e393d 100644 --- a/src/repositoriesGitea.php +++ b/src/repositoriesGitea.php @@ -14,6 +14,7 @@ register_hook("get-repositories", function ($result) { $r = array( 'path' => "{$repositoriesGitea['path']}/{$f1}/{$f2}", 'type' => 'git', + 'group' => 'gitea', ); if (array_key_exists('url', $repositoriesGitea)) { From cf1bedb5c176ed4aec970ca52f49b7a88c0fb7a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Sun, 7 Aug 2022 07:52:48 +0200 Subject: [PATCH 25/76] CustomCategory: add a tip to the tutorial to the edit window --- lang/en.json | 1 + src/customCategory.js | 8 ++++++++ style.css | 6 ++++++ 3 files changed, 15 insertions(+) diff --git a/lang/en.json b/lang/en.json index a63fc9c2..22f84b55 100644 --- a/lang/en.json +++ b/lang/en.json @@ -9,6 +9,7 @@ "closed": "closed", "default": "default", "apply": "Apply", + "tip-tutorial": "Check out the [Tutorial]", "customCategory:create": "Create custom category", "copied-clipboard": "Copied to clipboard", "edit": "edit", diff --git a/src/customCategory.js b/src/customCategory.js index 6dfbf82b..f74ace99 100644 --- a/src/customCategory.js +++ b/src/customCategory.js @@ -76,6 +76,14 @@ class CustomCategory { input.value = lang('apply') controls.appendChild(input) + 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 = () => { try { yaml.load(this.textarea.value) diff --git a/style.css b/style.css index ef60076f..9421763e 100644 --- a/style.css +++ b/style.css @@ -665,6 +665,12 @@ ul.overpass-layer-list > li > a > .content > .details { flex-direction: column; align-content: stretch; } +.tip-tutorial { + margin-left: 1em; +} +.tip-tutorial a { + text-decoration: underline; +} /* Copy to clipboard */ .share-button { From 4a8f7c02a40f6838a316ba1d99fc40e5d1105002 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Sun, 7 Aug 2022 14:48:14 +0200 Subject: [PATCH 26/76] More Categories: start re-design menu; include customCategory as module from index.js --- lang/en.json | 1 + src/addCategories.js | 5 ----- src/customCategory.js | 17 ++++++++++++----- src/index.js | 3 ++- src/moreCategories.js | 34 ++++++++++++++++++++++++++++++++++ 5 files changed, 49 insertions(+), 11 deletions(-) create mode 100644 src/moreCategories.js diff --git a/lang/en.json b/lang/en.json index 22f84b55..249f8fda 100644 --- a/lang/en.json +++ b/lang/en.json @@ -10,6 +10,7 @@ "default": "default", "apply": "Apply", "tip-tutorial": "Check out the [Tutorial]", + "customCategory:header": "Custom categories", "customCategory:create": "Create custom category", "copied-clipboard": "Copied to clipboard", "edit": "edit", diff --git a/src/addCategories.js b/src/addCategories.js index c0c0fe18..f32c075a 100644 --- a/src/addCategories.js +++ b/src/addCategories.js @@ -5,7 +5,6 @@ const tabs = require('modulekit-tabs') const weightSort = require('weight-sort') const OpenStreetBrowserLoader = require('./OpenStreetBrowserLoader') -const customCategory = require('./customCategory') let tab @@ -28,8 +27,6 @@ function addCategoriesList (options = {}) { var list = {} - customCategory(content) - if (typeof repositoriesGitea === 'object' && repositoriesGitea.url) { let a = document.createElement('a') a.href = repositoriesGitea.url @@ -121,8 +118,6 @@ function addCategoriesShow (repo, options={}) { var list = {} - customCategory(content) - var backLink = document.createElement('a') backLink.className = 'back' backLink.href = '#' diff --git a/src/customCategory.js b/src/customCategory.js index f74ace99..ca65ff6e 100644 --- a/src/customCategory.js +++ b/src/customCategory.js @@ -153,16 +153,23 @@ function editCustomCategory (id, category) { } } -module.exports = function customCategory (content) { - let div = document.createElement('div') +hooks.register('more-categories-index', (content) => { + let header = document.createElement('h4') + header.innerHTML = lang('customCategory:header') + content.appendChild(header) + + let ul = document.createElement('ul') + let li = document.createElement('li') let a = document.createElement('a') a.innerHTML = lang('customCategory:create') a.href = '#' a.onclick = createCustomCategory - div.appendChild(a) - content.appendChild(div) -} + li.appendChild(a) + ul.appendChild(li) + + content.appendChild(ul) +}) hooks.register('init', () => { OpenStreetBrowserLoader.registerRepository('custom', new CustomCategoryRepository()) diff --git a/src/index.js b/src/index.js index 8bb0a593..9740b29b 100644 --- a/src/index.js +++ b/src/index.js @@ -33,7 +33,7 @@ require('./markers') require('./categories') require('./wikipedia') require('./image') -require('./addCategories') +require('./moreCategories') require('./permalink') //require('./leaflet-geo-search') require('./nominatim-search') @@ -42,6 +42,7 @@ require('./GeoInfo') require('./PluginMeasure') require('./PluginGeoLocate') require('./tagsDisplay-tag2link') +require('./customCategory') const ObjectDisplay = require('./ObjectDisplay') let currentObjectDisplay = null diff --git a/src/moreCategories.js b/src/moreCategories.js new file mode 100644 index 00000000..b0e01770 --- /dev/null +++ b/src/moreCategories.js @@ -0,0 +1,34 @@ +const tabs = require('modulekit-tabs') + +let tab + +function moreCategoriesIndex () { + let content = tab.content + + content.innerHTML = '

' + lang('more_categories') + '

' + + const dom = document.createElement('div') + content.appendChild(dom) + + hooks.call('more-categories-index', dom) +} + +register_hook('init', function (callback) { + tab = new tabs.Tab({ + id: 'moreCategories' + }) + global.tabs.add(tab) + + tab.header.innerHTML = '' + tab.header.title = lang('more_categories') + + let initialized = false + + tab.on('select', () => { + if (!initialized) { + moreCategoriesIndex() + initialized = true + } + }) +}) + From 1f4cb8fab942a57647ecbfc3aa4b9cfb30b35afd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Sun, 7 Aug 2022 17:33:53 +0200 Subject: [PATCH 27/76] More Categories: use new 'browser' function for menu handling --- src/Browser.js | 49 +++++++++++++++++++++++++ src/addCategories.js | 84 +++++++++++++++++++------------------------ src/customCategory.js | 39 +++++++++++--------- src/index.js | 1 + src/moreCategories.js | 15 ++++---- 5 files changed, 118 insertions(+), 70 deletions(-) create mode 100644 src/Browser.js diff --git a/src/Browser.js b/src/Browser.js new file mode 100644 index 00000000..04b52a62 --- /dev/null +++ b/src/Browser.js @@ -0,0 +1,49 @@ +const EventEmitter = require('events') +const queryString = require('query-string') + +module.exports = class Browser extends EventEmitter { + constructor (id, dom) { + super() + + this.id = id + this.dom = dom + this.history = [] + } + + buildPage (parameters) { + this.clear() + + hooks.call('browser-' + this.id, this, parameters) + this.emit('buildPage', parameters) + this.parameters = parameters + } + + clear () { + while (this.dom.lastChild) { + this.dom.removeChild(this.dom.lastChild) + } + } + + catchLinks () { + const links = this.dom.getElementsByTagName('a') + Array.from(links).forEach(link => { + const href = link.getAttribute('href') + + if (href.substr(0, this.id.length + 2) === '#' + this.id + '?') { + link.onclick = () => { + this.history.push(this.parameters) + + const parameters = queryString.parse(href.substr(this.id.length + 2)) + this.buildPage(parameters) + + return false + } + } + }) + } + + close () { + this.clear() + this.emit('close') + } +} diff --git a/src/addCategories.js b/src/addCategories.js index f32c075a..57f05513 100644 --- a/src/addCategories.js +++ b/src/addCategories.js @@ -4,22 +4,19 @@ require('./addCategories.css') const tabs = require('modulekit-tabs') const weightSort = require('weight-sort') +const state = require('./state') const OpenStreetBrowserLoader = require('./OpenStreetBrowserLoader') let tab -function addCategoriesList (options = {}) { - let content = tab.content - - content.innerHTML = '

' + lang('more_categories') + '

' + ' ' + lang('loading') +function addCategoriesList (content, browser, options = {}) { + content.innerHTML = ' ' + lang('loading') OpenStreetBrowserLoader.getRepositoryList(options, function (err, repoData) { if (err) { return global.alert(err) } - content.innerHTML = '

' + lang('more_categories') + '

' - var categoryUrl = null if (repoData.categoryUrl) { categoryUrl = OverpassLayer.twig.twig({ data: repoData.categoryUrl, autoescape: true }) @@ -48,6 +45,10 @@ function addCategoriesList (options = {}) { header.innerHTML = lang('repositories') + ':' content.appendChild(header) + while (content.lastChild) { + content.removeChild(content.lastChild) + } + var ul = document.createElement('ul') for (var id in list) { @@ -61,11 +62,7 @@ function addCategoriesList (options = {}) { var li = document.createElement('li') let a = document.createElement('a') - a.href = '#' - a.onclick = function (id) { - addCategoriesShow(id) - return false - }.bind(this, id) + a.href = '#more-categories?id=' + id li.appendChild(a) a.appendChild(document.createTextNode('name' in data ? lang(data.name) : id)) @@ -88,11 +85,12 @@ function addCategoriesList (options = {}) { } content.appendChild(ul) + browser.catchLinks() }) } -function addCategoriesShow (repo, options={}) { - let content = tab.content +function addCategoriesShow (repo, browser, options={}) { + const content = browser.dom let [ repoId, branchId ] = repo.split(/~/) @@ -100,7 +98,7 @@ function addCategoriesShow (repo, options={}) { branchId = 'master' } - content.innerHTML = '

' + lang('more_categories') + '

' + ' ' + lang('loading') + content.innerHTML = ' ' + lang('loading') OpenStreetBrowserLoader.getRepository(repo, options, function (err, repository) { if (err) { @@ -109,7 +107,7 @@ function addCategoriesShow (repo, options={}) { const repoData = repository.data - content.innerHTML = '

' + lang('more_categories') + '

' + content.innerHTML = '' var categoryUrl = null if (repoData.categoryUrl) { @@ -120,15 +118,11 @@ function addCategoriesShow (repo, options={}) { var backLink = document.createElement('a') backLink.className = 'back' - backLink.href = '#' + backLink.href = '#more-categories?' backLink.innerHTML = ' ' backLink.appendChild(document.createTextNode(lang('back'))) - - backLink.onclick = function () { - addCategoriesList() - return false - } content.appendChild(backLink) + browser.catchLinks() let h = document.createElement('h2') h.appendChild(document.createTextNode(repoId)) @@ -146,7 +140,7 @@ function addCategoriesShow (repo, options={}) { let text = document.createElement('a') text.innerHTML = lang('repo-use-as-base') text.href = '#repo=' + repo - text.onclick = addCategoriesHide + text.onclick = () => browser.close() li.appendChild(text) li = document.createElement('li') @@ -154,10 +148,7 @@ function addCategoriesShow (repo, options={}) { text = document.createElement('a') text.innerHTML = lang('reload') - text.href = '#' - text.onclick = () => { - addCategoriesShow(repo, { force: true }) - } + text.href = '#more-categories?id=' + repo + '&force=true' li.appendChild(text) if ('branches' in repoData) { @@ -210,9 +201,7 @@ function addCategoriesShow (repo, options={}) { let a = document.createElement('a') a.href = '#categories=' + (repo === 'default' ? '' : repo + '/') + id - a.onclick = function () { - addCategoriesHide() - } + a.onclick = () => browser.close() li.appendChild(a) a.appendChild(document.createTextNode('name' in data ? lang(data.name) : id)) @@ -239,28 +228,29 @@ function addCategoriesShow (repo, options={}) { } content.appendChild(ul) + browser.catchLinks() }) } -function addCategoriesHide () { - tab.unselect() -} +hooks.register('browser-more-categories', (browser, parameters) => { + const content = browser.dom -register_hook('init', function (callback) { - tab = new tabs.Tab({ - id: 'addCategories' - }) - global.tabs.add(tab) - - tab.header.innerHTML = '' - tab.header.title = lang('more_categories') + if (!Object.keys(parameters).length) { + let header = document.createElement('h4') + header.innerHTML = lang('repositories') + content.appendChild(header) - let initialized = false + let div = document.createElement('div') + content.appendChild(div) + addCategoriesList(div, browser, parameters) - tab.on('select', () => { - if (!initialized) { - addCategoriesList() - initialized = true - } - }) + browser.catchLinks() + } + else if (parameters.id) { + addCategoriesShow(parameters.id, browser, parameters) + } + else if (parameters.repo || parameters.categories) { + state.apply(parameters) + browser.close() + } }) diff --git a/src/customCategory.js b/src/customCategory.js index ca65ff6e..fc5e1afd 100644 --- a/src/customCategory.js +++ b/src/customCategory.js @@ -153,22 +153,29 @@ function editCustomCategory (id, category) { } } -hooks.register('more-categories-index', (content) => { - let header = document.createElement('h4') - header.innerHTML = lang('customCategory:header') - content.appendChild(header) - - let ul = document.createElement('ul') - let li = document.createElement('li') - - let a = document.createElement('a') - a.innerHTML = lang('customCategory:create') - a.href = '#' - a.onclick = createCustomCategory - li.appendChild(a) - ul.appendChild(li) - - content.appendChild(ul) +hooks.register('browser-more-categories', (browser, parameters) => { + const content = browser.dom + + if (!Object.keys(parameters).length) { + let header = document.createElement('h4') + header.innerHTML = lang('customCategory:header') + content.appendChild(header) + + let ul = document.createElement('ul') + let li = document.createElement('li') + + let a = document.createElement('a') + a.innerHTML = lang('customCategory:create') + a.href = '#' + a.onclick = () => { + createCustomCategory() + browser.close() + } + li.appendChild(a) + ul.appendChild(li) + + content.appendChild(ul) + } }) hooks.register('init', () => { diff --git a/src/index.js b/src/index.js index 9740b29b..64e603c9 100644 --- a/src/index.js +++ b/src/index.js @@ -34,6 +34,7 @@ require('./categories') require('./wikipedia') require('./image') require('./moreCategories') +require('./addCategories') require('./permalink') //require('./leaflet-geo-search') require('./nominatim-search') diff --git a/src/moreCategories.js b/src/moreCategories.js index b0e01770..208801b5 100644 --- a/src/moreCategories.js +++ b/src/moreCategories.js @@ -1,5 +1,7 @@ const tabs = require('modulekit-tabs') +const Browser = require('./Browser') + let tab function moreCategoriesIndex () { @@ -10,7 +12,10 @@ function moreCategoriesIndex () { const dom = document.createElement('div') content.appendChild(dom) - hooks.call('more-categories-index', dom) + const browser = new Browser('more-categories', dom) + browser.buildPage({}) + + browser.on('close', () => tab.unselect()) } register_hook('init', function (callback) { @@ -22,13 +27,9 @@ register_hook('init', function (callback) { tab.header.innerHTML = '' tab.header.title = lang('more_categories') - let initialized = false - tab.on('select', () => { - if (!initialized) { - moreCategoriesIndex() - initialized = true - } + tab.content.innerHTML = '' + moreCategoriesIndex() }) }) From ed66265eb34ca53664c5a090f38642134d6edeaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Sun, 7 Aug 2022 18:55:11 +0200 Subject: [PATCH 28/76] Browser: sort children by weight --- src/Browser.js | 4 ++++ src/addCategories.js | 8 ++++++-- src/customCategory.js | 8 ++++++-- src/domSort.js | 7 +++++++ 4 files changed, 23 insertions(+), 4 deletions(-) create mode 100644 src/domSort.js diff --git a/src/Browser.js b/src/Browser.js index 04b52a62..4dc37a2f 100644 --- a/src/Browser.js +++ b/src/Browser.js @@ -1,6 +1,8 @@ const EventEmitter = require('events') const queryString = require('query-string') +const domSort = require('./domSort') + module.exports = class Browser extends EventEmitter { constructor (id, dom) { super() @@ -16,6 +18,8 @@ module.exports = class Browser extends EventEmitter { hooks.call('browser-' + this.id, this, parameters) this.emit('buildPage', parameters) this.parameters = parameters + + domSort(this.dom) } clear () { diff --git a/src/addCategories.js b/src/addCategories.js index 57f05513..f9ea0184 100644 --- a/src/addCategories.js +++ b/src/addCategories.js @@ -236,12 +236,16 @@ hooks.register('browser-more-categories', (browser, parameters) => { const content = browser.dom if (!Object.keys(parameters).length) { + let block = document.createElement('div') + block.setAttribute('weight', 1) + content.appendChild(block) + let header = document.createElement('h4') header.innerHTML = lang('repositories') - content.appendChild(header) + block.appendChild(header) let div = document.createElement('div') - content.appendChild(div) + block.appendChild(div) addCategoriesList(div, browser, parameters) browser.catchLinks() diff --git a/src/customCategory.js b/src/customCategory.js index fc5e1afd..92abb0a7 100644 --- a/src/customCategory.js +++ b/src/customCategory.js @@ -157,9 +157,13 @@ hooks.register('browser-more-categories', (browser, parameters) => { const content = browser.dom if (!Object.keys(parameters).length) { + let block = document.createElement('div') + block.setAttribute('weight', 0) + content.appendChild(block) + let header = document.createElement('h4') header.innerHTML = lang('customCategory:header') - content.appendChild(header) + block.appendChild(header) let ul = document.createElement('ul') let li = document.createElement('li') @@ -174,7 +178,7 @@ hooks.register('browser-more-categories', (browser, parameters) => { li.appendChild(a) ul.appendChild(li) - content.appendChild(ul) + block.appendChild(ul) } }) diff --git a/src/domSort.js b/src/domSort.js new file mode 100644 index 00000000..647c3cab --- /dev/null +++ b/src/domSort.js @@ -0,0 +1,7 @@ +module.exports = function (dom, attribute='weight') { + const list = Array.from(dom.children).sort( + (a, b) => (a.getAttribute(attribute) || 0) - (b.getAttribute(attribute) || 0) + ) + + list.forEach(el => dom.appendChild(el)) +} From 2696d08c6953dc7cefb578b926c7494afaf32122 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Sun, 7 Aug 2022 19:51:39 +0200 Subject: [PATCH 30/76] CustomCategory: add popularity column with declining relevance --- src/customCategory.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/customCategory.php b/src/customCategory.php index 864a2fd3..dc4b1427 100644 --- a/src/customCategory.php +++ b/src/customCategory.php @@ -7,9 +7,16 @@ function ajax_customCategory ($param) { } if (isset($param['list'])) { - $stmt = $db->prepare("select * from customCategory left join (select id, count(id) accessCount, max(ts) lastAccess from customCategoryAccess group by id) t on customCategory.id=t.id order by accessCount desc, created desc limit 25"); + // the popularity column counts every acess with declining value over time, + // it halves every year. + $stmt = $db->prepare("select customCategory.id, customCategory.created, customCategory.content, t.accessCount, t.popularity, t.lastAccess from customCategory left join (select id, count(id) accessCount, sum(1/((julianday('2023-08-06 00:00:00') - julianday(ts))/365.25 + 1)) popularity, max(ts) lastAccess from customCategoryAccess group by id) t on customCategory.id=t.id order by popularity desc, created desc limit 25"); $stmt->execute(); $data = $stmt->fetchAll(PDO::FETCH_ASSOC); + $data = array_map(function ($d) { + $d['popularity'] = (float)$d['popularity']; + $d['accessCount'] = (int)$d['accessCount']; + return $d; + }, $data); $stmt->closeCursor(); return $data; } From 832e4c67763de30a482076272c764e21d2f261d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Sun, 7 Aug 2022 20:31:05 +0200 Subject: [PATCH 31/76] CustomCategory: list popular categories --- lang/en.json | 1 + src/customCategory.js | 42 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/lang/en.json b/lang/en.json index 249f8fda..c01dd9ab 100644 --- a/lang/en.json +++ b/lang/en.json @@ -12,6 +12,7 @@ "tip-tutorial": "Check out the [Tutorial]", "customCategory:header": "Custom categories", "customCategory:create": "Create custom category", + "customCategory:list": "List popular custom categories", "copied-clipboard": "Copied to clipboard", "edit": "edit", "editor:id": "iD (in-browser editor)", diff --git a/src/customCategory.js b/src/customCategory.js index 92abb0a7..bdc175f9 100644 --- a/src/customCategory.js +++ b/src/customCategory.js @@ -166,8 +166,8 @@ hooks.register('browser-more-categories', (browser, parameters) => { block.appendChild(header) let ul = document.createElement('ul') - let li = document.createElement('li') + let li = document.createElement('li') let a = document.createElement('a') a.innerHTML = lang('customCategory:create') a.href = '#' @@ -178,10 +178,50 @@ hooks.register('browser-more-categories', (browser, parameters) => { 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') + + ajax('customCategory', { 'list': true }, (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.id)) + li.appendChild(a) + + const edit = document.createElement('a') + edit.onclick = () => editCustomCategory(cat.id) + edit.innerHTML = ' ' + li.appendChild(edit) + + ul.appendChild(li) + }) + + browser.catchLinks() + }) +} + hooks.register('init', () => { OpenStreetBrowserLoader.registerRepository('custom', new CustomCategoryRepository()) }) From 0660c738a2dd17c9cad98a0f0305199ff0814be9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Mon, 8 Aug 2022 18:40:38 +0200 Subject: [PATCH 32/76] CustomCategory: code-cleanup, use e.preventDefault() --- src/customCategory.js | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/src/customCategory.js b/src/customCategory.js index bdc175f9..f469e7c4 100644 --- a/src/customCategory.js +++ b/src/customCategory.js @@ -84,7 +84,7 @@ class CustomCategory { tutorial.innerHTML = text controls.appendChild(tutorial) - input.onclick = () => { + input.onclick = (e) => { try { yaml.load(this.textarea.value) } @@ -94,7 +94,7 @@ class CustomCategory { this.applyContent(this.textarea.value) ajax('customCategory', { content: this.textarea.value }, (result) => {}) - return true + e.preventDefault() } this.window.show() @@ -126,15 +126,6 @@ class CustomCategory { } -function createCustomCategory () { - let category - - category = new CustomCategory() - category.edit() - - return false -} - function editCustomCategory (id, category) { let done = customCategories.filter(customCategory => { if (customCategory.id === id) { @@ -171,9 +162,11 @@ hooks.register('browser-more-categories', (browser, parameters) => { let a = document.createElement('a') a.innerHTML = lang('customCategory:create') a.href = '#' - a.onclick = () => { - createCustomCategory() + a.onclick = (e) => { + const category = new CustomCategory() + category.edit() browser.close() + e.preventDefault() } li.appendChild(a) ul.appendChild(li) @@ -211,7 +204,10 @@ function customCategoriesList (browser, options) { li.appendChild(a) const edit = document.createElement('a') - edit.onclick = () => editCustomCategory(cat.id) + edit.onclick = (e) => { + editCustomCategory(cat.id) + e.preventDefault() + } edit.innerHTML = ' ' li.appendChild(edit) @@ -225,7 +221,8 @@ function customCategoriesList (browser, options) { hooks.register('init', () => { OpenStreetBrowserLoader.registerRepository('custom', new CustomCategoryRepository()) }) -register_hook('category-overpass-init', (category) => { + +hooks.register('category-overpass-init', (category) => { const m = category.id.match(/^custom\/(.*)$/) if (m) { const id = m[1] From 30b16cf68f260dd703ec00446b4b9f0d01d1ec2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Mon, 8 Aug 2022 18:50:07 +0200 Subject: [PATCH 33/76] Window: raise window when clicked; re-focus current element --- src/Window.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/Window.js b/src/Window.js index 09822f99..59ca371b 100644 --- a/src/Window.js +++ b/src/Window.js @@ -14,7 +14,10 @@ module.exports = class Window extends EventEmitter { this.closeBtn = document.createElement('div') this.closeBtn.className = 'closeBtn' - this.closeBtn.onclick = () => this.close() + this.closeBtn.onclick = (e) => { + this.close() + e.stopImmediatePropagation() + } this.header.appendChild(this.closeBtn) this.content = document.createElement('div') @@ -22,6 +25,15 @@ module.exports = class Window extends EventEmitter { this.dom.appendChild(this.content) dragElement(this.dom) + + this.dom.onclick = () => { + const activeEl = document.activeElement + + if (document.body.lastElementChild !== this.dom) { + document.body.appendChild(this.dom) + activeEl.focus() + } + } } show () { From ae5370dda2457b75b14cbc915323313dede513ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Mon, 8 Aug 2022 18:50:31 +0200 Subject: [PATCH 34/76] Browser: bugfix if a link has no href --- src/Browser.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Browser.js b/src/Browser.js index 4dc37a2f..d372426e 100644 --- a/src/Browser.js +++ b/src/Browser.js @@ -33,7 +33,7 @@ module.exports = class Browser extends EventEmitter { Array.from(links).forEach(link => { const href = link.getAttribute('href') - if (href.substr(0, this.id.length + 2) === '#' + this.id + '?') { + if (href && href.substr(0, this.id.length + 2) === '#' + this.id + '?') { link.onclick = () => { this.history.push(this.parameters) From 228a24a506a7fcc9f9bf3b3cd9298ea13544d445 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Mon, 8 Aug 2022 21:03:22 +0200 Subject: [PATCH 35/76] CustomCategory: when no name set, use 'Custom ID' as name --- src/customCategory.js | 10 ++++++++-- src/customCategory.php | 11 +++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/customCategory.js b/src/customCategory.js index f469e7c4..3b79194c 100644 --- a/src/customCategory.js +++ b/src/customCategory.js @@ -25,7 +25,13 @@ class CustomCategoryRepository { } ajax('customCategory', { id }, (result) => { - callback(null, yaml.load(result)) + const data = yaml.load(result) + + if (Object.is(data) && !('name' in data)) { + data.name = 'Custom ' + id.substr(0, 6) + } + + callback(null, data) }) } @@ -200,7 +206,7 @@ function customCategoriesList (browser, options) { const a = document.createElement('a') a.href = '#categories=custom/' + cat.id - a.appendChild(document.createTextNode(cat.id)) + a.appendChild(document.createTextNode(cat.name)) li.appendChild(a) const edit = document.createElement('a') diff --git a/src/customCategory.php b/src/customCategory.php index dc4b1427..388d78f0 100644 --- a/src/customCategory.php +++ b/src/customCategory.php @@ -15,8 +15,19 @@ function ajax_customCategory ($param) { $data = array_map(function ($d) { $d['popularity'] = (float)$d['popularity']; $d['accessCount'] = (int)$d['accessCount']; + + $content = yaml_parse($d['content']); + if ($content && is_array($content) && array_key_exists('name', $content)) { + $d['name'] = lang($content['name']); + } + else { + $d['name'] = 'Custom ' . substr($d['id'], 0, 6); + } + + unset($d['content']); return $d; }, $data); + $stmt->closeCursor(); return $data; } From bf8623de798032e3b512b9f29623697244fe9e2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Mon, 8 Aug 2022 21:04:49 +0200 Subject: [PATCH 36/76] CustomCategory: catch errors when loading; add to cache --- src/customCategory.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/customCategory.js b/src/customCategory.js index 3b79194c..387b2dff 100644 --- a/src/customCategory.js +++ b/src/customCategory.js @@ -25,7 +25,15 @@ class CustomCategoryRepository { } ajax('customCategory', { id }, (result) => { - const data = yaml.load(result) + let data + cache[id] = result + + try { + data = yaml.load(result) + } + catch (e) { + return global.alert(e) + } if (Object.is(data) && !('name' in data)) { data.name = 'Custom ' + id.substr(0, 6) From f6908492d59a1063369fe59dbf58347545ef0236 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Tue, 9 Aug 2022 23:28:28 +0200 Subject: [PATCH 37/76] CustomCategory: move testing validity of content to separate function --- src/customCategory.js | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/customCategory.js b/src/customCategory.js index 387b2dff..b4c4ac2d 100644 --- a/src/customCategory.js +++ b/src/customCategory.js @@ -99,11 +99,9 @@ class CustomCategory { controls.appendChild(tutorial) input.onclick = (e) => { - try { - yaml.load(this.textarea.value) - } - catch (e) { - return global.alert(e) + const err = customCategoryTest(this.textarea.value) + if (err) { + return global.alert(err) } this.applyContent(this.textarea.value) @@ -278,3 +276,13 @@ hooks.register('category-overpass-init', (category) => { } } }) + +function customCategoryTest (value) { + let data + try { + data = yaml.load(value) + } + catch (e) { + return e + } +} From e03a17756b6dc623274d9d9c48618b0f76c3966c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Tue, 9 Aug 2022 23:52:26 +0200 Subject: [PATCH 38/76] CustomCategory: also try to compile twig templates --- src/customCategory.js | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/customCategory.js b/src/customCategory.js index b4c4ac2d..a315cbf4 100644 --- a/src/customCategory.js +++ b/src/customCategory.js @@ -1,6 +1,7 @@ const tabs = require('modulekit-tabs') const yaml = require('js-yaml') const md5 = require('md5') +const OverpassLayer = require('overpass-layer') const Window = require('./Window') const OpenStreetBrowserLoader = require('./OpenStreetBrowserLoader') @@ -278,6 +279,10 @@ hooks.register('category-overpass-init', (category) => { }) function customCategoryTest (value) { + if (!value) { + return new Error('Empty category') + } + let data try { data = yaml.load(value) @@ -285,4 +290,39 @@ function customCategoryTest (value) { catch (e) { return e } + + const fields = ['feature', 'memberFeature'] + for (let i1 = 0; i1 < fields.length; i1++) { + const k1 = fields[i1] + if (data[k1]) { + for (k2 in data[k1]) { + const err = customCategoryTestCompile(data[k1][k2]) + if (err) { + return new Error('Compiling /' + k1 + '/' + k2 + ': ' + err.message) + } + + if (k2 === 'style' || k2.match(/^style:/)) { + for (const k3 in data[k1][k2]) { + const err = customCategoryTestCompile(data[k1][k2][k3]) + if (err) { + return new Error('Compiling /' + k1 + '/' + k2 + '/' + k3 + ': ' + err.message) + } + } + } + } + } + } +} + +function customCategoryTestCompile (data) { + if (typeof data !== 'string' || data.search('{') === -1) { + return + } + + try { + OverpassLayer.twig.twig({ data }) + } + catch (e) { + return e + } } From 16f92880ccd8b26f00679aaa8ec1c9c31e7b7d5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Wed, 10 Aug 2022 17:34:48 +0200 Subject: [PATCH 39/76] CustomCategory: Add a 'clone' button to other categories --- src/customCategory.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/customCategory.js b/src/customCategory.js index a315cbf4..be16c518 100644 --- a/src/customCategory.js +++ b/src/customCategory.js @@ -275,6 +275,28 @@ hooks.register('category-overpass-init', (category) => { 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.on('select', () => { + const clone = new CustomCategory() + OpenStreetBrowserLoader.getFile(category.id, {}, + (err, result) => { + if (err) { return global.alert(err) } + clone.content = yaml.dump(result) + clone.edit() + } + ) + }) + } }) From 77e0d8658d67ba8da17cce5ff2004a0d43184903 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Wed, 10 Aug 2022 18:48:20 +0200 Subject: [PATCH 40/76] repo.php: parameter 'file': get raw data of a file from a repository --- repo.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/repo.php b/repo.php index f40a680a..1e4e5111 100644 --- a/repo.php +++ b/repo.php @@ -60,6 +60,13 @@ if ($branchId) { } } +if (array_key_exists('file', $_REQUEST)) { + $file = $repo->file_get_contents($_REQUEST['file']); + Header("Content-Type: text/plain; charset=utf/8"); + print $file; + exit(0); +} + $cacheDir = null; $ts = $repo->timestamp($path); if (isset($config['cache'])) { From c74858af68bfb924a8bbc71b1d5f89fd2a0a4625 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Thu, 11 Aug 2022 10:45:15 +0200 Subject: [PATCH 41/76] Repository*: add a 'fileName' property --- src/RepositoryDir.php | 2 ++ src/RepositoryGit.php | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/RepositoryDir.php b/src/RepositoryDir.php index 07e33667..08f23fd0 100644 --- a/src/RepositoryDir.php +++ b/src/RepositoryDir.php @@ -34,6 +34,7 @@ class RepositoryDir extends RepositoryBase { if (preg_match("/^([0-9a-zA-Z_\-]+)\.json$/", $f, $m) && $f !== 'package.json') { $d1 = json_decode(file_get_contents("{$this->path}/{$f}"), true); $d1['format'] = 'json'; + $d1['fileName'] = $f; if (!$this->isCategory($d1)) { continue; @@ -45,6 +46,7 @@ class RepositoryDir extends RepositoryBase { if (preg_match("/^([0-9a-zA-Z_\-]+)\.yaml$/", $f, $m)) { $d1 = yaml_parse(file_get_contents("{$this->path}/{$f}")); $d1['format'] = 'yaml'; + $d1['fileName'] = $f; if (!$this->isCategory($d1)) { continue; diff --git a/src/RepositoryGit.php b/src/RepositoryGit.php index 6a95c89e..536e9072 100644 --- a/src/RepositoryGit.php +++ b/src/RepositoryGit.php @@ -72,6 +72,7 @@ class RepositoryGit extends RepositoryBase { $d1 = json_decode(shell_exec("cd " . escapeShellArg($this->path) . "; git show {$this->branchEsc}:" . escapeShellArg($f)), true); $d1['format'] = 'json'; + $d1['fileName'] = $f; if (!$this->isCategory($d1)) { continue; @@ -86,6 +87,7 @@ class RepositoryGit extends RepositoryBase { $d1 = yaml_parse(shell_exec("cd " . escapeShellArg($this->path) . "; git show {$this->branchEsc}:" . escapeShellArg($f))); $d1['format'] = 'yaml'; + $d1['fileName'] = $f; if (!$this->isCategory($d1)) { continue; From 5af57bb249fa7d41801f80d83a582a6673a25c52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Thu, 11 Aug 2022 10:59:34 +0200 Subject: [PATCH 42/76] RepositoryDir.file_get_contents(): return values - false=access denied - null=does not exist --- src/RepositoryDir.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/RepositoryDir.php b/src/RepositoryDir.php index 08f23fd0..769c59b8 100644 --- a/src/RepositoryDir.php +++ b/src/RepositoryDir.php @@ -82,13 +82,17 @@ class RepositoryDir extends RepositoryBase { function file_get_contents ($file) { if (substr($file, 0, 1) === '.' || preg_match("/\/\./", $file)) { - return null; + return false; } if (!$this->access($file)) { return false; } + if (!file_exists("{$this->path}/{$file}")) { + return null; + } + return file_get_contents("{$this->path}/{$file}"); } From 573e94f7a7dc24f9988129f4fbf8394ff838b8fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Thu, 11 Aug 2022 11:00:19 +0200 Subject: [PATCH 43/76] repo.php: parameter file: load raw file from repository --- repo.php | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/repo.php b/repo.php index 1e4e5111..c58ab485 100644 --- a/repo.php +++ b/repo.php @@ -62,8 +62,20 @@ if ($branchId) { if (array_key_exists('file', $_REQUEST)) { $file = $repo->file_get_contents($_REQUEST['file']); - Header("Content-Type: text/plain; charset=utf/8"); - print $file; + + if ($file === false) { + Header("HTTP/1.1 403 Forbidden"); + print "Access denied."; + } + else if ($file === null) { + Header("HTTP/1.1 404 File not found"); + print "File not found."; + } + else { + Header("Content-Type: text/plain; charset=utf-8"); + print $file; + } + exit(0); } From 8cc518bf5ab7b2bc5b9c2efd02a196a896199394 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Thu, 11 Aug 2022 11:18:55 +0200 Subject: [PATCH 44/76] Repository(JS): add a file_get_contents() function --- src/Repository.js | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/Repository.js b/src/Repository.js index bea94fa0..9f951665 100644 --- a/src/Repository.js +++ b/src/Repository.js @@ -10,6 +10,27 @@ module.exports = class Repository { } } + file_get_contents (fileName, options, callback) { + let param = [] + param.push('repo=' + encodeURIComponent(this.id)) + param.push('file=' + encodeURIComponent(fileName)) + param.push(config.categoriesRev) + param = param.length ? '?' + param.join('&') : '' + + fetch('repo.php' + param) + .then(res => res.text()) + .then(data => { + global.setTimeout(() => { + callback(null, data) + }, 0) + }) + .catch(err => { + global.setTimeout(() => { + callback(err) + }, 0) + }) + } + load (callback) { if (this.loadCallbacks) { return this.loadCallbacks.push(callback) From 3501f1bf3d3879af04a53f851c3b898a8db75c5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Thu, 11 Aug 2022 11:19:15 +0200 Subject: [PATCH 45/76] CustomCategory: clone other categories --- src/customCategory.js | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/customCategory.js b/src/customCategory.js index be16c518..d19ee70f 100644 --- a/src/customCategory.js +++ b/src/customCategory.js @@ -2,6 +2,7 @@ const tabs = require('modulekit-tabs') const yaml = require('js-yaml') const md5 = require('md5') const OverpassLayer = require('overpass-layer') +const jsonMultilineStrings = require('json-multiline-strings') const Window = require('./Window') const OpenStreetBrowserLoader = require('./OpenStreetBrowserLoader') @@ -285,13 +286,20 @@ hooks.register('category-overpass-init', (category) => { weight: 9 }) category.tools.add(category.tabClone) - category.tabClone.header.innerHTML = '' + category.tabClone.header.innerHTML = '' category.tabClone.on('select', () => { + category.tabClone.unselect() + const clone = new CustomCategory() - OpenStreetBrowserLoader.getFile(category.id, {}, - (err, result) => { - if (err) { return global.alert(err) } - clone.content = yaml.dump(result) + category.repository.file_get_contents(category.data.fileName, {}, + (err, content) => { + if (category.data.format === 'json') { + content = JSON.parse(content) + content = jsonMultilineStrings.join(content, { exclude: [ 'const', 'filter'] }) + content = yaml.dump(content) + } + + clone.content = content clone.edit() } ) From 3752208c4349cd5cf025e7f19a1869ff0dce412f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Thu, 11 Aug 2022 19:04:06 +0200 Subject: [PATCH 46/76] CustomCategory: when cloning a category, close the original --- src/customCategory.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/customCategory.js b/src/customCategory.js index d19ee70f..3ea5ec44 100644 --- a/src/customCategory.js +++ b/src/customCategory.js @@ -117,6 +117,10 @@ class CustomCategory { applyContent (content) { this.content = content + if (this.textarea) { + this.textarea.value = content + } + const id = md5(content) this.id = id cache[id] = content @@ -291,6 +295,8 @@ hooks.register('category-overpass-init', (category) => { category.tabClone.unselect() const clone = new CustomCategory() + clone.edit() + category.repository.file_get_contents(category.data.fileName, {}, (err, content) => { if (category.data.format === 'json') { @@ -299,8 +305,8 @@ hooks.register('category-overpass-init', (category) => { content = yaml.dump(content) } - clone.content = content - clone.edit() + clone.applyContent(content) + category.close() } ) }) From a73e9060a6edb905e670ad5a8836fce5f7bdce2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Thu, 11 Aug 2022 19:07:57 +0200 Subject: [PATCH 47/76] CustomCategory: Share-Button: create a link, so that hover works --- src/customCategory.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/customCategory.js b/src/customCategory.js index 3ea5ec44..0ee60233 100644 --- a/src/customCategory.js +++ b/src/customCategory.js @@ -261,16 +261,21 @@ hooks.register('category-overpass-init', (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.tabShare.header.innerHTML = '' + 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() - const url = location.origin + location.pathname + '#categories=custom/' + id navigator.clipboard.writeText(url) const notify = document.createElement('div') From b6b1dffb9e072ecfd593870479ca8d0593989e21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Thu, 11 Aug 2022 19:20:35 +0200 Subject: [PATCH 48/76] CustomCategory: move saving to applyContent() --- src/customCategory.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/customCategory.js b/src/customCategory.js index 0ee60233..efba1346 100644 --- a/src/customCategory.js +++ b/src/customCategory.js @@ -107,7 +107,6 @@ class CustomCategory { } this.applyContent(this.textarea.value) - ajax('customCategory', { content: this.textarea.value }, (result) => {}) e.preventDefault() } @@ -116,6 +115,7 @@ class CustomCategory { applyContent (content) { this.content = content + ajax('customCategory', { content: this.content }, () => {}) if (this.textarea) { this.textarea.value = content From 43cc32e0c34466d025e9b56e4807054f7ddf5a85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Thu, 11 Aug 2022 19:24:52 +0200 Subject: [PATCH 49/76] CustomCategory: tool tips --- lang/en.json | 1 + src/customCategory.js | 2 ++ 2 files changed, 3 insertions(+) diff --git a/lang/en.json b/lang/en.json index c01dd9ab..34072f7c 100644 --- a/lang/en.json +++ b/lang/en.json @@ -11,6 +11,7 @@ "apply": "Apply", "tip-tutorial": "Check out the [Tutorial]", "customCategory:header": "Custom categories", + "customCategory:clone": "Clone as custom category", "customCategory:create": "Create custom category", "customCategory:list": "List popular custom categories", "copied-clipboard": "Copied to clipboard", diff --git a/src/customCategory.js b/src/customCategory.js index efba1346..5b5aca9b 100644 --- a/src/customCategory.js +++ b/src/customCategory.js @@ -255,6 +255,7 @@ hooks.register('category-overpass-init', (category) => { }) 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) @@ -296,6 +297,7 @@ hooks.register('category-overpass-init', (category) => { }) category.tools.add(category.tabClone) category.tabClone.header.innerHTML = '' + category.tabClone.header.title = lang('customCategory:clone') category.tabClone.on('select', () => { category.tabClone.unselect() From e070a1b250b48e2b94b8a9b1f0e49ec81f3aad09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Thu, 11 Aug 2022 22:53:55 +0200 Subject: [PATCH 50/76] CustomCategory: improve cloning JSON categories to YAML (format, bugfix joining strings) --- src/customCategory.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/customCategory.js b/src/customCategory.js index 5b5aca9b..99d2c226 100644 --- a/src/customCategory.js +++ b/src/customCategory.js @@ -308,8 +308,10 @@ hooks.register('category-overpass-init', (category) => { (err, content) => { if (category.data.format === 'json') { content = JSON.parse(content) - content = jsonMultilineStrings.join(content, { exclude: [ 'const', 'filter'] }) - content = yaml.dump(content) + content = jsonMultilineStrings.join(content, { exclude: [ [ 'const' ], [ 'filter' ] ] }) + content = yaml.dump(content, { + lineWidth: 9999 + }) } clone.applyContent(content) From 43750913790afc9ff1a837fdcd5c5c52f48ab168 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Thu, 11 Aug 2022 22:54:25 +0200 Subject: [PATCH 51/76] TagTranslations: repoTrans(): bugfix, when a repository does not have translations --- src/tagTranslations.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tagTranslations.js b/src/tagTranslations.js index 9b06079f..e39eaec2 100644 --- a/src/tagTranslations.js +++ b/src/tagTranslations.js @@ -31,8 +31,8 @@ OverpassLayer.twig.extendFunction('repoTrans', function (str) { return str } - let lang = global.currentCategory.repository.lang - const format = str in lang ? lang[str] : str + const lang = global.currentCategory.repository.lang + const format = lang && str in lang ? lang[str] : str return vsprintf(format, Array.from(arguments).slice(1)) }) From 75d112b3d731db49c63db9d05993f2d62105f0b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Thu, 11 Aug 2022 23:06:43 +0200 Subject: [PATCH 52/76] ajax.php: do not json_decode post data --- ajax.php | 3 --- src/options.php | 4 +++- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/ajax.php b/ajax.php index 0d50087e..f64e080b 100644 --- a/ajax.php +++ b/ajax.php @@ -17,9 +17,6 @@ function error($msg) { Header("Content-Type: application/json; charset=UTF-8"); $postdata = file_get_contents("php://input"); -if ($postdata) { - $postdata = json_decode($postdata, true); -} $fun = "ajax_{$_REQUEST['__func']}"; $return = $fun($_REQUEST, $postdata); diff --git a/src/options.php b/src/options.php index 69153560..8999e230 100644 --- a/src/options.php +++ b/src/options.php @@ -1,5 +1,7 @@ Date: Thu, 11 Aug 2022 23:08:10 +0200 Subject: [PATCH 53/76] CustomCategory: when saving, submit payload as post data --- src/customCategory.js | 4 ++-- src/customCategory.php | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/customCategory.js b/src/customCategory.js index 99d2c226..b47d6623 100644 --- a/src/customCategory.js +++ b/src/customCategory.js @@ -115,7 +115,7 @@ class CustomCategory { applyContent (content) { this.content = content - ajax('customCategory', { content: this.content }, () => {}) + ajax('customCategory', { action: 'save' }, this.content, () => {}) if (this.textarea) { this.textarea.value = content @@ -207,7 +207,7 @@ hooks.register('browser-more-categories', (browser, parameters) => { function customCategoriesList (browser, options) { browser.dom.innerHTML = ' ' + lang('loading') - ajax('customCategory', { 'list': true }, (result) => { + ajax('customCategory', { action: 'list' }, (result) => { browser.dom.innerHTML = '' const ul = document.createElement('ul') diff --git a/src/customCategory.php b/src/customCategory.php index 388d78f0..eb7e331c 100644 --- a/src/customCategory.php +++ b/src/customCategory.php @@ -1,12 +1,12 @@ prepare("select customCategory.id, customCategory.created, customCategory.content, t.accessCount, t.popularity, t.lastAccess from customCategory left join (select id, count(id) accessCount, sum(1/((julianday('2023-08-06 00:00:00') - julianday(ts))/365.25 + 1)) popularity, max(ts) lastAccess from customCategoryAccess group by id) t on customCategory.id=t.id order by popularity desc, created desc limit 25"); @@ -32,7 +32,7 @@ function ajax_customCategory ($param) { return $data; } - if ($param['id']) { + if (isset($param['id'])) { $stmt = $db->prepare("select content from customCategory where id=:id"); $stmt->bindValue(':id', $param['id'], PDO::PARAM_STR); if ($stmt->execute()) { @@ -48,12 +48,12 @@ function ajax_customCategory ($param) { return false; } - if ($param['content']) { - $id = md5($param['content']); + if (isset($param['action']) && $param['action'] === 'save') { + $id = md5($content); $stmt = $db->prepare("insert or ignore into customCategory (id, content) values (:id, :content)"); $stmt->bindValue(':id', $id, PDO::PARAM_STR); - $stmt->bindValue(':content', $param['content'], PDO::PARAM_STR); + $stmt->bindValue(':content', $content, PDO::PARAM_STR); $result = $stmt->execute(); customCategoryUpdateAccess($id); From dcf945f8bc43d5fdae0bdc67b24f21d8d8a9658e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Sat, 13 Aug 2022 07:27:54 +0200 Subject: [PATCH 54/76] CustomCategory: move PHP functions into a class --- src/customCategory.php | 112 +++++++++++++++++++++++++---------------- 1 file changed, 70 insertions(+), 42 deletions(-) diff --git a/src/customCategory.php b/src/customCategory.php index eb7e331c..e752a106 100644 --- a/src/customCategory.php +++ b/src/customCategory.php @@ -1,12 +1,57 @@ prepare("select content from customCategory where id=:id"); + $stmt->bindValue(':id', $id, PDO::PARAM_STR); + if ($stmt->execute()) { + $row = $stmt->fetch(PDO::FETCH_ASSOC); + $result = $row['content']; + $stmt->closeCursor(); + + return $result; + } } - if (isset($param['action']) && $param['action'] === 'list') { + function recordAccess ($id) { + global $db; + + if (!isset($_SESSION['customCategoryAccess'])) { + $_SESSION['customCategoryAccess'] = []; + } + + // update access per session only once a day + if (array_key_exists($id, $_SESSION['customCategoryAccess']) && $_SESSION['customCategoryAccess'][$id] > time() - 86400) { + return; + } + + $_SESSION['customCategoryAccess'][$id] = time(); + + $stmt = $db->prepare("insert into customCategoryAccess (id) values (:id)"); + $stmt->bindValue(':id', $id); + $stmt->execute(); + } + + function saveCategory ($content) { + global $db; + + $id = md5($content); + + $stmt = $db->prepare("insert or ignore into customCategory (id, content) values (:id, :content)"); + $stmt->bindValue(':id', $id, PDO::PARAM_STR); + $stmt->bindValue(':content', $content, PDO::PARAM_STR); + $result = $stmt->execute(); + + return $id; + } + + function list ($options=[]) { + global $db; + // the popularity column counts every acess with declining value over time, // it halves every year. $stmt = $db->prepare("select customCategory.id, customCategory.created, customCategory.content, t.accessCount, t.popularity, t.lastAccess from customCategory left join (select id, count(id) accessCount, sum(1/((julianday('2023-08-06 00:00:00') - julianday(ts))/365.25 + 1)) popularity, max(ts) lastAccess from customCategoryAccess group by id) t on customCategory.id=t.id order by popularity desc, created desc limit 25"); @@ -31,52 +76,35 @@ function ajax_customCategory ($param, $content) { $stmt->closeCursor(); return $data; } +} - if (isset($param['id'])) { - $stmt = $db->prepare("select content from customCategory where id=:id"); - $stmt->bindValue(':id', $param['id'], PDO::PARAM_STR); - if ($stmt->execute()) { - $row = $stmt->fetch(PDO::FETCH_ASSOC); - $result = $row['content']; - $stmt->closeCursor(); - - customCategoryUpdateAccess($param['id']); +$customCategoryRepository = new CustomCategoryRepository(); - return $result; - } +function ajax_customCategory ($param, $content) { + global $db; + global $customCategoryRepository; - return false; + if (!$db) { + return null; } - if (isset($param['action']) && $param['action'] === 'save') { - $id = md5($content); - - $stmt = $db->prepare("insert or ignore into customCategory (id, content) values (:id, :content)"); - $stmt->bindValue(':id', $id, PDO::PARAM_STR); - $stmt->bindValue(':content', $content, PDO::PARAM_STR); - $result = $stmt->execute(); - - customCategoryUpdateAccess($id); - - return $result; + if (isset($param['action']) && $param['action'] === 'list') { + return $customCategoryRepository->list($param); } -} - -function customCategoryUpdateAccess ($id) { - global $db; - if (!isset($_SESSION['customCategoryAccess'])) { - $_SESSION['customCategoryAccess'] = []; - } + if (isset($param['id'])) { + $category = $customCategoryRepository->getCategory($param['id']); + if ($category) { + $customCategoryRepository->recordAccess($param['id']); + } - // update access per session only once a day - if (array_key_exists($id, $_SESSION['customCategoryAccess']) && $_SESSION['customCategoryAccess'][$id] > time() - 86400) { - return; + return $category; } - $_SESSION['customCategoryAccess'][$id] = time(); + if (isset($param['action']) && $param['action'] === 'save') { + $id = $customCategoryRepository->saveCategory($content); + $customCategoryRepository->recordAccess($id); - $stmt = $db->prepare("insert into customCategoryAccess (id) values (:id)"); - $stmt->bindValue(':id', $id); - $stmt->execute(); + return $id; + } } From dec58babdd8d5bf8c2474ffd1b34cb542e10ce2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Sat, 13 Aug 2022 16:48:11 +0200 Subject: [PATCH 55/76] CustomCategory: replace 'ajax' functions by own entry point --- customCategory.php | 36 ++++++++++++++ src/customCategory.js | 106 +++++++++++++++++++++++++---------------- src/customCategory.php | 29 ----------- 3 files changed, 101 insertions(+), 70 deletions(-) create mode 100644 customCategory.php diff --git a/customCategory.php b/customCategory.php new file mode 100644 index 00000000..a9ba9533 --- /dev/null +++ b/customCategory.php @@ -0,0 +1,36 @@ + + + + + +list($_REQUEST); + + Header("Content-Type: application/json; charset=utf-8"); + print json_readable_encode($result); +} + +if (isset($_REQUEST['id'])) { + $category = $customCategoryRepository->getCategory($_REQUEST['id']); + if ($category) { + $customCategoryRepository->recordAccess($_REQUEST['id']); + } + + Header("Content-Type: application/yaml; charset=utf-8"); + print $category; +} + +if (isset($_REQUEST['action']) && $_REQUEST['action'] === 'save') { + $content = file_get_contents("php://input"); + + $id = $customCategoryRepository->saveCategory($content); + $customCategoryRepository->recordAccess($id); + + Header("Content-Type: text/plain; charset=utf-8"); + print $id; +} diff --git a/src/customCategory.js b/src/customCategory.js index b47d6623..64c348af 100644 --- a/src/customCategory.js +++ b/src/customCategory.js @@ -26,23 +26,25 @@ class CustomCategoryRepository { return callback(null, yaml.load(cache[id])) } - ajax('customCategory', { id }, (result) => { - let data - cache[id] = result - - try { - data = yaml.load(result) - } - catch (e) { - return global.alert(e) - } + fetch('customCategory.php?id=' + id) + .then(res => res.text()) + .then(result => { + let data + cache[id] = result + + try { + data = yaml.load(result) + } + catch (e) { + return global.alert(e) + } - if (Object.is(data) && !('name' in data)) { - data.name = 'Custom ' + id.substr(0, 6) - } + if (Object.is(data) && !('name' in data)) { + data.name = 'Custom ' + id.substr(0, 6) + } - callback(null, data) - }) + callback(null, data) + }) } getTemplate (id, options, callback) { @@ -57,10 +59,27 @@ class CustomCategory { load (id, callback) { this.id = id - ajax('customCategory', { id }, (result) => { - this.content = result - callback(null, result) - }) + + fetch('customCategory.php?id=' + id) + .then(res => res.text()) + .then(result => { + let data + cache[id] = result + this.content = result + + try { + data = yaml.load(result) + } + catch (e) { + return global.alert(e) + } + + if (Object.is(data) && !('name' in data)) { + data.name = 'Custom ' + id.substr(0, 6) + } + + callback(null, data) + }) } edit () { @@ -115,7 +134,10 @@ class CustomCategory { applyContent (content) { this.content = content - ajax('customCategory', { action: 'save' }, this.content, () => {}) + fetch('customCategory.php?action=save', { + method: 'POST', + body: this.content + }) if (this.textarea) { this.textarea.value = content @@ -207,33 +229,35 @@ hooks.register('browser-more-categories', (browser, parameters) => { function customCategoriesList (browser, options) { browser.dom.innerHTML = ' ' + lang('loading') - ajax('customCategory', { action: 'list' }, (result) => { - browser.dom.innerHTML = '' + fetch('customCategory.php?action=list') + .then(res => res.json()) + .then(result => { + browser.dom.innerHTML = '' - const ul = document.createElement('ul') - browser.dom.appendChild(ul) + const ul = document.createElement('ul') + browser.dom.appendChild(ul) - result.forEach(cat => { - const li = document.createElement('li') + 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 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) + const edit = document.createElement('a') + edit.onclick = (e) => { + editCustomCategory(cat.id) + e.preventDefault() + } + edit.innerHTML = ' ' + li.appendChild(edit) - ul.appendChild(li) - }) + ul.appendChild(li) + }) - browser.catchLinks() - }) + browser.catchLinks() + }) } hooks.register('init', () => { diff --git a/src/customCategory.php b/src/customCategory.php index e752a106..b8239a3c 100644 --- a/src/customCategory.php +++ b/src/customCategory.php @@ -79,32 +79,3 @@ class CustomCategoryRepository { } $customCategoryRepository = new CustomCategoryRepository(); - -function ajax_customCategory ($param, $content) { - global $db; - global $customCategoryRepository; - - if (!$db) { - return null; - } - - if (isset($param['action']) && $param['action'] === 'list') { - return $customCategoryRepository->list($param); - } - - if (isset($param['id'])) { - $category = $customCategoryRepository->getCategory($param['id']); - if ($category) { - $customCategoryRepository->recordAccess($param['id']); - } - - return $category; - } - - if (isset($param['action']) && $param['action'] === 'save') { - $id = $customCategoryRepository->saveCategory($content); - $customCategoryRepository->recordAccess($id); - - return $id; - } -} From 799c0d3f69b099d19916b8e4f7067e0ca944c0fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Sat, 13 Aug 2022 19:08:57 +0200 Subject: [PATCH 56/76] CustomCategory: re-design js classes --- src/customCategory.js | 95 +++++++++++++++++++++---------------------- 1 file changed, 46 insertions(+), 49 deletions(-) diff --git a/src/customCategory.js b/src/customCategory.js index 64c348af..70b6dec1 100644 --- a/src/customCategory.js +++ b/src/customCategory.js @@ -7,11 +7,11 @@ const jsonMultilineStrings = require('json-multiline-strings') const Window = require('./Window') const OpenStreetBrowserLoader = require('./OpenStreetBrowserLoader') -const cache = {} -const customCategories = [] +const editors = [] class CustomCategoryRepository { constructor () { + this.clearCache() } load (callback) { @@ -19,21 +19,28 @@ class CustomCategoryRepository { } 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 cache) { - return callback(null, yaml.load(cache[id])) + if (id in this.cache) { + return callback(null, yaml.load(this.cache[id])) } fetch('customCategory.php?id=' + id) .then(res => res.text()) - .then(result => { + .then(content => { let data - cache[id] = result + this.cache[id] = content try { - data = yaml.load(result) + data = yaml.load(content) } catch (e) { return global.alert(e) @@ -43,42 +50,36 @@ class CustomCategoryRepository { data.name = 'Custom ' + id.substr(0, 6) } - callback(null, data) + callback(null, data, content) }) } + 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 CustomCategory { - constructor () { - customCategories.push(this) +class CustomCategoryEditor { + constructor (repository) { + this.repository = repository + editors.push(this) } load (id, callback) { - this.id = id - - fetch('customCategory.php?id=' + id) - .then(res => res.text()) - .then(result => { - let data - cache[id] = result - this.content = result - - try { - data = yaml.load(result) - } - catch (e) { - return global.alert(e) - } - - if (Object.is(data) && !('name' in data)) { - data.name = 'Custom ' + id.substr(0, 6) - } - - callback(null, data) + this.repository.getCategory(id, {}, + (err, category, content) => { + this.content = content + callback(err, content) }) } @@ -134,10 +135,7 @@ class CustomCategory { applyContent (content) { this.content = content - fetch('customCategory.php?action=save', { - method: 'POST', - body: this.content - }) + this.repository.saveCategory(this.content, {}, () => {}) if (this.textarea) { this.textarea.value = content @@ -145,7 +143,6 @@ class CustomCategory { const id = md5(content) this.id = id - cache[id] = content if (this.category) { this.category.remove() @@ -167,16 +164,16 @@ class CustomCategory { function editCustomCategory (id, category) { - let done = customCategories.filter(customCategory => { - if (customCategory.id === id) { - customCategory.edit() + let done = editors.filter(editor => { + if (editor.id === id) { + editor.edit() return true } }) if (!done.length) { - const customCategory = new CustomCategory() - customCategory.load(id, (err) => { + const editor = new CustomCategoryEditor(repository) + editor.load(id, (err) => { if (err) { return global.alert(err) } customCategory.category = category customCategory.edit() @@ -203,8 +200,8 @@ hooks.register('browser-more-categories', (browser, parameters) => { a.innerHTML = lang('customCategory:create') a.href = '#' a.onclick = (e) => { - const category = new CustomCategory() - category.edit() + const editor = new CustomCategoryEditor(repository) + editor.edit() browser.close() e.preventDefault() } @@ -229,9 +226,8 @@ hooks.register('browser-more-categories', (browser, parameters) => { function customCategoriesList (browser, options) { browser.dom.innerHTML = ' ' + lang('loading') - fetch('customCategory.php?action=list') - .then(res => res.json()) - .then(result => { + repository.listCategories({}, + (err, result) => { browser.dom.innerHTML = '' const ul = document.createElement('ul') @@ -260,8 +256,9 @@ function customCategoriesList (browser, options) { }) } +const repository = new CustomCategoryRepository() hooks.register('init', () => { - OpenStreetBrowserLoader.registerRepository('custom', new CustomCategoryRepository()) + OpenStreetBrowserLoader.registerRepository('custom', repository) }) hooks.register('category-overpass-init', (category) => { @@ -325,7 +322,7 @@ hooks.register('category-overpass-init', (category) => { category.tabClone.on('select', () => { category.tabClone.unselect() - const clone = new CustomCategory() + const clone = new CustomCategoryEditor(repository) clone.edit() category.repository.file_get_contents(category.data.fileName, {}, From 27ca80573d88d33e8e9a1d14f0aa203915de6bc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Sat, 13 Aug 2022 21:16:05 +0200 Subject: [PATCH 57/76] CustomCategory: make queries compatible with MYSQL/MariaDB --- src/customCategory.php | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/customCategory.php b/src/customCategory.php index b8239a3c..a5987845 100644 --- a/src/customCategory.php +++ b/src/customCategory.php @@ -41,7 +41,16 @@ class CustomCategoryRepository { $id = md5($content); - $stmt = $db->prepare("insert or ignore into customCategory (id, content) values (:id, :content)"); + switch ($db->getAttribute(PDO::ATTR_DRIVER_NAME)) { + case 'mysql': + $sqlAction = "insert ignore"; + break; + case 'sqlite': + default: + $sqlAction = "insert or ignore"; + } + + $stmt = $db->prepare("{$sqlAction} into customCategory (id, content) values (:id, :content)"); $stmt->bindValue(':id', $id, PDO::PARAM_STR); $stmt->bindValue(':content', $content, PDO::PARAM_STR); $result = $stmt->execute(); @@ -52,9 +61,18 @@ class CustomCategoryRepository { function list ($options=[]) { global $db; + // $sqlCalcAge: the age of the access in days + switch ($db->getAttribute(PDO::ATTR_DRIVER_NAME)) { + case 'mysql': + $sqlCalcAge = "datediff(now(), ts)"; + break; + case 'sqlite': + $sqlCalcAge = "julianday('now')-julianday(ts)"; + } + // the popularity column counts every acess with declining value over time, // it halves every year. - $stmt = $db->prepare("select customCategory.id, customCategory.created, customCategory.content, t.accessCount, t.popularity, t.lastAccess from customCategory left join (select id, count(id) accessCount, sum(1/((julianday('2023-08-06 00:00:00') - julianday(ts))/365.25 + 1)) popularity, max(ts) lastAccess from customCategoryAccess group by id) t on customCategory.id=t.id order by popularity desc, created desc limit 25"); + $stmt = $db->prepare("select customCategory.id, customCategory.created, customCategory.content, t.accessCount, t.popularity, t.lastAccess from customCategory left join (select id, count(id) accessCount, sum(1/(({$sqlCalcAge})/365.25+1)) popularity, max(ts) lastAccess from customCategoryAccess group by id) t on customCategory.id=t.id order by popularity desc, created desc limit 25"); $stmt->execute(); $data = $stmt->fetchAll(PDO::FETCH_ASSOC); $data = array_map(function ($d) { From d1283964cd28ec563d68daf87ffe4272b0374de5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Mon, 15 Aug 2022 21:40:41 +0100 Subject: [PATCH 58/76] CustomCategory: bugfix, editing loaded custom categories --- src/customCategory.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/customCategory.js b/src/customCategory.js index 70b6dec1..862f5302 100644 --- a/src/customCategory.js +++ b/src/customCategory.js @@ -30,7 +30,7 @@ class CustomCategoryRepository { getCategory (id, options, callback) { if (id in this.cache) { - return callback(null, yaml.load(this.cache[id])) + return callback(null, yaml.load(this.cache[id]), this.cache[id]) } fetch('customCategory.php?id=' + id) @@ -175,8 +175,8 @@ function editCustomCategory (id, category) { const editor = new CustomCategoryEditor(repository) editor.load(id, (err) => { if (err) { return global.alert(err) } - customCategory.category = category - customCategory.edit() + editor.category = category + editor.edit() }) } } From 63d73d3fdb74ad48930b28d02afcff16b202ecc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Tue, 16 Aug 2022 22:05:04 +0100 Subject: [PATCH 59/76] CustomCategory: Disable spellcheck in textarea --- src/customCategory.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/customCategory.js b/src/customCategory.js index 862f5302..ae37c21f 100644 --- a/src/customCategory.js +++ b/src/customCategory.js @@ -98,6 +98,7 @@ class CustomCategoryEditor { }) this.textarea = document.createElement('textarea') + this.textarea.spellcheck = false this.window.content.appendChild(this.textarea) if (this.content !== undefined) { this.textarea.value = this.content From 3669537cf29d78521e5e5c913a3d408256fab1aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Thu, 18 Aug 2022 21:18:13 +0100 Subject: [PATCH 60/76] OpenStreetBrowserLoader: if category has no type, assume 'overpass' --- src/OpenStreetBrowserLoader.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenStreetBrowserLoader.js b/src/OpenStreetBrowserLoader.js index 8442e15d..11c4ef45 100644 --- a/src/OpenStreetBrowserLoader.js +++ b/src/OpenStreetBrowserLoader.js @@ -204,7 +204,7 @@ class OpenStreetBrowserLoader { } if (!data.type) { - return callback(new Error('no type defined'), null) + data.type = 'overpass' } if (!(data.type in this.types)) { From 092bfd872429ab3279f90eb7e724e36ccb6b35ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Thu, 18 Aug 2022 21:20:57 +0100 Subject: [PATCH 61/76] CustomCategory: check that category can be parsed as object --- src/customCategory.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/customCategory.js b/src/customCategory.js index ae37c21f..2fe6fc7b 100644 --- a/src/customCategory.js +++ b/src/customCategory.js @@ -46,7 +46,11 @@ class CustomCategoryRepository { return global.alert(e) } - if (Object.is(data) && !('name' in data)) { + if (data && typeof data !== 'object') { + callback(new Error('Data can not be parsed into an object')) + } + + if (!data.name) { data.name = 'Custom ' + id.substr(0, 6) } @@ -358,6 +362,10 @@ function customCategoryTest (value) { return e } + if (!data || typeof data !== 'object') { + return new Error('Data can not be parsed into an object') + } + const fields = ['feature', 'memberFeature'] for (let i1 = 0; i1 < fields.length; i1++) { const k1 = fields[i1] From 530847148fc11e65fce8ad5f90e820a40f3b9b73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Fri, 19 Aug 2022 18:21:09 +0100 Subject: [PATCH 62/76] Twig Functions: add 'json_pp' filter --- doc/TwigJS.md | 2 ++ src/twigFunctions.js | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/doc/TwigJS.md b/doc/TwigJS.md index 0e7e9982..16a3e5da 100644 --- a/doc/TwigJS.md +++ b/doc/TwigJS.md @@ -78,6 +78,8 @@ Extra filters: * filter `debug`: print the value (and further arguments) to the javascript console (via `console.log()`) * filter `wikipediaAbstract`: shows the abstract of a Wikipedia article in the selected data language (or, if not available, the language which was used in input, resp. 'en' for Wikidata input). Input is either 'language:article' (e.g. 'en:Douglas Adams') or a wikidata id (e.g. 'Q42'). * filter `wikidataEntity`: returns the wikidata entity in structured form (or `null` if the entity is not cached or `false` if it does not exist). Example: https://www.wikidata.org/wiki/Special:EntityData/Q42.json +* filter `json_pp`: JSON pretty print the object. As parameter to the filter, the following options can be passed: + * `indent`: indentation (default: 2) Notes: * Variables will automatically be HTML escaped, unless the filter raw is used, e.g.: `{{ tags.name|raw }}` diff --git a/src/twigFunctions.js b/src/twigFunctions.js index 40584ee2..333a89d0 100644 --- a/src/twigFunctions.js +++ b/src/twigFunctions.js @@ -140,3 +140,11 @@ OverpassLayer.twig.extendFilter('debug', function (value, param) { console.log.apply(null, [ value, ...param ]) return value }) +OverpassLayer.twig.extendFilter('json_pp', function (value, param) { + const options = param[0] || {} + + value = JSON.parse(JSON.stringify(value)) + delete value._keys // remove TwigJS artefact + + return JSON.stringify(value, null, 'indent' in options ? ' '.repeat(options.indent) : ' ') +}) From 93b5626670689f7302767a75fe07a511306712f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Fri, 19 Aug 2022 18:28:35 +0100 Subject: [PATCH 63/76] Twig functions: add a 'yaml' filter --- doc/TwigJS.md | 1 + src/twigFunctions.js | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/doc/TwigJS.md b/doc/TwigJS.md index 16a3e5da..8d52d56e 100644 --- a/doc/TwigJS.md +++ b/doc/TwigJS.md @@ -80,6 +80,7 @@ Extra filters: * filter `wikidataEntity`: returns the wikidata entity in structured form (or `null` if the entity is not cached or `false` if it does not exist). Example: https://www.wikidata.org/wiki/Special:EntityData/Q42.json * filter `json_pp`: JSON pretty print the object. As parameter to the filter, the following options can be passed: * `indent`: indentation (default: 2) +* filter `yaml`: YAML pretty print the object. As options the filter, all options to [yaml.dump of js-yaml](https://github.com/nodeca/js-yaml#dump-object---options-) can be used. Notes: * Variables will automatically be HTML escaped, unless the filter raw is used, e.g.: `{{ tags.name|raw }}` diff --git a/src/twigFunctions.js b/src/twigFunctions.js index 333a89d0..d184356c 100644 --- a/src/twigFunctions.js +++ b/src/twigFunctions.js @@ -5,6 +5,7 @@ var osmParseDate = require('openstreetmap-date-parser') var osmFormatDate = require('openstreetmap-date-format') const natsort = require('natsort').default const md5 = require('md5') +const yaml = require('js-yaml') var md5cache = {} @@ -148,3 +149,11 @@ OverpassLayer.twig.extendFilter('json_pp', function (value, param) { return JSON.stringify(value, null, 'indent' in options ? ' '.repeat(options.indent) : ' ') }) +OverpassLayer.twig.extendFilter('yaml', function (value, param) { + const options = param[0] || {} + + value = JSON.parse(JSON.stringify(value)) + delete value._keys // remove TwigJS artefact + + return yaml.dump(value, options) +}) From 867f8b6fc6247f209cd9b97b0eb833721aae9cb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Fri, 19 Aug 2022 20:05:07 +0100 Subject: [PATCH 64/76] Window: Add a 'visible' property (to avoid re-showing closed windows) --- src/Window.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Window.js b/src/Window.js index 59ca371b..092f08d5 100644 --- a/src/Window.js +++ b/src/Window.js @@ -4,6 +4,7 @@ module.exports = class Window extends EventEmitter { constructor (options) { super() + this.visible = false this.dom = document.createElement('div') this.dom.className = 'Window' @@ -27,6 +28,8 @@ module.exports = class Window extends EventEmitter { dragElement(this.dom) this.dom.onclick = () => { + if (!this.visible) { return } + const activeEl = document.activeElement if (document.body.lastElementChild !== this.dom) { @@ -37,11 +40,13 @@ module.exports = class Window extends EventEmitter { } show () { + this.visible = true document.body.appendChild(this.dom) this.emit('show') } close () { + this.visible = false document.body.removeChild(this.dom) this.emit('close') } From 5c2539334cd1bcfa72a04675108bbfbe8c83b3c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Fri, 19 Aug 2022 20:05:52 +0100 Subject: [PATCH 65/76] CustomCategory: add a close button --- lang/en.json | 1 + src/customCategory.js | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/lang/en.json b/lang/en.json index 34072f7c..e85808d5 100644 --- a/lang/en.json +++ b/lang/en.json @@ -6,6 +6,7 @@ "cancel": "Cancel", "categories": "Categories", "category-info-tooltip": "Info & Map key", + "close": "Close", "closed": "closed", "default": "default", "apply": "Apply", diff --git a/src/customCategory.js b/src/customCategory.js index 2fe6fc7b..3e9f4b02 100644 --- a/src/customCategory.js +++ b/src/customCategory.js @@ -117,6 +117,12 @@ class CustomCategoryEditor { input.value = lang('apply') controls.appendChild(input) + const inputClose = document.createElement('input') + inputClose.type = 'button' + inputClose.value = lang('close') + inputClose.onclick = () => this.window.close() + controls.appendChild(inputClose) + const tutorial = document.createElement('span') tutorial.className = 'tip-tutorial' let text = lang('tip-tutorial') From 84b27e6a5ab301bf44085d727e7dd5fc37899f9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Fri, 19 Aug 2022 20:07:40 +0100 Subject: [PATCH 66/76] Window: add a tooltip on the close button --- src/Window.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Window.js b/src/Window.js index 092f08d5..c4203411 100644 --- a/src/Window.js +++ b/src/Window.js @@ -15,6 +15,7 @@ module.exports = class Window extends EventEmitter { this.closeBtn = document.createElement('div') this.closeBtn.className = 'closeBtn' + this.closeBtn.title = lang('close') this.closeBtn.onclick = (e) => { this.close() e.stopImmediatePropagation() From ceae25456a5ec937265dff4951398db4a8ecb61e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Fri, 19 Aug 2022 21:14:38 +0100 Subject: [PATCH 67/76] CustomCategory: add a download action --- lang/en.json | 1 + src/customCategory.js | 18 ++++++++++++++++++ style.css | 4 ++++ 3 files changed, 23 insertions(+) diff --git a/lang/en.json b/lang/en.json index e85808d5..48726d38 100644 --- a/lang/en.json +++ b/lang/en.json @@ -9,6 +9,7 @@ "close": "Close", "closed": "closed", "default": "default", + "download": "Download", "apply": "Apply", "tip-tutorial": "Check out the [Tutorial]", "customCategory:header": "Custom categories", diff --git a/src/customCategory.js b/src/customCategory.js index 3e9f4b02..6238cb7e 100644 --- a/src/customCategory.js +++ b/src/customCategory.js @@ -123,6 +123,17 @@ class CustomCategoryEditor { inputClose.onclick = () => 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') @@ -150,6 +161,7 @@ class CustomCategoryEditor { if (this.textarea) { this.textarea.value = content + this.updateDownload() } const id = md5(content) @@ -171,6 +183,12 @@ class CustomCategoryEditor { 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' + } } diff --git a/style.css b/style.css index 9421763e..613beb94 100644 --- a/style.css +++ b/style.css @@ -622,6 +622,10 @@ ul.overpass-layer-list > li > a > .content > .details { .Window > .content .controls { flex-grow: 0; } +.Window > .content .controls > .actions { + margin-left: 1em; + display: inline; +} /* Window */ .Window { From b496fab567d40ff52aa691825939fd76d2b32c41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Sat, 20 Aug 2022 07:29:42 +0100 Subject: [PATCH 68/76] CustomCategory: when loading file, add Content-Disposition --- customCategory.php | 1 + 1 file changed, 1 insertion(+) diff --git a/customCategory.php b/customCategory.php index a9ba9533..03f1db50 100644 --- a/customCategory.php +++ b/customCategory.php @@ -22,6 +22,7 @@ if (isset($_REQUEST['id'])) { } Header("Content-Type: application/yaml; charset=utf-8"); + Header("Content-Disposition: inline; filename=\"{$_REQUEST['id']}.yaml\""); print $category; } From c8a2b8bb47b29ad89bee7552757e89498b9ae803 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Sat, 20 Aug 2022 16:51:39 +0100 Subject: [PATCH 69/76] CustomCategory: when testing, execute templates to see if they validate --- src/customCategory.js | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/customCategory.js b/src/customCategory.js index 6238cb7e..8c99699c 100644 --- a/src/customCategory.js +++ b/src/customCategory.js @@ -418,8 +418,30 @@ function customCategoryTestCompile (data) { return } + let template try { - OverpassLayer.twig.twig({ data }) + template = OverpassLayer.twig.twig({ data }) + } + 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 From 3908dbc38a565c7bbfcdf2416222d5945828da6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Sat, 20 Aug 2022 17:06:24 +0100 Subject: [PATCH 70/76] twigFunctions yaml, json_pp: improve removing twig artefacts --- src/twigFunctions.js | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/src/twigFunctions.js b/src/twigFunctions.js index d184356c..1d49c321 100644 --- a/src/twigFunctions.js +++ b/src/twigFunctions.js @@ -144,16 +144,33 @@ OverpassLayer.twig.extendFilter('debug', function (value, param) { OverpassLayer.twig.extendFilter('json_pp', function (value, param) { const options = param[0] || {} - value = JSON.parse(JSON.stringify(value)) - delete value._keys // remove TwigJS artefact + if (value === 'undefined') { + return 'null' + } + + value = twigClear(value) return JSON.stringify(value, null, 'indent' in options ? ' '.repeat(options.indent) : ' ') }) OverpassLayer.twig.extendFilter('yaml', function (value, param) { const options = param[0] || {} - value = JSON.parse(JSON.stringify(value)) - delete value._keys // remove TwigJS artefact + value = twigClear(value) return yaml.dump(value, options) }) + +function twigClear (value) { + if (value === null || typeof value !== 'object') { + return value + } + + const v = {} + for (let k in value) { + if (k !== '_keys') { + v[k] = value[k] + } + } + + return v +} From a3939fce2a44686fa25c1510d032b6a21b4668cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Sat, 20 Aug 2022 20:48:25 +0100 Subject: [PATCH 71/76] YAML Tutorial: add a 'body' script, using the yaml filter --- doc/CategoryAsYAML.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/doc/CategoryAsYAML.md b/doc/CategoryAsYAML.md index 5e6596d1..e9f1b94a 100644 --- a/doc/CategoryAsYAML.md +++ b/doc/CategoryAsYAML.md @@ -128,6 +128,11 @@ feature: # each value individually. They are joined as enumeration. details: | {{ tagTransList('cuisine', tags.cuisine) }} + # Body is shown in the popup and the details in the sidebar. An easy way to + # show all tags is using the TwigJS 'yaml' filter, which produces YAML. + # Alternatively, you could use 'json_pp' (JSON pretty print). + body: | +
{{ tags|yaml }}
filter: cuisine: name: "{{ keyTrans('cuisine') }}" From 842ed055211d3d2519a522688b27040ed693e3b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Sat, 20 Aug 2022 22:30:46 +0100 Subject: [PATCH 72/76] CustomCategory: buttons 'Apply & keep editing', 'Apply & close' --- lang/en.json | 3 ++- src/customCategory.js | 25 ++++++++++++++++--------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/lang/en.json b/lang/en.json index 48726d38..5012e210 100644 --- a/lang/en.json +++ b/lang/en.json @@ -10,7 +10,8 @@ "closed": "closed", "default": "default", "download": "Download", - "apply": "Apply", + "apply-keep": "Apply & keep editing", + "apply-close": "Apply & close", "tip-tutorial": "Check out the [Tutorial]", "customCategory:header": "Custom categories", "customCategory:clone": "Clone as custom category", diff --git a/src/customCategory.js b/src/customCategory.js index 8c99699c..d6bb6f43 100644 --- a/src/customCategory.js +++ b/src/customCategory.js @@ -114,13 +114,16 @@ class CustomCategoryEditor { const input = document.createElement('input') input.type = 'submit' - input.value = lang('apply') + input.value = lang('apply-keep') controls.appendChild(input) const inputClose = document.createElement('input') inputClose.type = 'button' - inputClose.value = lang('close') - inputClose.onclick = () => this.window.close() + inputClose.value = lang('apply-close') + inputClose.onclick = () => { + this.submit() + this.window.close() + } controls.appendChild(inputClose) const icons = document.createElement('div') @@ -143,18 +146,22 @@ class CustomCategoryEditor { controls.appendChild(tutorial) input.onclick = (e) => { - const err = customCategoryTest(this.textarea.value) - if (err) { - return global.alert(err) - } - - this.applyContent(this.textarea.value) + this.submit() e.preventDefault() } this.window.show() } + submit () { + const err = customCategoryTest(this.textarea.value) + if (err) { + return global.alert(err) + } + + this.applyContent(this.textarea.value) + } + applyContent (content) { this.content = content this.repository.saveCategory(this.content, {}, () => {}) From 6c8bdd67a8a550f466077505b834183c5fa55d47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Sat, 20 Aug 2022 22:34:56 +0100 Subject: [PATCH 73/76] CustomCategory: a few style improvements --- style.css | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/style.css b/style.css index 613beb94..ede24e04 100644 --- a/style.css +++ b/style.css @@ -624,6 +624,7 @@ ul.overpass-layer-list > li > a > .content > .details { } .Window > .content .controls > .actions { margin-left: 1em; + margin-right: 1em; display: inline; } @@ -670,7 +671,8 @@ ul.overpass-layer-list > li > a > .content > .details { align-content: stretch; } .tip-tutorial { - margin-left: 1em; + padding: 0.25em 0; + display: inline-block; } .tip-tutorial a { text-decoration: underline; From ece3c11ec349552cfde981301a20da7797e046e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Sun, 21 Aug 2022 06:56:15 +0100 Subject: [PATCH 74/76] CustomCategory: move parsing category to a separate function --- src/customCategory.js | 40 ++++++++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/src/customCategory.js b/src/customCategory.js index d6bb6f43..238ca65d 100644 --- a/src/customCategory.js +++ b/src/customCategory.js @@ -30,34 +30,42 @@ class CustomCategoryRepository { getCategory (id, options, callback) { if (id in this.cache) { - return callback(null, yaml.load(this.cache[id]), this.cache[id]) + 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 => { - let data this.cache[id] = content - try { - data = yaml.load(content) - } - catch (e) { - return global.alert(e) - } - - if (data && typeof data !== 'object') { - callback(new Error('Data can not be parsed into an object')) - } - - if (!data.name) { - data.name = 'Custom ' + id.substr(0, 6) - } + 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') { + callback(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 From f8002ff58ecb9f41375f874be4f6c662e2385ac2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Mon, 22 Aug 2022 08:41:08 +0100 Subject: [PATCH 75/76] Documentation: Rename tutorial --- README.md | 2 ++ doc/{CategoryAsYAML.md => Tutorial.md} | 0 src/customCategory.js | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) rename doc/{CategoryAsYAML.md => Tutorial.md} (100%) diff --git a/README.md b/README.md index 67fe8be8..0c71ef87 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,8 @@ You are welcome to send pull requests via Github! ### Category definition There are currently two types of categories: `index` (for sub categories) and `overpass` (for OpenStreetMap data, loaded via an Overpass API request). Each of them is defined via a JSON structure. They can be combined into a single file. +Check out the [tutorial](./doc/Tutorial.md)! + #### Category 'index' File: dir.json ```json diff --git a/doc/CategoryAsYAML.md b/doc/Tutorial.md similarity index 100% rename from doc/CategoryAsYAML.md rename to doc/Tutorial.md diff --git a/src/customCategory.js b/src/customCategory.js index 238ca65d..5cd30543 100644 --- a/src/customCategory.js +++ b/src/customCategory.js @@ -148,7 +148,7 @@ class CustomCategoryEditor { const tutorial = document.createElement('span') tutorial.className = 'tip-tutorial' let text = lang('tip-tutorial') - text = text.replace('[', '') + text = text.replace('[', '') text = text.replace(']', '') tutorial.innerHTML = text controls.appendChild(tutorial) From 2bad4871ab7f4624a8731e5de2765c785a818541 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20B=C3=B6sch-Plepelits?= Date: Mon, 22 Aug 2022 08:43:20 +0100 Subject: [PATCH 76/76] README: categories can be defined as YAML --- README.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0c71ef87..807fb3b0 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ git clone https://github.com/plepe/openstreetbrowser-categories-main.git node_mo You are welcome to send pull requests via Github! ### Category definition -There are currently two types of categories: `index` (for sub categories) and `overpass` (for OpenStreetMap data, loaded via an Overpass API request). Each of them is defined via a JSON structure. They can be combined into a single file. +There are currently two types of categories: `index` (for sub categories) and `overpass` (for OpenStreetMap data, loaded via an Overpass API request). Each of them is defined via a JSON (old) or YAML (recommended) structure. They can be combined into a single file. Check out the [tutorial](./doc/Tutorial.md)! @@ -57,6 +57,16 @@ File: dir.json } ``` +or File: dir.yaml +```yaml +type: index +subCategories: + - id: foo + - id: bar + type: overpass + query: node[amenity=bar] +``` + This will define a category with the id 'dir' (from the file name) with two sub-categories: 'foo' (which will be loaded from the file `foo.json`) and 'bar' (which is defined inline as category of type 'overpass' and will show all nodes with the tag 'amenity' set to value 'bar' - see below for more details). #### Category 'overpass'