diff --git a/README.md b/README.md
index 4455a892..a06c76e9 100644
--- a/README.md
+++ b/README.md
@@ -90,7 +90,7 @@ The following values are possible for categories (the only mandatory value is qu
* feature: an object describing how the feature will be formated resp. styled.
* style: a Leaflet style.
* stroke: Whether to draw stroke along the path. Set it to false or empty string to disable borders on polygons or circles. (boolean, true)
- * weight: Stroke width in pixels (number, 3)
+ * width: Stroke width in pixels (number, 3)
* color: Stroke color (string, '#3388ff')
* opacity: Stroke opacity (number, 1.0)
* lineCap: shape at end of the stroke (string, 'round')
diff --git a/conf.php-dist b/conf.php-dist
index caa529f2..d0339532 100644
--- a/conf.php-dist
+++ b/conf.php-dist
@@ -1,10 +1,31 @@
array(
+ 'path' => 'node_modules/openstreetbrowser-categories-main',
+ 'type' => 'dir',
+ // public URL of repository
+ 'repositoryUrl' => 'https://github.com/example/categories',
+ // public URL of source of a category in repository
+ 'categoryUrl' => 'https://github.com/example/categories/tree/master/{{ categoryId }}.json',
+ ),
+);
+
+// Repositories which should be included from gitea
+$repositoriesGitea = array(
+ 'path' => "/home/gitea/gitea-repositories",
+ 'url' => "https://www.openstreetbrowser.org/dev",
+);
// Set to true to reload categories on every page visit.
$config['categoriesAlwaysReload'] = true;
+// (optional) URL, which points to the OpenStreetBrowser Editor
+#$config['urlCategoriesEditor'] = 'editor/';
+
// URL of the Overpass API
$config['overpassUrl'] = array(
'//overpass-api.de/api/interpreter',
diff --git a/doc/TwigJS.md b/doc/TwigJS.md
index bc740b14..2a86392b 100644
--- a/doc/TwigJS.md
+++ b/doc/TwigJS.md
@@ -1,17 +1,55 @@
+#### Examples
+Twig resp. TwigJS is a template language. Example:
+```twig
+Value of property "test": {{ test }}.
+```
+
+If-condition:
+```twig
+{% if test == "foo" %}
+It's foo!
+{% elseif test == "bar" %}
+It's bar!
+{% else %}
+Other value: {{ test }}
+{% endif %}
+```
+
+For-loop:
+```twig
+{% for k, v in tags %}
+Tag {{ k }} has value {{ v }}
+{% endfor %}
+```
+
+Assign value to variable:
+```twig
+{% set k = "foo" %}
+```
+
+For more information, please visit:
+* [https://twig.symfony.com/](Page of the original Twig template language)
+* [https://github.com/twigjs/twig.js/wiki](Wiki of the TwigJS template language which is almost identical to Twig)
+
#### TwigJS templates
-The following properties are available:
-* id (the id of the object is always available, prefixed 'n' for nodes, 'w' for ways and 'r' for relations; e.g. 'n1234')
-* osm_id (the numerical id of the object)
-* layer_id (the id of the category)
-* type ('node', 'way' or 'relation')
-* tags.* (all tags are available with the prefix tags., e.g. tags.amenity)
-* meta.timestamp (timestamp of last modification)
-* meta.version (version of the object)
-* meta.changeset (ID of the changeset, the object was last modified in)
-* meta.user (Username of the user, who changed the object last)
-* meta.uid (UID of the user, who changed the object last)
-* map.zoom (Current zoom level)
-* const.* (Values from the 'const' option)
+When rendering map features, the following properties are available:
+* `id` (the id of the object is always available, prefixed 'n' for nodes, 'w' for ways and 'r' for relations; e.g. 'n1234')
+* `osm_id` (the numerical id of the object)
+* `layer_id` (the id of the category)
+* `type` ('node', 'way' or 'relation')
+* `tags.*` (all tags are available with the prefix tags., e.g. tags.amenity)
+* `meta.timestamp` (timestamp of last modification)
+* `meta.version` (version of the object)
+* `meta.changeset` (ID of the changeset, the object was last modified in)
+* `meta.user` (Username of the user, who changed the object last)
+* `meta.uid` (UID of the user, who changed the object last)
+* `map.zoom` (Current zoom level)
+* `const.*` (Values from the 'const' option)
+
+For the info-section of a category the following properties are available:
+* `layer_id` (the id of the category)
+* `map.zoom` (Current zoom level)
+* `const.*` (Values from the 'const' option)
There are several extra functions defined for the TwigJS language:
* function `keyTrans`: return the translation of the given key. Parameters: key (required, e.g. 'amenity').
diff --git a/lang/ast.json b/lang/ast.json
index d6d91532..6e2dfef4 100644
--- a/lang/ast.json
+++ b/lang/ast.json
@@ -1,6 +1,7 @@
{
"main:options": "Opciones",
"more": "más",
+ "more_categories": "Más categoríes",
"options:data_lang": "Llingua de los datos",
"options:data_lang:local": "Llingua llocal",
"options:ui_lang": "Llingua de la interfaz",
diff --git a/lang/ca.json b/lang/ca.json
index 125c6797..9d4776cc 100644
--- a/lang/ca.json
+++ b/lang/ca.json
@@ -1,5 +1,6 @@
{
"main:options": "Opcions",
"more": "més",
+ "more_categories": "Més categories",
"save": "Guardar"
}
\ No newline at end of file
diff --git a/lang/cs.json b/lang/cs.json
index 9ee96b8f..7c670980 100644
--- a/lang/cs.json
+++ b/lang/cs.json
@@ -1,6 +1,7 @@
{
"main:options": "Nastavení",
"more": "více",
+ "more_categories": "Více kategorií",
"options:data_lang": "Jazyk dat",
"options:data_lang:local": "Místní jazyk",
"options:ui_lang": "Jazyk rozhraní",
diff --git a/lang/da.json b/lang/da.json
index 8145e1e2..ba4c1059 100644
--- a/lang/da.json
+++ b/lang/da.json
@@ -1,6 +1,7 @@
{
"main:options": "Indstillinger",
"more": "mere",
+ "more_categories": "Flere kategorier",
"options:data_lang": "Data sprog",
"options:data_lang:local": "Lokalt sprog",
"options:ui_lang": "Brugerfladesprog",
diff --git a/lang/de.json b/lang/de.json
index 07e2b059..c52e8f57 100644
--- a/lang/de.json
+++ b/lang/de.json
@@ -1,4 +1,5 @@
{
+ "back": "zurück",
"category-info-tooltip": "Info & Legende",
"closed": "geschlossen",
"default": "Standard",
@@ -10,6 +11,8 @@
"images": "Bilder",
"main:options": "Optionen",
"more": "mehr",
+ "more_categories": "Mehr Kategorien",
+ "more_categories_gitea": "Erstelle und verbessere Kategorien hier!",
"open": "geöffnet",
"options:data_lang": "Datensprache",
"options:data_lang:local": "Lokale Sprache",
diff --git a/lang/el.json b/lang/el.json
index c909aa30..d1fffd22 100644
--- a/lang/el.json
+++ b/lang/el.json
@@ -1,6 +1,7 @@
{
"main:options": "Επιλογές",
"more": "περισσότερα",
+ "more_categories": "Περισσότερες κατηγορίες",
"options:data_lang": "Γλωσσα δεδομένων",
"options:data_lang:local": "Τοπική γλώσσα",
"options:ui_lang": "Γλώσσα διεπαφής",
diff --git a/lang/en.json b/lang/en.json
index db92c67c..8bb7dfe5 100644
--- a/lang/en.json
+++ b/lang/en.json
@@ -1,4 +1,5 @@
{
+ "back": "back",
"category-info-tooltip": "Info & Map key",
"closed": "closed",
"default": "default",
@@ -10,6 +11,8 @@
"images": "Images",
"main:options": "Options",
"more": "more",
+ "more_categories": "More categories",
+ "more_categories_gitea": "Create & improve categories yourself!",
"open": "open",
"options:data_lang": "Data language",
"options:data_lang:desc": "Many map features have their name (and other tags) translated to different languages (e.g. with 'name:en', 'name:de'). Specify which language should be used for displaying, or 'Local language' so that always the untranslated value (e.g. 'name') will be used",
diff --git a/lang/es.json b/lang/es.json
index 0f4a7c32..310b5aab 100644
--- a/lang/es.json
+++ b/lang/es.json
@@ -1,6 +1,7 @@
{
"main:options": "Opciones",
"more": "más",
+ "more_categories": "Más categorías",
"options:data_lang": "Idioma de datos",
"options:data_lang:local": "Idioma local",
"options:ui_lang": "Idioma de interfaz",
diff --git a/lang/et.json b/lang/et.json
index 7028b216..4b6a190d 100644
--- a/lang/et.json
+++ b/lang/et.json
@@ -1,6 +1,7 @@
{
"main:options": "Valikud",
"more": "lisaks",
+ "more_categories": "Rohkem kategooriaid",
"options:data_lang": "Andmete keel",
"options:data_lang:local": "Kohalik keel",
"options:ui_lang": "Kasutajaliidese keel",
diff --git a/lang/fr.json b/lang/fr.json
index e9c15df0..4cbf03d4 100644
--- a/lang/fr.json
+++ b/lang/fr.json
@@ -1,6 +1,7 @@
{
"main:options": "Options",
"more": "plus",
+ "more_categories": "Plus de catégories",
"options:data_lang": "Langue des données",
"options:data_lang:local": "Langue locale",
"options:ui_lang": "Langue de l'interface",
diff --git a/lang/hu.json b/lang/hu.json
index 0d5c91da..b8422d91 100644
--- a/lang/hu.json
+++ b/lang/hu.json
@@ -1,6 +1,7 @@
{
"main:options": "Beállítások",
"more": "több",
+ "more_categories": "Több kategória",
"options:data_lang": "Adatnyelv",
"options:data_lang:local": "Helyi nyelv",
"options:ui_lang": "Menünyelv",
diff --git a/lang/it.json b/lang/it.json
index 3746ee15..9830f55a 100644
--- a/lang/it.json
+++ b/lang/it.json
@@ -1,6 +1,7 @@
{
"main:options": "Opzioni",
"more": "altri",
+ "more_categories": "Altre categorie",
"options:data_lang": "Lingua dei dati",
"options:data_lang:local": "Lingua del tuo browser",
"options:ui_lang": "Lingua dell'interfaccia",
diff --git a/lang/ja.json b/lang/ja.json
index 7c37bd2f..677dd258 100644
--- a/lang/ja.json
+++ b/lang/ja.json
@@ -1,6 +1,7 @@
{
"main:options": "オプション設定",
"more": "もっと",
+ "more_categories": "カテゴリを一覧から追加",
"options:data_lang": "データ表示",
"options:data_lang:local": "ブラウザの設定言語",
"options:ui_lang": "インタフェース表示",
diff --git a/lang/nl.json b/lang/nl.json
index 7bdb8e7b..812f5d23 100644
--- a/lang/nl.json
+++ b/lang/nl.json
@@ -1,6 +1,7 @@
{
"main:options": "Opties",
"more": "meer",
+ "more_categories": "Meer categorieën",
"options:data_lang": "Taal voor data",
"options:data_lang:local": "Lokale taal",
"options:ui_lang": "Interfacetaal",
diff --git a/lang/pl.json b/lang/pl.json
index 1b281fb5..108ffafb 100644
--- a/lang/pl.json
+++ b/lang/pl.json
@@ -1,6 +1,7 @@
{
"main:options": "Opcje",
"more": "więcej",
+ "more_categories": "Więcej kategorii",
"options:data_lang": "Język danych",
"options:data_lang:local": "Język lokalny",
"options:ui_lang": "Język interfejsu",
diff --git a/lang/ro.json b/lang/ro.json
index b2faa188..93199b31 100644
--- a/lang/ro.json
+++ b/lang/ro.json
@@ -1,6 +1,7 @@
{
"main:options": "Optiuni",
"more": "Mai mult",
+ "more_categories": "Mai multe categorii",
"options:data_lang": "Limba date",
"options:data_lang:local": "Limba locala",
"options:ui_lang": "Limba interfata",
diff --git a/lang/ru.json b/lang/ru.json
index 66dac2a2..29cb888f 100644
--- a/lang/ru.json
+++ b/lang/ru.json
@@ -1,6 +1,7 @@
{
"main:options": "Настройки",
"more": "Ещё",
+ "more_categories": "Больше категорий",
"options:data_lang": "Язык информации на карте",
"options:data_lang:local": "Определить язык автоматически",
"options:ui_lang": "Язык интерфейса",
diff --git a/lang/sr.json b/lang/sr.json
index 4a52d62d..c462a670 100644
--- a/lang/sr.json
+++ b/lang/sr.json
@@ -1,6 +1,7 @@
{
"main:options": "Опције",
"more": "још",
+ "more_categories": "Више категорија",
"options:data_lang": "Језик подетака",
"options:data_lang:local": "Локални језик",
"options:ui_lang": "Језик интерфејса",
diff --git a/lang/template.json b/lang/template.json
index 8bdbd5bf..e127a61a 100644
--- a/lang/template.json
+++ b/lang/template.json
@@ -2,6 +2,7 @@
"default": "",
"main:options": "",
"more": "",
+ "more_categories": "",
"options:data_lang": "",
"options:data_lang:desc": "",
"options:data_lang:local": "",
diff --git a/lang/uk.json b/lang/uk.json
index 93ab3b75..bbe22f2d 100644
--- a/lang/uk.json
+++ b/lang/uk.json
@@ -1,6 +1,7 @@
{
"main:options": "Налаштування",
"more": "Ще",
+ "more_categories": "Більше категорій",
"options:data_lang": "Мова мапи",
"options:data_lang:local": "Місцева мова",
"options:ui_lang": "Мова інтерфейсу",
diff --git a/lib/modulekit/form b/lib/modulekit/form
index 819380c6..4a94f64c 160000
--- a/lib/modulekit/form
+++ b/lib/modulekit/form
@@ -1 +1 @@
-Subproject commit 819380c621e2ec79d5ca22db6af44d0eaf1c158c
+Subproject commit 4a94f64c11d3f16b01a5aec6afd5cfb4b7257572
diff --git a/lib/modulekit/lang b/lib/modulekit/lang
index 1013930a..80118dbc 160000
--- a/lib/modulekit/lang
+++ b/lib/modulekit/lang
@@ -1 +1 @@
-Subproject commit 1013930aafa47f214f6e9d4b68c684acf4922efc
+Subproject commit 80118dbcaafa9ab95298be95548126071efc069f
diff --git a/modulekit.php b/modulekit.php
index 20c6354b..168eb2e7 100644
--- a/modulekit.php
+++ b/modulekit.php
@@ -16,6 +16,10 @@ $include = array(
'src/ip-location.php',
'src/wikipedia.php',
'src/ImageLoader.php',
+ 'src/RepositoryBase.php',
+ 'src/RepositoryDir.php',
+ 'src/RepositoryGit.php',
+ 'src/repositories.php',
),
'css' => array(
'style.css',
diff --git a/package.json b/package.json
index 40b06373..49e813dd 100644
--- a/package.json
+++ b/package.json
@@ -26,7 +26,8 @@
"openstreetmap-tag-translations": "https://github.com/plepe/openstreetmap-tag-translations",
"overpass-layer": "https://github.com/plepe/overpass-layer",
"query-string": "^5.0.0",
- "sheet-router": "^4.2.3"
+ "sheet-router": "^4.2.3",
+ "weight-sort": "^1.3.0"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
diff --git a/repo.php b/repo.php
new file mode 100644
index 00000000..2bbec439
--- /dev/null
+++ b/repo.php
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+ $repoData) {
+ $repo = getRepo($repoId, $repoData);
+
+ print $c++ ? ',' : '';
+ print json_encode($repoId, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES) . ':';
+ $info = $repo->info();
+
+ if (isset($repoData['repositoryUrl'])) {
+ $info['repositoryUrl'] = $repoData['repositoryUrl'];
+ }
+ if (isset($repoData['categoryUrl'])) {
+ $info['categoryUrl'] = $repoData['categoryUrl'];
+ }
+
+ print json_encode($info, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_FORCE_OBJECT);
+ }
+
+ print '}';
+ exit(0);
+}
+
+$repoId = $_REQUEST['repo'];
+if (!array_key_exists($repoId, $allRepositories)) {
+ Header("HTTP/1.1 404 Repository not found");
+ exit(0);
+}
+
+$repoData = $allRepositories[$repoId];
+$repo = getRepo($repoId, $repoData);
+
+$cacheDir = null;
+$ts = $repo->timestamp($path);
+if (isset($config['cache'])) {
+ $cacheDir = "{$config['cache']}/repo";
+ @mkdir($cacheDir);
+ $cacheTs = filemtime("{$cacheDir}/{$repoId}.json");
+ if ($cacheTs === $ts) {
+ Header("Content-Type: application/json; charset=utf-8");
+ readfile("{$cacheDir}/{$repoId}.json");
+ exit(0);
+ }
+}
+
+$data = $repo->data();
+
+if (isset($repoData['repositoryUrl'])) {
+ $data['repositoryUrl'] = $repoData['repositoryUrl'];
+}
+if (isset($repoData['categoryUrl'])) {
+ $data['categoryUrl'] = $repoData['categoryUrl'];
+}
+
+$ret = json_encode($data, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);
+
+Header("Content-Type: application/json; charset=utf-8");
+print $ret;
+
+file_put_contents("{$cacheDir}/{$repoId}.json", $ret);
+touch("{$cacheDir}/{$repoId}.json", $ts);
diff --git a/src/CategoryBase.js b/src/CategoryBase.js
index 4e9fb3eb..8d648156 100644
--- a/src/CategoryBase.js
+++ b/src/CategoryBase.js
@@ -2,8 +2,15 @@
var OpenStreetBrowserLoader = require('./OpenStreetBrowserLoader')
var tabs = require('modulekit-tabs')
-function CategoryBase (id, data) {
- this.id = id
+function CategoryBase (options, data) {
+ if (typeof options === 'string') {
+ this.id = options
+ this.options = {}
+ }
+ else {
+ this.id = options.id
+ this.options = options
+ }
this.parentCategory = null
this.childrenLoadingCount = 0
this.data = data
@@ -35,7 +42,14 @@ function CategoryBase (id, data) {
a.onclick = this.toggle.bind(this)
domHeader.appendChild(a)
- if (options.debug) {
+ if (this.options.repositoryId && this.options.repositoryId !== 'default') {
+ a = document.createElement('span')
+ a.className = 'repoId'
+ a.appendChild(document.createTextNode(this.options.repositoryId))
+ domHeader.appendChild(a)
+ }
+
+ if (this.shallShowReload()) {
a = document.createElement('a')
a.appendChild(document.createTextNode('⟳'))
a.title = lang('reload')
@@ -66,6 +80,14 @@ function CategoryBase (id, data) {
this.dom.appendChild(this.domContent)
}
+CategoryBase.prototype.load = function (callback) {
+ callback()
+}
+
+CategoryBase.prototype.shallShowReload = function () {
+ return options.debug
+}
+
CategoryBase.prototype.setMap = function (map) {
this.map = map
}
@@ -137,7 +159,7 @@ CategoryBase.prototype.reload = function (callback) {
OpenStreetBrowserLoader.forget(this.id)
- OpenStreetBrowserLoader.getCategory(this.id, function (err, category) {
+ OpenStreetBrowserLoader.getCategory(this.id, { force: true }, function (err, category) {
if (err) {
return callback(err)
}
diff --git a/src/CategoryIndex.js b/src/CategoryIndex.js
index 47f7d712..dc2d0828 100644
--- a/src/CategoryIndex.js
+++ b/src/CategoryIndex.js
@@ -5,8 +5,8 @@ var CategoryBase = require('./CategoryBase')
CategoryIndex.prototype = Object.create(CategoryBase.prototype)
CategoryIndex.prototype.constructor = CategoryIndex
-function CategoryIndex (id, data) {
- CategoryBase.call(this, id, data)
+function CategoryIndex (options, data) {
+ CategoryBase.call(this, options, data)
this.childrenDoms = {}
this.childrenCategories = null
@@ -51,9 +51,9 @@ CategoryIndex.prototype._loadChildrenCategories = function (callback) {
this.childrenCategories[data.id] = null
if ('type' in data) {
- OpenStreetBrowserLoader.getCategoryFromData(data.id, data, this._loadChildCategory.bind(this, callback))
+ OpenStreetBrowserLoader.getCategoryFromData(data.id, this.options, data, this._loadChildCategory.bind(this, data.id, callback))
} else {
- OpenStreetBrowserLoader.getCategory(data.id, this._loadChildCategory.bind(this, callback))
+ OpenStreetBrowserLoader.getCategory(data.id, this.options, this._loadChildCategory.bind(this, data.id, callback))
}
}.bind(this),
function (err) {
@@ -64,15 +64,15 @@ CategoryIndex.prototype._loadChildrenCategories = function (callback) {
)
}
-CategoryIndex.prototype._loadChildCategory = function (callback, err, category) {
+CategoryIndex.prototype._loadChildCategory = function (id, callback, err, category) {
if (err) {
return callback(err)
}
- this.childrenCategories[category.id] = category
+ this.childrenCategories[id] = category
category.setParent(this)
- category.setParentDom(this.childrenDoms[category.id])
+ category.setParentDom(this.childrenDoms[id])
callback(err, category)
}
diff --git a/src/CategoryOverpass.js b/src/CategoryOverpass.js
index 86c8f8fa..ada7d88a 100644
--- a/src/CategoryOverpass.js
+++ b/src/CategoryOverpass.js
@@ -11,7 +11,7 @@ var defaultValues = {
markerSign: '',
'style:hover': {
color: 'black',
- weight: 3,
+ width: 3,
opacity: 1,
radius: 12,
fill: false
@@ -26,10 +26,10 @@ var defaultValues = {
CategoryOverpass.prototype = Object.create(CategoryBase.prototype)
CategoryOverpass.prototype.constructor = CategoryOverpass
-function CategoryOverpass (id, data) {
+function CategoryOverpass (options, data) {
var p
- CategoryBase.call(this, id, data)
+ CategoryBase.call(this, options, data)
data.id = this.id
@@ -63,6 +63,7 @@ function CategoryOverpass (id, data) {
data.feature.appUrl = '#' + this.id + '/{{ id }}'
data.styleNoBindPopup = [ 'hover' ]
+ data.stylesNoAutoShow = [ 'hover' ]
this.layer = new OverpassLayer(data)
@@ -155,7 +156,7 @@ function CategoryOverpass (id, data) {
}
CategoryOverpass.prototype.load = function (callback) {
- OpenStreetBrowserLoader.getTemplate('popupBody', function (err, template) {
+ OpenStreetBrowserLoader.getTemplate('popupBody', this.options, function (err, template) {
if (err) {
console.log("can't load popupBody.html")
} else {
@@ -255,8 +256,13 @@ CategoryOverpass.prototype.updateInfo = function () {
}
global.currentCategory = this
- var data = JSON.parse(JSON.stringify(this.data))
- data.map = { zoom: map.getZoom() }
+ var data = {
+ layer_id: this.id,
+ 'const': this.data.const,
+ }
+ if (this.map) {
+ data.map = { zoom: map.getZoom() }
+ }
this.domInfo.innerHTML = this.templateInfo.render(data)
global.currentCategory = null
}
@@ -323,7 +329,7 @@ CategoryOverpass.prototype.updatePopupContent = function (object, popup) {
}
CategoryOverpass.prototype.renderTemplate = function (object, templateId, callback) {
- OpenStreetBrowserLoader.getTemplate(templateId, function (err, template) {
+ OpenStreetBrowserLoader.getTemplate(templateId, this.options, function (err, template) {
if (err) {
err = "can't load " + templateId + ': ' + err
return callback(err, null)
diff --git a/src/OpenStreetBrowserLoader.js b/src/OpenStreetBrowserLoader.js
index 6889dbac..070310a2 100644
--- a/src/OpenStreetBrowserLoader.js
+++ b/src/OpenStreetBrowserLoader.js
@@ -4,6 +4,7 @@ var jsonMultilineStrings = require('json-multiline-strings')
function OpenStreetBrowserLoader () {
this.types = {}
this.categories = {}
+ this.repoCache = {}
this.templates = {}
this._loadClash = {} // if a category is being loaded multiple times, collect callbacks
}
@@ -12,83 +13,168 @@ OpenStreetBrowserLoader.prototype.setMap = function (map) {
this.map = map
}
-OpenStreetBrowserLoader.prototype.getCategory = function (id, callback) {
- if (id in this.categories) {
- callback(null, this.categories[id])
- return
+/**
+ * @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 = {}
}
- if (id in this._loadClash) {
- this._loadClash[id].push(callback)
- return
+ var ids = this.getFullId(id, options)
+ if (ids === null) {
+ return callback('invalid id', null)
}
- this._loadClash[id] = []
+ if (options.force) {
+ delete this.categories[ids.fullId]
+ }
- function reqListener (req) {
- if (req.status !== 200) {
- console.log(req)
- return callback(req.statusText, null)
+ if (ids.fullId in this.categories) {
+ return callback(null, this.categories[ids.fullId])
+ }
+
+ var opt = JSON.parse(JSON.stringify(options))
+ opt.categoryId = ids.entityId
+ opt.repositoryId = ids.repositoryId
+
+ 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 data = JSON.parse(req.responseText)
- data = jsonMultilineStrings.join(data, { exclude: [ [ 'const' ] ] })
+ if (!(ids.entityId in repoData.categories)) {
+ return callback(new Error('category not defined'), null)
+ }
- this.getCategoryFromData(id, data, function (err, category) {
+ this.getCategoryFromData(ids.id, opt, repoData.categories[ids.entityId], function (err, category) {
if (category) {
category.setMap(this.map)
}
callback(err, category)
-
- this._loadClash[id].forEach(function (c) {
- c(err, category)
- })
- delete this._loadClash[id]
}.bind(this))
- }
-
- var req = new XMLHttpRequest()
- req.addEventListener('load', reqListener.bind(this, req))
- req.open('GET', config.categoriesDir + '/' + id + '.json?' + config.categoriesRev)
- req.send()
+ }.bind(this))
}
-OpenStreetBrowserLoader.prototype.getTemplate = function (id, callback) {
- if (id in this.templates) {
- callback.apply(this, this.templates[id])
- 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)
+ */
+OpenStreetBrowserLoader.prototype.getRepo = function (repo, options, callback) {
+ if (options.force) {
+ delete this.repoCache[repo]
}
- if (id in this._loadClash) {
- this._loadClash[id].push(callback)
+ if (repo in this.repoCache) {
+ return callback.apply(this, this.repoCache[repo])
+ }
+
+ if (repo in this._loadClash) {
+ this._loadClash[repo].push(callback)
return
}
- this._loadClash[id] = []
+ this._loadClash[repo] = [ callback ]
function reqListener (req) {
if (req.status !== 200) {
- console.log(req)
- this.templates[id] = [ req.statusText, null ]
+ console.log('http error when loading repository', req)
+ this.repoCache[repo] = [ req.statusText, null ]
} else {
- this.templates[id] = [ null, OverpassLayer.twig.twig({ data: req.responseText, autoescape: true }) ]
+ try {
+ this.repoCache[repo] = [ null, JSON.parse(req.responseText) ]
+ } catch (err) {
+ console.log('couldn\'t parse repository', req.responseText)
+ this.repoCache[repo] = [ 'couldn\t parse repository', null ]
+ }
}
- callback.apply(this, this.templates[id])
+ var todo = this._loadClash[repo]
+ delete this._loadClash[repo]
- this._loadClash[id].forEach(function (c) {
- c.apply(this, this.templates[id])
+ todo.forEach(function (callback) {
+ callback.apply(this, this.repoCache[repo])
}.bind(this))
}
+ var param = []
+ if (repo) {
+ param.push('repo=' + encodeURIComponent(repo))
+ }
+ param.push(config.categoriesRev)
+ param = param.length ? '?' + param.join('&') : ''
+
var req = new XMLHttpRequest()
req.addEventListener('load', reqListener.bind(this, req))
- req.open('GET', config.categoriesDir + '/' + id + '.html?' + config.categoriesRev)
+ req.open('GET', 'repo.php' + param)
req.send()
}
-OpenStreetBrowserLoader.prototype.getCategoryFromData = function (id, data, 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 = {}
+ }
+
+ var ids = this.getFullId(id, options)
+
+ if (options.force) {
+ delete this.templates[ids.fullId]
+ }
+
+ 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
+
+ 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)
+ }
+
+ if (!repoData.templates || !(ids.entityId in repoData.templates)) {
+ return callback(new Error('template not defined'), null)
+ }
+
+ this.templates[ids.fullId] = OverpassLayer.twig.twig({ data: repoData.templates[ids.entityId], autoescape: true })
+
+ callback(null, this.templates[ids.fullId])
+ }.bind(this))
+}
+
+OpenStreetBrowserLoader.prototype.getCategoryFromData = function (id, options, data, callback) {
+ var ids = this.getFullId(id, options)
+
+ if (ids.fullId in this.categories) {
+ return callback(null, this.categories[ids.fullId])
+ }
+
if (!data.type) {
return callback(new Error('no type defined'), null)
}
@@ -97,11 +183,13 @@ OpenStreetBrowserLoader.prototype.getCategoryFromData = function (id, data, call
return callback(new Error('unknown type'), null)
}
- var layer = new this.types[data.type](id, data)
+ var opt = JSON.parse(JSON.stringify(options))
+ opt.id = ids.id
+ var layer = new this.types[data.type](opt, data)
layer.setMap(this.map)
- this.categories[id] = layer
+ this.categories[ids.fullId] = layer
if ('load' in layer) {
layer.load(function (err) {
@@ -112,9 +200,38 @@ OpenStreetBrowserLoader.prototype.getCategoryFromData = function (id, data, call
}
}
+OpenStreetBrowserLoader.prototype.getFullId = function (id, options) {
+ var result = {}
+
+ if (!id) {
+ return null
+ }
+
+ var m
+ if (m = id.match(/^(.*)\/([^\/]*)/)) {
+ 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
+ }
+
+ result.fullId = result.repositoryId + '/' + result.entityId
+
+ return result
+}
+
OpenStreetBrowserLoader.prototype.forget = function (id) {
- this.categories[id].remove()
- delete this.categories[id]
+ var ids = this.getFullId(id, options)
+
+ this.categories[ids.fullId].remove()
+ delete this.categories[ids.fullId]
}
OpenStreetBrowserLoader.prototype.registerType = function (type, classObject) {
diff --git a/src/RepositoryBase.php b/src/RepositoryBase.php
new file mode 100644
index 00000000..f5f666f5
--- /dev/null
+++ b/src/RepositoryBase.php
@@ -0,0 +1,43 @@
+def = $def;
+ $this->path = $def['path'];
+ }
+
+ function timestamp () {
+ return null;
+ }
+
+ function info () {
+ $ret = array();
+
+ foreach (array('name') as $k) {
+ if (array_key_exists($k, $this->def)) {
+ $ret[$k] = $this->def[$k];
+ }
+ }
+
+ $ret['timestamp'] = Date(DATE_ISO8601, $this->timestamp());
+
+ return $ret;
+ }
+
+ function data () {
+ $data = array(
+ 'categories' => array(),
+ 'templates' => array(),
+ 'timestamp' => Date(DATE_ISO8601, $this->timestamp()),
+ );
+
+ return $data;
+ }
+
+ function isCategory ($data) {
+ if (!array_key_exists('type', $data)) {
+ return false;
+ }
+
+ return in_array($data['type'], array('index', 'overpass'));
+ }
+}
diff --git a/src/RepositoryDir.php b/src/RepositoryDir.php
new file mode 100644
index 00000000..5ec2d7ee
--- /dev/null
+++ b/src/RepositoryDir.php
@@ -0,0 +1,52 @@
+path);
+ while ($f = readdir($d)) {
+ $t = filemtime("{$this->path}/{$f}");
+ if ($t > $ts) {
+ $ts = $t;
+ }
+ }
+ closedir($d);
+
+ return $ts;
+ }
+
+ function data () {
+ $data = parent::data();
+
+ $d = opendir($this->path);
+ while ($f = readdir($d)) {
+ if (preg_match("/^([0-9a-zA-Z_\-]+)\.json$/", $f, $m) && $f !== 'package.json') {
+ $d1 = json_decode(file_get_contents("{$this->path}/{$f}"), true);
+
+ if (!$this->isCategory($d1)) {
+ continue;
+ }
+
+ $data['categories'][$m[1]] = jsonMultilineStringsJoin($d1, array('exclude' => array(array('const'))));
+ }
+
+ if (preg_match("/^(detailsBody|popupBody).html$/", $f, $m)) {
+ $data['templates'][$m[1]] = file_get_contents("{$this->path}/{$f}");
+ }
+ }
+ closedir($d);
+
+ return $data;
+ }
+
+ function scandir($path="") {
+ return scandir("{$this->path}/{$path}");
+ }
+
+ function file_get_contents ($file) {
+ return file_get_contents("{$this->path}/{$file}");
+ }
+
+ function file_put_contents ($file, $content) {
+ return file_put_contents("{$this->path}/{$file}", $content);
+ }
+}
diff --git a/src/RepositoryGit.php b/src/RepositoryGit.php
new file mode 100644
index 00000000..7058c592
--- /dev/null
+++ b/src/RepositoryGit.php
@@ -0,0 +1,58 @@
+path) . "; git log -1 --pretty=format:%ct");
+
+ return $ts;
+ }
+
+ function data () {
+ $data = parent::data();
+
+ $d = popen("cd " . escapeShellArg($this->path) . "; git ls-tree HEAD", "r");
+ while ($r = fgets($d)) {
+ if (preg_match("/^[0-9]{6} blob [0-9a-f]{40}\t(([0-9a-zA-Z_\-]+)\.json)$/", $r, $m)) {
+ $f = $m[1];
+ $id = $m[2];
+
+ if ($f === 'package.json') {
+ continue;
+ }
+
+ $d1 = json_decode(shell_exec("cd " . escapeShellArg($this->path) . "; git show HEAD:" . escapeShellArg($f)), true);
+
+ if (!$this->isCategory($d1)) {
+ continue;
+ }
+
+ $data['categories'][$id] = jsonMultilineStringsJoin($d1, array('exclude' => array(array('const'))));
+ }
+
+ if (preg_match("/^[0-9]{6} blob [0-9a-f]{40}\t((detailsBody|popupBody)\.html)$/", $r, $m)) {
+ $data['templates'][$m[2]] = shell_exec("cd " . escapeShellArg($this->path) . "; git show HEAD:" . escapeShellArg($m[1]));
+ }
+ }
+ pclose($d);
+
+ return $data;
+ }
+
+ function scandir($path="") {
+ if ($path !== '' && substr($path, -1) !== '/') {
+ $path .= '/';
+ }
+
+ $d = popen("cd " . escapeShellArg($this->path) . "; git ls-tree HEAD " . escapeShellArg($path), "r");
+ $ret = array();
+ while ($r = fgets($d)) {
+ $ret[] = substr($r, 53);
+ }
+ pclose($d);
+
+ return $ret;
+ }
+
+ function file_get_contents ($file) {
+ return shell_exec("cd " . escapeShellArg($this->path) . "; git show HEAD:" . escapeShellArg($file));
+ }
+}
diff --git a/src/addCategories.css b/src/addCategories.css
new file mode 100644
index 00000000..4a72bd97
--- /dev/null
+++ b/src/addCategories.css
@@ -0,0 +1,3 @@
+#content.addCategories > #contentAddCategories {
+ display: block;
+}
diff --git a/src/addCategories.js b/src/addCategories.js
new file mode 100644
index 00000000..261351a6
--- /dev/null
+++ b/src/addCategories.js
@@ -0,0 +1,140 @@
+var OpenStreetBrowserLoader = require('./OpenStreetBrowserLoader')
+require('./addCategories.css')
+var weightSort = require('weight-sort')
+
+var content
+
+function addCategoriesShow (repo) {
+ if (!content) {
+ content = document.createElement('div')
+ content.id = 'contentAddCategories'
+ document.getElementById('content').appendChild(content)
+ }
+
+ content.innerHTML = 'Loading ...'
+ document.getElementById('content').className = 'addCategories'
+
+ OpenStreetBrowserLoader.getRepo(repo, {}, function (err, repoData) {
+ while(content.firstChild)
+ content.removeChild(content.firstChild)
+
+ var backLink = document.createElement('a')
+ backLink.className = 'back'
+ backLink.href = '#'
+ backLink.innerHTML = ' '
+ backLink.appendChild(document.createTextNode(lang('back')))
+
+ var categoryUrl = null
+ if (repoData.categoryUrl) {
+ categoryUrl = OverpassLayer.twig.twig({ data: repoData.categoryUrl, autoescape: true })
+ }
+
+ var list = {}
+
+ if (repo) {
+ backLink.onclick = function () {
+ addCategoriesShow()
+ return false
+ }
+ content.appendChild(backLink)
+
+ var h = document.createElement('h2')
+ h.appendChild(document.createTextNode(repo))
+ content.appendChild(h)
+
+ list = repoData.categories
+ } else {
+ backLink.onclick = function () {
+ addCategoriesHide()
+ return false
+ }
+ content.appendChild(backLink)
+
+ var h = document.createElement('h2')
+ h.innerHTML = lang('more_categories')
+ content.appendChild(h)
+
+ if (typeof repositoriesGitea === 'object' && repositoriesGitea.url) {
+ var 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
+ })
+ }
+
+ 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')
+
+ var a = document.createElement('a')
+ if (repo) {
+ a.href = '#categories=' + repo + '/' + id
+ a.onclick = function () {
+ addCategoriesHide()
+ }
+ } else {
+ 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 (repo && categoryUrl) {
+ editLink = document.createElement('a')
+ editLink.href = categoryUrl.render({ repositoryId: repo, categoryId: id })
+ }
+ if (!repo && 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 addCategoriesHide () {
+ document.getElementById('content').className = 'list'
+}
+
+register_hook('init', function (callback) {
+ var link = document.createElement('a')
+ link.className = 'addCategories'
+ link.href = '#'
+ link.onclick = function () {
+ addCategoriesShow()
+ return false
+ }
+ link.innerHTML = ' ' + lang('more_categories')
+
+ document.getElementById('contentList').appendChild(link)
+})
diff --git a/src/category.css b/src/category.css
index 65dc2cff..ff922678 100644
--- a/src/category.css
+++ b/src/category.css
@@ -69,13 +69,11 @@
user-select: none;
font-size: 15px;
}
-.category header > a {
- text-decoration: none;
- color: black;
-}
-.category header > a:active,
-.category header > a:hover {
- text-decoration: underline;
+.category header > span.repoId {
+ margin-left: 0.2em;
+ font-size: 10px;
+ line-height: 10px;
+ color: #7f7f7f;
}
.category header > a.reload {
float: right;
diff --git a/src/index.js b/src/index.js
index 2d57c399..78047fb8 100644
--- a/src/index.js
+++ b/src/index.js
@@ -30,6 +30,7 @@ require('./markers')
require('./categories')
require('./wikipedia')
require('./image')
+require('./addCategories')
window.onload = function () {
initState = config.defaultView
@@ -172,15 +173,17 @@ function show (id, options, callback) {
document.getElementById('contentDetails').innerHTML = 'Loading ...'
}
- id = id.split('/')
-
- if (id.length < 2) {
+ var m = id.match(/^(.*)\/([nwr]\d+)(\/details)?$/)
+ if (!m) {
return callback(new Error('unknown request'))
}
- OpenStreetBrowserLoader.getCategory(id[0], function (err, category) {
+ var categoryId = m[1]
+ var featureId = m[2]
+
+ OpenStreetBrowserLoader.getCategory(categoryId, function (err, category) {
if (err) {
- return callback(new Error('error loading category "' + id[0] + '": ' + err))
+ return callback(new Error('error loading category "' + categoryId + '": ' + err))
}
if (!category.parentDom) {
@@ -188,12 +191,12 @@ function show (id, options, callback) {
}
category.show(
- id[1],
+ featureId,
{
},
function (err, data) {
if (err) {
- return callback(new Error('error loading object "' + id[0] + '/' + id[1] + '": ' + err))
+ return callback(new Error('error loading object "' + categoryId + '/' + featureId + '": ' + err))
}
if (!map._popup || map._popup !== data.popup) {
diff --git a/src/markers.js b/src/markers.js
index b2c9447f..9e5c6e96 100644
--- a/src/markers.js
+++ b/src/markers.js
@@ -5,9 +5,7 @@ function cssStyle (style) {
if ('color' in style) {
ret += 'stroke: ' + style.color + ';'
}
- if ('weight' in style) {
- ret += 'stroke-width: ' + style.weight + ';'
- }
+ ret += 'stroke-width: ' + ('width' in style ? style.width : '3') + ';'
if ('dashArray' in style) {
ret += 'stroke-dasharray: ' + style.dashArray + ';'
}
@@ -80,17 +78,17 @@ function markerPolygon (data) {
function markerCircle (style) {
var fillColor = 'fillColor' in style ? style.fillColor : '#f2756a'
var color = 'color' in style ? style.color : '#000000'
- var weight = 'weight' in style ? style.weight : 1
+ var width = 'width' in style ? style.width : 1
- return ''
+ return ''
}
function markerPointer (style) {
var fillColor = 'fillColor' in style ? style.fillColor : '#f2756a'
var color = 'color' in style ? style.color : '#000000'
- var weight = 'weight' in style ? style.weight : 1
+ var width = 'width' in style ? style.width : 1
- return ''
+ return ''
}
OverpassLayer.twig.extendFunction('markerLine', markerLine)
diff --git a/src/repositories.php b/src/repositories.php
new file mode 100644
index 00000000..b14b12f4
--- /dev/null
+++ b/src/repositories.php
@@ -0,0 +1,70 @@
+ array(
+ 'path' => $config['categoriesDir'],
+ ),
+ );
+ }
+
+ 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/{{ categoryId }}.json";
+ }
+
+ $result["{$f1}/{$f2id}"] = $r;
+ }
+ }
+ closedir($d2);
+ }
+ }
+ closedir($d1);
+ }
+
+ return $result;
+}
+
+function getRepo ($repoId, $repoData) {
+ switch (array_key_exists('type', $repoData) ? $repoData['type'] : 'dir') {
+ case 'git':
+ $repo = new RepositoryGit($repoId, $repoData);
+ break;
+ default:
+ $repo = new RepositoryDir($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/style.css b/style.css
index 0b806643..3fab58f5 100644
--- a/style.css
+++ b/style.css
@@ -8,6 +8,14 @@ body {
font-size: 11px;
color:#333;
}
+a {
+ text-decoration: none;
+ color: black;
+}
+a:hover,
+a:active {
+ text-decoration: underline;
+}
#sidebar {
top: 0px;
@@ -116,13 +124,6 @@ a.showDetails {
#menu li {
display: inline-block;
}
-#menu a {
- text-decoration: none;
- color: black;
-}
-#menu a:hover {
- text-decoration: underline;
-}
#menu li::after {
content: ' |';
}