├── .babelrc ├── .gitignore ├── .gitmodules ├── LICENCE.txt ├── README.md ├── ajax.php ├── asset.php ├── bin ├── build_lang_stat ├── categories-to-lang ├── convert_boundaries.js ├── download_dependencies ├── download_geoip2 ├── lang-to-categories └── tag2link-converter ├── composer.json ├── conf.php-dist ├── customCategory.php ├── dist └── .placeholder ├── doc ├── CategoryParameters.md ├── Filters.md ├── HowtoAddLanguage.md ├── INSTALL.md ├── Icons.md ├── Tutorial.md ├── TwigJS.md └── tutorial-customCategories.jpg ├── httpGet.php ├── img ├── crosshair.png ├── geo-info-bbox-center.svg ├── geo-info-bbox-none.svg ├── geo-info-bbox-nw.svg ├── geo-info-bbox-se.svg ├── geo-info-object-center.svg ├── geo-info-object-none.svg ├── geo-info-object-nw.svg ├── geo-info-object-se.svg ├── geo-info-object-shape.svg ├── map_pointer.png ├── map_pointer.xcf ├── osb-192.png └── osb_logo.png ├── index.php ├── init.sql ├── lang ├── ast.json ├── ca.json ├── cs.json ├── da.json ├── de.json ├── el.json ├── en.json ├── es.json ├── et.json ├── fr.json ├── gl.json ├── hu.json ├── it.json ├── ja.json ├── nb.json ├── nl.json ├── oc.json ├── pl.json ├── pt-br.json ├── pt.json ├── ro.json ├── ru.json ├── sr.json ├── ta.json ├── th.json ├── tr.json ├── uk.json └── zh-hans.json ├── lib ├── tag2link-sophox.qry └── tag2link-wikidata.qry ├── locales ├── ast.js ├── ca.js ├── cs.js ├── da.js ├── de.js ├── el.js ├── en.js ├── es.js ├── et.js ├── fr.js ├── hu.js ├── it.js ├── ja.js ├── nb.js ├── nl.js ├── pl.js ├── pt-br.js ├── pt.js ├── ro.js ├── ru.js ├── sr.js ├── th.js ├── tr.js ├── uk.js └── zh-hans.js ├── manifest.json ├── modulekit.php ├── package-lock.json ├── package.json ├── repo.php ├── src ├── Browser.js ├── CategoryBase.js ├── CategoryIndex.js ├── CategoryOverpass.js ├── CategoryOverpassConfig.js ├── CategoryOverpassFilter.js ├── ExportGeoJSON.js ├── ExportOSMJSON.js ├── ExportOSMXML.js ├── GeoInfo.css ├── GeoInfo.js ├── ImageLoader.js ├── ImageLoader.php ├── ObjectDisplay.js ├── OpenStreetBrowserLoader.js ├── PluginGeoLocate.js ├── PluginMeasure.js ├── Repository.js ├── RepositoryBase.php ├── RepositoryDir.php ├── RepositoryGit.php ├── Window.js ├── addCategories.css ├── addCategories.js ├── boundaries.js ├── categories.js ├── category.css ├── chunkSplit.js ├── customCategory.js ├── customCategory.php ├── database.php ├── defaults.php ├── displayBlock.js ├── domSort.js ├── editLink.js ├── export.js ├── exportAll.js ├── formatUnits.js ├── fullscreen.js ├── getPathFromJSON.js ├── httpGet.js ├── image.js ├── index.js ├── ip-location.php ├── language.js ├── language.php ├── leaflet-geo-search.js ├── maki.js ├── map-getMetersPerPixel.js ├── mapLayers.js ├── markers.js ├── moreCategories.js ├── nominatim-search.css ├── nominatim-search.js ├── options.js ├── options.php ├── optionsYaml.js ├── overpassChooser.js ├── permalink.js ├── pinnedCategories.js ├── repositories.php ├── repositoriesGitea.php ├── showMore.css ├── showMore.js ├── state.js ├── tagTranslations.js ├── tagsDisplay-tag2link.js ├── tagsDisplay.js ├── twigFunctions.js ├── wikidata.js ├── wikidata.php ├── wikipedia.js ├── wikipedia.php └── zenMode.js ├── style.css └── test └── getPathFromJSON.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /conf.php 2 | /dist/ 3 | /node_modules/ 4 | /npm-debug.log 5 | /vendor/ 6 | /data/ 7 | /composer.lock 8 | /yarn.lock 9 | *.swp 10 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "modulekit"] 2 | path = modulekit 3 | url = https://github.com/plepe/modulekit.git 4 | branch = nodejs 5 | [submodule "lib/modulekit/base"] 6 | path = lib/modulekit/base 7 | url = https://github.com/plepe/modulekit-base.git 8 | [submodule "lib/modulekit/lang"] 9 | path = lib/modulekit/lang 10 | url = https://github.com/plepe/modulekit-lang.git 11 | branch = master 12 | [submodule "lib/modulekit/form"] 13 | path = lib/modulekit/form 14 | url = https://github.com/plepe/modulekit-form.git 15 | [submodule "lib/modulekit/ajax"] 16 | path = lib/modulekit/ajax 17 | url = https://github.com/plepe/modulekit-ajax.git 18 | [submodule "lib/modulekit/mysql-sessions"] 19 | path = lib/modulekit/mysql-sessions 20 | url = https://github.com/plepe/PHP-MySQL-Sessions.git 21 | -------------------------------------------------------------------------------- /ajax.php: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 2 | 3 | 4 | 5 | 6 | scandir($_REQUEST['dir']) as $f) { 21 | if (substr($f, 0, 1) !== '.') { 22 | $contents[] = array('name' => $f); 23 | } 24 | } 25 | 26 | $mime_type = 'application/json; charset=utf-8'; 27 | $contents = json_encode($contents); 28 | } 29 | else { 30 | $tmpfile = tempnam('/tmp', 'osb-asset-'); 31 | $contents = $repo->file_get_contents((array_key_exists('dir', $_REQUEST) ? "{$_REQUEST['dir']}/" : '') . $_REQUEST['file']); 32 | 33 | if ($contents === false) { 34 | Header("HTTP/1.1 401 Permission denied"); 35 | exit(0); 36 | } 37 | 38 | file_put_contents($tmpfile, $contents); 39 | $mime_type = mime_content_type($tmpfile); 40 | } 41 | 42 | Header("Content-Type: {$mime_type}; charset=utf-8"); 43 | Header("Cache-Control: max-age=86400"); 44 | print $contents; 45 | -------------------------------------------------------------------------------- /bin/build_lang_stat: -------------------------------------------------------------------------------- 1 | #!/usr/bin/php 2 | 6 | 14 | 15 | {|class="wikitable sortable" 16 | |- 17 | !scope="col"| Code 18 | !scope="col"| Language 19 | !scope="col"| Native name 20 | "lang/", 26 | "Translations of OSM Tags" => "node_modules/openstreetmap-tag-translations/tags/", 27 | "Category Titles" => $repositories['default']['path'] . 'lang/', 28 | ); 29 | 30 | foreach ($dirs as $dirId => $dir) { 31 | $stat[$dirId] = build_statistic($dir); 32 | } 33 | 34 | $total = 0; 35 | foreach ($dirs as $dirId => $dir) { 36 | $total += $stat[$dirId]['']; 37 | print "!scope=\"col\"| {$dirId} ({$stat[$dirId]['']})\n"; 38 | } 39 | print "!scope=\"col\"| Total ({$total})\n"; 40 | 41 | 42 | foreach ($languages as $code => $native_name) { 43 | $sum = 0; 44 | foreach ($dirs as $dirId => $dir) { 45 | $sum += $stat[$dirId][$code] ?? 0; 46 | } 47 | 48 | if ($sum > 0) { 49 | print "|-\n"; 50 | print "| {$code}\n"; 51 | print "| {{Languagename|{$code}|en}} || {{Languagename|{$code}}}\n"; 52 | foreach ($dirs as $dirId => $dir) { 53 | print "| {{Progress Bar|max={$stat[$dirId]['']}|current=" . ($stat[$dirId][$code] ?? 0) . "}}\n"; 54 | } 55 | print "| {{Progress Bar|max={$total}|current={$sum}}}\n"; 56 | } 57 | } 58 | 59 | ?> 60 | |} 61 | -------------------------------------------------------------------------------- /bin/categories-to-lang: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const fs = require('fs') 3 | const async = require('async') 4 | const yaml = require('js-yaml') 5 | 6 | var all = {} 7 | var allIds = [] 8 | 9 | /* read existing translation files in lang/ */ 10 | fs.readdir('lang', function (err, files) { 11 | async.each(files, function (f, done) { 12 | let m = f.match(/^(.*)\.json$/) 13 | if (!m) { 14 | return done() 15 | } 16 | 17 | let lang = m[1] 18 | 19 | fs.readFile('lang/' + f, function (err, body) { 20 | if (!(lang in all)) { 21 | all[lang] = JSON.parse(body) 22 | } 23 | 24 | done(err) 25 | }) 26 | }) 27 | }) 28 | 29 | fs.readdir( 30 | '.', 31 | function (err, files) { 32 | async.each( 33 | files, 34 | function (f, done) { 35 | if (['package.json', 'package-lock.json'].includes(f)) { 36 | return done() 37 | } 38 | 39 | let m = f.match(/^(.*)\.(json|yaml)$/) 40 | if (!m) { 41 | return done() 42 | } 43 | 44 | let id = m[1] 45 | allIds.push('category:' + id) 46 | 47 | fs.readFile(f, function (err, contents) { 48 | if (err) { return done(err) } 49 | 50 | let data = parseFile(f, contents) 51 | 52 | if ('name' in data) { 53 | for (var lang in data.name) { 54 | if (!(lang in all)) { 55 | all[lang] = {} 56 | } 57 | 58 | all[lang]['category:' + id] = data.name[lang] 59 | } 60 | 61 | if (data.type && data.type === 'index') { 62 | parseSubCategories(data.subCategories, all) 63 | } 64 | if (data.type && data.type === 'overpass') { 65 | if (data.lists) { 66 | for (let listId in data.lists) { 67 | let list = data.lists[listId] 68 | let langStrId = 'category:' + id + ':' + listId 69 | 70 | allIds.push(langStrId) 71 | for (let lang in list.name) { 72 | all[lang][langStrId] = list.name[lang] 73 | } 74 | } 75 | } 76 | } 77 | } 78 | 79 | done() 80 | }) 81 | }, 82 | writeTranslationFiles 83 | ) 84 | } 85 | ) 86 | 87 | function parseSubCategories(categories, all) { 88 | categories.forEach(data => { 89 | if ('name' in data) { 90 | for (var lang in data.name) { 91 | if (!(lang in all)) { 92 | all[lang] = {} 93 | } 94 | 95 | allIds.push('category:' + data.id) 96 | all[lang]['category:' + data.id] = data.name[lang] 97 | } 98 | 99 | if (data.type && data.type === 'index') { 100 | parseSubCategories(data.subCategories, all) 101 | } 102 | } 103 | }) 104 | } 105 | 106 | function writeTranslationFiles () { 107 | async.each(Object.keys(all), function (lang, done) { 108 | allIds = allIds.sort() 109 | let data = JSON.parse(JSON.stringify(all[lang])) 110 | 111 | allIds.forEach(function (id) { 112 | data[id] = '' 113 | }) 114 | 115 | let keys = Object.keys(all[lang]) 116 | keys.sort() 117 | for (let i = 0; i < keys.length; i++) { 118 | data[keys[i]] = all[lang][keys[i]] 119 | } 120 | 121 | fs.writeFile( 122 | 'lang/' + lang + '.json', 123 | JSON.stringify(data, null, ' ') + '\n', 124 | function (err, result) { 125 | done(err) 126 | } 127 | ) 128 | }) 129 | } 130 | 131 | function parseFile (filename, contents) { 132 | const m = filename.match(/\.(json|yaml)$/) 133 | const mode = m[1] 134 | let data 135 | 136 | switch (mode) { 137 | case 'yaml': 138 | data = yaml.load(contents) 139 | break 140 | case 'json': 141 | default: 142 | data = JSON.parse(contents) 143 | } 144 | 145 | return data 146 | } 147 | -------------------------------------------------------------------------------- /bin/convert_boundaries.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const osmtogeojson = require('osmtogeojson') 3 | const DOMParser = require('@xmldom/xmldom').DOMParser 4 | 5 | const parser = new DOMParser({ 6 | errorHandler: { 7 | error: (err) => { throw new Error('Error parsing XML file: ' + err) }, 8 | fatalError: (err) => { throw new Error('Error parsing XML file: ' + err) } 9 | } 10 | }) 11 | 12 | // load and parse original file 13 | const content = fs.readFileSync('data/boundaries.osm') 14 | const input = parser.parseFromString(content.toString(), 'text/xml') 15 | 16 | // convert to geojson 17 | const output = osmtogeojson(input, { 18 | polygonFeatures: () => true 19 | }) 20 | 21 | // remove ids (as they are fake anyway) 22 | output.features.forEach(feature => { 23 | delete feature.id 24 | delete feature.properties.id 25 | feature.tags = feature.properties 26 | delete feature.properties 27 | }) 28 | 29 | fs.writeFileSync('data/boundaries.geojson', JSON.stringify(output)) 30 | -------------------------------------------------------------------------------- /bin/download_dependencies: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | curl -H "Accept: application/json" -H "Content-Type: application/sparql-query" -H "User-Agent: OpenStreetBrowser" -XPOST -d @'lib/tag2link-wikidata.qry' https://query.wikidata.org/sparql > data/tag2link-wikidata.json 4 | curl -H "Accept: application/json" -H "Content-Type: application/sparql-query" -H "User-Agent: OpenStreetBrowser" -XPOST -d @'lib/tag2link-sophox.qry' https://sophox.org/sparql > data/tag2link-sophox.json 5 | bin/tag2link-converter 6 | 7 | # Extract boundaries from JOSM 8 | wget -O data/josm-latest.jar https://josm.openstreetmap.de/josm-latest.jar 9 | unzip data/josm-latest.jar data/boundaries.osm 10 | node bin/convert_boundaries.js 11 | rm data/josm-latest.jar data/boundaries.osm 12 | 13 | bin/download_geoip2 14 | -------------------------------------------------------------------------------- /bin/download_geoip2: -------------------------------------------------------------------------------- 1 | #!/usr/bin/php 2 | $d) { 22 | $tag2link[$key]['formatter'][$i]['operator'] = $entry['operatorLabel']['value']; 23 | } 24 | } 25 | 26 | continue; 27 | } 28 | } 29 | else { 30 | $tag2link[$key] = array( 31 | 'label' => $entry['itemLabel']['value'], 32 | 'formatter' => array(), 33 | ); 34 | } 35 | 36 | $formatter = array( 37 | 'link' => $link, 38 | ); 39 | 40 | if (array_key_exists('operatorLabel', $entry)) { 41 | $formatter['operator'] = $entry['operatorLabel']['value']; 42 | print "{$formatter['operator']}\n"; 43 | } 44 | else if (preg_match("/^https?:\/\/([^\/]*)(\/.*|)$/", $link, $m)) { 45 | $formatter['operator'] = $m[1]; 46 | } 47 | 48 | $tag2link[$key]['formatter'][] = $formatter; 49 | } 50 | } 51 | 52 | file_put_contents('dist/tag2link.json', json_encode($tag2link, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE)); 53 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": { 3 | "geoip2/geoip2": "~2.0" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /customCategory.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | list($_REQUEST); 13 | 14 | Header("Content-Type: application/json; charset=utf-8"); 15 | print json_readable_encode($result); 16 | } 17 | 18 | if (isset($_REQUEST['id'])) { 19 | $category = $customCategoryRepository->getCategory($_REQUEST['id']); 20 | if ($category) { 21 | $customCategoryRepository->recordAccess($_REQUEST['id']); 22 | } 23 | 24 | Header("Content-Type: application/yaml; charset=utf-8"); 25 | Header("Content-Disposition: inline; filename=\"{$_REQUEST['id']}.yaml\""); 26 | print $category; 27 | } 28 | 29 | if (isset($_REQUEST['action']) && $_REQUEST['action'] === 'save') { 30 | $content = file_get_contents("php://input"); 31 | 32 | $id = $customCategoryRepository->saveCategory($content); 33 | $customCategoryRepository->recordAccess($id); 34 | 35 | Header("Content-Type: text/plain; charset=utf-8"); 36 | print $id; 37 | } 38 | -------------------------------------------------------------------------------- /dist/.placeholder: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plepe/OpenStreetBrowser/ccf9b3611617e05a4049fd571d9215df77d9adf5/dist/.placeholder -------------------------------------------------------------------------------- /doc/Filters.md: -------------------------------------------------------------------------------- 1 | Each category can define a list of filters. This is an additional JSON value with the key "filter", e.g.: 2 | 3 | ```json 4 | { 5 | "query": { 6 | "13": "nwr[amenity]" 7 | }, 8 | "filter": { 9 | "type": { 10 | "name": "{{ keyTrans('amenity') }}", 11 | "type": "select", 12 | "values": { 13 | "bar": { 14 | "nwr[ 15 | }, 16 | } 17 | } 18 | } 19 | ``` 20 | This defines a filter with the ID 'type' and the translated name of the key 'amenity'. It's of type 'select' and has several possible values. 21 | 22 | Each filter can define the following values: 23 | * name: Name of the filter. String, which can make use of twig functions, e.g. `keyTrans` as in the above example. 24 | * type: A form type, e.g. 'text', 'select', 'radio', 'checkbox' 25 | * values: Possible values. Can either be an array, an object or a html string with several ` 69 | {% endfor %} 70 | 71 | ``` 72 | 73 | * Name is generated from text content. If it is empty, it can be created via `valueName`. 74 | * Query is optional, it can be created from key (or filter id), op and the value. 75 | -------------------------------------------------------------------------------- /doc/HowtoAddLanguage.md: -------------------------------------------------------------------------------- 1 | # How to add an additional language to OpenStreetBrowser 2 | Assume the language code 'xy'. 3 | 4 | ## Update modulekit-lang 5 | Clone [modulekit-lang](https://github.com/plepe/modulekit-lang) 6 | 7 | Run on the shell: 8 | ```sh 9 | git clone https://github.com/plepe/modulekit-lang 10 | cd modulekit-lang 11 | git checkout lang-options 12 | ./bin/import_cldr xy 13 | git add lang/xy.json lang/list.json 14 | git commit -m "Adding language xy" 15 | git push 16 | ``` 17 | 18 | ## Update OpenStreetBrowser 19 | Clone [OpenStreetBrowser](https://github.com/plepe/modulekit-lang) 20 | 21 | Run on the shell: 22 | ```sh 23 | git clone https://github.com/plepe/OpenStreetBrowser 24 | cd OpenStreetBrowser 25 | 26 | cd lib/modulekit/lang 27 | git pull origin lang-options 28 | cd ../../.. 29 | git add lib/modulekit/lang 30 | 31 | cp locales/tr.js locales/xy.js 32 | nano locales/xy.js 33 | # replace all 'tr' by 'xy' 34 | git add locales/th.js 35 | 36 | nano conf.php-dist 37 | # Enter an entry for the language to the list of available languages 38 | git add conf.php-dist 39 | 40 | git commit -m "Adding language xy" 41 | ``` 42 | -------------------------------------------------------------------------------- /doc/INSTALL.md: -------------------------------------------------------------------------------- 1 | These install instructions are tested on a plain Ubuntu 22 or Debian 11 Server installation. 2 | 3 | You either need to [install a modern nodejs version](https://nodejs.org/en/download/package-manager/#debian-and-ubuntu-based-linux-distributions) 4 | or replace the `openstreetbrowser.min.js` with `openstreetbrowser.js` in `index.html`. 5 | 6 | ```sh 7 | sudo apt install apache2 libapache2-mod-php curl git php-cli composer nodejs npm php-curl php-yaml acl 8 | sudo chmod 777 /var/www/html 9 | cd /var/www/html 10 | git clone https://github.com/plepe/openstreetbrowser.git 11 | cd openstreetbrowser 12 | npm install 13 | composer install 14 | git submodule update --init 15 | cp conf.php-dist conf.php 16 | nano conf.php 17 | mkdir data 18 | bin/download_dependencies 19 | ``` 20 | 21 | For improved performance you should also run: 22 | ```sh 23 | modulekit/build_cache 24 | ``` 25 | 26 | Have fun on http://localhost/openstreetbrowser which is now served via apache from php! 27 | 28 | # Debugging 29 | 30 | For debugging add the following line to conf.php: 31 | ```php 32 | $modulekit_nocache = true; 33 | ``` 34 | 35 | And then run: 36 | ```sh 37 | npm run watch 38 | ``` 39 | 40 | This is very similar to `npm run build`, 41 | but watches JavaScript files for changes and updates the dist/openstreetbrowser.js file automatically. 42 | It also adds debugging information to the final JavaScript file. 43 | -------------------------------------------------------------------------------- /doc/Icons.md: -------------------------------------------------------------------------------- 1 | #### Unicode Icons 2 | Unicode defines many icons which can be used either directly or via their HTML codes, e.g.: 3 | ```html 4 | 🎂 🎂 🎂 5 | ``` 6 | 7 | A drawback of Unicode icons is, that the display will differ from system to system as they depend on the available Fonts. 8 | 9 | #### Self defined icons 10 | You may upload images to your repository and use them via a relative image link: 11 | ```html 12 | 13 | ``` 14 | 15 | This will include the image from your repository (when uploaded to your 'img' directory). 16 | 17 | #### Font Awesome Icons 18 | [Font Awesome 6 Free](https://fontawesome.com/) ([search](https://fontawesome.com/v5/search?o=r&m=free)) is included in OpenStreetBrowser, therefore you can use, e.g.: 19 | ```html 20 | 21 | 22 | ``` 23 | 24 | You can use normal CSS to modify its look, e.g. 25 | ```html 26 | 27 | ``` 28 | 29 | #### Mapbox Maki Icons 30 | [Mapbox Maki Icons 8](https://www.mapbox.com/maki-icons/) are also included in OpenStreetBrowser. They can be accessed as images with protocol 'maki', e.g.: 31 | ```html 32 | 33 | ``` 34 | 35 | ```html 36 | 37 | ``` 38 | 39 | You can pass URL options to the icon to modify its look. Note that every icon is a [SVG path](https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths) and all [style options](https://developer.mozilla.org/de/docs/Web/SVG/Tutorial/Fills_and_Strokes) are available: 40 | ```html 41 | 42 | ``` 43 | 44 | #### Temaki Icons 45 | [Temaki icons](https://rapideditor.github.io/temaki/docs/) are additions to the Mapbox Maki Icons. 46 | ```html 47 | 48 | 49 | ``` 50 | 51 | #### Markers 52 | Markers are rendered by the module [openstreetbrowser-markers](https://github.com/plepe/openstreetbrowser-markers). 53 | 54 | You can either use a `` syntax or TwigJS: `{{ markerLine({ ... }) }}`. A simple example (a black line, 3px wide): 55 | ```html 56 | 57 | {{ markerLine({ width: 3, color: 'black' }) }} 58 | ``` 59 | 60 | The following marker types are available: line, polygon (a rectangle), circle, pointer 61 | 62 | The following style parameters are possible: 63 | * `color`: outline color, default `#000000`. 64 | * `width`: outline width, default `3`. 65 | * `offset`: outline offset, default `0`. 66 | * `fill`: if the marker should be filled (boolean), default `true`. 67 | * `fillColor`: color of the fill, default value of `color`. If no `color` is set, use `#f2756a`. 68 | * `fillOpacity`: opacity of the fill, default `0.2`. 69 | * `dashArray`: outline dashes, e.g. `5,5`. Default: `none`. 70 | * `dashOffset`: offset of outline dashes. Default: `0`. 71 | * `radius` or `size`: Radius resp. size of the circle/pointer. Default: `10`. 72 | 73 | Syntax with multiple symbols (example: a white line with a black casing). Only styles which are listed in the `styles` parameter will be used. Instead of `style:default:width` use `style:width`: 74 | ```html 75 | 76 | {{ markerLine({ styles: 'casing,default', 'style:casing': { color: 'black', width: 4 }, default: { color: 'black', width: 2 }}) }} 77 | ``` 78 | 79 | You can use the `evaluate` function, to emulate a fake object (e.g. for map keys). The following example would draw a line, which looks like the symbol which is generated by this category for an OSM object with the tags highway=primary and maxspeed=80: 80 | ```html 81 | {{ markerLine(evaluate({ "highway": "primary", "maxspeed": "80" })) }} 82 | ``` 83 | -------------------------------------------------------------------------------- /doc/tutorial-customCategories.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plepe/OpenStreetBrowser/ccf9b3611617e05a4049fd571d9215df77d9adf5/doc/tutorial-customCategories.jpg -------------------------------------------------------------------------------- /httpGet.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 41 | 44 | 45 | 47 | 48 | 50 | image/svg+xml 51 | 53 | 54 | 55 | 56 | 57 | 62 | 68 | 74 | 80 | 86 | 92 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /img/geo-info-bbox-none.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 41 | 44 | 45 | 47 | 48 | 50 | image/svg+xml 51 | 53 | 54 | 55 | 56 | 57 | 62 | 68 | 74 | 80 | 86 | 92 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /img/geo-info-bbox-nw.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 41 | 44 | 45 | 47 | 48 | 50 | image/svg+xml 51 | 53 | 54 | 55 | 56 | 57 | 62 | 68 | 74 | 80 | 86 | 92 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /img/geo-info-bbox-se.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 41 | 44 | 45 | 47 | 48 | 50 | image/svg+xml 51 | 53 | 54 | 55 | 56 | 57 | 62 | 68 | 74 | 80 | 86 | 92 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /img/geo-info-object-center.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 41 | 44 | 45 | 47 | 48 | 50 | image/svg+xml 51 | 53 | 54 | 55 | 56 | 57 | 62 | 67 | 73 | 79 | 85 | 91 | 97 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /img/geo-info-object-none.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 41 | 44 | 45 | 47 | 48 | 50 | image/svg+xml 51 | 53 | 54 | 55 | 56 | 57 | 62 | 67 | 73 | 79 | 85 | 91 | 97 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /img/geo-info-object-nw.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 41 | 44 | 45 | 47 | 48 | 50 | image/svg+xml 51 | 53 | 54 | 55 | 56 | 57 | 62 | 67 | 73 | 79 | 85 | 91 | 97 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /img/geo-info-object-se.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 41 | 44 | 45 | 47 | 48 | 50 | image/svg+xml 51 | 53 | 54 | 55 | 56 | 57 | 62 | 67 | 73 | 79 | 85 | 91 | 97 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /img/map_pointer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plepe/OpenStreetBrowser/ccf9b3611617e05a4049fd571d9215df77d9adf5/img/map_pointer.png -------------------------------------------------------------------------------- /img/map_pointer.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plepe/OpenStreetBrowser/ccf9b3611617e05a4049fd571d9215df77d9adf5/img/map_pointer.xcf -------------------------------------------------------------------------------- /img/osb-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plepe/OpenStreetBrowser/ccf9b3611617e05a4049fd571d9215df77d9adf5/img/osb-192.png -------------------------------------------------------------------------------- /img/osb_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plepe/OpenStreetBrowser/ccf9b3611617e05a4049fd571d9215df77d9adf5/img/osb_logo.png -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | $config, 26 | )); 27 | ?> 28 | 29 | 30 | 31 | 32 | OpenStreetBrowser 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 |
58 |
59 | 80 |
81 |
82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /init.sql: -------------------------------------------------------------------------------- 1 | create table customCategory ( 2 | id char(32) not null, 3 | content mediumtext not null, 4 | created datetime not null default CURRENT_TIMESTAMP, 5 | primary key(id) 6 | ); 7 | 8 | create table customCategoryAccess ( 9 | id char(32) not null, 10 | ts datetime not null default CURRENT_TIMESTAMP, 11 | foreign key(id) references customCategory(id) on delete cascade 12 | ); 13 | -------------------------------------------------------------------------------- /lang/ast.json: -------------------------------------------------------------------------------- 1 | { 2 | "add_filter": "", 3 | "any value": "", 4 | "available_branches": "", 5 | "back": "", 6 | "categories": "", 7 | "category-info-tooltip": "", 8 | "closed": "", 9 | "default": "", 10 | "edit": "", 11 | "error": "", 12 | "export-all": "", 13 | "export-prepare": "", 14 | "export:GeoJSON": "", 15 | "export:OSMJSON": "", 16 | "export:OSMXML": "", 17 | "facilities": "", 18 | "filter:title": "", 19 | "filter:type": "", 20 | "header:attributes": "", 21 | "header:export": "", 22 | "header:osm_meta": "", 23 | "images": "", 24 | "invalid value": "", 25 | "loading": "", 26 | "main:options": "Opciones", 27 | "main:permalink": "", 28 | "more": "más", 29 | "more_categories": "Más categoríes", 30 | "more_categories_gitea": "", 31 | "more_results": "", 32 | "open": "", 33 | "options:data_lang": "Llingua de los datos", 34 | "options:data_lang:desc": "", 35 | "options:data_lang:local": "Llingua llocal", 36 | "options:overpassUrl": "", 37 | "options:preferredBaseMap": "", 38 | "options:ui_lang": "Llingua de la interfaz", 39 | "other": "", 40 | "repo-use-as-base": "", 41 | "repositories": "", 42 | "save": "Guardar", 43 | "show details": "", 44 | "toggle_fullscreen": "", 45 | "unknown": "", 46 | "unnamed": "ensin nome", 47 | "wikipedia:no-url-parse": "", 48 | "zoom_in_appear": "", 49 | "zoom_in_more": "" 50 | } 51 | -------------------------------------------------------------------------------- /lang/da.json: -------------------------------------------------------------------------------- 1 | { 2 | "add_filter": "", 3 | "any value": "", 4 | "available_branches": "", 5 | "back": "", 6 | "categories": "", 7 | "category-info-tooltip": "", 8 | "closed": "", 9 | "default": "", 10 | "edit": "", 11 | "error": "", 12 | "export-all": "", 13 | "export-prepare": "", 14 | "export:GeoJSON": "", 15 | "export:OSMJSON": "", 16 | "export:OSMXML": "", 17 | "facilities": "", 18 | "filter:title": "", 19 | "filter:type": "", 20 | "header:attributes": "", 21 | "header:export": "", 22 | "header:osm_meta": "", 23 | "images": "", 24 | "invalid value": "", 25 | "loading": "", 26 | "main:options": "Indstillinger", 27 | "main:permalink": "", 28 | "more": "mere", 29 | "more_categories": "Flere kategorier", 30 | "more_categories_gitea": "", 31 | "more_results": "", 32 | "open": "", 33 | "options:data_lang": "Data sprog", 34 | "options:data_lang:desc": "", 35 | "options:data_lang:local": "Lokalt sprog", 36 | "options:overpassUrl": "", 37 | "options:preferredBaseMap": "", 38 | "options:ui_lang": "Brugerfladesprog", 39 | "other": "", 40 | "repo-use-as-base": "", 41 | "repositories": "", 42 | "save": "Gem", 43 | "show details": "", 44 | "toggle_fullscreen": "", 45 | "unknown": "", 46 | "unnamed": "unavngivet", 47 | "wikipedia:no-url-parse": "", 48 | "zoom_in_appear": "", 49 | "zoom_in_more": "" 50 | } 51 | -------------------------------------------------------------------------------- /lang/el.json: -------------------------------------------------------------------------------- 1 | { 2 | "add_filter": "", 3 | "any value": "", 4 | "available_branches": "", 5 | "back": "", 6 | "categories": "", 7 | "category-info-tooltip": "", 8 | "closed": "", 9 | "default": "", 10 | "edit": "", 11 | "error": "", 12 | "export-all": "", 13 | "export-prepare": "", 14 | "export:GeoJSON": "", 15 | "export:OSMJSON": "", 16 | "export:OSMXML": "", 17 | "facilities": "", 18 | "filter:title": "", 19 | "filter:type": "", 20 | "header:attributes": "", 21 | "header:export": "", 22 | "header:osm_meta": "", 23 | "images": "", 24 | "invalid value": "", 25 | "loading": "", 26 | "main:options": "Επιλογές", 27 | "main:permalink": "", 28 | "more": "περισσότερα", 29 | "more_categories": "Περισσότερες κατηγορίες", 30 | "more_categories_gitea": "", 31 | "more_results": "", 32 | "open": "", 33 | "options:data_lang": "Γλωσσα δεδομένων", 34 | "options:data_lang:desc": "", 35 | "options:data_lang:local": "Τοπική γλώσσα", 36 | "options:overpassUrl": "", 37 | "options:preferredBaseMap": "", 38 | "options:ui_lang": "Γλώσσα διεπαφής", 39 | "other": "", 40 | "repo-use-as-base": "", 41 | "repositories": "", 42 | "save": "Αποθήκευση", 43 | "show details": "", 44 | "toggle_fullscreen": "", 45 | "unknown": "", 46 | "unnamed": "ανώνυμο", 47 | "wikipedia:no-url-parse": "", 48 | "zoom_in_appear": "", 49 | "zoom_in_more": "" 50 | } 51 | -------------------------------------------------------------------------------- /lang/et.json: -------------------------------------------------------------------------------- 1 | { 2 | "add_filter": "", 3 | "any value": "", 4 | "available_branches": "", 5 | "back": "", 6 | "categories": "", 7 | "category-info-tooltip": "", 8 | "closed": "", 9 | "default": "", 10 | "edit": "", 11 | "error": "", 12 | "export-all": "", 13 | "export-prepare": "", 14 | "export:GeoJSON": "", 15 | "export:OSMJSON": "", 16 | "export:OSMXML": "", 17 | "facilities": "", 18 | "filter:title": "", 19 | "filter:type": "", 20 | "header:attributes": "", 21 | "header:export": "", 22 | "header:osm_meta": "", 23 | "images": "", 24 | "invalid value": "", 25 | "loading": "", 26 | "main:options": "Valikud", 27 | "main:permalink": "", 28 | "more": "lisaks", 29 | "more_categories": "Rohkem kategooriaid", 30 | "more_categories_gitea": "", 31 | "more_results": "", 32 | "open": "", 33 | "options:data_lang": "Andmete keel", 34 | "options:data_lang:desc": "", 35 | "options:data_lang:local": "Kohalik keel", 36 | "options:overpassUrl": "", 37 | "options:preferredBaseMap": "", 38 | "options:ui_lang": "Kasutajaliidese keel", 39 | "other": "", 40 | "repo-use-as-base": "", 41 | "repositories": "", 42 | "save": "Salvesta", 43 | "show details": "", 44 | "toggle_fullscreen": "", 45 | "unknown": "", 46 | "unnamed": "nimeta", 47 | "wikipedia:no-url-parse": "", 48 | "zoom_in_appear": "", 49 | "zoom_in_more": "" 50 | } 51 | -------------------------------------------------------------------------------- /lang/gl.json: -------------------------------------------------------------------------------- 1 | { 2 | "add_filter": "Engadir filtro", 3 | "any value": "calquera valor", 4 | "available_branches": "Redes dispoñíbeis", 5 | "back": "voltar", 6 | "categories": "Categorías", 7 | "category-info-tooltip": "Informacións e lenda", 8 | "closed": "pechado", 9 | "default": "predeterminado", 10 | "edit": "editar", 11 | "error": "Erros", 12 | "export-all": "Exportar tódolos elementos visíbeis no mapa", 13 | "export-prepare": "Xestionar a baixada", 14 | "export:GeoJSON": "Baixar coma GeoJSON", 15 | "export:OSMJSON": "Baixar coma OSM JSON", 16 | "export:OSMXML": "Baixar coma OSM XML", 17 | "facilities": "Instalacións", 18 | "filter:title": "Título", 19 | "filter:type": "Tipo", 20 | "header:attributes": "Atributos", 21 | "header:export": "Exportar", 22 | "header:osm_meta": "OSM Meta", 23 | "images": "Imaxes", 24 | "invalid value": "valor non válido", 25 | "loading": "Estase a cargar...", 26 | "main:options": "Opcións", 27 | "main:permalink": "Permalink (ligazón permanente)", 28 | "more": "máis", 29 | "more_categories": "Máis categorías", 30 | "more_categories_gitea": "Crear e mellorar categorías ti mesmo!", 31 | "more_results": "Amosar máis resultados", 32 | "open": "abrir", 33 | "options:data_lang": "Lingua dos datos", 34 | "options:data_lang:desc": "Moitos elementos do mapa posúen os seus nomes (e outras etiquetas) traducidos en linguas diferentes (p.ex. co 'name:en', 'name:de'). Especificar que lingua ten que ser empregada para amosar, ou 'Lingua local' de xeito que sempre os valores non tranducidos (p.ex. 'name') sexan empregados.", 35 | "options:data_lang:local": "Lingua local", 36 | "options:overpassUrl": "URL do OverpassAPI", 37 | "options:preferredBaseMap": "Mapa de base preferido", 38 | "options:ui_lang": "Lingua da interface", 39 | "other": "Outro", 40 | "repo-use-as-base": "Empregar este repositorio coma repositorio de base", 41 | "repositories": "Repositorios", 42 | "save": "Gardar", 43 | "show details": "amosar detalles", 44 | "toggle_fullscreen": "Trocar modo de pantalla completa", 45 | "unknown": "descoñecido", 46 | "unnamed": "sen nome", 47 | "wikipedia:no-url-parse": "Non foi posíbel analizar a URL da Wikipedia", 48 | "zoom_in_appear": "achegar a vista para amosar os elementos do mapa", 49 | "zoom_in_more": "achegar a vista para amosar máis elementos do mapa", 50 | "cancel": "Desbotar", 51 | "form_element:please_select": "-- por favor, escolle --", 52 | "main:about": "Sobre", 53 | "main:code": "Código", 54 | "options:debug_mode": "Modo depuración" 55 | } 56 | -------------------------------------------------------------------------------- /lang/it.json: -------------------------------------------------------------------------------- 1 | { 2 | "add_filter": "Aggiungi filtro di ricerca", 3 | "any value": "un valore qualsiasi", 4 | "available_branches": "Rami disponibili", 5 | "back": "indietro", 6 | "categories": "Categorie", 7 | "category-info-tooltip": "Info & Map_key", 8 | "closed": "chiuso", 9 | "default": "predefinito", 10 | "edit": "modifica", 11 | "error": "Errore", 12 | "export-all": "Esporta tutte le caratteristiche visibili della mappa", 13 | "export-prepare": "Preparare il download", 14 | "export:GeoJSON": "Download in formato GeoJSON", 15 | "export:OSMJSON": "Downlaod in formato OSM JSON", 16 | "export:OSMXML": "Download in formato OSM XML", 17 | "facilities": "Servizi", 18 | "filter:title": "Titolo", 19 | "filter:type": "Tipologia", 20 | "header:attributes": "Attributi", 21 | "header:export": "Esporta", 22 | "header:osm_meta": "Meta OSM", 23 | "images": "Immagini", 24 | "invalid value": "valore non valido", 25 | "loading": "Caricamento ...", 26 | "main:options": "Opzioni", 27 | "main:permalink": "Link permanente Permalink", 28 | "more": "altri", 29 | "more_categories": "Altre categorie", 30 | "more_categories_gitea": "Crea &1 ed arricchisci le categorie!", 31 | "more_results": "Mostra ulteriori risultati", 32 | "open": "apri", 33 | "options:data_lang": "Lingua dei dati", 34 | "options:data_lang:desc": "Molte caratteristiche della mappa hanno nome (ed altre etichette proprie) tradotti in lingue differenti (mediante 'name:en' , 'name:de', etc. etc.). Va specificata quale lingua adottare nella visualizzazione, oppure utilizzare 'Local language' in modo che venga sempre usato il valore originario non tradotto (quello impostato con 'name').", 35 | "options:data_lang:local": "Lingua del tuo browser", 36 | "options:overpassUrl": "URL OverpassAPI", 37 | "options:preferredBaseMap": "Sfondo (basemap) preferito", 38 | "options:ui_lang": "Lingua dell'interfaccia", 39 | "other": "Altro", 40 | "repo-use-as-base": "Usare questo repertorio come repertorio base", 41 | "repositories": "Repertori", 42 | "save": "Salva", 43 | "show details": "mostra dettagli", 44 | "toggle_fullscreen": "Attiva/disattiva modalità Schermo intero", 45 | "unknown": "sconosciuto", 46 | "unnamed": "privo di nome", 47 | "wikipedia:no-url-parse": "Non è possibile decifrare la URL Wikipedia", 48 | "zoom_in_appear": "ingrandire la mappa per mostrare le caratteristiche", 49 | "zoom_in_more": "ingrandire la mappa per mostrare ulteriori caratteristiche", 50 | "editor:id": "iD (Editor nel browser)", 51 | "editor:remote": "Controllo remoto (JOSM or Merkaator)", 52 | "editor:remote:help": "Devi abilitare il controllo remoto in JOSM o Merkaator.", 53 | "add_config": "Aggiungi opzioni di configurazione", 54 | "cancel": "Annulla", 55 | "color_scheme": "Schema dei colori", 56 | "close": "Chiudi", 57 | "download": "Scarica", 58 | "apply-keep": "Applica e continua a modificare", 59 | "apply-close": "Applica e chiudi", 60 | "tip-tutorial": "Guarda il [Tutorial]", 61 | "customCategory:header": "Categorie personali", 62 | "customCategory:clone": "Colona come categoria personale", 63 | "customCategory:create": "Crea categoria personale", 64 | "customCategory:list": "Elenca categorie personali popolari", 65 | "pinnedCategories:forget": "Non fissare la categoria nel tuo profilo", 66 | "copied-clipboard": "Copiato negli appunti", 67 | "empty value": "valore vuoto", 68 | "formatUnits:coordFormat": "Formato coordinate", 69 | "options:fullscreenMode": "Schermo intero", 70 | "options:fullscreenMode:screen": "Usa schermo", 71 | "options:fullscreenMode:window": "Rimani in finestra", 72 | "pinnedCategories:remembered": "Categorie fissate", 73 | "pinnedCategories:remember": "Fissa la categoria nel tuo profilo" 74 | } 75 | -------------------------------------------------------------------------------- /lang/ja.json: -------------------------------------------------------------------------------- 1 | { 2 | "add_filter": "フィルタを追加", 3 | "any value": "任意の値", 4 | "available_branches": "利用可能なブランチ", 5 | "back": "戻る", 6 | "categories": "カテゴリ", 7 | "category-info-tooltip": "情報 & 地図キー", 8 | "closed": "クローズ", 9 | "default": "デフォルト", 10 | "edit": "編集", 11 | "error": "エラー", 12 | "export-all": "地図上にある地物を全てエクスポート", 13 | "export-prepare": "ダウンロードの準備", 14 | "export:GeoJSON": "GeoJSONとしてダウンロード", 15 | "export:OSMJSON": "OSM JSONとしてダウンロード", 16 | "export:OSMXML": "OSM XMLとしてダウンロード", 17 | "facilities": "施設", 18 | "filter:title": "タイトル", 19 | "filter:type": "種類", 20 | "header:attributes": "属性", 21 | "header:export": "エクスポート", 22 | "header:osm_meta": "OSM メタ", 23 | "images": "画像", 24 | "invalid value": "不適切な値", 25 | "loading": "読み込み中...", 26 | "main:options": "オプション設定", 27 | "main:permalink": "パーマリンク", 28 | "more": "もっと", 29 | "more_categories": "カテゴリを一覧から追加", 30 | "more_categories_gitea": "カテゴリを自分自身で作成 & 改善しよう!", 31 | "more_results": "結果をもっと表示", 32 | "open": "開く", 33 | "options:data_lang": "データ表示", 34 | "options:data_lang:desc": "多くの地物にはname(及びその他のタグ)があり、様々な言語(例:'name:en'や'name:de')に翻訳されます。表示に使う言語を、あるいは常に未翻訳の値(例:'name')を表示したければ'ブラウザの設定言語'を指定してください。", 35 | "options:data_lang:local": "ブラウザの設定言語", 36 | "options:overpassUrl": "OverpassAPI のURL", 37 | "options:preferredBaseMap": "ベース地図選択", 38 | "options:ui_lang": "インタフェース表示", 39 | "other": "その他", 40 | "repo-use-as-base": "このリポジトリをベースリポジトリとして使う", 41 | "repositories": "リポジトリ", 42 | "save": "保存", 43 | "show details": "詳細を表示", 44 | "toggle_fullscreen": "全画面モード切替", 45 | "unknown": "不明", 46 | "unnamed": "nameなし", 47 | "wikipedia:no-url-parse": "Wikipedia URLをパースできません", 48 | "zoom_in_appear": "ズームインして地物を表示させる", 49 | "zoom_in_more": "ズームインしてもっと地物を表示させる", 50 | "cancel": "キャンセル", 51 | "editor:id": "iD (ブラウザによるエディタ)", 52 | "editor:remote": "遠隔制御 (JOSMまたはMerkaator)", 53 | "editor:remote:help": "JOSMやMerkaatorで遠隔制御を有効化する必要があります。", 54 | "formatUnits:coordFormat": "座標の形式", 55 | "formatUnits:coordSpacer": "座標の区切文字", 56 | "formatUnits:system": "単位系", 57 | "formatUnits:system:si": "国際単位系", 58 | "formatUnits:system:imp": "ヤード・ポンド法", 59 | "formatUnits:system:nautical": "海里", 60 | "formatUnits:system:m": "常にメートル法", 61 | "formatUnits:speed": "速度単位", 62 | "formatUnits:speed:ft/s": "フィート/秒", 63 | "formatUnits:speed:km/h": "km/時間", 64 | "formatUnits:speed:kn": "ニュートン", 65 | "formatUnits:speed:m/s": "マイル/秒", 66 | "formatUnits:speed:mi/h": "mph", 67 | "form_element:please_select": "-- 選択してください --", 68 | "geoinfo:nw-corner": "北西の角", 69 | "geoinfo:center": "中央", 70 | "geoinfo:centroid": "重心", 71 | "geoinfo:se-corner": "南東の角", 72 | "geoinfo:mouse": "マウスの位置", 73 | "geoinfo:location": "現在地", 74 | "geoinfo:zoom": "ズームレベル", 75 | "geoinfo:header": "ジオメトリ", 76 | "geoinfo:length": "長さ", 77 | "geoinfo:area": "エリア", 78 | "heading:N": "北", 79 | "heading:NE": "北東", 80 | "heading:E": "東", 81 | "heading:SE": "南東", 82 | "heading:S": "南", 83 | "heading:SW": "南西", 84 | "heading:W": "西", 85 | "heading:NW": "北西", 86 | "main:about": "説明", 87 | "main:code": "コード", 88 | "options:debug_mode": "デバッグモード", 89 | "options:chooseEditor": "エディタを選択" 90 | } 91 | -------------------------------------------------------------------------------- /lang/nb.json: -------------------------------------------------------------------------------- 1 | { 2 | "add_filter": "Legg til filter", 3 | "any value": "hvilken som helst verdi", 4 | "available_branches": "Tilgjengelige grener", 5 | "back": "tilbake", 6 | "cancel": "Avbryt", 7 | "categories": "Kategorier", 8 | "category-info-tooltip": "Info & Kart-nøkkelverdi", 9 | "closed": "lukket", 10 | "default": "standard", 11 | "edit": "rediger", 12 | "error": "Feil", 13 | "export-all": "Eksporter alle synlige kartobjekter", 14 | "export-prepare": "Forbered nedlasting", 15 | "export:GeoJSON": "Last ned som GeoJSON", 16 | "export:OSMJSON": "Last ned som OSM-JSON", 17 | "export:OSMXML": "Last ned som OSM-XML", 18 | "facilities": "Fasiliteter", 19 | "filter:title": "Tittel", 20 | "filter:type": "Type", 21 | "form_element:please_select": "-- vennligst velg --", 22 | "header:attributes": "Attributter", 23 | "header:export": "Eksporter", 24 | "header:osm_meta": "OSM-meta", 25 | "images": "Bilder", 26 | "invalid value": "ugyldig verdi", 27 | "loading": "Laster ...", 28 | "main:about": "Om", 29 | "main:code": "Kode", 30 | "main:options": "Innstillinger", 31 | "main:permalink": "Permalink", 32 | "more": "mer", 33 | "more_categories": "Flere kategorier", 34 | "more_categories_gitea": "Lag & forbedre kategoriene selv!", 35 | "more_results": "Vis flere resultater", 36 | "open": "åpen", 37 | "options:data_lang": "Dataspråk", 38 | "options:data_lang:desc": "Mange kartobjekter har navnet sitt (og andre egenskaper) oversatt til andre språk (eks. med 'name:en', 'name:no'). Spesifiser hvilket språk som skal brukes for å vise objektene, eller velg 'Lokalt språk' slik at den uoversatte verdien (eks. 'name') skal brukes.", 39 | "options:data_lang:local": "Lokalt språk", 40 | "options:debug_mode": "Debug-modus", 41 | "options:overpassUrl": "OverpassAPI-URL", 42 | "options:preferredBaseMap": "Foretrukket grunnkart", 43 | "options:ui_lang": "Grensesnittspråk", 44 | "other": "Andre", 45 | "repo-use-as-base": "Bruk dette repo-området som grunn-repo", 46 | "repositories": "Repoer", 47 | "save": "Lagre", 48 | "show details": "vis detaljer", 49 | "toggle_fullscreen": "Slå av/på fullskjermsmodus", 50 | "unknown": "ukjent", 51 | "unnamed": "navnløs", 52 | "wikipedia:no-url-parse": "Kunne ikke håndtere Wikipedia-URLen", 53 | "zoom_in_appear": "zoom inn for å se kartobjekter", 54 | "zoom_in_more": "zoom inn for flere kartobjekter" 55 | } 56 | -------------------------------------------------------------------------------- /lang/nl.json: -------------------------------------------------------------------------------- 1 | { 2 | "add_filter": "Filter toevoegen", 3 | "any value": "om het even welke waarde", 4 | "available_branches": "Beschikbare delen", 5 | "back": "terug", 6 | "categories": "Categorieën", 7 | "category-info-tooltip": "Info en kaart sleutel", 8 | "closed": "gesloten", 9 | "default": "standaard", 10 | "edit": "bewerk", 11 | "error": "Fout", 12 | "export-all": "Exporteer alle zichtbare kaartelementen", 13 | "export-prepare": "Bereid download voor", 14 | "export:GeoJSON": "Download als GeoJSON", 15 | "export:OSMJSON": "Download als OSM JSON", 16 | "export:OSMXML": "Download als OSM XML", 17 | "facilities": "Uitrusting", 18 | "filter:title": "Titel", 19 | "filter:type": "Type", 20 | "header:attributes": "Attributen", 21 | "header:export": "Exporteer", 22 | "header:osm_meta": "OSM Meta", 23 | "images": "Afbeeldingen", 24 | "invalid value": "ongeldige waarde", 25 | "loading": "Laden...", 26 | "main:options": "Opties", 27 | "main:permalink": "Permalink", 28 | "more": "meer", 29 | "more_categories": "Meer categorieën", 30 | "more_categories_gitea": "Creëer & verbeter zelf categorieën!", 31 | "more_results": "Geef meer resultaten weer", 32 | "open": "open", 33 | "options:data_lang": "Taal voor data", 34 | "options:data_lang:desc": "Van veel kaartelementen zijn hun naam (en andere tags) vertaald naar verschillende talen (bv. met 'name:en, 'name:nl'). Geef aan welk taal gebruikt moet worden voor de weergave, of 'Lokale taal', zodat altijd de niet-vertaalde waarde (bv. 'naam') gebruikt wordt.", 35 | "options:data_lang:local": "Lokale taal", 36 | "options:overpassUrl": "OverpassAPI URL", 37 | "options:preferredBaseMap": "Voorkeur basiskaart", 38 | "options:ui_lang": "Interfacetaal", 39 | "other": "Andere", 40 | "repo-use-as-base": "Gebruik deze repository als basis repository", 41 | "repositories": "Repositories", 42 | "save": "Opslaan", 43 | "show details": "geef details weer", 44 | "toggle_fullscreen": "Wisselen van volledig scherm weergave", 45 | "unknown": "onbekend", 46 | "unnamed": "naamloos", 47 | "wikipedia:no-url-parse": "De Wikipedia URL kan niet verwerkt worden", 48 | "zoom_in_appear": "zoom in op de kaart om kaartelementen weer te geven", 49 | "zoom_in_more": "zoom in om voor meer kaartelementen" 50 | } 51 | -------------------------------------------------------------------------------- /lang/oc.json: -------------------------------------------------------------------------------- 1 | { 2 | "add_filter": "Ajustar un filtre de recèrca", 3 | "any value": "Que valor que siegue", 4 | "available_branches": "Brancas disponiblas", 5 | "back": "Tornar", 6 | "categories": "Categorias", 7 | "category-info-tooltip": "Info & Legenda", 8 | "closed": "Plegat", 9 | "default": "Predefinit", 10 | "edit": "editar", 11 | "error": "Error", 12 | "export-all": "Exportar totei leis objèctes cartografiats visibles", 13 | "export-prepare": "Preparar la descarga", 14 | "export:GeoJSON": "Format GeoJSON", 15 | "export:OSMJSON": "Format OSM JSON", 16 | "export:OSMXML": "Format OSM XML", 17 | "facilities": "Servicis", 18 | "filter:title": "Títol", 19 | "filter:type": "Tipe", 20 | "header:attributes": "Atributs", 21 | "header:export": "Exportar", 22 | "header:osm_meta": "Metadadas OSM", 23 | "images": "Imatges", 24 | "invalid value": "Valors non validas", 25 | "loading": "A se cargar...", 26 | "main:options": "Opcions", 27 | "main:permalink": "Permaliame", 28 | "more": "mai", 29 | "more_categories": "Mai de categorias", 30 | "more_categories_gitea": "Creatz e melhoratz vos lei categorias !", 31 | "more_results": "Mostrar mai de resultats", 32 | "open": "obrir", 33 | "options:data_lang": "Lenga dei dadas", 34 | "options:data_lang:desc": "Fòrça elements de la mapa an lo nom (emai d'autreis etiquetas) traduits en divèrsei lengas (exemple : 'name:oc', 'name:br'). Indicatz la lenga d'emplegar premiera, ò l'expression 'Lenga locala' per forçar l'emplec de la lenga de vòstre navigator.", 35 | "options:data_lang:local": "Lenga locala", 36 | "options:overpassUrl": "URL OverpassAPI", 37 | "options:preferredBaseMap": "Fons de mapa preferit", 38 | "options:ui_lang": "Lenga de l'interfàcia", 39 | "other": "Autrei", 40 | "repo-use-as-base": "Emplegar aqueste repertòri per depaus de basa", 41 | "repositories": "Repertòris", 42 | "save": "Sauvagardar", 43 | "show details": "monstrar per lo menut", 44 | "toggle_fullscreen": "Activar/ Desactivar lo plen escran", 45 | "unknown": "non conoissut, -da(s)", 46 | "unnamed": "ges de nom", 47 | "wikipedia:no-url-parse": "Impossible de deschifrar l'URL de Wikipédia", 48 | "zoom_in_appear": "regrandir per mostrar leis elements de la mapa", 49 | "zoom_in_more": "regrandir per monstrar mai d'elements de la mapa", 50 | "cancel": "Anullar", 51 | "editor:id": "iD (editor en linha)", 52 | "editor:remote": "Contròla a Distància (JOSM or Merkaator)", 53 | "editor:remote:help": "Devètz activar lo contròla a distància dins JOSM ò Meekaator.", 54 | "formatUnits:coordFormat": "Format dei coordonadas", 55 | "formatUnits:coordSpacer": "Separador de coordonadas", 56 | "formatUnits:system": "Sistèma d’unitats", 57 | "formatUnits:system:si": "Unitats SI", 58 | "formatUnits:system:imp": "Unitats Imperialas", 59 | "formatUnits:system:nautical": "Nautic", 60 | "formatUnits:system:m": "Sempre mètre", 61 | "formatUnits:speed": "Unitat de velocitat", 62 | "formatUnits:speed:ft/s": "ft/s", 63 | "formatUnits:speed:km/h": "km/h", 64 | "formatUnits:speed:kn": "nos", 65 | "formatUnits:speed:m/s": "m/s", 66 | "formatUnits:speed:mi/h": "mph", 67 | "form_element:please_select": "— seleccionatz per plaser —", 68 | "geoinfo:center": "Centre", 69 | "geoinfo:mouse": "Posicion de la rata", 70 | "geoinfo:zoom": "Nivèu de zoom", 71 | "geoinfo:header": "Geometria", 72 | "geoinfo:length": "Longor", 73 | "geoinfo:area": "Superficia", 74 | "heading:N": "N", 75 | "heading:NE": "NE", 76 | "heading:E": "E", 77 | "heading:SE": "SE", 78 | "heading:S": "S", 79 | "heading:SW": "SO", 80 | "heading:W": "O", 81 | "heading:NW": "NO", 82 | "main:about": "A prepaus de", 83 | "main:code": "Còde", 84 | "options:chooseEditor": "Chaussissètz un editor", 85 | "empty value": "valor vueja", 86 | "geoinfo:nw-corner": "Canton nòrd-oest", 87 | "geoinfo:centroid": "Centroïd", 88 | "geoinfo:se-corner": "Canton sud-est", 89 | "geoinfo:location": "Posicion actuala", 90 | "options:debug_mode": "Depuracion" 91 | } 92 | -------------------------------------------------------------------------------- /lang/pt.json: -------------------------------------------------------------------------------- 1 | { 2 | "add_filter": "Adicionar filtro", 3 | "any value": "qualquer valor", 4 | "available_branches": "", 5 | "back": "voltar", 6 | "categories": "Categorias", 7 | "category-info-tooltip": "Info & Legenda", 8 | "closed": "fechado", 9 | "default": "padrão", 10 | "edit": "editar", 11 | "error": "Erro", 12 | "export-all": "", 13 | "export-prepare": "", 14 | "export:GeoJSON": "Descarregar como GeoJSON", 15 | "export:OSMJSON": "", 16 | "export:OSMXML": "", 17 | "facilities": "Instalações", 18 | "filter:title": "", 19 | "filter:type": "", 20 | "header:attributes": "Atributos", 21 | "header:export": "Exportar", 22 | "header:osm_meta": "OSM Meta", 23 | "images": "Imagens", 24 | "invalid value": "", 25 | "loading": "A carregar...", 26 | "main:options": "Opções", 27 | "main:permalink": "", 28 | "more": "mais", 29 | "more_categories": "Mais categorias", 30 | "more_categories_gitea": "Criar & melhorar categorias você mesmo!", 31 | "more_results": "", 32 | "open": "abrir", 33 | "options:data_lang": "Língua dos dados", 34 | "options:data_lang:desc": "Muitos elementos do mapa possuem seus nomes (e outras etiquetas) traduzidas para línguas diferentes (p.ex. com 'name:en', 'name:de'). Especificar qual língua deve ser usada para exibição, ou 'Língua local' de forma que sempre os valores não tranduzidos (p.ex. 'name') sejam usados.", 35 | "options:data_lang:local": "Língua local", 36 | "options:overpassUrl": "URL do OverpassAPI", 37 | "options:preferredBaseMap": "Mapa-base preferido", 38 | "options:ui_lang": "Língua da interface", 39 | "other": "Outro", 40 | "repo-use-as-base": "", 41 | "repositories": "", 42 | "save": "Guardar", 43 | "show details": "mostrar detalhes", 44 | "toggle_fullscreen": "Alternar modo ecrã inteiro", 45 | "unknown": "desconhecido", 46 | "unnamed": "sem nome", 47 | "wikipedia:no-url-parse": "Não foi possível analisar URL da Wikipédia", 48 | "zoom_in_appear": "Faça zoom in para detalhes do mapa aparecerem", 49 | "zoom_in_more": "Faça zoom in para mostrar mais detalhes no mapa", 50 | "cancel": "Cancelar", 51 | "close": "Fechar", 52 | "download": "Baixar", 53 | "apply-keep": "Aplicar e continuar editando", 54 | "apply-close": "Aplicar e fechar" 55 | } 56 | -------------------------------------------------------------------------------- /lang/ro.json: -------------------------------------------------------------------------------- 1 | { 2 | "add_filter": "Adaugă filtru", 3 | "any value": "orice valoare", 4 | "available_branches": "", 5 | "back": "înapoi", 6 | "categories": "Categorii", 7 | "category-info-tooltip": "", 8 | "closed": "închis", 9 | "default": "Implicit", 10 | "edit": "", 11 | "error": "Eroare", 12 | "export-all": "", 13 | "export-prepare": "Pregătiți descărcarea", 14 | "export:GeoJSON": "", 15 | "export:OSMJSON": "", 16 | "export:OSMXML": "", 17 | "facilities": "Facilități", 18 | "filter:title": "Titlu", 19 | "filter:type": "Tip", 20 | "header:attributes": "", 21 | "header:export": "", 22 | "header:osm_meta": "", 23 | "images": "Imagini", 24 | "invalid value": "", 25 | "loading": "Se încarcă ...", 26 | "main:options": "Opțiuni", 27 | "main:permalink": "", 28 | "more": "Mai mult", 29 | "more_categories": "Mai multe categorii", 30 | "more_categories_gitea": "", 31 | "more_results": "Afișați mai multe rezultate", 32 | "open": "deschide", 33 | "options:data_lang": "Limba date", 34 | "options:data_lang:desc": "", 35 | "options:data_lang:local": "Limba locală", 36 | "options:overpassUrl": "", 37 | "options:preferredBaseMap": "", 38 | "options:ui_lang": "Limba interfata", 39 | "other": "", 40 | "repo-use-as-base": "", 41 | "repositories": "", 42 | "save": "Salvează", 43 | "show details": "arată detalii", 44 | "toggle_fullscreen": "", 45 | "unknown": "necunoscut", 46 | "unnamed": "anonim", 47 | "wikipedia:no-url-parse": "", 48 | "zoom_in_appear": "", 49 | "zoom_in_more": "", 50 | "cancel": "Renunță", 51 | "main:about": "Despre", 52 | "form_element:please_select": "-- te rog selectează --", 53 | "formatUnits:system": "Sistem de unitate", 54 | "formatUnits:speed": "Unitate de viteză", 55 | "formatUnits:speed:km/h": "km/h", 56 | "formatUnits:speed:m/s": "m/s", 57 | "geoinfo:location": "Locația curentă", 58 | "heading:N": "N", 59 | "heading:NE": "NE", 60 | "heading:E": "E", 61 | "heading:SE": "SE", 62 | "heading:S": "S", 63 | "heading:SW": "SV", 64 | "heading:W": "V", 65 | "heading:NW": "NV" 66 | } 67 | -------------------------------------------------------------------------------- /lang/ru.json: -------------------------------------------------------------------------------- 1 | { 2 | "add_filter": "Добавить фильтр", 3 | "any value": "любое значение", 4 | "available_branches": "", 5 | "back": "назад", 6 | "categories": "Категории", 7 | "category-info-tooltip": "Информация и легенда", 8 | "closed": "", 9 | "default": "по умолчанию", 10 | "edit": "изменить", 11 | "error": "Ошибка", 12 | "export-all": "Экспортировать все видимые объекты карты", 13 | "export-prepare": "Загрузить", 14 | "export:GeoJSON": "Скачать как GeoJSON", 15 | "export:OSMJSON": "Скачать как OSM JSON", 16 | "export:OSMXML": "Скачать как OSM XML", 17 | "facilities": "Удобства", 18 | "filter:title": "Название", 19 | "filter:type": "Тип", 20 | "header:attributes": "Атрибуты", 21 | "header:export": "Экспорт", 22 | "header:osm_meta": "OSM Meta", 23 | "images": "Изображения", 24 | "invalid value": "неверное значение", 25 | "loading": "Загрузка...", 26 | "main:options": "Настройки", 27 | "main:permalink": "Постоянная ссылка", 28 | "more": "Ещё", 29 | "more_categories": "Больше категорий", 30 | "more_categories_gitea": "Создавайте и улучшайте категории самостоятельно!", 31 | "more_results": "Больше результатов", 32 | "open": "открыть", 33 | "options:data_lang": "Язык информации на карте", 34 | "options:data_lang:desc": "Названия многих элементов карты (и другие теги), переведены на разные языки (например, с помощью «name: en», «name: de»). Укажите, какой язык следует использовать для отображения, или «Местный язык», чтобы всегда использовалось непереведенное значение (например, «name»).", 35 | "options:data_lang:local": "Определить язык автоматически", 36 | "options:overpassUrl": "OverpassAPI URL", 37 | "options:preferredBaseMap": "Предпочитаемая карта", 38 | "options:ui_lang": "Язык интерфейса", 39 | "other": "Прочие", 40 | "repo-use-as-base": "Использовать этот репозиторий как базовый", 41 | "repositories": "Репозитории", 42 | "save": "Сохранить", 43 | "show details": "подробнее", 44 | "toggle_fullscreen": "Полноэкранный режим", 45 | "unknown": "неизвестно", 46 | "unnamed": "безымянный", 47 | "wikipedia:no-url-parse": "Не удалось разобрать Wikipedia URL", 48 | "zoom_in_appear": "приблизьте, для отображения объектов карты", 49 | "zoom_in_more": "приблизьте, для большего количества объектов карты", 50 | "cancel": "Отменить", 51 | "color_scheme": "Цветовая схема", 52 | "close": "Закрыть", 53 | "apply-keep": "Применить и продолжить редактирование", 54 | "apply-close": "Применить и закрыть", 55 | "editor:id": "iD (редактор в браузере)", 56 | "download": "Скачать", 57 | "tip-tutorial": "Открыть [Tutorial]", 58 | "customCategory:header": "Избранные категории", 59 | "customCategory:clone": "Клонировать как избранную категорию", 60 | "customCategory:list": "Список популярных категорий", 61 | "customCategory:create": "Создать категорию", 62 | "empty value": "пустое значение", 63 | "editor:remote": "Удаленное управление (JOSM или Merkaator)", 64 | "editor:remote:help": "Требуется включить удалённое управление в JOSM или Merkaator.", 65 | "formatUnits:coordFormat": "Формат координат", 66 | "form_element:please_select": "-- выберите --", 67 | "formatUnits:speed": "Единицы измерения скорости", 68 | "geoinfo:location": "Текущее местоположение", 69 | "geoinfo:zoom": "Уровень приближения", 70 | "main:about": "О проекте", 71 | "main:code": "Github", 72 | "options:debug_mode": "Режим отладки", 73 | "options:chooseEditor": "Выберите редактор" 74 | } 75 | -------------------------------------------------------------------------------- /lang/sr.json: -------------------------------------------------------------------------------- 1 | { 2 | "add_filter": "Додај филтер", 3 | "any value": "било која вредност", 4 | "available_branches": "", 5 | "back": "назад", 6 | "categories": "Категорије", 7 | "category-info-tooltip": "", 8 | "closed": "затворено", 9 | "default": "", 10 | "edit": "измени", 11 | "error": "Грешка", 12 | "export-all": "", 13 | "export-prepare": "", 14 | "export:GeoJSON": "Преузми у GeoJSON формату", 15 | "export:OSMJSON": "Преузми у OSM JSON формату", 16 | "export:OSMXML": "Преузми у OSM XML формату", 17 | "facilities": "", 18 | "filter:title": "", 19 | "filter:type": "", 20 | "header:attributes": "Својства", 21 | "header:export": "Извоз", 22 | "header:osm_meta": "", 23 | "images": "Слике", 24 | "invalid value": "неприхватљива вредност", 25 | "loading": "Учитавање...", 26 | "main:options": "Опције", 27 | "main:permalink": "Трајна веза", 28 | "more": "још", 29 | "more_categories": "Више категорија", 30 | "more_categories_gitea": "", 31 | "more_results": "Прикажи још резултата", 32 | "open": "отвори", 33 | "options:data_lang": "Језик података", 34 | "options:data_lang:desc": "", 35 | "options:data_lang:local": "Локални језик", 36 | "options:overpassUrl": "", 37 | "options:preferredBaseMap": "", 38 | "options:ui_lang": "Језик интерфејса", 39 | "other": "", 40 | "repo-use-as-base": "", 41 | "repositories": "", 42 | "save": "Запамти", 43 | "show details": "прикажи детаље", 44 | "toggle_fullscreen": "Преко целог екрана", 45 | "unknown": "непознато", 46 | "unnamed": "без имена", 47 | "wikipedia:no-url-parse": "", 48 | "zoom_in_appear": "", 49 | "zoom_in_more": "", 50 | "cancel": "Откажи", 51 | "formatUnits:system": "Систем јединица", 52 | "formatUnits:system:si": "СИ јединице", 53 | "formatUnits:system:imp": "Империјалне јединице", 54 | "formatUnits:system:nautical": "Наутичке", 55 | "formatUnits:system:m": "Увек метар", 56 | "formatUnits:speed": "Јединица за брзину", 57 | "formatUnits:speed:km/h": "км/ч", 58 | "formatUnits:speed:kn": "чворова", 59 | "formatUnits:speed:m/s": "м/с", 60 | "formatUnits:speed:mi/h": "миља/ч", 61 | "form_element:please_select": "молимо одаберите", 62 | "geoinfo:nw-corner": "Северозападни угао", 63 | "geoinfo:center": "Центар", 64 | "geoinfo:se-corner": "Југоистични угао", 65 | "geoinfo:mouse": "Позиција миша", 66 | "geoinfo:location": "Тренутна локација", 67 | "geoinfo:zoom": "Степен увећања", 68 | "geoinfo:header": "Геометрија", 69 | "geoinfo:length": "Дужина", 70 | "geoinfo:area": "Површина", 71 | "heading:N": "С", 72 | "heading:NE": "СИ", 73 | "heading:E": "И", 74 | "heading:SE": "ЈИ", 75 | "heading:S": "Ј", 76 | "heading:SW": "ЈЗ", 77 | "heading:W": "З", 78 | "heading:NW": "СЗ", 79 | "main:code": "Код", 80 | "options:debug_mode": "Режим за откривање грешака" 81 | } 82 | -------------------------------------------------------------------------------- /lang/zh-hans.json: -------------------------------------------------------------------------------- 1 | { 2 | "add_filter": "添加过滤器", 3 | "any value": "任意值", 4 | "available_branches": "可用分支", 5 | "back": "返回", 6 | "cancel": "取消", 7 | "categories": "分类", 8 | "category-info-tooltip": "信息&地图键", 9 | "closed": "关闭的", 10 | "default": "默认", 11 | "edit": "编辑", 12 | "editor:id": "iD(浏览器内编辑器)", 13 | "editor:remote": "远程控制(JOSM或Merkaator)", 14 | "editor:remote:help": "你需要在JOSM或Merkaator中允许远程控制。", 15 | "error": "错误", 16 | "empty value": "空值", 17 | "export-all": "导出所有可见地图要素", 18 | "export-prepare": "准备下载", 19 | "export:GeoJSON": "以GeoJSON下载", 20 | "export:OSMJSON": "以OSM JSON下载", 21 | "export:OSMXML": "以OSM XML下载", 22 | "facilities": "设施", 23 | "filter:title": "标题", 24 | "filter:type": "类型", 25 | "formatUnits:coordFormat": "坐标格式", 26 | "formatUnits:coordSpacer": "坐标空间", 27 | "formatUnits:system": "单位系统", 28 | "formatUnits:system:si": "国际单位制", 29 | "formatUnits:system:imp": "英制", 30 | "formatUnits:system:nautical": "航海制", 31 | "formatUnits:system:m": "永远按米", 32 | "formatUnits:speed": "速度单位", 33 | "formatUnits:speed:ft/s": "ft/s", 34 | "formatUnits:speed:km/h": "km/h", 35 | "formatUnits:speed:kn": "kn", 36 | "formatUnits:speed:m/s": "m/s", 37 | "formatUnits:speed:mi/h": "mph", 38 | "form_element:please_select": "-- 请选择 --", 39 | "geoinfo:nw-corner": "西北角", 40 | "geoinfo:center": "中心", 41 | "geoinfo:centroid": "几何中心", 42 | "geoinfo:se-corner": "东南角", 43 | "geoinfo:mouse": "鼠标所处位置", 44 | "geoinfo:location": "当前位置", 45 | "geoinfo:zoom": "缩放等级", 46 | "geoinfo:header": "几何学信息", 47 | "geoinfo:length": "长度", 48 | "geoinfo:area": "面积", 49 | "header:attributes": "署名", 50 | "header:export": "导出", 51 | "header:osm_meta": "OSM 元数据", 52 | "heading:N": "北", 53 | "heading:NE": "东北", 54 | "heading:E": "东", 55 | "heading:SE": "东南", 56 | "heading:S": "南", 57 | "heading:SW": "西南", 58 | "heading:W": "西", 59 | "heading:NW": "西北", 60 | "images": "图像", 61 | "invalid value": "无效值", 62 | "loading": "加载中……", 63 | "main:about": "关于", 64 | "main:code": "代码", 65 | "main:options": "选项", 66 | "main:permalink": "固定链接", 67 | "more": "更多", 68 | "more_categories": "更多分类", 69 | "more_categories_gitea": "自己创建或改进分类", 70 | "more_results": "展示更多结果", 71 | "open": "打开", 72 | "options:data_lang": "数据语言", 73 | "options:data_lang:desc": "许多地图要素的名称或其他标签都有不同语言的翻译(如'name:en'、'name:de')。选择显示那种语言,或者选择“当地语言”以使用无翻译的值(如'name')。", 74 | "options:data_lang:local": "本地语言", 75 | "options:debug_mode": "调试模式", 76 | "options:overpassUrl": "OverpassAPI URL", 77 | "options:preferredBaseMap": "底图偏好", 78 | "options:ui_lang": "界面语言", 79 | "options:chooseEditor": "选择编辑器", 80 | "other": "其他", 81 | "repo-use-as-base": "将这个仓库作为基仓库", 82 | "repositories": "仓库", 83 | "save": "保存", 84 | "show details": "显示细节", 85 | "toggle_fullscreen": "切换全屏模式", 86 | "unknown": "未知", 87 | "unnamed": "未命名", 88 | "wikipedia:no-url-parse": "不能识别的Wikipedia URL", 89 | "zoom_in_appear": "放大以显示地图要素", 90 | "zoom_in_more": "放大以显示更多地图要素", 91 | "add_config": "增加配置选项", 92 | "color_scheme": "色彩方案", 93 | "close": "关闭", 94 | "download": "下载", 95 | "apply-keep": "应用并保存编辑内容", 96 | "apply-close": "应用并关闭", 97 | "tip-tutorial": "查看[教程]", 98 | "customCategory:header": "自定义类别", 99 | "customCategory:clone": "克隆为自定义类别", 100 | "customCategory:create": "创建自定义类别", 101 | "pinnedCategories:forget": "在个人简介中取消固定的类别", 102 | "pinnedCategories:remember": "固定类别至个人简介", 103 | "pinnedCategories:remembered": "已固定的类别", 104 | "copied-clipboard": "已复制到剪贴板", 105 | "formatUnits:coordSpacer:colon": "冒号", 106 | "formatUnits:coordSpacer:space": "空格" 107 | } 108 | -------------------------------------------------------------------------------- /lib/tag2link-sophox.qry: -------------------------------------------------------------------------------- 1 | SELECT ?item ?itemLabel (CONCAT("Key:", ?permanent_key_ID) as ?OSM_key) ?formatter_URL WHERE { 2 | FILTER(?permanent_key_ID NOT IN ('image', 'url', 'website', 'wikidata', 'wikimedia_commons')). 3 | ?item osmdt:P2 osmd:Q7. 4 | SERVICE wikibase:label { bd:serviceParam wikibase:language "en". } 5 | ?item osmdt:P16 ?permanent_key_ID. 6 | ?item osmdt:P8 ?formatter_URL. 7 | } 8 | -------------------------------------------------------------------------------- /lib/tag2link-wikidata.qry: -------------------------------------------------------------------------------- 1 | SELECT ?itemLabel ?OSM_key ?formatter_URL ?operatorLabel WHERE { 2 | ?item wdt:P1282 ?OSM_key . 3 | FILTER(?OSM_key NOT IN("Key:image", "Key:url", "Key:website", "Key:wikidata", "Key:wikimedia_commons")) 4 | SERVICE wikibase:label { bd:serviceParam wikibase:language "en". } 5 | { 6 | ?item p:P1630 ?statement. 7 | ?statement ps:P1630 ?formatter_URL. 8 | } 9 | UNION 10 | { 11 | ?item p:P3303 ?statement. 12 | ?statement ps:P3303 ?formatter_URL. 13 | } 14 | OPTIONAL { ?statement pq:P137 ?operator. } 15 | } 16 | -------------------------------------------------------------------------------- /locales/ast.js: -------------------------------------------------------------------------------- 1 | global.locale = { 2 | id: 'ast', 3 | moment: require('moment'), 4 | osmDateFormatTemplates: require('openstreetmap-date-format/templates/en') 5 | } 6 | 7 | //require('moment/locale/ast') 8 | -------------------------------------------------------------------------------- /locales/ca.js: -------------------------------------------------------------------------------- 1 | global.locale = { 2 | id: 'ca', 3 | moment: require('moment'), 4 | osmDateFormatTemplates: require('openstreetmap-date-format/templates/de') 5 | } 6 | 7 | require('moment/locale/ca') 8 | -------------------------------------------------------------------------------- /locales/cs.js: -------------------------------------------------------------------------------- 1 | global.locale = { 2 | id: 'cs', 3 | moment: require('moment'), 4 | osmDateFormatTemplates: require('openstreetmap-date-format/templates/en') 5 | } 6 | 7 | require('moment/locale/cs') 8 | -------------------------------------------------------------------------------- /locales/da.js: -------------------------------------------------------------------------------- 1 | global.locale = { 2 | id: 'da', 3 | moment: require('moment'), 4 | osmDateFormatTemplates: require('openstreetmap-date-format/templates/en') 5 | } 6 | 7 | require('moment/locale/da') 8 | -------------------------------------------------------------------------------- /locales/de.js: -------------------------------------------------------------------------------- 1 | global.locale = { 2 | id: 'de', 3 | moment: require('moment'), 4 | osmDateFormatTemplates: require('openstreetmap-date-format/templates/de') 5 | } 6 | 7 | require('moment/locale/de') 8 | -------------------------------------------------------------------------------- /locales/el.js: -------------------------------------------------------------------------------- 1 | global.locale = { 2 | id: 'el', 3 | moment: require('moment'), 4 | osmDateFormatTemplates: require('openstreetmap-date-format/templates/en') 5 | } 6 | 7 | require('moment/locale/el') 8 | -------------------------------------------------------------------------------- /locales/en.js: -------------------------------------------------------------------------------- 1 | global.locale = { 2 | id: 'en', 3 | moment: require('moment'), 4 | osmDateFormatTemplates: require('openstreetmap-date-format/templates/en') 5 | } 6 | -------------------------------------------------------------------------------- /locales/es.js: -------------------------------------------------------------------------------- 1 | global.locale = { 2 | id: 'es', 3 | moment: require('moment'), 4 | osmDateFormatTemplates: require('openstreetmap-date-format/templates/en') 5 | } 6 | 7 | require('moment/locale/es') 8 | -------------------------------------------------------------------------------- /locales/et.js: -------------------------------------------------------------------------------- 1 | global.locale = { 2 | id: 'et', 3 | moment: require('moment'), 4 | osmDateFormatTemplates: require('openstreetmap-date-format/templates/en') 5 | } 6 | 7 | require('moment/locale/et') 8 | -------------------------------------------------------------------------------- /locales/fr.js: -------------------------------------------------------------------------------- 1 | global.locale = { 2 | id: 'fr', 3 | moment: require('moment'), 4 | osmDateFormatTemplates: require('openstreetmap-date-format/templates/fr') 5 | } 6 | 7 | require('moment/locale/fr') 8 | -------------------------------------------------------------------------------- /locales/hu.js: -------------------------------------------------------------------------------- 1 | global.locale = { 2 | id: 'hu', 3 | moment: require('moment'), 4 | osmDateFormatTemplates: require('openstreetmap-date-format/templates/en') 5 | } 6 | 7 | require('moment/locale/hu') 8 | -------------------------------------------------------------------------------- /locales/it.js: -------------------------------------------------------------------------------- 1 | global.locale = { 2 | id: 'it', 3 | moment: require('moment'), 4 | osmDateFormatTemplates: require('openstreetmap-date-format/templates/en') 5 | } 6 | 7 | require('moment/locale/it') 8 | -------------------------------------------------------------------------------- /locales/ja.js: -------------------------------------------------------------------------------- 1 | global.locale = { 2 | id: 'ja', 3 | moment: require('moment'), 4 | osmDateFormatTemplates: require('openstreetmap-date-format/templates/en') 5 | } 6 | 7 | require('moment/locale/ja') 8 | -------------------------------------------------------------------------------- /locales/nb.js: -------------------------------------------------------------------------------- 1 | global.locale = { 2 | id: 'nb', 3 | moment: require('moment'), 4 | osmDateFormatTemplates: require('openstreetmap-date-format/templates/en') 5 | } 6 | 7 | require('moment/locale/nb') 8 | -------------------------------------------------------------------------------- /locales/nl.js: -------------------------------------------------------------------------------- 1 | global.locale = { 2 | id: 'nl', 3 | moment: require('moment'), 4 | osmDateFormatTemplates: require('openstreetmap-date-format/templates/nl') 5 | } 6 | 7 | require('moment/locale/nl') 8 | -------------------------------------------------------------------------------- /locales/pl.js: -------------------------------------------------------------------------------- 1 | global.locale = { 2 | id: 'pl', 3 | moment: require('moment'), 4 | osmDateFormatTemplates: require('openstreetmap-date-format/templates/en') 5 | } 6 | 7 | require('moment/locale/pl') 8 | -------------------------------------------------------------------------------- /locales/pt-br.js: -------------------------------------------------------------------------------- 1 | global.locale = { 2 | id: 'pt-br', 3 | moment: require('moment'), 4 | osmDateFormatTemplates: require('openstreetmap-date-format/templates/en') 5 | } 6 | 7 | require('moment/locale/pt-br') 8 | -------------------------------------------------------------------------------- /locales/pt.js: -------------------------------------------------------------------------------- 1 | global.locale = { 2 | id: 'pt', 3 | moment: require('moment'), 4 | osmDateFormatTemplates: require('openstreetmap-date-format/templates/en') 5 | } 6 | 7 | require('moment/locale/pt') 8 | -------------------------------------------------------------------------------- /locales/ro.js: -------------------------------------------------------------------------------- 1 | global.locale = { 2 | id: 'ro', 3 | moment: require('moment'), 4 | osmDateFormatTemplates: require('openstreetmap-date-format/templates/en') 5 | } 6 | 7 | require('moment/locale/ro') 8 | -------------------------------------------------------------------------------- /locales/ru.js: -------------------------------------------------------------------------------- 1 | global.locale = { 2 | id: 'ru', 3 | moment: require('moment'), 4 | osmDateFormatTemplates: require('openstreetmap-date-format/templates/en') 5 | } 6 | 7 | require('moment/locale/ru') 8 | -------------------------------------------------------------------------------- /locales/sr.js: -------------------------------------------------------------------------------- 1 | global.locale = { 2 | id: 'sr', 3 | moment: require('moment'), 4 | osmDateFormatTemplates: require('openstreetmap-date-format/templates/en') 5 | } 6 | 7 | require('moment/locale/sr') 8 | -------------------------------------------------------------------------------- /locales/th.js: -------------------------------------------------------------------------------- 1 | global.locale = { 2 | id: 'th', 3 | moment: require('moment'), 4 | osmDateFormatTemplates: require('openstreetmap-date-format/templates/en') 5 | } 6 | 7 | require('moment/locale/th') 8 | -------------------------------------------------------------------------------- /locales/tr.js: -------------------------------------------------------------------------------- 1 | global.locale = { 2 | id: 'tr', 3 | moment: require('moment'), 4 | osmDateFormatTemplates: require('openstreetmap-date-format/templates/en') 5 | } 6 | 7 | require('moment/locale/tr') 8 | -------------------------------------------------------------------------------- /locales/uk.js: -------------------------------------------------------------------------------- 1 | global.locale = { 2 | id: 'uk', 3 | moment: require('moment'), 4 | osmDateFormatTemplates: require('openstreetmap-date-format/templates/en') 5 | } 6 | 7 | require('moment/locale/uk') 8 | -------------------------------------------------------------------------------- /locales/zh-hans.js: -------------------------------------------------------------------------------- 1 | global.locale = { 2 | id: 'zh-hans', 3 | moment: require('moment'), 4 | osmDateFormatTemplates: require('openstreetmap-date-format/templates/en') 5 | } 6 | 7 | require('moment/locale/zh-cn') 8 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "OpenStreetBrowser", 3 | "name": "OpenStreetBrowser", 4 | "start_url": ".", 5 | "background_color": "#ffffff", 6 | "display": "browser", 7 | "icons": [ 8 | { 9 | "src": "img/osb-192.png", 10 | "type": "image/png", 11 | "sizes": "192x192" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /modulekit.php: -------------------------------------------------------------------------------- 1 | array( 14 | 'src/defaults.php', 15 | 'src/database.php', 16 | 'src/options.php', 17 | 'src/language.php', 18 | 'src/ip-location.php', 19 | 'src/wikidata.php', 20 | 'src/wikipedia.php', 21 | 'src/ImageLoader.php', 22 | 'src/RepositoryBase.php', 23 | 'src/RepositoryDir.php', 24 | 'src/RepositoryGit.php', 25 | 'src/repositories.php', 26 | 'src/repositoriesGitea.php', 27 | 'src/customCategory.php', 28 | ), 29 | 'css' => array( 30 | 'style.css', 31 | ), 32 | ); 33 | $version = "5.4"; 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "openstreetbrowser", 3 | "version": "5.4.0", 4 | "description": "A re-make of the famous OpenStreetBrowser (pure JS, using Overpass API)", 5 | "main": "src/export.js", 6 | "repository": "https://github.com/plepe/openstreetbrowser", 7 | "author": "Stephan Bösch-Plepelits ", 8 | "license": "GPL-3.0", 9 | "dependencies": { 10 | "@babel/cli": "^7.19.3", 11 | "@babel/core": "^7.19.3", 12 | "@babel/plugin-transform-runtime": "^7.19.1", 13 | "@babel/preset-env": "^7.19.4", 14 | "@fortawesome/fontawesome-free": "^6.5.1", 15 | "@mapbox/maki": "^8.0.1", 16 | "@rapideditor/temaki": "^5.7.0", 17 | "@turf/area": "^6.3.0", 18 | "@turf/boolean-within": "^7.0.0", 19 | "@turf/length": "^6.3.0", 20 | "async": "^3.2.4", 21 | "babelify": "^10.0.0", 22 | "color-interpolate": "^1.0.5", 23 | "event-emitter": "^0.3.5", 24 | "file-saver": "^2.0.5", 25 | "formatcoords": "^1.1.3", 26 | "i18next-client": "^1.11.4", 27 | "ip-location": "^1.0.1", 28 | "js-yaml": "^4.1.0", 29 | "json-multiline-strings": "^0.1.0", 30 | "leaflet": "^1.9.4", 31 | "leaflet-geosearch": "^3.7.0", 32 | "leaflet-polylineoffset": "^1.1.1", 33 | "leaflet-textpath": "git+https://github.com/makinacorpus/Leaflet.TextPath.git#leaflet0.8-dev", 34 | "leaflet.locatecontrol": "^0.72.2", 35 | "leaflet.polylinemeasure": "git+https://github.com/ppete2/Leaflet.PolylineMeasure.git", 36 | "md5": "^2.3.0", 37 | "measure-ts": "^3.3.2", 38 | "mini-svg-data-uri": "^1.4.4", 39 | "modulekit-tabs": "^0.2.2", 40 | "moment": "^2.29.3", 41 | "natsort": "^2.0.3", 42 | "opening_hours": "^3.8.0", 43 | "openstreetbrowser-categories-main": "git+https://github.com/plepe/openstreetbrowser-categories-main.git", 44 | "openstreetbrowser-markers": "^1.2.0", 45 | "openstreetmap-date-format": "^0.4.0", 46 | "openstreetmap-date-parser": "^0.1.2", 47 | "openstreetmap-tag-translations": "git+https://github.com/plepe/openstreetmap-tag-translations.git", 48 | "overpass-frontend": "^3.1.2", 49 | "overpass-layer": "^3.4.0", 50 | "query-string": "^6.13.8", 51 | "sheet-router": "^4.2.3", 52 | "sprintf-js": "^1.1.2", 53 | "weight-sort": "^1.3.1" 54 | }, 55 | "overrides": { 56 | "osmtogeojson": { 57 | "@xmldom/xmldom": "~0.8.10" 58 | } 59 | }, 60 | "scripts": { 61 | "test": "mocha --bail", 62 | "build": "npm run build-locales && npm run build-code", 63 | "build-code": "browserify -g browserify-css src/index.js -t [ babelify ] -o dist/openstreetbrowser.js && minify dist/openstreetbrowser.js > dist/openstreetbrowser.min.js", 64 | "build-locales": "for i in `ls locales/` ; do browserify locales/$i -o dist/locale-$i ; done", 65 | "watch": "watchify --debug -g browserify-css src/index.js -o dist/openstreetbrowser.js -v", 66 | "prepare": "npm run build", 67 | "lint": "standard src/*.js" 68 | }, 69 | "devDependencies": { 70 | "browserify": "^17.0.0", 71 | "browserify-css": "^0.15.0", 72 | "leaflet-polylinedecorator": "git+https://github.com/plepe/Leaflet.PolylineDecorator.git", 73 | "minify": "^7.2.2", 74 | "mocha": "^11.1.0", 75 | "standard": "^16.0.4", 76 | "watchify": "^4.0.0" 77 | }, 78 | "standard": { 79 | "global": [ 80 | "lang", 81 | "ui_lang", 82 | "config", 83 | "options", 84 | "alert", 85 | "L", 86 | "register_hook", 87 | "call_hooks", 88 | "call_hooks_callback", 89 | "XMLHttpRequest", 90 | "map", 91 | "overpassFrontend", 92 | "location", 93 | "baseCategory", 94 | "currentPath", 95 | "overpassUrl", 96 | "ajax" 97 | ], 98 | "rules": { 99 | "camelcase": 0 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /repo.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | $repoData) { 16 | $repo = getRepo($repoId, $repoData); 17 | 18 | if (!$repo->isEmpty()) { 19 | print $c++ ? ',' : ''; 20 | print json_encode($repoId, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES) . ':'; 21 | $info = $repo->info(); 22 | 23 | if (isset($repoData['repositoryUrl'])) { 24 | $info['repositoryUrl'] = $repoData['repositoryUrl']; 25 | } 26 | if (isset($repoData['categoryUrl'])) { 27 | $info['categoryUrl'] = $repoData['categoryUrl']; 28 | } 29 | $info['group'] = $repoData['group'] ?? 'default'; 30 | 31 | print json_encode($info, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_FORCE_OBJECT); 32 | } 33 | } 34 | 35 | print '}'; 36 | exit(0); 37 | } 38 | 39 | $fullRepoId = $_REQUEST['repo']; 40 | list($repoId, $branchId) = explode('~', $fullRepoId); 41 | if (array_key_exists('lang', $_REQUEST)) { 42 | $fullRepoId .= '~' . $_REQUEST['lang']; 43 | } 44 | 45 | if (!array_key_exists($repoId, $allRepositories)) { 46 | Header("HTTP/1.1 404 Repository not found"); 47 | exit(0); 48 | } 49 | 50 | $repoData = $allRepositories[$repoId]; 51 | $repo = getRepo($repoId, $repoData); 52 | 53 | if ($branchId) { 54 | try { 55 | $repo->setBranch($branchId); 56 | } 57 | catch (Exception $e) { 58 | Header("HTTP/1.1 404 No such branch"); 59 | exit(0); 60 | } 61 | } 62 | 63 | if (array_key_exists('file', $_REQUEST)) { 64 | $file = $repo->file_get_contents($_REQUEST['file']); 65 | 66 | if ($file === false) { 67 | Header("HTTP/1.1 403 Forbidden"); 68 | print "Access denied."; 69 | } 70 | else if ($file === null) { 71 | Header("HTTP/1.1 404 File not found"); 72 | print "File not found."; 73 | } 74 | else { 75 | Header("Content-Type: text/plain; charset=utf-8"); 76 | print $file; 77 | } 78 | 79 | exit(0); 80 | } 81 | 82 | $cacheDir = null; 83 | $ts = $repo->timestamp($path); 84 | if (isset($config['cache'])) { 85 | $cacheDir = "{$config['cache']}/repo"; 86 | @mkdir($cacheDir); 87 | $cacheTs = filemtime("{$cacheDir}/{$fullRepoId}.json"); 88 | if ($cacheTs === $ts) { 89 | Header("Content-Type: application/json; charset=utf-8"); 90 | readfile("{$cacheDir}/{$fullRepoId}.json"); 91 | exit(0); 92 | } 93 | } 94 | 95 | $data = $repo->data($_REQUEST); 96 | 97 | $repo->updateLang($data, $_REQUEST); 98 | 99 | if (!array_key_exists('index', $data['categories'])) { 100 | $data['categories']['index'] = array( 101 | 'type' => 'index', 102 | 'subCategories' => array_map( 103 | function ($k) { 104 | return array('id' => $k); 105 | }, array_keys($data['categories'])) 106 | ); 107 | } 108 | 109 | if (isset($repoData['repositoryUrl'])) { 110 | $data['repositoryUrl'] = $repoData['repositoryUrl']; 111 | } 112 | if (isset($repoData['categoryUrl'])) { 113 | $data['categoryUrl'] = $repoData['categoryUrl']; 114 | } 115 | 116 | $ret = json_encode($data, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES); 117 | 118 | Header("Content-Type: application/json; charset=utf-8"); 119 | print $ret; 120 | 121 | if ($cacheDir) { 122 | @mkdir(dirname("{$cacheDir}/{$fullRepoId}")); 123 | file_put_contents("{$cacheDir}/{$fullRepoId}.json", $ret); 124 | touch("{$cacheDir}/{$fullRepoId}.json", $ts); 125 | } 126 | -------------------------------------------------------------------------------- /src/Browser.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require('events') 2 | const queryString = require('query-string') 3 | 4 | const domSort = require('./domSort') 5 | 6 | module.exports = class Browser extends EventEmitter { 7 | constructor (id, dom) { 8 | super() 9 | 10 | this.id = id 11 | this.dom = dom 12 | this.history = [] 13 | } 14 | 15 | buildPage (parameters) { 16 | this.clear() 17 | 18 | hooks.call('browser-' + this.id, this, parameters) 19 | this.emit('buildPage', parameters) 20 | this.parameters = parameters 21 | 22 | domSort(this.dom) 23 | } 24 | 25 | clear () { 26 | while (this.dom.lastChild) { 27 | this.dom.removeChild(this.dom.lastChild) 28 | } 29 | } 30 | 31 | catchLinks () { 32 | const links = this.dom.getElementsByTagName('a') 33 | Array.from(links).forEach(link => { 34 | const href = link.getAttribute('href') 35 | 36 | if (href && href.substr(0, this.id.length + 2) === '#' + this.id + '?') { 37 | link.onclick = () => { 38 | this.history.push(this.parameters) 39 | 40 | const parameters = queryString.parse(href.substr(this.id.length + 2)) 41 | this.buildPage(parameters) 42 | 43 | return false 44 | } 45 | } 46 | }) 47 | } 48 | 49 | close () { 50 | this.clear() 51 | this.emit('close') 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/CategoryIndex.js: -------------------------------------------------------------------------------- 1 | /* global alert */ 2 | var async = require('async') 3 | var OpenStreetBrowserLoader = require('./OpenStreetBrowserLoader') 4 | var CategoryBase = require('./CategoryBase') 5 | 6 | CategoryIndex.prototype = Object.create(CategoryBase.prototype) 7 | CategoryIndex.prototype.constructor = CategoryIndex 8 | function CategoryIndex (options, data, repository) { 9 | CategoryBase.call(this, options, data, repository) 10 | 11 | this.childrenDoms = {} 12 | this.childrenCategories = null 13 | 14 | this._loadChildrenCategories((err) => { 15 | if (err) { 16 | console.log('Category "' + this.id + '": error loading child categories:', err) 17 | } 18 | }) 19 | } 20 | 21 | CategoryIndex.prototype.open = function () { 22 | if (this.isOpen) { 23 | return 24 | } 25 | 26 | CategoryBase.prototype.open.call(this) 27 | 28 | if (this.childrenCategories !== null) { 29 | this.isOpen = true 30 | } 31 | } 32 | 33 | CategoryIndex.prototype.recalc = function () { 34 | for (var k in this.childrenCategories) { 35 | if (this.childrenCategories[k]) { 36 | this.childrenCategories[k].recalc() 37 | } 38 | } 39 | } 40 | 41 | CategoryIndex.prototype._loadChildrenCategories = function (callback) { 42 | this.childrenCategories = {} 43 | 44 | async.forEach(this.data.subCategories, 45 | function (data, callback) { 46 | var childDom = document.createElement('div') 47 | childDom.className = 'categoryWrapper' 48 | this.domContent.appendChild(childDom) 49 | this.childrenDoms[data.id] = childDom 50 | 51 | this.childrenCategories[data.id] = null 52 | 53 | if ('type' in data) { 54 | OpenStreetBrowserLoader.getCategoryFromData(data.id, this.options, data, this._loadChildCategory.bind(this, data.id, callback)) 55 | } else { 56 | OpenStreetBrowserLoader.getCategory(data.id, this.options, this._loadChildCategory.bind(this, data.id, callback)) 57 | } 58 | }.bind(this), 59 | function (err) { 60 | if (callback) { 61 | callback(err) 62 | } 63 | } 64 | ) 65 | } 66 | 67 | CategoryIndex.prototype._loadChildCategory = function (id, callback, err, category) { 68 | if (err) { 69 | return callback(err) 70 | } 71 | 72 | this.childrenCategories[id] = category 73 | 74 | category.setParent(this) 75 | category.setParentDom(this.childrenDoms[id]) 76 | 77 | callback(err, category) 78 | } 79 | 80 | CategoryIndex.prototype.close = function () { 81 | if (!this.isOpen) { 82 | return 83 | } 84 | 85 | CategoryBase.prototype.close.call(this) 86 | 87 | for (var k in this.childrenCategories) { 88 | if (this.childrenCategories[k]) { 89 | this.childrenCategories[k].close() 90 | } 91 | } 92 | } 93 | 94 | CategoryIndex.prototype.toggleCategory = function (id) { 95 | OpenStreetBrowserLoader.getCategory(id, function (err, category) { 96 | if (err) { 97 | alert(err) 98 | return 99 | } 100 | 101 | category.setParent(this) 102 | category.setParentDom(this.childrenDoms[id]) 103 | this.childrenCategories[id] = category 104 | 105 | category.toggle() 106 | }.bind(this)) 107 | } 108 | 109 | CategoryIndex.prototype.allMapFeatures = function (callback) { 110 | let result = [] 111 | 112 | async.each(this.childrenCategories, 113 | (category, done) => category.allMapFeatures( 114 | (err, data) => { 115 | if (err) { 116 | return done(err) 117 | } 118 | 119 | result = result.concat(data) 120 | 121 | global.setTimeout(done, 0) 122 | } 123 | ), 124 | (err) => callback(err, result) 125 | ) 126 | } 127 | 128 | OpenStreetBrowserLoader.registerType('index', CategoryIndex) 129 | module.exports = CategoryIndex 130 | -------------------------------------------------------------------------------- /src/ExportGeoJSON.js: -------------------------------------------------------------------------------- 1 | class ExportGeoJSON { 2 | constructor (conf) { 3 | this.conf = conf 4 | } 5 | 6 | each (ob, callback) { 7 | ob.object.exportGeoJSON(this.conf, callback) 8 | } 9 | 10 | finishOne (object) { 11 | return { 12 | content: JSON.stringify(object, null, ' '), 13 | fileType: 'application/json', 14 | extension: 'geojson' 15 | } 16 | } 17 | 18 | finish (list) { 19 | if (!this.conf.singleFeature) { 20 | list = { 21 | type: 'FeatureCollection', 22 | features: list 23 | } 24 | } 25 | 26 | return { 27 | content: JSON.stringify(list, null, ' '), 28 | fileType: 'application/json', 29 | extension: 'geojson' 30 | } 31 | } 32 | } 33 | 34 | module.exports = ExportGeoJSON 35 | -------------------------------------------------------------------------------- /src/ExportOSMJSON.js: -------------------------------------------------------------------------------- 1 | class ExportOSMXML { 2 | constructor (conf) { 3 | this.conf = conf 4 | this.elements = {} 5 | } 6 | 7 | each (ob, callback) { 8 | ob.object.exportOSMJSON(this.conf, this.elements, callback) 9 | } 10 | 11 | finish (list) { 12 | return { 13 | content: JSON.stringify({ 14 | version: '0.6', 15 | generator: 'OpenStreetBrowser', 16 | elements: Object.values(this.elements) 17 | }, null, ' '), 18 | fileType: 'application/json', 19 | extension: 'osm.json' 20 | } 21 | } 22 | } 23 | 24 | module.exports = ExportOSMXML 25 | -------------------------------------------------------------------------------- /src/ExportOSMXML.js: -------------------------------------------------------------------------------- 1 | class ExportOSMXML { 2 | constructor (conf) { 3 | this.conf = conf 4 | this.parentNode = document.createElement('osm') 5 | } 6 | 7 | each (ob, callback) { 8 | ob.object.exportOSMXML(this.conf, this.parentNode, callback) 9 | } 10 | 11 | finish (list) { 12 | return { 13 | content: '' + this.parentNode.innerHTML + '', 14 | fileType: 'application/xml', 15 | extension: 'osm.xml' 16 | } 17 | } 18 | } 19 | 20 | module.exports = ExportOSMXML 21 | -------------------------------------------------------------------------------- /src/GeoInfo.css: -------------------------------------------------------------------------------- 1 | .geo-info > div { 2 | display: flex; 3 | position: relative; 4 | align-items: end; 5 | margin-top: 0.5em; 6 | } 7 | .geo-info > div:first-of-type { 8 | margin-top: 0; 9 | } 10 | .geo-info > div.empty { 11 | margin-top: 0; 12 | display: none; 13 | } 14 | .geo-info > div::before { 15 | width: 16px; 16 | height: 16px; 17 | content: ' '; 18 | margin-right: 0.5em; 19 | } 20 | 21 | .geo-info > .bbox-nw-corner::before { 22 | background: url("../img/geo-info-bbox-nw.svg"); 23 | } 24 | .geo-info > .bbox-center::before { 25 | background: url("../img/geo-info-bbox-center.svg"); 26 | } 27 | .geo-info > .bbox-se-corner::before { 28 | background: url("../img/geo-info-bbox-se.svg"); 29 | } 30 | .geo-info > .object-shape::before { 31 | background: url("../img/geo-info-object-shape.svg"); 32 | } 33 | .geo-info > .object-nw-corner::before { 34 | background: url("../img/geo-info-object-nw.svg"); 35 | } 36 | .geo-info > .object-center::before { 37 | background: url("../img/geo-info-object-center.svg"); 38 | } 39 | .geo-info > .object-se-corner::before { 40 | background: url("../img/geo-info-object-se.svg"); 41 | } 42 | .geo-info > .zoom::before, 43 | .geo-info > .location::before, 44 | .geo-info > .mouse::before { 45 | font-family: "Font Awesome 5 Free"; 46 | font-weight: 900; 47 | font-size: 1.25em; 48 | text-align: center; 49 | } 50 | .geo-info > .zoom::before { 51 | content: "\f689"; 52 | } 53 | .geo-info > .location::before { 54 | content: "\f3c5"; 55 | } 56 | .geo-info > .mouse::before { 57 | content: "\f245"; 58 | } 59 | -------------------------------------------------------------------------------- /src/ImageLoader.php: -------------------------------------------------------------------------------- 1 | "_"))); 7 | 8 | if (isset($param['continue'])) { 9 | $wm_url .= "&filefrom=" . urlencode(strtr($param['continue'], array(" " => "_"))); 10 | } 11 | 12 | $content = file_get_contents($wm_url); 13 | 14 | $dom = new DOMDocument(); 15 | $dom->loadHTML($content); 16 | 17 | $uls = $dom->getElementsByTagName('ul');//interlanguage-link interwiki-bar'); 18 | for ($i = 0; $i < $uls->length; $i++) { 19 | $ul = $uls->item($i); 20 | 21 | if ($ul->getAttribute('class') === 'gallery mw-gallery-traditional') { 22 | $imgs = $ul->getElementsByTagName('img'); 23 | 24 | for ($j = 0; $j < $imgs->length; $j++) { 25 | $item = $imgs->item($j); 26 | $srcPath = explode('/', $item->getAttribute('src')); 27 | 28 | if (sizeof($srcPath) < 2) { 29 | continue; 30 | } 31 | 32 | $id = urldecode($srcPath[sizeof($srcPath) - 2]); 33 | 34 | $ret[] = $id; 35 | $retData[] = array( 36 | 'id' => $id, 37 | 'width' => $item->getAttribute('data-file-width'), 38 | 'height' => $item->getAttribute('data-file-height'), 39 | ); 40 | } 41 | } 42 | } 43 | 44 | $continue = false; 45 | $as = $dom->getElementsByTagName('a'); 46 | for ($i = 0; $i < $as->length; $i++) { 47 | $a = $as->item($i); 48 | 49 | if (preg_match("/^\/w\/index.php\?title=(.*)&filefrom=([^#]+)#mw-category-media$/", $a->getAttribute('href'), $m)) { 50 | $continue = $m[2]; 51 | } 52 | } 53 | 54 | return array( 55 | 'images' => $ret, // deprecated as of 2017-09-27 56 | 'imageData' => $retData, 57 | 'continue' => $continue, 58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /src/PluginGeoLocate.js: -------------------------------------------------------------------------------- 1 | register_hook('init', function () { 2 | // Geo location 3 | L.control.locate({ 4 | locateOptions: { 5 | enableHighAccuracy: true 6 | }, 7 | flyTo: true, 8 | keepCurrentZoomLevel: true, 9 | initialZoomLevel: 17, 10 | drawCircle: true, 11 | circleStyle: { 12 | weight: 0, 13 | fillColor: '#ff0000' 14 | }, 15 | markerStyle: { 16 | color: '#ff0000', 17 | fillColor: '#ff0000' 18 | }, 19 | compassStyle: { 20 | color: '#ff0000', 21 | fillColor: '#ff0000' 22 | }, 23 | showCompass: true, 24 | showPopup: false 25 | }).addTo(map) 26 | }) 27 | -------------------------------------------------------------------------------- /src/PluginMeasure.js: -------------------------------------------------------------------------------- 1 | const formatUnits = require('./formatUnits') 2 | 3 | let control 4 | let unitSystems = { 5 | si: 'metres', 6 | imp: 'landmiles', 7 | nautical: 'nauticalmiles', 8 | m: 'metres' 9 | } 10 | 11 | register_hook('init', function () { 12 | // Measurement plugin 13 | if (L.control.polylineMeasure) { 14 | control = L.control.polylineMeasure({ 15 | unit: unitSystems[formatUnits.settings.system] 16 | }).addTo(map) 17 | } 18 | }) 19 | 20 | register_hook('format-units-refresh', () => { 21 | if (control) { 22 | control.options.unit = unitSystems[formatUnits.settings.system] 23 | } 24 | }) 25 | -------------------------------------------------------------------------------- /src/Repository.js: -------------------------------------------------------------------------------- 1 | module.exports = class Repository { 2 | constructor (id, data) { 3 | this.id = id 4 | this.isLoaded = false 5 | 6 | if (data) { 7 | this.data = data 8 | this.lang = this.data.lang || {} 9 | this.loadCallbacks = null 10 | } 11 | } 12 | 13 | file_get_contents (fileName, options, callback) { 14 | let param = [] 15 | param.push('repo=' + encodeURIComponent(this.id)) 16 | param.push('file=' + encodeURIComponent(fileName)) 17 | param.push(config.categoriesRev) 18 | param = param.length ? '?' + param.join('&') : '' 19 | 20 | fetch('repo.php' + param) 21 | .then(res => res.text()) 22 | .then(data => { 23 | global.setTimeout(() => { 24 | callback(null, data) 25 | }, 0) 26 | }) 27 | .catch(err => { 28 | global.setTimeout(() => { 29 | callback(err) 30 | }, 0) 31 | }) 32 | } 33 | 34 | load (callback) { 35 | if (this.loadCallbacks) { 36 | return this.loadCallbacks.push(callback) 37 | } 38 | 39 | this.loadCallbacks = [callback] 40 | 41 | var param = [] 42 | 43 | param.push('repo=' + encodeURIComponent(this.id)) 44 | param.push('lang=' + encodeURIComponent(ui_lang)) 45 | param.push(config.categoriesRev) 46 | param = param.length ? '?' + param.join('&') : '' 47 | 48 | fetch('repo.php' + param) 49 | .then(res => res.json()) 50 | .then(data => { 51 | this.data = data 52 | this.lang = this.data.lang || {} 53 | this.err = null 54 | 55 | global.setTimeout(() => { 56 | const cbs = this.loadCallbacks 57 | this.loadCallbacks = null 58 | cbs.forEach(cb => cb(null)) 59 | }, 0) 60 | }) 61 | .catch(err => { 62 | this.err = err 63 | global.setTimeout(() => { 64 | const cbs = this.loadCallbacks 65 | this.loadCallbacks = null 66 | cbs.forEach(cb => cb(err)) 67 | }, 0) 68 | }) 69 | } 70 | 71 | clearCache () { 72 | this.data = null 73 | } 74 | 75 | getCategory (id, options, callback) { 76 | if (!(id in this.data.categories)) { 77 | return callback(new Error('Repository ' + this.id + ': Category "' + id + '" not defined'), null) 78 | } 79 | 80 | callback(null, this.data.categories[id]) 81 | } 82 | 83 | getTemplate (id, options, callback) { 84 | if (!(id in this.data.templates)) { 85 | return callback(new Error('Repository ' + this.id + ': Template "' + id + '" not defined'), null) 86 | } 87 | 88 | callback(null, this.data.templates[id]) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/RepositoryBase.php: -------------------------------------------------------------------------------- 1 | def = $def; 5 | $this->path = $def['path']; 6 | } 7 | 8 | function timestamp () { 9 | return null; 10 | } 11 | 12 | function isEmpty () { 13 | return false; 14 | } 15 | 16 | function info () { 17 | $ret = array(); 18 | 19 | foreach (array('name') as $k) { 20 | if (array_key_exists($k, $this->def)) { 21 | $ret[$k] = $this->def[$k]; 22 | } 23 | } 24 | 25 | $ret['timestamp'] = Date(DATE_ISO8601, $this->timestamp()); 26 | 27 | return $ret; 28 | } 29 | 30 | function data ($options) { 31 | $data = array( 32 | 'categories' => array(), 33 | 'templates' => array(), 34 | 'timestamp' => Date(DATE_ISO8601, $this->timestamp()), 35 | 'lang' => array(), 36 | ); 37 | 38 | return $data; 39 | } 40 | 41 | function unfoldCategories (&$data, &$categories=null) { 42 | if ($categories === null) { 43 | $categories = &$data['categories']; 44 | } 45 | 46 | foreach ($categories as $id => $_category) { 47 | if (preg_match('/^[0-9]+$/', $id)) { 48 | $id = $_category['id']; 49 | 50 | if (!array_key_exists($id, $data['categories'])) { 51 | $category = $_category; 52 | $data['categories'][$id] = $category; 53 | } 54 | } else { 55 | $category = &$categories[$id]; 56 | } 57 | 58 | if (is_array($category) && $category['type'] === 'index') { 59 | foreach ($category['subCategories'] as $subIndex => $_subCategory) { 60 | $subCategory = &$category['subCategories'][$subIndex]; 61 | 62 | if (array_key_exists('type', $subCategory)) { 63 | $data['categories'][$subCategory['id']] = $subCategory; 64 | if (array_key_exists('subCategories', $subCategory)) { 65 | $this->unfoldCategories($data, $subCategory['subCategories']); 66 | $data['categories'][$subCategory['id']]['subCategories'] = array_map(function ($c) { 67 | return array('id' => $c['id']); 68 | }, $subCategory['subCategories']); 69 | } 70 | 71 | $category['subCategories'][$subIndex] = array( 72 | 'id' => $subCategory['id'] 73 | ); 74 | } 75 | } 76 | } 77 | } 78 | } 79 | 80 | function updateLang (&$data, $options) { 81 | $lang = array_key_exists('lang', $options) ? $options['lang'] : 'en'; 82 | 83 | $this->unfoldCategories($data); 84 | 85 | if (!is_array($data['lang'])) { 86 | $data['lang'] = array(); 87 | } 88 | 89 | foreach ($data['categories'] as $id => $category) { 90 | $name = null; 91 | if (array_key_exists("category:{$id}", $data['lang'])) { 92 | $name = $data['lang']["category:{$id}"]; 93 | 94 | if ($name !== '' && $name !== null) { 95 | $data['categories'][$id]['name'] = array( 96 | $lang => $data['lang']["category:{$id}"], 97 | ); 98 | } 99 | } 100 | elseif (is_array($category) && array_key_exists('name', $category)) { 101 | if (is_string($category['name'])) { 102 | $name = $category['name']; 103 | } 104 | elseif (array_key_exists($lang, $category['name'])) { 105 | $name = $category['name'][$lang]; 106 | } 107 | elseif (array_key_exists('en', $category['name'])) { 108 | $name = $category['name']['en']; 109 | } 110 | elseif (sizeof($category['name'])) { 111 | $name = $category['name'][array_keys($category['name'])[0]]; 112 | } 113 | 114 | $data['lang']["category:{$id}"] = $name; 115 | 116 | $data['categories'][$id]['name'] = array($lang => $name); 117 | } 118 | 119 | } 120 | } 121 | 122 | function isCategory ($data) { 123 | if (!array_key_exists('type', $data)) { 124 | return true; 125 | } 126 | 127 | return in_array($data['type'], array('index', 'overpass')); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/RepositoryDir.php: -------------------------------------------------------------------------------- 1 | path); 6 | while ($f = readdir($d)) { 7 | $t = filemtime("{$this->path}/{$f}"); 8 | if ($t > $ts) { 9 | $ts = $t; 10 | } 11 | } 12 | closedir($d); 13 | 14 | return $ts; 15 | } 16 | 17 | function data ($options) { 18 | $data = parent::data($options); 19 | 20 | $lang = array_key_exists('lang', $options) ? $options['lang'] : 'en'; 21 | 22 | if (file_exists("{$this->path}/lang/{$lang}.json")) { 23 | $data['lang'] = json_decode(file_get_contents("{$this->path}/lang/en.json"), true); 24 | $lang = json_decode(file_get_contents("{$this->path}/lang/{$options['lang']}.json"), true); 25 | foreach ($lang as $k => $v) { 26 | if ($v !== null && $v !== '') { 27 | $data['lang'][$k] = $v; 28 | } 29 | } 30 | } 31 | 32 | $d = opendir($this->path); 33 | while ($f = readdir($d)) { 34 | if (preg_match("/^([0-9a-zA-Z_\-]+)\.json$/", $f, $m) && $f !== 'package.json') { 35 | $d1 = json_decode(file_get_contents("{$this->path}/{$f}"), true); 36 | $d1['format'] = 'json'; 37 | $d1['fileName'] = $f; 38 | 39 | if (!$this->isCategory($d1)) { 40 | continue; 41 | } 42 | 43 | $data['categories'][$m[1]] = jsonMultilineStringsJoin($d1, array('exclude' => array(array('const'), array('filter')))); 44 | } 45 | 46 | if (preg_match("/^([0-9a-zA-Z_\-]+)\.yaml$/", $f, $m)) { 47 | $d1 = yaml_parse(file_get_contents("{$this->path}/{$f}")); 48 | $d1['format'] = 'yaml'; 49 | $d1['fileName'] = $f; 50 | 51 | if (!$this->isCategory($d1)) { 52 | continue; 53 | } 54 | 55 | $data['categories'][$m[1]] = $d1; 56 | } 57 | 58 | if (preg_match("/^(detailsBody|popupBody).html$/", $f, $m)) { 59 | $data['templates'][$m[1]] = file_get_contents("{$this->path}/{$f}"); 60 | } 61 | } 62 | closedir($d); 63 | 64 | return $data; 65 | } 66 | 67 | function access ($file) { 68 | return (substr($file, 0, 1) !== '.' && !preg_match('/\/\./', $file)); 69 | } 70 | 71 | function scandir($path="") { 72 | if (substr($path, 0, 1) === '.' || preg_match("/\/\./", $path)) { 73 | return false; 74 | } 75 | 76 | if (!$this->access($path)) { 77 | return false; 78 | } 79 | 80 | return scandir("{$this->path}/{$path}"); 81 | } 82 | 83 | function file_get_contents ($file) { 84 | if (substr($file, 0, 1) === '.' || preg_match("/\/\./", $file)) { 85 | return false; 86 | } 87 | 88 | if (!$this->access($file)) { 89 | return false; 90 | } 91 | 92 | if (!file_exists("{$this->path}/{$file}")) { 93 | return null; 94 | } 95 | 96 | return file_get_contents("{$this->path}/{$file}"); 97 | } 98 | 99 | function file_put_contents ($file, $content) { 100 | if (substr($file, 0, 1) === '.' || preg_match("/\/\./", $file)) { 101 | return false; 102 | } 103 | 104 | if (!$this->access($file)) { 105 | return false; 106 | } 107 | 108 | return file_put_contents("{$this->path}/{$file}", $content); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/Window.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require('events') 2 | 3 | module.exports = class Window extends EventEmitter { 4 | constructor (options) { 5 | super() 6 | 7 | this.visible = false 8 | this.dom = document.createElement('div') 9 | this.dom.className = 'Window' 10 | 11 | this.header = document.createElement('div') 12 | this.header.className = 'header' 13 | this.header.innerHTML = options.title 14 | this.dom.appendChild(this.header) 15 | 16 | this.closeBtn = document.createElement('div') 17 | this.closeBtn.className = 'closeBtn' 18 | this.closeBtn.title = lang('close') 19 | this.closeBtn.onclick = (e) => { 20 | this.close() 21 | e.stopImmediatePropagation() 22 | } 23 | this.header.appendChild(this.closeBtn) 24 | 25 | this.content = document.createElement('div') 26 | this.content.className = 'content' 27 | this.dom.appendChild(this.content) 28 | 29 | dragElement(this.dom) 30 | 31 | this.dom.onclick = () => { 32 | if (!this.visible) { return } 33 | 34 | const activeEl = document.activeElement 35 | 36 | if (document.body.lastElementChild !== this.dom) { 37 | document.body.appendChild(this.dom) 38 | activeEl.focus() 39 | } 40 | } 41 | } 42 | 43 | show () { 44 | this.visible = true 45 | document.body.appendChild(this.dom) 46 | this.emit('show') 47 | } 48 | 49 | close () { 50 | this.visible = false 51 | document.body.removeChild(this.dom) 52 | this.emit('close') 53 | } 54 | } 55 | 56 | // copied from https://www.w3schools.com/HOWTO/howto_js_draggable.asp 57 | // Make the DIV element draggable: 58 | function dragElement(elmnt) { 59 | var pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0; 60 | if (elmnt.firstChild) { 61 | // if present, the header is where you move the DIV from: 62 | elmnt.firstChild.onmousedown = dragMouseDown; 63 | } else { 64 | // otherwise, move the DIV from anywhere inside the DIV: 65 | elmnt.onmousedown = dragMouseDown; 66 | } 67 | 68 | function dragMouseDown(e) { 69 | e = e || window.event; 70 | e.preventDefault(); 71 | // get the mouse cursor position at startup: 72 | pos3 = e.clientX; 73 | pos4 = e.clientY; 74 | document.onmouseup = closeDragElement; 75 | // call a function whenever the cursor moves: 76 | document.onmousemove = elementDrag; 77 | } 78 | 79 | function elementDrag(e) { 80 | e = e || window.event; 81 | e.preventDefault(); 82 | // calculate the new cursor position: 83 | pos1 = pos3 - e.clientX; 84 | pos2 = pos4 - e.clientY; 85 | pos3 = e.clientX; 86 | pos4 = e.clientY; 87 | // set the element's new position: 88 | elmnt.style.top = (elmnt.offsetTop - pos2) + "px"; 89 | elmnt.style.left = (elmnt.offsetLeft - pos1) + "px"; 90 | } 91 | 92 | function closeDragElement() { 93 | // stop moving when mouse button is released: 94 | document.onmouseup = null; 95 | document.onmousemove = null; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/addCategories.css: -------------------------------------------------------------------------------- 1 | #content.addCategories > #contentAddCategories { 2 | display: block; 3 | } 4 | -------------------------------------------------------------------------------- /src/boundaries.js: -------------------------------------------------------------------------------- 1 | const turf = { 2 | booleanWithin: require('@turf/boolean-within').default 3 | } 4 | 5 | let data 6 | 7 | register_hook('init_callback', function (initState, callback) { 8 | fetch('data/boundaries.geojson') 9 | .then(req => { 10 | if (req.status === 404) { 11 | throw (new Error('data/boundaries.geojson not found, run bin/download_dependencies')) 12 | } 13 | 14 | if (!req.ok) { 15 | throw (new Error('error loading data/boundaries.geojson: ' + req.statusText)) 16 | } 17 | 18 | return req.json() 19 | }) 20 | .then(body => { 21 | data = body.features 22 | global.setTimeout(() => callback(), 0) 23 | }) 24 | .catch(err => global.setTimeout(() => callback(), 0)) 25 | }) 26 | 27 | function check (lat, lon) { 28 | // no data loaded 29 | if (!data) { return } 30 | 31 | const poi = { 32 | type: 'Point', 33 | coordinates: [ lon, lat ] 34 | } 35 | 36 | return data.filter(feature => turf.booleanWithin(poi, feature)) 37 | } 38 | 39 | OverpassLayer.twig.extendFunction('boundaries', check) 40 | 41 | register_hook('category-overpass-init', (category) => { 42 | category.layer.on('globalTwigData', (twigData) => { 43 | const center = category.layer.map.getCenter() 44 | const list = check(center.lat, center.lng) 45 | twigData.map.boundaries = list 46 | twigData.map.driving_side = 'right' 47 | 48 | if (list) { 49 | list.forEach(boundary => { 50 | if (boundary.tags.driving_side) { 51 | twigData.map.driving_side = boundary.tags.driving_side 52 | } 53 | }) 54 | } 55 | }) 56 | }) 57 | 58 | module.exports = { check } 59 | -------------------------------------------------------------------------------- /src/categories.js: -------------------------------------------------------------------------------- 1 | var queryString = require('query-string') 2 | 3 | var OpenStreetBrowserLoader = require('./OpenStreetBrowserLoader') 4 | 5 | register_hook('state-apply', function (state) { 6 | if (!('categories' in state)) { 7 | return 8 | } 9 | 10 | var list = state.categories.split(',') 11 | list.forEach(function (id) { 12 | let param 13 | 14 | let m = id.match(/^([0-9A-Z_-]+)(\[(.*)\])/i) 15 | if (m) { 16 | id = m[1] 17 | param = queryString.parse(m[3]) 18 | } 19 | 20 | OpenStreetBrowserLoader.getCategory(id, function (err, category) { 21 | if (err) { 22 | console.log("Can't load category " + id + ': ', err) 23 | return 24 | } 25 | 26 | if (category) { 27 | if (param) { 28 | category.setParam(param) 29 | } 30 | 31 | if (!category.parentDom) { 32 | category.setParentDom(document.getElementById('contentListAddCategories')) 33 | global.rootCategories[id] = category 34 | } 35 | 36 | category.open() 37 | } 38 | }) 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /src/category.css: -------------------------------------------------------------------------------- 1 | .category { 2 | position: relative; 3 | } 4 | .category > .loadingIndicator { 5 | position: absolute; 6 | right: 0; 7 | top: 0; 8 | font-size: 15px; 9 | display: none; 10 | } 11 | .category.loading > .loadingIndicator { 12 | padding-top: 2px; 13 | display: block; 14 | } 15 | 16 | .category > .loadingIndicator2 { 17 | display: none; 18 | } 19 | 20 | /* Source: http://tobiasahlin.com/spinkit/ */ 21 | .category.open.loading > .loadingIndicator2, 22 | .category.open.loading > .content > .categoryWrapper > .category-list.open > .loadingIndicator2 { 23 | text-align: left; 24 | display: block; 25 | background: #efefef; 26 | padding-left: 40px; 27 | } 28 | 29 | .category.loading > .loadingIndicator2 > div, 30 | .category.loading > .content > .categoryWrapper > .category-list.open > .loadingIndicator2 > div { 31 | width: 9px; 32 | height: 9px; 33 | margin-right: 9px; 34 | background-color: #333; 35 | 36 | border-radius: 100%; 37 | display: inline-block; 38 | -webkit-animation: sk-bouncedelay 1.4s infinite ease-in-out both; 39 | animation: sk-bouncedelay 1.4s infinite ease-in-out both; 40 | } 41 | 42 | .category.loading > .loadingIndicator2 .bounce1 { 43 | -webkit-animation-delay: -0.32s; 44 | animation-delay: -0.32s; 45 | } 46 | 47 | .category.loading > .loadingIndicator2 .bounce2 { 48 | -webkit-animation-delay: -0.16s; 49 | animation-delay: -0.16s; 50 | } 51 | 52 | @-webkit-keyframes sk-bouncedelay { 53 | 0%, 80%, 100% { -webkit-transform: scale(0) } 54 | 40% { -webkit-transform: scale(1.0) } 55 | } 56 | 57 | @keyframes sk-bouncedelay { 58 | 0%, 80%, 100% { 59 | -webkit-transform: scale(0); 60 | transform: scale(0); 61 | } 62 | 40% { 63 | -webkit-transform: scale(1.0); 64 | transform: scale(1.0); 65 | } 66 | } 67 | 68 | .category header { 69 | padding-top: 3px; 70 | border-bottom: 1px dotted #999; 71 | user-select: none; 72 | font-size: 15px; 73 | } 74 | .category header > span.repoId { 75 | margin-left: 0.2em; 76 | font-size: 10px; 77 | line-height: 10px; 78 | color: #7f7f7f; 79 | } 80 | .category header > a.reload { 81 | float: right; 82 | } 83 | .category > .content, 84 | .category > .tools, 85 | .category > .status { 86 | display: none; 87 | } 88 | .category.open > .content, 89 | .category.open > .tools, 90 | .category.open > .status { 91 | display: block; 92 | } 93 | .category .info { 94 | position: relative; 95 | } 96 | .category .info > .closeButton { 97 | position: absolute; 98 | top: 0; 99 | right: 0; 100 | text-decoration: none; 101 | font-size: 12px; 102 | } 103 | .category > .status, 104 | .category > .content > ul.overpass-layer-list { 105 | padding-top: 3px; 106 | background: #efefef; 107 | } 108 | .body h4 { 109 | margin-bottom: 0; 110 | } 111 | .body ul.overpass-layer-list { 112 | padding-left: 40px; 113 | } 114 | 115 | .category { 116 | margin-left: 1em; 117 | } 118 | #contentListBaseCategory { 119 | margin-left: -2em; 120 | } 121 | #contentListBaseCategory > .category > header { 122 | display: none; 123 | } 124 | #contentListAddCategories { 125 | margin-left: -1em; 126 | } 127 | .info > table > tr > td:first-of-type, 128 | .info > table > tbody > tr > td:first-of-type { 129 | position: relative; 130 | } 131 | .info .sign { 132 | text-align: center; 133 | position: absolute; 134 | top: 3px; 135 | font-size: 15px; 136 | left: 0; 137 | right: 0; 138 | z-index: 1; 139 | display: inline-block; 140 | } 141 | .leaflet-popup-content { 142 | min-width: 300px; 143 | max-height: 300px; 144 | overflow: auto; 145 | word-wrap: break-word; 146 | } 147 | .leaflet-popup-content:after { 148 | content: ' '; 149 | clear: both; 150 | display: table; 151 | } 152 | .overpass-layer-icon div.sign { 153 | font-size: 15px; 154 | } 155 | .info .details { 156 | display: none; 157 | } 158 | .info .infoShowDetails .details { 159 | display: initial; 160 | } 161 | .info .infoShowDetails .summary { 162 | display: none; 163 | } 164 | 165 | dl > dd { 166 | position: relative; 167 | } 168 | .tag2link { 169 | position: absolute; 170 | top: 1em; 171 | left: 0; 172 | border: 1px solid black; 173 | padding: 0.25em; 174 | background: white; 175 | z-index: 1; 176 | } 177 | .tag2link > .closeButton { 178 | float: right; 179 | } 180 | .tag2link > ul { 181 | padding-left: 0; 182 | margin: 0; 183 | } 184 | .tag2link > ul > li { 185 | list-style: none; 186 | } 187 | -------------------------------------------------------------------------------- /src/chunkSplit.js: -------------------------------------------------------------------------------- 1 | module.exports = function chunkSplit (data, size=1000) { 2 | let result = [] 3 | 4 | for (let i = 0; i < data.length; i += size) { 5 | result.push(data.slice(i, i + size)) 6 | } 7 | 8 | return result 9 | } 10 | -------------------------------------------------------------------------------- /src/customCategory.php: -------------------------------------------------------------------------------- 1 | prepare("select content from customCategory where id=:id"); 10 | $stmt->bindValue(':id', $id, PDO::PARAM_STR); 11 | if ($stmt->execute()) { 12 | $row = $stmt->fetch(PDO::FETCH_ASSOC); 13 | $result = $row['content']; 14 | $stmt->closeCursor(); 15 | 16 | return $result; 17 | } 18 | } 19 | 20 | function recordAccess ($id) { 21 | global $db; 22 | 23 | if (!isset($_SESSION['customCategoryAccess'])) { 24 | $_SESSION['customCategoryAccess'] = []; 25 | } 26 | 27 | // update access per session only once a day 28 | if (array_key_exists($id, $_SESSION['customCategoryAccess']) && $_SESSION['customCategoryAccess'][$id] > time() - 86400) { 29 | return; 30 | } 31 | 32 | $_SESSION['customCategoryAccess'][$id] = time(); 33 | 34 | $stmt = $db->prepare("insert into customCategoryAccess (id) values (:id)"); 35 | $stmt->bindValue(':id', $id); 36 | $stmt->execute(); 37 | } 38 | 39 | function saveCategory ($content) { 40 | global $db; 41 | 42 | $id = md5($content); 43 | 44 | switch ($db->getAttribute(PDO::ATTR_DRIVER_NAME)) { 45 | case 'mysql': 46 | $sqlAction = "insert ignore"; 47 | break; 48 | case 'sqlite': 49 | default: 50 | $sqlAction = "insert or ignore"; 51 | } 52 | 53 | $stmt = $db->prepare("{$sqlAction} into customCategory (id, content) values (:id, :content)"); 54 | $stmt->bindValue(':id', $id, PDO::PARAM_STR); 55 | $stmt->bindValue(':content', $content, PDO::PARAM_STR); 56 | $result = $stmt->execute(); 57 | 58 | return $id; 59 | } 60 | 61 | function list ($options=[]) { 62 | global $db; 63 | 64 | // $sqlCalcAge: the age of the access in days 65 | switch ($db->getAttribute(PDO::ATTR_DRIVER_NAME)) { 66 | case 'mysql': 67 | $sqlCalcAge = "datediff(now(), ts)"; 68 | break; 69 | case 'sqlite': 70 | $sqlCalcAge = "julianday('now')-julianday(ts)"; 71 | } 72 | 73 | // the popularity column counts every acess with declining value over time, 74 | // it halves every year. 75 | $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"); 76 | $stmt->execute(); 77 | $data = $stmt->fetchAll(PDO::FETCH_ASSOC); 78 | $data = array_map(function ($d) { 79 | $d['popularity'] = (float)$d['popularity']; 80 | $d['accessCount'] = (int)$d['accessCount']; 81 | 82 | $content = yaml_parse($d['content']); 83 | if ($content && is_array($content) && array_key_exists('name', $content)) { 84 | $d['name'] = lang($content['name']); 85 | } 86 | else { 87 | $d['name'] = 'Custom ' . substr($d['id'], 0, 6); 88 | } 89 | 90 | unset($d['content']); 91 | return $d; 92 | }, $data); 93 | 94 | $stmt->closeCursor(); 95 | return $data; 96 | } 97 | } 98 | 99 | $customCategoryRepository = new CustomCategoryRepository(); 100 | -------------------------------------------------------------------------------- /src/database.php: -------------------------------------------------------------------------------- 1 | { 33 | let o1 = child1.hasAttribute('data-order') ? parseFloat(child1.getAttribute('data-order')) : 0 34 | let o2 = child2.hasAttribute('data-order') ? parseFloat(child2.getAttribute('data-order')) : 0 35 | return o1 - o2 36 | }) 37 | children.forEach(child => dom.appendChild(child)) 38 | } 39 | -------------------------------------------------------------------------------- /src/domSort.js: -------------------------------------------------------------------------------- 1 | module.exports = function (dom, attribute='weight') { 2 | const list = Array.from(dom.children).sort( 3 | (a, b) => (a.getAttribute(attribute) || 0) - (b.getAttribute(attribute) || 0) 4 | ) 5 | 6 | list.forEach(el => dom.appendChild(el)) 7 | } 8 | -------------------------------------------------------------------------------- /src/editLink.js: -------------------------------------------------------------------------------- 1 | function editLinkRemote (type, osm_id) { 2 | let id = type.substr(0, 1) + osm_id 3 | 4 | global.overpassFrontend.get( 5 | id, 6 | { 7 | properties: global.overpassFrontend.OVERPASS_BBOX 8 | }, 9 | (err, object) => { 10 | if (err) { 11 | return console.error(err) 12 | } 13 | 14 | let bounds = object.bounds 15 | 16 | let xhr = new XMLHttpRequest() 17 | let url = 'http://127.0.0.1:8111/load_and_zoom' + 18 | '?left=' + (bounds.minlon - 0.0001).toFixed(5) + 19 | '&right=' + (bounds.maxlon + 0.0001).toFixed(5) + 20 | '&top=' + (bounds.maxlat + 0.0001).toFixed(5) + 21 | '&bottom=' + (bounds.minlat - 0.0001).toFixed(5) + 22 | '&select=' + type + osm_id 23 | 24 | xhr.open('get', url, true) 25 | xhr.responseType = 'text' 26 | xhr.send() 27 | }, 28 | (err) => { 29 | if (err) { 30 | alert(err) 31 | } 32 | } 33 | ) 34 | } 35 | 36 | window.editLink = function (type, osm_id) { 37 | switch (global.options.editor) { 38 | case 'remote': 39 | editLinkRemote(type, osm_id) 40 | break 41 | case 'id': 42 | default: 43 | let url = global.config.urlOpenStreetMap + '/edit?editor=id&' + type + '=' + osm_id 44 | window.open(url) 45 | } 46 | 47 | return false 48 | } 49 | 50 | module.exports = function (object) { 51 | return '' + lang('edit') + '' 52 | } 53 | 54 | register_hook('options_orig_data', function (data) { 55 | data.editor = 'id' 56 | }) 57 | 58 | register_hook('options_form', function (def) { 59 | def.editor = { 60 | 'name': lang('options:chooseEditor'), 61 | 'type': 'select', 62 | 'values': { 63 | 'id': lang('editor:id'), 64 | 'remote': { 65 | name: lang('editor:remote'), 66 | desc: lang('editor:remote:help') 67 | } 68 | }, 69 | 'default': 'id', 70 | 'weight': 5 71 | } 72 | }) 73 | -------------------------------------------------------------------------------- /src/export.js: -------------------------------------------------------------------------------- 1 | require('./twigFunctions') 2 | require('./tagTranslations') 3 | require('./markers') 4 | require('./category.css') 5 | require('./CategoryOverpassFilter') 6 | require('./CategoryOverpassConfig') 7 | 8 | module.exports = { 9 | CategoryIndex: require('./CategoryIndex'), 10 | CategoryOverpass: require('./CategoryOverpass') 11 | } 12 | -------------------------------------------------------------------------------- /src/fullscreen.js: -------------------------------------------------------------------------------- 1 | var FullscreenControl = L.Control.extend({ 2 | options: { 3 | position: 'topleft' 4 | // control position - allowed: 'topleft', 'topright', 'bottomleft', 'bottomright' 5 | }, 6 | onAdd: function (map) { 7 | var container = L.DomUtil.create('div', 'leaflet-bar leaflet-control-fullscreen') 8 | container.innerHTML = "" 9 | container.title = lang('toggle_fullscreen') 10 | 11 | const mapElement = document.body.querySelector('#map') 12 | mapElement.addEventListener('fullscreenchange', () => { 13 | if (document.fullscreenElement !== mapElement) { 14 | call_hooks('fullscreen-deactivate') 15 | document.body.classList.remove('fullscreen') 16 | } 17 | }) 18 | 19 | container.onclick = function () { 20 | document.body.classList.toggle('fullscreen') 21 | 22 | if (options.fullscreenMode !== 'window') { 23 | document.body.classList.contains('fullscreen') ? 24 | mapElement.requestFullscreen() : 25 | document.exitFullscreen() 26 | } 27 | 28 | map.invalidateSize() 29 | 30 | call_hooks('fullscreen-' + (document.body.classList.contains('fullscreen') ? 'activate' : 'deactivate')) 31 | 32 | return false 33 | } 34 | 35 | return container 36 | } 37 | }) 38 | 39 | register_hook('init', function (callback) { 40 | map.addControl(new FullscreenControl()) 41 | }) 42 | 43 | register_hook('show', function (url, options) { 44 | if (options.showDetails) { 45 | document.body.classList.remove('fullscreen') 46 | } 47 | }) 48 | 49 | register_hook('options_form', (def) => { 50 | def.fullscreenMode = { 51 | name: lang('options:fullscreenMode'), 52 | type: 'select', 53 | placeholder: lang('default'), 54 | values: { 55 | 'screen': lang('options:fullscreenMode:screen'), 56 | 'window': lang('options:fullscreenMode:window'), 57 | } 58 | } 59 | }) 60 | -------------------------------------------------------------------------------- /src/getPathFromJSON.js: -------------------------------------------------------------------------------- 1 | module.exports = function getPathFromJSON (path, json) { 2 | if (typeof path === 'string') { 3 | path = path.split(/\./) 4 | } 5 | 6 | if (path.length === 0) { 7 | return json 8 | } 9 | 10 | return getPathFromJSON(path.slice(1), json[path[0]]) 11 | } 12 | -------------------------------------------------------------------------------- /src/httpGet.js: -------------------------------------------------------------------------------- 1 | function httpGet (url, options, callback) { 2 | let corsRetry = true 3 | var xhr 4 | 5 | function readyStateChange () { 6 | if (xhr.readyState === 4) { 7 | if (corsRetry && xhr.status === 0) { 8 | corsRetry = false 9 | return viaServer() 10 | } 11 | 12 | if (xhr.status === 200) { 13 | callback(null, { body: xhr.responseText }) 14 | } else { 15 | callback(xhr.responseText) 16 | } 17 | } 18 | } 19 | 20 | function direct () { 21 | xhr = new XMLHttpRequest() 22 | xhr.open('get', url, true) 23 | xhr.responseType = 'text' 24 | xhr.onreadystatechange = readyStateChange 25 | xhr.send() 26 | } 27 | 28 | function viaServer () { 29 | xhr = new XMLHttpRequest() 30 | xhr.open('get', 'httpGet.php?url=' + encodeURIComponent(url), true) 31 | xhr.responseType = 'text' 32 | xhr.onreadystatechange = readyStateChange 33 | xhr.send() 34 | } 35 | 36 | if (options.forceServerLoad) { 37 | viaServer() 38 | } else { 39 | direct() 40 | } 41 | } 42 | 43 | module.exports = httpGet 44 | -------------------------------------------------------------------------------- /src/ip-location.php: -------------------------------------------------------------------------------- 1 | city($_SERVER['REMOTE_ADDR']); 20 | 21 | $config['defaultView']['lat'] = $record->location->latitude; 22 | $config['defaultView']['lon'] = $record->location->longitude; 23 | $config['defaultView']['zoom'] = 10; 24 | } 25 | catch (Exception $e) { 26 | // ignore error 27 | trigger_error("Can't resolve IP address: " . $e->getMessage(), E_USER_WARNING); 28 | } 29 | }); 30 | -------------------------------------------------------------------------------- /src/language.js: -------------------------------------------------------------------------------- 1 | /* global languages:false, lang_str:false */ 2 | /* eslint camelcase:0 */ 3 | var tagTranslations = require('./tagTranslations') 4 | 5 | function getPreferredDataLanguage () { 6 | var m = (navigator.language || navigator.userLanguage).match(/^([^-]+)(-.*|)$/) 7 | if (m) { 8 | return m[1].toLocaleLowerCase() 9 | } else { 10 | return ui_lang 11 | } 12 | } 13 | 14 | function getAcceptLanguages () { 15 | return navigator.languages || [ navigator.language || navigator.userLanguage ] 16 | } 17 | 18 | function getUiLanguages () { 19 | var i, code 20 | var ret = {} 21 | var acceptLanguages = getAcceptLanguages() 22 | 23 | for (i = 0; i < acceptLanguages.length; i++) { 24 | code = acceptLanguages[i] 25 | if (languages.indexOf(code) !== -1) { 26 | ret[code] = langName(code) 27 | } 28 | } 29 | 30 | for (i = 0; i < languages.length; i++) { 31 | code = languages[i] 32 | if (!(code in ret)) { 33 | ret[code] = langName(code) 34 | } 35 | } 36 | 37 | return ret 38 | } 39 | 40 | function getDataLanguages () { 41 | var code 42 | var ret = {} 43 | var acceptLanguages = getAcceptLanguages() 44 | 45 | for (var i = 0; i < acceptLanguages.length; i++) { 46 | code = acceptLanguages[i] 47 | ret[code] = langName(code) 48 | } 49 | 50 | for (var k in lang_str) { 51 | var m = k.match(/^lang:(.*)$/) 52 | if (m) { 53 | code = m[1] 54 | if (code === 'current') { 55 | continue 56 | } 57 | if (!(code in ret)) { 58 | ret[code] = langName(code) 59 | } 60 | } 61 | } 62 | 63 | return ret 64 | } 65 | 66 | function langName (code) { 67 | var ret = '' 68 | 69 | if (('lang_native:' + code) in lang_str && lang_str['lang_native:' + code]) { 70 | ret += lang_str['lang_native:' + code] 71 | } else { 72 | ret += 'Language "' + code + '"' 73 | } 74 | 75 | if (('lang:' + code) in lang_str && lang_str['lang:' + code]) { 76 | ret += ' (' + lang_str['lang:' + code] + ')' 77 | } 78 | 79 | return ret 80 | } 81 | 82 | register_hook('init_callback', function (initState, callback) { 83 | if (!('ui_lang' in options)) { 84 | options.ui_lang = ui_lang 85 | } 86 | 87 | if (!('data_lang' in options)) { 88 | options.data_lang = getPreferredDataLanguage() 89 | } 90 | tagTranslations.setTagLanguage(options.data_lang) 91 | 92 | callback(null) 93 | }) 94 | 95 | register_hook('options_form', function (def) { 96 | def.ui_lang = { 97 | 'name': lang('options:ui_lang'), 98 | 'type': 'select', 99 | 'values': getUiLanguages(), 100 | 'req': true, 101 | 'default': ui_lang, 102 | 'reloadOnChange': true 103 | } 104 | 105 | def.data_lang = { 106 | 'name': lang('options:data_lang'), 107 | 'desc': lang('options:data_lang:desc'), 108 | 'type': 'select', 109 | 'values': getDataLanguages(), 110 | 'default': getPreferredDataLanguage(), 111 | 'placeholder': lang('options:data_lang:local') 112 | } 113 | }) 114 | 115 | register_hook('options_save', function (options, old_options) { 116 | if ('data_lang' in options) { 117 | if (old_options.data_lang !== options.data_lang) { 118 | tagTranslations.setTagLanguage(options.data_lang) 119 | baseCategory.recalc() 120 | } 121 | } 122 | }) 123 | -------------------------------------------------------------------------------- /src/language.php: -------------------------------------------------------------------------------- 1 | query('select 1 from lang_non_translated'); 16 | if (!$res) { 17 | $query = <<query($query); 26 | } 27 | 28 | foreach ($strings as $k => $count) { 29 | if ($count > 0) { 30 | $query = 'insert or replace into lang_non_translated values (' . $db->quote($k) . ', ' . $db->quote($ui_lang) . ', coalesce((select count + ' . $db->quote($count) . ' from lang_non_translated where str=' . $db->quote($k) . ' and lang=' . $db->quote($ui_lang) . '), ' . $db->quote($count) . '))'; 31 | $db->query($query); 32 | } 33 | } 34 | }); 35 | -------------------------------------------------------------------------------- /src/leaflet-geo-search.js: -------------------------------------------------------------------------------- 1 | const LeafletGeoSearch = require('leaflet-geosearch') 2 | 3 | register_hook('init', function () { 4 | // Add Geo Search 5 | var provider = new LeafletGeoSearch.OpenStreetMapProvider() 6 | var searchControl = new LeafletGeoSearch.GeoSearchControl({ 7 | provider: provider, 8 | showMarker: false, 9 | retainZoomLevel: true 10 | }) 11 | global.map.addControl(searchControl) 12 | }) 13 | -------------------------------------------------------------------------------- /src/maki.js: -------------------------------------------------------------------------------- 1 | const svgToDataURI = require('mini-svg-data-uri') 2 | 3 | /* global openstreetbrowserPrefix */ 4 | var loadClash = {} 5 | var cache = {} 6 | var paths = { 7 | maki: 'node_modules/@mapbox/maki/icons/ID.svg', 8 | temaki: 'node_modules/@rapideditor/temaki/icons/ID.svg' 9 | } 10 | 11 | function applyOptions (code, options) { 12 | var style = '' 13 | 14 | for (var k in options) { 15 | if (k !== 'size') { 16 | style += k + ':' + options[k] + ';' 17 | } 18 | } 19 | 20 | let result = code.replace(/ p[1](req.statusText, null)) 52 | delete loadClash[url] 53 | return 54 | } 55 | 56 | cache[url] = req.responseText 57 | 58 | loadClash[url].forEach(p => p[1](null, applyOptions(cache[url], p[0]))) 59 | delete loadClash[url] 60 | }) 61 | req.open('GET', url) 62 | req.send() 63 | } 64 | 65 | module.exports = maki 66 | -------------------------------------------------------------------------------- /src/map-getMetersPerPixel.js: -------------------------------------------------------------------------------- 1 | function getMetersPerPixel () { 2 | return 40075016.686 * Math.abs(Math.cos(this.getCenter().lat / 180 * Math.PI)) / Math.pow(2, this.getZoom() + 8) 3 | } 4 | 5 | module.exports = getMetersPerPixel 6 | -------------------------------------------------------------------------------- /src/mapLayers.js: -------------------------------------------------------------------------------- 1 | const state = require('./state') 2 | 3 | var mapLayers = {} 4 | var currentMapLayer = null 5 | 6 | register_hook('init', function () { 7 | if (!config.baseMaps) { 8 | var osmMapnik = L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', 9 | { 10 | maxZoom: config.maxZoom || 19, 11 | attribution: '© OpenStreetMap' 12 | } 13 | ) 14 | osmMapnik.addTo(map) 15 | 16 | return 17 | } 18 | 19 | var layers = {} 20 | var preferredLayer = null 21 | for (var i = 0; i < config.baseMaps.length; i++) { 22 | var def = config.baseMaps[i] 23 | 24 | var layer = L.tileLayer( 25 | def.url, 26 | { 27 | attribution: def.attribution, 28 | maxNativeZoom: def.maxZoom, 29 | maxZoom: config.maxZoom || 19 30 | } 31 | ) 32 | 33 | if (preferredLayer === null) { 34 | preferredLayer = layer 35 | } 36 | if (def.id === options.preferredBaseMap) { 37 | preferredLayer = layer 38 | } 39 | 40 | layers[def.name] = layer 41 | mapLayers[def.id] = layer 42 | } 43 | 44 | preferredLayer.addTo(map) 45 | L.control.layers(layers).addTo(map) 46 | 47 | map.on('baselayerchange', function (e) { 48 | currentMapLayer = e.layer 49 | state.update() 50 | }) 51 | }) 52 | 53 | register_hook('options_form', function (def) { 54 | var baseMaps = {} 55 | 56 | if (!config.baseMaps) { 57 | return 58 | } 59 | 60 | for (var i = 0; i < config.baseMaps.length; i++) { 61 | baseMaps[config.baseMaps[i].id] = config.baseMaps[i].name 62 | } 63 | 64 | def.preferredBaseMap = { 65 | 'name': lang('options:preferredBaseMap'), 66 | 'type': 'select', 67 | 'values': baseMaps 68 | } 69 | }) 70 | 71 | register_hook('options_save', function (data) { 72 | if ('preferredBaseMap' in data && data.preferredBaseMap in mapLayers) { 73 | if (currentMapLayer) { 74 | map.removeLayer(currentMapLayer) 75 | } 76 | 77 | map.addLayer(mapLayers[data.preferredBaseMap]) 78 | } 79 | }) 80 | 81 | register_hook('state-get', (data) => { 82 | for (const k in mapLayers) { 83 | if (currentMapLayer === mapLayers[k]) { 84 | data.basemap = k 85 | } 86 | } 87 | }) 88 | 89 | register_hook('state-apply', (data) => { 90 | if ('basemap' in data) { 91 | if (currentMapLayer) { 92 | map.removeLayer(currentMapLayer) 93 | } 94 | 95 | mapLayers[data.basemap].addTo(map) 96 | } 97 | }) 98 | -------------------------------------------------------------------------------- /src/markers.js: -------------------------------------------------------------------------------- 1 | const markers = require('openstreetbrowser-markers') 2 | var OverpassLayer = require('overpass-layer') 3 | 4 | OverpassLayer.twig.extendFunction('markerLine', (data, options) => OverpassLayer.twig.filters.raw(markers.line(data, options))) 5 | OverpassLayer.twig.extendFunction('markerCircle', (data, options) => OverpassLayer.twig.filters.raw(markers.circle(data, options))) 6 | OverpassLayer.twig.extendFunction('markerPointer', (data, options) => OverpassLayer.twig.filters.raw(markers.pointer(data, options))) 7 | OverpassLayer.twig.extendFunction('markerPolygon', (data, options) => OverpassLayer.twig.filters.raw(markers.polygon(data, options))) 8 | 9 | module.exports = { 10 | line: markers.line, 11 | circle: markers.circle, 12 | pointer: markers.pointer, 13 | polygon: markers.polygon 14 | } 15 | -------------------------------------------------------------------------------- /src/moreCategories.js: -------------------------------------------------------------------------------- 1 | const tabs = require('modulekit-tabs') 2 | 3 | const Browser = require('./Browser') 4 | 5 | let tab 6 | 7 | function moreCategoriesIndex () { 8 | let content = tab.content 9 | 10 | content.innerHTML = '

