Merge branch 'develop' into bike-theme-duplicate-location

This commit is contained in:
Pieter Vander Vennet 2021-11-07 14:25:09 +01:00 committed by GitHub
commit b4468c676a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
241 changed files with 13493 additions and 7230 deletions

1
.github/FUNDING.yml vendored Normal file
View file

@ -0,0 +1 @@
liberapay: pietervdvn

70
.github/workflows/codeql-analysis.yml vendored Normal file
View file

@ -0,0 +1,70 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ develop, master ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ develop ]
schedule:
- cron: '21 18 * * 3'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'javascript' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://git.io/codeql-language-support
steps:
- name: Checkout repository
uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

View file

@ -37,6 +37,13 @@ jobs:
git clone --depth 1 --single-branch --branch master "https://x-access-token:$DEPLOY_KEY_PIETERVDVN@github.com/pietervdvn/pietervdvn.github.io.git"
echo "Destination repo is cloned"
- name: Sync repo
env:
DEPLOY_KEY_PIETERVDVN: ${{ secrets.DEPLOY_KEY_PIETERVDVN }}
run: |
cd pietervdvn.github.io
git pull
- name: "Copying files"
run: |
echo "Deploying"

View file

@ -15,7 +15,7 @@ export default class SharedTagRenderings {
const d = new Map<string, TagRenderingConfig>()
for (const key of Array.from(configJsons.keys())) {
try {
d.set(key, new TagRenderingConfig(configJsons.get(key), undefined, `SharedTagRenderings.${key}`))
d.set(key, new TagRenderingConfig(configJsons.get(key), `SharedTagRenderings.${key}`))
} catch (e) {
if (!Utils.runningFromConsole) {
console.error("BUG: could not parse", key, " from questions.json or icons.json - this error happened during the build step of the SharedTagRenderings", e)

View file

@ -100,7 +100,7 @@ Adds the time that the data got loaded - pretty much the time of downloading fro
### _last_edit:contributor, _last_edit:contributor:uid, _last_edit:changeset, _last_edit:timestamp, _version_number
### _last_edit:contributor, _last_edit:contributor:uid, _last_edit:changeset, _last_edit:timestamp, _version_number, _backend
@ -109,6 +109,15 @@ Information about the last edit of this object.
### sidewalk:left, sidewalk:right, generic_key:left:property, generic_key:right:property
Rewrites tags from 'generic_key:both:property' as 'generic_key:left:property' and 'generic_key:right:property' (and similar for sidewalk tagging). Note that this rewritten tags _will be reuploaded on a change_. To prevent to much unrelated retagging, this is only enabled if the layer has at least some lineRenderings with offset defined
Calculating tags with Javascript
----------------------------------
@ -157,11 +166,12 @@ The above code will be executed for every feature in the layer. The feature is a
Some advanced functions are available on **feat** as well:
- distanceTo
- overlapWith
- closest
- closestn
- memberships
- [distanceTo](#distanceTo)
- [overlapWith](#overlapWith)
- [closest](#closest)
- [closestn](#closestn)
- [memberships](#memberships)
- [get](#get)
### distanceTo
@ -172,7 +182,10 @@ Some advanced functions are available on **feat** as well:
### overlapWith
Gives a list of features from the specified layer which this feature (partly) overlaps with. If the current feature is a point, all features that embed the point are given. The returned value is `{ feat: GeoJSONFeature, overlap: number}[]` where `overlap` is the overlapping surface are (in m²) for areas, the overlapping length (in meter) if the current feature is a line or `undefined` if the current feature is a point.
Gives a list of features from the specified layer which this feature (partly) overlaps with. A point which is embedded in the feature is detected as well.If the current feature is a point, all features that this point is embeded in are given.
The returned value is `{ feat: GeoJSONFeature, overlap: number}[]` where `overlap` is the overlapping surface are (in m²) for areas, the overlapping length (in meter) if the current feature is a line or `undefined` if the current feature is a point.
The resulting list is sorted in descending order by overlap. The feature with the most overlap will thus be the first in the list
For example to get all objects which overlap or embed from a layer, use `_contained_climbing_routes_properties=feat.overlapWith('climbing_route')`
@ -202,4 +215,10 @@ If a 'unique tag key' is given, the tag with this key will only appear once (e.g
For example: `_part_of_walking_routes=feat.memberships().map(r => r.relation.tags.name).join(';')`
### get
Gets the property of the feature, parses it (as JSON) and returns it. Might return 'undefined' if not defined, null, ...
0. key
Generated from SimpleMetaTagger, ExtraFunction

32
Docs/ContributorRights.md Normal file
View file

@ -0,0 +1,32 @@
Rights of contributors
======================
If a contributor is quite active within MapComplete, this contributor might be granted access to the main repository.
If you have access to the repository, you can make a fork of an already existing branch and push this new branch to github.
This means that this branch will be _automatically built_ and be **deployed** to `https://pietervdvn.github.io/mc/<branchname>`. You can see the deploy process on [Github Actions](https://github.com/pietervdvn/MapComplete/actions).
Don't worry about pushing too much. These deploys are free and totally automatic. They might fail if something is wrong, but this will hinder no-one.
Additionaly, some other maintainer might step in and merge the latest develop with your branch, making later pull requests easier.
Don't worry about bugs
----------------------
As a non-admin contributor, you can _not_ make changes to the `master` nor to the `develop` branch. This is because, as soon as master is changed, this is built and deployed on `mapcomplete.osm.be`, which a lot of people use. An error there will cause a lot of grieve.
A push on `develop` is automatically deployed to [pietervdvn.github.io/mc/develop] and is used by quite some people to. People using this version should know that this is a testing ground for new features and might contain a bug every now and then.
In other words, to get your theme deployed on the main instances, you'll still have to create a pull request. The maintainers will then doublecheck and pull it in.
If you have a local repository
------------------------------
If you have made a fork earlier and have received contributor rights, you need to tell your local git repository that pushing to the main repository is possible.
To do this:
1. type `git remote add upstream git@github.com:pietervdvn/MapComplete`
2. Run `git push upstream` to push your latest changes to the main repo (and not your fork). Running `git push` will push to your fork.
Alternatively, if you don't have any unmerged changes, you can remove your local copy and clone `pietervdvn/MapComplete` again to start fresh.

View file

@ -1,4 +1,8 @@
# Available types for text fields
Available types for text fields
=================================
The listed types here trigger a special input element. Use them in `tagrendering.freeform.type` of your tagrendering to activate them
@ -24,7 +28,44 @@ A geographical length in meters (rounded at two points). Will give an extra mini
## wikidata
A wikidata identifier, e.g. Q42. Input helper arguments: [ key: the value of this tag will initialize search (default: name), options: { removePrefixes: string[], removePostfixes: string[] } these prefixes and postfixes will be removed from the initial search value]
A wikidata identifier, e.g. Q42.
### Helper arguments
name | doc
------ | -----
key | the value of this tag will initialize search (default: name)
options | A JSON-object of type `{ removePrefixes: string[], removePostfixes: string[] }`.
subarg | doc
-------- | -----
removePrefixes | remove these snippets of text from the start of the passed string to search
removePostfixes | remove these snippets of text from the end of the passed string to search
### Example usage
The following is the 'freeform'-part of a layer config which will trigger a search for the wikidata item corresponding with the name of the selected feature. It will also remove '-street', '-square', ... if found at the end of the name
```
"freeform": {
"key": "name:etymology:wikidata",
"type": "wikidata",
"helperArgs": [
"name",
{
"removePostfixes": [
"street",
"boulevard",
"path",
"square",
"plaza",
]
}
]
}
```
## int
@ -60,7 +101,40 @@ A phone number
## opening_hours
Has extra elements to easily input when a POI is opened
Has extra elements to easily input when a POI is opened.
### Helper arguments
name | doc
------ | -----
options | A JSON-object of type `{ prefix: string, postfix: string }`.
subarg | doc
-------- | -----
prefix | Piece of text that will always be added to the front of the generated opening hours. If the OSM-data does not start with this, it will fail to parse
postfix | Piece of text that will always be added to the end of the generated opening hours
### Example usage
To add a conditional (based on time) access restriction:
```
"freeform": {
"key": "access:conditional",
"type": "opening_hours",
"helperArgs": [
{
"prefix":"no @ (",
"postfix":")"
}
]
}
```
*Don't forget to pass the prefix and postfix in the rendering as well*: `{opening_hours_table(opening_hours,yes @ &LPARENS, &RPARENS )`
## color

View file

@ -1,24 +1,53 @@
### Special tag renderings
In a tagrendering, some special values are substituted by an advanced UI-element. This allows advanced features and visualizations to be reused by custom themes or even to query third-party API's. General usage is `{func_name()}`, `{func_name(arg, someotherarg)}` or `{func_name(args):cssStyle}`. Note that you _do not_fcs need to use quotes around your arguments, the comma is enough to separate them. This also implies you cannot use a comma in your args
In a tagrendering, some special values are substituted by an advanced UI-element. This allows advanced features and visualizations to be reused by custom themes or even to query third-party API's.
General usage is `{func_name()}`, `{func_name(arg, someotherarg)}` or `{func_name(args):cssStyle}`. Note that you _do not_ need to use quotes around your arguments, the comma is enough to separate them. This also implies you cannot use a comma in your args
- [all_tags](#all_tags)
- [image_carousel](#image_carousel)
- [image_upload](#image_upload)
- [wikipedia](#wikipedia)
- [minimap](#minimap)
- [sided_minimap](#sided_minimap)
- [reviews](#reviews)
- [opening_hours_table](#opening_hours_table)
- [live](#live)
- [histogram](#histogram)
- [share_link](#share_link)
- [canonical](#canonical)
- [import_button](#import_button)
- [multi_apply](#multi_apply)
- [tag_apply](#tag_apply)
### all_tags
Prints all key-value pairs of the object - used for debugging
#### Example usage
`{all_tags()}`
`{all_tags()}`
### image_carousel
Creates an image carousel for the given sources. An attempt will be made to guess what source is used. Supported: Wikidata identifiers, Wikipedia pages, Wikimedia categories, IMGUR (with attribution, direct links)
name | default | description
------ | --------- | -------------
image key/prefix (multiple values allowed if comma-seperated) | image | The keys given to the images, e.g. if <span class='literal-code'>image</span> is given, the first picture URL will be added as <span class='literal-code'>image</span>, the second as <span class='literal-code'>image:0</span>, the third as <span class='literal-code'>image:1</span>, etc...
image key/prefix (multiple values allowed if comma-seperated) | image,mapillary,image,wikidata,wikimedia_commons,image,image | The keys given to the images, e.g. if <span class='literal-code'>image</span> is given, the first picture URL will be added as <span class='literal-code'>image</span>, the second as <span class='literal-code'>image:0</span>, the third as <span class='literal-code'>image:1</span>, etc...
#### Example usage
`{image_carousel(image)}`
`{image_carousel(image,mapillary,image,wikidata,wikimedia_commons,image,image)}`
### image_upload
Creates a button where a user can upload an image to IMGUR
@ -30,7 +59,9 @@ label | Add image | The text to show on the button
#### Example usage
`{image_upload(image,Add image)}`
`{image_upload(image,Add image)}`
### wikipedia
A box showing the corresponding wikipedia article - based on the wikidata tag
@ -41,10 +72,12 @@ keyToShowWikipediaFor | wikidata | Use the wikidata entry from this key to show
#### Example usage
`{wikipedia()}` is a basic example, `{wikipedia(name:etymology:wikidata)}` to show the wikipedia page of whom the feature was named after. Also remember that these can be styled, e.g. `{wikipedia():max-height: 10rem}` to limit the height
`{wikipedia()}` is a basic example, `{wikipedia(name:etymology:wikidata)}` to show the wikipedia page of whom the feature was named after. Also remember that these can be styled, e.g. `{wikipedia():max-height: 10rem}` to limit the height
### minimap
A small map showing the selected feature. Note that no styling is applied, wrap this in a div
A small map showing the selected feature.
name | default | description
------ | --------- | -------------
@ -53,7 +86,22 @@ idKey | id | (Matches all resting arguments) This argument should be the key of
#### Example usage
`{minimap()}`, `{minimap(17, id, _list_of_embedded_feature_ids_calculated_by_calculated_tag):height:10rem; border: 2px solid black}`
`{minimap()}`, `{minimap(17, id, _list_of_embedded_feature_ids_calculated_by_calculated_tag):height:10rem; border: 2px solid black}`
### sided_minimap
A small map showing _only one side_ the selected feature. *This features requires to have linerenderings with offset* as only linerenderings with a postive or negative offset will be shown. Note: in most cases, this map will be automatically introduced
name | default | description
------ | --------- | -------------
side | _undefined_ | The side to show, either `left` or `right`
#### Example usage
`{sided_minimap(left)}`
### reviews
Adds an overview of the mangrove-reviews of this object. Mangrove.Reviews needs - in order to identify the reviewed object - a coordinate and a name. By default, the name of the object is given, but this can be overwritten
@ -61,11 +109,13 @@ idKey | id | (Matches all resting arguments) This argument should be the key of
name | default | description
------ | --------- | -------------
subjectKey | name | The key to use to determine the subject. If specified, the subject will be <b>tags[subjectKey]</b>
fallback | undefined | The identifier to use, if <i>tags[subjectKey]</i> as specified above is not available. This is effectively a fallback value
fallback | _undefined_ | The identifier to use, if <i>tags[subjectKey]</i> as specified above is not available. This is effectively a fallback value
#### Example usage
`{reviews()}` for a vanilla review, `{reviews(name, play_forest)}` to review a play forest. If a name is known, the name will be used as identifier, otherwise 'play_forest' is used
`{reviews()}` for a vanilla review, `{reviews(name, play_forest)}` to review a play forest. If a name is known, the name will be used as identifier, otherwise 'play_forest' is used
### opening_hours_table
Creates an opening-hours table. Usage: {opening_hours_table(opening_hours)} to create a table of the tag 'opening_hours'.
@ -73,63 +123,77 @@ fallback | undefined | The identifier to use, if <i>tags[subjectKey]</i> as spec
name | default | description
------ | --------- | -------------
key | opening_hours | The tagkey from which the table is constructed.
prefix | _empty string_ | Remove this string from the start of the value before parsing. __Note: use `&LPARENs` to indicate `(` if needed__
postfix | _empty string_ | Remove this string from the end of the value before parsing. __Note: use `&RPARENs` to indicate `)` if needed__
#### Example usage
`{opening_hours_table(opening_hours)}`
A normal opening hours table can be invoked with `{opening_hours_table()}`. A table for e.g. conditional access with opening hours can be `{opening_hours_table(access:conditional, no @ &LPARENS, &RPARENS)}`
### live
Downloads a JSON from the given URL, e.g. '{live(example.org/data.json, shorthand:x.y.z, other:a.b.c, shorthand)}' will download the given file, will create an object {shorthand: json[x][y][z], other: json[a][b][c] out of it and will return 'other' or 'json[a][b][c]. This is made to use in combination with tags, e.g. {live({url}, {url:format}, needed_value)}
name | default | description
------ | --------- | -------------
Url | undefined | The URL to load
Shorthands | undefined | A list of shorthands, of the format 'shorthandname:path.path.path'. separated by ;
path | undefined | The path (or shorthand) that should be returned
Url | _undefined_ | The URL to load
Shorthands | _undefined_ | A list of shorthands, of the format 'shorthandname:path.path.path'. separated by ;
path | _undefined_ | The path (or shorthand) that should be returned
#### Example usage
{live({url},{url:format},hour)} {live(https://data.mobility.brussels/bike/api/counts/?request=live&featureID=CB2105,hour:data.hour_cnt;day:data.day_cnt;year:data.year_cnt,hour)}
{live({url},{url:format},hour)} {live(https://data.mobility.brussels/bike/api/counts/?request=live&featureID=CB2105,hour:data.hour_cnt;day:data.day_cnt;year:data.year_cnt,hour)}
### histogram
Create a histogram for a list of given values, read from the properties.
name | default | description
------ | --------- | -------------
key | undefined | The key to be read and to generate a histogram from
title | | The text to put above the given values column
countHeader | | The text to put above the counts
colors* | undefined | (Matches all resting arguments - optional) Matches a regex onto a color value, e.g. `3[a-zA-Z+-]*:#33cc33`
key | _undefined_ | The key to be read and to generate a histogram from
title | _empty string_ | The text to put above the given values column
countHeader | _empty string_ | The text to put above the counts
colors* | _undefined_ | (Matches all resting arguments - optional) Matches a regex onto a color value, e.g. `3[a-zA-Z+-]*:#33cc33`
#### Example usage
`{histogram('some_key')}` with properties being `{some_key: ['a','b','a','c']} to create a histogram
`{histogram('some_key')}` with properties being `{some_key: ['a','b','a','c']} to create a histogram
### share_link
Creates a link that (attempts to) open the native 'share'-screen
name | default | description
------ | --------- | -------------
url | undefined | The url to share (default: current URL)
url | _undefined_ | The url to share (default: current URL)
#### Example usage
{share_link()} to share the current page, {share_link(<some_url>)} to share the given url
{share_link()} to share the current page, {share_link(<some_url>)} to share the given url
### canonical
Converts a short, canonical value into the long, translated text
name | default | description
------ | --------- | -------------
key | undefined | The key of the tag to give the canonical text for
key | _undefined_ | The key of the tag to give the canonical text for
#### Example usage
{canonical(length)} will give 42 metre (in french)
{canonical(length)} will give 42 metre (in french)
### import_button
This button will copy the data from an external dataset into OpenStreetMap. It is only functional in official themes but can be tested in unofficial themes.
#### Importing a dataset into OpenStreetMap: requirements
If you want to import a dataset, make sure that:
1. The dataset to import has a suitable license
@ -138,36 +202,97 @@ If you want to import a dataset, make sure that:
There are also some technicalities in your theme to keep in mind:
1. The new point will be added and will flow through the program as any other new point as if it came from OSM.
1. The new feature will be added and will flow through the program as any other new point as if it came from OSM.
This means that there should be a layer which will match the new tags and which will display it.
2. The original point from your geojson layer will gain the tag '_imported=yes'.
2. The original feature from your geojson layer will gain the tag '_imported=yes'.
This should be used to change the appearance or even to hide it (eg by changing the icon size to zero)
3. There should be a way for the theme to detect previously imported points, even after reloading.
A reference number to the original dataset is an excellen way to do this
A reference number to the original dataset is an excellent way to do this
4. When importing ways, the theme creator is also responsible of avoiding overlapping ways.
#### Disabled in unofficial themes
The import button can be tested in an unofficial theme by adding `test=true` or `backend=osm-test` as [URL-paramter](URL_Parameters.md).
The import button will show up then. If in testmode, you can read the changeset-XML directly in the web console.
In the case that MapComplete is pointed to the testing grounds, the edit will be made on https://master.apis.dev.openstreetmap.org
#### Specifying which tags to copy or add
The argument `tags` of the import button takes a `;`-seperated list of tags to add.
These can either be a tag to add, such as `amenity=fast_food` or can use a substitution, e.g. `addr:housenumber=$number`.
This new point will then have the tags `amenity=fast_food` and `addr:housenumber` with the value that was saved in `number` in the original feature.
If a value to substitute is undefined, empty string will be used instead.
This supports multiple values, e.g. `ref=$source:geometry:type/$source:geometry:ref`
Remark that the syntax is slightly different then expected; it uses '$' to note a value to copy, followed by a name (matched with `[a-zA-Z0-9_:]*`). Sadly, delimiting with `{}` as these already mark the boundaries of the special rendering...
Note that these values can be prepare with javascript in the theme by using a [calculatedTag](calculatedTags.md#calculating-tags-with-javascript)
name | default | description
------ | --------- | -------------
tags | undefined | Tags to copy-specification. This contains one or more pairs (seperated by a `;`), e.g. `amenity=fast_food; addr:housenumber=$number`. This new point will then have the tags `amenity=fast_food` and `addr:housenumber` with the value that was saved in `number` in the original feature. (Hint: prepare these values, e.g. with calculatedTags)
targetLayer | _undefined_ | The id of the layer where this point should end up. This is not very strict, it will simply result in checking that this layer is shown preventing possible duplicate elements
tags | _undefined_ | The tags to add onto the new object - see specification above
text | Import this data into OpenStreetMap | The text to show on the button
icon | ./assets/svg/addSmall.svg | A nice icon to show in the button
minzoom | 18 | How far the contributor must zoom in before being able to import the point
Snap onto layer(s)/replace geometry with this other way | _undefined_ | - If the value corresponding with this key starts with 'way/' and the feature is a LineString or Polygon, the original OSM-way geometry will be changed to match the new geometry
- If a way of the given layer(s) is closeby, will snap the new point onto this way (similar as preset might snap). To show multiple layers to snap onto, use a `;`-seperated list
snap max distance | 5 | The maximum distance that this point will move to snap onto a layer (in meters)
#### Example usage
`{import_button(,Import this data into OpenStreetMap,./assets/svg/addSmall.svg,18)}`
`{import_button(,,Import this data into OpenStreetMap,./assets/svg/addSmall.svg,18,,5)}`
### multi_apply
A button to apply the tagging of this object onto a list of other features. This is an advanced feature for which you'll need calculatedTags
name | default | description
------ | --------- | -------------
feature_ids | undefined | A JSOn-serialized list of IDs of features to apply the tagging on
keys | undefined | One key (or multiple keys, seperated by ';') of the attribute that should be copied onto the other features.
text | undefined | The text to show on the button
autoapply | undefined | A boolean indicating wether this tagging should be applied automatically if the relevant tags on this object are changed. A visual element indicating the multi_apply is still shown
overwrite | undefined | If set to 'true', the tags on the other objects will always be overwritten. The default behaviour will be to only change the tags on other objects if they are either undefined or had the same value before the change
feature_ids | _undefined_ | A JSOn-serialized list of IDs of features to apply the tagging on
keys | _undefined_ | One key (or multiple keys, seperated by ';') of the attribute that should be copied onto the other features.
text | _undefined_ | The text to show on the button
autoapply | _undefined_ | A boolean indicating wether this tagging should be applied automatically if the relevant tags on this object are changed. A visual element indicating the multi_apply is still shown
overwrite | _undefined_ | If set to 'true', the tags on the other objects will always be overwritten. The default behaviour will be to only change the tags on other objects if they are either undefined or had the same value before the change
#### Example usage
{multi_apply(_features_with_the_same_name_within_100m, name:etymology:wikidata;name:etymology, Apply etymology information on all nearby objects with the same name)} Generated from UI/SpecialVisualisations.ts
{multi_apply(_features_with_the_same_name_within_100m, name:etymology:wikidata;name:etymology, Apply etymology information on all nearby objects with the same name)}
### tag_apply
Shows a big button; clicking this button will apply certain tags onto the feature.
The first argument takes a specification of which tags to add.
These can either be a tag to add, such as `amenity=fast_food` or can use a substitution, e.g. `addr:housenumber=$number`.
This new point will then have the tags `amenity=fast_food` and `addr:housenumber` with the value that was saved in `number` in the original feature.
If a value to substitute is undefined, empty string will be used instead.
This supports multiple values, e.g. `ref=$source:geometry:type/$source:geometry:ref`
Remark that the syntax is slightly different then expected; it uses '$' to note a value to copy, followed by a name (matched with `[a-zA-Z0-9_:]*`). Sadly, delimiting with `{}` as these already mark the boundaries of the special rendering...
Note that these values can be prepare with javascript in the theme by using a [calculatedTag](calculatedTags.md#calculating-tags-with-javascript)
name | default | description
------ | --------- | -------------
tags_to_apply | _undefined_ | A specification of the tags to apply
message | _undefined_ | The text to show to the contributor
image | _undefined_ | An image to show to the contributor on the button
id_of_object_to_apply_this_one | _undefined_ | If specified, applies the the tags onto _another_ object. The id will be read from properties[id_of_object_to_apply_this_one] of the selected object. The tags are still calculated based on the tags of the _selected_ element
#### Example usage
`{tag_apply(survey_date:=$_now:date, Surveyed today!)}` Generated from UI/SpecialVisualisations.ts

View file

@ -112,7 +112,7 @@
},
{
"key": "wheelchair",
"description": "Layer 'Defibrillators' shows wheelchair=designated with a fixed text, namely 'This place is specially adapated for wheelchair users' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Open AED Map')",
"description": "Layer 'Defibrillators' shows wheelchair=designated with a fixed text, namely 'This place is specially adapted for wheelchair users' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Open AED Map')",
"value": "designated"
},
{

View file

@ -103,7 +103,7 @@
},
{
"key": "wheelchair",
"description": "Layer 'Cafés and pubs' shows wheelchair=designated with a fixed text, namely 'This place is specially adapated for wheelchair users' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cafés and pubs')",
"description": "Layer 'Cafés and pubs' shows wheelchair=designated with a fixed text, namely 'This place is specially adapted for wheelchair users' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cafés and pubs')",
"value": "designated"
},
{

View file

@ -48,7 +48,7 @@
},
{
"key": "bicycle",
"description": "Layer 'Charging stations' shows bicycle=yes with a fixed text, namely '<b>bicycles</b> can be charged here' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"description": "Layer 'Charging stations' shows bicycle=yes with a fixed text, namely '<b>Bcycles</b> can be charged here' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "yes"
},
{
@ -92,7 +92,7 @@
},
{
"key": "access",
"description": "Layer 'Charging stations' shows access=customers with a fixed text, namely 'Only customers of the place this station belongs to can use this charging station<br/><span class='subtle'>E.g. a charging station operated by hotel which is only usable by their guests</span> ' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"description": "Layer 'Charging stations' shows access=customers with a fixed text, namely 'Only customers of the place this station belongs to can use this charging station<br/><span class='subtle'>E.g. a charging station operated by hotel which is only usable by their guests</span>' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "customers"
},
{
@ -252,677 +252,66 @@
"key": "socket:schuko",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:schuko' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:schuko:voltage",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:schuko:voltage' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:socket:schuko:voltage",
"description": "Layer 'Charging stations' shows socket:socket:schuko:voltage=230 V with a fixed text, namely '<div style='display: inline-block'><b><b>Schuko wall plug</b> without ground pin (CEE7/4 type F)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/CEE7_4F.svg'/></div> outputs 230 volt' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "230 V"
},
{
"key": "socket:schuko:current",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:schuko:current' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:socket:schuko:current",
"description": "Layer 'Charging stations' shows socket:socket:schuko:current=16 A with a fixed text, namely '<div style='display: inline-block'><b><b>Schuko wall plug</b> without ground pin (CEE7/4 type F)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/CEE7_4F.svg'/></div> outputs at most 16 A' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "16 A"
},
{
"key": "socket:schuko:output",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:schuko:output' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:socket:schuko:output",
"description": "Layer 'Charging stations' shows socket:socket:schuko:output=3.6 kw with a fixed text, namely '<div style='display: inline-block'><b><b>Schuko wall plug</b> without ground pin (CEE7/4 type F)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/CEE7_4F.svg'/></div> outputs at most 3.6 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "3.6 kw"
},
{
"key": "socket:typee",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:typee' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:typee:voltage",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:typee:voltage' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:socket:typee:voltage",
"description": "Layer 'Charging stations' shows socket:socket:typee:voltage=230 V with a fixed text, namely '<div style='display: inline-block'><b><b>European wall plug</b> with ground pin (CEE7/4 type E)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/TypeE.svg'/></div> outputs 230 volt' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "230 V"
},
{
"key": "socket:typee:current",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:typee:current' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:socket:typee:current",
"description": "Layer 'Charging stations' shows socket:socket:typee:current=16 A with a fixed text, namely '<div style='display: inline-block'><b><b>European wall plug</b> with ground pin (CEE7/4 type E)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/TypeE.svg'/></div> outputs at most 16 A' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "16 A"
},
{
"key": "socket:typee:output",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:typee:output' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:socket:typee:output",
"description": "Layer 'Charging stations' shows socket:socket:typee:output=3 kw with a fixed text, namely '<div style='display: inline-block'><b><b>European wall plug</b> with ground pin (CEE7/4 type E)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/TypeE.svg'/></div> outputs at most 3 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "3 kw"
},
{
"key": "socket:socket:typee:output",
"description": "Layer 'Charging stations' shows socket:socket:typee:output=22 kw with a fixed text, namely '<div style='display: inline-block'><b><b>European wall plug</b> with ground pin (CEE7/4 type E)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/TypeE.svg'/></div> outputs at most 22 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "22 kw"
},
{
"key": "socket:chademo",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:chademo' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:chademo:voltage",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:chademo:voltage' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:socket:chademo:voltage",
"description": "Layer 'Charging stations' shows socket:socket:chademo:voltage=500 V with a fixed text, namely '<div style='display: inline-block'><b><b>Chademo</b></b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Chademo_type4.svg'/></div> outputs 500 volt' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "500 V"
},
{
"key": "socket:chademo:current",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:chademo:current' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:socket:chademo:current",
"description": "Layer 'Charging stations' shows socket:socket:chademo:current=120 A with a fixed text, namely '<div style='display: inline-block'><b><b>Chademo</b></b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Chademo_type4.svg'/></div> outputs at most 120 A' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "120 A"
},
{
"key": "socket:chademo:output",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:chademo:output' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:socket:chademo:output",
"description": "Layer 'Charging stations' shows socket:socket:chademo:output=50 kw with a fixed text, namely '<div style='display: inline-block'><b><b>Chademo</b></b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Chademo_type4.svg'/></div> outputs at most 50 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "50 kw"
},
{
"key": "socket:type1_cable",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:type1_cable' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:type1_cable:voltage",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:type1_cable:voltage' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:socket:type1_cable:voltage",
"description": "Layer 'Charging stations' shows socket:socket:type1_cable:voltage=200 V with a fixed text, namely '<div style='display: inline-block'><b><b>Type 1 with cable</b> (J1772)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type1_J1772.svg'/></div> outputs 200 volt' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "200 V"
},
{
"key": "socket:socket:type1_cable:voltage",
"description": "Layer 'Charging stations' shows socket:socket:type1_cable:voltage=240 V with a fixed text, namely '<div style='display: inline-block'><b><b>Type 1 with cable</b> (J1772)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type1_J1772.svg'/></div> outputs 240 volt' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "240 V"
},
{
"key": "socket:type1_cable:current",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:type1_cable:current' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:socket:type1_cable:current",
"description": "Layer 'Charging stations' shows socket:socket:type1_cable:current=32 A with a fixed text, namely '<div style='display: inline-block'><b><b>Type 1 with cable</b> (J1772)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type1_J1772.svg'/></div> outputs at most 32 A' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "32 A"
},
{
"key": "socket:type1_cable:output",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:type1_cable:output' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:socket:type1_cable:output",
"description": "Layer 'Charging stations' shows socket:socket:type1_cable:output=3.7 kw with a fixed text, namely '<div style='display: inline-block'><b><b>Type 1 with cable</b> (J1772)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type1_J1772.svg'/></div> outputs at most 3.7 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "3.7 kw"
},
{
"key": "socket:socket:type1_cable:output",
"description": "Layer 'Charging stations' shows socket:socket:type1_cable:output=7 kw with a fixed text, namely '<div style='display: inline-block'><b><b>Type 1 with cable</b> (J1772)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type1_J1772.svg'/></div> outputs at most 7 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "7 kw"
},
{
"key": "socket:type1",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:type1' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:type1:voltage",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:type1:voltage' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:socket:type1:voltage",
"description": "Layer 'Charging stations' shows socket:socket:type1:voltage=200 V with a fixed text, namely '<div style='display: inline-block'><b><b>Type 1 <i>without</i> cable</b> (J1772)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type1_J1772.svg'/></div> outputs 200 volt' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "200 V"
},
{
"key": "socket:socket:type1:voltage",
"description": "Layer 'Charging stations' shows socket:socket:type1:voltage=240 V with a fixed text, namely '<div style='display: inline-block'><b><b>Type 1 <i>without</i> cable</b> (J1772)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type1_J1772.svg'/></div> outputs 240 volt' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "240 V"
},
{
"key": "socket:type1:current",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:type1:current' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:socket:type1:current",
"description": "Layer 'Charging stations' shows socket:socket:type1:current=32 A with a fixed text, namely '<div style='display: inline-block'><b><b>Type 1 <i>without</i> cable</b> (J1772)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type1_J1772.svg'/></div> outputs at most 32 A' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "32 A"
},
{
"key": "socket:type1:output",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:type1:output' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:socket:type1:output",
"description": "Layer 'Charging stations' shows socket:socket:type1:output=3.7 kw with a fixed text, namely '<div style='display: inline-block'><b><b>Type 1 <i>without</i> cable</b> (J1772)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type1_J1772.svg'/></div> outputs at most 3.7 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "3.7 kw"
},
{
"key": "socket:socket:type1:output",
"description": "Layer 'Charging stations' shows socket:socket:type1:output=6.6 kw with a fixed text, namely '<div style='display: inline-block'><b><b>Type 1 <i>without</i> cable</b> (J1772)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type1_J1772.svg'/></div> outputs at most 6.6 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "6.6 kw"
},
{
"key": "socket:socket:type1:output",
"description": "Layer 'Charging stations' shows socket:socket:type1:output=7 kw with a fixed text, namely '<div style='display: inline-block'><b><b>Type 1 <i>without</i> cable</b> (J1772)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type1_J1772.svg'/></div> outputs at most 7 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "7 kw"
},
{
"key": "socket:socket:type1:output",
"description": "Layer 'Charging stations' shows socket:socket:type1:output=7.2 kw with a fixed text, namely '<div style='display: inline-block'><b><b>Type 1 <i>without</i> cable</b> (J1772)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type1_J1772.svg'/></div> outputs at most 7.2 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "7.2 kw"
},
{
"key": "socket:type1_combo",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:type1_combo' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:type1_combo:voltage",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:type1_combo:voltage' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:socket:type1_combo:voltage",
"description": "Layer 'Charging stations' shows socket:socket:type1_combo:voltage=400 V with a fixed text, namely '<div style='display: inline-block'><b><b>Type 1 CCS</b> (aka Type 1 Combo)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type1-ccs.svg'/></div> outputs 400 volt' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "400 V"
},
{
"key": "socket:socket:type1_combo:voltage",
"description": "Layer 'Charging stations' shows socket:socket:type1_combo:voltage=1000 V with a fixed text, namely '<div style='display: inline-block'><b><b>Type 1 CCS</b> (aka Type 1 Combo)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type1-ccs.svg'/></div> outputs 1000 volt' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "1000 V"
},
{
"key": "socket:type1_combo:current",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:type1_combo:current' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:socket:type1_combo:current",
"description": "Layer 'Charging stations' shows socket:socket:type1_combo:current=50 A with a fixed text, namely '<div style='display: inline-block'><b><b>Type 1 CCS</b> (aka Type 1 Combo)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type1-ccs.svg'/></div> outputs at most 50 A' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "50 A"
},
{
"key": "socket:socket:type1_combo:current",
"description": "Layer 'Charging stations' shows socket:socket:type1_combo:current=125 A with a fixed text, namely '<div style='display: inline-block'><b><b>Type 1 CCS</b> (aka Type 1 Combo)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type1-ccs.svg'/></div> outputs at most 125 A' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "125 A"
},
{
"key": "socket:type1_combo:output",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:type1_combo:output' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:socket:type1_combo:output",
"description": "Layer 'Charging stations' shows socket:socket:type1_combo:output=50 kw with a fixed text, namely '<div style='display: inline-block'><b><b>Type 1 CCS</b> (aka Type 1 Combo)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type1-ccs.svg'/></div> outputs at most 50 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "50 kw"
},
{
"key": "socket:socket:type1_combo:output",
"description": "Layer 'Charging stations' shows socket:socket:type1_combo:output=62.5 kw with a fixed text, namely '<div style='display: inline-block'><b><b>Type 1 CCS</b> (aka Type 1 Combo)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type1-ccs.svg'/></div> outputs at most 62.5 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "62.5 kw"
},
{
"key": "socket:socket:type1_combo:output",
"description": "Layer 'Charging stations' shows socket:socket:type1_combo:output=150 kw with a fixed text, namely '<div style='display: inline-block'><b><b>Type 1 CCS</b> (aka Type 1 Combo)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type1-ccs.svg'/></div> outputs at most 150 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "150 kw"
},
{
"key": "socket:socket:type1_combo:output",
"description": "Layer 'Charging stations' shows socket:socket:type1_combo:output=350 kw with a fixed text, namely '<div style='display: inline-block'><b><b>Type 1 CCS</b> (aka Type 1 Combo)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type1-ccs.svg'/></div> outputs at most 350 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "350 kw"
},
{
"key": "socket:tesla_supercharger",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:tesla_supercharger' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:tesla_supercharger:voltage",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:tesla_supercharger:voltage' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:socket:tesla_supercharger:voltage",
"description": "Layer 'Charging stations' shows socket:socket:tesla_supercharger:voltage=480 V with a fixed text, namely '<div style='display: inline-block'><b><b>Tesla Supercharger</b></b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Tesla-hpwc-model-s.svg'/></div> outputs 480 volt' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "480 V"
},
{
"key": "socket:tesla_supercharger:current",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:tesla_supercharger:current' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:socket:tesla_supercharger:current",
"description": "Layer 'Charging stations' shows socket:socket:tesla_supercharger:current=125 A with a fixed text, namely '<div style='display: inline-block'><b><b>Tesla Supercharger</b></b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Tesla-hpwc-model-s.svg'/></div> outputs at most 125 A' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "125 A"
},
{
"key": "socket:socket:tesla_supercharger:current",
"description": "Layer 'Charging stations' shows socket:socket:tesla_supercharger:current=350 A with a fixed text, namely '<div style='display: inline-block'><b><b>Tesla Supercharger</b></b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Tesla-hpwc-model-s.svg'/></div> outputs at most 350 A' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "350 A"
},
{
"key": "socket:tesla_supercharger:output",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:tesla_supercharger:output' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:socket:tesla_supercharger:output",
"description": "Layer 'Charging stations' shows socket:socket:tesla_supercharger:output=120 kw with a fixed text, namely '<div style='display: inline-block'><b><b>Tesla Supercharger</b></b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Tesla-hpwc-model-s.svg'/></div> outputs at most 120 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "120 kw"
},
{
"key": "socket:socket:tesla_supercharger:output",
"description": "Layer 'Charging stations' shows socket:socket:tesla_supercharger:output=150 kw with a fixed text, namely '<div style='display: inline-block'><b><b>Tesla Supercharger</b></b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Tesla-hpwc-model-s.svg'/></div> outputs at most 150 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "150 kw"
},
{
"key": "socket:socket:tesla_supercharger:output",
"description": "Layer 'Charging stations' shows socket:socket:tesla_supercharger:output=250 kw with a fixed text, namely '<div style='display: inline-block'><b><b>Tesla Supercharger</b></b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Tesla-hpwc-model-s.svg'/></div> outputs at most 250 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "250 kw"
},
{
"key": "socket:type2",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:type2' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:type2:voltage",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:type2:voltage' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:socket:type2:voltage",
"description": "Layer 'Charging stations' shows socket:socket:type2:voltage=230 V with a fixed text, namely '<div style='display: inline-block'><b><b>Type 2</b> (mennekes)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type2_socket.svg'/></div> outputs 230 volt' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "230 V"
},
{
"key": "socket:socket:type2:voltage",
"description": "Layer 'Charging stations' shows socket:socket:type2:voltage=400 V with a fixed text, namely '<div style='display: inline-block'><b><b>Type 2</b> (mennekes)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type2_socket.svg'/></div> outputs 400 volt' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "400 V"
},
{
"key": "socket:type2:current",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:type2:current' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:socket:type2:current",
"description": "Layer 'Charging stations' shows socket:socket:type2:current=16 A with a fixed text, namely '<div style='display: inline-block'><b><b>Type 2</b> (mennekes)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type2_socket.svg'/></div> outputs at most 16 A' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "16 A"
},
{
"key": "socket:socket:type2:current",
"description": "Layer 'Charging stations' shows socket:socket:type2:current=32 A with a fixed text, namely '<div style='display: inline-block'><b><b>Type 2</b> (mennekes)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type2_socket.svg'/></div> outputs at most 32 A' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "32 A"
},
{
"key": "socket:type2:output",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:type2:output' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:socket:type2:output",
"description": "Layer 'Charging stations' shows socket:socket:type2:output=11 kw with a fixed text, namely '<div style='display: inline-block'><b><b>Type 2</b> (mennekes)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type2_socket.svg'/></div> outputs at most 11 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "11 kw"
},
{
"key": "socket:socket:type2:output",
"description": "Layer 'Charging stations' shows socket:socket:type2:output=22 kw with a fixed text, namely '<div style='display: inline-block'><b><b>Type 2</b> (mennekes)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type2_socket.svg'/></div> outputs at most 22 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "22 kw"
},
{
"key": "socket:type2_combo",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:type2_combo' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:type2_combo:voltage",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:type2_combo:voltage' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:socket:type2_combo:voltage",
"description": "Layer 'Charging stations' shows socket:socket:type2_combo:voltage=500 V with a fixed text, namely '<div style='display: inline-block'><b><b>Type 2 CCS</b> (mennekes)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type2_CCS.svg'/></div> outputs 500 volt' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "500 V"
},
{
"key": "socket:socket:type2_combo:voltage",
"description": "Layer 'Charging stations' shows socket:socket:type2_combo:voltage=920 V with a fixed text, namely '<div style='display: inline-block'><b><b>Type 2 CCS</b> (mennekes)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type2_CCS.svg'/></div> outputs 920 volt' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "920 V"
},
{
"key": "socket:type2_combo:current",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:type2_combo:current' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:socket:type2_combo:current",
"description": "Layer 'Charging stations' shows socket:socket:type2_combo:current=125 A with a fixed text, namely '<div style='display: inline-block'><b><b>Type 2 CCS</b> (mennekes)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type2_CCS.svg'/></div> outputs at most 125 A' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "125 A"
},
{
"key": "socket:socket:type2_combo:current",
"description": "Layer 'Charging stations' shows socket:socket:type2_combo:current=350 A with a fixed text, namely '<div style='display: inline-block'><b><b>Type 2 CCS</b> (mennekes)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type2_CCS.svg'/></div> outputs at most 350 A' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "350 A"
},
{
"key": "socket:type2_combo:output",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:type2_combo:output' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:socket:type2_combo:output",
"description": "Layer 'Charging stations' shows socket:socket:type2_combo:output=50 kw with a fixed text, namely '<div style='display: inline-block'><b><b>Type 2 CCS</b> (mennekes)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type2_CCS.svg'/></div> outputs at most 50 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "50 kw"
},
{
"key": "socket:type2_cable",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:type2_cable' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:type2_cable:voltage",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:type2_cable:voltage' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:socket:type2_cable:voltage",
"description": "Layer 'Charging stations' shows socket:socket:type2_cable:voltage=230 V with a fixed text, namely '<div style='display: inline-block'><b><b>Type 2 with cable</b> (mennekes)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type2_tethered.svg'/></div> outputs 230 volt' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "230 V"
},
{
"key": "socket:socket:type2_cable:voltage",
"description": "Layer 'Charging stations' shows socket:socket:type2_cable:voltage=400 V with a fixed text, namely '<div style='display: inline-block'><b><b>Type 2 with cable</b> (mennekes)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type2_tethered.svg'/></div> outputs 400 volt' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "400 V"
},
{
"key": "socket:type2_cable:current",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:type2_cable:current' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:socket:type2_cable:current",
"description": "Layer 'Charging stations' shows socket:socket:type2_cable:current=16 A with a fixed text, namely '<div style='display: inline-block'><b><b>Type 2 with cable</b> (mennekes)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type2_tethered.svg'/></div> outputs at most 16 A' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "16 A"
},
{
"key": "socket:socket:type2_cable:current",
"description": "Layer 'Charging stations' shows socket:socket:type2_cable:current=32 A with a fixed text, namely '<div style='display: inline-block'><b><b>Type 2 with cable</b> (mennekes)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type2_tethered.svg'/></div> outputs at most 32 A' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "32 A"
},
{
"key": "socket:type2_cable:output",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:type2_cable:output' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:socket:type2_cable:output",
"description": "Layer 'Charging stations' shows socket:socket:type2_cable:output=11 kw with a fixed text, namely '<div style='display: inline-block'><b><b>Type 2 with cable</b> (mennekes)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type2_tethered.svg'/></div> outputs at most 11 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "11 kw"
},
{
"key": "socket:socket:type2_cable:output",
"description": "Layer 'Charging stations' shows socket:socket:type2_cable:output=22 kw with a fixed text, namely '<div style='display: inline-block'><b><b>Type 2 with cable</b> (mennekes)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type2_tethered.svg'/></div> outputs at most 22 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "22 kw"
},
{
"key": "socket:tesla_supercharger_ccs",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:tesla_supercharger_ccs' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:tesla_supercharger_ccs:voltage",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:tesla_supercharger_ccs:voltage' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:socket:tesla_supercharger_ccs:voltage",
"description": "Layer 'Charging stations' shows socket:socket:tesla_supercharger_ccs:voltage=500 V with a fixed text, namely '<div style='display: inline-block'><b><b>Tesla Supercharger CCS</b> (a branded type2_css)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type2_CCS.svg'/></div> outputs 500 volt' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "500 V"
},
{
"key": "socket:socket:tesla_supercharger_ccs:voltage",
"description": "Layer 'Charging stations' shows socket:socket:tesla_supercharger_ccs:voltage=920 V with a fixed text, namely '<div style='display: inline-block'><b><b>Tesla Supercharger CCS</b> (a branded type2_css)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type2_CCS.svg'/></div> outputs 920 volt' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "920 V"
},
{
"key": "socket:tesla_supercharger_ccs:current",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:tesla_supercharger_ccs:current' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:socket:tesla_supercharger_ccs:current",
"description": "Layer 'Charging stations' shows socket:socket:tesla_supercharger_ccs:current=125 A with a fixed text, namely '<div style='display: inline-block'><b><b>Tesla Supercharger CCS</b> (a branded type2_css)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type2_CCS.svg'/></div> outputs at most 125 A' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "125 A"
},
{
"key": "socket:socket:tesla_supercharger_ccs:current",
"description": "Layer 'Charging stations' shows socket:socket:tesla_supercharger_ccs:current=350 A with a fixed text, namely '<div style='display: inline-block'><b><b>Tesla Supercharger CCS</b> (a branded type2_css)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type2_CCS.svg'/></div> outputs at most 350 A' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "350 A"
},
{
"key": "socket:tesla_supercharger_ccs:output",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:tesla_supercharger_ccs:output' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:socket:tesla_supercharger_ccs:output",
"description": "Layer 'Charging stations' shows socket:socket:tesla_supercharger_ccs:output=50 kw with a fixed text, namely '<div style='display: inline-block'><b><b>Tesla Supercharger CCS</b> (a branded type2_css)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type2_CCS.svg'/></div> outputs at most 50 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "50 kw"
},
{
"key": "socket:tesla_destination",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:tesla_destination' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:tesla_destination:voltage",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:tesla_destination:voltage' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:socket:tesla_destination:voltage",
"description": "Layer 'Charging stations' shows socket:socket:tesla_destination:voltage=480 V with a fixed text, namely '<div style='display: inline-block'><b><b>Tesla Supercharger (destination)</b></b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Tesla-hpwc-model-s.svg'/></div> outputs 480 volt' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "480 V"
},
{
"key": "socket:tesla_destination:current",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:tesla_destination:current' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:socket:tesla_destination:current",
"description": "Layer 'Charging stations' shows socket:socket:tesla_destination:current=125 A with a fixed text, namely '<div style='display: inline-block'><b><b>Tesla Supercharger (destination)</b></b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Tesla-hpwc-model-s.svg'/></div> outputs at most 125 A' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "125 A"
},
{
"key": "socket:socket:tesla_destination:current",
"description": "Layer 'Charging stations' shows socket:socket:tesla_destination:current=350 A with a fixed text, namely '<div style='display: inline-block'><b><b>Tesla Supercharger (destination)</b></b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Tesla-hpwc-model-s.svg'/></div> outputs at most 350 A' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "350 A"
},
{
"key": "socket:tesla_destination:output",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:tesla_destination:output' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:socket:tesla_destination:output",
"description": "Layer 'Charging stations' shows socket:socket:tesla_destination:output=120 kw with a fixed text, namely '<div style='display: inline-block'><b><b>Tesla Supercharger (destination)</b></b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Tesla-hpwc-model-s.svg'/></div> outputs at most 120 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "120 kw"
},
{
"key": "socket:socket:tesla_destination:output",
"description": "Layer 'Charging stations' shows socket:socket:tesla_destination:output=150 kw with a fixed text, namely '<div style='display: inline-block'><b><b>Tesla Supercharger (destination)</b></b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Tesla-hpwc-model-s.svg'/></div> outputs at most 150 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "150 kw"
},
{
"key": "socket:socket:tesla_destination:output",
"description": "Layer 'Charging stations' shows socket:socket:tesla_destination:output=250 kw with a fixed text, namely '<div style='display: inline-block'><b><b>Tesla Supercharger (destination)</b></b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Tesla-hpwc-model-s.svg'/></div> outputs at most 250 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "250 kw"
},
{
"key": "socket:tesla_destination",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:tesla_destination' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:tesla_destination:voltage",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:tesla_destination:voltage' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:socket:tesla_destination:voltage",
"description": "Layer 'Charging stations' shows socket:socket:tesla_destination:voltage=230 V with a fixed text, namely '<div style='display: inline-block'><b><b>Tesla supercharger (destination</b> (A Type 2 with cable branded as tesla)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type2_tethered.svg'/></div> outputs 230 volt' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "230 V"
},
{
"key": "socket:socket:tesla_destination:voltage",
"description": "Layer 'Charging stations' shows socket:socket:tesla_destination:voltage=400 V with a fixed text, namely '<div style='display: inline-block'><b><b>Tesla supercharger (destination</b> (A Type 2 with cable branded as tesla)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type2_tethered.svg'/></div> outputs 400 volt' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "400 V"
},
{
"key": "socket:tesla_destination:current",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:tesla_destination:current' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:socket:tesla_destination:current",
"description": "Layer 'Charging stations' shows socket:socket:tesla_destination:current=16 A with a fixed text, namely '<div style='display: inline-block'><b><b>Tesla supercharger (destination</b> (A Type 2 with cable branded as tesla)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type2_tethered.svg'/></div> outputs at most 16 A' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "16 A"
},
{
"key": "socket:socket:tesla_destination:current",
"description": "Layer 'Charging stations' shows socket:socket:tesla_destination:current=32 A with a fixed text, namely '<div style='display: inline-block'><b><b>Tesla supercharger (destination</b> (A Type 2 with cable branded as tesla)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type2_tethered.svg'/></div> outputs at most 32 A' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "32 A"
},
{
"key": "socket:tesla_destination:output",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:tesla_destination:output' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:socket:tesla_destination:output",
"description": "Layer 'Charging stations' shows socket:socket:tesla_destination:output=11 kw with a fixed text, namely '<div style='display: inline-block'><b><b>Tesla supercharger (destination</b> (A Type 2 with cable branded as tesla)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type2_tethered.svg'/></div> outputs at most 11 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "11 kw"
},
{
"key": "socket:socket:tesla_destination:output",
"description": "Layer 'Charging stations' shows socket:socket:tesla_destination:output=22 kw with a fixed text, namely '<div style='display: inline-block'><b><b>Tesla supercharger (destination</b> (A Type 2 with cable branded as tesla)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type2_tethered.svg'/></div> outputs at most 22 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "22 kw"
},
{
"key": "socket:USB-A",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:USB-A' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:USB-A:voltage",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:USB-A:voltage' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:socket:USB-A:voltage",
"description": "Layer 'Charging stations' shows socket:socket:USB-A:voltage=5 V with a fixed text, namely '<div style='display: inline-block'><b><b>USB</b> to charge phones and small electronics</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/usb_port.svg'/></div> outputs 5 volt' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "5 V"
},
{
"key": "socket:USB-A:current",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:USB-A:current' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:socket:USB-A:current",
"description": "Layer 'Charging stations' shows socket:socket:USB-A:current=1 A with a fixed text, namely '<div style='display: inline-block'><b><b>USB</b> to charge phones and small electronics</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/usb_port.svg'/></div> outputs at most 1 A' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "1 A"
},
{
"key": "socket:socket:USB-A:current",
"description": "Layer 'Charging stations' shows socket:socket:USB-A:current=2 A with a fixed text, namely '<div style='display: inline-block'><b><b>USB</b> to charge phones and small electronics</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/usb_port.svg'/></div> outputs at most 2 A' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "2 A"
},
{
"key": "socket:USB-A:output",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:USB-A:output' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:socket:USB-A:output",
"description": "Layer 'Charging stations' shows socket:socket:USB-A:output=5w with a fixed text, namely '<div style='display: inline-block'><b><b>USB</b> to charge phones and small electronics</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/usb_port.svg'/></div> outputs at most 5w' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "5w"
},
{
"key": "socket:socket:USB-A:output",
"description": "Layer 'Charging stations' shows socket:socket:USB-A:output=10w with a fixed text, namely '<div style='display: inline-block'><b><b>USB</b> to charge phones and small electronics</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/usb_port.svg'/></div> outputs at most 10w' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "10w"
},
{
"key": "socket:bosch_3pin",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:bosch_3pin' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:bosch_3pin:voltage",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:bosch_3pin:voltage' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:bosch_3pin:current",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:bosch_3pin:current' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:bosch_3pin:output",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:bosch_3pin:output' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:bosch_5pin",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:bosch_5pin' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:bosch_5pin:voltage",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:bosch_5pin:voltage' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:bosch_5pin:current",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:bosch_5pin:current' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:bosch_5pin:output",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:bosch_5pin:output' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "authentication:membership_card",
"description": "Layer 'Charging stations' shows authentication:membership_card=yes with a fixed text, namely 'Authentication by a membership card' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "yes"
},
{
"key": "authentication:app",
"description": "Layer 'Charging stations' shows authentication:app=yes with a fixed text, namely 'Authentication by an app' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "yes"
},
{
"key": "authentication:phone_call",
"description": "Layer 'Charging stations' shows authentication:phone_call=yes with a fixed text, namely 'Authentication via phone call is available' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "yes"
},
{
"key": "authentication:short_message",
"description": "Layer 'Charging stations' shows authentication:short_message=yes with a fixed text, namely 'Authentication via phone call is available' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "yes"
},
{
"key": "authentication:nfc",
"description": "Layer 'Charging stations' shows authentication:nfc=yes with a fixed text, namely 'Authentication via NFC is available' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "yes"
},
{
"key": "authentication:money_card",
"description": "Layer 'Charging stations' shows authentication:money_card=yes with a fixed text, namely 'Authentication via Money Card is available' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "yes"
},
{
"key": "authentication:debit_card",
"description": "Layer 'Charging stations' shows authentication:debit_card=yes with a fixed text, namely 'Authentication via debit card is available' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "yes"
},
{
"key": "authentication:none",
"description": "Layer 'Charging stations' shows authentication:none=yes with a fixed text, namely 'No authentication is needed' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "yes"
},
{
"key": "authentication:phone_call:number",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'authentication:phone_call:number' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "opening_hours",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'opening_hours' (in the MapComplete.osm.be theme 'Charging stations')"
@ -932,20 +321,75 @@
"description": "Layer 'Charging stations' shows opening_hours=24/7 with a fixed text, namely '24/7 opened (including holidays)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "24/7"
},
{
"key": "charge",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'charge' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "fee",
"description": "Layer 'Charging stations' shows fee=no&charge= with a fixed text, namely 'Free to use' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"description": "Layer 'Charging stations' shows fee=no with a fixed text, namely 'Free to use' (in the MapComplete.osm.be theme 'Charging stations')",
"value": "no"
},
{
"key": "charge",
"description": "Layer 'Charging stations' shows fee=no&charge= with a fixed text, namely 'Free to use' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations') Picking this answer will delete the key charge.",
"key": "fee",
"description": "Layer 'Charging stations' shows fee=no&fee:conditional=&charge=&authentication:none=yes with a fixed text, namely 'Free to use (without authenticating)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "no"
},
{
"key": "fee:conditional",
"description": "Layer 'Charging stations' shows fee=no&fee:conditional=&charge=&authentication:none=yes with a fixed text, namely 'Free to use (without authenticating)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations') Picking this answer will delete the key fee:conditional.",
"value": ""
},
{
"key": "charge",
"description": "Layer 'Charging stations' shows fee=no&fee:conditional=&charge=&authentication:none=yes with a fixed text, namely 'Free to use (without authenticating)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations') Picking this answer will delete the key charge.",
"value": ""
},
{
"key": "authentication:none",
"description": "Layer 'Charging stations' shows fee=no&fee:conditional=&charge=&authentication:none=yes with a fixed text, namely 'Free to use (without authenticating)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "yes"
},
{
"key": "fee",
"description": "Layer 'Charging stations' shows fee=no&fee:conditional=&charge=&authentication:none=no with a fixed text, namely 'Free to use, but one has to authenticate' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "no"
},
{
"key": "fee:conditional",
"description": "Layer 'Charging stations' shows fee=no&fee:conditional=&charge=&authentication:none=no with a fixed text, namely 'Free to use, but one has to authenticate' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations') Picking this answer will delete the key fee:conditional.",
"value": ""
},
{
"key": "charge",
"description": "Layer 'Charging stations' shows fee=no&fee:conditional=&charge=&authentication:none=no with a fixed text, namely 'Free to use, but one has to authenticate' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations') Picking this answer will delete the key charge.",
"value": ""
},
{
"key": "authentication:none",
"description": "Layer 'Charging stations' shows fee=no&fee:conditional=&charge=&authentication:none=no with a fixed text, namely 'Free to use, but one has to authenticate' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "no"
},
{
"key": "fee",
"description": "Layer 'Charging stations' shows fee=yes&fee:conditional=no @ customers with a fixed text, namely 'Paid use, but free for customers of the hotel/pub/hospital/... who operates the charging station' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "yes"
},
{
"key": "fee:conditional",
"description": "Layer 'Charging stations' shows fee=yes&fee:conditional=no @ customers with a fixed text, namely 'Paid use, but free for customers of the hotel/pub/hospital/... who operates the charging station' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "no @ customers"
},
{
"key": "fee",
"description": "Layer 'Charging stations' shows fee=yes&fee:conditional= with a fixed text, namely 'Paid use' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "yes"
},
{
"key": "fee:conditional",
"description": "Layer 'Charging stations' shows fee=yes&fee:conditional= with a fixed text, namely 'Paid use' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations') Picking this answer will delete the key fee:conditional.",
"value": ""
},
{
"key": "charge",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'charge' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "payment:cash",
"description": "Layer 'Charging stations' shows payment:cash=yes with a fixed text, namely 'Cash is accepted here' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
@ -966,6 +410,50 @@
"description": "Layer 'Charging stations' shows payment:membership_card=yes with a fixed text, namely 'Payment is done using a membership card' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "yes"
},
{
"key": "authentication:membership_card",
"description": "Layer 'Charging stations' shows authentication:membership_card=yes with a fixed text, namely 'Authentication by a membership card' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "yes"
},
{
"key": "authentication:app",
"description": "Layer 'Charging stations' shows authentication:app=yes with a fixed text, namely 'Authentication by an app' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "yes"
},
{
"key": "authentication:phone_call",
"description": "Layer 'Charging stations' shows authentication:phone_call=yes with a fixed text, namely 'Authentication via phone call is available' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "yes"
},
{
"key": "authentication:short_message",
"description": "Layer 'Charging stations' shows authentication:short_message=yes with a fixed text, namely 'Authentication via SMS is available' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "yes"
},
{
"key": "authentication:nfc",
"description": "Layer 'Charging stations' shows authentication:nfc=yes with a fixed text, namely 'Authentication via NFC is available' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "yes"
},
{
"key": "authentication:money_card",
"description": "Layer 'Charging stations' shows authentication:money_card=yes with a fixed text, namely 'Authentication via Money Card is available' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "yes"
},
{
"key": "authentication:debit_card",
"description": "Layer 'Charging stations' shows authentication:debit_card=yes with a fixed text, namely 'Authentication via debit card is available' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "yes"
},
{
"key": "authentication:none",
"description": "Layer 'Charging stations' shows authentication:none=yes with a fixed text, namely 'Charging here is (also) possible without authentication' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "yes"
},
{
"key": "authentication:phone_call:number",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'authentication:phone_call:number' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "maxstay",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'maxstay' (in the MapComplete.osm.be theme 'Charging stations')"
@ -1053,49 +541,129 @@
"key": "ref",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'ref' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "operational_status",
"description": "Layer 'Charging stations' shows operational_status=broken with a fixed text, namely 'This charging station is broken' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "broken"
},
{
"key": "planned:amenity",
"description": "Layer 'Charging stations' shows planned:amenity=charging_station&amenity= with a fixed text, namely 'A charging station is planned here' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "charging_station"
},
{
"key": "amenity",
"description": "Layer 'Charging stations' shows planned:amenity=charging_station&amenity= with a fixed text, namely 'A charging station is planned here' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations') Picking this answer will delete the key amenity.",
"description": "Layer 'Charging stations' shows planned:amenity=&construction:amenity=&disused:amenity=&operational_status=&amenity=charging_station with a fixed text, namely 'This charging station works' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations') Picking this answer will delete the key planned:amenity.",
"value": ""
},
{
"key": "construction:amenity",
"description": "Layer 'Charging stations' shows construction:amenity=charging_station&amenity= with a fixed text, namely 'A charging station is constructed here' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "charging_station"
},
{
"key": "amenity",
"description": "Layer 'Charging stations' shows construction:amenity=charging_station&amenity= with a fixed text, namely 'A charging station is constructed here' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations') Picking this answer will delete the key amenity.",
"description": "Layer 'Charging stations' shows planned:amenity=&construction:amenity=&disused:amenity=&operational_status=&amenity=charging_station with a fixed text, namely 'This charging station works' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations') Picking this answer will delete the key construction:amenity.",
"value": ""
},
{
"key": "disused:amenity",
"description": "Layer 'Charging stations' shows disused:amenity=charging_station&amenity= with a fixed text, namely 'This charging station has beed permanently disabled and is not in use anymore but is still visible' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "charging_station"
"description": "Layer 'Charging stations' shows planned:amenity=&construction:amenity=&disused:amenity=&operational_status=&amenity=charging_station with a fixed text, namely 'This charging station works' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations') Picking this answer will delete the key disused:amenity.",
"value": ""
},
{
"key": "amenity",
"description": "Layer 'Charging stations' shows disused:amenity=charging_station&amenity= with a fixed text, namely 'This charging station has beed permanently disabled and is not in use anymore but is still visible' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations') Picking this answer will delete the key amenity.",
"key": "operational_status",
"description": "Layer 'Charging stations' shows planned:amenity=&construction:amenity=&disused:amenity=&operational_status=&amenity=charging_station with a fixed text, namely 'This charging station works' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations') Picking this answer will delete the key operational_status.",
"value": ""
},
{
"key": "amenity",
"description": "Layer 'Charging stations' shows amenity=charging_station&operational_status= with a fixed text, namely 'This charging station works' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"description": "Layer 'Charging stations' shows planned:amenity=&construction:amenity=&disused:amenity=&operational_status=&amenity=charging_station with a fixed text, namely 'This charging station works' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "charging_station"
},
{
"key": "planned:amenity",
"description": "Layer 'Charging stations' shows planned:amenity=&construction:amenity=&disused:amenity=&operational_status=broken&amenity=charging_station with a fixed text, namely 'This charging station is broken' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations') Picking this answer will delete the key planned:amenity.",
"value": ""
},
{
"key": "construction:amenity",
"description": "Layer 'Charging stations' shows planned:amenity=&construction:amenity=&disused:amenity=&operational_status=broken&amenity=charging_station with a fixed text, namely 'This charging station is broken' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations') Picking this answer will delete the key construction:amenity.",
"value": ""
},
{
"key": "disused:amenity",
"description": "Layer 'Charging stations' shows planned:amenity=&construction:amenity=&disused:amenity=&operational_status=broken&amenity=charging_station with a fixed text, namely 'This charging station is broken' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations') Picking this answer will delete the key disused:amenity.",
"value": ""
},
{
"key": "operational_status",
"description": "Layer 'Charging stations' shows planned:amenity=&construction:amenity=&disused:amenity=&operational_status=broken&amenity=charging_station with a fixed text, namely 'This charging station is broken' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "broken"
},
{
"key": "amenity",
"description": "Layer 'Charging stations' shows planned:amenity=&construction:amenity=&disused:amenity=&operational_status=broken&amenity=charging_station with a fixed text, namely 'This charging station is broken' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "charging_station"
},
{
"key": "planned:amenity",
"description": "Layer 'Charging stations' shows planned:amenity=charging_station&construction:amenity=&disused:amenity=&operational_status=&amenity= with a fixed text, namely 'A charging station is planned here' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "charging_station"
},
{
"key": "construction:amenity",
"description": "Layer 'Charging stations' shows planned:amenity=charging_station&construction:amenity=&disused:amenity=&operational_status=&amenity= with a fixed text, namely 'A charging station is planned here' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations') Picking this answer will delete the key construction:amenity.",
"value": ""
},
{
"key": "disused:amenity",
"description": "Layer 'Charging stations' shows planned:amenity=charging_station&construction:amenity=&disused:amenity=&operational_status=&amenity= with a fixed text, namely 'A charging station is planned here' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations') Picking this answer will delete the key disused:amenity.",
"value": ""
},
{
"key": "operational_status",
"description": "Layer 'Charging stations' shows planned:amenity=charging_station&construction:amenity=&disused:amenity=&operational_status=&amenity= with a fixed text, namely 'A charging station is planned here' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations') Picking this answer will delete the key operational_status.",
"value": ""
},
{
"key": "amenity",
"description": "Layer 'Charging stations' shows planned:amenity=charging_station&construction:amenity=&disused:amenity=&operational_status=&amenity= with a fixed text, namely 'A charging station is planned here' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations') Picking this answer will delete the key amenity.",
"value": ""
},
{
"key": "planned:amenity",
"description": "Layer 'Charging stations' shows planned:amenity=&construction:amenity=charging_station&disused:amenity=&operational_status=&amenity= with a fixed text, namely 'A charging station is constructed here' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations') Picking this answer will delete the key planned:amenity.",
"value": ""
},
{
"key": "construction:amenity",
"description": "Layer 'Charging stations' shows planned:amenity=&construction:amenity=charging_station&disused:amenity=&operational_status=&amenity= with a fixed text, namely 'A charging station is constructed here' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "charging_station"
},
{
"key": "disused:amenity",
"description": "Layer 'Charging stations' shows planned:amenity=&construction:amenity=charging_station&disused:amenity=&operational_status=&amenity= with a fixed text, namely 'A charging station is constructed here' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations') Picking this answer will delete the key disused:amenity.",
"value": ""
},
{
"key": "operational_status",
"description": "Layer 'Charging stations' shows planned:amenity=&construction:amenity=charging_station&disused:amenity=&operational_status=&amenity= with a fixed text, namely 'A charging station is constructed here' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations') Picking this answer will delete the key operational_status.",
"value": ""
},
{
"key": "amenity",
"description": "Layer 'Charging stations' shows planned:amenity=&construction:amenity=charging_station&disused:amenity=&operational_status=&amenity= with a fixed text, namely 'A charging station is constructed here' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations') Picking this answer will delete the key amenity.",
"value": ""
},
{
"key": "planned:amenity",
"description": "Layer 'Charging stations' shows planned:amenity=&construction:amenity=&disused:amenity=charging_station&operational_status=&amenity= with a fixed text, namely 'This charging station has beed permanently disabled and is not in use anymore but is still visible' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations') Picking this answer will delete the key planned:amenity.",
"value": ""
},
{
"key": "construction:amenity",
"description": "Layer 'Charging stations' shows planned:amenity=&construction:amenity=&disused:amenity=charging_station&operational_status=&amenity= with a fixed text, namely 'This charging station has beed permanently disabled and is not in use anymore but is still visible' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations') Picking this answer will delete the key construction:amenity.",
"value": ""
},
{
"key": "disused:amenity",
"description": "Layer 'Charging stations' shows planned:amenity=&construction:amenity=&disused:amenity=charging_station&operational_status=&amenity= with a fixed text, namely 'This charging station has beed permanently disabled and is not in use anymore but is still visible' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "charging_station"
},
{
"key": "operational_status",
"description": "Layer 'Charging stations' shows amenity=charging_station&operational_status= with a fixed text, namely 'This charging station works' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations') Picking this answer will delete the key operational_status.",
"description": "Layer 'Charging stations' shows planned:amenity=&construction:amenity=&disused:amenity=charging_station&operational_status=&amenity= with a fixed text, namely 'This charging station has beed permanently disabled and is not in use anymore but is still visible' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations') Picking this answer will delete the key operational_status.",
"value": ""
},
{
"key": "amenity",
"description": "Layer 'Charging stations' shows planned:amenity=&construction:amenity=&disused:amenity=charging_station&operational_status=&amenity= with a fixed text, namely 'This charging station has beed permanently disabled and is not in use anymore but is still visible' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations') Picking this answer will delete the key amenity.",
"value": ""
},
{

View file

@ -194,6 +194,26 @@
"key": "wikipedia",
"description": "The layer 'Climbing gyms allows to upload images and adds them under the 'image'-tag (and image:0, image:1, ... for multiple images). Furhtermore, this layer shows images based on the keys image, wikidata, wikipedia, wikimedia_commons and mapillary"
},
{
"key": "name",
"description": "Layer 'Climbing gyms' shows and asks freeform values for key 'name' (in the MapComplete.osm.be theme 'Open Climbing Map')"
},
{
"key": "website",
"description": "Layer 'Climbing gyms' shows and asks freeform values for key 'website' (in the MapComplete.osm.be theme 'Open Climbing Map')"
},
{
"key": "phone",
"description": "Layer 'Climbing gyms' shows and asks freeform values for key 'phone' (in the MapComplete.osm.be theme 'Open Climbing Map')"
},
{
"key": "email",
"description": "Layer 'Climbing gyms' shows and asks freeform values for key 'email' (in the MapComplete.osm.be theme 'Open Climbing Map')"
},
{
"key": "opening_hours",
"description": "Layer 'Climbing gyms' shows and asks freeform values for key 'opening_hours' (in the MapComplete.osm.be theme 'Open Climbing Map')"
},
{
"key": "url",
"description": "Layer 'Climbing gyms' shows and asks freeform values for key 'url' (in the MapComplete.osm.be theme 'Open Climbing Map')"
@ -314,26 +334,6 @@
"key": "climbing:speed",
"description": "Layer 'Climbing gyms' shows climbing:speed~^..*$ with a fixed text, namely 'There are {climbing:speed} speed climbing walls' (in the MapComplete.osm.be theme 'Open Climbing Map')"
},
{
"key": "name",
"description": "Layer 'Climbing gyms' shows and asks freeform values for key 'name' (in the MapComplete.osm.be theme 'Open Climbing Map')"
},
{
"key": "website",
"description": "Layer 'Climbing gyms' shows and asks freeform values for key 'website' (in the MapComplete.osm.be theme 'Open Climbing Map')"
},
{
"key": "phone",
"description": "Layer 'Climbing gyms' shows and asks freeform values for key 'phone' (in the MapComplete.osm.be theme 'Open Climbing Map')"
},
{
"key": "email",
"description": "Layer 'Climbing gyms' shows and asks freeform values for key 'email' (in the MapComplete.osm.be theme 'Open Climbing Map')"
},
{
"key": "opening_hours",
"description": "Layer 'Climbing gyms' shows and asks freeform values for key 'opening_hours' (in the MapComplete.osm.be theme 'Open Climbing Map')"
},
{
"key": "climbing",
"description": "The MapComplete theme Open Climbing Map has a layer Climbing routes showing features with this tag",
@ -355,6 +355,46 @@
"key": "wikipedia",
"description": "The layer 'Climbing routes allows to upload images and adds them under the 'image'-tag (and image:0, image:1, ... for multiple images). Furhtermore, this layer shows images based on the keys image, wikidata, wikipedia, wikimedia_commons and mapillary"
},
{
"key": "name",
"description": "Layer 'Climbing routes' shows and asks freeform values for key 'name' (in the MapComplete.osm.be theme 'Open Climbing Map')"
},
{
"key": "noname",
"description": "Layer 'Climbing routes' shows noname=yes&name= with a fixed text, namely 'This climbing route doesn't have a name' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Open Climbing Map')",
"value": "yes"
},
{
"key": "name",
"description": "Layer 'Climbing routes' shows noname=yes&name= with a fixed text, namely 'This climbing route doesn't have a name' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Open Climbing Map') Picking this answer will delete the key name.",
"value": ""
},
{
"key": "climbing:length",
"description": "Layer 'Climbing routes' shows and asks freeform values for key 'climbing:length' (in the MapComplete.osm.be theme 'Open Climbing Map')"
},
{
"key": "climbing:grade:french",
"description": "Layer 'Climbing routes' shows and asks freeform values for key 'climbing:grade:french' (in the MapComplete.osm.be theme 'Open Climbing Map')"
},
{
"key": "climbing:bolts",
"description": "Layer 'Climbing routes' shows and asks freeform values for key 'climbing:bolts' (in the MapComplete.osm.be theme 'Open Climbing Map')"
},
{
"key": "climbing:bolted",
"description": "Layer 'Climbing routes' shows climbing:bolted=no with a fixed text, namely 'This route is not bolted' (in the MapComplete.osm.be theme 'Open Climbing Map')",
"value": "no"
},
{
"key": "climbing:bolted",
"description": "Layer 'Climbing routes' shows climbing:bolted=no&climbing:bolts= with a fixed text, namely 'This route is not bolted' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Open Climbing Map')",
"value": "no&climbing:bolts="
},
{
"key": "description",
"description": "Layer 'Climbing routes' shows and asks freeform values for key 'description' (in the MapComplete.osm.be theme 'Open Climbing Map')"
},
{
"key": "url",
"description": "Layer 'Climbing routes' shows and asks freeform values for key 'url' (in the MapComplete.osm.be theme 'Open Climbing Map')"
@ -475,46 +515,6 @@
"key": "climbing:speed",
"description": "Layer 'Climbing routes' shows climbing:speed~^..*$ with a fixed text, namely 'There are {climbing:speed} speed climbing walls' (in the MapComplete.osm.be theme 'Open Climbing Map')"
},
{
"key": "name",
"description": "Layer 'Climbing routes' shows and asks freeform values for key 'name' (in the MapComplete.osm.be theme 'Open Climbing Map')"
},
{
"key": "noname",
"description": "Layer 'Climbing routes' shows noname=yes&name= with a fixed text, namely 'This climbing route doesn't have a name' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Open Climbing Map')",
"value": "yes"
},
{
"key": "name",
"description": "Layer 'Climbing routes' shows noname=yes&name= with a fixed text, namely 'This climbing route doesn't have a name' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Open Climbing Map') Picking this answer will delete the key name.",
"value": ""
},
{
"key": "climbing:length",
"description": "Layer 'Climbing routes' shows and asks freeform values for key 'climbing:length' (in the MapComplete.osm.be theme 'Open Climbing Map')"
},
{
"key": "climbing:grade:french",
"description": "Layer 'Climbing routes' shows and asks freeform values for key 'climbing:grade:french' (in the MapComplete.osm.be theme 'Open Climbing Map')"
},
{
"key": "climbing:bolts",
"description": "Layer 'Climbing routes' shows and asks freeform values for key 'climbing:bolts' (in the MapComplete.osm.be theme 'Open Climbing Map')"
},
{
"key": "climbing:bolted",
"description": "Layer 'Climbing routes' shows climbing:bolted=no with a fixed text, namely 'This route is not bolted' (in the MapComplete.osm.be theme 'Open Climbing Map')",
"value": "no"
},
{
"key": "climbing:bolted",
"description": "Layer 'Climbing routes' shows climbing:bolted=no&climbing:bolts= with a fixed text, namely 'This route is not bolted' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Open Climbing Map')",
"value": "no&climbing:bolts="
},
{
"key": "description",
"description": "Layer 'Climbing routes' shows and asks freeform values for key 'description' (in the MapComplete.osm.be theme 'Open Climbing Map')"
},
{
"key": "sport",
"description": "The MapComplete theme Open Climbing Map has a layer Climbing opportunities showing features with this tag",
@ -536,6 +536,44 @@
"key": "wikipedia",
"description": "The layer 'Climbing opportunities allows to upload images and adds them under the 'image'-tag (and image:0, image:1, ... for multiple images). Furhtermore, this layer shows images based on the keys image, wikidata, wikipedia, wikimedia_commons and mapillary"
},
{
"key": "name",
"description": "Layer 'Climbing opportunities' shows and asks freeform values for key 'name' (in the MapComplete.osm.be theme 'Open Climbing Map')"
},
{
"key": "noname",
"description": "Layer 'Climbing opportunities' shows noname=yes&name= with a fixed text, namely 'This climbing opportunity doesn't have a name' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Open Climbing Map')",
"value": "yes"
},
{
"key": "name",
"description": "Layer 'Climbing opportunities' shows noname=yes&name= with a fixed text, namely 'This climbing opportunity doesn't have a name' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Open Climbing Map') Picking this answer will delete the key name.",
"value": ""
},
{
"key": "climbing",
"description": "Layer 'Climbing opportunities' shows climbing=boulder with a fixed text, namely 'A climbing boulder - a single rock or cliff with one or a few climbing routes which can be climbed safely without rope' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Open Climbing Map')",
"value": "boulder"
},
{
"key": "climbing",
"description": "Layer 'Climbing opportunities' shows climbing=crag with a fixed text, namely 'A climbing crag - a single rock or cliff with at least a few climbing routes' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Open Climbing Map')",
"value": "crag"
},
{
"key": "climbing",
"description": "Layer 'Climbing opportunities' shows climbing=area with a fixed text, namely 'A climbing area with one or more climbing crags and/or boulders' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Open Climbing Map')",
"value": "area"
},
{
"key": "rock",
"description": "Layer 'Climbing opportunities' shows and asks freeform values for key 'rock' (in the MapComplete.osm.be theme 'Open Climbing Map')"
},
{
"key": "rock",
"description": "Layer 'Climbing opportunities' shows rock=limestone with a fixed text, namely 'Limestone' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Open Climbing Map')",
"value": "limestone"
},
{
"key": "url",
"description": "Layer 'Climbing opportunities' shows and asks freeform values for key 'url' (in the MapComplete.osm.be theme 'Open Climbing Map')"
@ -656,44 +694,6 @@
"key": "climbing:speed",
"description": "Layer 'Climbing opportunities' shows climbing:speed~^..*$ with a fixed text, namely 'There are {climbing:speed} speed climbing walls' (in the MapComplete.osm.be theme 'Open Climbing Map')"
},
{
"key": "name",
"description": "Layer 'Climbing opportunities' shows and asks freeform values for key 'name' (in the MapComplete.osm.be theme 'Open Climbing Map')"
},
{
"key": "noname",
"description": "Layer 'Climbing opportunities' shows noname=yes&name= with a fixed text, namely 'This climbing opportunity doesn't have a name' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Open Climbing Map')",
"value": "yes"
},
{
"key": "name",
"description": "Layer 'Climbing opportunities' shows noname=yes&name= with a fixed text, namely 'This climbing opportunity doesn't have a name' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Open Climbing Map') Picking this answer will delete the key name.",
"value": ""
},
{
"key": "climbing",
"description": "Layer 'Climbing opportunities' shows climbing=boulder with a fixed text, namely 'A climbing boulder - a single rock or cliff with one or a few climbing routes which can be climbed safely without rope' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Open Climbing Map')",
"value": "boulder"
},
{
"key": "climbing",
"description": "Layer 'Climbing opportunities' shows climbing=crag with a fixed text, namely 'A climbing crag - a single rock or cliff with at least a few climbing routes' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Open Climbing Map')",
"value": "crag"
},
{
"key": "climbing",
"description": "Layer 'Climbing opportunities' shows climbing=area with a fixed text, namely 'A climbing area with one or more climbing crags and/or boulders' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Open Climbing Map')",
"value": "area"
},
{
"key": "rock",
"description": "Layer 'Climbing opportunities' shows and asks freeform values for key 'rock' (in the MapComplete.osm.be theme 'Open Climbing Map')"
},
{
"key": "rock",
"description": "Layer 'Climbing opportunities' shows rock=limestone with a fixed text, namely 'Limestone' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Open Climbing Map')",
"value": "limestone"
},
{
"key": "leisure",
"description": "The MapComplete theme Open Climbing Map has a layer Climbing opportunities? showing features with this tag",

View file

@ -116,25 +116,6 @@
"key": "opening_hours",
"description": "Layer 'Bike cafe' shows and asks freeform values for key 'opening_hours' (in the MapComplete.osm.be theme 'Cyclofix - an open map for cyclists')"
},
{
"key": "service:bicycle:cleaning:charge",
"description": "Layer 'Bike cafe' shows and asks freeform values for key 'service:bicycle:cleaning:charge' (in the MapComplete.osm.be theme 'Cyclofix - an open map for cyclists')"
},
{
"key": "service:bicycle:cleaning:fee",
"description": "Layer 'Bike cafe' shows service:bicycle:cleaning:fee=no&service:bicycle:cleaning:charge= with a fixed text, namely 'The cleaning service is free to use' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclofix - an open map for cyclists')",
"value": "no&service:bicycle:cleaning:charge="
},
{
"key": "service:bicycle:cleaning:fee",
"description": "Layer 'Bike cafe' shows service:bicycle:cleaning:fee=no& with a fixed text, namely 'Free to use' (in the MapComplete.osm.be theme 'Cyclofix - an open map for cyclists')",
"value": "no&"
},
{
"key": "service:bicycle:cleaning:fee",
"description": "Layer 'Bike cafe' shows service:bicycle:cleaning:fee=yes with a fixed text, namely 'The cleaning service has a fee' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclofix - an open map for cyclists')",
"value": "yes"
},
{
"key": "shop",
"description": "The MapComplete theme Cyclofix - an open map for cyclists has a layer Bike repair/shop showing features with this tag",
@ -417,25 +398,6 @@
"key": "description",
"description": "Layer 'Bicycle library' shows and asks freeform values for key 'description' (in the MapComplete.osm.be theme 'Cyclofix - an open map for cyclists')"
},
{
"key": "service:bicycle:cleaning:charge",
"description": "Layer 'Bicycle library' shows and asks freeform values for key 'service:bicycle:cleaning:charge' (in the MapComplete.osm.be theme 'Cyclofix - an open map for cyclists')"
},
{
"key": "service:bicycle:cleaning:fee",
"description": "Layer 'Bicycle library' shows service:bicycle:cleaning:fee=no&service:bicycle:cleaning:charge= with a fixed text, namely 'The cleaning service is free to use' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclofix - an open map for cyclists')",
"value": "no&service:bicycle:cleaning:charge="
},
{
"key": "service:bicycle:cleaning:fee",
"description": "Layer 'Bicycle library' shows service:bicycle:cleaning:fee=no& with a fixed text, namely 'Free to use' (in the MapComplete.osm.be theme 'Cyclofix - an open map for cyclists')",
"value": "no&"
},
{
"key": "service:bicycle:cleaning:fee",
"description": "Layer 'Bicycle library' shows service:bicycle:cleaning:fee=yes with a fixed text, namely 'The cleaning service has a fee' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclofix - an open map for cyclists')",
"value": "yes"
},
{
"key": "amenity",
"description": "The MapComplete theme Cyclofix - an open map for cyclists has a layer Bike stations (repair, pump or both) showing features with this tag",
@ -616,25 +578,6 @@
"description": "Layer 'Bike stations (repair, pump or both)' shows level=1 with a fixed text, namely 'Located on the first floor' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclofix - an open map for cyclists')",
"value": "1"
},
{
"key": "service:bicycle:cleaning:charge",
"description": "Layer 'Bike stations (repair, pump or both)' shows and asks freeform values for key 'service:bicycle:cleaning:charge' (in the MapComplete.osm.be theme 'Cyclofix - an open map for cyclists')"
},
{
"key": "service:bicycle:cleaning:fee",
"description": "Layer 'Bike stations (repair, pump or both)' shows service:bicycle:cleaning:fee=no&service:bicycle:cleaning:charge= with a fixed text, namely 'The cleaning service is free to use' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclofix - an open map for cyclists')",
"value": "no&service:bicycle:cleaning:charge="
},
{
"key": "service:bicycle:cleaning:fee",
"description": "Layer 'Bike stations (repair, pump or both)' shows service:bicycle:cleaning:fee=no& with a fixed text, namely 'Free to use' (in the MapComplete.osm.be theme 'Cyclofix - an open map for cyclists')",
"value": "no&"
},
{
"key": "service:bicycle:cleaning:fee",
"description": "Layer 'Bike stations (repair, pump or both)' shows service:bicycle:cleaning:fee=yes with a fixed text, namely 'The cleaning service has a fee' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclofix - an open map for cyclists')",
"value": "yes"
},
{
"key": "amenity",
"description": "The MapComplete theme Cyclofix - an open map for cyclists has a layer Bicycle tube vending machine showing features with this tag",
@ -751,25 +694,6 @@
"description": "Layer 'Bicycle tube vending machine' shows vending:bicycle_lock=yes with a fixed text, namely 'Bicycle locks are sold here' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclofix - an open map for cyclists')",
"value": "yes"
},
{
"key": "service:bicycle:cleaning:charge",
"description": "Layer 'Bicycle tube vending machine' shows and asks freeform values for key 'service:bicycle:cleaning:charge' (in the MapComplete.osm.be theme 'Cyclofix - an open map for cyclists')"
},
{
"key": "service:bicycle:cleaning:fee",
"description": "Layer 'Bicycle tube vending machine' shows service:bicycle:cleaning:fee=no&service:bicycle:cleaning:charge= with a fixed text, namely 'The cleaning service is free to use' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclofix - an open map for cyclists')",
"value": "no&service:bicycle:cleaning:charge="
},
{
"key": "service:bicycle:cleaning:fee",
"description": "Layer 'Bicycle tube vending machine' shows service:bicycle:cleaning:fee=no& with a fixed text, namely 'Free to use' (in the MapComplete.osm.be theme 'Cyclofix - an open map for cyclists')",
"value": "no&"
},
{
"key": "service:bicycle:cleaning:fee",
"description": "Layer 'Bicycle tube vending machine' shows service:bicycle:cleaning:fee=yes with a fixed text, namely 'The cleaning service has a fee' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclofix - an open map for cyclists')",
"value": "yes"
},
{
"key": "amenity",
"description": "The MapComplete theme Cyclofix - an open map for cyclists has a layer Drinking water showing features with this tag",
@ -820,25 +744,6 @@
"description": "Layer 'Drinking water' shows bottle=no with a fixed text, namely 'Water bottles may not fit' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclofix - an open map for cyclists')",
"value": "no"
},
{
"key": "service:bicycle:cleaning:charge",
"description": "Layer 'Drinking water' shows and asks freeform values for key 'service:bicycle:cleaning:charge' (in the MapComplete.osm.be theme 'Cyclofix - an open map for cyclists')"
},
{
"key": "service:bicycle:cleaning:fee",
"description": "Layer 'Drinking water' shows service:bicycle:cleaning:fee=no&service:bicycle:cleaning:charge= with a fixed text, namely 'The cleaning service is free to use' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclofix - an open map for cyclists')",
"value": "no&service:bicycle:cleaning:charge="
},
{
"key": "service:bicycle:cleaning:fee",
"description": "Layer 'Drinking water' shows service:bicycle:cleaning:fee=no& with a fixed text, namely 'Free to use' (in the MapComplete.osm.be theme 'Cyclofix - an open map for cyclists')",
"value": "no&"
},
{
"key": "service:bicycle:cleaning:fee",
"description": "Layer 'Drinking water' shows service:bicycle:cleaning:fee=yes with a fixed text, namely 'The cleaning service has a fee' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclofix - an open map for cyclists')",
"value": "yes"
},
{
"key": "theme",
"description": "The MapComplete theme Cyclofix - an open map for cyclists has a layer Bike related object showing features with this tag",
@ -920,25 +825,6 @@
"key": "opening_hours",
"description": "Layer 'Bike related object' shows and asks freeform values for key 'opening_hours' (in the MapComplete.osm.be theme 'Cyclofix - an open map for cyclists')"
},
{
"key": "service:bicycle:cleaning:charge",
"description": "Layer 'Bike related object' shows and asks freeform values for key 'service:bicycle:cleaning:charge' (in the MapComplete.osm.be theme 'Cyclofix - an open map for cyclists')"
},
{
"key": "service:bicycle:cleaning:fee",
"description": "Layer 'Bike related object' shows service:bicycle:cleaning:fee=no&service:bicycle:cleaning:charge= with a fixed text, namely 'The cleaning service is free to use' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclofix - an open map for cyclists')",
"value": "no&service:bicycle:cleaning:charge="
},
{
"key": "service:bicycle:cleaning:fee",
"description": "Layer 'Bike related object' shows service:bicycle:cleaning:fee=no& with a fixed text, namely 'Free to use' (in the MapComplete.osm.be theme 'Cyclofix - an open map for cyclists')",
"value": "no&"
},
{
"key": "service:bicycle:cleaning:fee",
"description": "Layer 'Bike related object' shows service:bicycle:cleaning:fee=yes with a fixed text, namely 'The cleaning service has a fee' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclofix - an open map for cyclists')",
"value": "yes"
},
{
"key": "service:bicycle:cleaning",
"description": "The MapComplete theme Cyclofix - an open map for cyclists has a layer Bike cleaning service showing features with this tag",
@ -1149,25 +1035,6 @@
{
"key": "capacity:cargo_bike",
"description": "Layer 'Bike parking' shows and asks freeform values for key 'capacity:cargo_bike' (in the MapComplete.osm.be theme 'Cyclofix - an open map for cyclists')"
},
{
"key": "service:bicycle:cleaning:charge",
"description": "Layer 'Bike parking' shows and asks freeform values for key 'service:bicycle:cleaning:charge' (in the MapComplete.osm.be theme 'Cyclofix - an open map for cyclists')"
},
{
"key": "service:bicycle:cleaning:fee",
"description": "Layer 'Bike parking' shows service:bicycle:cleaning:fee=no&service:bicycle:cleaning:charge= with a fixed text, namely 'The cleaning service is free to use' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclofix - an open map for cyclists')",
"value": "no&service:bicycle:cleaning:charge="
},
{
"key": "service:bicycle:cleaning:fee",
"description": "Layer 'Bike parking' shows service:bicycle:cleaning:fee=no& with a fixed text, namely 'Free to use' (in the MapComplete.osm.be theme 'Cyclofix - an open map for cyclists')",
"value": "no&"
},
{
"key": "service:bicycle:cleaning:fee",
"description": "Layer 'Bike parking' shows service:bicycle:cleaning:fee=yes with a fixed text, namely 'The cleaning service has a fee' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclofix - an open map for cyclists')",
"value": "yes"
}
]
}

View file

@ -59,25 +59,6 @@
"key": "bottle",
"description": "Layer 'Drinking water' shows bottle=no with a fixed text, namely 'Water bottles may not fit' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Drinking Water')",
"value": "no"
},
{
"key": "service:bicycle:cleaning:charge",
"description": "Layer 'Drinking water' shows and asks freeform values for key 'service:bicycle:cleaning:charge' (in the MapComplete.osm.be theme 'Drinking Water')"
},
{
"key": "service:bicycle:cleaning:fee",
"description": "Layer 'Drinking water' shows service:bicycle:cleaning:fee=no&service:bicycle:cleaning:charge= with a fixed text, namely 'The cleaning service is free to use' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Drinking Water')",
"value": "no&service:bicycle:cleaning:charge="
},
{
"key": "service:bicycle:cleaning:fee",
"description": "Layer 'Drinking water' shows service:bicycle:cleaning:fee=no& with a fixed text, namely 'Free to use' (in the MapComplete.osm.be theme 'Drinking Water')",
"value": "no&"
},
{
"key": "service:bicycle:cleaning:fee",
"description": "Layer 'Drinking water' shows service:bicycle:cleaning:fee=yes with a fixed text, namely 'The cleaning service has a fee' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Drinking Water')",
"value": "yes"
}
]
}

View file

@ -0,0 +1,206 @@
{
"data_format": 1,
"project": {
"name": "MapComplete Open Etymology Map",
"description": "What is the origin of a toponym?",
"project_url": "https://mapcomplete.osm.be/etymology",
"doc_url": "https://github.com/pietervdvn/MapComplete/tree/master/assets/themes/",
"icon_url": "https://mapcomplete.osm.be/assets/layers/etymology/logo.svg",
"contact_name": "Pieter Vander Vennet, ",
"contact_email": "pietervdvn@posteo.net"
},
"tags": [
{
"key": "name:etymology:wikidata",
"description": "The MapComplete theme Open Etymology Map has a layer Has etymolgy showing features with this tag"
},
{
"key": "name:etymology",
"description": "The MapComplete theme Open Etymology Map has a layer Has etymolgy showing features with this tag"
},
{
"key": "image",
"description": "The layer 'Has etymolgy shows images based on the keys image, image:0, image:1,... and wikidata, wikipedia, wikimedia_commons and mapillary"
},
{
"key": "mapillary",
"description": "The layer 'Has etymolgy shows images based on the keys image, image:0, image:1,... and wikidata, wikipedia, wikimedia_commons and mapillary"
},
{
"key": "wikidata",
"description": "The layer 'Has etymolgy shows images based on the keys image, image:0, image:1,... and wikidata, wikipedia, wikimedia_commons and mapillary"
},
{
"key": "wikipedia",
"description": "The layer 'Has etymolgy shows images based on the keys image, image:0, image:1,... and wikidata, wikipedia, wikimedia_commons and mapillary"
},
{
"key": "name:etymology:wikidata",
"description": "Layer 'Has etymolgy' shows and asks freeform values for key 'name:etymology:wikidata' (in the MapComplete.osm.be theme 'Open Etymology Map')"
},
{
"key": "name:etymology",
"description": "Layer 'Has etymolgy' shows and asks freeform values for key 'name:etymology' (in the MapComplete.osm.be theme 'Open Etymology Map')"
},
{
"key": "name:etymology",
"description": "Layer 'Has etymolgy' shows name:etymology=unknown with a fixed text, namely 'The origin of this name is unknown in all literature' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Open Etymology Map')",
"value": "unknown"
},
{
"key": "image",
"description": "The layer 'Has etymolgy allows to upload images and adds them under the 'image'-tag (and image:0, image:1, ... for multiple images). Furhtermore, this layer shows images based on the keys image, wikidata, wikipedia, wikimedia_commons and mapillary"
},
{
"key": "mapillary",
"description": "The layer 'Has etymolgy allows to upload images and adds them under the 'image'-tag (and image:0, image:1, ... for multiple images). Furhtermore, this layer shows images based on the keys image, wikidata, wikipedia, wikimedia_commons and mapillary"
},
{
"key": "wikidata",
"description": "The layer 'Has etymolgy allows to upload images and adds them under the 'image'-tag (and image:0, image:1, ... for multiple images). Furhtermore, this layer shows images based on the keys image, wikidata, wikipedia, wikimedia_commons and mapillary"
},
{
"key": "wikipedia",
"description": "The layer 'Has etymolgy allows to upload images and adds them under the 'image'-tag (and image:0, image:1, ... for multiple images). Furhtermore, this layer shows images based on the keys image, wikidata, wikipedia, wikimedia_commons and mapillary"
},
{
"key": "wikidata",
"description": "Layer 'Has etymolgy' shows and asks freeform values for key 'wikidata' (in the MapComplete.osm.be theme 'Open Etymology Map')"
},
{
"key": "wikidata",
"description": "Layer 'Has etymolgy' shows wikidata= with a fixed text, namely 'No Wikipedia page has been linked yet' (in the MapComplete.osm.be theme 'Open Etymology Map') Picking this answer will delete the key wikidata.",
"value": ""
},
{
"key": "name",
"description": "The MapComplete theme Open Etymology Map has a layer Streets without etymology information showing features with this tag"
},
{
"key": "highway",
"description": "The MapComplete theme Open Etymology Map has a layer Streets without etymology information showing features with this tag"
},
{
"key": "image",
"description": "The layer 'Streets without etymology information shows images based on the keys image, image:0, image:1,... and wikidata, wikipedia, wikimedia_commons and mapillary"
},
{
"key": "mapillary",
"description": "The layer 'Streets without etymology information shows images based on the keys image, image:0, image:1,... and wikidata, wikipedia, wikimedia_commons and mapillary"
},
{
"key": "wikidata",
"description": "The layer 'Streets without etymology information shows images based on the keys image, image:0, image:1,... and wikidata, wikipedia, wikimedia_commons and mapillary"
},
{
"key": "wikipedia",
"description": "The layer 'Streets without etymology information shows images based on the keys image, image:0, image:1,... and wikidata, wikipedia, wikimedia_commons and mapillary"
},
{
"key": "name:etymology:wikidata",
"description": "Layer 'Streets without etymology information' shows and asks freeform values for key 'name:etymology:wikidata' (in the MapComplete.osm.be theme 'Open Etymology Map')"
},
{
"key": "name:etymology",
"description": "Layer 'Streets without etymology information' shows and asks freeform values for key 'name:etymology' (in the MapComplete.osm.be theme 'Open Etymology Map')"
},
{
"key": "name:etymology",
"description": "Layer 'Streets without etymology information' shows name:etymology=unknown with a fixed text, namely 'The origin of this name is unknown in all literature' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Open Etymology Map')",
"value": "unknown"
},
{
"key": "image",
"description": "The layer 'Streets without etymology information allows to upload images and adds them under the 'image'-tag (and image:0, image:1, ... for multiple images). Furhtermore, this layer shows images based on the keys image, wikidata, wikipedia, wikimedia_commons and mapillary"
},
{
"key": "mapillary",
"description": "The layer 'Streets without etymology information allows to upload images and adds them under the 'image'-tag (and image:0, image:1, ... for multiple images). Furhtermore, this layer shows images based on the keys image, wikidata, wikipedia, wikimedia_commons and mapillary"
},
{
"key": "wikidata",
"description": "The layer 'Streets without etymology information allows to upload images and adds them under the 'image'-tag (and image:0, image:1, ... for multiple images). Furhtermore, this layer shows images based on the keys image, wikidata, wikipedia, wikimedia_commons and mapillary"
},
{
"key": "wikipedia",
"description": "The layer 'Streets without etymology information allows to upload images and adds them under the 'image'-tag (and image:0, image:1, ... for multiple images). Furhtermore, this layer shows images based on the keys image, wikidata, wikipedia, wikimedia_commons and mapillary"
},
{
"key": "wikidata",
"description": "Layer 'Streets without etymology information' shows and asks freeform values for key 'wikidata' (in the MapComplete.osm.be theme 'Open Etymology Map')"
},
{
"key": "wikidata",
"description": "Layer 'Streets without etymology information' shows wikidata= with a fixed text, namely 'No Wikipedia page has been linked yet' (in the MapComplete.osm.be theme 'Open Etymology Map') Picking this answer will delete the key wikidata.",
"value": ""
},
{
"key": "name",
"description": "The MapComplete theme Open Etymology Map has a layer Parks and forests without etymology information showing features with this tag"
},
{
"key": "leisure",
"description": "The MapComplete theme Open Etymology Map has a layer Parks and forests without etymology information showing features with this tag",
"value": "park"
},
{
"key": "landuse",
"description": "The MapComplete theme Open Etymology Map has a layer Parks and forests without etymology information showing features with this tag",
"value": "forest"
},
{
"key": "image",
"description": "The layer 'Parks and forests without etymology information shows images based on the keys image, image:0, image:1,... and wikidata, wikipedia, wikimedia_commons and mapillary"
},
{
"key": "mapillary",
"description": "The layer 'Parks and forests without etymology information shows images based on the keys image, image:0, image:1,... and wikidata, wikipedia, wikimedia_commons and mapillary"
},
{
"key": "wikidata",
"description": "The layer 'Parks and forests without etymology information shows images based on the keys image, image:0, image:1,... and wikidata, wikipedia, wikimedia_commons and mapillary"
},
{
"key": "wikipedia",
"description": "The layer 'Parks and forests without etymology information shows images based on the keys image, image:0, image:1,... and wikidata, wikipedia, wikimedia_commons and mapillary"
},
{
"key": "name:etymology:wikidata",
"description": "Layer 'Parks and forests without etymology information' shows and asks freeform values for key 'name:etymology:wikidata' (in the MapComplete.osm.be theme 'Open Etymology Map')"
},
{
"key": "name:etymology",
"description": "Layer 'Parks and forests without etymology information' shows and asks freeform values for key 'name:etymology' (in the MapComplete.osm.be theme 'Open Etymology Map')"
},
{
"key": "name:etymology",
"description": "Layer 'Parks and forests without etymology information' shows name:etymology=unknown with a fixed text, namely 'The origin of this name is unknown in all literature' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Open Etymology Map')",
"value": "unknown"
},
{
"key": "image",
"description": "The layer 'Parks and forests without etymology information allows to upload images and adds them under the 'image'-tag (and image:0, image:1, ... for multiple images). Furhtermore, this layer shows images based on the keys image, wikidata, wikipedia, wikimedia_commons and mapillary"
},
{
"key": "mapillary",
"description": "The layer 'Parks and forests without etymology information allows to upload images and adds them under the 'image'-tag (and image:0, image:1, ... for multiple images). Furhtermore, this layer shows images based on the keys image, wikidata, wikipedia, wikimedia_commons and mapillary"
},
{
"key": "wikidata",
"description": "The layer 'Parks and forests without etymology information allows to upload images and adds them under the 'image'-tag (and image:0, image:1, ... for multiple images). Furhtermore, this layer shows images based on the keys image, wikidata, wikipedia, wikimedia_commons and mapillary"
},
{
"key": "wikipedia",
"description": "The layer 'Parks and forests without etymology information allows to upload images and adds them under the 'image'-tag (and image:0, image:1, ... for multiple images). Furhtermore, this layer shows images based on the keys image, wikidata, wikipedia, wikimedia_commons and mapillary"
},
{
"key": "wikidata",
"description": "Layer 'Parks and forests without etymology information' shows and asks freeform values for key 'wikidata' (in the MapComplete.osm.be theme 'Open Etymology Map')"
},
{
"key": "wikidata",
"description": "Layer 'Parks and forests without etymology information' shows wikidata= with a fixed text, namely 'No Wikipedia page has been linked yet' (in the MapComplete.osm.be theme 'Open Etymology Map') Picking this answer will delete the key wikidata.",
"value": ""
}
]
}

View file

@ -76,9 +76,19 @@
"description": "Layer 'Restaurants and fast food' shows payment:cards=yes with a fixed text, namely 'Payment cards are accepted here' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Restaurants and fast food')",
"value": "yes"
},
{
"key": "payment:app",
"description": "Layer 'Restaurants and fast food' shows payment:app=yes with a fixed text, namely 'Payment is done using a dedicated app' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Restaurants and fast food')",
"value": "yes"
},
{
"key": "payment:membership_card",
"description": "Layer 'Restaurants and fast food' shows payment:membership_card=yes with a fixed text, namely 'Payment is done using a membership card' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Restaurants and fast food')",
"value": "yes"
},
{
"key": "wheelchair",
"description": "Layer 'Restaurants and fast food' shows wheelchair=designated with a fixed text, namely 'This place is specially adapated for wheelchair users' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Restaurants and fast food')",
"description": "Layer 'Restaurants and fast food' shows wheelchair=designated with a fixed text, namely 'This place is specially adapted for wheelchair users' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Restaurants and fast food')",
"value": "designated"
},
{

View file

@ -81,9 +81,19 @@
"description": "Layer 'Fries shop' shows payment:cards=yes with a fixed text, namely 'Payment cards are accepted here' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Friturenkaart')",
"value": "yes"
},
{
"key": "payment:app",
"description": "Layer 'Fries shop' shows payment:app=yes with a fixed text, namely 'Payment is done using a dedicated app' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Friturenkaart')",
"value": "yes"
},
{
"key": "payment:membership_card",
"description": "Layer 'Fries shop' shows payment:membership_card=yes with a fixed text, namely 'Payment is done using a membership card' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Friturenkaart')",
"value": "yes"
},
{
"key": "wheelchair",
"description": "Layer 'Fries shop' shows wheelchair=designated with a fixed text, namely 'This place is specially adapated for wheelchair users' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Friturenkaart')",
"description": "Layer 'Fries shop' shows wheelchair=designated with a fixed text, namely 'This place is specially adapted for wheelchair users' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Friturenkaart')",
"value": "designated"
},
{
@ -396,9 +406,19 @@
"description": "Layer 'Restaurants and fast food' shows payment:cards=yes with a fixed text, namely 'Payment cards are accepted here' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Friturenkaart')",
"value": "yes"
},
{
"key": "payment:app",
"description": "Layer 'Restaurants and fast food' shows payment:app=yes with a fixed text, namely 'Payment is done using a dedicated app' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Friturenkaart')",
"value": "yes"
},
{
"key": "payment:membership_card",
"description": "Layer 'Restaurants and fast food' shows payment:membership_card=yes with a fixed text, namely 'Payment is done using a membership card' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Friturenkaart')",
"value": "yes"
},
{
"key": "wheelchair",
"description": "Layer 'Restaurants and fast food' shows wheelchair=designated with a fixed text, namely 'This place is specially adapated for wheelchair users' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Friturenkaart')",
"description": "Layer 'Restaurants and fast food' shows wheelchair=designated with a fixed text, namely 'This place is specially adapted for wheelchair users' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Friturenkaart')",
"value": "designated"
},
{

View file

@ -52,7 +52,7 @@
},
{
"key": "wheelchair",
"description": "Layer 'Hackerspace' shows wheelchair=designated with a fixed text, namely 'This place is specially adapated for wheelchair users' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Hackerspaces')",
"description": "Layer 'Hackerspace' shows wheelchair=designated with a fixed text, namely 'This place is specially adapted for wheelchair users' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Hackerspaces')",
"value": "designated"
},
{

View file

@ -74,25 +74,6 @@
"key": "map_source:attribution",
"description": "Layer 'Maps' shows map_source:attribution=no with a fixed text, namely 'There is no attribution at all' (in the MapComplete.osm.be theme 'A map of maps')",
"value": "no"
},
{
"key": "service:bicycle:cleaning:charge",
"description": "Layer 'Maps' shows and asks freeform values for key 'service:bicycle:cleaning:charge' (in the MapComplete.osm.be theme 'A map of maps')"
},
{
"key": "service:bicycle:cleaning:fee",
"description": "Layer 'Maps' shows service:bicycle:cleaning:fee=no&service:bicycle:cleaning:charge= with a fixed text, namely 'The cleaning service is free to use' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'A map of maps')",
"value": "no&service:bicycle:cleaning:charge="
},
{
"key": "service:bicycle:cleaning:fee",
"description": "Layer 'Maps' shows service:bicycle:cleaning:fee=no& with a fixed text, namely 'Free to use' (in the MapComplete.osm.be theme 'A map of maps')",
"value": "no&"
},
{
"key": "service:bicycle:cleaning:fee",
"description": "Layer 'Maps' shows service:bicycle:cleaning:fee=yes with a fixed text, namely 'The cleaning service has a fee' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'A map of maps')",
"value": "yes"
}
]
}

View file

@ -60,25 +60,6 @@
"description": "Layer 'Drinking water' shows bottle=no with a fixed text, namely 'Water bottles may not fit' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'De Natuur in')",
"value": "no"
},
{
"key": "service:bicycle:cleaning:charge",
"description": "Layer 'Drinking water' shows and asks freeform values for key 'service:bicycle:cleaning:charge' (in the MapComplete.osm.be theme 'De Natuur in')"
},
{
"key": "service:bicycle:cleaning:fee",
"description": "Layer 'Drinking water' shows service:bicycle:cleaning:fee=no&service:bicycle:cleaning:charge= with a fixed text, namely 'The cleaning service is free to use' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'De Natuur in')",
"value": "no&service:bicycle:cleaning:charge="
},
{
"key": "service:bicycle:cleaning:fee",
"description": "Layer 'Drinking water' shows service:bicycle:cleaning:fee=no& with a fixed text, namely 'Free to use' (in the MapComplete.osm.be theme 'De Natuur in')",
"value": "no&"
},
{
"key": "service:bicycle:cleaning:fee",
"description": "Layer 'Drinking water' shows service:bicycle:cleaning:fee=yes with a fixed text, namely 'The cleaning service has a fee' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'De Natuur in')",
"value": "yes"
},
{
"key": "leisure",
"description": "The MapComplete theme De Natuur in has a layer Vogelkijkhutten showing features with this tag",
@ -189,25 +170,6 @@
"description": "Layer 'Vogelkijkhutten' shows operator=Agentschap Natuur en Bos with a fixed text, namely 'Beheer door het Agentschap Natuur en Bos ' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'De Natuur in')",
"value": "Agentschap Natuur en Bos"
},
{
"key": "service:bicycle:cleaning:charge",
"description": "Layer 'Vogelkijkhutten' shows and asks freeform values for key 'service:bicycle:cleaning:charge' (in the MapComplete.osm.be theme 'De Natuur in')"
},
{
"key": "service:bicycle:cleaning:fee",
"description": "Layer 'Vogelkijkhutten' shows service:bicycle:cleaning:fee=no&service:bicycle:cleaning:charge= with a fixed text, namely 'The cleaning service is free to use' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'De Natuur in')",
"value": "no&service:bicycle:cleaning:charge="
},
{
"key": "service:bicycle:cleaning:fee",
"description": "Layer 'Vogelkijkhutten' shows service:bicycle:cleaning:fee=no& with a fixed text, namely 'Free to use' (in the MapComplete.osm.be theme 'De Natuur in')",
"value": "no&"
},
{
"key": "service:bicycle:cleaning:fee",
"description": "Layer 'Vogelkijkhutten' shows service:bicycle:cleaning:fee=yes with a fixed text, namely 'The cleaning service has a fee' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'De Natuur in')",
"value": "yes"
},
{
"key": "tourism",
"description": "The MapComplete theme De Natuur in has a layer Maps showing features with this tag",
@ -273,25 +235,6 @@
"description": "Layer 'Maps' shows map_source:attribution=no with a fixed text, namely 'There is no attribution at all' (in the MapComplete.osm.be theme 'De Natuur in')",
"value": "no"
},
{
"key": "service:bicycle:cleaning:charge",
"description": "Layer 'Maps' shows and asks freeform values for key 'service:bicycle:cleaning:charge' (in the MapComplete.osm.be theme 'De Natuur in')"
},
{
"key": "service:bicycle:cleaning:fee",
"description": "Layer 'Maps' shows service:bicycle:cleaning:fee=no&service:bicycle:cleaning:charge= with a fixed text, namely 'The cleaning service is free to use' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'De Natuur in')",
"value": "no&service:bicycle:cleaning:charge="
},
{
"key": "service:bicycle:cleaning:fee",
"description": "Layer 'Maps' shows service:bicycle:cleaning:fee=no& with a fixed text, namely 'Free to use' (in the MapComplete.osm.be theme 'De Natuur in')",
"value": "no&"
},
{
"key": "service:bicycle:cleaning:fee",
"description": "Layer 'Maps' shows service:bicycle:cleaning:fee=yes with a fixed text, namely 'The cleaning service has a fee' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'De Natuur in')",
"value": "yes"
},
{
"key": "information",
"description": "The MapComplete theme De Natuur in has a layer Information boards showing features with this tag",
@ -313,25 +256,6 @@
"key": "wikipedia",
"description": "The layer 'Information boards allows to upload images and adds them under the 'image'-tag (and image:0, image:1, ... for multiple images). Furhtermore, this layer shows images based on the keys image, wikidata, wikipedia, wikimedia_commons and mapillary"
},
{
"key": "service:bicycle:cleaning:charge",
"description": "Layer 'Information boards' shows and asks freeform values for key 'service:bicycle:cleaning:charge' (in the MapComplete.osm.be theme 'De Natuur in')"
},
{
"key": "service:bicycle:cleaning:fee",
"description": "Layer 'Information boards' shows service:bicycle:cleaning:fee=no&service:bicycle:cleaning:charge= with a fixed text, namely 'The cleaning service is free to use' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'De Natuur in')",
"value": "no&service:bicycle:cleaning:charge="
},
{
"key": "service:bicycle:cleaning:fee",
"description": "Layer 'Information boards' shows service:bicycle:cleaning:fee=no& with a fixed text, namely 'Free to use' (in the MapComplete.osm.be theme 'De Natuur in')",
"value": "no&"
},
{
"key": "service:bicycle:cleaning:fee",
"description": "Layer 'Information boards' shows service:bicycle:cleaning:fee=yes with a fixed text, namely 'The cleaning service has a fee' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'De Natuur in')",
"value": "yes"
},
{
"key": "leisure",
"description": "The MapComplete theme De Natuur in has a layer Natuurgebied showing features with this tag",
@ -505,25 +429,6 @@
"key": "wikidata",
"description": "Layer 'Natuurgebied' shows wikidata= with a fixed text, namely 'No Wikipedia page has been linked yet' (in the MapComplete.osm.be theme 'De Natuur in') Picking this answer will delete the key wikidata.",
"value": ""
},
{
"key": "service:bicycle:cleaning:charge",
"description": "Layer 'Natuurgebied' shows and asks freeform values for key 'service:bicycle:cleaning:charge' (in the MapComplete.osm.be theme 'De Natuur in')"
},
{
"key": "service:bicycle:cleaning:fee",
"description": "Layer 'Natuurgebied' shows service:bicycle:cleaning:fee=no&service:bicycle:cleaning:charge= with a fixed text, namely 'The cleaning service is free to use' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'De Natuur in')",
"value": "no&service:bicycle:cleaning:charge="
},
{
"key": "service:bicycle:cleaning:fee",
"description": "Layer 'Natuurgebied' shows service:bicycle:cleaning:fee=no& with a fixed text, namely 'Free to use' (in the MapComplete.osm.be theme 'De Natuur in')",
"value": "no&"
},
{
"key": "service:bicycle:cleaning:fee",
"description": "Layer 'Natuurgebied' shows service:bicycle:cleaning:fee=yes with a fixed text, namely 'The cleaning service has a fee' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'De Natuur in')",
"value": "yes"
}
]
}

View file

@ -88,7 +88,7 @@
},
{
"key": "wheelchair",
"description": "Layer 'Observation towers' shows wheelchair=designated with a fixed text, namely 'This place is specially adapated for wheelchair users' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Observation towers')",
"description": "Layer 'Observation towers' shows wheelchair=designated with a fixed text, namely 'This place is specially adapted for wheelchair users' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Observation towers')",
"value": "designated"
},
{

View file

@ -0,0 +1,225 @@
{
"data_format": 1,
"project": {
"name": "MapComplete Street Lighting",
"description": "On this map you can find everything about street lighting",
"project_url": "https://mapcomplete.osm.be/street_lighting",
"doc_url": "https://github.com/pietervdvn/MapComplete/tree/master/assets/themes/",
"icon_url": "https://mapcomplete.osm.be/assets/layers/street_lamps/street_lamp.svg",
"contact_name": "Pieter Vander Vennet, Robin van der Linde",
"contact_email": "pietervdvn@posteo.net"
},
"tags": [
{
"key": "highway",
"description": "The MapComplete theme Street Lighting has a layer Street Lamps showing features with this tag",
"value": "street_lamp"
},
{
"key": "ref",
"description": "Layer 'Street Lamps' shows and asks freeform values for key 'ref' (in the MapComplete.osm.be theme 'Street Lighting')"
},
{
"key": "support",
"description": "Layer 'Street Lamps' shows support=catenary with a fixed text, namely 'This lamp is suspended using cables' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Street Lighting')",
"value": "catenary"
},
{
"key": "support",
"description": "Layer 'Street Lamps' shows support=ceiling with a fixed text, namely 'This lamp is mounted on a ceiling' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Street Lighting')",
"value": "ceiling"
},
{
"key": "support",
"description": "Layer 'Street Lamps' shows support=ground with a fixed text, namely 'This lamp is mounted in the ground' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Street Lighting')",
"value": "ground"
},
{
"key": "support",
"description": "Layer 'Street Lamps' shows support=pedestal with a fixed text, namely 'This lamp is mounted on a short pole (mostly < 1.5m)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Street Lighting')",
"value": "pedestal"
},
{
"key": "support",
"description": "Layer 'Street Lamps' shows support=pole with a fixed text, namely 'This lamp is mounted on a pole' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Street Lighting')",
"value": "pole"
},
{
"key": "support",
"description": "Layer 'Street Lamps' shows support=wall with a fixed text, namely 'This lamp is mounted directly to the wall' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Street Lighting')",
"value": "wall"
},
{
"key": "support",
"description": "Layer 'Street Lamps' shows support=wall_mount with a fixed text, namely 'This lamp is mounted to the wall using a metal bar' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Street Lighting')",
"value": "wall_mount"
},
{
"key": "lamp_mount",
"description": "Layer 'Street Lamps' shows lamp_mount=straight_mast with a fixed text, namely 'This lamp sits atop of a straight mast' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Street Lighting')",
"value": "straight_mast"
},
{
"key": "lamp_mount",
"description": "Layer 'Street Lamps' shows lamp_mount=bent_mast with a fixed text, namely 'This lamp sits at the end of a bent mast' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Street Lighting')",
"value": "bent_mast"
},
{
"key": "light:method",
"description": "Layer 'Street Lamps' shows light:method=electric with a fixed text, namely 'This lamp is lit electrically' (in the MapComplete.osm.be theme 'Street Lighting')",
"value": "electric"
},
{
"key": "light:method",
"description": "Layer 'Street Lamps' shows light:method=LED with a fixed text, namely 'This lamp uses LEDs' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Street Lighting')",
"value": "LED"
},
{
"key": "light:method",
"description": "Layer 'Street Lamps' shows light:method=incandescent with a fixed text, namely 'This lamp uses incandescent lighting' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Street Lighting')",
"value": "incandescent"
},
{
"key": "light:method",
"description": "Layer 'Street Lamps' shows light:method=halogen with a fixed text, namely 'This lamp uses halogen lighting' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Street Lighting')",
"value": "halogen"
},
{
"key": "light:method",
"description": "Layer 'Street Lamps' shows light:method=discharge with a fixed text, namely 'This lamp uses discharge lamps (unknown type)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Street Lighting')",
"value": "discharge"
},
{
"key": "light:method",
"description": "Layer 'Street Lamps' shows light:method=mercury with a fixed text, namely 'This lamp uses a mercury-vapour lamp (lightly blueish)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Street Lighting')",
"value": "mercury"
},
{
"key": "light:method",
"description": "Layer 'Street Lamps' shows light:method=metal-halide with a fixed text, namely 'This lamp uses metal-halide lamps (bright white)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Street Lighting')",
"value": "metal-halide"
},
{
"key": "light:method",
"description": "Layer 'Street Lamps' shows light:method=fluorescent with a fixed text, namely 'This lamp uses fluorescent lighting' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Street Lighting')",
"value": "fluorescent"
},
{
"key": "light:method",
"description": "Layer 'Street Lamps' shows light:method=sodium with a fixed text, namely 'This lamp uses sodium lamps (unknown type)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Street Lighting')",
"value": "sodium"
},
{
"key": "light:method",
"description": "Layer 'Street Lamps' shows light:method=low_pressure_sodium with a fixed text, namely 'This lamp uses low pressure sodium lamps (monochrome orange)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Street Lighting')",
"value": "low_pressure_sodium"
},
{
"key": "light:method",
"description": "Layer 'Street Lamps' shows light:method=high_pressure_sodium with a fixed text, namely 'This lamp uses high pressure sodium lamps (orange with white)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Street Lighting')",
"value": "high_pressure_sodium"
},
{
"key": "light:method",
"description": "Layer 'Street Lamps' shows light:method=gas with a fixed text, namely 'This lamp is lit using gas' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Street Lighting')",
"value": "gas"
},
{
"key": "light:colour",
"description": "Layer 'Street Lamps' shows and asks freeform values for key 'light:colour' (in the MapComplete.osm.be theme 'Street Lighting')"
},
{
"key": "light:colour",
"description": "Layer 'Street Lamps' shows light:colour=white with a fixed text, namely 'This lamp emits white light' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Street Lighting')",
"value": "white"
},
{
"key": "light:colour",
"description": "Layer 'Street Lamps' shows light:colour=green with a fixed text, namely 'This lamp emits green light' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Street Lighting')",
"value": "green"
},
{
"key": "light:colour",
"description": "Layer 'Street Lamps' shows light:colour=orange with a fixed text, namely 'This lamp emits orange light' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Street Lighting')",
"value": "orange"
},
{
"key": "light:count",
"description": "Layer 'Street Lamps' shows and asks freeform values for key 'light:count' (in the MapComplete.osm.be theme 'Street Lighting')"
},
{
"key": "light:count",
"description": "Layer 'Street Lamps' shows light:count=1 with a fixed text, namely 'This lamp has 1 fixture' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Street Lighting')",
"value": "1"
},
{
"key": "light:count",
"description": "Layer 'Street Lamps' shows light:count=2 with a fixed text, namely 'This lamp has 2 fixtures' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Street Lighting')",
"value": "2"
},
{
"key": "light:lit",
"description": "Layer 'Street Lamps' shows light:lit=dusk-dawn with a fixed text, namely 'This lamp is lit at night' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Street Lighting')",
"value": "dusk-dawn"
},
{
"key": "light:lit",
"description": "Layer 'Street Lamps' shows light:lit=24/7 with a fixed text, namely 'This lamp is lit 24/7' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Street Lighting')",
"value": "24/7"
},
{
"key": "light:lit",
"description": "Layer 'Street Lamps' shows light:lit=motion with a fixed text, namely 'This lamp is lit based on motion' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Street Lighting')",
"value": "motion"
},
{
"key": "light:lit",
"description": "Layer 'Street Lamps' shows light:lit=demand with a fixed text, namely 'This lamp is lit based on demand (e.g. with a pushbutton)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Street Lighting')",
"value": "demand"
},
{
"key": "light:direction",
"description": "Layer 'Street Lamps' shows and asks freeform values for key 'light:direction' (in the MapComplete.osm.be theme 'Street Lighting')"
},
{
"key": "lit",
"description": "Layer 'Lit streets' shows lit=yes with a fixed text, namely 'This street is lit' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Street Lighting')",
"value": "yes"
},
{
"key": "lit",
"description": "Layer 'Lit streets' shows lit=no with a fixed text, namely 'This street is not lit' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Street Lighting')",
"value": "no"
},
{
"key": "lit",
"description": "Layer 'Lit streets' shows lit=sunset-sunrise with a fixed text, namely 'This street is lit at night' (in the MapComplete.osm.be theme 'Street Lighting')",
"value": "sunset-sunrise"
},
{
"key": "lit",
"description": "Layer 'Lit streets' shows lit=24/7 with a fixed text, namely 'This street is lit 24/7' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Street Lighting')",
"value": "24/7"
},
{
"key": "lit",
"description": "Layer 'All streets' shows lit=yes with a fixed text, namely 'This street is lit' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Street Lighting')",
"value": "yes"
},
{
"key": "lit",
"description": "Layer 'All streets' shows lit=no with a fixed text, namely 'This street is not lit' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Street Lighting')",
"value": "no"
},
{
"key": "lit",
"description": "Layer 'All streets' shows lit=sunset-sunrise with a fixed text, namely 'This street is lit at night' (in the MapComplete.osm.be theme 'Street Lighting')",
"value": "sunset-sunrise"
},
{
"key": "lit",
"description": "Layer 'All streets' shows lit=24/7 with a fixed text, namely 'This street is lit 24/7' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Street Lighting')",
"value": "24/7"
}
]
}

View file

@ -44,6 +44,31 @@
"key": "waste",
"description": "Layer 'Waste Basket' shows waste=sharps with a fixed text, namely 'A waste basket for needles and other sharp objects' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Waste Basket')",
"value": "sharps"
},
{
"key": "vending",
"description": "Layer 'Waste Basket' shows vending=dog_excrement_bag&not:vending= with a fixed text, namely 'This waste basket has a dispenser for (dog) excrement bags' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Waste Basket')",
"value": "dog_excrement_bag"
},
{
"key": "not:vending",
"description": "Layer 'Waste Basket' shows vending=dog_excrement_bag&not:vending= with a fixed text, namely 'This waste basket has a dispenser for (dog) excrement bags' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Waste Basket') Picking this answer will delete the key not:vending.",
"value": ""
},
{
"key": "not:vending",
"description": "Layer 'Waste Basket' shows not:vending=dog_excrement_bag&vending= with a fixed text, namely 'This waste basket <b>does not</b> have a dispenser for (dog) excrement bags' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Waste Basket')",
"value": "dog_excrement_bag"
},
{
"key": "vending",
"description": "Layer 'Waste Basket' shows not:vending=dog_excrement_bag&vending= with a fixed text, namely 'This waste basket <b>does not</b> have a dispenser for (dog) excrement bags' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Waste Basket') Picking this answer will delete the key vending.",
"value": ""
},
{
"key": "vending",
"description": "Layer 'Waste Basket' shows vending= with a fixed text, namely 'This waste basket <b>does not</b> have a dispenser for (dog) excrement bags' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Waste Basket') Picking this answer will delete the key vending.",
"value": ""
}
]
}

View file

@ -20,42 +20,6 @@ the URL-parameters are stated in the part between the `?` and the `#`. There are
Finally, the URL-hash is the part after the `#`. It is `node/1234` in this case.
download-control-toggle
-------------------------
Whether or not the download panel is shown The default value is _false_
filter-toggle
---------------
Whether or not the filter view is shown The default value is _false_
tab
-----
The tab that is shown in the welcome-message. 0 = the explanation of the theme,1 = OSM-credits, 2 = sharescreen, 3 = more themes, 4 = about mapcomplete (user must be logged in and have >50 changesets) The default value is _0_
z
---
The initial/current zoom level The default value is _0_
lat
-----
The initial/current latitude The default value is _0_
lon
-----
The initial/current longitude of the app The default value is _0_
fs-userbadge
--------------
@ -92,10 +56,10 @@ Finally, the URL-hash is the part after the `#`. It is `node/1234` in this case.
Disables/enables the help menu or welcome message The default value is _true_
fs-iframe
-----------
fs-iframe-popout
------------------
Disables/Enables the iframe-popup The default value is _false_
Disables/Enables the iframe-popout button. If in iframe mode and the welcome message is hidden, a popout button to the full mapcomplete instance is shown instead (unless disabled with this switch) The default value is _true_
fs-more-quests
@ -134,6 +98,12 @@ Finally, the URL-hash is the part after the `#`. It is `node/1234` in this case.
Enable the PDF download button The default value is _false_
backend
---------
The OSM backend to use - can be used to redirect mapcomplete to the testing backend when using 'osm-test' The default value is _osm_
test
------
@ -152,12 +122,6 @@ Finally, the URL-hash is the part after the `#`. It is `node/1234` in this case.
If true, 'dryrun' mode is activated and a fake user account is loaded The default value is _false_
backend
---------
The OSM backend to use - can be used to redirect mapcomplete to the testing backend when using 'osm-test' The default value is _osm_
overpassUrl
-------------
@ -170,10 +134,16 @@ Finally, the URL-hash is the part after the `#`. It is `node/1234` in this case.
Set a different timeout (in seconds) for queries in overpass The default value is _30_
custom-css
------------
overpassMaxZoom
-----------------
If specified, the custom css from the given link will be loaded additionaly The default value is __
point to switch between OSM-api and overpass The default value is _17_
osmApiTileSize
----------------
Tilesize when the OSM-API is used to fetch data within a BBOX The default value is _18_
background

View file

@ -1,13 +1,16 @@
import * as L from "leaflet";
import {UIEventSource} from "../UIEventSource";
import Svg from "../../Svg";
import Img from "../../UI/Base/Img";
import {LocalStorageSource} from "../Web/LocalStorageSource";
import {VariableUiElement} from "../../UI/Base/VariableUIElement";
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
import {QueryParameters} from "../Web/QueryParameters";
import FeatureSource from "../FeatureSource/FeatureSource";
import StaticFeatureSource from "../FeatureSource/Sources/StaticFeatureSource";
export default class GeoLocationHandler extends VariableUiElement {
public readonly currentLocation : FeatureSource
/**
* Wether or not the geolocation is active, aka the user requested the current location
* @private
@ -25,20 +28,12 @@ export default class GeoLocationHandler extends VariableUiElement {
* @private
*/
private readonly _permission: UIEventSource<string>;
/***
* The marker on the map, in order to update it
* @private
*/
private _marker: L.Marker;
/**
* Literally: _currentGPSLocation.data != undefined
* @private
*/
private readonly _hasLocation: UIEventSource<boolean>;
private readonly _currentGPSLocation: UIEventSource<{
latlng: any;
accuracy: number;
}>;
private readonly _currentGPSLocation: UIEventSource<Coordinates>;
/**
* Kept in order to update the marker
* @private
@ -63,10 +58,15 @@ export default class GeoLocationHandler extends VariableUiElement {
private readonly _layoutToUse: LayoutConfig;
constructor(
currentGPSLocation: UIEventSource<{ latlng: any; accuracy: number }>,
leafletMap: UIEventSource<L.Map>,
layoutToUse: LayoutConfig
state: {
currentGPSLocation: UIEventSource<Coordinates>,
leafletMap: UIEventSource<any>,
layoutToUse: LayoutConfig,
featureSwitchGeolocation: UIEventSource<boolean>
}
) {
const currentGPSLocation = state.currentGPSLocation
const leafletMap = state.leafletMap
const hasLocation = currentGPSLocation.map(
(location) => location !== undefined
);
@ -127,7 +127,7 @@ export default class GeoLocationHandler extends VariableUiElement {
this._previousLocationGrant = previousLocationGrant;
this._currentGPSLocation = currentGPSLocation;
this._leafletMap = leafletMap;
this._layoutToUse = layoutToUse;
this._layoutToUse = state.layoutToUse;
this._hasLocation = hasLocation;
const self = this;
@ -172,7 +172,7 @@ export default class GeoLocationHandler extends VariableUiElement {
const latLonGiven = QueryParameters.wasInitialized("lat") && QueryParameters.wasInitialized("lon")
this.init(false, !latLonGiven);
this.init(false, !latLonGiven && state.featureSwitchGeolocation.data);
isLocked.addCallbackAndRunD(isLocked => {
if (isLocked) {
@ -182,10 +182,25 @@ export default class GeoLocationHandler extends VariableUiElement {
}
})
this.currentLocation = new StaticFeatureSource([], false)
this._currentGPSLocation.addCallback((location) => {
self._previousLocationGrant.setData("granted");
const feature = {
"type": "Feature",
properties: {
"user:location":"yes",
"accuracy":location.accuracy,
"speed":location.speed,
},
geometry:{
type:"Point",
coordinates: [location.longitude, location.latitude],
}
}
self.currentLocation.features.setData([{feature, freshness: new Date()}])
const timeSinceRequest =
(new Date().getTime() - (self._lastUserRequest?.getTime() ?? 0)) / 1000;
if (timeSinceRequest < 30) {
@ -194,36 +209,11 @@ export default class GeoLocationHandler extends VariableUiElement {
self.MoveToCurrentLoction();
}
let color = "#1111cc";
try {
color = getComputedStyle(document.body).getPropertyValue(
"--catch-detail-color"
);
} catch (e) {
console.error(e);
}
const icon = L.icon({
iconUrl: Img.AsData(Svg.location.replace(/#000000/g, color).replace(/#000/g, color)),
iconSize: [40, 40], // size of the icon
iconAnchor: [20, 20], // point of the icon which will correspond to marker's location
});
const map = self._leafletMap.data;
if(map === undefined){
return;
}
const newMarker = L.marker(location.latlng, {icon: icon});
newMarker.addTo(map);
if (self._marker !== undefined) {
map.removeLayer(self._marker);
}
self._marker = newMarker;
});
}
private init(askPermission: boolean, forceZoom: boolean) {
private init(askPermission: boolean, zoomToLocation: boolean) {
const self = this;
if (self._isActive.data) {
@ -237,7 +227,7 @@ export default class GeoLocationHandler extends VariableUiElement {
?.then(function (status) {
console.log("Geolocation permission is ", status.state);
if (status.state === "granted") {
self.StartGeolocating(forceZoom);
self.StartGeolocating(zoomToLocation);
}
self._permission.setData(status.state);
status.onchange = function () {
@ -249,10 +239,10 @@ export default class GeoLocationHandler extends VariableUiElement {
}
if (askPermission) {
self.StartGeolocating(forceZoom);
self.StartGeolocating(zoomToLocation);
} else if (this._previousLocationGrant.data === "granted") {
this._previousLocationGrant.setData("");
self.StartGeolocating(forceZoom);
self.StartGeolocating(zoomToLocation);
}
}
@ -261,8 +251,8 @@ export default class GeoLocationHandler extends VariableUiElement {
this._lastUserRequest = undefined;
if (
this._currentGPSLocation.data.latlng[0] === 0 &&
this._currentGPSLocation.data.latlng[1] === 0
this._currentGPSLocation.data.latitude === 0 &&
this._currentGPSLocation.data.longitude === 0
) {
console.debug("Not moving to GPS-location: it is null island");
return;
@ -275,20 +265,22 @@ export default class GeoLocationHandler extends VariableUiElement {
if (b !== true) {
// B is an array with our locklocation
inRange =
b[0][0] <= location.latlng[0] &&
location.latlng[0] <= b[1][0] &&
b[0][1] <= location.latlng[1] &&
location.latlng[1] <= b[1][1];
b[0][0] <= location.latitude &&
location.latitude <= b[1][0] &&
b[0][1] <= location.longitude &&
location.longitude <= b[1][1];
}
}
if (!inRange) {
console.log(
"Not zooming to GPS location: out of bounds",
b,
location.latlng
location
);
} else {
this._leafletMap.data.setView(location.latlng, targetZoom);
const currentZoom = this._leafletMap.data.getZoom()
this._leafletMap.data.setView([location.latitude, location.longitude], Math.max(targetZoom ?? 0, currentZoom));
}
}
@ -312,10 +304,7 @@ export default class GeoLocationHandler extends VariableUiElement {
navigator.geolocation.watchPosition(
function (position) {
self._currentGPSLocation.setData({
latlng: [position.coords.latitude, position.coords.longitude],
accuracy: position.coords.accuracy,
});
self._currentGPSLocation.setData(position.coords);
},
function () {
console.warn("Could not get location with navigator.geolocation");

View file

@ -6,6 +6,8 @@ import {ElementStorage} from "../ElementStorage";
import {Changes} from "../Osm/Changes";
import {OsmObject} from "../Osm/OsmObject";
import {OsmConnection} from "../Osm/OsmConnection";
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
import SimpleMetaTagger from "../SimpleMetaTagger";
export default class SelectedElementTagsUpdater {
@ -14,13 +16,14 @@ export default class SelectedElementTagsUpdater {
"changeset",
"user",
"uid",
"id"] )
"id"])
constructor(state: {
selectedElement: UIEventSource<any>,
allElements: ElementStorage,
changes: Changes,
osmConnection: OsmConnection
osmConnection: OsmConnection,
layoutToUse: LayoutConfig
}) {
@ -37,7 +40,8 @@ export default class SelectedElementTagsUpdater {
selectedElement: UIEventSource<any>,
allElements: ElementStorage,
changes: Changes,
osmConnection: OsmConnection
osmConnection: OsmConnection,
layoutToUse: LayoutConfig
}) {
@ -70,11 +74,18 @@ export default class SelectedElementTagsUpdater {
selectedElement: UIEventSource<any>,
allElements: ElementStorage,
changes: Changes,
osmConnection: OsmConnection
osmConnection: OsmConnection,
layoutToUse: LayoutConfig
}, latestTags: any, id: string
) {
try {
const leftRightSensitive = state.layoutToUse.isLeftRightSensitive()
if (leftRightSensitive) {
SimpleMetaTagger.removeBothTagging(latestTags)
}
const pendingChanges = state.changes.pendingChanges.data
.filter(change => change.type + "/" + change.id === id)
.filter(change => change.tags !== undefined);
@ -92,6 +103,7 @@ export default class SelectedElementTagsUpdater {
}
}
// With the changes applied, we merge them onto the upstream object
let somethingChanged = false;
const currentTagsSource = state.allElements.getEventSourceById(id);
@ -115,7 +127,7 @@ export default class SelectedElementTagsUpdater {
if (currentKey.startsWith("_")) {
continue
}
if(this.metatags.has(currentKey)){
if (this.metatags.has(currentKey)) {
continue
}
if (currentKey in latestTags) {

View file

@ -10,7 +10,7 @@ import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
* Makes sure the hash shows the selected element and vice-versa.
*/
export default class SelectedFeatureHandler {
private static readonly _no_trigger_on = new Set(["welcome", "copyright", "layers", "new", "filter","", undefined])
private static readonly _no_trigger_on = new Set(["welcome", "copyright", "layers", "new", "filters","", undefined])
private readonly hash: UIEventSource<string>;
private readonly state: {
selectedElement: UIEventSource<any>,
@ -114,6 +114,7 @@ export default class SelectedFeatureHandler {
// Hash has been cleared - we clear the selected element
state.selectedElement.setData(undefined);
} else {
// we search the element to select
const feature = state.allElements.ContainingFeatures.get(h)
if (feature === undefined) {

View file

@ -116,6 +116,11 @@ export class BBox {
getSouth() {
return this.minLat
}
contains(lonLat: [number, number]){
return this.minLat <= lonLat[1] && lonLat[1] <= this.maxLat
&& this.minLon<= lonLat[0] && lonLat[0] <= this.maxLon
}
pad(factor: number, maxIncrease = 2): BBox {

View file

@ -10,6 +10,7 @@ import {UIEventSource} from "./UIEventSource";
import {LocalStorageSource} from "./Web/LocalStorageSource";
import LZString from "lz-string";
import * as personal from "../assets/themes/personal/personal.json";
import LegacyJsonConvert from "../Models/ThemeConfig/LegacyJsonConvert";
export default class DetermineLayout {
@ -74,6 +75,7 @@ export default class DetermineLayout {
const parsed = await Utils.downloadJson(link)
console.log("Got ", parsed)
LegacyJsonConvert.fixThemeConfig(parsed)
try {
parsed.id = link;
return new LayoutConfig(parsed, false).patchImages(link, JSON.stringify(parsed));
@ -87,7 +89,7 @@ export default class DetermineLayout {
}
} catch (e) {
console.erorr(e)
console.error(e)
DetermineLayout.ShowErrorOnCustomTheme(
`<a href="${link}">${link}</a> is invalid - probably not found or invalid JSON:`,
new FixedUiElement(e)
@ -136,6 +138,7 @@ export default class DetermineLayout {
}
}
LegacyJsonConvert.fixThemeConfig(json)
const layoutToUse = new LayoutConfig(json, false);
userLayoutParam.setData(layoutToUse.id);
return [layoutToUse, btoa(Utils.MinifyJSON(JSON.stringify(json)))];

View file

@ -54,16 +54,17 @@ export class ExtraFunction {
private static readonly OverlapFunc = new ExtraFunction(
{
name: "overlapWith",
doc: "Gives a list of features from the specified layer which this feature (partly) overlaps with. " +
"If the current feature is a point, all features that embed the point are given. " +
doc: "Gives a list of features from the specified layer which this feature (partly) overlaps with. A point which is embedded in the feature is detected as well." +
"If the current feature is a point, all features that this point is embeded in are given.\n\n" +
"The returned value is `{ feat: GeoJSONFeature, overlap: number}[]` where `overlap` is the overlapping surface are (in m²) for areas, the overlapping length (in meter) if the current feature is a line or `undefined` if the current feature is a point.\n" +
"The resulting list is sorted in descending order by overlap. The feature with the most overlap will thus be the first in the list\n" +
"\n" +
"For example to get all objects which overlap or embed from a layer, use `_contained_climbing_routes_properties=feat.overlapWith('climbing_route')`",
args: ["...layerIds - one or more layer ids of the layer from which every feature is checked for overlap)"]
},
(params, feat) => {
return (...layerIds: string[]) => {
const result = []
const result : {feat:any, overlap: number}[]= []
const bbox = BBox.get(feat)
@ -79,6 +80,9 @@ export class ExtraFunction {
result.push(...GeoOperations.calculateOverlap(feat, otherLayer));
}
}
result.sort((a, b) => b.overlap - a.overlap)
return result;
}
}
@ -163,12 +167,41 @@ export class ExtraFunction {
}
)
private static readonly GetParsed = new ExtraFunction(
{
name: "get",
doc: "Gets the property of the feature, parses it (as JSON) and returns it. Might return 'undefined' if not defined, null, ...",
args: ["key"]
},
(params, feat) => {
return key => {
const value = feat.properties[key]
if (value === undefined) {
return undefined;
}
try {
const parsed = JSON.parse(value)
if(parsed === null){
return undefined;
}
return parsed;
} catch (e) {
console.warn("Could not parse property " + key + " due to: " + e + ", the value is " + value)
return undefined;
}
}
}
)
private static readonly allFuncs: ExtraFunction[] = [
ExtraFunction.DistanceToFunc,
ExtraFunction.OverlapFunc,
ExtraFunction.ClosestObjectFunc,
ExtraFunction.ClosestNObjectFunc,
ExtraFunction.Memberships
ExtraFunction.Memberships,
ExtraFunction.GetParsed
];
private readonly _name: string;
private readonly _args: string[];
@ -200,7 +233,7 @@ export class ExtraFunction {
return new Combine([
ExtraFunction.intro,
new List(ExtraFunction.allFuncs.map(func => func._name)),
new List(ExtraFunction.allFuncs.map(func => `[${func._name}](#${func._name})`)),
...elems
]);
}
@ -222,7 +255,6 @@ export class ExtraFunction {
const maxFeatures = options?.maxFeatures ?? 1
const maxDistance = options?.maxDistance ?? 500
const uniqueTag: string | undefined = options?.uniqueTag
console.log("Requested closestN")
if (typeof features === "string") {
const name = features
const bbox = GeoOperations.bbox(GeoOperations.buffer(GeoOperations.bbox(feature), maxDistance))
@ -238,7 +270,7 @@ export class ExtraFunction {
let closestFeatures: { feat: any, distance: number }[] = [];
for (const featureList of features) {
for (const otherFeature of featureList) {
if (otherFeature === feature || otherFeature.id === feature.id) {
if (otherFeature === feature || otherFeature.properties.id === feature.properties.id) {
continue; // We ignore self
}
const distance = GeoOperations.distanceBetween(
@ -249,6 +281,11 @@ export class ExtraFunction {
console.error("Could not calculate the distance between", feature, "and", otherFeature)
throw "Undefined distance!"
}
if (distance === 0) {
console.trace("Got a suspiciously zero distance between", otherFeature, "and self-feature", feature)
}
if (distance > maxDistance) {
continue
}

View file

@ -12,7 +12,6 @@ import OverpassFeatureSource from "../Actors/OverpassFeatureSource";
import {Changes} from "../Osm/Changes";
import GeoJsonSource from "./Sources/GeoJsonSource";
import Loc from "../../Models/Loc";
import WayHandlingApplyingFeatureSource from "./Sources/WayHandlingApplyingFeatureSource";
import RegisteringAllFromFeatureSourceActor from "./Actors/RegisteringAllFromFeatureSourceActor";
import TiledFromLocalStorageSource from "./TiledFeatureSource/TiledFromLocalStorageSource";
import SaveTileToLocalStorageActor from "./Actors/SaveTileToLocalStorageActor";
@ -26,6 +25,8 @@ import OsmFeatureSource from "./TiledFeatureSource/OsmFeatureSource";
import {OsmConnection} from "../Osm/OsmConnection";
import {Tiles} from "../../Models/TileRange";
import TileFreshnessCalculator from "./TileFreshnessCalculator";
import {ElementStorage} from "../ElementStorage";
import FullNodeDatabaseSource from "./TiledFeatureSource/FullNodeDatabaseSource";
/**
@ -85,7 +86,8 @@ export default class FeaturePipeline {
readonly overpassMaxZoom: UIEventSource<number>;
readonly osmConnection: OsmConnection
readonly currentBounds: UIEventSource<BBox>,
readonly osmApiTileSize: UIEventSource<number>
readonly osmApiTileSize: UIEventSource<number>,
readonly allElements: ElementStorage
}) {
this.state = state;
@ -127,9 +129,7 @@ export default class FeaturePipeline {
// This will already contain the merged features for this tile. In other words, this will only be triggered once for every tile
const srcFiltered =
new FilteringFeatureSource(state, src.tileIndex,
new WayHandlingApplyingFeatureSource(
new ChangeGeometryApplicator(src, state.changes)
)
)
handleFeatureSource(srcFiltered)
@ -147,6 +147,11 @@ export default class FeaturePipeline {
this.freshnesses.set(id, new TileFreshnessCalculator())
if(id === "type_node"){
// Handles by the 'FullNodeDatabaseSource'
continue;
}
if (source.geojsonSource === undefined) {
// This is an OSM layer
// We load the cached values and register them
@ -221,6 +226,18 @@ export default class FeaturePipeline {
self.freshnesses.get(flayer.layerDef.id).addTileLoad(tileId, new Date())
})
})
if(state.layoutToUse.trackAllNodes){
const fullNodeDb = new FullNodeDatabaseSource(
state.filteredLayers.data.filter(l => l.layerDef.id === "type_node")[0],
tile => {
new RegisteringAllFromFeatureSourceActor(tile)
perLayerHierarchy.get(tile.layer.layerDef.id).registerTile(tile)
tile.features.addCallbackAndRunD(_ => self.newDataLoadedSignal.setData(tile))
})
osmFeatureSource.rawDataHandlers.push((osmJson, tileId) => fullNodeDb.handleOsmJson(osmJson, tileId))
}
const updater = this.initOverpassUpdater(state, useOsmApi)
@ -266,7 +283,7 @@ export default class FeaturePipeline {
// Whenever fresh data comes in, we need to update the metatagging
self.newDataLoadedSignal.stabilized(1000).addCallback(_ => {
self.newDataLoadedSignal.stabilized(250).addCallback(src => {
self.updateAllMetaTagging()
})
@ -391,7 +408,7 @@ export default class FeaturePipeline {
window.setTimeout(
() => {
const layerDef = src.layer.layerDef;
MetaTagging.addMetatags(
const somethingChanged = MetaTagging.addMetatags(
src.features.data,
{
memberships: this.relationTracker,
@ -412,9 +429,10 @@ export default class FeaturePipeline {
private updateAllMetaTagging() {
const self = this;
console.debug("Updating the meta tagging of all tiles as new data got loaded")
this.perLayerHierarchy.forEach(hierarchy => {
hierarchy.loadedTiles.forEach(src => {
self.applyMetaTags(src)
hierarchy.loadedTiles.forEach(tile => {
self.applyMetaTags(tile)
})
})

View file

@ -1,9 +1,10 @@
import {UIEventSource} from "../../UIEventSource";
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig";
import FilteredLayer from "../../../Models/FilteredLayer";
import {FeatureSourceForLayer, Tiled} from "../FeatureSource";
import Hash from "../../Web/Hash";
import {BBox} from "../../BBox";
import {ElementStorage} from "../../ElementStorage";
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig";
export default class FilteringFeatureSource implements FeatureSourceForLayer, Tiled {
public features: UIEventSource<{ feature: any; freshness: Date }[]> =
@ -12,79 +13,107 @@ export default class FilteringFeatureSource implements FeatureSourceForLayer, Ti
public readonly layer: FilteredLayer;
public readonly tileIndex: number
public readonly bbox: BBox
private readonly upstream: FeatureSourceForLayer;
private readonly state: {
locationControl: UIEventSource<{ zoom: number }>; selectedElement: UIEventSource<any>,
allElements: ElementStorage
};
constructor(
state: {
locationControl: UIEventSource<{ zoom: number }>,
selectedElement: UIEventSource<any>,
allElements: ElementStorage
},
tileIndex,
upstream: FeatureSourceForLayer
) {
const self = this;
this.name = "FilteringFeatureSource(" + upstream.name + ")"
this.tileIndex = tileIndex
this.bbox = BBox.fromTileIndex(tileIndex)
this.upstream = upstream
this.state = state
this.layer = upstream.layer;
const layer = upstream.layer;
function update() {
const features: { feature: any; freshness: Date }[] = upstream.features.data;
const newFeatures = features.filter((f) => {
if (
state.selectedElement.data?.id === f.feature.id ||
f.feature.id === Hash.hash.data) {
// This is the selected object - it gets a free pass even if zoom is not sufficient or it is filtered away
return true;
}
const isShown = layer.layerDef.isShown;
const tags = f.feature.properties;
if (isShown.IsKnown(tags)) {
const result = layer.layerDef.isShown.GetRenderValue(
f.feature.properties
).txt;
if (result !== "yes") {
return false;
}
}
const tagsFilter = layer.appliedFilters.data;
for (const filter of tagsFilter ?? []) {
const neededTags = filter.filter.options[filter.selected].osmTags
if (!neededTags.matchesProperties(f.feature.properties)) {
// Hidden by the filter on the layer itself - we want to hide it no matter wat
return false;
}
}
return true;
});
self.features.setData(newFeatures);
}
const self = this;
upstream.features.addCallback(() => {
update();
self.update();
});
layer.appliedFilters.addCallback(_ => {
update()
self.update()
})
update();
this._is_dirty.stabilized(250).addCallbackAndRunD(dirty => {
if (dirty) {
self.update()
}
})
this.update();
}
private static showLayer(
layer: {
isDisplayed: UIEventSource<boolean>;
layerDef: LayerConfig;
}) {
return layer.isDisplayed.data;
private readonly _alreadyRegistered = new Set<UIEventSource<any>>();
private readonly _is_dirty = new UIEventSource(false)
private registerCallback(feature: any, layer: LayerConfig) {
const src = this.state.allElements.addOrGetElement(feature)
if (this._alreadyRegistered.has(src)) {
return
}
this._alreadyRegistered.add(src)
if (layer.isShown !== undefined) {
const self = this;
src.map(tags => layer.isShown?.GetRenderValue(tags, "yes").txt).addCallbackAndRunD(isShown => {
self._is_dirty.setData(true)
})
}
}
public update() {
const self = this;
const layer = this.upstream.layer;
const features: { feature: any; freshness: Date }[] = this.upstream.features.data;
const newFeatures = features.filter((f) => {
self.registerCallback(f.feature, layer.layerDef)
if (
this.state.selectedElement.data?.id === f.feature.id ||
f.feature.id === Hash.hash.data) {
// This is the selected object - it gets a free pass even if zoom is not sufficient or it is filtered away
return true;
}
const isShown = layer.layerDef.isShown;
const tags = f.feature.properties;
if (isShown.IsKnown(tags)) {
const result = layer.layerDef.isShown.GetRenderValue(
f.feature.properties
).txt;
if (result !== "yes") {
return false;
}
}
const tagsFilter = layer.appliedFilters.data;
for (const filter of tagsFilter ?? []) {
const neededTags = filter.filter.options[filter.selected].osmTags
if (!neededTags.matchesProperties(f.feature.properties)) {
// Hidden by the filter on the layer itself - we want to hide it no matter wat
return false;
}
}
return true;
});
this.features.setData(newFeatures);
this._is_dirty.setData(false)
}
}

View file

@ -31,7 +31,6 @@ export class NewGeometryFromChangesFeatureSource implements FeatureSource {
// Already handled
!seenChanges.has(ch)))
.addCallbackAndRunD(changes => {
if (changes.length === 0) {
return;
}
@ -71,7 +70,7 @@ export class NewGeometryFromChangesFeatureSource implements FeatureSource {
const w = new OsmWay(change.id)
w.tags = tags
w.nodes = change.changes["nodes"]
w.coordinates = change.changes["coordinates"].map(coor => coor.reverse())
w.coordinates = change.changes["coordinates"].map(coor => [coor[1], coor[0]])
add(w.asGeoJson())
break;
case "relation":

View file

@ -0,0 +1,105 @@
/**
* This feature source helps the ShowDataLayer class: it introduces the necessary extra features and indiciates with what renderConfig it should be rendered.
*/
import {UIEventSource} from "../../UIEventSource";
import {GeoOperations} from "../../GeoOperations";
import FeatureSource from "../FeatureSource";
import PointRenderingConfig from "../../../Models/ThemeConfig/PointRenderingConfig";
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig";
export default class RenderingMultiPlexerFeatureSource {
public readonly features: UIEventSource<(any & { pointRenderingIndex: number | undefined, lineRenderingIndex: number | undefined })[]>;
constructor(upstream: FeatureSource, layer: LayerConfig) {
this.features = upstream.features.map(
features => {
if (features === undefined) {
return;
}
const pointRenderObjects: { rendering: PointRenderingConfig, index: number }[] = layer.mapRendering.map((r, i) => ({
rendering: r,
index: i
}))
const pointRenderings = pointRenderObjects.filter(r => r.rendering.location.has("point"))
const centroidRenderings = pointRenderObjects.filter(r => r.rendering.location.has("centroid"))
const startRenderings = pointRenderObjects.filter(r => r.rendering.location.has("start"))
const endRenderings = pointRenderObjects.filter(r => r.rendering.location.has("end"))
const lineRenderObjects = layer.lineRendering
const withIndex: (any & { pointRenderingIndex: number | undefined, lineRenderingIndex: number | undefined })[] = [];
function addAsPoint(feat, rendering, coordinate) {
const patched = {
...feat,
pointRenderingIndex: rendering.index
}
patched.geometry = {
type: "Point",
coordinates: coordinate
}
withIndex.push(patched)
}
for (const f of features) {
const feat = f.feature;
if (feat.geometry.type === "Point") {
for (const rendering of pointRenderings) {
withIndex.push({
...feat,
pointRenderingIndex: rendering.index
})
}
} else {
// This is a a line
for (const rendering of centroidRenderings) {
addAsPoint(feat, rendering, GeoOperations.centerpointCoordinates(feat))
}
if (feat.geometry.type === "LineString") {
const coordinates = feat.geometry.coordinates
for (const rendering of startRenderings) {
addAsPoint(feat, rendering, coordinates[0])
}
for (const rendering of endRenderings) {
const coordinate = coordinates[coordinates.length - 1]
addAsPoint(feat, rendering, coordinate)
}
}
if (feat.geometry.type === "MultiLineString") {
const lineList = feat.geometry.coordinates
for (const coordinates of lineList) {
for (const rendering of startRenderings) {
const coordinate = coordinates[0]
addAsPoint(feat, rendering, coordinate)
}
for (const rendering of endRenderings) {
const coordinate = coordinates[coordinates.length - 1]
addAsPoint(feat, rendering, coordinate)
}
}
}
for (let i = 0; i < lineRenderObjects.length; i++) {
withIndex.push({
...feat,
lineRenderingIndex: i
})
}
}
}
return withIndex;
}
);
}
}

View file

@ -1,8 +1,6 @@
import {UIEventSource} from "../../UIEventSource";
import FilteredLayer from "../../../Models/FilteredLayer";
import {FeatureSourceForLayer, Tiled} from "../FeatureSource";
import {Utils} from "../../../Utils";
import {Tiles} from "../../../Models/TileRange";
import {BBox} from "../../BBox";
export default class SimpleFeatureSource implements FeatureSourceForLayer, Tiled {

View file

@ -1,61 +0,0 @@
/**
* This is the part of the pipeline which introduces extra points at the center of an area (but only if this is demanded by the wayhandling)
*/
import {UIEventSource} from "../../UIEventSource";
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig";
import {GeoOperations} from "../../GeoOperations";
import {FeatureSourceForLayer} from "../FeatureSource";
export default class WayHandlingApplyingFeatureSource implements FeatureSourceForLayer {
public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>;
public readonly name;
public readonly layer;
constructor(upstream: FeatureSourceForLayer) {
this.name = "Wayhandling(" + upstream.name + ")";
this.layer = upstream.layer
const layer = upstream.layer.layerDef;
if (layer.wayHandling === LayerConfig.WAYHANDLING_DEFAULT) {
// We don't have to do anything fancy
// lets just wire up the upstream
this.features = upstream.features;
return;
}
this.features = upstream.features.map(
features => {
if (features === undefined) {
return;
}
const newFeatures: { feature: any, freshness: Date }[] = [];
for (const f of features) {
const feat = f.feature;
if (layer.wayHandling === LayerConfig.WAYHANDLING_DEFAULT) {
newFeatures.push(f);
continue;
}
if (feat.geometry.type === "Point") {
newFeatures.push(f);
// feature is a point, nothing to do here
continue;
}
// Create the copy
const centerPoint = GeoOperations.centerpoint(feat);
newFeatures.push({feature: centerPoint, freshness: f.freshness});
if (layer.wayHandling === LayerConfig.WAYHANDLING_CENTER_AND_WAY) {
newFeatures.push(f);
}
}
return newFeatures;
}
);
}
}

View file

@ -0,0 +1,67 @@
import TileHierarchy from "./TileHierarchy";
import FeatureSource, {FeatureSourceForLayer, Tiled} from "../FeatureSource";
import {OsmNode, OsmObject, OsmWay} from "../../Osm/OsmObject";
import SimpleFeatureSource from "../Sources/SimpleFeatureSource";
import FilteredLayer from "../../../Models/FilteredLayer";
export default class FullNodeDatabaseSource implements TileHierarchy<FeatureSource & Tiled> {
public readonly loadedTiles = new Map<number, FeatureSource & Tiled>()
private readonly onTileLoaded: (tile: (Tiled & FeatureSourceForLayer)) => void;
private readonly layer: FilteredLayer
constructor(
layer: FilteredLayer,
onTileLoaded: ((tile: Tiled & FeatureSourceForLayer) => void)) {
this.onTileLoaded = onTileLoaded
this.layer = layer;
if (this.layer === undefined) {
throw "Layer is undefined"
}
}
public handleOsmJson(osmJson: any, tileId: number) {
const allObjects = OsmObject.ParseObjects(osmJson.elements)
const nodesById = new Map<number, OsmNode>()
for (const osmObj of allObjects) {
if (osmObj.type !== "node") {
continue
}
const osmNode = <OsmNode>osmObj;
nodesById.set(osmNode.id, osmNode)
}
const parentWaysByNodeId = new Map<number, OsmWay[]>()
for (const osmObj of allObjects) {
if (osmObj.type !== "way") {
continue
}
const osmWay = <OsmWay>osmObj;
for (const nodeId of osmWay.nodes) {
if (!parentWaysByNodeId.has(nodeId)) {
parentWaysByNodeId.set(nodeId, [])
}
parentWaysByNodeId.get(nodeId).push(osmWay)
}
}
parentWaysByNodeId.forEach((allWays, nodeId) => {
nodesById.get(nodeId).tags["parent_ways"] = JSON.stringify(allWays.map(w => w.tags))
})
const now = new Date()
const asGeojsonFeatures = Array.from(nodesById.values()).map(osmNode => ({
feature: osmNode.asGeoJson(), freshness: now
}))
const featureSource = new SimpleFeatureSource(this.layer, tileId)
featureSource.features.setData(asGeojsonFeatures)
this.loadedTiles.set(tileId, featureSource)
this.onTileLoaded(featureSource)
}
}

View file

@ -30,6 +30,8 @@ export default class OsmFeatureSource {
};
public readonly downloadedTiles = new Set<number>()
private readonly allowedTags: TagsFilter;
public rawDataHandlers: ((osmJson: any, tileId: number) => void)[] = []
constructor(options: {
handleTile: (tile: FeatureSourceForLayer & Tiled) => void;
@ -66,7 +68,7 @@ export default class OsmFeatureSource {
console.log("Tile download", Tiles.tile_from_index(neededTile).join("/"), "started")
self.downloadedTiles.add(neededTile)
self.LoadTile(...Tiles.tile_from_index(neededTile)).then(_ => {
console.log("Tile ", Tiles.tile_from_index(neededTile).join("/"), "loaded")
console.debug("Tile ", Tiles.tile_from_index(neededTile).join("/"), "loaded")
})
}
} catch (e) {
@ -94,11 +96,11 @@ export default class OsmFeatureSource {
try {
console.log("Attempting to get tile", z, x, y, "from the osm api")
const osmXml = await Utils.download(url, {"accept": "application/xml"})
const osmJson = await Utils.downloadJson(url)
try {
const parsed = new DOMParser().parseFromString(osmXml, "text/xml");
console.log("Got tile", z, x, y, "from the osm api")
const geojson = OsmToGeoJson.default(parsed,
console.debug("Got tile", z, x, y, "from the osm api")
this.rawDataHandlers.forEach(handler => handler(osmJson, Tiles.tile_index(z, x, y)))
const geojson = OsmToGeoJson.default(osmJson,
// @ts-ignore
{
flatProperties: true
@ -108,10 +110,8 @@ export default class OsmFeatureSource {
// We only keep what is needed
geojson.features = geojson.features.filter(feature => this.allowedTags.matchesProperties(feature.properties))
geojson.features.forEach(f => f.properties["_backend"] = this._backend)
console.log("Tile geojson:", z, x, y, "is", geojson)
const index = Tiles.tile_index(z, x, y);
new PerLayerFeatureSourceSplitter(this.filteredLayers,
this.handleTile,

View file

@ -37,8 +37,10 @@ export class GeoOperations {
* The features with which 'feature' overlaps, are returned together with their overlap area in m²
*
* If 'feature' is a LineString, the features in which this feature is (partly) embedded is returned, the overlap length in meter is given
* If 'feature' is a Polygon, overlapping points and points within the polygon will be returned
*
* If 'feature' is a point, it will return every feature the point is embedded in. Overlap will be undefined
*
*/
static calculateOverlap(feature: any, otherFeatures: any[]): { feat: any, overlap: number }[] {
@ -235,6 +237,13 @@ export class GeoOperations {
* @param point Point defined as [lon, lat]
*/
public static nearestPoint(way, point: [number, number]) {
if(way.geometry.type === "Polygon"){
way = {...way}
way.geometry = {...way.geometry}
way.geometry.type = "LineString"
way.geometry.coordinates = way.geometry.coordinates[0]
}
return turf.nearestPointOnLine(way, point, {units: "kilometers"});
}
@ -297,7 +306,7 @@ export class GeoOperations {
return [x, y];
}
//Converts XY point from (Spherical) Web Mercator EPSG:3785 (unofficially EPSG:900913) to lat/lon in WGS84 Datum
//Converts XY point from (Spherical) Web Mercator EPSG:3785 (unofficially EPSG:900913) to lat/lon in WGS84 Datum
public static Convert900913ToWgs84(lonLat: [number, number]): [number, number] {
const lon = lonLat[0]
const lat = lonLat[1]
@ -403,6 +412,31 @@ export class GeoOperations {
return undefined;
}
/**
* Tries to remove points which do not contribute much to the general outline.
* Points for which the angle is ~ 180° are removed
* @param coordinates
* @constructor
*/
public static SimplifyCoordinates(coordinates: [number, number][]){
const newCoordinates = []
for (let i = 1; i < coordinates.length - 1; i++){
const coordinate = coordinates[i];
const prev = coordinates[i - 1]
const next = coordinates[i + 1]
const b0 = turf.bearing(prev, coordinate, {final: true})
const b1 = turf.bearing(coordinate, next)
const diff = Math.abs(b1 - b0)
if(diff < 2){
continue
}
newCoordinates.push(coordinate)
}
return newCoordinates
}
}

View file

@ -18,6 +18,8 @@ export default class MetaTagging {
/**
* This method (re)calculates all metatags and calculated tags on every given object.
* The given features should be part of the given layer
*
* Returns true if at least one feature has changed properties
*/
public static addMetatags(features: { feature: any; freshness: Date }[],
params: ExtraFuncParams,
@ -25,7 +27,7 @@ export default class MetaTagging {
options?: {
includeDates?: true | boolean,
includeNonDates?: true | boolean
}) {
}): boolean {
if (features === undefined || features.length === 0) {
return;
@ -48,6 +50,7 @@ export default class MetaTagging {
// The calculated functions - per layer - which add the new keys
const layerFuncs = this.createRetaggingFunc(layer)
let atLeastOneFeatureChanged = false;
for (let i = 0; i < features.length; i++) {
const ff = features[i];
@ -95,8 +98,10 @@ export default class MetaTagging {
if (somethingChanged) {
State.state?.allElements?.getEventSourceById(feature.properties.id)?.ping()
atLeastOneFeatureChanged = true
}
}
return atLeastOneFeatureChanged
}

View file

@ -16,7 +16,7 @@ export interface ChangeDescription {
/**
* The type of the change
*/
changeType: "answer" | "create" | "split" | "delete" | "move" | string
changeType: "answer" | "create" | "split" | "delete" | "move" | "import" | string | null
/**
* THe motivation for the change, e.g. 'deleted because does not exist anymore'
*/
@ -51,7 +51,8 @@ export interface ChangeDescription {
lat: number,
lon: number
} | {
// Coordinates are only used for rendering. They should be LAT, LON
/* Coordinates are only used for rendering. They should be LON, LAT
* */
coordinates: [number, number][]
nodes: number[],
} | {

View file

@ -11,7 +11,7 @@ export default class ChangeTagAction extends OsmChangeAction {
constructor(elementId: string, tagsFilter: TagsFilter, currentTags: any, meta: {
theme: string,
changeType: "answer" | "soft-delete" | "add-image"
changeType: "answer" | "soft-delete" | "add-image" | string
}) {
super();
this._elementId = elementId;
@ -27,11 +27,16 @@ export default class ChangeTagAction extends OsmChangeAction {
const key = kv.k;
const value = kv.v;
if (key === undefined || key === null) {
console.log("Invalid key");
console.error("Invalid key:", key);
return undefined;
}
if (value === undefined || value === null) {
console.log("Invalid value for ", key);
console.error("Invalid value for ", key,":", value);
return undefined;
}
if(typeof value !== "string"){
console.error("Invalid value for ", key, "as it is not a string:", value)
return undefined;
}

View file

@ -8,20 +8,29 @@ import {GeoOperations} from "../../GeoOperations";
export default class CreateNewNodeAction extends OsmChangeAction {
/**
* Maps previously created points onto their assigned ID, to reuse the point if uplaoded
* "lat,lon" --> id
*/
private static readonly previouslyCreatedPoints = new Map<string, number>()
public newElementId: string = undefined
public newElementIdNumber: number = undefined
private readonly _basicTags: Tag[];
private readonly _lat: number;
private readonly _lon: number;
private readonly _snapOnto: OsmWay;
private readonly _reusePointDistance: number;
private meta: { changeType: "create" | "import"; theme: string };
private readonly _reusePreviouslyCreatedPoint: boolean;
constructor(basicTags: Tag[],
lat: number, lon: number,
options: {
snapOnto?: OsmWay,
reusePointWithinMeters?: number,
theme: string, changeType: "create" | "import" }) {
allowReuseOfPreviouslyCreatedPoints?: boolean,
snapOnto?: OsmWay,
reusePointWithinMeters?: number,
theme: string, changeType: "create" | "import" | null
}) {
super()
this._basicTags = basicTags;
this._lat = lat;
@ -31,18 +40,47 @@ export default class CreateNewNodeAction extends OsmChangeAction {
}
this._snapOnto = options?.snapOnto;
this._reusePointDistance = options?.reusePointWithinMeters ?? 1
this._reusePreviouslyCreatedPoint = options?.allowReuseOfPreviouslyCreatedPoints ?? (basicTags.length === 0)
this.meta = {
theme: options.theme,
changeType: options.changeType
}
}
public static registerIdRewrites(mappings: Map<string, string>) {
const toAdd: [string, number][] = []
this.previouslyCreatedPoints.forEach((oldId, key) => {
if (!mappings.has("node/" + oldId)) {
return;
}
const newId = Number(mappings.get("node/" + oldId).substr("node/".length))
toAdd.push([key, newId])
})
for (const [key, newId] of toAdd) {
CreateNewNodeAction.previouslyCreatedPoints.set(key, newId)
}
}
async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
if (this._reusePreviouslyCreatedPoint) {
const key = this._lat + "," + this._lon
const prev = CreateNewNodeAction.previouslyCreatedPoints
if (prev.has(key)) {
this.newElementIdNumber = prev.get(key)
this.newElementId = "node/" + this.newElementIdNumber
return []
}
}
const id = changes.getNewID()
const properties = {
id: "node/" + id
}
this.newElementId = "node/" + id
this.setElementId(id)
for (const kv of this._basicTags) {
if (typeof kv.value !== "string") {
throw "Invalid value: don't use a regex in a preset"
@ -84,8 +122,7 @@ export default class CreateNewNodeAction extends OsmChangeAction {
}
if (reusedPointId !== undefined) {
console.log("Reusing an existing point:", reusedPointId)
this.newElementId = "node/" + reusedPointId
this.setElementId(reusedPointId)
return [{
tags: new And(this._basicTags).asChange(properties),
type: "node",
@ -112,10 +149,20 @@ export default class CreateNewNodeAction extends OsmChangeAction {
coordinates: locations,
nodes: ids
},
meta:this.meta
meta: this.meta
}
]
}
private setElementId(id: number) {
this.newElementIdNumber = id;
this.newElementId = "node/"+id
if (!this._reusePreviouslyCreatedPoint) {
return
}
const key = this._lat + "," + this._lon
CreateNewNodeAction.previouslyCreatedPoints.set(key, id)
}
}

View file

@ -0,0 +1,78 @@
import {ChangeDescription} from "./ChangeDescription";
import OsmChangeAction from "./OsmChangeAction";
import {Changes} from "../Changes";
import {Tag} from "../../Tags/Tag";
import CreateNewNodeAction from "./CreateNewNodeAction";
import {And} from "../../Tags/And";
export default class CreateNewWayAction extends OsmChangeAction {
public newElementId: string = undefined
private readonly coordinates: ({ nodeId?: number, lat: number, lon: number })[];
private readonly tags: Tag[];
private readonly _options: {
theme: string
};
/***
* Creates a new way to upload to OSM
* @param tags: the tags to apply to the way
* @param coordinates: the coordinates. Might have a nodeId, in this case, this node will be used
* @param options
*/
constructor(tags: Tag[], coordinates: ({ nodeId?: number, lat: number, lon: number })[],
options: {
theme: string
}) {
super()
this.coordinates = coordinates;
this.tags = tags;
this._options = options;
}
protected async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
const newElements: ChangeDescription[] = []
const pointIds: number[] = []
for (const coordinate of this.coordinates) {
if (coordinate.nodeId !== undefined) {
pointIds.push(coordinate.nodeId)
continue
}
const newPoint = new CreateNewNodeAction([], coordinate.lat, coordinate.lon, {
allowReuseOfPreviouslyCreatedPoints: true,
changeType: null,
theme: this._options.theme
})
await changes.applyAction(newPoint)
pointIds.push(newPoint.newElementIdNumber)
}
// We have all created (or reused) all the points!
// Time to create the actual way
const id = changes.getNewID()
const newWay = <ChangeDescription>{
id,
type: "way",
meta: {
theme: this._options.theme,
changeType: "import"
},
tags: new And(this.tags).asChange({}),
changes: {
nodes: pointIds,
coordinates: this.coordinates.map(c => [c.lon, c.lat])
}
}
newElements.push(newWay)
this.newElementId = "way/" + id
return newElements
}
}

View file

@ -0,0 +1,313 @@
import OsmChangeAction from "./OsmChangeAction";
import {Tag} from "../../Tags/Tag";
import {Changes} from "../Changes";
import {ChangeDescription} from "./ChangeDescription";
import FeaturePipelineState from "../../State/FeaturePipelineState";
import {BBox} from "../../BBox";
import {TagsFilter} from "../../Tags/TagsFilter";
import {GeoOperations} from "../../GeoOperations";
import FeatureSource from "../../FeatureSource/FeatureSource";
import StaticFeatureSource from "../../FeatureSource/Sources/StaticFeatureSource";
import CreateNewNodeAction from "./CreateNewNodeAction";
import CreateNewWayAction from "./CreateNewWayAction";
export interface MergePointConfig {
withinRangeOfM: number,
ifMatches: TagsFilter,
mode: "reuse_osm_point" | "move_osm_point"
}
interface CoordinateInfo {
lngLat: [number, number],
identicalTo?: number,
closebyNodes?: {
d: number,
node: any,
config: MergePointConfig
}[]
}
/**
* More or less the same as 'CreateNewWay', except that it'll try to reuse already existing points
*/
export default class CreateWayWithPointReuseAction extends OsmChangeAction {
private readonly _tags: Tag[];
/**
* lngLat-coordinates
* @private
*/
private _coordinateInfo: CoordinateInfo[];
private _state: FeaturePipelineState;
private _config: MergePointConfig[];
constructor(tags: Tag[],
coordinates: [number, number][],
state: FeaturePipelineState,
config: MergePointConfig[]
) {
super();
this._tags = tags;
this._state = state;
this._config = config;
this._coordinateInfo = this.CalculateClosebyNodes(coordinates);
}
public async getPreview(): Promise<FeatureSource> {
const features = []
let geometryMoved = false;
for (let i = 0; i < this._coordinateInfo.length; i++) {
const coordinateInfo = this._coordinateInfo[i];
if (coordinateInfo.identicalTo !== undefined) {
continue
}
if (coordinateInfo.closebyNodes === undefined || coordinateInfo.closebyNodes.length === 0) {
const newPoint = {
type: "Feature",
properties: {
"newpoint": "yes",
id: "new-geometry-with-reuse-" + i
},
geometry: {
type: "Point",
coordinates: coordinateInfo.lngLat
}
};
features.push(newPoint)
continue
}
const reusedPoint = coordinateInfo.closebyNodes[0]
if (reusedPoint.config.mode === "move_osm_point") {
const moveDescription = {
type: "Feature",
properties: {
"move": "yes",
"osm-id": reusedPoint.node.properties.id,
"id": "new-geometry-move-existing" + i
},
geometry: {
type: "LineString",
coordinates: [reusedPoint.node.geometry.coordinates, coordinateInfo.lngLat]
}
}
features.push(moveDescription)
} else {
// The geometry is moved
geometryMoved = true
}
}
if (geometryMoved) {
const coords: [number, number][] = []
for (const info of this._coordinateInfo) {
if (info.identicalTo !== undefined) {
coords.push(coords[info.identicalTo])
continue
}
if (info.closebyNodes === undefined || info.closebyNodes.length === 0) {
coords.push(coords[info.identicalTo])
continue
}
const closest = info.closebyNodes[0]
if (closest.config.mode === "reuse_osm_point") {
coords.push(closest.node.geometry.coordinates)
} else {
coords.push(info.lngLat)
}
}
const newGeometry = {
type: "Feature",
properties: {
"resulting-geometry": "yes",
"id": "new-geometry"
},
geometry: {
type: "LineString",
coordinates: coords
}
}
features.push(newGeometry)
}
console.log("Preview:", features)
return new StaticFeatureSource(features, false)
}
protected async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
const theme = this._state.layoutToUse.id
const allChanges: ChangeDescription[] = []
const nodeIdsToUse: { lat: number, lon: number, nodeId?: number }[] = []
for (let i = 0; i < this._coordinateInfo.length; i++) {
const info = this._coordinateInfo[i]
const lat = info.lngLat[1]
const lon = info.lngLat[0]
if (info.identicalTo !== undefined) {
nodeIdsToUse.push(nodeIdsToUse[info.identicalTo])
continue
}
if (info.closebyNodes === undefined || info.closebyNodes[0] === undefined) {
const newNodeAction = new CreateNewNodeAction([], lat, lon, {
allowReuseOfPreviouslyCreatedPoints: true,
changeType: null,
theme
})
allChanges.push(...(await newNodeAction.CreateChangeDescriptions(changes)))
nodeIdsToUse.push({
lat, lon,
nodeId : newNodeAction.newElementIdNumber})
continue
}
const closestPoint = info.closebyNodes[0]
const id = Number(closestPoint.node.properties.id.split("/")[1])
if(closestPoint.config.mode === "move_osm_point"){
allChanges.push({
type: "node",
id,
changes: {
lat, lon
},
meta: {
theme,
changeType: null
}
})
}
nodeIdsToUse.push({lat, lon, nodeId: id})
}
const newWay = new CreateNewWayAction(this._tags, nodeIdsToUse, {
theme
})
allChanges.push(...(await newWay.Perform(changes)))
return allChanges
}
private CalculateClosebyNodes(coordinates: [number, number][]): CoordinateInfo[] {
const bbox = new BBox(coordinates)
const state = this._state
const allNodes = [].concat(...state.featurePipeline.GetFeaturesWithin("type_node", bbox.pad(1.2)))
const maxDistance = Math.max(...this._config.map(c => c.withinRangeOfM))
const coordinateInfo: {
lngLat: [number, number],
identicalTo?: number,
closebyNodes?: {
d: number,
node: any,
config: MergePointConfig
}[]
}[] = coordinates.map(_ => undefined)
for (let i = 0; i < coordinates.length; i++) {
if (coordinateInfo[i] !== undefined) {
// Already seen, probably a duplicate coordinate
continue
}
const coor = coordinates[i]
// Check closeby (and probably identical) point further in the coordinate list, mark them as duplicate
for (let j = i + 1; j < coordinates.length; j++) {
if (1000 * GeoOperations.distanceBetween(coor, coordinates[j]) < 0.1) {
coordinateInfo[j] = {
lngLat: coor,
identicalTo: i
}
break;
}
}
// Gather the actual info for this point
// Lets search applicable points and determine the merge mode
const closebyNodes: {
d: number,
node: any,
config: MergePointConfig
}[] = []
for (const node of allNodes) {
const center = node.geometry.coordinates
const d = 1000 * GeoOperations.distanceBetween(coor, center)
if (d > maxDistance) {
continue
}
for (const config of this._config) {
if (d > config.withinRangeOfM) {
continue
}
if (!config.ifMatches.matchesProperties(node.properties)) {
continue
}
closebyNodes.push({node, d, config})
}
}
closebyNodes.sort((n0, n1) => {
return n0.d - n1.d
})
coordinateInfo[i] = {
identicalTo: undefined,
lngLat: coor,
closebyNodes
}
}
let conflictFree = true;
do {
conflictFree = true;
for (let i = 0; i < coordinateInfo.length; i++) {
const coorInfo = coordinateInfo[i]
if (coorInfo.identicalTo !== undefined) {
continue
}
if (coorInfo.closebyNodes === undefined || coorInfo.closebyNodes[0] === undefined) {
continue
}
for (let j = i + 1; j < coordinates.length; j++) {
const other = coordinateInfo[j]
if (other.closebyNodes === undefined || other.closebyNodes[0] === undefined) {
continue
}
if (other.closebyNodes[0].node === coorInfo.closebyNodes[0].node) {
conflictFree = false
// We have found a conflict!
// We only keep the closest point
if (other.closebyNodes[0].d > coorInfo.closebyNodes[0].d) {
other.closebyNodes.shift()
} else {
coorInfo.closebyNodes.shift()
}
}
}
}
} while (!conflictFree)
return coordinateInfo
}
}

View file

@ -0,0 +1,278 @@
import OsmChangeAction from "./OsmChangeAction";
import {Changes} from "../Changes";
import {ChangeDescription} from "./ChangeDescription";
import {Tag} from "../../Tags/Tag";
import FeatureSource from "../../FeatureSource/FeatureSource";
import {OsmNode, OsmObject, OsmWay} from "../OsmObject";
import {GeoOperations} from "../../GeoOperations";
import StaticFeatureSource from "../../FeatureSource/Sources/StaticFeatureSource";
import CreateNewNodeAction from "./CreateNewNodeAction";
import ChangeTagAction from "./ChangeTagAction";
import {And} from "../../Tags/And";
import {Utils} from "../../../Utils";
import {OsmConnection} from "../OsmConnection";
export default class ReplaceGeometryAction extends OsmChangeAction {
private readonly feature: any;
private readonly state: {
osmConnection: OsmConnection
};
private readonly wayToReplaceId: string;
private readonly theme: string;
/**
* The target coordinates that should end up in OpenStreetMap
*/
private readonly targetCoordinates: [number, number][];
/**
* If a target coordinate is close to another target coordinate, 'identicalTo' will point to the first index.
* @private
*/
private readonly identicalTo: number[]
private readonly newTags: Tag[] | undefined;
constructor(
state: {
osmConnection: OsmConnection
},
feature: any,
wayToReplaceId: string,
options: {
theme: string,
newTags?: Tag[]
}
) {
super();
this.state = state;
this.feature = feature;
this.wayToReplaceId = wayToReplaceId;
this.theme = options.theme;
const geom = this.feature.geometry
let coordinates: [number, number][]
if (geom.type === "LineString") {
coordinates = geom.coordinates
} else if (geom.type === "Polygon") {
coordinates = geom.coordinates[0]
}
this.identicalTo = coordinates.map(_ => undefined)
for (let i = 0; i < coordinates.length; i++) {
if (this.identicalTo[i] !== undefined) {
continue
}
for (let j = i + 1; j < coordinates.length; j++) {
const d = 1000 * GeoOperations.distanceBetween(coordinates[i], coordinates[j])
if (d < 0.1) {
console.log("Identical coordinates detected: ", i, " and ", j, ": ", coordinates[i], coordinates[j], "distance is", d)
this.identicalTo[j] = i
}
}
}
this.targetCoordinates = coordinates
this.newTags = options.newTags
}
public async getPreview(): Promise<FeatureSource> {
const {closestIds, allNodesById} = await this.GetClosestIds();
console.log("Generating preview, identicals are ", )
const preview = closestIds.map((newId, i) => {
if(this.identicalTo[i] !== undefined){
return undefined
}
if (newId === undefined) {
return {
type: "Feature",
properties: {
"newpoint": "yes",
"id": "replace-geometry-move-" + i
},
geometry: {
type: "Point",
coordinates: this.targetCoordinates[i]
}
};
}
const origPoint = allNodesById.get(newId).centerpoint()
return {
type: "Feature",
properties: {
"move": "yes",
"osm-id": newId,
"id": "replace-geometry-move-" + i
},
geometry: {
type: "LineString",
coordinates: [[origPoint[1], origPoint[0]], this.targetCoordinates[i]]
}
};
})
return new StaticFeatureSource(Utils.NoNull(preview), false)
}
protected async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
const allChanges: ChangeDescription[] = []
const actualIdsToUse: number[] = []
const {closestIds, osmWay} = await this.GetClosestIds()
for (let i = 0; i < closestIds.length; i++) {
if(this.identicalTo[i] !== undefined){
const j = this.identicalTo[i]
actualIdsToUse.push(actualIdsToUse[j])
continue
}
const closestId = closestIds[i];
const [lon, lat] = this.targetCoordinates[i]
if (closestId === undefined) {
const newNodeAction = new CreateNewNodeAction(
[],
lat, lon,
{
allowReuseOfPreviouslyCreatedPoints: true,
theme: this.theme, changeType: null
})
const changeDescr = await newNodeAction.CreateChangeDescriptions(changes)
allChanges.push(...changeDescr)
actualIdsToUse.push(newNodeAction.newElementIdNumber)
} else {
const change = <ChangeDescription>{
id: closestId,
type: "node",
meta: {
theme: this.theme,
changeType: "move"
},
changes: {lon, lat}
}
actualIdsToUse.push(closestId)
allChanges.push(change)
}
}
if (this.newTags !== undefined && this.newTags.length > 0) {
const addExtraTags = new ChangeTagAction(
this.wayToReplaceId,
new And(this.newTags),
osmWay.tags, {
theme: this.theme,
changeType: "conflation"
}
)
allChanges.push(...await addExtraTags.CreateChangeDescriptions(changes))
}
// AT the very last: actually change the nodes of the way!
allChanges.push({
type: "way",
id: osmWay.id,
changes: {
nodes: actualIdsToUse,
coordinates: this.targetCoordinates
},
meta: {
theme: this.theme,
changeType: "conflation"
}
})
return allChanges
}
/**
* For 'this.feature`, gets a corresponding closest node that alreay exsists
* @constructor
* @private
*/
private async GetClosestIds(): Promise<{ closestIds: number[], allNodesById: Map<number, OsmNode>, osmWay: OsmWay }> {
// TODO FIXME: cap move length on points which are embedded into other ways (ev. disconnect them)
// TODO FIXME: if a new point has to be created, snap to already existing ways
// TODO FIXME: detect intersections with other ways if moved
const splitted = this.wayToReplaceId.split("/");
const type = splitted[0];
const idN = Number(splitted[1]);
if (idN < 0 || type !== "way") {
throw "Invalid ID to conflate: " + this.wayToReplaceId
}
const url = `${this.state.osmConnection._oauth_config.url}/api/0.6/${this.wayToReplaceId}/full`;
const rawData = await Utils.downloadJsonCached(url, 1000)
const parsed = OsmObject.ParseObjects(rawData.elements);
const allNodesById = new Map<number, OsmNode>()
const allNodes = parsed.filter(o => o.type === "node")
for (const node of allNodes) {
allNodesById.set(node.id, <OsmNode>node)
}
/**
* Allright! We know all the nodes of the original way and all the nodes of the target coordinates.
* For each of the target coordinates, we search the closest, already existing point and reuse this point
*/
const closestIds = []
const distances = []
for (let i = 0; i < this.targetCoordinates.length; i++){
const target = this.targetCoordinates[i];
let closestDistance = undefined
let closestId = undefined;
for (const osmNode of allNodes) {
const cp = osmNode.centerpoint()
const d = GeoOperations.distanceBetween(target, [cp[1], cp[0]])
if (closestId === undefined || closestDistance > d) {
closestId = osmNode.id
closestDistance = d
}
}
closestIds.push(closestId)
distances.push(closestDistance)
}
// Next step: every closestId can only occur once in the list
// We skip the ones which are identical
console.log("Erasing double ids")
for (let i = 0; i < closestIds.length; i++) {
if(this.identicalTo[i] !== undefined){
closestIds[i] = closestIds[this.identicalTo[i]]
continue
}
const closestId = closestIds[i]
for (let j = i + 1; j < closestIds.length; j++) {
if(this.identicalTo[j] !== undefined){
continue
}
const otherClosestId = closestIds[j]
if (closestId !== otherClosestId) {
continue
}
// We have two occurences of 'closestId' - we only keep the closest instance!
const di = distances[i]
const dj = distances[j]
if (di < dj) {
closestIds[j] = undefined
} else {
closestIds[i] = undefined
}
}
}
const osmWay = <OsmWay>parsed[parsed.length - 1]
if (osmWay.type !== "way") {
throw "WEIRD: expected an OSM-way as last element here!"
}
return {closestIds, allNodesById, osmWay};
}
}

View file

@ -6,6 +6,8 @@ import OsmChangeAction from "./Actions/OsmChangeAction";
import {ChangeDescription} from "./Actions/ChangeDescription";
import {Utils} from "../../Utils";
import {LocalStorageSource} from "../Web/LocalStorageSource";
import SimpleMetaTagger from "../SimpleMetaTagger";
import CreateNewNodeAction from "./Actions/CreateNewNodeAction";
/**
* Handles all changes made to OSM.
@ -13,21 +15,21 @@ import {LocalStorageSource} from "../Web/LocalStorageSource";
*/
export class Changes {
private _nextId: number = -1; // Newly assigned ID's are negative
public readonly name = "Newly added features"
/**
* All the newly created features as featureSource + all the modified features
*/
public features = new UIEventSource<{ feature: any, freshness: Date }[]>([]);
public readonly pendingChanges: UIEventSource<ChangeDescription[]> = LocalStorageSource.GetParsed<ChangeDescription[]>("pending-changes", [])
public readonly allChanges = new UIEventSource<ChangeDescription[]>(undefined)
private _nextId: number = -1; // Newly assigned ID's are negative
private readonly isUploading = new UIEventSource(false);
private readonly previouslyCreated: OsmObject[] = []
private readonly _leftRightSensitive: boolean;
constructor() {
constructor(leftRightSensitive: boolean = false) {
this._leftRightSensitive = leftRightSensitive;
// We keep track of all changes just as well
this.allChanges.setData([...this.pendingChanges.data])
// If a pending change contains a negative ID, we save that
@ -111,16 +113,44 @@ export class Changes {
})
}
public async applyAction(action: OsmChangeAction): Promise<void> {
this.applyChanges(await action.Perform(this))
}
public async applyActions(actions: OsmChangeAction[]) {
for (const action of actions) {
await this.applyAction(action)
}
}
public applyChanges(changes: ChangeDescription[]) {
console.log("Received changes:", changes)
this.pendingChanges.data.push(...changes);
this.pendingChanges.ping();
this.allChanges.data.push(...changes)
this.allChanges.ping()
}
public registerIdRewrites(mappings: Map<string, string>): void {
CreateNewNodeAction.registerIdRewrites(mappings)
}
/**
* UPload the selected changes to OSM.
* Returns 'true' if successfull and if they can be removed
* @param pending
* @private
*/
private async flushSelectChanges(pending: ChangeDescription[]): Promise<boolean>{
private async flushSelectChanges(pending: ChangeDescription[]): Promise<boolean> {
const self = this;
const neededIds = Changes.GetNeededIds(pending)
const osmObjects = await Promise.all(neededIds.map(id => OsmObject.DownloadObjectAsync(id)));
if (this._leftRightSensitive) {
osmObjects.forEach(obj => SimpleMetaTagger.removeBothTagging(obj.tags))
}
console.log("Got the fresh objects!", osmObjects, "pending: ", pending)
const changes: {
newObjects: OsmObject[],
@ -129,35 +159,38 @@ export class Changes {
} = self.CreateChangesetObjects(pending, osmObjects)
if (changes.newObjects.length + changes.deletedObjects.length + changes.modifiedObjects.length === 0) {
console.log("No changes to be made")
return true
return true
}
const meta = pending[0].meta
const perType = Array.from(Utils.Hist(pending.map(descr => descr.meta.changeType)), ([key, count]) => ({
key: key,
value: count,
aggregate: true
}))
const perType = Array.from(
Utils.Hist(pending.filter(descr => descr.meta.changeType !== undefined && descr.meta.changeType !== null)
.map(descr => descr.meta.changeType)), ([key, count]) => (
{
key: key,
value: count,
aggregate: true
}))
const motivations = pending.filter(descr => descr.meta.specialMotivation !== undefined)
.map(descr => ({
key: descr.meta.changeType+":"+descr.type+"/"+descr.id,
value: descr.meta.specialMotivation
key: descr.meta.changeType + ":" + descr.type + "/" + descr.id,
value: descr.meta.specialMotivation
}))
const metatags = [{
key: "comment",
value: "Adding data with #MapComplete for theme #"+meta.theme
value: "Adding data with #MapComplete for theme #" + meta.theme
},
{
key:"theme",
value:meta.theme
key: "theme",
value: meta.theme
},
...perType,
...motivations
]
await State.state.osmConnection.changesetHandler.UploadChangeset(
(csId) => Changes.createChangesetFor(""+csId, changes),
(csId) => Changes.createChangesetFor("" + csId, changes),
metatags
)
@ -170,27 +203,27 @@ export class Changes {
try {
// At last, we build the changeset and upload
const pending = self.pendingChanges.data;
const pendingPerTheme = new Map<string, ChangeDescription[]>()
for (const changeDescription of pending) {
const theme = changeDescription.meta.theme
if(!pendingPerTheme.has(theme)){
if (!pendingPerTheme.has(theme)) {
pendingPerTheme.set(theme, [])
}
pendingPerTheme.get(theme).push(changeDescription)
}
const successes = await Promise.all(Array.from(pendingPerTheme, ([key , value]) => value)
const successes = await Promise.all(Array.from(pendingPerTheme, ([key, value]) => value)
.map(async pendingChanges => {
try{
try {
return await self.flushSelectChanges(pendingChanges);
}catch(e){
console.error("Could not upload some changes:",e)
} catch (e) {
console.error("Could not upload some changes:", e)
return false
}
}))
if(!successes.some(s => s == false)){
if (!successes.some(s => s == false)) {
// All changes successfull, we clear the data!
this.pendingChanges.setData([]);
}
@ -198,22 +231,13 @@ export class Changes {
} catch (e) {
console.error("Could not handle changes - probably an old, pending changeset in localstorage with an invalid format; erasing those", e)
self.pendingChanges.setData([])
}finally {
} finally {
self.isUploading.setData(false)
}
}
public async applyAction(action: OsmChangeAction): Promise<void> {
const changes = await action.Perform(this)
console.log("Received changes:", changes)
this.pendingChanges.data.push(...changes);
this.pendingChanges.ping();
this.allChanges.data.push(...changes)
this.allChanges.ping()
}
private CreateChangesetObjects(changes: ChangeDescription[], downloadedOsmObjects: OsmObject[]): {
newObjects: OsmObject[],
modifiedObjects: OsmObject[]
@ -365,8 +389,4 @@ export class Changes {
return result
}
public registerIdRewrites(mappings: Map<string, string>): void {
}
}

View file

@ -206,7 +206,7 @@ export abstract class OsmObject {
return result;
}
private static ParseObjects(elements: any[]): OsmObject[] {
public static ParseObjects(elements: any[]): OsmObject[] {
const objects: OsmObject[] = [];
const allNodes: Map<number, OsmNode> = new Map<number, OsmNode>()

View file

@ -26,7 +26,7 @@ export default class SimpleMetaTagger {
"_last_edit:changeset",
"_last_edit:timestamp",
"_version_number",
"_backend"],
"_backend"],
doc: "Information about the last edit of this object."
},
(feature) => {/*Note: also called by 'UpdateTagsFromOsmAPI'*/
@ -67,17 +67,110 @@ export default class SimpleMetaTagger {
private static layerInfo = new SimpleMetaTagger(
{
doc: "The layer-id to which this feature belongs. Note that this might be return any applicable if `passAllFeatures` is defined.",
keys:["_layer"],
keys: ["_layer"],
includesDates: false,
},
(feature, freshness, layer) => {
if(feature.properties._layer === layer.id){
if (feature.properties._layer === layer.id) {
return false;
}
feature.properties._layer = layer.id
return true;
}
)
/**
* Edits the given object to rewrite 'both'-tagging into a 'left-right' tagging scheme.
* These changes are performed in-place.
*
* Returns 'true' is at least one change has been made
* @param tags
*/
public static removeBothTagging(tags: any): boolean{
let somethingChanged = false
/**
* Sets the key onto the properties (but doesn't overwrite if already existing)
*/
function set(k, value) {
if (tags[k] === undefined || tags[k] === "") {
tags[k] = value
somethingChanged = true
}
}
if (tags["sidewalk"]) {
const v = tags["sidewalk"]
switch (v) {
case "none":
case "no":
set("sidewalk:left", "no");
set("sidewalk:right", "no");
break
case "both":
set("sidewalk:left", "yes");
set("sidewalk:right", "yes");
break;
case "left":
set("sidewalk:left", "yes");
set("sidewalk:right", "no");
break;
case "right":
set("sidewalk:left", "no");
set("sidewalk:right", "yes");
break;
default:
set("sidewalk:left", v);
set("sidewalk:right", v);
break;
}
delete tags["sidewalk"]
somethingChanged = true
}
const regex = /\([^:]*\):both:\(.*\)/
for (const key in tags) {
const v = tags[key]
if (key.endsWith(":both")) {
const strippedKey = key.substring(0, key.length - ":both".length)
set(strippedKey + ":left", v)
set(strippedKey + ":right", v)
delete tags[key]
continue
}
const match = key.match(regex)
if (match !== null) {
const strippedKey = match[1]
const property = match[1]
set(strippedKey + ":left:" + property, v)
set(strippedKey + ":right:" + property, v)
console.log("Left-right rewritten " + key)
delete tags[key]
}
}
return somethingChanged
}
private static noBothButLeftRight = new SimpleMetaTagger(
{
keys: ["sidewalk:left", "sidewalk:right", "generic_key:left:property", "generic_key:right:property"],
doc: "Rewrites tags from 'generic_key:both:property' as 'generic_key:left:property' and 'generic_key:right:property' (and similar for sidewalk tagging). Note that this rewritten tags _will be reuploaded on a change_. To prevent to much unrelated retagging, this is only enabled if the layer has at least some lineRenderings with offset defined",
includesDates: false,
cleanupRetagger: true
},
((feature, state, layer) => {
if(!layer.lineRendering.some(lr => lr.leftRightSensitive)){
return;
}
return SimpleMetaTagger.removeBothTagging(feature.properties)
})
)
private static surfaceArea = new SimpleMetaTagger(
{
keys: ["_surface", "_surface:ha"],
@ -85,12 +178,12 @@ export default class SimpleMetaTagger {
isLazy: true
},
(feature => {
Object.defineProperty(feature.properties, "_surface", {
enumerable: false,
configurable: true,
get: () => {
const sqMeters = ""+ GeoOperations.surfaceAreaInSqMeters(feature);
const sqMeters = "" + GeoOperations.surfaceAreaInSqMeters(feature);
delete feature.properties["_surface"]
feature.properties["_surface"] = sqMeters;
return sqMeters
@ -108,7 +201,7 @@ export default class SimpleMetaTagger {
return sqMetersHa
}
})
return true;
})
);
@ -219,8 +312,8 @@ export default class SimpleMetaTagger {
// isOpen is irrelevant
return false
}
Object.defineProperty(feature.properties, "_isOpen",{
Object.defineProperty(feature.properties, "_isOpen", {
enumerable: false,
configurable: true,
get: () => {
@ -247,7 +340,7 @@ export default class SimpleMetaTagger {
if (oldNextChange > (new Date()).getTime() &&
tags["_isOpen:oldvalue"] === tags["opening_hours"]
&& tags["_isOpen"] !== undefined) {
&& tags["_isOpen"] !== undefined) {
// Already calculated and should not yet be triggered
return false;
}
@ -354,7 +447,8 @@ export default class SimpleMetaTagger {
SimpleMetaTagger.isOpen,
SimpleMetaTagger.directionSimplified,
SimpleMetaTagger.currentTime,
SimpleMetaTagger.objectMetaInfo
SimpleMetaTagger.objectMetaInfo,
SimpleMetaTagger.noBothButLeftRight
];
public static readonly lazyTags: string[] = [].concat(...SimpleMetaTagger.metatags.filter(tagger => tagger.isLazy)
@ -365,22 +459,24 @@ export default class SimpleMetaTagger {
public readonly isLazy: boolean;
public readonly includesDates: boolean
public readonly applyMetaTagsOnFeature: (feature: any, freshness: Date, layer: LayerConfig) => boolean;
/***
* A function that adds some extra data to a feature
* @param docs: what does this extra data do?
* @param f: apply the changes. Returns true if something changed
*/
constructor(docs: { keys: string[], doc: string, includesDates?: boolean, isLazy?: boolean },
constructor(docs: { keys: string[], doc: string, includesDates?: boolean, isLazy?: boolean, cleanupRetagger?: boolean },
f: ((feature: any, freshness: Date, layer: LayerConfig) => boolean)) {
this.keys = docs.keys;
this.doc = docs.doc;
this.isLazy = docs.isLazy
this.applyMetaTagsOnFeature = f;
this.includesDates = docs.includesDates ?? false;
for (const key of docs.keys) {
if (!key.startsWith('_') && key.toLowerCase().indexOf("theme") < 0) {
throw `Incorrect metakey ${key}: it should start with underscore (_)`
if (!docs.cleanupRetagger) {
for (const key of docs.keys) {
if (!key.startsWith('_') && key.toLowerCase().indexOf("theme") < 0) {
throw `Incorrect metakey ${key}: it should start with underscore (_)`
}
}
}
}

View file

@ -15,7 +15,7 @@ import TitleHandler from "../Actors/TitleHandler";
/**
* The part of the state keeping track of where the elements, loading them, configuring the feature pipeline etc
*/
export default class ElementsState extends FeatureSwitchState{
export default class ElementsState extends FeatureSwitchState {
/**
The mapping from id -> UIEventSource<properties>
@ -24,7 +24,7 @@ export default class ElementsState extends FeatureSwitchState{
/**
THe change handler
*/
public changes: Changes = new Changes();
public changes: Changes;
/**
The latest element that was selected
@ -34,7 +34,7 @@ export default class ElementsState extends FeatureSwitchState{
"Selected element"
);
/**
* The map location: currently centered lat, lon and zoom
*/
@ -48,6 +48,9 @@ export default class ElementsState extends FeatureSwitchState{
constructor(layoutToUse: LayoutConfig) {
super(layoutToUse);
this.changes = new Changes(layoutToUse?.isLeftRightSensitive() ?? false)
{
// -- Location control initialization
const zoom = UIEventSource.asFloat(
@ -84,10 +87,10 @@ export default class ElementsState extends FeatureSwitchState{
lon.setData(latlonz.lon);
});
}
new ChangeToElementsActor(this.changes, this.allElements)
new PendingChangesUploader(this.changes, this.selectedElement);
new TitleHandler(this);
}
}

View file

@ -37,7 +37,7 @@ export default class FeatureSwitchState {
public readonly osmApiTileSize: UIEventSource<number>;
public readonly backgroundLayerId: UIEventSource<string>;
protected constructor(layoutToUse: LayoutConfig) {
public constructor(layoutToUse: LayoutConfig) {
this.layoutToUse = layoutToUse;

View file

@ -14,6 +14,7 @@ import {QueryParameters} from "../Web/QueryParameters";
import * as personal from "../../assets/themes/personal/personal.json";
import FilterConfig from "../../Models/ThemeConfig/FilterConfig";
import ShowOverlayLayer from "../../UI/ShowDataLayer/ShowOverlayLayer";
import {Coord} from "@turf/turf";
/**
* Contains all the leaflet-map related state
@ -44,13 +45,7 @@ export default class MapState extends UserRelatedState {
/**
* The location as delivered by the GPS
*/
public currentGPSLocation: UIEventSource<{
latlng: { lat: number; lng: number };
accuracy: number;
}> = new UIEventSource<{
latlng: { lat: number; lng: number };
accuracy: number;
}>(undefined);
public currentGPSLocation: UIEventSource<Coordinates> = new UIEventSource<Coordinates>(undefined);
public readonly mainMapObject: BaseUIElement & MinimapObj;

View file

@ -19,16 +19,6 @@ export class TagUtils {
[">", (a, b) => a > b],
]
static ApplyTemplate(template: string, tags: any): string {
for (const k in tags) {
while (template.indexOf("{" + k + "}") >= 0) {
const escaped = tags[k].replace(/</g, '&lt;').replace(/>/g, '&gt;');
template = template.replace("{" + k + "}", escaped);
}
}
return template;
}
static KVtoProperties(tags: Tag[]): any {
const properties = {};
for (const tag of tags) {
@ -37,6 +27,14 @@ export class TagUtils {
return properties;
}
static changeAsProperties(kvs : {k: string, v: string}[]): any {
const tags = {}
for (const kv of kvs) {
tags[kv.k] = kv.v
}
return tags
}
/**
* Given two hashes of {key --> values[]}, makes sure that every neededTag is present in availableTags
*/

View file

@ -109,6 +109,20 @@ export class UIEventSource<T> {
promise?.catch(err => src.setData({error: err}))
return src
}
public withEqualityStabilized(comparator: (t:T | undefined, t1:T | undefined) => boolean): UIEventSource<T>{
let oldValue = undefined;
return this.map(v => {
if(v == oldValue){
return oldValue
}
if(comparator(oldValue, v)){
return oldValue
}
oldValue = v;
return v;
})
}
/**
* Given a UIEVentSource with a list, returns a new UIEventSource which is only updated if the _contents_ of the list are different.

View file

@ -2,7 +2,7 @@ import {Utils} from "../Utils";
export default class Constants {
public static vNumber = "0.11.3";
public static vNumber = "0.12.2-beta";
public static ImgurApiKey = '7070e7167f0a25a'
public static readonly mapillary_client_token_v3 = 'TXhLaWthQ1d4RUg0czVxaTVoRjFJZzowNDczNjUzNmIyNTQyYzI2'
public static readonly mapillary_client_token_v4 = "MLY|4441509239301885|b40ad2d3ea105435bd40c7e76993ae85"

View file

@ -4,6 +4,8 @@ import FilterConfigJson from "./FilterConfigJson";
import {DeleteConfigJson} from "./DeleteConfigJson";
import UnitConfigJson from "./UnitConfigJson";
import MoveConfigJson from "./MoveConfigJson";
import PointRenderingConfigJson from "./PointRenderingConfigJson";
import LineRenderingConfigJson from "./LineRenderingConfigJson";
/**
* Configuration for a single layer
@ -126,72 +128,8 @@ export interface LayerConfigJson {
*/
titleIcons?: (string | TagRenderingConfigJson)[];
/**
* The icon for an element.
* Note that this also doubles as the icon for this layer (rendered with the overpass-tags) ánd the icon in the presets.
*
* The result of the icon is rendered as follows:
* the resulting string is interpreted as a _list_ of items, separated by ";". The bottommost layer is the first layer.
* As a result, on could use a generic pin, then overlay it with a specific icon.
* To make things even more practical, one can use all SVG's from the folder "assets/svg" and _substitute the color_ in it.
* E.g. to draw a red pin, use "pin:#f00", to have a green circle with your icon on top, use `circle:#0f0;<path to my icon.svg>`
*
*/
icon?: string | TagRenderingConfigJson;
/**
* IconsOverlays are a list of extra icons/badges to overlay over the icon.
* The 'badge'-toggle changes their behaviour.
* If badge is set, it will be added as a 25% height icon at the bottom right of the icon, with all the badges in a flex layout.
* If badges is false, it'll be a simple overlay
*
* Note: strings are interpreted as icons, so layering and substituting is supported
*/
iconOverlays?: { if: string | AndOrTagConfigJson, then: string | TagRenderingConfigJson, badge?: boolean }[]
/**
* A string containing "width,height" or "width,height,anchorpoint" where anchorpoint is any of 'center', 'top', 'bottom', 'left', 'right', 'bottomleft','topright', ...
* Default is '40,40,center'
*/
iconSize?: string | TagRenderingConfigJson;
/**
* The rotation of an icon, useful for e.g. directions.
* Usage: as if it were a css property for 'rotate', thus has to end with 'deg', e.g. `90deg`, `{direction}deg`, `calc(90deg - {camera:direction}deg)``
*/
rotation?: string | TagRenderingConfigJson;
/**
* A HTML-fragment that is shown below the icon, for example:
* <div style="background: white; display: block">{name}</div>
*
* If the icon is undefined, then the label is shown in the center of the feature.
* Note that, if the wayhandling hides the icon then no label is shown as well.
*/
label?: string | TagRenderingConfigJson;
/**
* The color for way-elements and SVG-elements.
* If the value starts with "--", the style of the body element will be queried for the corresponding variable instead
*/
color?: string | TagRenderingConfigJson;
/**
* The stroke-width for way-elements
*/
width?: string | TagRenderingConfigJson;
/**
* A dasharray, e.g. "5 6"
* The dasharray defines 'pixels of line, pixels of gap, pixels of line, pixels of gap',
* Default value: "" (empty string == full line)
*/
dashArray?: string | TagRenderingConfigJson
/**
* Wayhandling: should a way/area be displayed as:
* 0) The way itself
* 1) Only the centerpoint
* 2) The centerpoint and the way
*/
wayHandling?: number;
mapRendering: (PointRenderingConfigJson | LineRenderingConfigJson)[]
/**
* If set, this layer will pass all the features it receives onto the next layer.
@ -265,8 +203,19 @@ export interface LayerConfigJson {
*
* A special value is 'questions', which indicates the location of the questions box. If not specified, it'll be appended to the bottom of the featureInfobox.
*
* At last, one can define a group of renderings where parts of all strings will be replaced by multiple other strings.
* This is mainly create questions for a 'left' and a 'right' side of the road.
* These will be grouped and questions will be asked together
*/
tagRenderings?: (string | {builtin: string, override: any} | TagRenderingConfigJson) [],
tagRenderings?: (string | {builtin: string, override: any} | TagRenderingConfigJson | {
rewrite: {
sourceString: string,
into: string[]
}[],
renderings: (string | {builtin: string, override: any} | TagRenderingConfigJson)[]
}) [],
/**

View file

@ -118,18 +118,6 @@ export interface LayoutConfigJson {
* Default: overpassMaxZoom + 1
*/
osmApiTileSize?: number
/**
* A tagrendering depicts how to show some tags or how to show a question for it.
*
* These tagrenderings are applied to _all_ the loaded layers and are a way to reuse tagrenderings.
* Note that if multiple themes are loaded (e.g. via the personal theme)
* that these roamingRenderings are applied to the layers of the OTHER themes too!
*
* In order to prevent them to do too much damage, all the overpass-tags of the layers are taken and combined as OR.
* These tag renderings will only show up if the object matches this filter.
*/
roamingRenderings?: (TagRenderingConfigJson | string)[],
/**
* An override applied on all layers of the theme.

View file

@ -0,0 +1,38 @@
import {TagRenderingConfigJson} from "./TagRenderingConfigJson";
/**
* The LineRenderingConfig gives all details onto how to render a single line of a feature.
*
* This can be used if:
*
* - The feature is a line
* - The feature is an area
*/
export default interface LineRenderingConfigJson {
/**
* The color for way-elements and SVG-elements.
* If the value starts with "--", the style of the body element will be queried for the corresponding variable instead
*/
color?: string | TagRenderingConfigJson;
/**
* The stroke-width for way-elements
*/
width?: string | TagRenderingConfigJson;
/**
* A dasharray, e.g. "5 6"
* The dasharray defines 'pixels of line, pixels of gap, pixels of line, pixels of gap',
* Default value: "" (empty string == full line)
*/
dashArray?: string | TagRenderingConfigJson
/**
* The number of pixels this line should be moved.
* Use a positive numbe to move to the right, a negative to move to the left (left/right as defined by the drawing direction of the line).
*
* IMPORTANT: MapComplete will already normalize 'key:both:property' and 'key:both' into the corresponding 'key:left' and 'key:right' tagging (same for 'sidewalk=left/right/both' which is rewritten to 'sidewalk:left' and 'sidewalk:right')
* This simplifies programming. Refer to the CalculatedTags.md-documentation for more details
*/
offset?: number | TagRenderingConfigJson
}

View file

@ -0,0 +1,60 @@
import {TagRenderingConfigJson} from "./TagRenderingConfigJson";
import {AndOrTagConfigJson} from "./TagConfigJson";
/**
* The PointRenderingConfig gives all details onto how to render a single point of a feature.
*
* This can be used if:
*
* - The feature is a point
* - To render something at the centroid of an area, or at the start, end or projected centroid of a way
*/
export default interface PointRenderingConfigJson {
/**
* All the locations that this point should be rendered at.
* Using `location: ["point", "centroid"] will always render centerpoint
*/
location: ("point" | "centroid" | "start" | "end")[]
/**
* The icon for an element.
* Note that this also doubles as the icon for this layer (rendered with the overpass-tags) ánd the icon in the presets.
*
* The result of the icon is rendered as follows:
* the resulting string is interpreted as a _list_ of items, separated by ";". The bottommost layer is the first layer.
* As a result, on could use a generic pin, then overlay it with a specific icon.
* To make things even more practical, one can use all SVG's from the folder "assets/svg" and _substitute the color_ in it.
* E.g. to draw a red pin, use "pin:#f00", to have a green circle with your icon on top, use `circle:#0f0;<path to my icon.svg>`
*
*/
icon?: string | TagRenderingConfigJson;
/**
* A list of extra badges to show next to the icon as small badge
* They will be added as a 25% height icon at the bottom right of the icon, with all the badges in a flex layout.
*
* Note: strings are interpreted as icons, so layering and substituting is supported. You can use `circle:white;./my_icon.svg` to add a background circle
*/
iconBadges?: { if: string | AndOrTagConfigJson, then: string | TagRenderingConfigJson }[]
/**
* A string containing "width,height" or "width,height,anchorpoint" where anchorpoint is any of 'center', 'top', 'bottom', 'left', 'right', 'bottomleft','topright', ...
* Default is '40,40,center'
*/
iconSize?: string | TagRenderingConfigJson;
/**
* The rotation of an icon, useful for e.g. directions.
* Usage: as if it were a css property for 'rotate', thus has to end with 'deg', e.g. `90deg`, `{direction}deg`, `calc(90deg - {camera:direction}deg)``
*/
rotation?: string | TagRenderingConfigJson;
/**
* A HTML-fragment that is shown below the icon, for example:
* <div style="background: white; display: block">{name}</div>
*
* If the icon is undefined, then the label is shown in the center of the feature.
* Note that, if the wayhandling hides the icon then no label is shown as well.
*/
label?: string | TagRenderingConfigJson;
}

View file

@ -11,6 +11,12 @@ export interface TagRenderingConfigJson {
* Used to keep the translations in sync. Only used in the tagRenderings-array of a layerConfig, not requered otherwise
*/
id?: string,
/**
* If 'group' is defined on many tagRenderings, these are grouped together when shown. The questions are grouped together as well.
* The first tagRendering of a group will always be a sticky element.
*/
group?: string
/**
* Renders this value. Note that "{key}"-parts are substituted by the corresponding values of the element.
@ -169,7 +175,6 @@ export interface TagRenderingConfigJson {
*/
ifnot?: AndOrTagConfigJson | string
/**
* If chosen as answer, these tags will be applied as well onto the object.
* Not compatible with multiAnswer
@ -177,10 +182,4 @@ export interface TagRenderingConfigJson {
addExtraTags?: string[]
}[]
/**
* If set to true, this tagRendering will escape the current layer and attach itself to all the other layers too.
* However, it will _only_ be shown if it matches the overpass-tags of the layer it was originally defined in.
*/
roaming?: boolean
}

View file

@ -1,30 +1,24 @@
import {Translation} from "../../UI/i18n/Translation";
import SourceConfig from "./SourceConfig";
import TagRenderingConfig from "./TagRenderingConfig";
import {TagsFilter} from "../../Logic/Tags/TagsFilter";
import PresetConfig from "./PresetConfig";
import {LayerConfigJson} from "./Json/LayerConfigJson";
import Translations from "../../UI/i18n/Translations";
import {TagUtils} from "../../Logic/Tags/TagUtils";
import SharedTagRenderings from "../../Customizations/SharedTagRenderings";
import {TagRenderingConfigJson} from "./Json/TagRenderingConfigJson";
import {Utils} from "../../Utils";
import {UIEventSource} from "../../Logic/UIEventSource";
import BaseUIElement from "../../UI/BaseUIElement";
import {FixedUiElement} from "../../UI/Base/FixedUiElement";
import Combine from "../../UI/Base/Combine";
import {VariableUiElement} from "../../UI/Base/VariableUIElement";
import FilterConfig from "./FilterConfig";
import {Unit} from "../Unit";
import DeleteConfig from "./DeleteConfig";
import Svg from "../../Svg";
import Img from "../../UI/Base/Img";
import MoveConfig from "./MoveConfig";
import PointRenderingConfig from "./PointRenderingConfig";
import WithContextLoader from "./WithContextLoader";
import LineRenderingConfig from "./LineRenderingConfig";
import PointRenderingConfigJson from "./Json/PointRenderingConfigJson";
import LineRenderingConfigJson from "./Json/LineRenderingConfigJson";
import {TagRenderingConfigJson} from "./Json/TagRenderingConfigJson";
import {UIEventSource} from "../../Logic/UIEventSource";
import BaseUIElement from "../../UI/BaseUIElement";
export default class LayerConfig {
static WAYHANDLING_DEFAULT = 0;
static WAYHANDLING_CENTER_ONLY = 1;
static WAYHANDLING_CENTER_AND_WAY = 2;
export default class LayerConfig extends WithContextLoader {
id: string;
name: Translation;
@ -39,15 +33,10 @@ export default class LayerConfig {
maxzoom: number;
title?: TagRenderingConfig;
titleIcons: TagRenderingConfig[];
icon: TagRenderingConfig;
iconOverlays: { if: TagsFilter; then: TagRenderingConfig; badge: boolean }[];
iconSize: TagRenderingConfig;
label: TagRenderingConfig;
rotation: TagRenderingConfig;
color: TagRenderingConfig;
width: TagRenderingConfig;
dashArray: TagRenderingConfig;
wayHandling: number;
public readonly mapRendering: PointRenderingConfig[]
public readonly lineRendering: LineRenderingConfig[]
public readonly units: Unit[];
public readonly deletion: DeleteConfig | null;
public readonly allowMove: MoveConfig | null
@ -67,10 +56,47 @@ export default class LayerConfig {
context?: string,
official: boolean = true
) {
context = context + "." + json.id;
const self = this;
super(json, context)
this.id = json.id;
if (json.source === undefined) {
throw "Layer " + this.id + " does not define a source section (" + context + ")"
}
if (json.source.osmTags === undefined) {
throw "Layer " + this.id + " does not define a osmTags in the source section - these should always be present, even for geojson layers (" + context + ")"
}
this.maxAgeOfCache = json.source.maxCacheAge ?? 24 * 60 * 60 * 30
const osmTags = TagUtils.Tag(
json.source.osmTags,
context + "source.osmTags"
);
if (json.source["geoJsonSource"] !== undefined) {
throw context + "Use 'geoJson' instead of 'geoJsonSource'";
}
if (json.source["geojson"] !== undefined) {
throw context + "Use 'geoJson' instead of 'geojson' (the J is a capital letter)";
}
this.source = new SourceConfig(
{
osmTags: osmTags,
geojsonSource: json.source["geoJson"],
geojsonSourceLevel: json.source["geoJsonZoomLevel"],
overpassScript: json.source["overpassScript"],
isOsmCache: json.source["isOsmCache"],
mercatorCrs: json.source["mercatorCrs"]
},
json.id
);
this.allowSplit = json.allowSplit ?? false;
this.name = Translations.T(json.name, context + ".name");
this.units = (json.units ?? []).map(((unitJson, i) => Unit.fromJson(unitJson, `${context}.unit[${i}]`)))
@ -86,54 +112,7 @@ export default class LayerConfig {
context + ".description"
);
let legacy = undefined;
if (json["overpassTags"] !== undefined) {
// @ts-ignore
legacy = TagUtils.Tag(json["overpassTags"], context + ".overpasstags");
}
if (json.source !== undefined) {
this.maxAgeOfCache = json.source.maxCacheAge ?? 24 * 60 * 60 * 30
if (legacy !== undefined) {
throw (
context +
"Both the legacy 'layer.overpasstags' and the new 'layer.source'-field are defined"
);
}
let osmTags: TagsFilter = legacy;
if (json.source["osmTags"]) {
osmTags = TagUtils.Tag(
json.source["osmTags"],
context + "source.osmTags"
);
}
if (json.source["geoJsonSource"] !== undefined) {
throw context + "Use 'geoJson' instead of 'geoJsonSource'";
}
if (json.source["geojson"] !== undefined) {
throw context + "Use 'geoJson' instead of 'geojson' (the J is a capital letter)";
}
this.source = new SourceConfig(
{
osmTags: osmTags,
geojsonSource: json.source["geoJson"],
geojsonSourceLevel: json.source["geoJsonZoomLevel"],
overpassScript: json.source["overpassScript"],
isOsmCache: json.source["isOsmCache"],
mercatorCrs: json.source["mercatorCrs"]
},
this.id
);
} else {
this.source = new SourceConfig({
osmTags: legacy,
});
}
this.calculatedTags = undefined;
if (json.calculatedTags !== undefined) {
if (!official) {
@ -163,7 +142,6 @@ export default class LayerConfig {
this.passAllFeatures = json.passAllFeatures ?? false;
this.minzoom = json.minzoom ?? 0;
this.minzoomVisible = json.minzoomVisible ?? this.minzoom;
this.wayHandling = json.wayHandling ?? 0;
if (json.presets !== undefined && json.presets?.map === undefined) {
throw "Presets should be a list of items (at " + context + ")"
}
@ -209,103 +187,21 @@ export default class LayerConfig {
return config;
});
/** Given a key, gets the corresponding property from the json (or the default if not found
*
* The found value is interpreted as a tagrendering and fetched/parsed
* */
function tr(key: string, deflt) {
const v = json[key];
if (v === undefined || v === null) {
if (deflt === undefined) {
return undefined;
}
return new TagRenderingConfig(
deflt,
self.source.osmTags,
`${context}.${key}.default value`
);
}
if (typeof v === "string") {
const shared = SharedTagRenderings.SharedTagRendering.get(v);
if (shared) {
return shared;
}
}
return new TagRenderingConfig(
v,
self.source.osmTags,
`${context}.${key}`
);
if (json.mapRendering === undefined) {
throw "MapRendering is undefined in " + context
}
/**
* Converts a list of tagRenderingCOnfigJSON in to TagRenderingConfig
* A string is interpreted as a name to call
*/
function trs(
tagRenderings?: (string | { builtin: string, override: any } | TagRenderingConfigJson)[],
readOnly = false
) {
if (tagRenderings === undefined) {
return [];
}
this.mapRendering = json.mapRendering
.filter(r => r["location"] !== undefined)
.map((r, i) => new PointRenderingConfig(<PointRenderingConfigJson>r, context + ".mapRendering[" + i + "]"))
return Utils.NoNull(
tagRenderings.map((renderingJson, i) => {
if (typeof renderingJson === "string") {
renderingJson = {builtin: renderingJson, override: undefined}
}
this.lineRendering = json.mapRendering
.filter(r => r["location"] === undefined)
.map((r, i) => new LineRenderingConfig(<LineRenderingConfigJson>r, context + ".mapRendering[" + i + "]"))
if (renderingJson["builtin"] !== undefined) {
const renderingId = renderingJson["builtin"]
if (renderingId === "questions") {
if (readOnly) {
throw `A tagrendering has a question, but asking a question does not make sense here: is it a title icon or a geojson-layer? ${context}. The offending tagrendering is ${JSON.stringify(
renderingJson
)}`;
}
return new TagRenderingConfig("questions", undefined, context);
}
if (renderingJson["override"] !== undefined) {
const sharedJson = SharedTagRenderings.SharedTagRenderingJson.get(renderingId)
return new TagRenderingConfig(
Utils.Merge(renderingJson["override"], sharedJson),
self.source.osmTags,
`${context}.tagrendering[${i}]+override`
);
}
const shared = SharedTagRenderings.SharedTagRendering.get(renderingId);
if (shared !== undefined) {
return shared;
}
if (Utils.runningFromConsole) {
return undefined;
}
const keys = Array.from(
SharedTagRenderings.SharedTagRendering.keys()
);
throw `Predefined tagRendering ${renderingId} not found in ${context}.\n Try one of ${keys.join(
", "
)}\n If you intent to output this text literally, use {\"render\": <your text>} instead"}`;
}
return new TagRenderingConfig(
<TagRenderingConfigJson>renderingJson,
self.source.osmTags,
`${context}.tagrendering[${i}]`
);
})
);
}
this.tagRenderings = trs(json.tagRenderings, false);
const missingIds = json.tagRenderings?.filter(tr => typeof tr !== "string" && tr["builtin"] === undefined && tr["id"] === undefined) ?? [];
this.tagRenderings = this.ExtractLayerTagRenderings(json)
const missingIds = json.tagRenderings?.filter(tr => typeof tr !== "string" && tr["builtin"] === undefined && tr["id"] === undefined && tr["rewrite"] === undefined) ?? [];
if (missingIds.length > 0 && official) {
console.error("Some tagRenderings of", this.id, "are missing an id:", missingIds)
@ -336,43 +232,10 @@ export default class LayerConfig {
}
}
this.titleIcons = trs(titleIcons, true);
this.titleIcons = this.ParseTagRenderings(titleIcons, true);
this.title = tr("title", undefined);
this.icon = tr("icon", "");
this.iconOverlays = (json.iconOverlays ?? []).map((overlay, i) => {
let tr = new TagRenderingConfig(
overlay.then,
self.source.osmTags,
`iconoverlays.${i}`
);
if (
typeof overlay.then === "string" &&
SharedTagRenderings.SharedIcons.get(overlay.then) !== undefined
) {
tr = SharedTagRenderings.SharedIcons.get(overlay.then);
}
return {
if: TagUtils.Tag(overlay.if),
then: tr,
badge: overlay.badge ?? false,
};
});
const iconPath = this.icon.GetRenderValue({id: "node/-1"}).txt;
if (iconPath.startsWith(Utils.assets_path)) {
const iconKey = iconPath.substr(Utils.assets_path.length);
if (Svg.All[iconKey] === undefined) {
throw "Builtin SVG asset not found: " + iconPath;
}
}
this.isShown = tr("isShown", "yes");
this.iconSize = tr("iconSize", "40,40,center");
this.label = tr("label", "");
this.color = tr("color", "#0000ff");
this.width = tr("width", "7");
this.rotation = tr("rotation", "0");
this.dashArray = tr("dashArray", "");
this.title = this.tr("title", undefined);
this.isShown = this.tr("isShown", "yes");
this.deletion = null;
if (json.deletion === true) {
@ -401,258 +264,128 @@ export default class LayerConfig {
}
}
public defaultIcon() : BaseUIElement | undefined{
const mapRendering = this.mapRendering.filter(r => r.location.has("point"))[0]
if (mapRendering === undefined) {
return undefined
}
const defaultTags = new UIEventSource(TagUtils.changeAsProperties(this.source.osmTags.asChange({id: "node/-1"})))
return mapRendering.GenerateLeafletStyle(defaultTags, false, {noSize: true}).html
}
public ExtractLayerTagRenderings(json: LayerConfigJson): TagRenderingConfig[] {
if (json.tagRenderings === undefined) {
return []
}
const normalTagRenderings: (string | { builtin: string, override: any } | TagRenderingConfigJson)[] = []
const renderingsToRewrite: ({
rewrite: {
sourceString: string,
into: string[]
}, renderings: (string | { builtin: string, override: any } | TagRenderingConfigJson)[]
})[] = []
for (let i = 0; i < json.tagRenderings.length; i++) {
const tr = json.tagRenderings[i];
const rewriteDefined = tr["rewrite"] !== undefined
const renderingsDefined = tr["renderings"]
if (!rewriteDefined && !renderingsDefined) {
// @ts-ignore
normalTagRenderings.push(tr)
continue
}
if (rewriteDefined && renderingsDefined) {
// @ts-ignore
renderingsToRewrite.push(tr)
continue
}
throw `Error in ${this._context}.tagrenderings[${i}]: got a value which defines either \`rewrite\` or \`renderings\`, but not both. Either define both or move the \`renderings\` out of this scope`
}
const allRenderings = this.ParseTagRenderings(normalTagRenderings, false);
if (renderingsToRewrite.length === 0) {
return allRenderings
}
function prepConfig(keyToRewrite: string, target: string, tr: TagRenderingConfigJson) {
function replaceRecursive(transl: string | any) {
if (typeof transl === "string") {
return transl.replace(keyToRewrite, target)
}
if (transl.map !== undefined) {
return transl.map(o => replaceRecursive(o))
}
transl = {...transl}
for (const key in transl) {
transl[key] = replaceRecursive(transl[key])
}
return transl
}
const orig = tr;
tr = replaceRecursive(tr)
tr.id = target + "-" + orig.id
tr.group = target
return tr
}
const rewriteGroups: Map<string, TagRenderingConfig[]> = new Map<string, TagRenderingConfig[]>()
for (const rewriteGroup of renderingsToRewrite) {
const tagRenderings = rewriteGroup.renderings
const textToReplace = rewriteGroup.rewrite.sourceString
const targets = rewriteGroup.rewrite.into
for (const target of targets) {
const parsedRenderings = this.ParseTagRenderings(tagRenderings, false, tr => prepConfig(textToReplace, target, tr))
if (!rewriteGroups.has(target)) {
rewriteGroups.set(target, [])
}
rewriteGroups.get(target).push(...parsedRenderings)
}
}
rewriteGroups.forEach((group, groupName) => {
group.push(new TagRenderingConfig({
id: "questions",
group: groupName
}))
})
rewriteGroups.forEach(group => {
allRenderings.push(...group)
})
return allRenderings;
}
public CustomCodeSnippets(): string[] {
if (this.calculatedTags === undefined) {
return [];
}
return this.calculatedTags.map((code) => code[1]);
}
public AddRoamingRenderings(addAll: {
tagRenderings: TagRenderingConfig[];
titleIcons: TagRenderingConfig[];
iconOverlays: {
if: TagsFilter;
then: TagRenderingConfig;
badge: boolean;
}[];
}): LayerConfig {
let insertionPoint = this.tagRenderings
.map((tr) => tr.IsQuestionBoxElement())
.indexOf(true);
if (insertionPoint < 0) {
// No 'questions' defined - we just add them all to the end
insertionPoint = this.tagRenderings.length;
}
this.tagRenderings.splice(insertionPoint, 0, ...addAll.tagRenderings);
this.iconOverlays.push(...addAll.iconOverlays);
for (const icon of addAll.titleIcons) {
this.titleIcons.splice(0, 0, icon);
}
return this;
}
public GetRoamingRenderings(): {
tagRenderings: TagRenderingConfig[];
titleIcons: TagRenderingConfig[];
iconOverlays: {
if: TagsFilter;
then: TagRenderingConfig;
badge: boolean;
}[];
} {
const tagRenderings = this.tagRenderings.filter((tr) => tr.roaming);
const titleIcons = this.titleIcons.filter((tr) => tr.roaming);
const iconOverlays = this.iconOverlays.filter((io) => io.then.roaming);
return {
tagRenderings: tagRenderings,
titleIcons: titleIcons,
iconOverlays: iconOverlays,
};
}
public GenerateLeafletStyle(
tags: UIEventSource<any>,
clickable: boolean
): {
icon: {
html: BaseUIElement;
iconSize: [number, number];
iconAnchor: [number, number];
popupAnchor: [number, number];
iconUrl: string;
className: string;
};
color: string;
weight: number;
dashArray: number[];
} {
function num(str, deflt = 40) {
const n = Number(str);
if (isNaN(n)) {
return deflt;
}
return n;
}
function rendernum(tr: TagRenderingConfig, deflt: number) {
const str = Number(render(tr, "" + deflt));
const n = Number(str);
if (isNaN(n)) {
return deflt;
}
return n;
}
function render(tr: TagRenderingConfig, deflt?: string) {
if (tags === undefined) {
return deflt
}
const str = tr?.GetRenderValue(tags.data)?.txt ?? deflt;
return Utils.SubstituteKeys(str, tags.data).replace(/{.*}/g, "");
}
const iconSize = render(this.iconSize, "40,40,center").split(",");
const dashArray = render(this.dashArray)?.split(" ")?.map(Number);
let color = render(this.color, "#00f");
if (color.startsWith("--")) {
color = getComputedStyle(document.body).getPropertyValue(
"--catch-detail-color"
);
}
const weight = rendernum(this.width, 5);
const iconW = num(iconSize[0]);
let iconH = num(iconSize[1]);
const mode = iconSize[2]?.trim()?.toLowerCase() ?? "center";
let anchorW = iconW / 2;
let anchorH = iconH / 2;
if (mode === "left") {
anchorW = 0;
}
if (mode === "right") {
anchorW = iconW;
}
if (mode === "top") {
anchorH = 0;
}
if (mode === "bottom") {
anchorH = iconH;
}
const iconUrlStatic = render(this.icon);
const self = this;
function genHtmlFromString(sourcePart: string, rotation: string): BaseUIElement {
const style = `width:100%;height:100%;transform: rotate( ${rotation} );display:block;position: absolute; top: 0; left: 0`;
let html: BaseUIElement = new FixedUiElement(
`<img src="${sourcePart}" style="${style}" />`
);
const match = sourcePart.match(/([a-zA-Z0-9_]*):([^;]*)/);
if (match !== null && Svg.All[match[1] + ".svg"] !== undefined) {
html = new Img(
(Svg.All[match[1] + ".svg"] as string).replace(
/#000000/g,
match[2]
),
true
).SetStyle(style);
}
return html;
}
const mappedHtml = tags?.map((tgs) => {
// What do you mean, 'tgs' is never read?
// It is read implicitly in the 'render' method
const iconUrl = render(self.icon);
const rotation = render(self.rotation, "0deg");
let htmlParts: BaseUIElement[] = [];
let sourceParts = Utils.NoNull(
iconUrl.split(";").filter((prt) => prt != "")
);
for (const sourcePart of sourceParts) {
htmlParts.push(genHtmlFromString(sourcePart, rotation));
}
let badges = [];
for (const iconOverlay of self.iconOverlays) {
if (!iconOverlay.if.matchesProperties(tgs)) {
continue;
}
if (iconOverlay.badge) {
const badgeParts: BaseUIElement[] = [];
const renderValue = iconOverlay
.then
.GetRenderValue(tgs)
if (renderValue === undefined) {
continue;
}
const partDefs = renderValue.txt.split(";")
.filter((prt) => prt != "");
for (const badgePartStr of partDefs) {
badgeParts.push(genHtmlFromString(badgePartStr, "0"));
}
const badgeCompound = new Combine(badgeParts).SetStyle(
"display:flex;position:relative;width:100%;height:100%;"
);
badges.push(badgeCompound);
} else {
htmlParts.push(
genHtmlFromString(iconOverlay.then.GetRenderValue(tgs).txt, "0")
);
}
}
if (badges.length > 0) {
const badgesComponent = new Combine(badges).SetStyle(
"display:flex;height:50%;width:100%;position:absolute;top:50%;left:50%;"
);
htmlParts.push(badgesComponent);
}
if (sourceParts.length == 0) {
iconH = 0;
}
try {
const label = self.label
?.GetRenderValue(tgs)
?.Subs(tgs)
?.SetClass("block text-center")
?.SetStyle("margin-top: " + (iconH + 2) + "px");
if (label !== undefined) {
htmlParts.push(
new Combine([label]).SetClass("flex flex-col items-center")
);
}
} catch (e) {
console.error(e, tgs);
}
return new Combine(htmlParts);
});
return {
icon: {
html: mappedHtml === undefined ? new FixedUiElement(self.icon.render.txt) : new VariableUiElement(mappedHtml),
iconSize: [iconW, iconH],
iconAnchor: [anchorW, anchorH],
popupAnchor: [0, 3 - anchorH],
iconUrl: iconUrlStatic,
className: clickable
? "leaflet-div-icon"
: "leaflet-div-icon unclickable",
},
color: color,
weight: weight,
dashArray: dashArray,
};
}
public ExtractImages(): Set<string> {
const parts: Set<string>[] = [];
parts.push(...this.tagRenderings?.map((tr) => tr.ExtractImages(false)));
parts.push(...this.titleIcons?.map((tr) => tr.ExtractImages(true)));
parts.push(this.icon?.ExtractImages(true));
parts.push(
...this.iconOverlays?.map((overlay) => overlay.then.ExtractImages(true))
);
for (const preset of this.presets) {
parts.push(new Set<string>(preset.description?.ExtractImages(false)));
}
for (const pointRenderingConfig of this.mapRendering) {
parts.push(pointRenderingConfig.ExtractImages())
}
const allIcons = new Set<string>();
for (const part of parts) {
part?.forEach(allIcons.add, allIcons);
@ -660,4 +393,8 @@ export default class LayerConfig {
return allIcons;
}
public isLeftRightSensitive(): boolean {
return this.lineRendering.some(lr => lr.leftRightSensitive)
}
}

View file

@ -1,7 +1,5 @@
import {Translation} from "../../UI/i18n/Translation";
import TagRenderingConfig from "./TagRenderingConfig";
import {LayoutConfigJson} from "./Json/LayoutConfigJson";
import SharedTagRenderings from "../../Customizations/SharedTagRenderings";
import AllKnownLayers from "../../Customizations/AllKnownLayers";
import {Utils} from "../../Utils";
import LayerConfig from "./LayerConfig";
@ -25,7 +23,6 @@ export default class LayoutConfig {
public readonly startLat: number;
public readonly startLon: number;
public readonly widenFactor: number;
public readonly roamingRenderings: TagRenderingConfig[];
public readonly defaultBackgroundId?: string;
public layers: LayerConfig[];
public tileLayerSources: TilesourceConfig[]
@ -55,6 +52,7 @@ export default class LayoutConfig {
public readonly overpassMaxZoom: number
public readonly osmApiTileSize: number
public readonly official: boolean;
public readonly trackAllNodes : boolean;
constructor(json: LayoutConfigJson, official = true, context?: string) {
this.official = official;
@ -64,6 +62,8 @@ export default class LayoutConfig {
this.credits = json.credits;
this.version = json.version;
this.language = [];
this.trackAllNodes = false
if (typeof json.language === "string") {
this.language = [json.language];
} else {
@ -93,45 +93,16 @@ export default class LayoutConfig {
if(json.widenFactor > 20){
throw "Widenfactor is very big, use a value between 1 and 5 (current value is "+json.widenFactor+") at "+context
}
this.widenFactor = json.widenFactor ?? 1.5;
this.roamingRenderings = (json.roamingRenderings ?? []).map((tr, i) => {
if (typeof tr === "string") {
if (SharedTagRenderings.SharedTagRendering.get(tr) !== undefined) {
return SharedTagRenderings.SharedTagRendering.get(tr);
}
}
return new TagRenderingConfig(tr, undefined, `${this.id}.roaming_renderings[${i}]`);
}
);
this.defaultBackgroundId = json.defaultBackgroundId;
this.tileLayerSources = (json.tileLayerSources??[]).map((config, i) => new TilesourceConfig(config, `${this.id}.tileLayerSources[${i}]`))
this.layers = LayoutConfig.ExtractLayers(json, official, context);
// ALl the layers are constructed, let them share tagRenderings now!
const roaming: { r, source: LayerConfig }[] = []
for (const layer of this.layers) {
roaming.push({r: layer.GetRoamingRenderings(), source: layer});
}
for (const layer of this.layers) {
for (const r of roaming) {
if (r.source == layer) {
continue;
}
layer.AddRoamingRenderings(r.r);
}
}
for (const layer of this.layers) {
layer.AddRoamingRenderings(
{
titleIcons: [],
iconOverlays: [],
tagRenderings: this.roamingRenderings
}
);
}
const layerInfo = LayoutConfig.ExtractLayers(json, official, context);
this.layers = layerInfo.layers
this.trackAllNodes = layerInfo.extractAllNodes
this.clustering = {
maxZoom: 16,
minNeededElements: 25,
@ -181,10 +152,11 @@ export default class LayoutConfig {
}
private static ExtractLayers(json: LayoutConfigJson, official: boolean, context: string): LayerConfig[] {
private static ExtractLayers(json: LayoutConfigJson, official: boolean, context: string): {layers: LayerConfig[], extractAllNodes: boolean} {
const result: LayerConfig[] = []
let exportAllNodes = false
json.layers.forEach((layer, i) => {
if (typeof layer === "string") {
if (AllKnownLayers.sharedLayersJson.get(layer) !== undefined) {
if (json.overrideAll !== undefined) {
@ -211,12 +183,19 @@ export default class LayoutConfig {
result.push(newLayer)
return
}
// @ts-ignore
let names = layer.builtin;
if (typeof names === "string") {
names = [names]
}
names.forEach(name => {
if(name === "type_node"){
// This is a very special layer which triggers special behaviour
exportAllNodes = true;
}
const shared = AllKnownLayers.sharedLayersJson.get(name);
if (shared === undefined) {
throw `Unknown shared/builtin layer ${name} at ${context}.layers[${i}]. Available layers are ${Array.from(AllKnownLayers.sharedLayersJson.keys()).join(", ")}`;
@ -233,7 +212,7 @@ export default class LayoutConfig {
});
return result
return {layers: result, extractAllNodes: exportAllNodes}
}
public CustomCodeSnippets(): string[] {
@ -308,5 +287,9 @@ export default class LayoutConfig {
})
return new LayoutConfig(JSON.parse(originalJson), false, "Layout rewriting")
}
public isLeftRightSensitive(){
return this.layers.some(l => l.isLeftRightSensitive())
}
}

View file

@ -0,0 +1,108 @@
import LineRenderingConfigJson from "./Json/LineRenderingConfigJson";
export default class LegacyJsonConvert {
/**
* Updates the config file in-place
* @param config
* @private
*/
public static fixLayerConfig(config: any): void {
if (config["overpassTags"]) {
config.source = config.source ?? {}
config.source.osmTags = config["overpassTags"]
delete config["overpassTags"]
}
if (config.tagRenderings !== undefined) {
for (const tagRendering of config.tagRenderings) {
if (tagRendering["id"] === undefined) {
if (tagRendering["#"] !== undefined) {
tagRendering["id"] = tagRendering["#"]
delete tagRendering["#"]
} else if (tagRendering["freeform"]?.key !== undefined) {
tagRendering["id"] = config.id + "-" + tagRendering["freeform"]["key"]
}
}
}
}
if (config.mapRendering === undefined && config.id !== "sidewalks") {
// This is a legacy format, lets create a pointRendering
let location: ("point" | "centroid")[] = ["point"]
let wayHandling: number = config["wayHandling"] ?? 0
if (wayHandling === 2) {
location = ["point", "centroid"]
}
config.mapRendering = [
{
icon: config["icon"],
iconBadges: config["iconOverlays"],
label: config["label"],
iconSize: config["iconSize"],
location,
rotation: config["rotation"]
}
]
if (wayHandling !== 1) {
const lineRenderConfig = <LineRenderingConfigJson>{
color: config["color"],
width: config["width"],
dashArray: config["dashArray"]
}
if (Object.keys(lineRenderConfig).length > 0) {
config.mapRendering.push(lineRenderConfig)
}
}
delete config["color"]
delete config["width"]
delete config["dashArray"]
delete config["icon"]
delete config["iconOverlays"]
delete config["label"]
delete config["iconSize"]
delete config["rotation"]
delete config["wayHandling"]
}
for (const mapRenderingElement of config.mapRendering) {
if (mapRenderingElement["iconOverlays"] !== undefined) {
mapRenderingElement["iconBadges"] = mapRenderingElement["iconOverlays"]
}
for (const overlay of mapRenderingElement["iconBadges"] ?? []) {
if (overlay["badge"] !== true) {
console.log("Warning: non-overlay element for ", config.id)
}
delete overlay["badge"]
}
}
}
/**
* Given an old (parsed) JSON-config, will (in place) fix some issues
* @param oldThemeConfig: the config to update to the latest format
*/
public static fixThemeConfig(oldThemeConfig: any): void {
for (const layerConfig of oldThemeConfig.layers ?? []) {
if (typeof layerConfig === "string" || layerConfig["builtin"] !== undefined) {
continue
}
// @ts-ignore
LegacyJsonConvert.fixLayerConfig(layerConfig)
}
if (oldThemeConfig["roamingRenderings"] !== undefined && oldThemeConfig["roamingRenderings"].length == 0) {
delete oldThemeConfig["roamingRenderings"]
}
}
}

View file

@ -0,0 +1,70 @@
import PointRenderingConfigJson from "./Json/PointRenderingConfigJson";
import WithContextLoader from "./WithContextLoader";
import {UIEventSource} from "../../Logic/UIEventSource";
import TagRenderingConfig from "./TagRenderingConfig";
import {Utils} from "../../Utils";
import LineRenderingConfigJson from "./Json/LineRenderingConfigJson";
export default class LineRenderingConfig extends WithContextLoader {
public readonly color: TagRenderingConfig;
public readonly width: TagRenderingConfig;
public readonly dashArray: TagRenderingConfig;
public readonly offset: TagRenderingConfig;
public readonly leftRightSensitive: boolean
constructor(json: LineRenderingConfigJson, context: string) {
super(json, context)
this.color = this.tr("color", "#0000ff");
this.width = this.tr("width", "7");
this.dashArray = this.tr("dashArray", "");
this.leftRightSensitive = json.offset !== undefined && json.offset !== 0 && json.offset !== "0"
this.offset = this.tr("offset", "0");
}
public GenerateLeafletStyle(tags: {}):
{
color: string,
weight: number,
dashArray: string,
offset: number
} {
function rendernum(tr: TagRenderingConfig, deflt: number) {
const str = Number(render(tr, "" + deflt));
const n = Number(str);
if (isNaN(n)) {
return deflt;
}
return n;
}
function render(tr: TagRenderingConfig, deflt?: string) {
if (tags === undefined) {
return deflt
}
const str = tr?.GetRenderValue(tags)?.txt ?? deflt;
return Utils.SubstituteKeys(str, tags)?.replace(/{.*}/g, "");
}
const dashArray = render(this.dashArray);
let color = render(this.color, "#00f");
if (color.startsWith("--")) {
color = getComputedStyle(document.body).getPropertyValue(
"--catch-detail-color"
);
}
const weight = rendernum(this.width, 5);
const offset = rendernum(this.offset, 0)
return {
color,
weight,
dashArray,
offset
}
}
}

View file

@ -0,0 +1,267 @@
import PointRenderingConfigJson from "./Json/PointRenderingConfigJson";
import TagRenderingConfig from "./TagRenderingConfig";
import {TagsFilter} from "../../Logic/Tags/TagsFilter";
import SharedTagRenderings from "../../Customizations/SharedTagRenderings";
import {TagUtils} from "../../Logic/Tags/TagUtils";
import {Utils} from "../../Utils";
import Svg from "../../Svg";
import WithContextLoader from "./WithContextLoader";
import {UIEventSource} from "../../Logic/UIEventSource";
import BaseUIElement from "../../UI/BaseUIElement";
import {FixedUiElement} from "../../UI/Base/FixedUiElement";
import Img from "../../UI/Base/Img";
import Combine from "../../UI/Base/Combine";
import {VariableUiElement} from "../../UI/Base/VariableUIElement";
export default class PointRenderingConfig extends WithContextLoader {
private static readonly allowed_location_codes = new Set<string>(["point", "centroid","start","end"])
public readonly location: Set<"point" | "centroid" | "start" | "end">
public readonly icon: TagRenderingConfig;
public readonly iconBadges: { if: TagsFilter; then: TagRenderingConfig }[];
public readonly iconSize: TagRenderingConfig;
public readonly label: TagRenderingConfig;
public readonly rotation: TagRenderingConfig;
constructor(json: PointRenderingConfigJson, context: string) {
super(json, context)
if(typeof json.location === "string"){
json.location = [json.location]
}
this.location = new Set(json.location)
this.location.forEach(l => {
const allowed = PointRenderingConfig.allowed_location_codes
if(!allowed.has(l)){
throw `A point rendering has an invalid location: '${l}' is not one of ${Array.from(allowed).join(", ")} (at ${context}.location)`
}
})
if(json.icon === undefined && json.label === undefined){
throw `A point rendering should define at least an icon or a label`
}
if(this.location.size == 0){
throw "A pointRendering should have at least one 'location' to defined where it should be rendered. (At "+context+".location)"
}
this.icon = this.tr("icon", "");
this.iconBadges = (json.iconBadges ?? []).map((overlay, i) => {
let tr : TagRenderingConfig;
if (typeof overlay.then === "string" &&
SharedTagRenderings.SharedIcons.get(overlay.then) !== undefined) {
tr = SharedTagRenderings.SharedIcons.get(overlay.then);
}else{
tr = new TagRenderingConfig(
overlay.then,
`iconBadges.${i}`
);
}
return {
if: TagUtils.Tag(overlay.if),
then: tr
};
});
const iconPath = this.icon.GetRenderValue({id: "node/-1"}).txt;
if (iconPath.startsWith(Utils.assets_path)) {
const iconKey = iconPath.substr(Utils.assets_path.length);
if (Svg.All[iconKey] === undefined) {
throw "Builtin SVG asset not found: " + iconPath;
}
}
this.iconSize = this.tr("iconSize", "40,40,center");
this.label = this.tr("label", undefined);
this.rotation = this.tr("rotation", "0");
}
public ExtractImages(): Set<string> {
const parts: Set<string>[] = [];
parts.push(this.icon?.ExtractImages(true));
parts.push(
...this.iconBadges?.map((overlay) => overlay.then.ExtractImages(true))
);
const allIcons = new Set<string>();
for (const part of parts) {
part?.forEach(allIcons.add, allIcons);
}
return allIcons;
}
/**
* Given a single HTML spec (either a single image path OR "image_path_to_known_svg:fill-colour", returns a fixedUIElement containing that
* The element will fill 100% and be positioned absolutely with top:0 and left: 0
*/
private static FromHtmlSpec(htmlSpec: string, style: string, isBadge = false): BaseUIElement {
if (htmlSpec === undefined) {
return undefined;
}
const match = htmlSpec.match(/([a-zA-Z0-9_]*):([^;]*)/);
if (match !== null && Svg.All[match[1] + ".svg"] !== undefined) {
const svg = (Svg.All[match[1] + ".svg"] as string)
const targetColor = match[2]
const img = new Img(svg.replace(/#000000/g, targetColor), true)
.SetStyle(style)
if(isBadge){
img.SetClass("badge")
}
return img
} else {
return new FixedUiElement(`<img src="${htmlSpec}" style="${style}" />`);
}
}
private static FromHtmlMulti(multiSpec: string, rotation: string , isBadge: boolean, defaultElement: BaseUIElement = undefined){
if(multiSpec === undefined){
return defaultElement
}
const style = `width:100%;height:100%;transform: rotate( ${rotation} );display:block;position: absolute; top: 0; left: 0`;
const htmlDefs = multiSpec.trim()?.split(";") ?? []
const elements = Utils.NoEmpty(htmlDefs).map(def => PointRenderingConfig.FromHtmlSpec(def, style, isBadge))
if (elements.length === 0) {
return defaultElement
} else {
return new Combine(elements).SetClass("relative block w-full h-full")
}
}
public GetSimpleIcon(tags: UIEventSource<any>): BaseUIElement {
const self = this;
if (this.icon === undefined) {
return undefined;
}
return new VariableUiElement(tags.map(tags => {
const rotation = Utils.SubstituteKeys(self.rotation?.GetRenderValue(tags)?.txt ?? "0deg", tags)
const htmlDefs = Utils.SubstituteKeys(self.icon.GetRenderValue(tags)?.txt, tags)
let defaultPin : BaseUIElement = undefined
if(self.label === undefined){
defaultPin = Svg.teardrop_with_hole_green_svg()
}
return PointRenderingConfig.FromHtmlMulti(htmlDefs, rotation,false, defaultPin)
})).SetClass("w-full h-full block")
}
private GetBadges(tags: UIEventSource<any>): BaseUIElement {
if (this.iconBadges.length === 0) {
return undefined
}
return new VariableUiElement(
tags.map(tags => {
const badgeElements = this.iconBadges.map(badge => {
if (!badge.if.matchesProperties(tags)) {
// Doesn't match...
return undefined
}
const htmlDefs = Utils.SubstituteKeys(badge.then.GetRenderValue(tags)?.txt, tags)
const badgeElement= PointRenderingConfig.FromHtmlMulti(htmlDefs, "0", true)?.SetClass("block relative")
if(badgeElement === undefined){
return undefined;
}
return new Combine([badgeElement]).SetStyle("width: 1.5rem").SetClass("block")
})
return new Combine(badgeElements).SetClass("inline-flex h-full")
})).SetClass("absolute bottom-0 right-1/3 h-1/2 w-0")
}
private GetLabel(tags: UIEventSource<any>): BaseUIElement {
if (this.label === undefined) {
return undefined;
}
const self = this;
return new VariableUiElement(tags.map(tags => {
const label = self.label
?.GetRenderValue(tags)
?.Subs(tags)
?.SetClass("block text-center")
return new Combine([label]).SetClass("flex flex-col items-center mt-1")
}))
}
public GenerateLeafletStyle(
tags: UIEventSource<any>,
clickable: boolean,
options?: {
noSize: false | boolean
}
):
{
html: BaseUIElement;
iconSize: [number, number];
iconAnchor: [number, number];
popupAnchor: [number, number];
iconUrl: string;
className: string;
} {
function num(str, deflt = 40) {
const n = Number(str);
if (isNaN(n)) {
return deflt;
}
return n;
}
function render(tr: TagRenderingConfig, deflt?: string) {
if (tags === undefined) {
return deflt
}
const str = tr?.GetRenderValue(tags.data)?.txt ?? deflt;
return Utils.SubstituteKeys(str, tags.data).replace(/{.*}/g, "");
}
const iconSize = render(this.iconSize, "40,40,center").split(",");
const iconW = num(iconSize[0]);
let iconH = num(iconSize[1]);
const mode = iconSize[2]?.trim()?.toLowerCase() ?? "center";
let anchorW = iconW / 2;
let anchorH = iconH / 2;
if (mode === "left") {
anchorW = 0;
}
if (mode === "right") {
anchorW = iconW;
}
if (mode === "top") {
anchorH = 0;
}
if (mode === "bottom") {
anchorH = iconH;
}
const iconAndBadges = new Combine([this.GetSimpleIcon(tags), this.GetBadges(tags)])
.SetClass("block relative")
if(!options?.noSize){
iconAndBadges.SetStyle(`width: ${iconW}px; height: ${iconH}px`)
}else{
iconAndBadges.SetClass("w-full h-full")
}
return {
html: new Combine([iconAndBadges, this.GetLabel(tags)]).SetStyle("flex flex-col"),
iconSize: [iconW, iconH],
iconAnchor: [anchorW, anchorH],
popupAnchor: [0, 3 - anchorH],
iconUrl: undefined,
className: clickable
? "leaflet-div-icon"
: "leaflet-div-icon unclickable",
};
}
}

View file

@ -15,6 +15,7 @@ import {Tag} from "../../Logic/Tags/Tag";
export default class TagRenderingConfig {
readonly id: string;
readonly group: string;
readonly render?: Translation;
readonly question?: Translation;
readonly condition?: TagsFilter;
@ -39,9 +40,8 @@ export default class TagRenderingConfig {
readonly hideInAnswer: boolean | TagsFilter
readonly addExtraTags: Tag[]
}[]
readonly roaming: boolean;
constructor(json: string | TagRenderingConfigJson, conditionIfRoaming: TagsFilter, context?: string) {
constructor(json: string | TagRenderingConfigJson, context?: string) {
if (json === "questions") {
// Very special value
@ -49,7 +49,14 @@ export default class TagRenderingConfig {
this.question = null;
this.condition = null;
}
if(typeof json === "number"){
this.render = Translations.WT( ""+json)
return;
}
if (json === undefined) {
throw "Initing a TagRenderingConfig with undefined in " + context;
}
@ -61,18 +68,10 @@ export default class TagRenderingConfig {
this.id = json.id ?? "";
this.group = json.group ?? "";
this.render = Translations.T(json.render, context + ".render");
this.question = Translations.T(json.question, context + ".question");
this.roaming = json.roaming ?? false;
if(this.roaming){
console.warn("Deprecation notice: roaming renderings will be scrapped.", this.id, context)
}
const condition = TagUtils.Tag(json.condition ?? {"and": []}, `${context}.condition`);
if (this.roaming && conditionIfRoaming !== undefined) {
this.condition = new And([condition, conditionIfRoaming]);
} else {
this.condition = condition;
}
this.condition = TagUtils.Tag(json.condition ?? {"and": []}, `${context}.condition`);
if (json.freeform) {
if(json.freeform.addExtraTags !== undefined && json.freeform.addExtraTags.map === undefined){
@ -149,7 +148,7 @@ export default class TagRenderingConfig {
const mp = {
if: TagUtils.Tag(mapping.if, `${ctx}.if`),
ifnot: (mapping.ifnot !== undefined ? TagUtils.Tag(mapping.ifnot, `${ctx}.ifnot`) : undefined),
then: Translations.T(mapping.then, `{mappingContext}.then`),
then: Translations.T(mapping.then, `${ctx}.then`),
hideInAnswer: hideInAnswer,
addExtraTags: (mapping.addExtraTags??[]).map((str, j) => TagUtils.SimpleTag(str, `${ctx}.addExtraTags[${j}]`))
};
@ -229,7 +228,6 @@ export default class TagRenderingConfig {
}
}
/**
* Returns true if it is known or not shown, false if the question should be asked
* @constructor
@ -262,11 +260,6 @@ export default class TagRenderingConfig {
return false;
}
public IsQuestionBoxElement(): boolean {
return this.question === null && this.condition === null;
}
/**
* Gets all the render values. Will return multiple render values if 'multianswer' is enabled.
* The result will equal [GetRenderValue] if not 'multiAnswer'
@ -311,7 +304,7 @@ export default class TagRenderingConfig {
* Not compatible with multiAnswer - use GetRenderValueS instead in that case
* @constructor
*/
public GetRenderValue(tags: any): Translation {
public GetRenderValue(tags: any, defltValue: any = undefined): Translation {
if (this.mappings !== undefined && !this.multiAnswer) {
for (const mapping of this.mappings) {
if (mapping.if === undefined) {
@ -331,7 +324,7 @@ export default class TagRenderingConfig {
if (tags[this.freeform.key] !== undefined) {
return this.render;
}
return undefined;
return defltValue;
}
public ExtractImages(isIcon: boolean): Set<string> {

View file

@ -0,0 +1,102 @@
import TagRenderingConfig from "./TagRenderingConfig";
import SharedTagRenderings from "../../Customizations/SharedTagRenderings";
import {TagRenderingConfigJson} from "./Json/TagRenderingConfigJson";
import {Utils} from "../../Utils";
export default class WithContextLoader {
private readonly _json: any;
protected readonly _context: string;
constructor(json: any, context: string) {
this._json = json;
this._context = context;
}
/** Given a key, gets the corresponding property from the json (or the default if not found
*
* The found value is interpreted as a tagrendering and fetched/parsed
* */
public tr(key: string, deflt) {
const v = this._json[key];
if (v === undefined || v === null) {
if (deflt === undefined) {
return undefined;
}
return new TagRenderingConfig(
deflt,
`${this._context}.${key}.default value`
);
}
if (typeof v === "string") {
const shared = SharedTagRenderings.SharedTagRendering.get(v);
if (shared) {
return shared;
}
}
return new TagRenderingConfig(
v,
`${this._context}.${key}`
);
}
/**
* Converts a list of tagRenderingCOnfigJSON in to TagRenderingConfig
* A string is interpreted as a name to call
*/
public ParseTagRenderings(
tagRenderings?: (string | { builtin: string, override: any } | TagRenderingConfigJson)[],
readOnly = false,
prepConfig: ((config: TagRenderingConfigJson) => TagRenderingConfigJson) = undefined
) : TagRenderingConfig[]{
if (tagRenderings === undefined) {
return [];
}
const context = this._context
const renderings: TagRenderingConfig[] = []
if (prepConfig === undefined) {
prepConfig = c => c
}
for (let i = 0; i < tagRenderings.length; i++) {
let renderingJson = tagRenderings[i]
if (typeof renderingJson === "string") {
renderingJson = {builtin: renderingJson, override: undefined}
}
if (renderingJson["builtin"] !== undefined) {
const renderingId = renderingJson["builtin"]
if (renderingId === "questions") {
if (readOnly) {
throw `A tagrendering has a question, but asking a question does not make sense here: is it a title icon or a geojson-layer? ${context}. The offending tagrendering is ${JSON.stringify(
renderingJson
)}`;
}
const tr = new TagRenderingConfig("questions", context);
renderings.push(tr)
continue;
}
let sharedJson = SharedTagRenderings.SharedTagRenderingJson.get(renderingId)
if (sharedJson === undefined) {
const keys = Array.from(SharedTagRenderings.SharedTagRenderingJson.keys());
throw `Predefined tagRendering ${renderingId} not found in ${context}.\n Try one of ${keys.join(
", "
)}\n If you intent to output this text literally, use {\"render\": <your text>} instead"}`;
}
if (renderingJson["override"] !== undefined) {
sharedJson = Utils.Merge(renderingJson["override"], sharedJson)
}
renderingJson = sharedJson
}
const patchedConfig = prepConfig(<TagRenderingConfigJson>renderingJson)
const tr = new TagRenderingConfig(patchedConfig, `${context}.tagrendering[${i}]`);
renderings.push(tr)
}
return renderings;
}
}

View file

@ -33,6 +33,7 @@ It is possible to quickly make and distribute your own theme
## Examples
- [An overview of all official themes](https://pietervdvn.github.io/mc/develop/index.html).
- [Buurtnatuur.be](http://buurtnatuur.be), developed for the Belgian [Green party](https://www.groen.be/). They also
funded the initial development!
- [Cyclofix](https://pietervdvn.github.io/MapComplete/index.html?layout=cyclofix), further development
@ -43,7 +44,7 @@ It is possible to quickly make and distribute your own theme
- [Map of Maps](https://pietervdvn.github.io/MapComplete/index.html?layout=maps&z=14&lat=50.650&lon=4.2668#element),
after a tweet
There are plenty more. Discover them in the app.
There are plenty more. [Discover them in the app](https://mapcomplete.osm.be/index.html).
### Statistics

View file

@ -7,11 +7,14 @@ import UserRelatedState from "../Logic/State/UserRelatedState";
import {Utils} from "../Utils";
import LanguagePicker from "./LanguagePicker";
import IndexText from "./BigComponents/IndexText";
import {feature} from "@turf/turf";
import FeaturedMessage from "./BigComponents/FeaturedMessage";
import {AllKnownLayouts} from "../Customizations/AllKnownLayouts";
export default class AllThemesGui {
constructor() {
try{
new FixedUiElement("").AttachTo("centermessage")
const state = new UserRelatedState(undefined);
const intro = new Combine([
@ -30,5 +33,9 @@ export default class AllThemesGui {
]).SetClass("block m-5 lg:w-3/4 lg:ml-40")
.SetStyle("pointer-events: all;")
.AttachTo("topleft-tools");
}catch (e) {
new FixedUiElement("Seems like no layers are compiled - check the output of `npm run generate:layeroverview`. Is this visible online? Contact pietervdvn immediately!").SetClass("alert")
.AttachTo("centermessage")
}
}
}
}

28
UI/Base/AsyncLazy.ts Normal file
View file

@ -0,0 +1,28 @@
import BaseUIElement from "../BaseUIElement";
import {VariableUiElement} from "./VariableUIElement";
import {UIEventSource} from "../../Logic/UIEventSource";
import Loading from "./Loading";
export default class AsyncLazy extends BaseUIElement{
private readonly _f: () => Promise<BaseUIElement>;
constructor(f: () => Promise<BaseUIElement>) {
super();
this._f = f;
}
protected InnerConstructElement(): HTMLElement {
// The caching of the BaseUIElement will guarantee that _f will only be called once
return new VariableUiElement(
UIEventSource.FromPromise(this._f()).map(el => {
if(el === undefined){
return new Loading()
}
return el
})
).ConstructElement()
}
}

View file

@ -19,6 +19,7 @@ export interface MinimapOptions {
export interface MinimapObj {
readonly leafletMap: UIEventSource<any>,
installBounds(factor: number | BBox, showRange?: boolean) : void
TakeScreenshot(): Promise<any>;
}
export default class Minimap {

View file

@ -8,6 +8,8 @@ import * as L from "leaflet";
import {Map} from "leaflet";
import Minimap, {MinimapObj, MinimapOptions} from "./Minimap";
import {BBox} from "../../Logic/BBox";
import 'leaflet-polylineoffset'
import {SimpleMapScreenshoter} from "leaflet-simple-map-screenshoter";
export default class MinimapImplementation extends BaseUIElement implements MinimapObj {
private static _nextId = 0;
@ -277,4 +279,10 @@ export default class MinimapImplementation extends BaseUIElement implements Mini
this.leafletMap.setData(map)
}
public async TakeScreenshot(){
const screenshotter = new SimpleMapScreenshoter();
screenshotter.addTo(this.leafletMap.data);
return await screenshotter.takeScreen('image')
}
}

View file

@ -19,6 +19,7 @@ import Img from "./Img";
export default class ScrollableFullScreen extends UIElement {
private static readonly empty = new FixedUiElement("");
private static _currentlyOpen: ScrollableFullScreen;
private hashToShow: string;
public isShown: UIEventSource<boolean>;
private _component: BaseUIElement;
private _fullscreencomponent: BaseUIElement;
@ -28,6 +29,7 @@ export default class ScrollableFullScreen extends UIElement {
isShown: UIEventSource<boolean> = new UIEventSource<boolean>(false)
) {
super();
this.hashToShow = hashToShow;
this.isShown = isShown;
if (hashToShow === undefined) {
@ -45,24 +47,25 @@ export default class ScrollableFullScreen extends UIElement {
self.Activate();
Hash.hash.setData(hashToShow)
} else {
ScrollableFullScreen.clear();
self.clear();
}
})
Hash.hash.addCallback(hash => {
if (hash === hashToShow) {
return
if (!isShown.data) {
return;
}
if (hash === undefined || hash === "") {
isShown.setData(false)
}
isShown.setData(false)
})
}
private static clear() {
private clear() {
ScrollableFullScreen.empty.AttachTo("fullscreen")
const fs = document.getElementById("fullscreen");
ScrollableFullScreen._currentlyOpen?.isShown?.setData(false);
fs.classList.add("hidden")
Hash.hash.setData(undefined);
}
InnerRender(): BaseUIElement {

View file

@ -21,6 +21,9 @@ export class TabbedComponent extends Combine {
let element = elements[i];
const header = Translations.W(element.header).onClick(() => openedTabSrc.setData(i))
openedTabSrc.addCallbackAndRun(selected => {
if(selected >= elements.length){
selected = 0
}
if (selected === i) {
header.SetClass("tab-active")
header.RemoveClass("tab-non-active")

View file

@ -45,6 +45,7 @@ export default abstract class BaseUIElement {
* Adds all the relevant classes, space separated
*/
public SetClass(clss: string) {
if(clss == undefined){return }
const all = clss.split(" ").map(clsName => clsName.trim());
let recordedChange = false;
for (let c of all) {

View file

@ -16,12 +16,12 @@ export default class AddNewMarker extends Combine {
const layer = filteredLayer.layerDef;
for (const preset of filteredLayer.layerDef.presets) {
const tags = TagUtils.KVtoProperties(preset.tags)
const icon = layer.GenerateLeafletStyle(new UIEventSource<any>(tags), false).icon.html
const icon = layer.mapRendering[0].GenerateLeafletStyle(new UIEventSource<any>(tags), false).html
.SetClass("block relative")
.SetStyle("width: 42px; height: 42px;");
icons.push(icon)
if (last === undefined) {
last = layer.GenerateLeafletStyle(new UIEventSource<any>(tags), false).icon.html
last = layer.mapRendering[0].GenerateLeafletStyle(new UIEventSource<any>(tags), false).html
.SetClass("block relative")
.SetStyle("width: 42px; height: 42px;");
}

View file

@ -56,6 +56,7 @@ export default class Attribution extends Combine {
)
)
super([mapComplete, reportBug, stats, editHere, editWithJosm, mapillary]);
this.SetClass("flex")
}

View file

@ -12,23 +12,99 @@ import {VariableUiElement} from "../Base/VariableUIElement";
import * as contributors from "../../assets/contributors.json"
import BaseUIElement from "../BaseUIElement";
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
import Title from "../Base/Title";
import {SubtleButton} from "../Base/SubtleButton";
import Svg from "../../Svg";
import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline";
import {BBox} from "../../Logic/BBox";
import Loc from "../../Models/Loc";
import Toggle from "../Input/Toggle";
import {OsmConnection} from "../../Logic/Osm/OsmConnection";
import Constants from "../../Models/Constants";
/**
* The attribution panel shown on mobile
*/
export default class AttributionPanel extends Combine {
export default class CopyrightPanel extends Combine {
private static LicenseObject = AttributionPanel.GenerateLicenses();
private static LicenseObject = CopyrightPanel.GenerateLicenses();
constructor(layoutToUse: LayoutConfig, contributions: UIEventSource<Map<string, number>>) {
constructor(state: {
layoutToUse: LayoutConfig,
featurePipeline: FeaturePipeline,
currentBounds: UIEventSource<BBox>,
locationControl: UIEventSource<Loc>,
osmConnection: OsmConnection
}, contributions: UIEventSource<Map<string, number>>) {
const t =Translations.t.general.attribution
const layoutToUse = state.layoutToUse
const josmState = new UIEventSource<string>(undefined)
// Reset after 15s
josmState.stabilized(15000).addCallbackD(_ => josmState.setData(undefined))
const iconStyle = "height: 1.5rem; width: auto"
const actionButtons = [
new SubtleButton(Svg.liberapay_ui().SetStyle(iconStyle), t.donate, {
url: "https://liberapay.com/pietervdvn/",
newTab: true
}),
new SubtleButton(Svg.bug_ui().SetStyle(iconStyle), t.openIssueTracker, {
url: "https://github.com/pietervdvn/MapComplete/issues",
newTab: true
}),
new SubtleButton(Svg.statistics_ui().SetStyle(iconStyle), t.openOsmcha.Subs({theme: state.layoutToUse.title}), {
url: Utils.OsmChaLinkFor(31, state.layoutToUse.id),
newTab: true
}),
new VariableUiElement(state.locationControl.map(location => {
const idLink = `https://www.openstreetmap.org/edit?editor=id#map=${location?.zoom ?? 0}/${location?.lat ?? 0}/${location?.lon ?? 0}`
return new SubtleButton(Svg.pencil_ui().SetStyle(iconStyle), t.editId, {url: idLink, newTab: true})
})),
new VariableUiElement(state.locationControl.map(location => {
const mapillaryLink = `https://www.mapillary.com/app/?focus=map&lat=${location?.lat ?? 0}&lng=${location?.lon ?? 0}&z=${Math.max((location?.zoom ?? 2) - 1, 1)}`
return new SubtleButton(Svg.mapillary_black_ui().SetStyle(iconStyle), t.openMapillary, {url: mapillaryLink, newTab: true})
})),
new VariableUiElement(josmState.map(state => {
if(state === undefined){
return undefined
}
state = state.toUpperCase()
if(state === "OK"){
return t.josmOpened.SetClass("thanks")
}
return t.josmNotOpened.SetClass("alert")
})),
new Toggle(
new SubtleButton(Svg.josm_logo_ui().SetStyle(iconStyle) , t.editJosm).onClick(() => {
const bounds: any = state.currentBounds.data;
if (bounds === undefined) {
return undefined
}
const top = bounds.getNorth();
const bottom = bounds.getSouth();
const right = bounds.getEast();
const left = bounds.getWest();
const josmLink = `http://127.0.0.1:8111/load_and_zoom?left=${left}&right=${right}&top=${top}&bottom=${bottom}`
Utils.download(josmLink).then(answer => josmState.setData(answer.replace(/\n/g, '').trim())).catch(_ => josmState.setData("ERROR"))
}), undefined, state.osmConnection.userDetails.map(ud => ud.loggedIn && ud.csCount >= Constants.userJourney.historyLinkVisible)),
].map(button => button.SetStyle("max-height: 3rem"))
const iconAttributions = Utils.NoNull(Array.from(layoutToUse.ExtractImages()))
.map(CopyrightPanel.IconAttribution)
let maintainer : BaseUIElement= undefined
if(layoutToUse.maintainer !== undefined && layoutToUse.maintainer !== "" && layoutToUse.maintainer.toLowerCase() !== "mapcomplete"){
maintainer = Translations.t.general.attribution.themeBy.Subs({author: layoutToUse.maintainer})
}
super([
Translations.t.general.attribution.attributionContent,
((layoutToUse.maintainer ?? "") == "") ? "" : Translations.t.general.attribution.themeBy.Subs({author: layoutToUse.maintainer}),
layoutToUse.credits,
"<br/>",
maintainer,
new Combine(actionButtons).SetClass("block w-full"),
new FixedUiElement(layoutToUse.credits),
new Attribution(State.state.locationControl, State.state.osmConnection.userDetails, State.state.layoutToUse, State.state.currentBounds),
"<br/>",
new VariableUiElement(contributions.map(contributions => {
if(contributions === undefined){
return ""
@ -62,14 +138,12 @@ export default class AttributionPanel extends Combine {
})),
"<br/>",
AttributionPanel.CodeContributors(),
"<h3>", Translations.t.general.attribution.iconAttribution.title.Clone().SetClass("pt-6 pb-3"), "</h3>",
...Utils.NoNull(Array.from(layoutToUse.ExtractImages()))
.map(AttributionPanel.IconAttribution)
]);
CopyrightPanel.CodeContributors(),
new Title(t.iconAttribution.title, 3),
...iconAttributions
].map(e => e?.SetClass("mt-4")));
this.SetClass("flex flex-col link-underline overflow-hidden")
this.SetStyle("max-width: calc(100vw - 5em); width: 40rem;")
this.SetStyle("max-width: calc(100vw - 5em); width: 40rem; margin-left: 0.75rem; margin-right: 0.5rem")
}
private static CodeContributors(): BaseUIElement {
@ -97,7 +171,7 @@ export default class AttributionPanel extends Combine {
iconPath = "." + new URL(iconPath).pathname;
}
const license: SmallLicense = AttributionPanel.LicenseObject[iconPath]
const license: SmallLicense = CopyrightPanel.LicenseObject[iconPath]
if (license == undefined) {
return undefined;
}

View file

@ -42,9 +42,8 @@ export default class FilterView extends VariableUiElement {
);
const name: Translation = config.config.name;
const styledNameChecked = name.Clone().SetStyle("font-size:large;padding-left:1.25rem");
const styledNameUnChecked = name.Clone().SetStyle("font-size:large;padding-left:1.25rem");
const styledNameChecked = name.Clone().SetStyle("font-size:large").SetClass("ml-2");
const styledNameUnChecked = name.Clone().SetStyle("font-size:large").SetClass("ml-2");
const zoomStatus =
new Toggle(
@ -82,6 +81,8 @@ export default class FilterView extends VariableUiElement {
const iconStyle = "width:1.5rem;height:1.5rem;margin-left:1.25rem;flex-shrink: 0;";
const icon = new Combine([Svg.checkbox_filled]).SetStyle(iconStyle);
const layer = filteredLayer.layerDef
const iconUnselected = new Combine([Svg.checkbox_empty]).SetStyle(
iconStyle
);
@ -95,9 +96,9 @@ export default class FilterView extends VariableUiElement {
filteredLayer.layerDef.name
);
const styledNameChecked = name.Clone().SetStyle("font-size:large;padding-left:1.25rem");
const styledNameChecked = name.Clone().SetStyle("font-size:large").SetClass("ml-3");
const styledNameUnChecked = name.Clone().SetStyle("font-size:large;padding-left:1.25rem");
const styledNameUnChecked = name.Clone().SetStyle("font-size:large").SetClass("ml-3");
const zoomStatus =
new Toggle(
@ -111,11 +112,14 @@ export default class FilterView extends VariableUiElement {
const style =
"display:flex;align-items:center;padding:0.5rem 0;";
const layerChecked = new Combine([icon, styledNameChecked, zoomStatus])
const layerIcon = layer.defaultIcon()?.SetClass("w-8 h-8 ml-2")
const layerIconUnchecked = layer.defaultIcon()?.SetClass("opacity-50 w-8 h-8 ml-2")
const layerChecked = new Combine([icon, layerIcon, styledNameChecked, zoomStatus])
.SetStyle(style)
.onClick(() => filteredLayer.isDisplayed.setData(false));
const layerNotChecked = new Combine([iconUnselected, styledNameUnChecked])
const layerNotChecked = new Combine([iconUnselected, layerIconUnchecked, styledNameUnChecked])
.SetStyle(style)
.onClick(() => filteredLayer.isDisplayed.setData(true));

View file

@ -14,6 +14,9 @@ import Toggle from "../Input/Toggle";
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
import {Utils} from "../../Utils";
import UserRelatedState from "../../Logic/State/UserRelatedState";
import Loc from "../../Models/Loc";
import BaseLayer from "../../Models/BaseLayer";
import FilteredLayer from "../../Models/FilteredLayer";
export default class FullWelcomePaneWithTabs extends ScrollableFullScreen {
@ -24,7 +27,10 @@ export default class FullWelcomePaneWithTabs extends ScrollableFullScreen {
layoutToUse: LayoutConfig,
osmConnection: OsmConnection,
featureSwitchShareScreen: UIEventSource<boolean>,
featureSwitchMoreQuests: UIEventSource<boolean>
featureSwitchMoreQuests: UIEventSource<boolean>,
locationControl: UIEventSource<Loc>,
backgroundLayer: UIEventSource<BaseLayer>,
filteredLayers: UIEventSource<FilteredLayer[]>
} & UserRelatedState) {
const layoutToUse = state.layoutToUse;
super(
@ -39,7 +45,8 @@ export default class FullWelcomePaneWithTabs extends ScrollableFullScreen {
layoutToUse: LayoutConfig,
osmConnection: OsmConnection,
featureSwitchShareScreen: UIEventSource<boolean>,
featureSwitchMoreQuests: UIEventSource<boolean>
featureSwitchMoreQuests: UIEventSource<boolean>,
locationControl: UIEventSource<Loc>, backgroundLayer: UIEventSource<BaseLayer>, filteredLayers: UIEventSource<FilteredLayer[]>
} & UserRelatedState,
isShown: UIEventSource<boolean>):
{ header: string | BaseUIElement; content: BaseUIElement }[] {
@ -77,7 +84,8 @@ export default class FullWelcomePaneWithTabs extends ScrollableFullScreen {
layoutToUse: LayoutConfig,
osmConnection: OsmConnection,
featureSwitchShareScreen: UIEventSource<boolean>,
featureSwitchMoreQuests: UIEventSource<boolean>
featureSwitchMoreQuests: UIEventSource<boolean>,
locationControl: UIEventSource<Loc>, backgroundLayer: UIEventSource<BaseLayer>, filteredLayers: UIEventSource<FilteredLayer[]>
} & UserRelatedState, currentTab: UIEventSource<number>, isShown: UIEventSource<boolean>) {
const tabs = FullWelcomePaneWithTabs.ConstructBaseTabs(state, isShown)

View file

@ -4,27 +4,212 @@ import {UIEventSource} from "../../Logic/UIEventSource";
import Combine from "../Base/Combine";
import {VariableUiElement} from "../Base/VariableUIElement";
import Translations from "../i18n/Translations";
import State from "../../State";
import Constants from "../../Models/Constants";
import Toggle from "../Input/Toggle";
import CreateNewNodeAction from "../../Logic/Osm/Actions/CreateNewNodeAction";
import {Tag} from "../../Logic/Tags/Tag";
import Loading from "../Base/Loading";
import CreateNewWayAction from "../../Logic/Osm/Actions/CreateNewWayAction";
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
import {OsmConnection} from "../../Logic/Osm/OsmConnection";
import {Changes} from "../../Logic/Osm/Changes";
import {ElementStorage} from "../../Logic/ElementStorage";
import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline";
import Lazy from "../Base/Lazy";
import ConfirmLocationOfPoint from "../NewPoint/ConfirmLocationOfPoint";
import {PresetInfo} from "./SimpleAddUI";
import Img from "../Base/Img";
import {Translation} from "../i18n/Translation";
import FilteredLayer from "../../Models/FilteredLayer";
import SpecialVisualizations, {SpecialVisualization} from "../SpecialVisualizations";
import {FixedUiElement} from "../Base/FixedUiElement";
import Svg from "../../Svg";
import {Utils} from "../../Utils";
import Minimap from "../Base/Minimap";
import ShowDataLayer from "../ShowDataLayer/ShowDataLayer";
import AllKnownLayers from "../../Customizations/AllKnownLayers";
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource";
import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer";
import BaseLayer from "../../Models/BaseLayer";
import ReplaceGeometryAction from "../../Logic/Osm/Actions/ReplaceGeometryAction";
import FullNodeDatabaseSource from "../../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource";
import CreateWayWithPointReuseAction from "../../Logic/Osm/Actions/CreateWayWithPointReuseAction";
import OsmChangeAction from "../../Logic/Osm/Actions/OsmChangeAction";
import FeatureSource from "../../Logic/FeatureSource/FeatureSource";
export interface ImportButtonState {
description?: Translation;
image: () => BaseUIElement,
message: string | BaseUIElement,
originalTags: UIEventSource<any>,
newTags: UIEventSource<Tag[]>,
targetLayer: FilteredLayer,
feature: any,
minZoom: number,
state: {
backgroundLayer: UIEventSource<BaseLayer>;
filteredLayers: UIEventSource<FilteredLayer[]>;
featureSwitchUserbadge: UIEventSource<boolean>;
featurePipeline: FeaturePipeline;
allElements: ElementStorage;
selectedElement: UIEventSource<any>;
layoutToUse: LayoutConfig,
osmConnection: OsmConnection,
changes: Changes,
locationControl: UIEventSource<{ zoom: number }>
},
guiState: { filterViewIsOpened: UIEventSource<boolean> },
snapSettings?: {
snapToLayers: string[],
snapToLayersMaxDist?: number
},
conflationSettings?: {
conflateWayId: string
}
}
export class ImportButtonSpecialViz implements SpecialVisualization {
funcName = "import_button"
docs = `This button will copy the data from an external dataset into OpenStreetMap. It is only functional in official themes but can be tested in unofficial themes.
#### Importing a dataset into OpenStreetMap: requirements
If you want to import a dataset, make sure that:
1. The dataset to import has a suitable license
2. The community has been informed of the import
3. All other requirements of the [import guidelines](https://wiki.openstreetmap.org/wiki/Import/Guidelines) have been followed
There are also some technicalities in your theme to keep in mind:
1. The new feature will be added and will flow through the program as any other new point as if it came from OSM.
This means that there should be a layer which will match the new tags and which will display it.
2. The original feature from your geojson layer will gain the tag '_imported=yes'.
This should be used to change the appearance or even to hide it (eg by changing the icon size to zero)
3. There should be a way for the theme to detect previously imported points, even after reloading.
A reference number to the original dataset is an excellent way to do this
4. When importing ways, the theme creator is also responsible of avoiding overlapping ways.
#### Disabled in unofficial themes
The import button can be tested in an unofficial theme by adding \`test=true\` or \`backend=osm-test\` as [URL-paramter](URL_Parameters.md).
The import button will show up then. If in testmode, you can read the changeset-XML directly in the web console.
In the case that MapComplete is pointed to the testing grounds, the edit will be made on ${OsmConnection.oauth_configs["osm-test"].url}
#### Specifying which tags to copy or add
The argument \`tags\` of the import button takes a \`;\`-seperated list of tags to add.
${Utils.Special_visualizations_tagsToApplyHelpText}
`
args = [
{
name: "targetLayer",
doc: "The id of the layer where this point should end up. This is not very strict, it will simply result in checking that this layer is shown preventing possible duplicate elements"
},
{
name: "tags",
doc: "The tags to add onto the new object - see specification above"
},
{
name: "text",
doc: "The text to show on the button",
defaultValue: "Import this data into OpenStreetMap"
},
{
name: "icon",
doc: "A nice icon to show in the button",
defaultValue: "./assets/svg/addSmall.svg"
},
{
name: "minzoom",
doc: "How far the contributor must zoom in before being able to import the point",
defaultValue: "18"
}, {
name: "Snap onto layer(s)/replace geometry with this other way",
doc: " - If the value corresponding with this key starts with 'way/' and the feature is a LineString or Polygon, the original OSM-way geometry will be changed to match the new geometry\n" +
" - If a way of the given layer(s) is closeby, will snap the new point onto this way (similar as preset might snap). To show multiple layers to snap onto, use a `;`-seperated list",
}, {
name: "snap max distance",
doc: "The maximum distance that this point will move to snap onto a layer (in meters)",
defaultValue: "5"
}]
constr(state, tagSource, args, guiState) {
if (!state.layoutToUse.official && !(state.featureSwitchIsTesting.data || state.osmConnection._oauth_config.url === OsmConnection.oauth_configs["osm-test"].url)) {
return new Combine([new FixedUiElement("The import button is disabled for unofficial themes to prevent accidents.").SetClass("alert"),
new FixedUiElement("To test, add <b>test=true</b> or <b>backend=osm-test</b> to the URL. The changeset will be printed in the console. Please open a PR to officialize this theme to actually enable the import button.")])
}
const newTags = SpecialVisualizations.generateTagsToApply(args[1], tagSource)
const id = tagSource.data.id;
const feature = state.allElements.ContainingFeatures.get(id)
let minZoom = args[4] == "" ? 18 : Number(args[4])
if (isNaN(minZoom)) {
console.warn("Invalid minzoom:", minZoom)
minZoom = 18
}
const message = args[2]
const imageUrl = args[3]
let img: () => BaseUIElement
const targetLayer: FilteredLayer = state.filteredLayers.data.filter(fl => fl.layerDef.id === args[0])[0]
if (imageUrl !== undefined && imageUrl !== "") {
img = () => new Img(imageUrl)
} else {
img = () => Svg.add_ui()
}
let snapSettings = undefined
let conflationSettings = undefined
const possibleWayId = tagSource.data[args[5]]
if (possibleWayId?.startsWith("way/")) {
// This is a conflation
conflationSettings = {
conflateWayId: possibleWayId
}
} else {
const snapToLayers = args[5]?.split(";").filter(s => s !== "")
const snapToLayersMaxDist = Number(args[6] ?? 6)
if (targetLayer === undefined) {
const e = "Target layer not defined: error in import button for theme: " + state.layoutToUse.id + ": layer " + args[0] + " not found"
console.error(e)
return new FixedUiElement(e).SetClass("alert")
}
snapSettings = {
snapToLayers,
snapToLayersMaxDist
}
}
return new ImportButton(
{
state, guiState, image: img,
feature, newTags, message, minZoom,
originalTags: tagSource,
targetLayer,
snapSettings,
conflationSettings
}
);
}
}
export default class ImportButton extends Toggle {
constructor(imageUrl: string | BaseUIElement, message: string | BaseUIElement,
originalTags: UIEventSource<any>,
newTags: UIEventSource<Tag[]>,
lat: number, lon: number,
minZoom: number,
state: {
locationControl: UIEventSource<{ zoom: number }>
}) {
constructor(o: ImportButtonState) {
const t = Translations.t.general.add;
const isImported = originalTags.map(tags => tags._imported === "yes")
const isImported = o.originalTags.map(tags => tags._imported === "yes")
const appliedTags = new Toggle(
new VariableUiElement(
newTags.map(tgs => {
o.newTags.map(tgs => {
const parts = []
for (const tag of tgs) {
parts.push(tag.key + "=" + tag.value)
@ -32,53 +217,232 @@ export default class ImportButton extends Toggle {
const txt = parts.join(" & ")
return t.presetInfo.Subs({tags: txt}).SetClass("subtle")
})), undefined,
State.state.osmConnection.userDetails.map(ud => ud.csCount >= Constants.userJourney.tagsVisibleAt)
o.state.osmConnection.userDetails.map(ud => ud.csCount >= Constants.userJourney.tagsVisibleAt)
)
const button = new SubtleButton(imageUrl, message)
const button = new SubtleButton(o.image(), o.message)
minZoom = Math.max(16, minZoom ?? 19)
o.minZoom = Math.max(16, o.minZoom ?? 19)
button.onClick(async () => {
if (isImported.data) {
return
}
originalTags.data["_imported"] = "yes"
originalTags.ping() // will set isImported as per its definition
const newElementAction = new CreateNewNodeAction(newTags.data, lat, lon, {
theme: State.state.layoutToUse.id,
changeType: "import"
})
await State.state.changes.applyAction(newElementAction)
State.state.selectedElement.setData(State.state.allElements.ContainingFeatures.get(
newElementAction.newElementId
))
console.log("Did set selected element to", State.state.allElements.ContainingFeatures.get(
newElementAction.newElementId
))
})
const withLoadingCheck = new Toggle(new Toggle(
new Loading(t.stillLoading.Clone()),
new Combine([button, appliedTags]).SetClass("flex flex-col"),
State.state.featurePipeline.runningQuery
),t.zoomInFurther.Clone(),
state.locationControl.map(l => l.zoom >= minZoom)
)
new Loading(t.stillLoading.Clone()),
new Combine([button, appliedTags]).SetClass("flex flex-col"),
o.state.featurePipeline.runningQuery
), t.zoomInFurther.Clone(),
o.state.locationControl.map(l => l.zoom >= o.minZoom)
)
const importButton = new Toggle(t.hasBeenImported, withLoadingCheck, isImported)
const importClicked = new UIEventSource(false);
const importFlow = new Toggle(
ImportButton.createConfirmPanel(o, isImported, importClicked),
importButton,
importClicked
)
button.onClick(() => {
importClicked.setData(true);
})
const pleaseLoginButton =
new Toggle(t.pleaseLogin.Clone()
.onClick(() => State.state.osmConnection.AttemptLogin())
.onClick(() => o.state.osmConnection.AttemptLogin())
.SetClass("login-button-friendly"),
undefined,
State.state.featureSwitchUserbadge)
o.state.featureSwitchUserbadge)
super(importButton,
pleaseLoginButton,
State.state.osmConnection.isLoggedIn
super(new Toggle(importFlow,
pleaseLoginButton,
o.state.osmConnection.isLoggedIn
),
t.wrongType,
new UIEventSource(ImportButton.canBeImported(o.feature))
)
}
public static createConfirmPanel(o: ImportButtonState,
isImported: UIEventSource<boolean>,
importClicked: UIEventSource<boolean>) {
const geometry = o.feature.geometry
if (geometry.type === "Point") {
return new Lazy(() => ImportButton.createConfirmPanelForPoint(o, isImported, importClicked))
}
if (geometry.type === "Polygon" || geometry.type == "LineString") {
return new Lazy(() => ImportButton.createConfirmForWay(o, isImported, importClicked))
}
console.error("Invalid type to import", geometry.type)
return new FixedUiElement("Invalid geometry type:" + geometry.type).SetClass("alert")
}
public static createConfirmForWay(o: ImportButtonState,
isImported: UIEventSource<boolean>,
importClicked: UIEventSource<boolean>): BaseUIElement {
const confirmationMap = Minimap.createMiniMap({
allowMoving: true,
background: o.state.backgroundLayer
})
confirmationMap.SetStyle("height: 20rem; overflow: hidden").SetClass("rounded-xl")
const relevantFeatures = Utils.NoNull([o.feature, o.state.allElements?.ContainingFeatures?.get(o.conflationSettings?.conflateWayId)])
// SHow all relevant data - including (eventually) the way of which the geometry will be replaced
new ShowDataMultiLayer({
leafletMap: confirmationMap.leafletMap,
enablePopups: false,
zoomToFeatures: true,
features: new StaticFeatureSource(relevantFeatures, false),
allElements: o.state.allElements,
layers: o.state.filteredLayers
})
let action : OsmChangeAction & {getPreview() : Promise<FeatureSource>}
const theme = o.state.layoutToUse.id
const changes = o.state.changes
let confirm: () => Promise<string>
if (o.conflationSettings !== undefined) {
action = new ReplaceGeometryAction(
o.state,
o.feature,
o.conflationSettings.conflateWayId,
{
theme: o.state.layoutToUse.id,
newTags: o.newTags.data
}
)
confirm = async () => {
changes.applyAction (action)
return o.feature.properties.id
}
} else {
const geom = o.feature.geometry
let coordinates: [number, number][]
if (geom.type === "LineString") {
coordinates = geom.coordinates
} else if (geom.type === "Polygon") {
coordinates = geom.coordinates[0]
}
action = new CreateWayWithPointReuseAction(
o.newTags.data,
coordinates,
// @ts-ignore
o.state,
[{
withinRangeOfM: 1,
ifMatches: new Tag("_is_part_of_building","true"),
mode:"move_osm_point"
}]
)
confirm = async () => {
changes.applyAction(action)
return undefined
}
}
action.getPreview().then(changePreview => {
new ShowDataLayer({
leafletMap: confirmationMap.leafletMap,
enablePopups: false,
zoomToFeatures: false,
features: changePreview,
allElements: o.state.allElements,
layerToShow: AllKnownLayers.sharedLayers.get("conflation")
})
})
const confirmButton = new SubtleButton(o.image(), o.message)
confirmButton.onClick(async () => {
{
if (isImported.data) {
return
}
o.originalTags.data["_imported"] = "yes"
o.originalTags.ping() // will set isImported as per its definition
const idToSelect = await confirm()
o.state.selectedElement.setData(o.state.allElements.ContainingFeatures.get(idToSelect))
}
})
const cancel = new SubtleButton(Svg.close_ui(), Translations.t.general.cancel).onClick(() => {
importClicked.setData(false)
})
return new Combine([confirmationMap, confirmButton, cancel]).SetClass("flex flex-col")
}
public static createConfirmPanelForPoint(
o: ImportButtonState,
isImported: UIEventSource<boolean>,
importClicked: UIEventSource<boolean>): BaseUIElement {
async function confirm() {
if (isImported.data) {
return
}
o.originalTags.data["_imported"] = "yes"
o.originalTags.ping() // will set isImported as per its definition
const geometry = o.feature.geometry
const lat = geometry.coordinates[1]
const lon = geometry.coordinates[0];
const newElementAction = new CreateNewNodeAction(o.newTags.data, lat, lon, {
theme: o.state.layoutToUse.id,
changeType: "import"
})
await o.state.changes.applyAction(newElementAction)
o.state.selectedElement.setData(o.state.allElements.ContainingFeatures.get(
newElementAction.newElementId
))
}
function cancel() {
importClicked.setData(false)
}
const presetInfo = <PresetInfo>{
tags: o.newTags.data,
icon: o.image,
description: o.description,
layerToAddTo: o.targetLayer,
name: o.message,
title: o.message,
preciseInput: {
snapToLayers: o.snapSettings?.snapToLayers,
maxSnapDistance: o.snapSettings?.snapToLayersMaxDist
}
}
const [lon, lat] = o.feature.geometry.coordinates
return new ConfirmLocationOfPoint(o.state, o.guiState.filterViewIsOpened, presetInfo, Translations.W(o.message), {
lon,
lat
}, confirm, cancel)
}
private static canBeImported(feature: any) {
const type = feature.geometry.type
return type === "Point" || type === "LineString" || (type === "Polygon" && feature.geometry.coordinates.length === 1)
}
}

View file

@ -1,7 +1,7 @@
import Combine from "../Base/Combine";
import ScrollableFullScreen from "../Base/ScrollableFullScreen";
import Translations from "../i18n/Translations";
import AttributionPanel from "./AttributionPanel";
import CopyrightPanel from "./CopyrightPanel";
import ContributorCount from "../../Logic/ContributorCount";
import Toggle from "../Input/Toggle";
import MapControlButton from "../MapControlButton";
@ -14,6 +14,8 @@ import Loc from "../../Models/Loc";
import {BBox} from "../../Logic/BBox";
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
import FilteredLayer from "../../Models/FilteredLayer";
import BaseLayer from "../../Models/BaseLayer";
import {OsmConnection} from "../../Logic/Osm/OsmConnection";
export default class LeftControls extends Combine {
@ -26,7 +28,9 @@ export default class LeftControls extends Combine {
featureSwitchEnableExport: UIEventSource<boolean>,
featureSwitchExportAsPdf: UIEventSource<boolean>,
filteredLayers: UIEventSource<FilteredLayer[]>,
featureSwitchFilter: UIEventSource<boolean>
featureSwitchFilter: UIEventSource<boolean>,
backgroundLayer: UIEventSource<BaseLayer>,
osmConnection: OsmConnection
},
guiState: {
downloadControlIsOpened: UIEventSource<boolean>,
@ -37,8 +41,8 @@ export default class LeftControls extends Combine {
const toggledCopyright = new ScrollableFullScreen(
() => Translations.t.general.attribution.attributionTitle.Clone(),
() =>
new AttributionPanel(
state.layoutToUse,
new CopyrightPanel(
state,
new ContributorCount(state).Contributors
),
"copyright",

View file

@ -4,17 +4,28 @@ import MapControlButton from "../MapControlButton";
import GeoLocationHandler from "../../Logic/Actors/GeoLocationHandler";
import Svg from "../../Svg";
import MapState from "../../Logic/State/MapState";
import ShowDataLayer from "../ShowDataLayer/ShowDataLayer";
import AllKnownLayers from "../../Customizations/AllKnownLayers";
export default class RightControls extends Combine {
constructor(state:MapState) {
const geolocatioHandler = new GeoLocationHandler(
state
)
new ShowDataLayer({
layerToShow: AllKnownLayers.sharedLayers.get("gps_location"),
leafletMap: state.leafletMap,
enablePopups: true,
features: geolocatioHandler.currentLocation
})
const geolocationButton = new Toggle(
new MapControlButton(
new GeoLocationHandler(
state.currentGPSLocation,
state.leafletMap,
state.layoutToUse
), {
geolocatioHandler
, {
dontStyle: true
}
),

View file

@ -8,11 +8,14 @@ import Toggle from "../Input/Toggle";
import Translations from "../i18n/Translations";
import BaseUIElement from "../BaseUIElement";
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
import MapState from "../../Logic/State/MapState";
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
import Loc from "../../Models/Loc";
import BaseLayer from "../../Models/BaseLayer";
import FilteredLayer from "../../Models/FilteredLayer";
export default class ShareScreen extends Combine {
constructor(state: MapState) {
constructor(state: {layoutToUse: LayoutConfig, locationControl: UIEventSource<Loc>, backgroundLayer: UIEventSource<BaseLayer>, filteredLayers: UIEventSource<FilteredLayer[]>}) {
const layout = state?.layoutToUse;
const tr = Translations.t.general.sharescreen;

View file

@ -12,18 +12,16 @@ import BaseUIElement from "../BaseUIElement";
import {VariableUiElement} from "../Base/VariableUIElement";
import Toggle from "../Input/Toggle";
import UserDetails, {OsmConnection} from "../../Logic/Osm/OsmConnection";
import LocationInput from "../Input/LocationInput";
import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers";
import CreateNewNodeAction from "../../Logic/Osm/Actions/CreateNewNodeAction";
import {OsmObject, OsmWay} from "../../Logic/Osm/OsmObject";
import PresetConfig from "../../Models/ThemeConfig/PresetConfig";
import FilteredLayer from "../../Models/FilteredLayer";
import {BBox} from "../../Logic/BBox";
import Loc from "../../Models/Loc";
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
import {Changes} from "../../Logic/Osm/Changes";
import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline";
import {ElementStorage} from "../../Logic/ElementStorage";
import ConfirmLocationOfPoint from "../NewPoint/ConfirmLocationOfPoint";
/*
* The SimpleAddUI is a single panel, which can have multiple states:
@ -33,8 +31,7 @@ import {ElementStorage} from "../../Logic/ElementStorage";
* - A 'read your unread messages before adding a point'
*/
/*private*/
interface PresetInfo extends PresetConfig {
export interface PresetInfo extends PresetConfig {
name: string | BaseUIElement,
icon: () => BaseUIElement,
layerToAddTo: FilteredLayer
@ -91,20 +88,29 @@ export default class SimpleAddUI extends Toggle {
if (preset === undefined) {
return presetsOverview
}
return SimpleAddUI.CreateConfirmButton(state, filterViewIsOpened, preset,
(tags, location, snapOntoWayId?: string) => {
if (snapOntoWayId === undefined) {
createNewPoint(tags, location, undefined)
} else {
OsmObject.DownloadObject(snapOntoWayId).addCallbackAndRunD(way => {
createNewPoint(tags, location, <OsmWay>way)
return true;
})
}
},
() => {
selectedPreset.setData(undefined)
})
function confirm(tags, location, snapOntoWayId?: string) {
if (snapOntoWayId === undefined) {
createNewPoint(tags, location, undefined)
} else {
OsmObject.DownloadObject(snapOntoWayId).addCallbackAndRunD(way => {
createNewPoint(tags, location, <OsmWay>way)
return true;
})
}
}
function cancel() {
selectedPreset.setData(undefined)
}
const message =Translations.t.general.add.addNew.Subs({category: preset.name});
return new ConfirmLocationOfPoint(state, filterViewIsOpened, preset,
message,
state.LastClickLocation.data,
confirm,
cancel)
}
))
@ -134,170 +140,7 @@ export default class SimpleAddUI extends Toggle {
}
private static CreateConfirmButton(
state: {
LastClickLocation: UIEventSource<{ lat: number, lon: number }>,
osmConnection: OsmConnection,
featurePipeline: FeaturePipeline
},
filterViewIsOpened: UIEventSource<boolean>,
preset: PresetInfo,
confirm: (tags: any[], location: { lat: number, lon: number }, snapOntoWayId: string) => void,
cancel: () => void): BaseUIElement {
let location = state.LastClickLocation;
let preciseInput: LocationInput = undefined
if (preset.preciseInput !== undefined) {
// We uncouple the event source
const locationSrc = new UIEventSource({
lat: location.data.lat,
lon: location.data.lon,
zoom: 19
});
let backgroundLayer = undefined;
if (preset.preciseInput.preferredBackground) {
backgroundLayer = AvailableBaseLayers.SelectBestLayerAccordingTo(locationSrc, new UIEventSource<string | string[]>(preset.preciseInput.preferredBackground))
}
let snapToFeatures: UIEventSource<{ feature: any }[]> = undefined
let mapBounds: UIEventSource<BBox> = undefined
if (preset.preciseInput.snapToLayers) {
snapToFeatures = new UIEventSource<{ feature: any }[]>([])
mapBounds = new UIEventSource<BBox>(undefined)
}
const tags = TagUtils.KVtoProperties(preset.tags ?? []);
preciseInput = new LocationInput({
mapBackground: backgroundLayer,
centerLocation: locationSrc,
snapTo: snapToFeatures,
snappedPointTags: tags,
maxSnapDistance: preset.preciseInput.maxSnapDistance,
bounds: mapBounds
})
preciseInput.installBounds(0.15, true)
preciseInput.SetClass("h-32 rounded-xl overflow-hidden border border-gray").SetStyle("height: 12rem;")
if (preset.preciseInput.snapToLayers) {
// We have to snap to certain layers.
// Lets fetch them
let loadedBbox: BBox = undefined
mapBounds?.addCallbackAndRunD(bbox => {
if (loadedBbox !== undefined && bbox.isContainedIn(loadedBbox)) {
// All is already there
// return;
}
bbox = bbox.pad(2);
loadedBbox = bbox;
const allFeatures: { feature: any }[] = []
preset.preciseInput.snapToLayers.forEach(layerId => {
state.featurePipeline.GetFeaturesWithin(layerId, bbox).forEach(feats => allFeatures.push(...feats.map(f => ({feature: f}))))
})
snapToFeatures.setData(allFeatures)
})
}
}
let confirmButton: BaseUIElement = new SubtleButton(preset.icon(),
new Combine([
Translations.t.general.add.addNew.Subs({category: preset.name}),
Translations.t.general.add.warnVisibleForEveryone.Clone().SetClass("alert")
]).SetClass("flex flex-col")
).SetClass("font-bold break-words")
.onClick(() => {
confirm(preset.tags, (preciseInput?.GetValue() ?? location).data, preciseInput?.snappedOnto?.data?.properties?.id);
});
if (preciseInput !== undefined) {
confirmButton = new Combine([preciseInput, confirmButton])
}
const openLayerControl =
new SubtleButton(
Svg.layers_ui(),
new Combine([
Translations.t.general.add.layerNotEnabled
.Subs({layer: preset.layerToAddTo.layerDef.name})
.SetClass("alert"),
Translations.t.general.add.openLayerControl
])
)
.onClick(() => filterViewIsOpened.setData(true))
const openLayerOrConfirm = new Toggle(
confirmButton,
openLayerControl,
preset.layerToAddTo.isDisplayed
)
const disableFilter = new SubtleButton(
new Combine([
Svg.filter_ui().SetClass("absolute w-full"),
Svg.cross_bottom_right_svg().SetClass("absolute red-svg")
]).SetClass("relative"),
new Combine(
[
Translations.t.general.add.disableFiltersExplanation.Clone(),
Translations.t.general.add.disableFilters.Clone().SetClass("text-xl")
]
).SetClass("flex flex-col")
).onClick(() => {
preset.layerToAddTo.appliedFilters.setData([])
cancel()
})
const disableFiltersOrConfirm = new Toggle(
openLayerOrConfirm,
disableFilter,
preset.layerToAddTo.appliedFilters.map(filters => {
if (filters === undefined || filters.length === 0) {
return true;
}
for (const filter of filters) {
if (filter.selected === 0 && filter.filter.options.length === 1) {
return false;
}
if (filter.selected !== undefined) {
const tags = filter.filter.options[filter.selected].osmTags
if (tags !== undefined && tags["and"]?.length !== 0) {
// This actually doesn't filter anything at all
return false;
}
}
}
return true
})
)
const tagInfo = SimpleAddUI.CreateTagInfoFor(preset, state.osmConnection);
const cancelButton = new SubtleButton(Svg.close_ui(),
Translations.t.general.cancel
).onClick(cancel)
return new Combine([
state.osmConnection.userDetails.data.dryRun ?
Translations.t.general.testing.Clone().SetClass("alert") : undefined,
disableFiltersOrConfirm,
cancelButton,
preset.description,
tagInfo
]).SetClass("flex flex-col")
}
private static CreateTagInfoFor(preset: PresetInfo, osmConnection: OsmConnection, optionallyLinkToWiki = true) {
public static CreateTagInfoFor(preset: PresetInfo, osmConnection: OsmConnection, optionallyLinkToWiki = true) {
const csCount = osmConnection.userDetails.data.csCount;
return new Toggle(
Translations.t.general.add.presetInfo.Subs({
@ -329,7 +172,7 @@ export default class SimpleAddUI extends Toggle {
private static CreatePresetSelectButton(preset: PresetInfo, osmConnection: OsmConnection) {
const tagInfo = SimpleAddUI.CreateTagInfoFor(preset, osmConnection ,false);
const tagInfo = SimpleAddUI.CreateTagInfoFor(preset, osmConnection, false);
return new SubtleButton(
preset.icon(),
new Combine([
@ -368,7 +211,7 @@ export default class SimpleAddUI extends Toggle {
for (const preset of presets) {
const tags = TagUtils.KVtoProperties(preset.tags ?? []);
let icon: () => BaseUIElement = () => layer.layerDef.GenerateLeafletStyle(new UIEventSource<any>(tags), false).icon.html
let icon: () => BaseUIElement = () => layer.layerDef.mapRendering[0].GenerateLeafletStyle(new UIEventSource<any>(tags), false).html
.SetClass("w-12 h-12 block relative");
const presetInfo: PresetInfo = {
tags: preset.tags,

View file

@ -6,9 +6,6 @@ import FullWelcomePaneWithTabs from "./BigComponents/FullWelcomePaneWithTabs";
import MapControlButton from "./MapControlButton";
import Svg from "../Svg";
import Toggle from "./Input/Toggle";
import Hash from "../Logic/Web/Hash";
import {QueryParameters} from "../Logic/Web/QueryParameters";
import Constants from "../Models/Constants";
import UserBadge from "./BigComponents/UserBadge";
import SearchAndGo from "./BigComponents/SearchAndGo";
import Link from "./Base/Link";
@ -24,76 +21,7 @@ import Translations from "./i18n/Translations";
import SimpleAddUI from "./BigComponents/SimpleAddUI";
import StrayClickHandler from "../Logic/Actors/StrayClickHandler";
import Lazy from "./Base/Lazy";
export class DefaultGuiState {
public readonly welcomeMessageIsOpened : UIEventSource<boolean>;
public readonly downloadControlIsOpened: UIEventSource<boolean>;
public readonly filterViewIsOpened: UIEventSource<boolean>;
public readonly copyrightViewIsOpened: UIEventSource<boolean>;
public readonly welcomeMessageOpenedTab: UIEventSource<number>
public readonly allFullScreenStates: UIEventSource<boolean>[] = []
constructor() {
this.welcomeMessageOpenedTab = UIEventSource.asFloat(QueryParameters.GetQueryParameter(
"tab",
"0",
`The tab that is shown in the welcome-message. 0 = the explanation of the theme,1 = OSM-credits, 2 = sharescreen, 3 = more themes, 4 = about mapcomplete (user must be logged in and have >${Constants.userJourney.mapCompleteHelpUnlock} changesets)`
));
this.welcomeMessageIsOpened = QueryParameters.GetBooleanQueryParameter(
"welcome-control-toggle",
"false",
"Whether or not the welcome panel is shown"
)
this.downloadControlIsOpened = QueryParameters.GetBooleanQueryParameter(
"download-control-toggle",
"false",
"Whether or not the download panel is shown"
)
this.filterViewIsOpened = QueryParameters.GetBooleanQueryParameter(
"filter-toggle",
"false",
"Whether or not the filter view is shown"
)
this.copyrightViewIsOpened = QueryParameters.GetBooleanQueryParameter(
"copyright-toggle",
"false",
"Whether or not the copyright view is shown"
)
if(Hash.hash.data === "download"){
this.downloadControlIsOpened.setData(true)
}
if(Hash.hash.data === "filter"){
this.filterViewIsOpened.setData(true)
}
if(Hash.hash.data === "copyright"){
this.copyrightViewIsOpened.setData(true)
}
if(Hash.hash.data === "" || Hash.hash.data === undefined || Hash.hash.data === "welcome"){
this.welcomeMessageIsOpened.setData(true)
}
this.allFullScreenStates.push(this.downloadControlIsOpened, this.filterViewIsOpened, this.copyrightViewIsOpened, this.welcomeMessageIsOpened)
for (let i = 0; i < this.allFullScreenStates.length; i++){
const fullScreenState = this.allFullScreenStates[i];
for (let j = 0; j < this.allFullScreenStates.length; j++){
if(i == j){
continue
}
const otherState = this.allFullScreenStates[j];
fullScreenState.addCallbackAndRunD(isOpened => {
if(isOpened){
otherState.setData(false)
}
})
}
}
}
}
import {DefaultGuiState} from "./DefaultGuiState";
/**
@ -114,10 +42,8 @@ export default class DefaultGUI {
Utils.LoadCustomCss(state.layoutToUse.customCss);
}
this.SetupUIElements();
this.SetupMap()
}

74
UI/DefaultGuiState.ts Normal file
View file

@ -0,0 +1,74 @@
import {UIEventSource} from "../Logic/UIEventSource";
import {QueryParameters} from "../Logic/Web/QueryParameters";
import Constants from "../Models/Constants";
import Hash from "../Logic/Web/Hash";
export class DefaultGuiState {
public readonly welcomeMessageIsOpened: UIEventSource<boolean>;
public readonly downloadControlIsOpened: UIEventSource<boolean>;
public readonly filterViewIsOpened: UIEventSource<boolean>;
public readonly copyrightViewIsOpened: UIEventSource<boolean>;
public readonly welcomeMessageOpenedTab: UIEventSource<number>
public readonly allFullScreenStates: UIEventSource<boolean>[] = []
static state: DefaultGuiState;
constructor() {
this.welcomeMessageOpenedTab = UIEventSource.asFloat(QueryParameters.GetQueryParameter(
"tab",
"0",
`The tab that is shown in the welcome-message. 0 = the explanation of the theme,1 = OSM-credits, 2 = sharescreen, 3 = more themes, 4 = about mapcomplete (user must be logged in and have >${Constants.userJourney.mapCompleteHelpUnlock} changesets)`
));
this.welcomeMessageIsOpened = QueryParameters.GetBooleanQueryParameter(
"welcome-control-toggle",
"false",
"Whether or not the welcome panel is shown"
)
this.downloadControlIsOpened = QueryParameters.GetBooleanQueryParameter(
"download-control-toggle",
"false",
"Whether or not the download panel is shown"
)
this.filterViewIsOpened = QueryParameters.GetBooleanQueryParameter(
"filter-toggle",
"false",
"Whether or not the filter view is shown"
)
this.copyrightViewIsOpened = QueryParameters.GetBooleanQueryParameter(
"copyright-toggle",
"false",
"Whether or not the copyright view is shown"
)
if (Hash.hash.data === "download") {
this.downloadControlIsOpened.setData(true)
}
if (Hash.hash.data === "filters") {
this.filterViewIsOpened.setData(true)
}
if (Hash.hash.data === "copyright") {
this.copyrightViewIsOpened.setData(true)
}
if (Hash.hash.data === "" || Hash.hash.data === undefined || Hash.hash.data === "welcome") {
this.welcomeMessageIsOpened.setData(true)
}
this.allFullScreenStates.push(this.downloadControlIsOpened, this.filterViewIsOpened, this.copyrightViewIsOpened, this.welcomeMessageIsOpened)
for (let i = 0; i < this.allFullScreenStates.length; i++) {
const fullScreenState = this.allFullScreenStates[i];
for (let j = 0; j < this.allFullScreenStates.length; j++) {
if (i == j) {
continue
}
const otherState = this.allFullScreenStates[j];
fullScreenState.addCallbackAndRunD(isOpened => {
if (isOpened) {
otherState.setData(false)
}
})
}
}
}
}

View file

@ -1,9 +1,6 @@
import jsPDF from "jspdf";
import {SimpleMapScreenshoter} from "leaflet-simple-map-screenshoter";
import {UIEventSource} from "../Logic/UIEventSource";
import Minimap from "./Base/Minimap";
import Minimap, {MinimapObj} from "./Base/Minimap";
import Loc from "../Models/Loc";
import BaseLayer from "../Models/BaseLayer";
import {FixedUiElement} from "./Base/FixedUiElement";
@ -14,7 +11,6 @@ import LayoutConfig from "../Models/ThemeConfig/LayoutConfig";
import FeaturePipeline from "../Logic/FeatureSource/FeaturePipeline";
import ShowDataLayer from "./ShowDataLayer/ShowDataLayer";
import {BBox} from "../Logic/BBox";
import ShowOverlayLayer from "./ShowDataLayer/ShowOverlayLayer";
/**
* Creates screenshoter to take png screenshot
* Creates jspdf and downloads it
@ -63,14 +59,12 @@ export default class ExportPDF {
location: new UIEventSource<Loc>(loc), // We remove the link between the old and the new UI-event source as moving the map while the export is running fucks up the screenshot
background: options.background,
allowMoving: false,
onFullyLoaded: leaflet => window.setTimeout(() => {
onFullyLoaded: _ => window.setTimeout(() => {
if (self._screenhotTaken) {
return;
}
try {
self.CreatePdf(leaflet)
self.CreatePdf(minimap)
.then(() => self.cleanup())
.catch(() => self.cleanup())
} catch (e) {
@ -112,20 +106,17 @@ export default class ExportPDF {
this._screenhotTaken = true;
}
private async CreatePdf(leaflet: L.Map) {
private async CreatePdf(minimap: MinimapObj) {
console.log("PDF creation started")
const t = Translations.t.general.pdf;
const layout = this._layout
const screenshotter = new SimpleMapScreenshoter();
//minimap op index.html -> hidden daar alles op doen en dan weg
//minimap - leaflet map ophalen - boundaries ophalen - State.state.featurePipeline
screenshotter.addTo(leaflet);
let doc = new jsPDF('landscape');
const image = (await screenshotter.takeScreen('image'))
const image = await minimap.TakeScreenshot()
// @ts-ignore
doc.addImage(image, 'PNG', 0, 0, this.mapW, this.mapH);

View file

@ -25,8 +25,8 @@ export default class InputElementMap<T, X> extends InputElement<X> {
const self = this;
this._value = inputElement.GetValue().map(
(t => {
const currentX = self.GetValue()?.data;
const newX = toX(t);
const currentX = self.GetValue()?.data;
if (isSame(currentX, newX)) {
return currentX;
}

View file

@ -45,6 +45,7 @@ export default class LengthInput extends InputElement<string> {
background: this.background,
allowMoving: false,
location: this._location,
attribution:true,
leafletOptions: {
tap: true
}

View file

@ -6,7 +6,6 @@ import BaseLayer from "../../Models/BaseLayer";
import Combine from "../Base/Combine";
import Svg from "../../Svg";
import State from "../../State";
import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers";
import {GeoOperations} from "../../Logic/GeoOperations";
import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer";
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource";
@ -16,7 +15,6 @@ import {FixedUiElement} from "../Base/FixedUiElement";
import ShowDataLayer from "../ShowDataLayer/ShowDataLayer";
import BaseUIElement from "../BaseUIElement";
import Toggle from "./Toggle";
import {start} from "repl";
export default class LocationInput extends InputElement<Loc> implements MinimapObj {
@ -25,12 +23,17 @@ export default class LocationInput extends InputElement<Loc> implements MinimapO
id: "matchpoint", source: {
osmTags: {and: []}
},
icon: "./assets/svg/crosshair-empty.svg"
mapRendering: [{
location: ["point"],
icon: "./assets/svg/crosshair-empty.svg"
}]
}, "matchpoint icon", true
)
IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
public readonly snappedOnto: UIEventSource<any> = new UIEventSource<any>(undefined)
public readonly _matching_layer: LayerConfig;
public readonly leafletMap: UIEventSource<any>
private _centerLocation: UIEventSource<Loc>;
private readonly mapBackground: UIEventSource<BaseLayer>;
/**
@ -43,10 +46,7 @@ export default class LocationInput extends InputElement<Loc> implements MinimapO
private readonly _maxSnapDistance: number
private readonly _snappedPointTags: any;
private readonly _bounds: UIEventSource<BBox>;
public readonly _matching_layer: LayerConfig;
private readonly map: BaseUIElement & MinimapObj;
public readonly leafletMap: UIEventSource<any>
private readonly clickLocation: UIEventSource<Loc>;
private readonly _minZoom: number;
@ -83,7 +83,7 @@ export default class LocationInput extends InputElement<Loc> implements MinimapO
}
this._matching_layer = matchingLayer;
} else {
this._matching_layer = LocationInput.matchLayer
this._matching_layer = LocationInput.matchLayer
}
this._snappedPoint = options.centerLocation.map(loc => {
@ -96,6 +96,8 @@ export default class LocationInput extends InputElement<Loc> implements MinimapO
let min = undefined;
let matchedWay = undefined;
for (const feature of self._snapTo.data ?? []) {
try{
const nearestPointOnLine = GeoOperations.nearestPoint(feature.feature, [loc.lon, loc.lat])
if (min === undefined) {
min = nearestPointOnLine
@ -108,6 +110,9 @@ export default class LocationInput extends InputElement<Loc> implements MinimapO
matchedWay = feature.feature;
}
}catch(e){
console.log("Snapping to a nearest point failed for ", feature.feature,"due to ", e)
}
}
if (min === undefined || min.properties.dist * 1000 > self._maxSnapDistance) {
@ -158,25 +163,32 @@ export default class LocationInput extends InputElement<Loc> implements MinimapO
IsValid(t: Loc): boolean {
return t !== undefined;
}
installBounds(factor: number | BBox, showRange?: boolean): void {
this.map.installBounds(factor, showRange)
}
TakeScreenshot(): Promise<any> {
return this.map.TakeScreenshot()
}
protected InnerConstructElement(): HTMLElement {
try {
const self = this;
const hasMoved = new UIEventSource(false)
const startLocation = { ...this._centerLocation.data }
this._centerLocation. addCallbackD(newLocation => {
const startLocation = {...this._centerLocation.data}
this._centerLocation.addCallbackD(newLocation => {
const f = 100000
console.log(newLocation.lon, startLocation.lon)
const diff = (Math.abs(newLocation.lon * f - startLocation.lon* f ) + Math.abs(newLocation.lat* f - startLocation.lat* f ))
if(diff < 1){
const diff = (Math.abs(newLocation.lon * f - startLocation.lon * f) + Math.abs(newLocation.lat * f - startLocation.lat * f))
if (diff < 1) {
return;
}
hasMoved.setData(true)
return true;
})
this.clickLocation.addCallbackAndRunD(location => this._centerLocation.setData(location))
if (this._snapTo !== undefined) {
if (this._snapTo !== undefined) {
// Show the lines to snap to
new ShowDataMultiLayer({
features: new StaticFeatureSource(this._snapTo, true),
@ -184,7 +196,7 @@ export default class LocationInput extends InputElement<Loc> implements MinimapO
zoomToFeatures: false,
leafletMap: this.map.leafletMap,
layers: State.state.filteredLayers,
allElements: State.state.allElements
allElements: State.state.allElements
}
)
// Show the central point
@ -194,16 +206,16 @@ export default class LocationInput extends InputElement<Loc> implements MinimapO
}
return [{feature: loc}];
})
new ShowDataLayer({
features: new StaticFeatureSource(matchPoint, true),
enablePopups: false,
zoomToFeatures: false,
leafletMap: this.map.leafletMap,
layerToShow: this._matching_layer,
allElements: State.state.allElements,
selectedElement: State.state.selectedElement
})
new ShowDataLayer({
features: new StaticFeatureSource(matchPoint, true),
enablePopups: false,
zoomToFeatures: false,
leafletMap: this.map.leafletMap,
layerToShow: this._matching_layer,
allElements: State.state.allElements,
selectedElement: State.state.selectedElement
})
}
this.mapBackground.map(layer => {
const leaflet = this.map.leafletMap.data
@ -216,19 +228,19 @@ export default class LocationInput extends InputElement<Loc> implements MinimapO
leaflet.setZoom(layer.max_zoom - 1)
}, [this.map.leafletMap])
const animatedHand = Svg.hand_ui()
.SetStyle("width: 2rem; height: unset;")
.SetClass("hand-drag-animation block pointer-events-none")
return new Combine([
new Combine([
Svg.move_arrows_ui()
.SetClass("block relative pointer-events-none")
.SetStyle("left: -2.5rem; top: -2.5rem; width: 5rem; height: 5rem")
]).SetClass("block w-0 h-0 z-10 relative")
]).SetClass("block w-0 h-0 z-10 relative")
.SetStyle("background: rgba(255, 128, 128, 0.21); left: 50%; top: 50%; opacity: 0.5"),
new Toggle(undefined,
animatedHand, hasMoved)
.SetClass("block w-0 h-0 z-10 relative")
@ -244,9 +256,4 @@ export default class LocationInput extends InputElement<Loc> implements MinimapO
}
}
installBounds(factor: number | BBox, showRange?: boolean): void {
this.map.installBounds(factor, showRange)
}
}

View file

@ -19,6 +19,9 @@ import {FixedInputElement} from "./FixedInputElement";
import WikidataSearchBox from "../Wikipedia/WikidataSearchBox";
import Wikidata from "../../Logic/Web/Wikidata";
import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers";
import Table from "../Base/Table";
import Combine from "../Base/Combine";
import Title from "../Base/Title";
interface TextFieldDef {
name: string,
@ -28,12 +31,163 @@ interface TextFieldDef {
inputHelper?: (value: UIEventSource<string>, options?: {
location: [number, number],
mapBackgroundLayer?: UIEventSource<any>,
args: (string | number | boolean)[]
args: (string | number | boolean | any)[]
feature?: any
}) => InputElement<string>,
inputmode?: string
}
class WikidataTextField implements TextFieldDef {
name = "wikidata"
explanation =
new Combine([
"A wikidata identifier, e.g. Q42.",
new Title("Helper arguments"),
new Table(["name", "doc"],
[
["key", "the value of this tag will initialize search (default: name)"],
["options", new Combine(["A JSON-object of type `{ removePrefixes: string[], removePostfixes: string[] }`.",
new Table(
["subarg", "doc"],
[["removePrefixes", "remove these snippets of text from the start of the passed string to search"],
["removePostfixes", "remove these snippets of text from the end of the passed string to search"],
]
)])
]]),
new Title("Example usage"),
`The following is the 'freeform'-part of a layer config which will trigger a search for the wikidata item corresponding with the name of the selected feature. It will also remove '-street', '-square', ... if found at the end of the name
\`\`\`
"freeform": {
"key": "name:etymology:wikidata",
"type": "wikidata",
"helperArgs": [
"name",
{
"removePostfixes": [
"street",
"boulevard",
"path",
"square",
"plaza",
]
}
]
}
\`\`\``
]).AsMarkdown()
public isValid(str) {
if (str === undefined) {
return false;
}
if (str.length <= 2) {
return false;
}
return !str.split(";").some(str => Wikidata.ExtractKey(str) === undefined)
}
public reformat(str) {
if (str === undefined) {
return undefined;
}
let out = str.split(";").map(str => Wikidata.ExtractKey(str)).join("; ")
if (str.endsWith(";")) {
out = out + ";"
}
return out;
}
public inputHelper(currentValue, inputHelperOptions) {
const args = inputHelperOptions.args ?? []
const searchKey = args[0] ?? "name"
let searchFor = <string>inputHelperOptions.feature?.properties[searchKey]?.toLowerCase()
const options = args[1]
if (searchFor !== undefined && options !== undefined) {
const prefixes = <string[]>options["removePrefixes"]
const postfixes = <string[]>options["removePostfixes"]
for (const postfix of postfixes ?? []) {
if (searchFor.endsWith(postfix)) {
searchFor = searchFor.substring(0, searchFor.length - postfix.length)
break;
}
}
for (const prefix of prefixes ?? []) {
if (searchFor.startsWith(prefix)) {
searchFor = searchFor.substring(prefix.length)
break;
}
}
}
return new WikidataSearchBox({
value: currentValue,
searchText: new UIEventSource<string>(searchFor)
})
}
}
class OpeningHoursTextField implements TextFieldDef {
name = "opening_hours"
explanation =
new Combine([
"Has extra elements to easily input when a POI is opened.",
new Title("Helper arguments"),
new Table(["name", "doc"],
[
["options", new Combine([
"A JSON-object of type `{ prefix: string, postfix: string }`. ",
new Table(["subarg", "doc"],
[
["prefix", "Piece of text that will always be added to the front of the generated opening hours. If the OSM-data does not start with this, it will fail to parse"],
["postfix", "Piece of text that will always be added to the end of the generated opening hours"],
])
])
]
]),
new Title("Example usage"),
"To add a conditional (based on time) access restriction:\n\n```\n" + `
"freeform": {
"key": "access:conditional",
"type": "opening_hours",
"helperArgs": [
{
"prefix":"no @ (",
"postfix":")"
}
]
}` + "\n```\n\n*Don't forget to pass the prefix and postfix in the rendering as well*: `{opening_hours_table(opening_hours,yes @ &LPARENS, &RPARENS )`"]).AsMarkdown()
isValid() {
return true
}
reformat(str) {
return str
}
inputHelper(value: UIEventSource<string>, inputHelperOptions: {
location: [number, number],
mapBackgroundLayer?: UIEventSource<any>,
args: (string | number | boolean | any)[]
feature?: any
}) {
const args = (inputHelperOptions.args ?? [])[0]
const prefix = <string>args?.prefix ?? ""
const postfix = <string>args?.postfix ?? ""
return new OpeningHoursInput(value, prefix, postfix)
}
}
export default class ValidatedTextField {
public static tpList: TextFieldDef[] = [
@ -117,7 +271,8 @@ export default class ValidatedTextField {
if (args[0]) {
zoom = Number(args[0])
if (isNaN(zoom)) {
throw "Invalid zoom level for argument at 'length'-input"
console.error("Invalid zoom level for argument at 'length'-input. The offending argument is: ",args[0]," (using 19 instead)")
zoom = 19
}
}
@ -146,60 +301,7 @@ export default class ValidatedTextField {
},
"decimal"
),
ValidatedTextField.tp(
"wikidata",
"A wikidata identifier, e.g. Q42. Input helper arguments: [ key: the value of this tag will initialize search (default: name), options: { removePrefixes: string[], removePostfixes: string[] } these prefixes and postfixes will be removed from the initial search value]",
(str) => {
if (str === undefined) {
return false;
}
if(str.length <= 2){
return false;
}
return !str.split(";").some(str => Wikidata.ExtractKey(str) === undefined)
},
(str) => {
if (str === undefined) {
return undefined;
}
let out = str.split(";").map(str => Wikidata.ExtractKey(str)).join("; ")
if(str.endsWith(";")){
out = out + ";"
}
return out;
},
(currentValue, inputHelperOptions) => {
const args = inputHelperOptions.args ?? []
const searchKey = args[0] ?? "name"
let searchFor = <string>inputHelperOptions.feature?.properties[searchKey]?.toLowerCase()
const options = args[1]
if (searchFor !== undefined && options !== undefined) {
const prefixes = <string[]>options["removePrefixes"]
const postfixes = <string[]>options["removePostfixes"]
for (const postfix of postfixes ?? []) {
if (searchFor.endsWith(postfix)) {
searchFor = searchFor.substring(0, searchFor.length - postfix.length)
break;
}
}
for (const prefix of prefixes ?? []) {
if (searchFor.startsWith(prefix)) {
searchFor = searchFor.substring(prefix.length)
break;
}
}
}
return new WikidataSearchBox({
value: currentValue,
searchText: new UIEventSource<string>(searchFor)
})
}
),
new WikidataTextField(),
ValidatedTextField.tp(
"int",
@ -299,15 +401,7 @@ export default class ValidatedTextField {
undefined,
"tel"
),
ValidatedTextField.tp(
"opening_hours",
"Has extra elements to easily input when a POI is opened",
() => true,
str => str,
(value) => {
return new OpeningHoursInput(value);
}
),
new OpeningHoursTextField(),
ValidatedTextField.tp(
"color",
"Shows a color picker",
@ -458,7 +552,11 @@ export default class ValidatedTextField {
public static HelpText(): string {
const explanations = ValidatedTextField.tpList.map(type => ["## " + type.name, "", type.explanation].join("\n")).join("\n\n")
return "# Available types for text fields\n\nThe listed types here trigger a special input element. Use them in `tagrendering.freeform.type` of your tagrendering to activate them\n\n" + explanations
return new Combine([
new Title("Available types for text fields", 1),
"The listed types here trigger a special input element. Use them in `tagrendering.freeform.type` of your tagrendering to activate them",
explanations
]).SetClass("flex flex-col").AsMarkdown()
}
private static tp(name: string,

View file

@ -0,0 +1,184 @@
import {UIEventSource} from "../../Logic/UIEventSource";
import {OsmConnection} from "../../Logic/Osm/OsmConnection";
import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline";
import BaseUIElement from "../BaseUIElement";
import LocationInput from "../Input/LocationInput";
import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers";
import {BBox} from "../../Logic/BBox";
import {TagUtils} from "../../Logic/Tags/TagUtils";
import {SubtleButton} from "../Base/SubtleButton";
import Combine from "../Base/Combine";
import Translations from "../i18n/Translations";
import Svg from "../../Svg";
import Toggle from "../Input/Toggle";
import SimpleAddUI, {PresetInfo} from "../BigComponents/SimpleAddUI";
export default class ConfirmLocationOfPoint extends Combine {
constructor(
state: {
osmConnection: OsmConnection,
featurePipeline: FeaturePipeline
},
filterViewIsOpened: UIEventSource<boolean>,
preset: PresetInfo,
confirmText: BaseUIElement,
loc: { lon: number, lat: number },
confirm: (tags: any[], location: { lat: number, lon: number }, snapOntoWayId: string) => void,
cancel: () => void,
) {
let preciseInput: LocationInput = undefined
if (preset.preciseInput !== undefined) {
// We uncouple the event source
const zloc = {...loc, zoom: 19}
const locationSrc = new UIEventSource(zloc);
let backgroundLayer = undefined;
if (preset.preciseInput.preferredBackground) {
backgroundLayer = AvailableBaseLayers.SelectBestLayerAccordingTo(locationSrc, new UIEventSource<string | string[]>(preset.preciseInput.preferredBackground))
}
let snapToFeatures: UIEventSource<{ feature: any }[]> = undefined
let mapBounds: UIEventSource<BBox> = undefined
if (preset.preciseInput.snapToLayers && preset.preciseInput.snapToLayers.length > 0) {
snapToFeatures = new UIEventSource<{ feature: any }[]>([])
mapBounds = new UIEventSource<BBox>(undefined)
}
const tags = TagUtils.KVtoProperties(preset.tags ?? []);
preciseInput = new LocationInput({
mapBackground: backgroundLayer,
centerLocation: locationSrc,
snapTo: snapToFeatures,
snappedPointTags: tags,
maxSnapDistance: preset.preciseInput.maxSnapDistance,
bounds: mapBounds
})
preciseInput.installBounds(0.15, true)
preciseInput.SetClass("h-32 rounded-xl overflow-hidden border border-gray").SetStyle("height: 12rem;")
if (preset.preciseInput.snapToLayers && preset.preciseInput.snapToLayers.length > 0) {
// We have to snap to certain layers.
// Lets fetch them
let loadedBbox: BBox = undefined
mapBounds?.addCallbackAndRunD(bbox => {
if (loadedBbox !== undefined && bbox.isContainedIn(loadedBbox)) {
// All is already there
// return;
}
bbox = bbox.pad(2);
loadedBbox = bbox;
const allFeatures: { feature: any }[] = []
preset.preciseInput.snapToLayers.forEach(layerId => {
console.log("Snapping to", layerId)
state.featurePipeline.GetFeaturesWithin(layerId, bbox)?.forEach(feats => allFeatures.push(...feats.map(f => ({feature: f}))))
})
console.log("Snapping to", allFeatures)
snapToFeatures.setData(allFeatures)
})
}
}
let confirmButton: BaseUIElement = new SubtleButton(preset.icon(),
new Combine([
confirmText,
Translations.t.general.add.warnVisibleForEveryone.Clone().SetClass("alert")
]).SetClass("flex flex-col")
).SetClass("font-bold break-words")
.onClick(() => {
confirm(preset.tags, (preciseInput?.GetValue()?.data ?? loc), preciseInput?.snappedOnto?.data?.properties?.id);
});
if (preciseInput !== undefined) {
confirmButton = new Combine([preciseInput, confirmButton])
}
const openLayerControl =
new SubtleButton(
Svg.layers_ui(),
new Combine([
Translations.t.general.add.layerNotEnabled
.Subs({layer: preset.layerToAddTo.layerDef.name})
.SetClass("alert"),
Translations.t.general.add.openLayerControl
])
)
.onClick(() => filterViewIsOpened.setData(true))
const openLayerOrConfirm = new Toggle(
confirmButton,
openLayerControl,
preset.layerToAddTo.isDisplayed
)
const disableFilter = new SubtleButton(
new Combine([
Svg.filter_ui().SetClass("absolute w-full"),
Svg.cross_bottom_right_svg().SetClass("absolute red-svg")
]).SetClass("relative"),
new Combine(
[
Translations.t.general.add.disableFiltersExplanation.Clone(),
Translations.t.general.add.disableFilters.Clone().SetClass("text-xl")
]
).SetClass("flex flex-col")
).onClick(() => {
preset.layerToAddTo.appliedFilters.setData([])
cancel()
})
const disableFiltersOrConfirm = new Toggle(
openLayerOrConfirm,
disableFilter,
preset.layerToAddTo.appliedFilters.map(filters => {
if (filters === undefined || filters.length === 0) {
return true;
}
for (const filter of filters) {
if (filter.selected === 0 && filter.filter.options.length === 1) {
return false;
}
if (filter.selected !== undefined) {
const tags = filter.filter.options[filter.selected].osmTags
if (tags !== undefined && tags["and"]?.length !== 0) {
// This actually doesn't filter anything at all
return false;
}
}
}
return true
})
)
const tagInfo = SimpleAddUI.CreateTagInfoFor(preset, state.osmConnection);
const cancelButton = new SubtleButton(Svg.close_ui(),
Translations.t.general.cancel
).onClick(cancel)
super([
state.osmConnection.userDetails.data.dryRun ?
Translations.t.general.testing.Clone().SetClass("alert") : undefined,
disableFiltersOrConfirm,
cancelButton,
preset.description,
tagInfo
])
this.SetClass("flex flex-col")
}
}

View file

@ -23,11 +23,39 @@ export default class OpeningHoursInput extends InputElement<string> {
private readonly _value: UIEventSource<string>;
private readonly _element: BaseUIElement;
constructor(value: UIEventSource<string> = new UIEventSource<string>("")) {
constructor(value: UIEventSource<string> = new UIEventSource<string>(""), prefix = "", postfix = "") {
super();
this._value = value;
let valueWithoutPrefix = value
if (prefix !== "" && postfix !== "") {
valueWithoutPrefix = value.map(str => {
if (str === undefined) {
return undefined;
}
if(str === ""){
return ""
}
if (str.startsWith(prefix) && str.endsWith(postfix)) {
return str.substring(prefix.length, str.length - postfix.length)
}
return str
}, [], noPrefix => {
if (noPrefix === undefined) {
return undefined;
}
if(noPrefix === ""){
return ""
}
if (noPrefix.startsWith(prefix) && noPrefix.endsWith(postfix)) {
return noPrefix
}
const leftoverRules = value.map<string[]>(str => {
return prefix + noPrefix + postfix
})
}
const leftoverRules = valueWithoutPrefix.map<string[]>(str => {
if (str === undefined) {
return []
}
@ -45,9 +73,9 @@ export default class OpeningHoursInput extends InputElement<string> {
return leftOvers;
})
// Note: MUST be bound AFTER the leftover rules!
const rulesFromOhPicker = value.map(OH.Parse);
const rulesFromOhPicker = valueWithoutPrefix.map(OH.Parse);
const ph = value.map<string>(str => {
const ph = valueWithoutPrefix.map<string>(str => {
if (str === undefined) {
return ""
}
@ -68,7 +96,7 @@ export default class OpeningHoursInput extends InputElement<string> {
...leftoverRules.data,
ph.data
]
value.setData(Utils.NoEmpty(rules).join(";"));
valueWithoutPrefix.setData(Utils.NoEmpty(rules).join(";"));
}
rulesFromOhPicker.addCallback(update);

View file

@ -23,11 +23,23 @@ export default class OpeningHoursVisualization extends Toggle {
Translations.t.general.weekdays.abbreviations.sunday,
]
constructor(tags: UIEventSource<any>, key: string) {
constructor(tags: UIEventSource<any>, key: string, prefix = "", postfix = "") {
const tagsDirect = tags.data;
const ohTable = new VariableUiElement(tags
.map(tags => tags[key]) // This mapping will absorb all other changes to tags in order to prevent regeneration
.map(tags => {
const value: string = tags[key];
if (value === undefined) {
return undefined
}
if (value.startsWith(prefix) && value.endsWith(postfix)) {
return value.substring(prefix.length, value.length - postfix.length).trim()
}
return value;
}) // This mapping will absorb all other changes to tags in order to prevent regeneration
.map(ohtext => {
if (ohtext === undefined) {
return new FixedUiElement("No opening hours defined with key " + key).SetClass("alert")
}
try {
// noinspection JSPotentiallyInvalidConstructorUsage
const oh = new opening_hours(ohtext, {
@ -35,12 +47,12 @@ export default class OpeningHoursVisualization extends Toggle {
lon: tagsDirect._lon,
address: {
country_code: tagsDirect._country
}
}, {tag_key: key});
},
}, {tag_key: "opening_hours"});
return OpeningHoursVisualization.CreateFullVisualisation(oh)
} catch (e) {
console.log(e);
console.warn(e, e.stack);
return new Combine([Translations.t.general.opening_hours.error_loading,
new Toggle(
new FixedUiElement(e).SetClass("subtle"),

View file

@ -264,7 +264,7 @@ export default class DeleteWizard extends Toggle {
]
}, undefined, "Delete wizard"
}, "Delete wizard"
)
}

View file

@ -16,7 +16,10 @@ export default class EditableTagRendering extends Toggle {
constructor(tags: UIEventSource<any>,
configuration: TagRenderingConfig,
units: Unit [],
editMode = new UIEventSource<boolean>(false)
options:{
editMode? : UIEventSource<boolean> ,
innerElementClasses?: string
}
) {
// The tagrendering is hidden if:
@ -27,7 +30,12 @@ export default class EditableTagRendering extends Toggle {
(configuration?.condition?.matchesProperties(tags) ?? true))
super(
new Lazy(() => EditableTagRendering.CreateRendering(tags, configuration, units, editMode)),
new Lazy(() => {
const editMode = options.editMode ?? new UIEventSource<boolean>(false)
const rendering = EditableTagRendering.CreateRendering(tags, configuration, units, editMode);
rendering.SetClass(options.innerElementClasses)
return rendering
}),
undefined,
renderingIsShown
)
@ -71,7 +79,6 @@ export default class EditableTagRendering extends Toggle {
editMode
)
}
rendering.SetClass("block w-full break-word text-default m-1 p-1 border-b border-gray-200 mb-2 pb-2")
return rendering;
}

View file

@ -5,7 +5,6 @@ import Combine from "../Base/Combine";
import TagRenderingAnswer from "./TagRenderingAnswer";
import State from "../../State";
import ScrollableFullScreen from "../Base/ScrollableFullScreen";
import {Tag} from "../../Logic/Tags/Tag";
import Constants from "../../Models/Constants";
import SharedTagRenderings from "../../Customizations/SharedTagRenderings";
import BaseUIElement from "../BaseUIElement";
@ -37,7 +36,7 @@ export default class FeatureInfoBox extends ScrollableFullScreen {
private static GenerateTitleBar(tags: UIEventSource<any>,
layerConfig: LayerConfig): BaseUIElement {
const title = new TagRenderingAnswer(tags, layerConfig.title ?? new TagRenderingConfig("POI", undefined))
const title = new TagRenderingAnswer(tags, layerConfig.title ?? new TagRenderingConfig("POI"))
.SetClass("break-words font-bold sm:p-0.5 md:p-1 sm:p-1.5 md:p-2");
const titleIcons = new Combine(
layerConfig.titleIcons.map(icon => new TagRenderingAnswer(tags, icon,
@ -52,26 +51,57 @@ export default class FeatureInfoBox extends ScrollableFullScreen {
private static GenerateContent(tags: UIEventSource<any>,
layerConfig: LayerConfig): BaseUIElement {
let questionBox: BaseUIElement = undefined;
let questionBoxes: Map<string, BaseUIElement> = new Map<string, BaseUIElement>();
const allGroupNames = Utils.Dedup(layerConfig.tagRenderings.map(tr => tr.group))
if (State.state.featureSwitchUserbadge.data) {
questionBox = new QuestionBox(tags, layerConfig.tagRenderings, layerConfig.units);
for (const groupName of allGroupNames) {
const questions = layerConfig.tagRenderings.filter(tr => tr.group === groupName)
const questionBox = new QuestionBox(tags, questions, layerConfig.units);
questionBoxes.set(groupName, questionBox)
}
}
let questionBoxIsUsed = false;
const renderings: BaseUIElement[] = layerConfig.tagRenderings.map(tr => {
if (tr.question === null) {
// This is the question box!
questionBoxIsUsed = true;
return questionBox;
const allRenderings = []
for (let i = 0; i < allGroupNames.length; i++) {
const groupName = allGroupNames[i];
const trs = layerConfig.tagRenderings.filter(tr => tr.group === groupName)
const renderingsForGroup: (EditableTagRendering | BaseUIElement)[] = []
const innerClasses = "block w-full break-word text-default m-1 p-1 border-b border-gray-200 mb-2 pb-2";
for (const tr of trs) {
if (tr.question === null || tr.id === "questions") {
// This is a question box!
const questionBox = questionBoxes.get(tr.group)
questionBoxes.delete(tr.group)
renderingsForGroup.push(questionBox)
} else {
let classes = innerClasses
let isHeader = renderingsForGroup.length === 0 && i > 0
if(isHeader){
// This is the first element of a group!
// It should act as header and be sticky
classes= ""
}
const etr = new EditableTagRendering(tags, tr, layerConfig.units,{
innerElementClasses: innerClasses
})
if(isHeader){
etr.SetClass("sticky top-0")
}
renderingsForGroup.push(etr)
}
}
return new EditableTagRendering(tags, tr, layerConfig.units);
});
allRenderings.push(...renderingsForGroup)
}
let editElements: BaseUIElement[] = []
if (!questionBoxIsUsed) {
questionBoxes.forEach(questionBox => {
editElements.push(questionBox);
}
})
if (layerConfig.allowMove) {
editElements.push(
@ -107,7 +137,7 @@ export default class FeatureInfoBox extends ScrollableFullScreen {
const hasMinimap = layerConfig.tagRenderings.some(tr => FeatureInfoBox.hasMinimap(tr))
if (!hasMinimap) {
renderings.push(new TagRenderingAnswer(tags, SharedTagRenderings.SharedTagRendering.get("minimap")))
allRenderings.push(new TagRenderingAnswer(tags, SharedTagRenderings.SharedTagRendering.get("minimap")))
}
editElements.push(
@ -132,7 +162,7 @@ export default class FeatureInfoBox extends ScrollableFullScreen {
new VariableUiElement(
State.state.featureSwitchIsDebugging.map(isDebugging => {
if (isDebugging) {
const config: TagRenderingConfig = new TagRenderingConfig({render: "{all_tags()}"}, new Tag("id", ""), "");
const config: TagRenderingConfig = new TagRenderingConfig({render: "{all_tags()}"}, "");
return new TagRenderingAnswer(tags, config, "all_tags")
}
})
@ -147,10 +177,9 @@ export default class FeatureInfoBox extends ScrollableFullScreen {
return new Combine(editElements).SetClass("flex flex-col")
}
))
renderings.push(editors)
allRenderings.push(editors)
return new Combine(renderings).SetClass("block")
return new Combine(allRenderings).SetClass("block")
}
/**

View file

@ -16,6 +16,7 @@ import Lazy from "../Base/Lazy";
export default class QuestionBox extends VariableUiElement {
constructor(tagsSource: UIEventSource<any>, tagRenderings: TagRenderingConfig[], units: Unit[]) {
const skippedQuestions: UIEventSource<number[]> = new UIEventSource<number[]>([])
tagRenderings = tagRenderings
@ -33,7 +34,7 @@ export default class QuestionBox extends VariableUiElement {
{
units: units,
afterSave: () => {
// We save
// We save and indicate progress by pinging and recalculating
skippedQuestions.ping();
},
cancelButton: Translations.t.general.skip.Clone()
@ -45,7 +46,7 @@ export default class QuestionBox extends VariableUiElement {
}
)));
const skippedQuestionsButton = Translations.t.general.skippedQuestions.Clone()
const skippedQuestionsButton = Translations.t.general.skippedQuestions
.onClick(() => {
skippedQuestions.setData([]);
})

View file

@ -21,8 +21,13 @@ export default class SplitRoadWizard extends Toggle {
private static splitLayerStyling = new LayerConfig({
id: "splitpositions",
source: {osmTags: "_cutposition=yes"},
icon: {render: "circle:white;./assets/svg/scissors.svg"},
iconSize: {render: "30,30,center"},
mapRendering: [
{
location: ["point","centroid"],
icon: {render: "circle:white;./assets/svg/scissors.svg"},
iconSize: {render: "30,30,center"}
}
],
}, "(BUILTIN) SplitRoadWizard.ts", true)
public dialogIsOpened: UIEventSource<boolean>
@ -61,7 +66,7 @@ export default class SplitRoadWizard extends Toggle {
miniMap.installBounds(BBox.get(roadElement).pad(0.25), false)
// Define how a cut is displayed on the map
// Datalayer displaying the road and the cut points (if any)
new ShowDataLayer({
features: new StaticFeatureSource(splitPoints, true),

Some files were not shown because too many files have changed in this diff Show more