Stephan Bösch-Plepelits
5 years ago
39 changed files with 522 additions and 20 deletions
-
70doc/Filters.md
-
3lang/ast.json
-
3lang/ca.json
-
3lang/cs.json
-
3lang/da.json
-
3lang/de.json
-
3lang/el.json
-
3lang/en.json
-
3lang/es.json
-
3lang/et.json
-
3lang/fr.json
-
3lang/hu.json
-
3lang/it.json
-
3lang/ja.json
-
3lang/nl.json
-
3lang/pl.json
-
3lang/pt-br.json
-
3lang/pt.json
-
3lang/ro.json
-
3lang/ru.json
-
3lang/sr.json
-
3lang/template.json
-
3lang/uk.json
-
2lib/modulekit/form
-
8package.json
-
3src/CategoryBase.js
-
4src/CategoryIndex.js
-
58src/CategoryOverpass.js
-
234src/CategoryOverpassFilter.js
-
26src/OpenStreetBrowserLoader.js
-
8src/Repository.js
-
2src/RepositoryDir.php
-
2src/RepositoryGit.php
-
16src/categories.js
-
11src/getPathFromJSON.js
-
1src/index.js
-
2src/tagTranslations.js
-
4style.css
-
25test/getPathFromJSON.js
@ -0,0 +1,70 @@ |
|||
Each category can define a list of filters. This is an additional JSON value with the key "filter", e.g.: |
|||
|
|||
```json |
|||
{ |
|||
"query": { |
|||
"13": "nwr[amenity]" |
|||
}, |
|||
"filter": { |
|||
"type": { |
|||
"name": "{{ keyTrans('amenity') }}", |
|||
"type": "select", |
|||
"values": { |
|||
"bar": { |
|||
"nwr[ |
|||
}, |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
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. |
|||
|
|||
Each filter can define the following values: |
|||
* name: Name of the filter. String, which can make use of twig functions, e.g. `keyTrans` as in the above example. |
|||
* type: A form type, e.g. 'text', 'select', 'radio', 'checkbox' |
|||
* values: Possible values. Can either be an array, an object or a html string with several `<option>` tags (even for radio and checkbox). See below for more information. |
|||
* valueName: if the values do not have names (resp. an `<option>` without text content), use this twig template to create a each name. Use `{{ value }}` for the current value. |
|||
* query: A twig template which builds a query from the selected value (if the value has not a query defined), e.g. `nwr[amenity={{ value }}]`. If not defined the query will be built from `key` (or the filter ID), `op` and the selected value. |
|||
* key: If not overridden by query, use this key for searching. If not defined, use the filter's ID. Can also be an array with a list of keys. You can use wildcards too, e.g. "name:*" to query all localized name tags. |
|||
* op: operator to use (if not overridden by query): |
|||
* '=' exact match (default) |
|||
* '!=' any value nut this |
|||
* '~' regular expression, case sensitive |
|||
* '~i' regular expression, case insensitive |
|||
* '!~', '!~i' regular expression, negated |
|||
* 'has' query in semicolon-separated lists like `cuisine=kebap;noodles` |
|||
* 'strsearch' query string parts (e.g. "kai keb" would match "Kaiser Kebap") and query character variants (e.g. "cafe" would match "café"). |
|||
* show_default: if true, this filter will be shown by default, others need to be added via the select box. |
|||
|
|||
### Values |
|||
#### Array |
|||
Values can either be an array, e.g. |
|||
```json |
|||
[ "bar", "restaurant", ... ] |
|||
``` |
|||
|
|||
#### Object |
|||
Values can be an object, e.g. |
|||
```json |
|||
{ "bar": |
|||
{ |
|||
"name": "Bar", |
|||
"query": "nwr[amenity=bar]" |
|||
} |
|||
} |
|||
``` |
|||
* Name is optional and can be created via `valueName`. |
|||
* Query is optional, it can be created from key (or filter id), op and the value. |
|||
|
|||
#### Twig template |
|||
Values can be a twig template (string). It has access to the `const` part of the category. It can create a list of options: |
|||
|
|||
```html |
|||
{% for k in const %} |
|||
<option value="{{ k }}"></option> |
|||
{% endfor %} |
|||
<option value="restaurant" query="nwr[amenity=restaurant]">Restaurant</option> |
|||
``` |
|||
|
|||
* Name is generated from text content. If it is empty, it can be created via `valueName`. |
|||
* Query is optional, it can be created from key (or filter id), op and the value. |
@ -1 +1 @@ |
|||
Subproject commit 50f1ea5eb822240876ed89504e3180f9c7dcbc05 |
|||
Subproject commit 5198dc8e00d46893bd93ed3b3d71c8ea7994e1c2 |
@ -0,0 +1,234 @@ |
|||
const OverpassLayer = require('overpass-layer') |
|||
const tabs = require('modulekit-tabs') |
|||
|
|||
const state = require('./state') |
|||
const Filter = require('overpass-frontend').Filter |
|||
const getPathFromJSON = require('./getPathFromJSON') |
|||
const CategoryOverpass = require('./CategoryOverpass') |
|||
|
|||
CategoryOverpass.defaultValues.filter = { |
|||
title: { |
|||
type: 'text', |
|||
key: [ 'name', 'name:*', 'operator', 'operator:*', 'ref', 'ref:*' ], |
|||
name: '{{ trans("filter:title") }}', |
|||
op: 'strsearch', |
|||
weight: -1, |
|||
show_default: true |
|||
} |
|||
} |
|||
|
|||
class CategoryOverpassFilter { |
|||
constructor (master) { |
|||
this.master = master |
|||
this.data = this.master.data.filter |
|||
|
|||
this.tabFilter = new tabs.Tab({ |
|||
id: 'filter' |
|||
}) |
|||
this.master.tools.add(this.tabFilter) |
|||
|
|||
this.tabFilter.header.innerHTML = '<i class="fa fa-filter" aria-hidden="true"></i>' |
|||
this.tabFilter.header.title = lang('filter') |
|||
|
|||
this.domFilter = document.createElement('form') |
|||
this.tabFilter.content.appendChild(this.domFilter) |
|||
|
|||
this.tabFilter.on('select', () => { |
|||
this.formFilter.resize() |
|||
this.formFilter.focus() |
|||
}) |
|||
|
|||
for (var k in this.data) { |
|||
let f = this.data[k] |
|||
if ('name' in f && typeof f.name === 'string') { |
|||
global.currentCategory = this.master |
|||
let t = OverpassLayer.twig.twig({ data: f.name, autoescape: true }) |
|||
f.name = t.render({}).toString() |
|||
} else if (!('name' in f)) { |
|||
f.name = lang('tag:' + k) |
|||
} |
|||
|
|||
if ('query' in f) { |
|||
f.queryTemplate = OverpassLayer.twig.twig({ data: f.query, autoescape: false }) |
|||
} |
|||
|
|||
if ('values' in f) { |
|||
let valueNameTemplate = OverpassLayer.twig.twig({ data: f.valueName || '{{ value }}', autoescape: true }) |
|||
|
|||
if (typeof f.values === 'string') { |
|||
let valuesTemplate = OverpassLayer.twig.twig({ data: f.values, autoescape: true }) |
|||
let div = document.createElement('div') |
|||
div.innerHTML = valuesTemplate.render(this.master.data) |
|||
|
|||
let options = div.getElementsByTagName('option') |
|||
f.values = {} |
|||
|
|||
for (let i = 0; i < options.length; i++) { |
|||
let option = options[i] |
|||
|
|||
let k = option.value |
|||
f.values[k] = {} |
|||
|
|||
if (option.textContent) { |
|||
f.values[k].name = option.textContent |
|||
} |
|||
|
|||
if (option.hasAttribute('query')) { |
|||
f.values[k].query = option.getAttribute('query') |
|||
} |
|||
} |
|||
} |
|||
|
|||
if (Array.isArray(f.values) && f.valueName) { |
|||
let newValues = {} |
|||
f.values.forEach(value => { |
|||
newValues[value] = valueNameTemplate.render({ value }).toString() |
|||
}) |
|||
f.values = newValues |
|||
} else if (typeof f.values === 'object') { |
|||
for (var k1 in f.values) { |
|||
if (typeof f.values[k1] === 'string') { |
|||
let t = OverpassLayer.twig.twig({ data: f.values[k1], autoescape: true }) |
|||
f.values[k1] = t.render({}).toString() |
|||
} else if (typeof f.values[k1] === 'object') { |
|||
if (!('name' in f.values[k1])) { |
|||
f.values[k1].name = valueNameTemplate.render({ value: k1 }).toString() |
|||
} else if (f.values[k1].name) { |
|||
let t = OverpassLayer.twig.twig({ data: f.values[k1].name, autoescape: true }) |
|||
f.values[k1].name = t.render({}).toString() |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
let masterOptions = {} |
|||
if (Object.keys(this.data).length > 1) { |
|||
masterOptions = { |
|||
'type': 'form_chooser', |
|||
'button:add_element': '-- ' + lang('add_filter') + ' --', |
|||
'order': false, |
|||
'change_on_input': true |
|||
} |
|||
} |
|||
|
|||
this.formFilter = new form('filter-' + this.master.id, this.data, masterOptions) |
|||
this.formFilter.show(this.domFilter) |
|||
this.formFilter.onchange = function () { |
|||
let param = JSON.parse(JSON.stringify(this.formFilter.get_data())) |
|||
|
|||
this.applyParam(param) |
|||
|
|||
this.master.layer.check_update_map() |
|||
state.update() |
|||
}.bind(this) |
|||
|
|||
this.master.on('setParam', this.setParam.bind(this)) |
|||
this.master.on('applyParam', this.applyParam.bind(this)) |
|||
this.master.on('open', this.openCategory.bind(this)) |
|||
this.master.on('stateGet', this.stateGet.bind(this)) |
|||
} |
|||
|
|||
setParam (param) { |
|||
this.formFilter.set_data(param) |
|||
} |
|||
|
|||
applyParam (param) { |
|||
this.additionalFilter = [] |
|||
let kvFilter = [] |
|||
|
|||
for (var k in param) { |
|||
if (param[k] === null) { |
|||
continue |
|||
} |
|||
|
|||
var d = this.data[k] |
|||
|
|||
if ('values' in d && param[k] in d.values && typeof d.values[param[k]] === 'object' && 'query' in d.values[param[k]]) { |
|||
let f = new Filter(d.values[param[k]].query) |
|||
this.additionalFilter.push(f.def) |
|||
continue |
|||
} else if (d.queryTemplate) { |
|||
let f = new Filter(d.queryTemplate.render({ value: param[k] }).toString()) |
|||
this.additionalFilter.push(f.def) |
|||
continue |
|||
} |
|||
|
|||
var v = { |
|||
key: 'key' in d ? d.key : k, |
|||
value: param[k], |
|||
op: '=' |
|||
} |
|||
|
|||
if ('op' in d) { |
|||
if (d.op === 'has_key_value') { |
|||
v = { |
|||
key: param[k], |
|||
op: 'has_key' |
|||
} |
|||
} else { |
|||
v.op = d.op |
|||
} |
|||
} |
|||
|
|||
if (Array.isArray(v.key)) { |
|||
v = { |
|||
"or": v.key.map( |
|||
key => { |
|||
let v1 = { key, value: v.value, op: v.op } |
|||
|
|||
let m = key.match(/^(.*)\*(.*)/) |
|||
if (m) { |
|||
v1.key = '^' + m[1] + '.*' + m[2] |
|||
v1.keyRegexp = true |
|||
} |
|||
|
|||
return [ v1 ] |
|||
} |
|||
) |
|||
} |
|||
} |
|||
|
|||
kvFilter.push(v) |
|||
} |
|||
|
|||
if (kvFilter.length) { |
|||
this.additionalFilter.push(kvFilter) |
|||
} |
|||
|
|||
if (this.additionalFilter.length === 0) { |
|||
this.additionalFilter = [] |
|||
} else if (this.additionalFilter.length === 1) { |
|||
this.additionalFilter = this.additionalFilter[0] |
|||
} else { |
|||
this.additionalFilter = { and: this.additionalFilter } |
|||
} |
|||
|
|||
this.master.layer.options.queryOptions.filter = this.additionalFilter |
|||
|
|||
if (!this.tabFilter.isSelected()) { |
|||
this.tabFilter.select() |
|||
} |
|||
} |
|||
|
|||
openCategory () { |
|||
this.formFilter.resize() |
|||
} |
|||
|
|||
stateGet (param) { |
|||
let data = this.formFilter.get_data() |
|||
|
|||
for (var k in data) { |
|||
if (data[k]) { |
|||
param[k] = data[k] |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
register_hook('category-overpass-init', (category) => { |
|||
if (category.data.filter) { |
|||
new CategoryOverpassFilter(category) |
|||
} |
|||
}) |
@ -0,0 +1,8 @@ |
|||
module.exports = class Repository { |
|||
constructor (id, data) { |
|||
this.id = id |
|||
this.data = data |
|||
|
|||
this.lang = this.data.lang || {} |
|||
} |
|||
} |
@ -0,0 +1,11 @@ |
|||
module.exports = function getPathFromJSON (path, json) { |
|||
if (typeof path === 'string') { |
|||
path = path.split(/\./) |
|||
} |
|||
|
|||
if (path.length === 0) { |
|||
return json |
|||
} |
|||
|
|||
return getPathFromJSON(path.slice(1), json[path[0]]) |
|||
} |
@ -0,0 +1,25 @@ |
|||
const getPathFromJSON = require('../src/getPathFromJSON') |
|||
const assert = require('assert') |
|||
|
|||
describe('getPathFromJSON', function () { |
|||
it('const', function () { |
|||
assert.deepEqual( |
|||
getPathFromJSON('const', { const: { 'foo': 'foo', 'bar': 'bar' } }), |
|||
{ 'foo': 'foo', 'bar': 'bar' } |
|||
) |
|||
}) |
|||
|
|||
it('const.x', function () { |
|||
assert.deepEqual( |
|||
getPathFromJSON('const.x', { const: { x: { 'foo': 'foo', 'bar': 'bar' } } }), |
|||
{ 'foo': 'foo', 'bar': 'bar' } |
|||
) |
|||
}) |
|||
|
|||
it('const.y (not exist)', function () { |
|||
assert.deepEqual( |
|||
getPathFromJSON('const.y', { const: { x: { 'foo': 'foo', 'bar': 'bar' } } }), |
|||
undefined |
|||
) |
|||
}) |
|||
}) |
Write
Preview
Loading…
Cancel
Save
Reference in new issue