' + lang('more_categories') + '

' 11 | 12 | const dom = document.createElement('div') 13 | content.appendChild(dom) 14 | 15 | const browser = new Browser('more-categories', dom) 16 | browser.buildPage({}) 17 | 18 | browser.on('close', () => tab.unselect()) 19 | } 20 | 21 | register_hook('init', function (callback) { 22 | tab = new tabs.Tab({ 23 | id: 'moreCategories' 24 | }) 25 | global.tabs.add(tab) 26 | 27 | tab.header.innerHTML = '' 28 | tab.header.title = lang('more_categories') 29 | 30 | tab.on('select', () => { 31 | tab.content.innerHTML = '' 32 | moreCategoriesIndex() 33 | }) 34 | }) 35 | 36 | -------------------------------------------------------------------------------- /src/nominatim-search.css: -------------------------------------------------------------------------------- 1 | .nominatim-search input { 2 | display: block; 3 | width: 100%; 4 | box-sizing: border-box; 5 | } 6 | .nominatim-search ul { 7 | margin: 0 0.5em; 8 | padding-left: 0; 9 | } 10 | .nominatim-search ul li { 11 | margin: 0.5em 0; 12 | list-style: none; 13 | } 14 | -------------------------------------------------------------------------------- /src/nominatim-search.js: -------------------------------------------------------------------------------- 1 | const tabs = require('modulekit-tabs') 2 | const httpGet = require('./httpGet') 3 | require('./nominatim-search.css') 4 | 5 | let tab 6 | let input 7 | let domResults 8 | 9 | function show (data) { 10 | while(domResults.lastChild) { 11 | domResults.removeChild(domResults.lastChild) 12 | } 13 | 14 | data.forEach( 15 | entry => { 16 | let a = document.createElement('a') 17 | a.appendChild(document.createTextNode(entry.display_name)) 18 | 19 | a.href = '#' 20 | a.onclick = () => { 21 | let bounds = new L.LatLngBounds( 22 | L.latLng(entry.boundingbox[0], entry.boundingbox[2]), 23 | L.latLng(entry.boundingbox[1], entry.boundingbox[3]) 24 | ) 25 | 26 | global.map.fitBounds(bounds, { animate: true }) 27 | 28 | return false 29 | } 30 | 31 | let li = document.createElement('li') 32 | li.appendChild(a) 33 | 34 | domResults.appendChild(li) 35 | } 36 | ) 37 | } 38 | 39 | function search (str) { 40 | httpGet( 41 | 'https://nominatim.openstreetmap.org/search?format=json&q=' + encodeURIComponent(str), 42 | {}, 43 | (err, result) => { 44 | if (err) { 45 | return alert(err) 46 | } 47 | 48 | let data = JSON.parse(result.body) 49 | show(data) 50 | } 51 | ) 52 | } 53 | 54 | register_hook('init', function () { 55 | tab = new tabs.Tab({ 56 | id: 'search', 57 | weight: -1 58 | }) 59 | tab.content.classList.add('nominatim-search') 60 | global.tabs.add(tab) 61 | 62 | tab.header.innerHTML = '' 63 | tab.header.title = lang('search') 64 | 65 | let input = document.createElement('input') 66 | let inputTimer 67 | input.type = 'text' 68 | input.addEventListener('input', () => { 69 | if (inputTimer) { 70 | global.clearTimeout(inputTimer) 71 | } 72 | 73 | inputTimer = global.setTimeout( 74 | () => search(input.value), 75 | 300 76 | ) 77 | }) 78 | 79 | tab.content.appendChild(input) 80 | 81 | domResults = document.createElement('ul') 82 | tab.content.appendChild(domResults) 83 | 84 | tab.on('select', () => input.focus()) 85 | }) 86 | -------------------------------------------------------------------------------- /src/options.js: -------------------------------------------------------------------------------- 1 | /* globals form, ajax, options:true */ 2 | var moduleOptions = {} 3 | var prevPage 4 | var optionsFormEl 5 | 6 | register_hook('init', function () { 7 | var menu = document.getElementById('menu') 8 | 9 | var li = document.createElement('li') 10 | menu.appendChild(li) 11 | 12 | var link = document.createElement('a') 13 | link.innerHTML = lang('main:options') 14 | link.href = '#options' 15 | link.onclick = moduleOptions.open 16 | 17 | li.appendChild(link) 18 | }) 19 | 20 | moduleOptions.open = function () { 21 | var def = { 22 | 'debug': { 23 | 'type': 'boolean', 24 | 'name': lang('options:debug_mode'), 25 | 'weight': 10, 26 | 'reloadOnChange': true 27 | } 28 | } 29 | 30 | call_hooks('options_form', def) 31 | 32 | var optionsForm = new form('options', def) 33 | prevPage = document.getElementById('content').className 34 | document.getElementById('content').className = 'options' 35 | var dom = document.getElementById('contentOptions') 36 | dom.innerHTML = '' 37 | 38 | let orig_options = { 39 | debug: false 40 | } 41 | call_hooks('options_orig_data', orig_options) 42 | for (let k in orig_options) { 43 | if (!(k in options)) { 44 | options[k] = orig_options[k] 45 | } 46 | } 47 | 48 | optionsForm.set_data(options) 49 | 50 | optionsFormEl = document.createElement('form') 51 | optionsFormEl.onsubmit = moduleOptions.submit.bind(this, optionsForm) 52 | dom.appendChild(optionsFormEl) 53 | 54 | optionsForm.show(optionsFormEl) 55 | 56 | var input = document.createElement('button') 57 | input.innerHTML = lang('save') 58 | optionsFormEl.appendChild(input) 59 | 60 | input = document.createElement('button') 61 | input.innerHTML = lang('cancel') 62 | optionsFormEl.appendChild(input) 63 | input.onclick = function () { 64 | document.getElementById('content').className = prevPage 65 | dom.removeChild(optionsFormEl) 66 | return false 67 | } 68 | 69 | call_hooks('options_open', optionsForm, optionsFormEl) 70 | 71 | return false 72 | } 73 | 74 | moduleOptions.submit = function (optionsForm) { 75 | var data = optionsForm.get_data() 76 | 77 | var reload = false 78 | for (var k in data) { 79 | if (optionsForm.def[k].reloadOnChange && options[k] !== data[k]) { 80 | reload = true 81 | } 82 | } 83 | 84 | ajax('options_save', null, data, function (ret) { 85 | let oldOptions = options 86 | options = data 87 | 88 | optionsFormEl.parentNode.removeChild(optionsFormEl) 89 | 90 | document.getElementById('content').className = prevPage 91 | 92 | call_hooks('options_save', data, oldOptions) 93 | 94 | if (reload) { 95 | location.reload() 96 | } 97 | }) 98 | 99 | return false 100 | } 101 | 102 | module.exports = moduleOptions 103 | -------------------------------------------------------------------------------- /src/options.php: -------------------------------------------------------------------------------- 1 | true, 'options' => $_SESSION['options']); 10 | } 11 | 12 | function ajax_options_save_key ($get_param, $postdata) { 13 | $kv = json_decode($postdata, true); 14 | 15 | if ($kv['value'] === null) { 16 | unset($_SESSION['options'][$kv['key']]); 17 | } else { 18 | $_SESSION['options'][$kv['key']] = $kv['value']; 19 | } 20 | 21 | call_hooks('options_save', $_SESSION['options']); 22 | 23 | return array('success' => true, 'options' => $_SESSION['options']); 24 | } 25 | 26 | function ajax_options_save_key_array_add ($get_param, $postdata) { 27 | $kv = json_decode($postdata, true); 28 | 29 | if (!array_key_exists($kv['option'], $_SESSION['options'])) { 30 | $_SESSION['options'][$kv['option']] = []; 31 | } 32 | 33 | $_SESSION['options'][$kv['option']][] = $kv['element']; 34 | 35 | call_hooks('options_save', $_SESSION['options']); 36 | 37 | return array('success' => true, 'options' => $_SESSION['options']); 38 | } 39 | 40 | function ajax_options_save_key_array_remove ($get_param, $postdata) { 41 | $kv = json_decode($postdata, true); 42 | 43 | if (!array_key_exists($kv['option'], $_SESSION['options'])) { 44 | $_SESSION['options'][$kv['option']] = []; 45 | } 46 | 47 | $pos = array_search($kv['element'], $_SESSION['options'][$kv['option']]); 48 | if ($pos !== false) { 49 | array_splice($_SESSION['options'][$kv['option']], $pos, 1); 50 | } 51 | 52 | call_hooks('options_save', $_SESSION['options']); 53 | 54 | return array('success' => true, 'options' => $_SESSION['options']); 55 | } 56 | 57 | if (!array_key_exists('options', $_SESSION)) { 58 | $_SESSION['options'] = array(); 59 | } 60 | 61 | html_export_var(array('options' => $_SESSION['options'])); 62 | -------------------------------------------------------------------------------- /src/optionsYaml.js: -------------------------------------------------------------------------------- 1 | const yaml = require('js-yaml') 2 | let error = false 3 | 4 | function createYaml (form) { 5 | const data = form.get_data() 6 | let result = '### OpenStreetBrowser Options ###\n' 7 | 8 | Object.entries(form.def).forEach(([k, d]) => { 9 | result += '## ' + d.name + '\n' 10 | if (d.desc) { 11 | result += '# ' + d.desc.split('\n').join('\n# ') + '\n' 12 | } 13 | 14 | const o = {} 15 | o[k] = data[k] 16 | result += yaml.dump(o) + '\n' 17 | }) 18 | 19 | return result 20 | } 21 | 22 | register_hook('options_open', (optionsForm, optionsFormEl) => { 23 | const inputYaml = document.createElement('textarea') 24 | 25 | const button = document.createElement('button') 26 | button.innerHTML = lang('YAML') 27 | optionsFormEl.appendChild(button) 28 | button.onclick = function () { 29 | if (optionsForm.table.style.display === 'none') { 30 | if (!error) { 31 | optionsForm.table.style.display = 'block' 32 | inputYaml.style.display = 'none' 33 | } 34 | 35 | return false 36 | } 37 | 38 | optionsForm.table.style.display = 'none' 39 | inputYaml.style.display = 'block' 40 | inputYaml.value = createYaml(optionsForm) 41 | inputYaml.style.width = '100%' 42 | inputYaml.style.height = inputYaml.scrollHeight + 'px' 43 | return false 44 | } 45 | 46 | inputYaml.name = 'options-yaml' 47 | inputYaml.style.display = 'none' 48 | optionsFormEl.insertBefore(inputYaml, optionsFormEl.firstChild) 49 | inputYaml.onblur = () => { 50 | updateForm() 51 | } 52 | 53 | function updateForm () { 54 | let options 55 | try { 56 | options = yaml.load(inputYaml.value) 57 | error = false 58 | } catch (e) { 59 | global.alert(e.message) 60 | error = true 61 | } 62 | 63 | optionsForm.set_data(options) 64 | } 65 | }) 66 | -------------------------------------------------------------------------------- /src/overpassChooser.js: -------------------------------------------------------------------------------- 1 | /* globals overpassUrl:true */ 2 | 3 | register_hook('init', function () { 4 | if (options.overpassUrl) { 5 | overpassUrl = options.overpassUrl 6 | } 7 | }) 8 | 9 | register_hook('options_form', function (def) { 10 | var values = config.overpassUrl 11 | if (!Array.isArray(values)) { 12 | values = [ values ] 13 | } 14 | 15 | def.overpassUrl = { 16 | 'name': lang('options:overpassUrl'), 17 | 'type': 'select', 18 | 'values': values, 19 | 'req': false, 20 | 'placeholder': lang('default') 21 | } 22 | }) 23 | 24 | register_hook('options_save', function (data) { 25 | if ('overpassUrl' in data) { 26 | if (data.overpassUrl === null) { 27 | overpassUrl = config.overpassUrl 28 | if (Array.isArray(overpassUrl) && overpassUrl.length) { 29 | overpassUrl = overpassUrl[0] 30 | } 31 | } else { 32 | overpassUrl = data.overpassUrl 33 | } 34 | 35 | overpassFrontend.url = overpassUrl 36 | } 37 | }) 38 | -------------------------------------------------------------------------------- /src/permalink.js: -------------------------------------------------------------------------------- 1 | let permalink 2 | 3 | register_hook('state-update', function (state, hash) { 4 | permalink.href = hash 5 | }) 6 | 7 | register_hook('init', function () { 8 | let li = document.createElement('li') 9 | 10 | permalink = document.createElement('a') 11 | li.appendChild(permalink) 12 | permalink.innerHTML = lang('main:permalink') 13 | 14 | let menu = document.getElementById('menu') 15 | menu.appendChild(li) 16 | }) 17 | -------------------------------------------------------------------------------- /src/pinnedCategories.js: -------------------------------------------------------------------------------- 1 | const tabs = require('modulekit-tabs') 2 | 3 | register_hook('init', startup) 4 | function startup () { 5 | if ('pinned-categories' in options && Array.isArray(options['pinned-categories'])) { 6 | options['pinned-categories'].forEach(id => { 7 | OpenStreetBrowserLoader.getCategory(id, {}, (err, category) => { 8 | if (err) { 9 | console.error(err) 10 | return global.alert('Error loading pinned category "' + id + '":\n' + err.message) 11 | } 12 | 13 | if (!category.parentDom) { 14 | global.rootCategories[id] = category 15 | category.setParentDom(document.getElementById('contentListAddCategories')) 16 | } 17 | }) 18 | }) 19 | } 20 | } 21 | 22 | function isPinned (id) { 23 | return 'pinned-categories' in options && Array.isArray(options['pinned-categories']) ? options['pinned-categories'].includes(id) : false 24 | } 25 | 26 | hooks.register('category-overpass-init', (category) => { 27 | const m = category.id.match(/^(.*)\/(.*)$/) 28 | if (!m) { 29 | return 30 | } 31 | 32 | const id = category.id 33 | category.tabPin = new tabs.Tab({ 34 | id: 'pin', 35 | weight: 9 36 | }) 37 | 38 | category.tools.add(category.tabPin) 39 | let pinHeader = document.createElement('span') 40 | pinHeader.href = '#' 41 | category.tabPin.header.appendChild(pinHeader) 42 | updateHeader(category, isPinned(id), pinHeader) 43 | 44 | category.on('editor-init', (editor) => { 45 | // overwrite default postApplyContent action 46 | editor._postApplyContent = () => { 47 | if (!isPinned(id) && editor.category) { 48 | editor.category.remove() 49 | editor.category = null 50 | } else { 51 | editor.category.close() 52 | } 53 | } 54 | }) 55 | 56 | category.tabPin.on('select', () => { 57 | category.tabPin.unselect() 58 | let nowPinned = !isPinned(id) 59 | updateHeader(category, nowPinned, pinHeader) 60 | 61 | ajax(nowPinned ? 'options_save_key_array_add' : 'options_save_key_array_remove', 62 | {}, 63 | { option: 'pinned-categories', element: id }, 64 | result => { 65 | if (result.success) { 66 | options = result.options 67 | } 68 | } 69 | ) 70 | }) 71 | }) 72 | 73 | register_hook('options_form', def => { 74 | def['pinned-categories'] = { 75 | name: lang('pinnedCategories:remembered'), 76 | type: 'text', 77 | count: {default: 1, index_type: 'array'} 78 | } 79 | }) 80 | 81 | register_hook('options_save', (newOptions, oldOptions) => { 82 | startup(newOptions) 83 | 84 | if (oldOptions && Array.isArray(oldOptions['pinned-categories'])) { 85 | const newList = newOptions['pinned-categories'] ?? [] 86 | oldOptions['pinned-categories'].forEach(id => { 87 | if (!newList.includes(id)) { 88 | OpenStreetBrowserLoader.getCategory(id, {}, (err, category) => { 89 | updateHeader(category, false, category.tabPin.header.firstChild) 90 | }) 91 | } 92 | }) 93 | } 94 | }) 95 | 96 | function updateHeader (category, isPinned, pinHeader) { 97 | pinHeader.title = lang(isPinned ? 'pinnedCategories:forget' : 'pinnedCategories:remember') 98 | pinHeader.innerHTML = isPinned ? '' : '' 99 | 100 | if (!category.tabEdit) { 101 | return 102 | } 103 | 104 | if (isPinned) { 105 | category.tabEdit.header.innerHTML = '' 106 | category.tabEdit.header.title = lang('pinnedCategories:clone') 107 | } else { 108 | category.tabEdit.header.innerHTML = '' 109 | category.tabEdit.header.title = lang('edit') 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/repositories.php: -------------------------------------------------------------------------------- 1 | array( 12 | 'path' => $config['categoriesDir'], 13 | ), 14 | ); 15 | } 16 | 17 | call_hooks("get-repositories", $result); 18 | 19 | return $result; 20 | } 21 | 22 | function getRepo ($repoId, $repoData) { 23 | switch (array_key_exists('type', $repoData) ? $repoData['type'] : 'dir') { 24 | case 'git': 25 | $repo = new RepositoryGit($repoId, $repoData); 26 | break; 27 | default: 28 | $repo = new RepositoryDir($repoId, $repoData); 29 | } 30 | 31 | return $repo; 32 | } 33 | -------------------------------------------------------------------------------- /src/repositoriesGitea.php: -------------------------------------------------------------------------------- 1 | "{$repositoriesGitea['path']}/{$f1}/{$f2}", 16 | 'type' => 'git', 17 | 'group' => 'gitea', 18 | ); 19 | 20 | if (array_key_exists('url', $repositoriesGitea)) { 21 | $r['repositoryUrl'] = "{$repositoriesGitea['url']}/{{ repositoryId }}"; 22 | $r['categoryUrl'] = "{$repositoriesGitea['url']}/{{ repositoryId }}/src/branch/{{ branchId }}/{{ categoryId }}.{{ categoryFormat }}"; 23 | } 24 | 25 | $result["{$f1}/{$f2id}"] = $r; 26 | } 27 | } 28 | closedir($d2); 29 | } 30 | } 31 | closedir($d1); 32 | } 33 | }); 34 | 35 | register_hook('init', function () { 36 | global $repositoriesGitea; 37 | 38 | if (isset($repositoriesGitea) && array_key_exists('url', $repositoriesGitea)) { 39 | $d = array('repositoriesGitea' => array( 40 | 'url' => $repositoriesGitea['url'], 41 | )); 42 | html_export_var($d); 43 | } 44 | }); 45 | -------------------------------------------------------------------------------- /src/showMore.css: -------------------------------------------------------------------------------- 1 | .collapsed { 2 | max-height: 15em; 3 | overflow-y: hidden; 4 | } 5 | .category > .showMore { 6 | display: none; 7 | } 8 | .category.open > .showMore.active { 9 | display: block; 10 | background: #efefef; 11 | } 12 | -------------------------------------------------------------------------------- /src/showMore.js: -------------------------------------------------------------------------------- 1 | require('./showMore.css') 2 | 3 | function delayedUpdate (dom, p) { 4 | if (!p.timer) { 5 | p.timer = global.setTimeout( 6 | () => { 7 | delete p.timer 8 | 9 | if (dom.scrollHeight > dom.offsetHeight && dom.classList.contains('collapsed')) { 10 | p.classList.add('active') 11 | } 12 | 13 | if (dom.scrollHeight <= dom.offsetHeight && dom.classList.contains('collapsed')) { 14 | p.classList.remove('active') 15 | } 16 | }, 17 | 1 18 | ) 19 | } 20 | } 21 | 22 | function showMore (category, dom) { 23 | dom.classList.add('collapsed') 24 | 25 | let p = document.createElement('div') 26 | p.className = 'showMore' 27 | dom.parentNode.insertBefore(p, dom.nextSibling) 28 | 29 | let a = document.createElement('a') 30 | a.href = '#' 31 | a.innerHTML = lang('more_results') 32 | a.onclick = () => { 33 | dom.classList.remove('collapsed') 34 | p.classList.remove('active') 35 | return false 36 | } 37 | p.appendChild(a) 38 | 39 | category.on('add', delayedUpdate.bind(this, dom, p)) 40 | category.on('remove', delayedUpdate.bind(this, dom, p)) 41 | category.on('open', () => { 42 | p.classList.remove('active') 43 | dom.classList.add('collapsed') 44 | delayedUpdate(dom, p) 45 | }) 46 | } 47 | 48 | module.exports = showMore 49 | -------------------------------------------------------------------------------- /src/state.js: -------------------------------------------------------------------------------- 1 | /* globals setPath, history */ 2 | 3 | var queryString = require('query-string') 4 | 5 | function get () { 6 | var state = {} 7 | 8 | // repo 9 | if (global.mainRepo !== '') { 10 | state.repo = global.mainRepo 11 | } 12 | 13 | // path 14 | if (currentPath) { 15 | state.path = currentPath 16 | } 17 | 18 | // location 19 | if (typeof map.getZoom() !== 'undefined') { 20 | var center = map.getCenter().wrap() 21 | var zoom = map.getZoom() 22 | 23 | state.lat = center.lat 24 | state.lon = center.lng 25 | state.zoom = zoom 26 | } 27 | 28 | // other modules 29 | call_hooks('state-get', state) 30 | 31 | // done 32 | return state 33 | } 34 | 35 | function apply (state) { 36 | // path 37 | setPath(state.path, state) 38 | 39 | // location 40 | if (state.lat && state.lon && state.zoom) { 41 | if (typeof map.getZoom() === 'undefined') { 42 | map.setView({ lat: state.lat, lng: state.lon }, state.zoom) 43 | } else { 44 | map.flyTo({ lat: state.lat, lng: state.lon }, state.zoom) 45 | } 46 | } 47 | 48 | // other modules 49 | call_hooks('state-apply', state) 50 | } 51 | 52 | function stringify (state) { 53 | var link = '' 54 | 55 | if (!state) { 56 | state = get() 57 | } 58 | 59 | var tmpState = JSON.parse(JSON.stringify(state)) 60 | 61 | // path 62 | if (state.path) { 63 | link += state.path 64 | delete tmpState.path 65 | } 66 | 67 | // location 68 | var locPrecision = 5 69 | if (state.zoom) { 70 | locPrecision = 71 | state.zoom > 16 ? 5 72 | : state.zoom > 8 ? 4 73 | : state.zoom > 4 ? 3 74 | : state.zoom > 2 ? 2 75 | : state.zoom > 1 ? 1 76 | : 0 77 | } 78 | 79 | if (state.zoom && state.lat && state.lon) { 80 | link += (link === '' ? '' : '&') + 'map=' + 81 | parseFloat(state.zoom).toFixed(0) + '/' + 82 | state.lat.toFixed(locPrecision) + '/' + 83 | state.lon.toFixed(locPrecision) 84 | 85 | delete tmpState.zoom 86 | delete tmpState.lat 87 | delete tmpState.lon 88 | } 89 | 90 | var newHash = queryString.stringify(tmpState) 91 | 92 | // Characters we dont's want escaped 93 | newHash = newHash.replace(/%2F/g, '/') 94 | newHash = newHash.replace(/%2C/g, ',') 95 | 96 | if (newHash !== '') { 97 | link += (link === '' ? '' : '&') + newHash 98 | } 99 | 100 | return link 101 | } 102 | 103 | function parse (link) { 104 | var firstEquals = link.search('=') 105 | var firstAmp = link.search('&') 106 | var urlNonPathPart = '' 107 | var newState = {} 108 | var newPath = '' 109 | 110 | if (link === '') { 111 | // nothing 112 | } else if (firstEquals === -1) { 113 | if (firstAmp === -1) { 114 | newPath = link 115 | } else { 116 | newPath = link.substr(0, firstAmp) 117 | } 118 | } else { 119 | if (firstAmp === -1) { 120 | urlNonPathPart = link 121 | } else if (firstAmp < firstEquals) { 122 | newPath = link.substr(0, firstAmp) 123 | urlNonPathPart = link.substr(firstAmp + 1) 124 | } else { 125 | urlNonPathPart = link 126 | } 127 | } 128 | 129 | newState = queryString.parse(urlNonPathPart) 130 | if (newPath !== '') { 131 | newState.path = newPath 132 | } 133 | 134 | if ('map' in newState) { 135 | var parts = newState.map.split('/') 136 | newState.zoom = parts[0] 137 | newState.lat = parts[1] 138 | newState.lon = parts[2] 139 | delete newState.map 140 | } 141 | 142 | return newState 143 | } 144 | 145 | function update (state, push) { 146 | if (!state) { 147 | state = get() 148 | } 149 | 150 | var newHash = '#' + stringify(state) 151 | 152 | call_hooks('state-update', state, newHash) 153 | 154 | if (push) { 155 | history.pushState(null, null, newHash) 156 | call_hooks('statePush', state, newHash) 157 | } else if (location.hash !== newHash && (location.hash !== '' || newHash !== '#')) { 158 | history.replaceState(null, null, newHash) 159 | call_hooks('stateReplace', state, newHash) 160 | } 161 | } 162 | 163 | module.exports = { 164 | get: get, // get the current app state 165 | apply: apply, // apply a state to the current app 166 | 167 | stringify: stringify, // create a link from a state (or the current state) 168 | parse: parse, // parse a state from a link 169 | 170 | update: update // update url (either replace or push) 171 | } 172 | -------------------------------------------------------------------------------- /src/tagTranslations.js: -------------------------------------------------------------------------------- 1 | /* global lang_str lang_non_translated */ 2 | /* eslint camelcase:0 */ 3 | const sprintf = require('sprintf-js') 4 | var OverpassLayer = require('overpass-layer') 5 | var tagLang = null 6 | 7 | OverpassLayer.twig.extendFunction('keyTrans', function () { 8 | return tagTranslationsTrans.call(this, arguments[0], undefined, arguments[1]) 9 | }) 10 | OverpassLayer.twig.extendFunction('tagTrans', function () { 11 | return tagTranslationsTrans.apply(this, arguments) 12 | }) 13 | OverpassLayer.twig.extendFunction('tagTransList', function () { 14 | return tagTranslationsTransList.apply(this, arguments) 15 | }) 16 | OverpassLayer.twig.extendFunction('localizedTag', function (tags, id) { 17 | if (tagLang && id + ':' + tagLang in tags) { 18 | return tags[id + ':' + tagLang] 19 | } 20 | 21 | return tags[id] 22 | }) 23 | OverpassLayer.twig.extendFunction('trans', function () { 24 | return lang.apply(this, arguments) 25 | }) 26 | OverpassLayer.twig.extendFunction('isTranslated', function (str) { 27 | return tagTranslationsIsTranslated(str) 28 | }) 29 | OverpassLayer.twig.extendFunction('repoTrans', function (str) { 30 | if (!global.currentCategory.repository) { 31 | return str 32 | } 33 | 34 | const lang = global.currentCategory.repository.lang 35 | const format = lang && str in lang ? lang[str] : str 36 | 37 | return vsprintf(format, Array.from(arguments).slice(1)) 38 | }) 39 | 40 | function tagTranslationsIsTranslated (str) { 41 | return !(str in lang_non_translated) && (str in lang_str) 42 | } 43 | 44 | function tagTranslationsTrans () { 45 | var tag = arguments[0] 46 | var value 47 | var count 48 | if (arguments.length > 1) { 49 | value = arguments[1] 50 | } 51 | if (arguments.length > 2) { 52 | count = arguments[2] 53 | } 54 | 55 | if (typeof value === 'undefined') { 56 | return lang('tag:' + tag, count) 57 | } else { 58 | return lang('tag:' + tag + '=' + value, count) 59 | } 60 | } 61 | 62 | function tagTranslationsTransList (key, values) { 63 | if (typeof values === 'undefined') { 64 | return null 65 | } 66 | 67 | values = values.split(';') 68 | 69 | values = values.map(function (key, value) { 70 | return tagTranslationsTrans(key, value.trim()) 71 | }.bind(this, key)) 72 | 73 | return lang_enumerate(values) 74 | } 75 | 76 | module.exports = { 77 | trans: tagTranslationsTrans, 78 | isTranslated: tagTranslationsIsTranslated, 79 | setTagLanguage: function (lang) { 80 | tagLang = lang 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/tagsDisplay-tag2link.js: -------------------------------------------------------------------------------- 1 | const httpGet = require('./httpGet') 2 | const formatter = require('./tagsDisplay').formatter 3 | 4 | let tag2link 5 | 6 | register_hook('init_callback', (initState, callback) => { 7 | httpGet('dist/tag2link.json', {}, (err, result) => { 8 | if (err) { 9 | console.error('Can\'t read dist/tag2link.json - execute bin/download_dependencies') 10 | return callback() 11 | } 12 | 13 | tag2link = JSON.parse(result.body) 14 | 15 | Object.keys(tag2link).forEach(key => { 16 | let tag = tag2link[key] 17 | let link = tag.formatter[0].link.replace('$1', '{{ value }}') 18 | 19 | if (tag.formatter.length > 1) { 20 | link = "#\" onclick=\"return tag2link(this, " + JSON.stringify(key).replace(/"/g, '"') + ", {{ value|json_encode }})" 21 | } 22 | 23 | formatter.push({ 24 | regexp: new RegExp("^" + key + "$"), 25 | link 26 | }) 27 | }) 28 | 29 | callback() 30 | }) 31 | }) 32 | 33 | global.tag2link = function (dom, key, value) { 34 | let div = document.createElement('div') 35 | div.className = 'tag2link' 36 | dom.parentNode.appendChild(div) 37 | 38 | let closeButton = document.createElement('div') 39 | closeButton.className = 'closeButton' 40 | closeButton.innerHTML = '❌' 41 | closeButton.onclick = () => { 42 | dom.parentNode.removeChild(div) 43 | } 44 | div.appendChild(closeButton) 45 | 46 | let selector = document.createElement('ul') 47 | div.appendChild(selector) 48 | 49 | let tag = tag2link[key] 50 | tag.formatter.forEach(formatter => { 51 | let li = document.createElement('li') 52 | 53 | let a = document.createElement('a') 54 | a.target = '_blank' 55 | a.href = formatter.link.replace('$1', value) 56 | a.appendChild(document.createTextNode(formatter.operator)) 57 | 58 | li.appendChild(a) 59 | selector.appendChild(li) 60 | }) 61 | 62 | return false 63 | } 64 | -------------------------------------------------------------------------------- /src/tagsDisplay.js: -------------------------------------------------------------------------------- 1 | const OverpassLayer = require('overpass-layer') 2 | 3 | const formatter = [ 4 | { 5 | regexp: /^(.*:)?wikidata$/, 6 | link: 'https://wikidata.org/wiki/{{ value }}' 7 | }, 8 | { 9 | regexp: /^(.*:)?wikipedia$/, 10 | link: '{% set v = value|split(":") %}https://{{ v[0] }}.wikipedia.org/wiki/{{ v[1]|replace({" ": "_"}) }}' 11 | }, 12 | { 13 | regexp: /^(.*:)?wikipedia:([a-zA-Z]+)$/, 14 | link: '{% set v = key|matches(":([a-zA-Z]+)") %}https://{{ v[1] }}.wikipedia.org/wiki/{{ value|replace({" ": "_"}) }}' 15 | }, 16 | { 17 | regexp: /^((.*:)?website(:.*)?|(.*:)?url(:.*)?|contact:website)$/, 18 | link: '{{ value|websiteUrl }}' 19 | }, 20 | { 21 | regexp: /^(image|wikimedia_commons)$/, 22 | link: '{% if value matches "/^(File|Category):/" %}' + 23 | 'https://commons.wikimedia.org/wiki/{{ value|replace({" ": "_"}) }}' + 24 | '{% else %}' + 25 | '{{ value|websiteUrl }}' + 26 | '{% endif %}' 27 | }, 28 | { 29 | regexp: /^(species)$/, 30 | link: 'https://species.wikimedia.org/wiki/{{ value|replace({" ": "_"}) }}' 31 | }, 32 | { 33 | regexp: /^(phone|contact:phone|fax|contact:fax)(:.*|)$/, 34 | link: 'tel:{{ value }}' 35 | }, 36 | { 37 | regexp: /^(email|contact:email)(:.*|)$/, 38 | link: 'mailto:{{ value }}' 39 | } 40 | ] 41 | 42 | let compiled = false 43 | let defaultTemplate 44 | 45 | function tagsDisplay (tags) { 46 | if (!compiled) { 47 | defaultTemplate = OverpassLayer.twig.twig({ data: '{{ value }}', autoescape: true }) 48 | for (let i in formatter) { 49 | if (formatter[i].format) { 50 | formatter[i].template = OverpassLayer.twig.twig({ data: formatter[i].format, autoescape: true }) 51 | } else { 52 | formatter[i].template = OverpassLayer.twig.twig({ data: '{{ value }}', autoescape: true }) 53 | } 54 | } 55 | 56 | compiled = true 57 | } 58 | 59 | const div = document.createElement('dl') 60 | div.className = 'tags' 61 | for (let k in tags) { 62 | const dt = document.createElement('dt') 63 | dt.appendChild(document.createTextNode(k)) 64 | div.appendChild(dt) 65 | 66 | let template = defaultTemplate 67 | 68 | const dd = document.createElement('dd') 69 | for (let i = 0; i < formatter.length; i++) { 70 | if (k.match(formatter[i].regexp)) { 71 | template = formatter[i].template 72 | break 73 | } 74 | } 75 | 76 | let value = tags[k].split(/;/g) 77 | value = value.map(v => { 78 | // trim whitespace (but add it around the formatted value later) 79 | let m = v.match(/^( *)([^ ].*[^ ]|[^ ])( *)$/) 80 | if (m) { 81 | return m[1] + template.render({ key: k, value: m[2] }) + m[3] 82 | } 83 | return v 84 | }).join(';') 85 | 86 | dd.innerHTML = value 87 | div.appendChild(dd) 88 | } 89 | 90 | return div 91 | } 92 | 93 | module.exports = { 94 | display: tagsDisplay, 95 | formatter 96 | } 97 | -------------------------------------------------------------------------------- /src/wikidata.js: -------------------------------------------------------------------------------- 1 | const OverpassLayer = require('overpass-layer') 2 | 3 | var httpGet = require('./httpGet') 4 | var loadClash = {} 5 | var cache = {} 6 | 7 | function wikidataLoad (id, callback) { 8 | if (id in cache) { 9 | return callback(null, cache[id]) 10 | } 11 | 12 | if (id in loadClash) { 13 | loadClash[id].push(callback) 14 | return 15 | } 16 | loadClash[id] = [] 17 | 18 | httpGet('https://www.wikidata.org/wiki/Special:EntityData/' + id + '.json', {}, function (err, result) { 19 | if (err) { 20 | return callback(err, null) 21 | } 22 | 23 | result = JSON.parse(result.body) 24 | 25 | if (!result.entities || !result.entities[id]) { 26 | console.log('invalid result', result) 27 | cache[id] = false 28 | return callback(err, null) 29 | } 30 | 31 | cache[id] = result.entities[id] 32 | 33 | callback(null, result.entities[id]) 34 | 35 | loadClash[id].forEach(function (d) { 36 | d(null, result.entities[id]) 37 | }) 38 | delete loadClash[id] 39 | }) 40 | } 41 | 42 | module.exports = { 43 | load: wikidataLoad 44 | } 45 | 46 | OverpassLayer.twig.extendFilter('wikidataEntity', function (value, param) { 47 | const ob = global.currentMapFeature 48 | if (value in cache) { 49 | return cache[value] 50 | } 51 | 52 | wikidataLoad(value, () => { 53 | if (ob) { 54 | ob.recalc() 55 | } 56 | }) 57 | 58 | return null 59 | }) 60 | -------------------------------------------------------------------------------- /src/wikidata.php: -------------------------------------------------------------------------------- 1 | 'Y', 61 | 10 => 'M Y', 62 | 11 => 'j. M Y', 63 | 12 => 'j. M Y - G:00', 64 | 13 => 'j. M Y - G:i', 65 | 14 => 'j. M Y - G:i:s', 66 | ]; 67 | 68 | return $v->format($formats[$p]); 69 | } 70 | } 71 | 72 | function wikidataFormat ($id, $lang) { 73 | $ret = '' . wikidataGetLabel($id, $lang) . ''; 74 | 75 | $birthDate = wikidataGetValues($id, 'P569'); 76 | $deathDate = wikidataGetValues($id, 'P570'); 77 | if (sizeof($birthDate) && sizeof($deathDate)) { 78 | $ret .= ' (' . wikidataFormatDate($birthDate[0], 11) . ' — ' . wikidataFormatDate($deathDate[0], 11) . ')'; 79 | } 80 | elseif (sizeof($birthDate)) { 81 | $ret .= ' (* ' . wikidataFormatDate($birthDate[0], 11) . ')'; 82 | } 83 | elseif (sizeof($deathDate)) { 84 | $ret .= ' († ' . wikidataFormatDate($birthDate[0], 11) . ')'; 85 | } 86 | 87 | $occupation = wikidataGetValues($id, 'P106'); 88 | if (sizeof($occupation)) { 89 | $ret .= ', ' . implode(', ', array_map( 90 | function ($value) use ($lang) { 91 | return wikidataGetLabel($value['id'], $lang); 92 | }, 93 | $occupation 94 | )); 95 | } 96 | 97 | return $ret; 98 | } 99 | -------------------------------------------------------------------------------- /src/wikipedia.php: -------------------------------------------------------------------------------- 1 |

