Browse Source

Merge branch 'custom-category'

master
parent
commit
3fa53bd076
  1. 14
      README.md
  2. 3
      ajax.php
  3. 8
      conf.php-dist
  4. 37
      customCategory.php
  5. 5
      doc/Tutorial.md
  6. 3
      doc/TwigJS.md
  7. 12
      init.sql
  8. 10
      lang/en.json
  9. 3
      modulekit.php
  10. 9
      package-lock.json
  11. 1
      package.json
  12. 20
      repo.php
  13. 53
      src/Browser.js
  14. 412
      src/OpenStreetBrowserLoader.js
  15. 86
      src/Repository.js
  16. 8
      src/RepositoryDir.php
  17. 2
      src/RepositoryGit.php
  18. 97
      src/Window.js
  19. 232
      src/addCategories.js
  20. 464
      src/customCategory.js
  21. 99
      src/customCategory.php
  22. 9
      src/database.php
  23. 7
      src/domSort.js
  24. 2
      src/index.js
  25. 35
      src/moreCategories.js
  26. 4
      src/options.php
  27. 40
      src/repositories.php
  28. 44
      src/repositoriesGitea.php
  29. 4
      src/tagTranslations.js
  30. 34
      src/twigFunctions.js
  31. 79
      style.css

14
README.md

@ -35,7 +35,9 @@ 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)!
#### Category 'index'
File: dir.json
@ -55,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'

3
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);

8
conf.php-dist

@ -95,6 +95,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

37
customCategory.php

@ -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;
}

5
doc/CategoryAsYAML.md → doc/Tutorial.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: |
<pre>{{ tags|yaml }}</pre>
filter:
cuisine:
name: "{{ keyTrans('cuisine') }}"

3
doc/TwigJS.md

