├── .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 |
100 |
--------------------------------------------------------------------------------
/img/geo-info-bbox-none.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
100 |
--------------------------------------------------------------------------------
/img/geo-info-bbox-nw.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
100 |
--------------------------------------------------------------------------------
/img/geo-info-bbox-se.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
100 |
--------------------------------------------------------------------------------
/img/geo-info-object-center.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
105 |
--------------------------------------------------------------------------------
/img/geo-info-object-none.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
105 |
--------------------------------------------------------------------------------
/img/geo-info-object-nw.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
105 |
--------------------------------------------------------------------------------
/img/geo-info-object-se.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
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 |
--------------------------------------------------------------------------------