" . wikidataFormat($param['page'], $param['lang']) . "

"; 31 | $url = "https://wikidata.org/wiki/{$param['page']}"; 32 | if (array_key_exists('commonswiki', $data['sitelinks'])) { 33 | $url = $data['sitelinks']['commonswiki']['url']; 34 | } 35 | 36 | return array( 37 | 'content' => $content, 38 | 'languages' => [ 39 | $param['lang'] => $url, 40 | ], 41 | 'language' => $param['lang'], 42 | ); 43 | } 44 | } 45 | } 46 | 47 | if (!isset($wp_lang) || !(isset($wp_page) || isset($wp_url))) { 48 | return false; 49 | } 50 | 51 | if (!isset($wp_url)) { 52 | $wp_url = "https://{$wp_lang}.wikipedia.org/wiki/" . urlencode(strtr($wp_page, array(" " => "_"))); 53 | } 54 | 55 | $content = file_get_contents($wp_url); 56 | 57 | $langList = array($wp_lang => $wp_url); 58 | 59 | $dom = new DOMDocument(); 60 | $dom->loadHTML($content); 61 | 62 | $langDiv = $dom->getElementsByTagName('li');//interlanguage-link interwiki-bar'); 63 | for ($i = 0; $i < $langDiv->length; $i++) { 64 | $li = $langDiv->item($i); 65 | 66 | if (preg_match('/^interlanguage-link interwiki-([a-z\-]+)( .*|)$/', $li->getAttribute('class'), $m)) { 67 | $a = $li->firstChild; 68 | $langList[$m[1]] = $a->getAttribute('href'); 69 | } 70 | } 71 | 72 | if ($wp_lang !== $param['lang'] && array_key_exists($param['lang'], $langList)) { 73 | $content = file_get_contents($langList[$param['lang']]); 74 | $wp_lang = $param['lang']; 75 | } 76 | 77 | return array( 78 | 'content' => $content, 79 | 'languages' => $langList, 80 | 'language' => $wp_lang, 81 | ); 82 | } 83 | -------------------------------------------------------------------------------- /src/zenMode.js: -------------------------------------------------------------------------------- 1 | const zenmodeTimeoutPeriod = 2000 2 | let zenmodeTimeout 3 | let zenmodeListener 4 | 5 | register_hook('fullscreen-activate', activateZenMode) 6 | register_hook('fullscreen-deactivate', deactivateZenMode) 7 | 8 | const showEvents = ['mousemove', 'touchstart'] 9 | 10 | function activateZenMode () { 11 | showEvents.forEach(ev => 12 | document.querySelector('#map').addEventListener(ev, startZenTimeout) 13 | ) 14 | startZenTimeout() 15 | } 16 | 17 | function startZenTimeout () { 18 | document.body.classList.remove('zenMode') 19 | if (zenmodeTimeout) { 20 | global.clearTimeout(zenmodeTimeout) 21 | } 22 | 23 | zenmodeTimeout = global.setTimeout(startZenMode, zenmodeTimeoutPeriod) 24 | } 25 | 26 | function deactivateZenMode () { 27 | global.clearTimeout(zenmodeTimeout) 28 | showEvents.forEach(ev => 29 | document.querySelector('#map').removeEventListener(ev, startZenTimeout) 30 | ) 31 | document.body.classList.remove('zenMode') 32 | } 33 | 34 | function startZenMode () { 35 | document.body.classList.add('zenMode') 36 | } 37 | -------------------------------------------------------------------------------- /test/getPathFromJSON.js: -------------------------------------------------------------------------------- 1 | const getPathFromJSON = require('../src/getPathFromJSON') 2 | const assert = require('assert') 3 | 4 | describe('getPathFromJSON', function () { 5 | it('const', function () { 6 | assert.deepEqual( 7 | getPathFromJSON('const', { const: { 'foo': 'foo', 'bar': 'bar' } }), 8 | { 'foo': 'foo', 'bar': 'bar' } 9 | ) 10 | }) 11 | 12 | it('const.x', function () { 13 | assert.deepEqual( 14 | getPathFromJSON('const.x', { const: { x: { 'foo': 'foo', 'bar': 'bar' } } }), 15 | { 'foo': 'foo', 'bar': 'bar' } 16 | ) 17 | }) 18 | 19 | it('const.y (not exist)', function () { 20 | assert.deepEqual( 21 | getPathFromJSON('const.y', { const: { x: { 'foo': 'foo', 'bar': 'bar' } } }), 22 | undefined 23 | ) 24 | }) 25 | }) 26 | --------------------------------------------------------------------------------