@ -78,6 +78,9 @@ 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)
* 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 }}`

12
init.sql

@ -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
);

10
lang/en.json

@ -6,8 +6,18 @@
"cancel": "Cancel",
"categories": "Categories",
"category-info-tooltip": "Info & Map key",
"close": "Close",
"closed": "closed",
"default": "default",
"download": "Download",
"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",
"customCategory:create": "Create custom category",
"customCategory:list": "List popular custom categories",
"copied-clipboard": "Copied to clipboard",
"edit": "edit",
"editor:id": "iD (in-browser editor)",
"editor:remote": "Remote Control (JOSM or Merkaator)",

3
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,8 @@ $include = array(
'src/RepositoryDir.php',
'src/RepositoryGit.php',
'src/repositories.php',
'src/repositoriesGitea.php',
'src/customCategory.php',
),
'css' => array(
'style.css',

9
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",

1
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",

20
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);
}
@ -59,6 +60,25 @@ if ($branchId) {
}
}
if (array_key_exists('file', $_REQUEST)) {
$file = $repo->file_get_contents($_REQUEST['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);
}
$cacheDir = null;
$ts = $repo->timestamp($path);
if (isset($config['cache'])) {

53
src/Browser.js

@ -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')
}
}

412
src/OpenStreetBrowserLoader.js

@ -2,268 +2,282 @@ 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.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.getRepository(ids.repositoryId, opt, (err, repository) => {
// 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))
}
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])
}
/**
* @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]
}
if (err) { return callback(err) }
if (repo in this.repoCache) {
return callback.apply(this, this.repoCache[repo])
}
this.getCategoryFromData(ids.id, opt, data, (err, category) =>{
if (category) {
category.setMap(this.map)
}
if (repo in this._loadClash) {
this._loadClash[repo].push(callback)
return
callback(err, category)
})
})
})
}
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 ]
/**
* @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) {
const repository = this.repositories[id]
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))
}
options.force = false
callback(repository.err, repository)
})
}
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()
}
return callback(repository.err, repository)
}
OpenStreetBrowserLoader.prototype.getRepository = function (id, options, callback) {
if (id in this.repositories) {
return callback(null, this.repositories[id])
this.repositories[id] = new Repository(id)
this.repositories[id].load((err) => callback(err, this.repositories[id]))
}
this.getRepo(id, options, (err, repoData) => {
if (err) {
return callback(err)
/**
* @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)
}
callback(null, this.repositories[id])
})
}
if (this.repositoryListCallbacks) {
return this.repositoryListCallbacks.push(callback)
}
/**
* @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 = {}
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)
})
}
var ids = this.getFullId(id, options)
if (options.force) {
delete this.templates[ids.fullId]
}
/**
* @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 (ids.fullId in this.templates) {
return callback(null, this.templates[ids.fullId])
}
var ids = this.getFullId(id, options)
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.getRepository(ids.repositoryId, opt, (err, repository) => {
// 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))
}
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])
}
OpenStreetBrowserLoader.prototype.getCategoryFromData = function (id, options, data, callback) {
var ids = this.getFullId(id, options)
if (err) { return callback(err) }
if (ids.fullId in this.categories) {
return callback(null, this.categories[ids.fullId])
}
this.templates[ids.fullId] = OverpassLayer.twig.twig({ data, autoescape: true })
if (!data.type) {
return callback(new Error('no type defined'), null)
callback(null, this.templates[ids.fullId])
})
})
}
if (!(data.type in this.types)) {
return callback(new Error('unknown type'), null)
}
getCategoryFromData (id, options, data, callback) {
var ids = this.getFullId(id, options)
let repository = this.repositories[ids.repositoryId]
if (ids.fullId in this.categories) {
return callback(null, this.categories[ids.fullId])
}
var opt = JSON.parse(JSON.stringify(options))
opt.id = ids.id
var layer = new this.types[data.type](opt, data, repository)
if (!data.type) {
data.type = 'overpass'
}
layer.setMap(this.map)
if (!(data.type in this.types)) {
return callback(new Error('unknown type'), null)
}
this.categories[ids.fullId] = layer
let repository = this.repositories[ids.repositoryId]
if ('load' in layer) {
layer.load(function (err) {
callback(err, layer)
})
} else {
callback(null, layer)
}
}
var opt = JSON.parse(JSON.stringify(options))
opt.id = ids.id
var layer = new this.types[data.type](opt, data, repository)
OpenStreetBrowserLoader.prototype.getFullId = function (id, options) {
var result = {}
layer.setMap(this.map)
if (!id) {
return null
}
this.categories[ids.fullId] = 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
if ('load' in layer) {
layer.load(function (err) {
callback(err, layer)
})
} else {
callback(null, layer)
}
}
result.sublayerId = null
m = result.entityId.split(/:/)
if (m.length > 1) {
result.sublayerId = m[0]
result.entityId = m[1]
}
getFullId (id, options) {
var result = {}
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]
}
result.fullId = result.repositoryId + '/' + (result.sublayerId ? result.sublayerId + ':' : '') + result.entityId
return result
}
OpenStreetBrowserLoader.prototype.forget = function (id) {
var ids = this.getFullId(id, options)
forget (id) {
var ids = this.getFullId(id, options)
this.categories[ids.fullId].remove()
delete this.categories[ids.fullId]
this.categories[ids.fullId].remove()
delete this.categories[ids.fullId]
}
registerType (type, classObject) {
this.types[type] = classObject
}
}
OpenStreetBrowserLoader.prototype.registerType = function (type, classObject) {
this.types[type] = classObject
OpenStreetBrowserLoader.prototype.registerRepository = function (id, repository) {
this.repositories[id] = repository
}
module.exports = new OpenStreetBrowserLoader()

86
src/Repository.js

@ -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])
}
}

8
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;
@ -80,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}");
}

2
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;

97
src/Window.js

@ -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;
}
}

232
src/addCategories.js

@ -4,31 +4,110 @@ require('./addCategories.css')
const tabs = require('modulekit-tabs')
const weightSort = require('weight-sort')
const state = require('./state')
const OpenStreetBrowserLoader = require('./OpenStreetBrowserLoader')
let tab
function addCategoriesShow (repo, options={}) {
let content = tab.content
let repoId
let branchId
function addCategoriesList (content, browser, options = {}) {
content.innerHTML = '<i class="fa fa-spinner fa-pulse fa-fw"></i> ' + lang('loading')
if (repo) {
[ repoId, branchId ] = repo.split(/~/)
}
OpenStreetBrowserLoader.getRepositoryList(options, function (err, repoData) {
if (err) {
return global.alert(err)
}
var categoryUrl = null
if (repoData.categoryUrl) {
categoryUrl = OverpassLayer.twig.twig({ data: repoData.categoryUrl, autoescape: true })
}
var list = {}
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)
while (content.lastChild) {
content.removeChild(content.lastChild)
}
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 = '#more-categories?id=' + 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 = '<i class="fa fa-file-code-o" aria-hidden="true"></i>'
li.appendChild(document.createTextNode(' '))
li.appendChild(editLink)
}
ul.appendChild(li)
}
content.appendChild(ul)
browser.catchLinks()
})
}
function addCategoriesShow (repo, browser, options={}) {
const content = browser.dom
let [ repoId, branchId ] = repo.split(/~/)
if (!branchId) {
branchId = 'master'
}
content.innerHTML = '<h3>' + lang('more_categories') + '</h3>' + '<i class="fa fa-spinner fa-pulse fa-fw"></i> ' + lang('loading')
content.innerHTML = '<i class="fa fa-spinner fa-pulse fa-fw"></i> ' + lang('loading')
OpenStreetBrowserLoader.getRepo(repo, options, function (err, repoData) {
OpenStreetBrowserLoader.getRepository(repo, options, function (err, repository) {
if (err) {
alert(err)
return global.alert(err)
}
content.innerHTML = '<h3>' + lang('more_categories') + '</h3>'
const repoData = repository.data
content.innerHTML = ''
var categoryUrl = null
if (repoData.categoryUrl) {
@ -37,66 +116,40 @@ function addCategoriesShow (repo, options={}) {
var list = {}
if (repo) {
var backLink = document.createElement('a')
backLink.className = 'back'
backLink.href = '#'
backLink.innerHTML = '<i class="fa fa-chevron-circle-left" aria-hidden="true"></i> '
backLink.appendChild(document.createTextNode(lang('back')))
var backLink = document.createElement('a')
backLink.className = 'back'
backLink.href = '#more-categories?'
backLink.innerHTML = '<i class="fa fa-chevron-circle-left" aria-hidden="true"></i> '
backLink.appendChild(document.createTextNode(lang('back')))
content.appendChild(backLink)
browser.catchLinks()
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)
}
let h = document.createElement('h2')
h.appendChild(document.createTextNode(repoId))
content.appendChild(h)
list = weightSort(repoData, {
key: 'timestamp',
reverse: true
})
}
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 = () => browser.close()
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 = '#more-categories?id=' + repo + '&force=true'
li.appendChild(text)
if ('branches' in repoData) {
let li = document.createElement('li')
@ -131,7 +184,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')
@ -144,21 +197,11 @@ 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 = () => browser.close()
li.appendChild(a)
a.appendChild(document.createTextNode('name' in data ? lang(data.name) : id))
@ -185,28 +228,33 @@ 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)
if (!Object.keys(parameters).length) {
let block = document.createElement('div')
block.setAttribute('weight', 1)
content.appendChild(block)
tab.header.innerHTML = '<i class="fa fa-plus" aria-hidden="true"></i>'
tab.header.title = lang('more_categories')
let header = document.createElement('h4')
header.innerHTML = lang('repositories')
block.appendChild(header)
let initialized = false
let div = document.createElement('div')
block.appendChild(div)
addCategoriesList(div, browser, parameters)
tab.on('select', () => {
if (!initialized) {
addCategoriesShow()
initialized = true
}
})
browser.catchLinks()
}
else if (parameters.id) {
addCategoriesShow(parameters.id, browser, parameters)
}
else if (parameters.repo || parameters.categories) {
state.apply(parameters)
browser.close()
}
})

464
src/customCategory.js

@ -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
}
}

99
src/customCategory.php

@ -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();

9
src/database.php

@ -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']);
}
});

7
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))
}

2
src/index.js

@ -33,6 +33,7 @@ require('./markers')
require('./categories')
require('./wikipedia')
require('./image')
require('./moreCategories')
require('./addCategories')
require('./permalink')
//require('./leaflet-geo-search')
@ -42,6 +43,7 @@ require('./GeoInfo')
require('./PluginMeasure')
require('./PluginGeoLocate')
require('./tagsDisplay-tag2link')
require('./customCategory')
const ObjectDisplay = require('./ObjectDisplay')
let currentObjectDisplay = null

35
src/moreCategories.js

@ -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()
})
})

4
src/options.php

@ -1,5 +1,7 @@
<?php
function ajax_options_save($get_param, $post_param) {
function ajax_options_save($get_param, $postdata) {
$post_param = json_decode($postdata, true);
call_hooks('options_save', $post_param);
$_SESSION['options'] = $post_param;

40
src/repositories.php

@ -1,7 +1,6 @@
<?php
function getRepositories () {
global $repositories;
global $repositoriesGitea;
$result = array();
if (isset($repositories)) {
@ -15,33 +14,7 @@ function getRepositories () {
);
}
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',
);
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);
}
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);
}
});

44
src/repositoriesGitea.php

@ -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);
}
});

4
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))
})

34
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 = {}
@ -140,3 +141,36 @@ 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] || {}
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 = 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
}

79
style.css

@ -612,3 +612,82 @@ ul.overpass-layer-list > li > a > .content > .details {
margin-bottom: 0.25em;
background-color: #ffdfdf;
}
.Window > .content textarea {
height: 100%;
width: 100%;
resize: none;
box-sizing: border-box;
}
.Window > .content .controls {
flex-grow: 0;
}
.Window > .content .controls > .actions {
margin-left: 1em;
margin-right: 1em;
display: inline;
}
/* 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;
}
.tip-tutorial {
padding: 0.25em 0;
display: inline-block;
}
.tip-tutorial a {
text-decoration: underline;
}
/* 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;
}
Loading…
Cancel
Save