Stephan Bösch-Plepelits
2 years ago
31 changed files with 1493 additions and 343 deletions
-
14README.md
-
3ajax.php
-
8conf.php-dist
-
37customCategory.php
-
5doc/Tutorial.md
-
3doc/TwigJS.md
-
12init.sql
-
10lang/en.json
-
3modulekit.php
-
9package-lock.json
-
1package.json
-
20repo.php
-
53src/Browser.js
-
412src/OpenStreetBrowserLoader.js
-
86src/Repository.js
-
8src/RepositoryDir.php
-
2src/RepositoryGit.php
-
97src/Window.js
-
232src/addCategories.js
-
464src/customCategory.js
-
99src/customCategory.php
-
9src/database.php
-
7src/domSort.js
-
2src/index.js
-
35src/moreCategories.js
-
4src/options.php
-
40src/repositories.php
-
44src/repositoriesGitea.php
-
4src/tagTranslations.js
-
34src/twigFunctions.js
-
79style.css
@ -0,0 +1,37 @@ |
|||
<?php include "conf.php"; /* load a local configuration */ ?>
|
|||
<?php session_start(); ?>
|
|||
<?php require 'vendor/autoload.php'; /* composer includes */ ?>
|
|||
<?php include "modulekit/loader.php"; /* loads all php-includes */ ?>
|
|||
<?php call_hooks("ajax_start"); /* initialize submodules */ ?>
|
|||
<?php |
|||
if (!isset($db)) { |
|||
exit(0); |
|||
} |
|||
|
|||
if (isset($_REQUEST['action']) && $_REQUEST['action'] === 'list') { |
|||
$result = $customCategoryRepository->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"); |
|||
Header("Content-Disposition: inline; filename=\"{$_REQUEST['id']}.yaml\""); |
|||
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; |
|||
} |
@ -0,0 +1,12 @@ |
|||
create table customCategory ( |
|||
id char(32) not null, |
|||
content mediumtext not null, |
|||
created 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 |
|||
); |
@ -0,0 +1,53 @@ |
|||
const EventEmitter = require('events') |
|||
const queryString = require('query-string') |
|||
|
|||
const domSort = require('./domSort') |
|||
|
|||
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 |
|||
|
|||
domSort(this.dom) |
|||
} |
|||
|
|||
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 && 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') |
|||
} |
|||
} |
@ -1,8 +1,90 @@ |
|||
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 |
|||
} |
|||
} |
|||
|
|||
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) |
|||
} |
|||
|
|||
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) { |
|||
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]) |
|||
} |
|||
} |
@ -0,0 +1,97 @@ |
|||
const EventEmitter = require('events') |
|||
|
|||
module.exports = class Window extends EventEmitter { |
|||
constructor (options) { |
|||
super() |
|||
|
|||
this.visible = false |
|||
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.title = lang('close') |
|||
this.closeBtn.onclick = (e) => { |
|||
this.close() |
|||
e.stopImmediatePropagation() |
|||
} |
|||
this.header.appendChild(this.closeBtn) |
|||
|
|||
this.content = document.createElement('div') |
|||
this.content.className = 'content' |
|||
this.dom.appendChild(this.content) |
|||
|
|||
dragElement(this.dom) |
|||
|
|||
this.dom.onclick = () => { |
|||
if (!this.visible) { return } |
|||
|
|||
const activeEl = document.activeElement |
|||
|
|||
if (document.body.lastElementChild !== this.dom) { |
|||
document.body.appendChild(this.dom) |
|||
activeEl.focus() |
|||
} |
|||
} |
|||
} |
|||
|
|||
show () { |
|||
this.visible = true |
|||
document.body.appendChild(this.dom) |
|||
this.emit('show') |
|||
} |
|||
|
|||
close () { |
|||
this.visible = false |
|||
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; |
|||
} |
|||
} |
@ -0,0 +1,464 @@ |
|||
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') |
|||
|
|||
const editors = [] |
|||
|
|||
class CustomCategoryRepository { |
|||
constructor () { |
|||
this.clearCache() |
|||
} |
|||
|
|||
load (callback) { |
|||
callback(null) |
|||
} |
|||
|
|||
clearCache () { |
|||
this.cache = {} |
|||
} |
|||
|
|||
listCategories (options, callback) { |
|||
fetch('customCategory.php?action=list') |
|||
.then(res => res.json()) |
|||
.then(result => callback(null, result)) |
|||
} |
|||
|
|||
getCategory (id, options, callback) { |
|||
if (id in this.cache) { |
|||
const data = this.parseCategory(id, this.cache[id]) |
|||
return callback(null, data, this.cache[id]) |
|||
} |
|||
|
|||
fetch('customCategory.php?id=' + id) |
|||
.then(res => res.text()) |
|||
.then(content => { |
|||
this.cache[id] = content |
|||
|
|||
const data = this.parseCategory(id, content) |
|||
|
|||
callback(null, data, content) |
|||
}) |
|||
} |
|||
|
|||
parseCategory (id, content) { |
|||
let data |
|||
|
|||
try { |
|||
data = yaml.load(content) |
|||
} |
|||
catch (e) { |
|||
return global.alert(e) |
|||
} |
|||
|
|||
if (data && typeof data !== 'object') { |
|||
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 |
|||
|
|||
fetch('customCategory.php?action=save', { |
|||
method: 'POST', |
|||
body |
|||
}) |
|||
} |
|||
|
|||
getTemplate (id, options, callback) { |
|||
callback(null, '') |
|||
} |
|||
} |
|||
|
|||
class CustomCategoryEditor { |
|||
constructor (repository) { |
|||
this.repository = repository |
|||
editors.push(this) |
|||
} |
|||
|
|||
load (id, callback) { |
|||
this.repository.getCategory(id, {}, |
|||
(err, category, content) => { |
|||
this.content = content |
|||
callback(err, content) |
|||
}) |
|||
} |
|||
|
|||
edit () { |
|||
if (this.window) { |
|||
this.window.focused = true |
|||
return |
|||
} |
|||
|
|||
this.window = new Window({ |
|||
title: 'Custom Category' |
|||
}) |
|||
|
|||
this.window.on('close', () => { |
|||
this.window = null |
|||
}) |
|||
|
|||
this.textarea = document.createElement('textarea') |
|||
this.textarea.spellcheck = false |
|||
this.window.content.appendChild(this.textarea) |
|||
if (this.content !== undefined) { |
|||
this.textarea.value = this.content |
|||
} |
|||
|
|||
const controls = document.createElement('div') |
|||
controls.className = 'controls' |
|||
this.window.content.appendChild(controls) |
|||
|
|||
const input = document.createElement('input') |
|||
input.type = 'submit' |
|||
input.value = lang('apply-keep') |
|||
controls.appendChild(input) |
|||
|
|||
const inputClose = document.createElement('input') |
|||
inputClose.type = 'button' |
|||
inputClose.value = lang('apply-close') |
|||
inputClose.onclick = () => { |
|||
this.submit() |
|||
this.window.close() |
|||
} |
|||
controls.appendChild(inputClose) |
|||
|
|||
const icons = document.createElement('div') |
|||
icons.className = 'actions' |
|||
controls.appendChild(icons) |
|||
|
|||
this.inputDownload = document.createElement('a') |
|||
this.textarea.onchange = () => this.updateDownload() |
|||
this.updateDownload() |
|||
this.inputDownload.title = lang('download') |
|||
this.inputDownload.innerHTML = '<i class="fas fa-download"></i>' |
|||
icons.appendChild(this.inputDownload) |
|||
|
|||
const tutorial = document.createElement('span') |
|||
tutorial.className = 'tip-tutorial' |
|||
let text = lang('tip-tutorial') |
|||
text = text.replace('[', '<a target="_blank" href="https://github.com/plepe/OpenStreetBrowser/blob/master/doc/Tutorial.md">') |
|||
text = text.replace(']', '</a>') |
|||
tutorial.innerHTML = text |
|||
controls.appendChild(tutorial) |
|||
|
|||
input.onclick = (e) => { |
|||
this.submit() |
|||
e.preventDefault() |
|||
} |
|||
|
|||
this.window.show() |
|||
} |
|||
|
|||
submit () { |
|||
const err = customCategoryTest(this.textarea.value) |
|||
if (err) { |
|||
return global.alert(err) |
|||
} |
|||
|
|||
this.applyContent(this.textarea.value) |
|||
} |
|||
|
|||
applyContent (content) { |
|||
this.content = content |
|||
this.repository.saveCategory(this.content, {}, () => {}) |
|||
|
|||
if (this.textarea) { |
|||
this.textarea.value = content |
|||
this.updateDownload() |
|||
} |
|||
|
|||
const id = md5(content) |
|||
this.id = id |
|||
|
|||
if (this.category) { |
|||
this.category.remove() |
|||
this.category = null |
|||
} |
|||
|
|||
OpenStreetBrowserLoader.getCategory('custom/' + id, {}, (err, category) => { |
|||
if (err) { |
|||
return global.alert(err) |
|||
} |
|||
|
|||
this.category = category |
|||
this.category.setParentDom(document.getElementById('contentListAddCategories')) |
|||
|
|||
this.category.open() |
|||
}) |
|||
} |
|||
|
|||
updateDownload () { |
|||
const file = new Blob([this.textarea.value], { type: 'application/yaml' }) |
|||
this.inputDownload.href = URL.createObjectURL(file) |
|||
this.inputDownload.download = md5(this.textarea.value) + '.yaml' |
|||
} |
|||
} |
|||
|
|||
|
|||
function editCustomCategory (id, category) { |
|||
let done = editors.filter(editor => { |
|||
if (editor.id === id) { |
|||
editor.edit() |
|||
return true |
|||
} |
|||
}) |
|||
|
|||
if (!done.length) { |
|||
const editor = new CustomCategoryEditor(repository) |
|||
editor.load(id, (err) => { |
|||
if (err) { return global.alert(err) } |
|||
editor.category = category |
|||
editor.edit() |
|||
}) |
|||
} |
|||
} |
|||
|
|||
hooks.register('browser-more-categories', (browser, parameters) => { |
|||
const content = browser.dom |
|||
|
|||
if (!Object.keys(parameters).length) { |
|||
let block = document.createElement('div') |
|||
block.setAttribute('weight', 0) |
|||
content.appendChild(block) |
|||
|
|||
let header = document.createElement('h4') |
|||
header.innerHTML = lang('customCategory:header') |
|||
block.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 = (e) => { |
|||
const editor = new CustomCategoryEditor(repository) |
|||
editor.edit() |
|||
browser.close() |
|||
e.preventDefault() |
|||
} |
|||
li.appendChild(a) |
|||
ul.appendChild(li) |
|||
|
|||
li = document.createElement('li') |
|||
a = document.createElement('a') |
|||
a.innerHTML = lang('customCategory:list') |
|||
a.href = '#more-categories?custom=list' |
|||
li.appendChild(a) |
|||
ul.appendChild(li) |
|||
|
|||
block.appendChild(ul) |
|||
browser.catchLinks() |
|||
} |
|||
else if (parameters.custom === 'list') { |
|||
customCategoriesList(browser, parameters) |
|||
} |
|||
}) |
|||
|
|||
function customCategoriesList (browser, options) { |
|||
browser.dom.innerHTML = '<i class="fa fa-spinner fa-pulse fa-fw"></i> ' + lang('loading') |
|||
|
|||
repository.listCategories({}, |
|||
(err, result) => { |
|||
browser.dom.innerHTML = '' |
|||
|
|||
const ul = document.createElement('ul') |
|||
browser.dom.appendChild(ul) |
|||
|
|||
result.forEach(cat => { |
|||
const li = document.createElement('li') |
|||
|
|||
const a = document.createElement('a') |
|||
a.href = '#categories=custom/' + cat.id |
|||
a.appendChild(document.createTextNode(cat.name)) |
|||
li.appendChild(a) |
|||
|
|||
const edit = document.createElement('a') |
|||
edit.onclick = (e) => { |
|||
editCustomCategory(cat.id) |
|||
e.preventDefault() |
|||
} |
|||
edit.innerHTML = ' <i class="fa fa-pen"></i>' |
|||
li.appendChild(edit) |
|||
|
|||
ul.appendChild(li) |
|||
}) |
|||
|
|||
browser.catchLinks() |
|||
}) |
|||
} |
|||
|
|||
const repository = new CustomCategoryRepository() |
|||
hooks.register('init', () => { |
|||
OpenStreetBrowserLoader.registerRepository('custom', repository) |
|||
}) |
|||
|
|||
hooks.register('category-overpass-init', (category) => { |
|||
const m = category.id.match(/^custom\/(.*)$/) |
|||
if (m) { |
|||
const id = m[1] |
|||
|
|||
if (category.tabEdit) { |
|||
category.tools.remove(this.category.tabEdit) |
|||
} |
|||
|
|||
category.tabEdit = new tabs.Tab({ |
|||
id: 'edit', |
|||
weight: 9 |
|||
}) |
|||
category.tools.add(category.tabEdit) |
|||
category.tabEdit.header.innerHTML = '<i class="fa fa-pen"></i>' |
|||
category.tabEdit.header.title = lang('edit') |
|||
category.tabEdit.on('select', () => { |
|||
category.tabEdit.unselect() |
|||
editCustomCategory(id, category) |
|||
}) |
|||
|
|||
if (!category.tabShare) { |
|||
const url = location.origin + location.pathname + '#categories=custom/' + id |
|||
|
|||
category.tabShare = new tabs.Tab({ |
|||
id: 'share', |
|||
weight: 10 |
|||
}) |
|||
category.tools.add(category.tabShare) |
|||
category.shareLink = document.createElement('a') |
|||
category.shareLink.href = url |
|||
category.shareLink.innerHTML = '<i class="fa fa-share-alt"></i>' |
|||
|
|||
category.tabShare.header.appendChild(category.shareLink) |
|||
category.tabShare.header.className = 'share-button' |
|||
category.tabShare.on('select', () => { |
|||
category.tabShare.unselect() |
|||
navigator.clipboard.writeText(url) |
|||
|
|||
const notify = document.createElement('div') |
|||
notify.className = 'notify' |
|||
notify.innerHTML = lang('copied-clipboard') |
|||
category.tabShare.header.appendChild(notify) |
|||
global.setTimeout(() => category.tabShare.header.removeChild(notify), 2000) |
|||
}) |
|||
} |
|||
} else { |
|||
if (category.tabClone) { |
|||
category.tools.remove(this.category.tabClone) |
|||
} |
|||
|
|||
category.tabClone = new tabs.Tab({ |
|||
id: 'clone', |
|||
weight: 9 |
|||
}) |
|||
category.tools.add(category.tabClone) |
|||
category.tabClone.header.innerHTML = '<i class="fa fa-clone"></i>' |
|||
category.tabClone.header.title = lang('customCategory:clone') |
|||
category.tabClone.on('select', () => { |
|||
category.tabClone.unselect() |
|||
|
|||
const clone = new CustomCategoryEditor(repository) |
|||
clone.edit() |
|||
|
|||
category.repository.file_get_contents(category.data.fileName, {}, |
|||
(err, content) => { |
|||
if (category.data.format === 'json') { |
|||
content = JSON.parse(content) |
|||
content = jsonMultilineStrings.join(content, { exclude: [ [ 'const' ], [ 'filter' ] ] }) |
|||
content = yaml.dump(content, { |
|||
lineWidth: 9999 |
|||
}) |
|||
} |
|||
|
|||
clone.applyContent(content) |
|||
category.close() |
|||
} |
|||
) |
|||
}) |
|||
|
|||
} |
|||
}) |
|||
|
|||
function customCategoryTest (value) { |
|||
if (!value) { |
|||
return new Error('Empty category') |
|||
} |
|||
|
|||
let data |
|||
try { |
|||
data = yaml.load(value) |
|||
} |
|||
catch (e) { |
|||
return e |
|||
} |
|||
|
|||
if (!data || typeof data !== 'object') { |
|||
return new Error('Data can not be parsed into an object') |
|||
} |
|||
|
|||
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 |
|||
} |
|||
|
|||
let template |
|||
try { |
|||
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 |
|||
} |
|||
} |
@ -0,0 +1,99 @@ |
|||
<?php |
|||
class CustomCategoryRepository { |
|||
function clearCache () { |
|||
} |
|||
|
|||
function getCategory ($id, $options=[]) { |
|||
global $db; |
|||
|
|||
$stmt = $db->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; |
|||
} |
|||
} |
|||
|
|||
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); |
|||
|
|||
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(); |
|||
|
|||
return $id; |
|||
} |
|||
|
|||
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/(({$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) { |
|||
$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; |
|||
} |
|||
} |
|||
|
|||
$customCategoryRepository = new CustomCategoryRepository(); |
@ -0,0 +1,9 @@ |
|||
<?php |
|||
register_hook('ajax_start', function () { |
|||
global $db; |
|||
global $db_conf; |
|||
|
|||
if ($db_conf) { |
|||
$db = new PDO($db_conf['dsn'], $db_conf['username'], $db_conf['password']); |
|||
} |
|||
}); |
@ -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)) |
|||
} |
@ -0,0 +1,35 @@ |
|||
const tabs = require('modulekit-tabs') |
|||
|
|||
const Browser = require('./Browser') |
|||
|
|||
let tab |
|||
|
|||
function moreCategoriesIndex () { |
|||
let content = tab.content |
|||
|
|||
content.innerHTML = '<h3>' + lang('more_categories') + '</h3>' |
|||
|
|||
const dom = document.createElement('div') |
|||
content.appendChild(dom) |
|||
|
|||
const browser = new Browser('more-categories', dom) |
|||
browser.buildPage({}) |
|||
|
|||
browser.on('close', () => tab.unselect()) |
|||
} |
|||
|
|||
register_hook('init', function (callback) { |
|||
tab = new tabs.Tab({ |
|||
id: 'moreCategories' |
|||
}) |
|||
global.tabs.add(tab) |
|||
|
|||
tab.header.innerHTML = '<i class="fa fa-plus" aria-hidden="true"></i>' |
|||
tab.header.title = lang('more_categories') |
|||
|
|||
tab.on('select', () => { |
|||
tab.content.innerHTML = '' |
|||
moreCategoriesIndex() |
|||
}) |
|||
}) |
|||
|
@ -0,0 +1,44 @@ |
|||
<?php |
|||
register_hook("get-repositories", function ($result) { |
|||
global $repositoriesGitea; |
|||
|
|||
if (isset($repositoriesGitea)) { |
|||
$d1 = opendir($repositoriesGitea['path']); |
|||
while ($f1 = readdir($d1)) { |
|||
if (substr($f1, 0, 1) !== '.') { |
|||
$d2 = opendir("{$repositoriesGitea['path']}/{$f1}"); |
|||
while ($f2 = readdir($d2)) { |
|||
if (substr($f2, 0, 1) !== '.') { |
|||
$f2id = substr($f2, 0, -4); |
|||
|
|||
$r = array( |
|||
'path' => "{$repositoriesGitea['path']}/{$f1}/{$f2}", |
|||
'type' => 'git', |
|||
'group' => 'gitea', |
|||
); |
|||
|
|||
if (array_key_exists('url', $repositoriesGitea)) { |
|||
$r['repositoryUrl'] = "{$repositoriesGitea['url']}/{{ repositoryId }}"; |
|||
$r['categoryUrl'] = "{$repositoriesGitea['url']}/{{ repositoryId }}/src/branch/{{ branchId }}/{{ categoryId }}.{{ categoryFormat }}"; |
|||
} |
|||
|
|||
$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); |
|||
} |
|||
}); |
Write
Preview
Loading…
Cancel
Save
Reference in new issue