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: ' |'; }