forked from MapComplete/MapComplete
Further improvements to entrances theme, add layer-crossdependency detection, add layers which another layer depends on automatically to the theme, add documentation on which layers depends on which other layers, regenerate documentation
This commit is contained in:
parent
8e40d76281
commit
0ee23ce36d
27 changed files with 9032 additions and 331 deletions
|
@ -13,6 +13,7 @@ export default class AllKnownLayers {
|
|||
return true
|
||||
})()
|
||||
|
||||
public static runningGenerateScript = false;
|
||||
|
||||
// Must be below the list...
|
||||
public static sharedLayers: Map<string, LayerConfig> = AllKnownLayers.getSharedLayers();
|
||||
|
@ -44,7 +45,7 @@ export default class AllKnownLayers {
|
|||
const [layerId, id] = renderingId.split(".")
|
||||
const layer = AllKnownLayers.getSharedLayersJson().get(layerId)
|
||||
if (layer === undefined) {
|
||||
if (Utils.runningFromConsole) {
|
||||
if (AllKnownLayers.runningGenerateScript) {
|
||||
// Probably generating the layer overview
|
||||
return <TagRenderingConfigJson[]>[{
|
||||
id: "dummy"
|
||||
|
|
|
@ -6,6 +6,7 @@ import BaseUIElement from "../UI/BaseUIElement";
|
|||
import Combine from "../UI/Base/Combine";
|
||||
import Title from "../UI/Base/Title";
|
||||
import List from "../UI/Base/List";
|
||||
import DependencyCalculator from "../Models/ThemeConfig/DependencyCalculator";
|
||||
|
||||
export class AllKnownLayouts {
|
||||
|
||||
|
@ -69,6 +70,21 @@ export class AllKnownLayouts {
|
|||
|
||||
const unpopularLayers = allLayers.filter(layer => themesPerLayer.get(layer.id)?.length < 2)
|
||||
|
||||
// Determine the cross-dependencies
|
||||
const layerIsNeededBy : Map<string, string[]> = new Map<string, string[]>()
|
||||
|
||||
for (const layer of allLayers) {
|
||||
for (const dep of DependencyCalculator.getLayerDependencies(layer)) {
|
||||
const dependency = dep.neededLayer
|
||||
if(!layerIsNeededBy.has(dependency)){
|
||||
layerIsNeededBy.set(dependency, [])
|
||||
}
|
||||
layerIsNeededBy.get(dependency).push(layer.id)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
return new Combine([
|
||||
new Title("Special and other useful layers", 1),
|
||||
"MapComplete has a few data layers available in the theme which have special properties through builtin-hooks. Furthermore, there are some normal layers (which are built from normal Theme-config files) but are so general that they get a mention here.",
|
||||
|
@ -76,15 +92,15 @@ export class AllKnownLayouts {
|
|||
new List(AllKnownLayers.priviliged_layers.map(id => "[" + id + "](#" + id + ")")),
|
||||
...AllKnownLayers.priviliged_layers
|
||||
.map(id => AllKnownLayers.sharedLayers.get(id))
|
||||
.map((l) => l.GenerateDocumentation(themesPerLayer.get(l.id), AllKnownLayers.added_by_default.indexOf(l.id) >= 0, AllKnownLayers.no_include.indexOf(l.id) < 0)),
|
||||
.map((l) => l.GenerateDocumentation(themesPerLayer.get(l.id), layerIsNeededBy, DependencyCalculator.getLayerDependencies(l),AllKnownLayers.added_by_default.indexOf(l.id) >= 0, AllKnownLayers.no_include.indexOf(l.id) < 0)),
|
||||
new Title("Normal layers", 1),
|
||||
"The following layers are included in MapComplete",
|
||||
new Title("Frequently reused layers", 2),
|
||||
"The following layers are used by at least " + popularLayerCutoff + " mapcomplete themes and might be interesting for your custom theme too",
|
||||
new List(popuparLayers.map(layer => "[" + layer.id + "](#" + layer.id + ")")),
|
||||
...popuparLayers.map((layer) => layer.GenerateDocumentation(themesPerLayer.get(layer.id))),
|
||||
...popuparLayers.map((layer) => layer.GenerateDocumentation(themesPerLayer.get(layer.id),layerIsNeededBy,DependencyCalculator.getLayerDependencies(layer))),
|
||||
new List(unpopularLayers.map(layer => "[" + layer.id + "](#" + layer.id + ")")),
|
||||
...unpopularLayers.map(layer => layer.GenerateDocumentation(themesPerLayer.get(layer.id))
|
||||
...unpopularLayers.map(layer => layer.GenerateDocumentation(themesPerLayer.get(layer.id),layerIsNeededBy,DependencyCalculator.getLayerDependencies(layer))
|
||||
)
|
||||
])
|
||||
|
||||
|
|
|
@ -69,6 +69,8 @@
|
|||
* [Themes using this layer](#themes-using-this-layer)
|
||||
+ [direction](#direction)
|
||||
* [Themes using this layer](#themes-using-this-layer)
|
||||
+ [entrance](#entrance)
|
||||
* [Themes using this layer](#themes-using-this-layer)
|
||||
+ [etymology](#etymology)
|
||||
* [Themes using this layer](#themes-using-this-layer)
|
||||
+ [extinguisher](#extinguisher)
|
||||
|
@ -318,7 +320,8 @@ A facility where bicycles can be lent for longer period of times
|
|||
|
||||
|
||||
|
||||
|
||||
- This layer will automatically load [drinking_water](#drinking_water) into the layout as it depends on it: A calculated tag loads features from this layer (calculatedTag[0] which calculates the value for _closest_other_drinking_water)
|
||||
- This layer is needed as dependency for layer [drinking_water](#drinking_water)
|
||||
|
||||
|
||||
|
||||
|
@ -394,6 +397,9 @@ Special builtin layer providing all walls and buildings. This layer is useful in
|
|||
|
||||
- Not visible in the layer selection by default. If you want to make this layer toggable, override `name`
|
||||
- Not rendered on the map by default. If you want to rendering this on the map, override `mapRenderings`
|
||||
- This layer is needed as dependency for layer [defibrillator](#defibrillator)
|
||||
- This layer is needed as dependency for layer [entrance](#entrance)
|
||||
- This layer is needed as dependency for layer [surveillance_camera](#surveillance_camera)
|
||||
|
||||
|
||||
|
||||
|
@ -405,6 +411,7 @@ Special builtin layer providing all walls and buildings. This layer is useful in
|
|||
|
||||
|
||||
- [aed](https://mapcomplete.osm.be/aed)
|
||||
- [entrances](https://mapcomplete.osm.be/entrances)
|
||||
- [surveillance](https://mapcomplete.osm.be/surveillance)
|
||||
|
||||
|
||||
|
@ -451,6 +458,7 @@ Special builtin layer providing all walls and buildings. This layer is useful in
|
|||
- [cycleways_and_roads](#cycleways_and_roads)
|
||||
- [defibrillator](#defibrillator)
|
||||
- [direction](#direction)
|
||||
- [entrance](#entrance)
|
||||
- [etymology](#etymology)
|
||||
- [extinguisher](#extinguisher)
|
||||
- [fire_station](#fire_station)
|
||||
|
@ -545,7 +553,8 @@ Obstacles while cycling, such as bollards and cycle barriers
|
|||
|
||||
|
||||
|
||||
- This layer will automatically load [cycleways_and_roads](#cycleways_and_roads) into the layout as it depends on it.
|
||||
- This layer will automatically load [cycleways_and_roads](#cycleways_and_roads) into the layout as it depends on it: a preset snaps to this layer (presets[0])
|
||||
- This layer will automatically load [cycleways_and_roads](#cycleways_and_roads) into the layout as it depends on it: a preset snaps to this layer (presets[1])
|
||||
|
||||
|
||||
|
||||
|
@ -863,7 +872,8 @@ Crossings for pedestrians and cyclists
|
|||
|
||||
|
||||
|
||||
- This layer will automatically load [cycleways_and_roads](#cycleways_and_roads) into the layout as it depends on it.
|
||||
- This layer will automatically load [cycleways_and_roads](#cycleways_and_roads) into the layout as it depends on it: a preset snaps to this layer (presets[0])
|
||||
- This layer will automatically load [cycleways_and_roads](#cycleways_and_roads) into the layout as it depends on it: a preset snaps to this layer (presets[1])
|
||||
|
||||
|
||||
|
||||
|
@ -885,7 +895,10 @@ Crossings for pedestrians and cyclists
|
|||
|
||||
|
||||
|
||||
|
||||
- This layer is needed as dependency for layer [barrier](#barrier)
|
||||
- This layer is needed as dependency for layer [barrier](#barrier)
|
||||
- This layer is needed as dependency for layer [crossings](#crossings)
|
||||
- This layer is needed as dependency for layer [crossings](#crossings)
|
||||
|
||||
|
||||
|
||||
|
@ -907,7 +920,8 @@ Crossings for pedestrians and cyclists
|
|||
|
||||
|
||||
|
||||
- This layer will automatically load [walls_and_buildings](#walls_and_buildings) into the layout as it depends on it.
|
||||
- This layer will automatically load [walls_and_buildings](#walls_and_buildings) into the layout as it depends on it: a preset snaps to this layer (presets[1])
|
||||
- This layer is needed as dependency for layer [Brugge](#Brugge)
|
||||
|
||||
|
||||
|
||||
|
@ -945,6 +959,30 @@ This layer visualizes directions
|
|||
- [surveillance](https://mapcomplete.osm.be/surveillance)
|
||||
|
||||
|
||||
### entrance
|
||||
|
||||
|
||||
|
||||
A layer showing entrances and offering capabilities to survey some advanced data which is important for e.g. wheelchair users (but also bicycle users, people who want to deliver, ...)
|
||||
|
||||
[Go to the source code](../assets/layers/entrance/entrance.json)
|
||||
|
||||
|
||||
|
||||
- This layer will automatically load [walls_and_buildings](#walls_and_buildings) into the layout as it depends on it: a preset snaps to this layer (presets[0])
|
||||
|
||||
|
||||
|
||||
|
||||
#### Themes using this layer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
- [entrances](https://mapcomplete.osm.be/entrances)
|
||||
|
||||
|
||||
### etymology
|
||||
|
||||
|
||||
|
@ -1285,7 +1323,7 @@ A sport pitch
|
|||
|
||||
|
||||
|
||||
|
||||
- This layer is needed as dependency for layer [Assen](#Assen)
|
||||
|
||||
|
||||
|
||||
|
@ -1307,7 +1345,7 @@ A sport pitch
|
|||
|
||||
|
||||
|
||||
- This layer will automatically load [walls_and_buildings](#walls_and_buildings) into the layout as it depends on it.
|
||||
- This layer will automatically load [walls_and_buildings](#walls_and_buildings) into the layout as it depends on it: a preset snaps to this layer (presets[1])
|
||||
|
||||
|
||||
|
||||
|
@ -1493,7 +1531,7 @@ A climbing gym
|
|||
|
||||
|
||||
|
||||
|
||||
- This layer is needed as dependency for layer [climbing](#climbing)
|
||||
|
||||
|
||||
|
||||
|
@ -1517,7 +1555,7 @@ A climbing opportunity
|
|||
|
||||
|
||||
|
||||
|
||||
- This layer will automatically load [climbing_route](#climbing_route) into the layout as it depends on it: A calculated tag loads features from this layer (calculatedTag[0] which calculates the value for _contained_climbing_routes_properties)
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -239,10 +239,7 @@ For example to get all objects which overlap or embed from a layer, use `_contai
|
|||
|
||||
If a 'unique tag key' is given, the tag with this key will only appear once (e.g. if 'name' is given, all features will have a different name)
|
||||
|
||||
0. list of features or layer name or '*' to get all features
|
||||
1. amount of features
|
||||
2. unique tag key (optional)
|
||||
3. maxDistanceInMeters (optional)
|
||||
|
||||
|
||||
|
||||
### memberships
|
||||
|
|
|
@ -7,39 +7,39 @@
|
|||
|
||||
1. [Special tag renderings](#special-tag-renderings)
|
||||
+ [all_tags](#all_tags)
|
||||
* [Example usage](#example-usage)
|
||||
* [Example usage of all_tags](#example-usage-of-all_tags)
|
||||
+ [image_carousel](#image_carousel)
|
||||
* [Example usage](#example-usage)
|
||||
* [Example usage of image_carousel](#example-usage-of-image_carousel)
|
||||
+ [image_upload](#image_upload)
|
||||
* [Example usage](#example-usage)
|
||||
* [Example usage of image_upload](#example-usage-of-image_upload)
|
||||
+ [wikipedia](#wikipedia)
|
||||
* [Example usage](#example-usage)
|
||||
* [Example usage of wikipedia](#example-usage-of-wikipedia)
|
||||
+ [minimap](#minimap)
|
||||
* [Example usage](#example-usage)
|
||||
* [Example usage of minimap](#example-usage-of-minimap)
|
||||
+ [sided_minimap](#sided_minimap)
|
||||
* [Example usage](#example-usage)
|
||||
* [Example usage of sided_minimap](#example-usage-of-sided_minimap)
|
||||
+ [reviews](#reviews)
|
||||
* [Example usage](#example-usage)
|
||||
* [Example usage of reviews](#example-usage-of-reviews)
|
||||
+ [opening_hours_table](#opening_hours_table)
|
||||
* [Example usage](#example-usage)
|
||||
* [Example usage of opening_hours_table](#example-usage-of-opening_hours_table)
|
||||
+ [live](#live)
|
||||
* [Example usage](#example-usage)
|
||||
* [Example usage of live](#example-usage-of-live)
|
||||
+ [histogram](#histogram)
|
||||
* [Example usage](#example-usage)
|
||||
* [Example usage of histogram](#example-usage-of-histogram)
|
||||
+ [share_link](#share_link)
|
||||
* [Example usage](#example-usage)
|
||||
* [Example usage of share_link](#example-usage-of-share_link)
|
||||
+ [canonical](#canonical)
|
||||
* [Example usage](#example-usage)
|
||||
* [Example usage of canonical](#example-usage-of-canonical)
|
||||
+ [import_button](#import_button)
|
||||
* [Example usage](#example-usage)
|
||||
* [Example usage of import_button](#example-usage-of-import_button)
|
||||
+ [multi_apply](#multi_apply)
|
||||
* [Example usage](#example-usage)
|
||||
* [Example usage of multi_apply](#example-usage-of-multi_apply)
|
||||
+ [tag_apply](#tag_apply)
|
||||
* [Example usage](#example-usage)
|
||||
* [Example usage of tag_apply](#example-usage-of-tag_apply)
|
||||
+ [export_as_gpx](#export_as_gpx)
|
||||
* [Example usage](#example-usage)
|
||||
* [Example usage of export_as_gpx](#example-usage-of-export_as_gpx)
|
||||
+ [clear_location_history](#clear_location_history)
|
||||
* [Example usage](#example-usage)
|
||||
* [Example usage of clear_location_history](#example-usage-of-clear_location_history)
|
||||
|
||||
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
|
||||
|
||||
|
@ -47,7 +47,7 @@
|
|||
|
||||
Prints all key-value pairs of the object - used for debugging
|
||||
|
||||
#### Example usage
|
||||
#### Example usage of all_tags
|
||||
|
||||
`{all_tags()}`
|
||||
|
||||
|
@ -60,7 +60,7 @@ name | default | description
|
|||
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
|
||||
#### Example usage of image_carousel
|
||||
|
||||
`{image_carousel(image,mapillary,image,wikidata,wikimedia_commons,image,image)}`
|
||||
|
||||
|
@ -74,7 +74,7 @@ image-key | image | Image tag to add the URL to (or image-tag:0, image-tag:1 whe
|
|||
label | Add image | The text to show on the button
|
||||
|
||||
|
||||
#### Example usage
|
||||
#### Example usage of image_upload
|
||||
|
||||
`{image_upload(image,Add image)}`
|
||||
|
||||
|
@ -87,7 +87,7 @@ name | default | description
|
|||
keyToShowWikipediaFor | wikidata | Use the wikidata entry from this key to show the wikipedia article for
|
||||
|
||||
|
||||
#### Example usage
|
||||
#### Example usage of wikipedia
|
||||
|
||||
`{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
|
||||
|
||||
|
@ -101,7 +101,7 @@ zoomlevel | 18 | The (maximum) zoomlevel: the target zoomlevel after fitting the
|
|||
idKey | id | (Matches all resting arguments) This argument should be the key of a property of the feature. The corresponding value is interpreted as either the id or the a list of ID's. The features with these ID's will be shown on this minimap.
|
||||
|
||||
|
||||
#### Example usage
|
||||
#### Example usage of minimap
|
||||
|
||||
`{minimap()}`, `{minimap(17, id, _list_of_embedded_feature_ids_calculated_by_calculated_tag):height:10rem; border: 2px solid black}`
|
||||
|
||||
|
@ -114,7 +114,7 @@ name | default | description
|
|||
side | _undefined_ | The side to show, either `left` or `right`
|
||||
|
||||
|
||||
#### Example usage
|
||||
#### Example usage of sided_minimap
|
||||
|
||||
`{sided_minimap(left)}`
|
||||
|
||||
|
@ -128,7 +128,7 @@ subjectKey | name | The key to use to determine the subject. If specified, the s
|
|||
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
|
||||
#### Example usage of reviews
|
||||
|
||||
`{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
|
||||
|
||||
|
@ -143,7 +143,7 @@ prefix | _empty string_ | Remove this string from the start of the value before
|
|||
postfix | _empty string_ | Remove this string from the end of the value before parsing. __Note: use `&RPARENs` to indicate `)` if needed__
|
||||
|
||||
|
||||
#### Example usage
|
||||
#### Example usage of opening_hours_table
|
||||
|
||||
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)}`
|
||||
|
||||
|
@ -158,7 +158,7 @@ Shorthands | _undefined_ | A list of shorthands, of the format 'shorthandname:pa
|
|||
path | _undefined_ | The path (or shorthand) that should be returned
|
||||
|
||||
|
||||
#### Example usage
|
||||
#### Example usage of live
|
||||
|
||||
{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)}
|
||||
|
||||
|
@ -174,7 +174,7 @@ 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
|
||||
#### Example usage of histogram
|
||||
|
||||
`{histogram('some_key')}` with properties being `{some_key: ['a','b','a','c']} to create a histogram
|
||||
|
||||
|
@ -187,7 +187,7 @@ name | default | description
|
|||
url | _undefined_ | The url to share (default: current URL)
|
||||
|
||||
|
||||
#### Example usage
|
||||
#### Example usage of share_link
|
||||
|
||||
{share_link()} to share the current page, {share_link(<some_url>)} to share the given url
|
||||
|
||||
|
@ -200,7 +200,7 @@ name | default | description
|
|||
key | _undefined_ | The key of the tag to give the canonical text for
|
||||
|
||||
|
||||
#### Example usage
|
||||
#### Example usage of canonical
|
||||
|
||||
{canonical(length)} will give 42 metre (in french)
|
||||
|
||||
|
@ -264,7 +264,7 @@ Snap onto layer(s)/replace geometry with this other way | _undefined_ | - If th
|
|||
snap max distance | 5 | The maximum distance that this point will move to snap onto a layer (in meters)
|
||||
|
||||
|
||||
#### Example usage
|
||||
#### Example usage of import_button
|
||||
|
||||
`{import_button(,,Import this data into OpenStreetMap,./assets/svg/addSmall.svg,18,,5)}`
|
||||
|
||||
|
@ -281,7 +281,7 @@ autoapply | _undefined_ | A boolean indicating wether this tagging should be app
|
|||
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
|
||||
#### Example usage of multi_apply
|
||||
|
||||
{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)}
|
||||
|
||||
|
@ -310,7 +310,7 @@ 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
|
||||
#### Example usage of tag_apply
|
||||
|
||||
`{tag_apply(survey_date:=$_now:date, Surveyed today!)}`
|
||||
|
||||
|
@ -318,7 +318,7 @@ id_of_object_to_apply_this_one | _undefined_ | If specified, applies the the tag
|
|||
|
||||
Exports the selected feature as GPX-file
|
||||
|
||||
#### Example usage
|
||||
#### Example usage of export_as_gpx
|
||||
|
||||
`{export_as_gpx()}`
|
||||
|
||||
|
@ -326,7 +326,7 @@ id_of_object_to_apply_this_one | _undefined_ | If specified, applies the the tag
|
|||
|
||||
A button to remove the travelled track information from the device
|
||||
|
||||
#### Example usage
|
||||
#### Example usage of clear_location_history
|
||||
|
||||
`{clear_location_history()}`
|
||||
|
||||
|
|
203
Docs/TagInfo/mapcomplete_entrances.json
Normal file
203
Docs/TagInfo/mapcomplete_entrances.json
Normal file
|
@ -0,0 +1,203 @@
|
|||
{
|
||||
"data_format": 1,
|
||||
"project": {
|
||||
"name": "MapComplete Entrances",
|
||||
"description": "Survey entrances to help wheelchair routing",
|
||||
"project_url": "https://mapcomplete.osm.be/entrances",
|
||||
"doc_url": "https://github.com/pietervdvn/MapComplete/tree/master/assets/themes/",
|
||||
"icon_url": "https://mapcomplete.osm.be/assets/layers/entrance/door.svg",
|
||||
"contact_name": "Pieter Vander Vennet, MapComplete",
|
||||
"contact_email": "pietervdvn@posteo.net"
|
||||
},
|
||||
"tags": [
|
||||
{
|
||||
"key": "entrance",
|
||||
"description": "The MapComplete theme Entrances has a layer Entrance showing features with this tag"
|
||||
},
|
||||
{
|
||||
"key": "indoor",
|
||||
"description": "The MapComplete theme Entrances has a layer Entrance showing features with this tag",
|
||||
"value": "door"
|
||||
},
|
||||
{
|
||||
"key": "image",
|
||||
"description": "The layer 'Entrance 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 'Entrance 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 'Entrance 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 'Entrance 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": "entrance",
|
||||
"description": "Layer 'Entrance' shows entrance=yes with a fixed text, namely 'No specific entrance type is known' (in the MapComplete.osm.be theme 'Entrances')",
|
||||
"value": "yes"
|
||||
},
|
||||
{
|
||||
"key": "entrance",
|
||||
"description": "Layer 'Entrance' shows entrance=&indoor=door with a fixed text, namely 'This is an indoor door, separating a room or a corridor within a single building' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Entrances') Picking this answer will delete the key entrance.",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"key": "indoor",
|
||||
"description": "Layer 'Entrance' shows entrance=&indoor=door with a fixed text, namely 'This is an indoor door, separating a room or a corridor within a single building' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Entrances')",
|
||||
"value": "door"
|
||||
},
|
||||
{
|
||||
"key": "indoor",
|
||||
"description": "Layer 'Entrance' shows indoor=&entrance=main with a fixed text, namely 'This is the main entrance' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Entrances') Picking this answer will delete the key indoor.",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"key": "entrance",
|
||||
"description": "Layer 'Entrance' shows indoor=&entrance=main with a fixed text, namely 'This is the main entrance' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Entrances')",
|
||||
"value": "main"
|
||||
},
|
||||
{
|
||||
"key": "indoor",
|
||||
"description": "Layer 'Entrance' shows indoor=&entrance=secondary with a fixed text, namely 'This is a secondary entrance' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Entrances') Picking this answer will delete the key indoor.",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"key": "entrance",
|
||||
"description": "Layer 'Entrance' shows indoor=&entrance=secondary with a fixed text, namely 'This is a secondary entrance' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Entrances')",
|
||||
"value": "secondary"
|
||||
},
|
||||
{
|
||||
"key": "indoor",
|
||||
"description": "Layer 'Entrance' shows indoor=&entrance=service with a fixed text, namely 'This is a service entrance - normally only used for employees, delivery, ...' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Entrances') Picking this answer will delete the key indoor.",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"key": "entrance",
|
||||
"description": "Layer 'Entrance' shows indoor=&entrance=service with a fixed text, namely 'This is a service entrance - normally only used for employees, delivery, ...' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Entrances')",
|
||||
"value": "service"
|
||||
},
|
||||
{
|
||||
"key": "indoor",
|
||||
"description": "Layer 'Entrance' shows indoor=&entrance=exit with a fixed text, namely 'This is an exit where one can not enter' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Entrances') Picking this answer will delete the key indoor.",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"key": "entrance",
|
||||
"description": "Layer 'Entrance' shows indoor=&entrance=exit with a fixed text, namely 'This is an exit where one can not enter' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Entrances')",
|
||||
"value": "exit"
|
||||
},
|
||||
{
|
||||
"key": "indoor",
|
||||
"description": "Layer 'Entrance' shows indoor=&entrance=entrance with a fixed text, namely 'This is an entrance where one can only enter (but not exit)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Entrances') Picking this answer will delete the key indoor.",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"key": "entrance",
|
||||
"description": "Layer 'Entrance' shows indoor=&entrance=entrance with a fixed text, namely 'This is an entrance where one can only enter (but not exit)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Entrances')",
|
||||
"value": "entrance"
|
||||
},
|
||||
{
|
||||
"key": "indoor",
|
||||
"description": "Layer 'Entrance' shows indoor=&entrance=emergency with a fixed text, namely 'This is emergency exit' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Entrances') Picking this answer will delete the key indoor.",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"key": "entrance",
|
||||
"description": "Layer 'Entrance' shows indoor=&entrance=emergency with a fixed text, namely 'This is emergency exit' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Entrances')",
|
||||
"value": "emergency"
|
||||
},
|
||||
{
|
||||
"key": "indoor",
|
||||
"description": "Layer 'Entrance' shows indoor=&entrance=home with a fixed text, namely 'This is the entrance to a private home' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Entrances') Picking this answer will delete the key indoor.",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"key": "entrance",
|
||||
"description": "Layer 'Entrance' shows indoor=&entrance=home with a fixed text, namely 'This is the entrance to a private home' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Entrances')",
|
||||
"value": "home"
|
||||
},
|
||||
{
|
||||
"key": "door",
|
||||
"description": "Layer 'Entrance' shows door=yes with a fixed text, namely 'The door type is not known' (in the MapComplete.osm.be theme 'Entrances')",
|
||||
"value": "yes"
|
||||
},
|
||||
{
|
||||
"key": "door",
|
||||
"description": "Layer 'Entrance' shows door=hinged with a fixed text, namely 'A classical, hinged door supported by joints' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Entrances')",
|
||||
"value": "hinged"
|
||||
},
|
||||
{
|
||||
"key": "door",
|
||||
"description": "Layer 'Entrance' shows door=revolving with a fixed text, namely 'A revolving door which hangs on a central shaft, rotating within a cylindrical enclosure' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Entrances')",
|
||||
"value": "revolving"
|
||||
},
|
||||
{
|
||||
"key": "door",
|
||||
"description": "Layer 'Entrance' shows door=overhead with a fixed text, namely 'A door which rolls from overhead, typically seen for garages' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Entrances')",
|
||||
"value": "overhead"
|
||||
},
|
||||
{
|
||||
"key": "door",
|
||||
"description": "Layer 'Entrance' shows door=no with a fixed text, namely 'This is an entrance without a physical door' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Entrances')",
|
||||
"value": "no"
|
||||
},
|
||||
{
|
||||
"key": "automatic_door",
|
||||
"description": "Layer 'Entrance' shows automatic_door=yes with a fixed text, namely 'This is an automatic door' (in the MapComplete.osm.be theme 'Entrances')",
|
||||
"value": "yes"
|
||||
},
|
||||
{
|
||||
"key": "automatic_door",
|
||||
"description": "Layer 'Entrance' shows automatic_door=no with a fixed text, namely 'This door is <b>not</b> automated' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Entrances')",
|
||||
"value": "no"
|
||||
},
|
||||
{
|
||||
"key": "automatic_door",
|
||||
"description": "Layer 'Entrance' shows automatic_door=motion with a fixed text, namely 'This door will open automatically when <b>motion</b> is detected' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Entrances')",
|
||||
"value": "motion"
|
||||
},
|
||||
{
|
||||
"key": "automatic_door",
|
||||
"description": "Layer 'Entrance' shows automatic_door=floor with a fixed text, namely 'This door will open automatically when a <b>sensor in the floor</b> is triggered' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Entrances')",
|
||||
"value": "floor"
|
||||
},
|
||||
{
|
||||
"key": "automatic_door",
|
||||
"description": "Layer 'Entrance' shows automatic_door=button with a fixed text, namely 'This door will open automatically when a <b>button is pressed</b>' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Entrances')",
|
||||
"value": "button"
|
||||
},
|
||||
{
|
||||
"key": "automatic_door",
|
||||
"description": "Layer 'Entrance' shows automatic_door=slowdown_button with a fixed text, namely 'This door revolves automatically all the time, but has a <b>button to slow it down</b>, e.g. for wheelchair users' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Entrances')",
|
||||
"value": "slowdown_button"
|
||||
},
|
||||
{
|
||||
"key": "automatic_door",
|
||||
"description": "Layer 'Entrance' shows automatic_door=continuous with a fixed text, namely 'This door revolves automatically all the time' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Entrances')",
|
||||
"value": "continuous"
|
||||
},
|
||||
{
|
||||
"key": "automatic_door",
|
||||
"description": "Layer 'Entrance' shows automatic_door=serviced_on_button_press with a fixed text, namely 'This door will be opened by staff when requested by <b>pressing a button</b' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Entrances')",
|
||||
"value": "serviced_on_button_press"
|
||||
},
|
||||
{
|
||||
"key": "automatic_door",
|
||||
"description": "Layer 'Entrance' shows automatic_door=serviced_on_request with a fixed text, namely 'This door will be opened by staff when requested' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Entrances')",
|
||||
"value": "serviced_on_request"
|
||||
},
|
||||
{
|
||||
"key": "width",
|
||||
"description": "Layer 'Entrance' shows and asks freeform values for key 'width' (in the MapComplete.osm.be theme 'Entrances')"
|
||||
},
|
||||
{
|
||||
"key": "id",
|
||||
"description": "The MapComplete theme Entrances has a layer Your track showing features with this tag",
|
||||
"value": "location_track"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
import {GeoOperations} from "./GeoOperations";
|
||||
import Combine from "../UI/Base/Combine";
|
||||
import RelationsTracker from "./Osm/RelationsTracker";
|
||||
import State from "../State";
|
||||
import BaseUIElement from "../UI/BaseUIElement";
|
||||
import List from "../UI/Base/List";
|
||||
import Title from "../UI/Base/Title";
|
||||
|
@ -17,226 +16,123 @@ export interface ExtraFuncParams {
|
|||
memberships: RelationsTracker
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes a function that is added to a geojson object in order to calculate calculated tags
|
||||
*/
|
||||
interface ExtraFunction {
|
||||
readonly _name: string;
|
||||
readonly _args: string[];
|
||||
readonly _doc: string;
|
||||
readonly _f: (params: ExtraFuncParams, feat: any) => any;
|
||||
|
||||
export class ExtraFunction {
|
||||
}
|
||||
|
||||
|
||||
static readonly intro = new Combine([
|
||||
new Title("Calculating tags with Javascript", 2),
|
||||
"In some cases, it is useful to have some tags calculated based on other properties. Some useful tags are available by default (e.g. `lat`, `lon`, `_country`), as detailed above.",
|
||||
"It is also possible to calculate your own tags - but this requires some javascript knowledge.",
|
||||
"",
|
||||
"Before proceeding, some warnings:",
|
||||
new List([
|
||||
"DO NOT DO THIS AS BEGINNER",
|
||||
"**Only do this if all other techniques fail** This should _not_ be done to create a rendering effect, only to calculate a specific value",
|
||||
"**THIS MIGHT BE DISABLED WITHOUT ANY NOTICE ON UNOFFICIAL THEMES** As unofficial themes might be loaded from the internet, this is the equivalent of injecting arbitrary code into the client. It'll be disabled if abuse occurs."
|
||||
]),
|
||||
"To enable this feature, add a field `calculatedTags` in the layer object, e.g.:",
|
||||
"````",
|
||||
"\"calculatedTags\": [",
|
||||
" \"_someKey=javascript-expression\",",
|
||||
" \"name=feat.properties.name ?? feat.properties.ref ?? feat.properties.operator\",",
|
||||
" \"_distanceCloserThen3Km=feat.distanceTo( some_lon, some_lat) < 3 ? 'yes' : 'no'\" ",
|
||||
" ]",
|
||||
"````",
|
||||
"",
|
||||
"The above code will be executed for every feature in the layer. The feature is accessible as `feat` and is an amended geojson object:",
|
||||
|
||||
new List([
|
||||
"`area` contains the surface area (in square meters) of the object",
|
||||
"`lat` and `lon` contain the latitude and longitude"
|
||||
]),
|
||||
"Some advanced functions are available on **feat** as well:"
|
||||
]).SetClass("flex-col").AsMarkdown();
|
||||
class OverlapFunc implements 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. 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: { feat: any, overlap: number }[] = []
|
||||
_name = "overlapWith";
|
||||
_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)"]
|
||||
|
||||
const bbox = BBox.get(feat)
|
||||
_f(params, feat) {
|
||||
return (...layerIds: string[]) => {
|
||||
const result: { feat: any, overlap: number }[] = []
|
||||
|
||||
for (const layerId of layerIds) {
|
||||
const otherLayers = params.getFeaturesWithin(layerId, bbox)
|
||||
if (otherLayers === undefined) {
|
||||
continue;
|
||||
}
|
||||
if (otherLayers.length === 0) {
|
||||
continue;
|
||||
}
|
||||
for (const otherLayer of otherLayers) {
|
||||
result.push(...GeoOperations.calculateOverlap(feat, otherLayer));
|
||||
}
|
||||
const bbox = BBox.get(feat)
|
||||
|
||||
for (const layerId of layerIds) {
|
||||
const otherLayers = params.getFeaturesWithin(layerId, bbox)
|
||||
if (otherLayers === undefined) {
|
||||
continue;
|
||||
}
|
||||
if (otherLayers.length === 0) {
|
||||
continue;
|
||||
}
|
||||
for (const otherLayer of otherLayers) {
|
||||
result.push(...GeoOperations.calculateOverlap(feat, otherLayer));
|
||||
}
|
||||
|
||||
result.sort((a, b) => b.overlap - a.overlap)
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
result.sort((a, b) => b.overlap - a.overlap)
|
||||
|
||||
return result;
|
||||
}
|
||||
)
|
||||
private static readonly DistanceToFunc = new ExtraFunction(
|
||||
{
|
||||
name: "distanceTo",
|
||||
doc: "Calculates the distance between the feature and a specified point in meter. The input should either be a pair of coordinates, a geojson feature or the ID of an object",
|
||||
args: ["feature OR featureID OR longitude", "undefined OR latitude"]
|
||||
},
|
||||
(featuresPerLayer, feature) => {
|
||||
return (arg0, lat) => {
|
||||
if (arg0 === undefined) {
|
||||
}
|
||||
}
|
||||
|
||||
class DistanceToFunc implements ExtraFunction {
|
||||
|
||||
_name = "distanceTo";
|
||||
_doc = "Calculates the distance between the feature and a specified point in meter. The input should either be a pair of coordinates, a geojson feature or the ID of an object";
|
||||
_args = ["feature OR featureID OR longitude", "undefined OR latitude"]
|
||||
|
||||
_f(featuresPerLayer, feature) {
|
||||
return (arg0, lat) => {
|
||||
if (arg0 === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
if (typeof arg0 === "number") {
|
||||
// Feature._lon and ._lat is conveniently place by one of the other metatags
|
||||
return GeoOperations.distanceBetween([arg0, lat], [feature._lon, feature._lat]);
|
||||
}
|
||||
if (typeof arg0 === "string") {
|
||||
// This is an identifier
|
||||
// TODO FIXME
|
||||
const feature = undefined // State.state.allElements.ContainingFeatures.get(arg0);
|
||||
if (feature === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
if (typeof arg0 === "number") {
|
||||
// Feature._lon and ._lat is conveniently place by one of the other metatags
|
||||
return GeoOperations.distanceBetween([arg0, lat], [feature._lon, feature._lat]);
|
||||
}
|
||||
if (typeof arg0 === "string") {
|
||||
// This is an identifier
|
||||
const feature = State.state.allElements.ContainingFeatures.get(arg0);
|
||||
if (feature === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
arg0 = feature;
|
||||
}
|
||||
|
||||
// arg0 is probably a feature
|
||||
return GeoOperations.distanceBetween(GeoOperations.centerpointCoordinates(arg0), [feature._lon, feature._lat])
|
||||
|
||||
}
|
||||
}
|
||||
)
|
||||
private static readonly ClosestObjectFunc = new ExtraFunction(
|
||||
{
|
||||
name: "closest",
|
||||
doc: "Given either a list of geojson features or a single layer name, gives the single object which is nearest to the feature. In the case of ways/polygons, only the centerpoint is considered. Returns a single geojson feature or undefined if nothing is found (or not yet laoded)",
|
||||
args: ["list of features or a layer name or '*' to get all features"]
|
||||
},
|
||||
(params, feature) => {
|
||||
return (features) => ExtraFunction.GetClosestNFeatures(params, feature, features)?.[0]?.feat
|
||||
}
|
||||
)
|
||||
|
||||
private static readonly ClosestNObjectFunc = new ExtraFunction(
|
||||
{
|
||||
name: "closestn",
|
||||
doc: "Given either a list of geojson features or a single layer name, gives the n closest objects which are nearest to the feature (excluding the feature itself). In the case of ways/polygons, only the centerpoint is considered. " +
|
||||
"Returns a list of `{feat: geojson, distance:number}` the empty list if nothing is found (or not yet loaded)\n\n" +
|
||||
"If a 'unique tag key' is given, the tag with this key will only appear once (e.g. if 'name' is given, all features will have a different name)",
|
||||
args: ["list of features or layer name or '*' to get all features", "amount of features", "unique tag key (optional)", "maxDistanceInMeters (optional)"]
|
||||
},
|
||||
(params, feature) => {
|
||||
|
||||
return (features, amount, uniqueTag, maxDistanceInMeters) => {
|
||||
let distance: number = Number(maxDistanceInMeters)
|
||||
if (isNaN(distance)) {
|
||||
distance = undefined
|
||||
}
|
||||
return ExtraFunction.GetClosestNFeatures(params, feature, features, {
|
||||
maxFeatures: Number(amount),
|
||||
uniqueTag: uniqueTag,
|
||||
maxDistance: distance
|
||||
});
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
private static readonly Memberships = new ExtraFunction(
|
||||
{
|
||||
name: "memberships",
|
||||
doc: "Gives a list of `{role: string, relation: Relation}`-objects, containing all the relations that this feature is part of. " +
|
||||
"\n\n" +
|
||||
"For example: `_part_of_walking_routes=feat.memberships().map(r => r.relation.tags.name).join(';')`",
|
||||
args: []
|
||||
},
|
||||
(params, feat) => {
|
||||
return () =>
|
||||
params.memberships.knownRelations.data.get(feat.properties.id) ?? []
|
||||
|
||||
}
|
||||
)
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
arg0 = feature;
|
||||
}
|
||||
|
||||
// arg0 is probably a feature
|
||||
return GeoOperations.distanceBetween(GeoOperations.centerpointCoordinates(arg0), [feature._lon, feature._lat])
|
||||
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly allFuncs: ExtraFunction[] = [
|
||||
ExtraFunction.DistanceToFunc,
|
||||
ExtraFunction.OverlapFunc,
|
||||
ExtraFunction.ClosestObjectFunc,
|
||||
ExtraFunction.ClosestNObjectFunc,
|
||||
ExtraFunction.Memberships,
|
||||
ExtraFunction.GetParsed
|
||||
];
|
||||
private readonly _name: string;
|
||||
private readonly _args: string[];
|
||||
private readonly _doc: string;
|
||||
private readonly _f: (params: ExtraFuncParams, feat: any) => any;
|
||||
|
||||
constructor(options: { name: string, doc: string, args: string[] },
|
||||
f: ((params: ExtraFuncParams, feat: any) => any)) {
|
||||
this._name = options.name;
|
||||
this._doc = options.doc;
|
||||
this._args = options.args;
|
||||
this._f = f;
|
||||
class ClosestObjectFunc implements ExtraFunction {
|
||||
_name = "closest"
|
||||
_doc = "Given either a list of geojson features or a single layer name, gives the single object which is nearest to the feature. In the case of ways/polygons, only the centerpoint is considered. Returns a single geojson feature or undefined if nothing is found (or not yet laoded)"
|
||||
|
||||
_args = ["list of features or a layer name or '*' to get all features"]
|
||||
|
||||
_f(params, feature) {
|
||||
return (features) => ClosestNObjectFunc.GetClosestNFeatures(params, feature, features)?.[0]?.feat
|
||||
}
|
||||
|
||||
public static FullPatchFeature(params: ExtraFuncParams, feature) {
|
||||
for (const func of ExtraFunction.allFuncs) {
|
||||
func.PatchFeature(params, feature);
|
||||
}
|
||||
|
||||
|
||||
class ClosestNObjectFunc implements ExtraFunction {
|
||||
_f(params, feature) {
|
||||
|
||||
return (features, amount, uniqueTag, maxDistanceInMeters) => {
|
||||
let distance: number = Number(maxDistanceInMeters)
|
||||
if (isNaN(distance)) {
|
||||
distance = undefined
|
||||
}
|
||||
return ClosestNObjectFunc.GetClosestNFeatures(params, feature, features, {
|
||||
maxFeatures: Number(amount),
|
||||
uniqueTag: uniqueTag,
|
||||
maxDistance: distance
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public static HelpText(): BaseUIElement {
|
||||
|
||||
const elems = []
|
||||
for (const func of ExtraFunction.allFuncs) {
|
||||
elems.push(new Title(func._name, 3),
|
||||
func._doc,
|
||||
new List(func._args, true))
|
||||
}
|
||||
|
||||
return new Combine([
|
||||
ExtraFunction.intro,
|
||||
new List(ExtraFunction.allFuncs.map(func => `[${func._name}](#${func._name})`)),
|
||||
...elems
|
||||
]);
|
||||
}
|
||||
_name = "closestn"
|
||||
_doc = "Given either a list of geojson features or a single layer name, gives the n closest objects which are nearest to the feature (excluding the feature itself). In the case of ways/polygons, only the centerpoint is considered. " +
|
||||
"Returns a list of `{feat: geojson, distance:number}` the empty list if nothing is found (or not yet loaded)\n\n" +
|
||||
"If a 'unique tag key' is given, the tag with this key will only appear once (e.g. if 'name' is given, all features will have a different name)"
|
||||
_args: ["list of features or layer name or '*' to get all features", "amount of features", "unique tag key (optional)", "maxDistanceInMeters (optional)"]
|
||||
|
||||
/**
|
||||
* Gets the closes N features, sorted by ascending distance.
|
||||
|
@ -248,10 +144,10 @@ export class ExtraFunction {
|
|||
* @constructor
|
||||
* @private
|
||||
*/
|
||||
private static GetClosestNFeatures(params: ExtraFuncParams,
|
||||
feature: any,
|
||||
features: string | any[],
|
||||
options?: { maxFeatures?: number, uniqueTag?: string | undefined, maxDistance?: number }): { feat: any, distance: number }[] {
|
||||
static GetClosestNFeatures(params: ExtraFuncParams,
|
||||
feature: any,
|
||||
features: string | any[],
|
||||
options?: { maxFeatures?: number, uniqueTag?: string | undefined, maxDistance?: number }): { feat: any, distance: number }[] {
|
||||
const maxFeatures = options?.maxFeatures ?? 1
|
||||
const maxDistance = options?.maxDistance ?? 500
|
||||
const uniqueTag: string | undefined = options?.uniqueTag
|
||||
|
@ -366,7 +262,115 @@ export class ExtraFunction {
|
|||
return closestFeatures;
|
||||
}
|
||||
|
||||
public PatchFeature(params: ExtraFuncParams, feature: any) {
|
||||
feature[this._name] = this._f(params, feature)
|
||||
}
|
||||
|
||||
|
||||
class Memberships implements ExtraFunction {
|
||||
_name = "memberships"
|
||||
_doc = "Gives a list of `{role: string, relation: Relation}`-objects, containing all the relations that this feature is part of. " +
|
||||
"\n\n" +
|
||||
"For example: `_part_of_walking_routes=feat.memberships().map(r => r.relation.tags.name).join(';')`"
|
||||
_args = []
|
||||
|
||||
_f(params, feat) {
|
||||
return () =>
|
||||
params.memberships.knownRelations.data.get(feat.properties.id) ?? []
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class GetParsed implements 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"]
|
||||
_f(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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class ExtraFunctions {
|
||||
|
||||
|
||||
static readonly intro = new Combine([
|
||||
new Title("Calculating tags with Javascript", 2),
|
||||
"In some cases, it is useful to have some tags calculated based on other properties. Some useful tags are available by default (e.g. `lat`, `lon`, `_country`), as detailed above.",
|
||||
"It is also possible to calculate your own tags - but this requires some javascript knowledge.",
|
||||
"",
|
||||
"Before proceeding, some warnings:",
|
||||
new List([
|
||||
"DO NOT DO THIS AS BEGINNER",
|
||||
"**Only do this if all other techniques fail** This should _not_ be done to create a rendering effect, only to calculate a specific value",
|
||||
"**THIS MIGHT BE DISABLED WITHOUT ANY NOTICE ON UNOFFICIAL THEMES** As unofficial themes might be loaded from the internet, this is the equivalent of injecting arbitrary code into the client. It'll be disabled if abuse occurs."
|
||||
]),
|
||||
"To enable this feature, add a field `calculatedTags` in the layer object, e.g.:",
|
||||
"````",
|
||||
"\"calculatedTags\": [",
|
||||
" \"_someKey=javascript-expression\",",
|
||||
" \"name=feat.properties.name ?? feat.properties.ref ?? feat.properties.operator\",",
|
||||
" \"_distanceCloserThen3Km=feat.distanceTo( some_lon, some_lat) < 3 ? 'yes' : 'no'\" ",
|
||||
" ]",
|
||||
"````",
|
||||
"",
|
||||
"The above code will be executed for every feature in the layer. The feature is accessible as `feat` and is an amended geojson object:",
|
||||
|
||||
new List([
|
||||
"`area` contains the surface area (in square meters) of the object",
|
||||
"`lat` and `lon` contain the latitude and longitude"
|
||||
]),
|
||||
"Some advanced functions are available on **feat** as well:"
|
||||
]).SetClass("flex-col").AsMarkdown();
|
||||
|
||||
|
||||
private static readonly allFuncs: ExtraFunction[] = [
|
||||
new DistanceToFunc(),
|
||||
new OverlapFunc(),
|
||||
new ClosestObjectFunc(),
|
||||
new ClosestNObjectFunc(),
|
||||
new Memberships(),
|
||||
new GetParsed()
|
||||
];
|
||||
|
||||
public static FullPatchFeature(params: ExtraFuncParams, feature) {
|
||||
for (const func of ExtraFunctions.allFuncs) {
|
||||
feature[func._name] = func._f(params, feature)
|
||||
}
|
||||
}
|
||||
|
||||
public static HelpText(): BaseUIElement {
|
||||
|
||||
const elems = []
|
||||
for (const func of ExtraFunctions.allFuncs) {
|
||||
console.log("Generating ", func.constructor.name)
|
||||
elems.push(new Title(func._name, 3),
|
||||
func._doc,
|
||||
new List(func._args ?? [], true))
|
||||
}
|
||||
|
||||
return new Combine([
|
||||
ExtraFunctions.intro,
|
||||
new List(ExtraFunctions.allFuncs.map(func => `[${func._name}](#${func._name})`)),
|
||||
...elems
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
import SimpleMetaTagger from "./SimpleMetaTagger";
|
||||
import {ExtraFuncParams, ExtraFunction} from "./ExtraFunction";
|
||||
import {ExtraFuncParams, ExtraFunctions} from "./ExtraFunctions";
|
||||
import LayerConfig from "../Models/ThemeConfig/LayerConfig";
|
||||
import State from "../State";
|
||||
|
||||
|
@ -105,7 +105,7 @@ export default class MetaTagging {
|
|||
}
|
||||
|
||||
|
||||
private static createFunctionsForFeature(calculatedTags: [string, string][]): ((feature: any) => void)[] {
|
||||
public static createFunctionsForFeature(calculatedTags: [string, string][]): ((feature: any) => void)[] {
|
||||
const functions: ((feature: any) => void)[] = [];
|
||||
for (const entry of calculatedTags) {
|
||||
const key = entry[0]
|
||||
|
@ -176,7 +176,7 @@ export default class MetaTagging {
|
|||
const functions = MetaTagging.createFunctionsForFeature(calculatedTags)
|
||||
|
||||
|
||||
ExtraFunction.FullPatchFeature(params, feature);
|
||||
ExtraFunctions.FullPatchFeature(params, feature);
|
||||
for (const f of functions) {
|
||||
f(feature);
|
||||
}
|
||||
|
|
|
@ -224,6 +224,7 @@ export class UIEventSource<T> {
|
|||
|
||||
public ping(): void {
|
||||
let toDelete = undefined
|
||||
let startTime = new Date().getTime() / 1000;
|
||||
for (const callback of this._callbacks) {
|
||||
if (callback(this.data) === true) {
|
||||
// This callback wants to be deleted
|
||||
|
@ -235,6 +236,10 @@ export class UIEventSource<T> {
|
|||
}
|
||||
}
|
||||
}
|
||||
let endTime = new Date().getTime() / 1000
|
||||
if((endTime - startTime) > 500){
|
||||
console.trace("Warning: a ping of ",this.tag," took more then 500ms; this is probably a performance issue")
|
||||
}
|
||||
if (toDelete !== undefined) {
|
||||
for (const toDeleteElement of toDelete) {
|
||||
this._callbacks.splice(this._callbacks.indexOf(toDeleteElement), 1)
|
||||
|
|
111
Models/ThemeConfig/DependencyCalculator.ts
Normal file
111
Models/ThemeConfig/DependencyCalculator.ts
Normal file
|
@ -0,0 +1,111 @@
|
|||
import {SpecialVisualization} from "../../UI/SpecialVisualizations";
|
||||
import {SubstitutedTranslation} from "../../UI/SubstitutedTranslation";
|
||||
import TagRenderingConfig from "./TagRenderingConfig";
|
||||
import {ExtraFuncParams, ExtraFunctions} from "../../Logic/ExtraFunctions";
|
||||
import LayerConfig from "./LayerConfig";
|
||||
|
||||
export default class DependencyCalculator {
|
||||
|
||||
public static GetTagRenderingDependencies(tr: TagRenderingConfig): string[] {
|
||||
|
||||
if(tr === undefined){
|
||||
throw "Got undefined tag rendering in getTagRenderingDependencies"
|
||||
}
|
||||
const deps: string[] = []
|
||||
|
||||
// All translated snippets
|
||||
const parts: string[] = [].concat(...(tr.EnumerateTranslations().map(tr => tr.AllValues())))
|
||||
|
||||
for (const part of parts) {
|
||||
const specialVizs: { func: SpecialVisualization, args: string[] }[]
|
||||
= SubstitutedTranslation.ExtractSpecialComponents(part).map(o => o.special)
|
||||
.filter(o => o?.func?.getLayerDependencies !== undefined)
|
||||
for (const specialViz of specialVizs) {
|
||||
deps.push(...specialViz.func.getLayerDependencies(specialViz.args))
|
||||
}
|
||||
}
|
||||
return deps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a set of all other layer-ids that this layer needs to function.
|
||||
* E.g. if this layers does snap to another layer in the preset, this other layer id will be mentioned
|
||||
*/
|
||||
public static getLayerDependencies(layer: LayerConfig): { neededLayer: string, reason: string, context?: string, neededBy: string }[] {
|
||||
const deps: { neededLayer: string, reason: string, context?: string, neededBy: string }[] = []
|
||||
|
||||
for (let i = 0; layer.presets !== undefined && i < layer.presets.length; i++) {
|
||||
const preset = layer.presets[i];
|
||||
preset.preciseInput?.snapToLayers?.forEach(id => {
|
||||
deps.push({
|
||||
neededLayer: id,
|
||||
reason: "a preset snaps to this layer",
|
||||
context: "presets[" + i + "]",
|
||||
neededBy: layer.id
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
for (const tr of layer.AllTagRenderings()) {
|
||||
for (const dep of DependencyCalculator.GetTagRenderingDependencies(tr)) {
|
||||
deps.push({
|
||||
neededLayer: dep,
|
||||
reason: "a tagrendering needs this layer",
|
||||
context: tr.id,
|
||||
neededBy: layer.id
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (layer.calculatedTags?.length > 0) {
|
||||
const obj = {
|
||||
type: "Feature",
|
||||
geometry: {
|
||||
type: "Point",
|
||||
coordinates: [0, 0]
|
||||
},
|
||||
properties: {
|
||||
id: "node/1"
|
||||
}
|
||||
}
|
||||
let currentKey = undefined
|
||||
let currentLine = undefined
|
||||
const params: ExtraFuncParams = {
|
||||
getFeaturesWithin: (layerId, _) => {
|
||||
|
||||
if(layerId === '*'){
|
||||
// This is a wildcard
|
||||
return []
|
||||
}
|
||||
|
||||
// The important line: steal the dependencies!
|
||||
deps.push({
|
||||
neededLayer: layerId, reason: "A calculated tag loads features from this layer",
|
||||
context: "calculatedTag[" + currentLine + "] which calculates the value for " + currentKey,
|
||||
neededBy: layer.id
|
||||
})
|
||||
|
||||
return []
|
||||
},
|
||||
memberships: undefined
|
||||
}
|
||||
// Init the extra patched functions...
|
||||
ExtraFunctions.FullPatchFeature(params, obj)
|
||||
// ... Run the calculated tag code, which will trigger the getFeaturesWithin above...
|
||||
for (let i = 0; i < layer.calculatedTags.length; i++) {
|
||||
const [key, code] = layer.calculatedTags[i];
|
||||
currentLine = i; // Leak the state...
|
||||
currentKey = key;
|
||||
try {
|
||||
|
||||
const func = new Function("feat", "return " + code + ";");
|
||||
const result = func(obj)
|
||||
obj.properties[key] = JSON.stringify(result);
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return deps
|
||||
}
|
||||
}
|
|
@ -413,7 +413,11 @@ export default class LayerConfig extends WithContextLoader {
|
|||
|
||||
}
|
||||
|
||||
public GenerateDocumentation(usedInThemes: string[], addedByDefault = false, canBeIncluded = true): BaseUIElement {
|
||||
public GenerateDocumentation(usedInThemes: string[], layerIsNeededBy: Map<string, string[]>, dependencies: {
|
||||
context?: string;
|
||||
reason: string;
|
||||
neededLayer: string;
|
||||
}[], addedByDefault = false, canBeIncluded = true): BaseUIElement {
|
||||
const extraProps = []
|
||||
|
||||
if (canBeIncluded) {
|
||||
|
@ -441,8 +445,12 @@ export default class LayerConfig extends WithContextLoader {
|
|||
]
|
||||
}
|
||||
|
||||
for (const dep of Array.from(this.getDependencies())) {
|
||||
extraProps.push(new Combine(["This layer will automatically load ", new Link(dep, "#"+dep)," into the layout as it depends on it."]))
|
||||
for (const dep of dependencies) {
|
||||
extraProps.push(new Combine(["This layer will automatically load ", new Link(dep.neededLayer, "#"+dep.neededLayer)," into the layout as it depends on it: ", dep.reason, "("+dep.context+")"]))
|
||||
}
|
||||
|
||||
for(const revDep of layerIsNeededBy?.get(this.id) ?? []){
|
||||
extraProps.push(new Combine(["This layer is needed as dependency for layer",new Link(revDep, "#"+revDep)]))
|
||||
}
|
||||
|
||||
return new Combine([
|
||||
|
@ -462,6 +470,10 @@ export default class LayerConfig extends WithContextLoader {
|
|||
}
|
||||
return this.calculatedTags.map((code) => code[1]);
|
||||
}
|
||||
|
||||
AllTagRenderings(): TagRenderingConfig[]{
|
||||
return Utils.NoNull([...this.tagRenderings, ...this.titleIcons, this.title, this.isShown])
|
||||
}
|
||||
|
||||
public ExtractImages(): Set<string> {
|
||||
const parts: Set<string>[] = [];
|
||||
|
@ -485,22 +497,4 @@ export default class LayerConfig extends WithContextLoader {
|
|||
return this.lineRendering.some(lr => lr.leftRightSensitive)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a set of all other layer-ids that this layer needs to function.
|
||||
* E.g. if this layers does snap to another layer in the preset, this other layer id will be mentioned
|
||||
*/
|
||||
public getDependencies(): Set<string>{
|
||||
const deps = new Set<string>()
|
||||
|
||||
for (const preset of this.presets ?? []) {
|
||||
if(preset.preciseInput?.snapToLayers === undefined){
|
||||
continue
|
||||
}
|
||||
preset.preciseInput?.snapToLayers?.forEach(id => {
|
||||
deps.add(id);
|
||||
})
|
||||
}
|
||||
|
||||
return deps
|
||||
}
|
||||
}
|
|
@ -6,6 +6,7 @@ import LayerConfig from "./LayerConfig";
|
|||
import {LayerConfigJson} from "./Json/LayerConfigJson";
|
||||
import Constants from "../Constants";
|
||||
import TilesourceConfig from "./TilesourceConfig";
|
||||
import DependencyCalculator from "./DependencyCalculator";
|
||||
|
||||
export default class LayoutConfig {
|
||||
public readonly id: string;
|
||||
|
@ -193,7 +194,6 @@ export default class LayoutConfig {
|
|||
names = [names]
|
||||
}
|
||||
names.forEach(name => {
|
||||
|
||||
if (name === "type_node") {
|
||||
// This is a very special layer which triggers special behaviour
|
||||
exportAllNodes = true;
|
||||
|
@ -221,29 +221,47 @@ export default class LayoutConfig {
|
|||
const sharedLayer = AllKnownLayers.sharedLayers.get(defaultLayer)
|
||||
if (sharedLayer !== undefined) {
|
||||
result.push(sharedLayer)
|
||||
}else if(!AllKnownLayers.runningGenerateScript){
|
||||
throw "SharedLayer "+defaultLayer+" not found"
|
||||
}
|
||||
}
|
||||
|
||||
let unmetDependencies: { dependency: string, layer: string }[] = []
|
||||
if(AllKnownLayers.runningGenerateScript){
|
||||
return {layers: result, extractAllNodes: exportAllNodes}
|
||||
}
|
||||
// Verify cross-dependencies
|
||||
let unmetDependencies: { neededLayer: string, neededBy: string, reason: string, context?: string }[] = []
|
||||
do {
|
||||
const dependencies: { dependency: string, layer: string }[] = [].concat(...result.map(l => Array.from(l.getDependencies()).map(d => ({
|
||||
dependency: d,
|
||||
layer: l.id
|
||||
}))))
|
||||
const loadedLayers = new Set(result.map(r => r.id))
|
||||
unmetDependencies = dependencies.filter(dep => !loadedLayers.has(dep.dependency))
|
||||
for (const unmetDependency of unmetDependencies) {
|
||||
const dependencies: { neededLayer: string, reason: string, context?: string, neededBy: string }[] = []
|
||||
|
||||
console.log("Recursively loading unmet dependency ", unmetDependency.dependency, "(needed by " + unmetDependency.layer + ")")
|
||||
const dep = AllKnownLayers.sharedLayers.get(unmetDependency.dependency)
|
||||
for (const layerConfig of result) {
|
||||
const layerDeps = DependencyCalculator.getLayerDependencies(layerConfig)
|
||||
dependencies.push(...layerDeps)
|
||||
}
|
||||
|
||||
const loadedLayers = new Set(result.map(r => r.id))
|
||||
// During the generate script, builtin layers are verified but not loaded - so we have to add them manually here
|
||||
// Their existance is checked elsewhere, so this is fine
|
||||
unmetDependencies = dependencies.filter(dep => !loadedLayers.has(dep.neededLayer))
|
||||
for (const unmetDependency of unmetDependencies) {
|
||||
const dep = AllKnownLayers.sharedLayers.get(unmetDependency.neededLayer)
|
||||
if (dep === undefined) {
|
||||
throw "The layer '" + unmetDependency.layer + "' needs '" + unmetDependency.dependency + "' to be loaded, but it could not be found as builtin layer (at " + context + ")"
|
||||
|
||||
const message =
|
||||
["Loading a dependency failed: layer "+unmetDependency.neededLayer+" is not found, neither as layer of "+json.id+" nor as builtin layer.",
|
||||
"This layer is needed by "+unmetDependency.neededBy,
|
||||
unmetDependency.reason+" (at "+unmetDependency.context+")",
|
||||
"Loaded layers are: "+result.map(l => l.id).join(",")
|
||||
|
||||
]
|
||||
throw message.join("\n\t");
|
||||
}
|
||||
result.unshift(dep)
|
||||
unmetDependencies = unmetDependencies.filter(d => d.dependency !== unmetDependency.dependency)
|
||||
unmetDependencies = unmetDependencies.filter(d => d.neededLayer !== unmetDependency.neededLayer)
|
||||
}
|
||||
|
||||
} while (unmetDependencies.length > 0)
|
||||
|
||||
return {layers: result, extractAllNodes: exportAllNodes}
|
||||
}
|
||||
|
||||
|
|
|
@ -58,11 +58,10 @@ export default class TagRenderingConfig {
|
|||
|
||||
|
||||
if (typeof json === "number") {
|
||||
json = ""+json
|
||||
json = "" + json
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (typeof json === "string") {
|
||||
this.render = Translations.T(json, context + ".render");
|
||||
this.multiAnswer = false;
|
||||
|
@ -71,12 +70,11 @@ export default class TagRenderingConfig {
|
|||
|
||||
|
||||
this.id = json.id ?? "";
|
||||
if(this.id.match(/^[a-zA-Z0-9 ()?\/=:;,_-]*$/) === null){
|
||||
throw "Invalid ID in "+context+": an id can only contain [a-zA-Z0-0_-] as characters. The offending id is: "+this.id
|
||||
if (this.id.match(/^[a-zA-Z0-9 ()?\/=:;,_-]*$/) === null) {
|
||||
throw "Invalid ID in " + context + ": an id can only contain [a-zA-Z0-0_-] as characters. The offending id is: " + this.id
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
this.group = json.group ?? "";
|
||||
this.render = Translations.T(json.render, context + ".render");
|
||||
this.question = Translations.T(json.question, context + ".question");
|
||||
|
@ -106,9 +104,9 @@ export default class TagRenderingConfig {
|
|||
throw `Freeform.args is defined. This should probably be 'freeform.helperArgs' (at ${context})`
|
||||
|
||||
}
|
||||
|
||||
if(json.freeform.key === "questions"){
|
||||
if(this.id !== "questions"){
|
||||
|
||||
if (json.freeform.key === "questions") {
|
||||
if (this.id !== "questions") {
|
||||
throw `If you use a freeform key 'questions', the ID must be 'questions' too to trigger the special behaviour. The current id is '${this.id}' (at ${context})`
|
||||
}
|
||||
}
|
||||
|
@ -187,53 +185,52 @@ export default class TagRenderingConfig {
|
|||
|
||||
if (this.id === "questions" && this.render !== undefined) {
|
||||
for (const ln in this.render.translations) {
|
||||
const txt :string = this.render.translations[ln]
|
||||
if(txt.indexOf("{questions}") >= 0){
|
||||
const txt: string = this.render.translations[ln]
|
||||
if (txt.indexOf("{questions}") >= 0) {
|
||||
continue
|
||||
}
|
||||
throw `${context}: The rendering for language ${ln} does not contain {questions}. This is a bug, as this rendering should include exactly this to trigger those questions to be shown!`
|
||||
|
||||
}
|
||||
if(this.freeform?.key !== undefined && this.freeform?.key !== "questions"){
|
||||
if (this.freeform?.key !== undefined && this.freeform?.key !== "questions") {
|
||||
throw `${context}: If the ID is questions to trigger a question box, the only valid freeform value is 'questions' as well. Set freeform to questions or remove the freeform all together`
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (this.freeform) {
|
||||
if(this.render === undefined){
|
||||
if (this.render === undefined) {
|
||||
throw `${context}: Detected a freeform key without rendering... Key: ${this.freeform.key} in ${context}`
|
||||
}
|
||||
for (const ln in this.render.translations) {
|
||||
const txt :string = this.render.translations[ln]
|
||||
if(txt === ""){
|
||||
throw context+" Rendering for language "+ln+" is empty"
|
||||
const txt: string = this.render.translations[ln]
|
||||
if (txt === "") {
|
||||
throw context + " Rendering for language " + ln + " is empty"
|
||||
}
|
||||
if(txt.indexOf("{"+this.freeform.key+"}") >= 0){
|
||||
if (txt.indexOf("{" + this.freeform.key + "}") >= 0) {
|
||||
continue
|
||||
}
|
||||
if(txt.indexOf("{"+this.freeform.key+":") >= 0){
|
||||
if (txt.indexOf("{" + this.freeform.key + ":") >= 0) {
|
||||
continue
|
||||
}
|
||||
if(txt.indexOf("{canonical("+this.freeform.key+")") >= 0){
|
||||
if (txt.indexOf("{canonical(" + this.freeform.key + ")") >= 0) {
|
||||
continue
|
||||
}
|
||||
if(this.freeform.type === "opening_hours" && txt.indexOf("{opening_hours_table(") >= 0){
|
||||
if (this.freeform.type === "opening_hours" && txt.indexOf("{opening_hours_table(") >= 0) {
|
||||
continue
|
||||
}
|
||||
if(this.freeform.type === "wikidata" && txt.indexOf("{wikipedia("+this.freeform.key) >= 0){
|
||||
if (this.freeform.type === "wikidata" && txt.indexOf("{wikipedia(" + this.freeform.key) >= 0) {
|
||||
continue
|
||||
}
|
||||
if(this.freeform.key === "wikidata" && txt.indexOf("{wikipedia()") >= 0){
|
||||
if (this.freeform.key === "wikidata" && txt.indexOf("{wikipedia()") >= 0) {
|
||||
continue
|
||||
}
|
||||
throw `${context}: The rendering for language ${ln} does not contain the freeform key {${this.freeform.key}}. This is a bug, as this rendering should show exactly this freeform key!\nThe rendering is ${txt} `
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (this.render && this.question && this.freeform === undefined) {
|
||||
throw `${context}: Detected a tagrendering which takes input without freeform key in ${context}; the question is ${this.question.txt}`
|
||||
}
|
||||
|
@ -377,7 +374,7 @@ export default class TagRenderingConfig {
|
|||
}
|
||||
}
|
||||
|
||||
if(this.id === "questions"){
|
||||
if (this.id === "questions") {
|
||||
return this.render
|
||||
}
|
||||
|
||||
|
@ -391,6 +388,26 @@ export default class TagRenderingConfig {
|
|||
return defltValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all translations that might be rendered in all languages
|
||||
* USed for static analysis
|
||||
* @constructor
|
||||
* @private
|
||||
*/
|
||||
EnumerateTranslations(): Translation[] {
|
||||
const translations: Translation[] = []
|
||||
for (const key in this) {
|
||||
if(!this.hasOwnProperty(key)){
|
||||
continue;
|
||||
}
|
||||
const o = this[key]
|
||||
if (o instanceof Translation) {
|
||||
translations.push(o)
|
||||
}
|
||||
}
|
||||
return translations;
|
||||
}
|
||||
|
||||
public ExtractImages(isIcon: boolean): Set<string> {
|
||||
|
||||
const usedIcons = new Set<string>()
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import BaseUIElement from "../BaseUIElement";
|
||||
import {FixedUiElement} from "./FixedUiElement";
|
||||
import {Utils} from "../../Utils";
|
||||
|
||||
export default class Title extends BaseUIElement {
|
||||
public readonly title: BaseUIElement;
|
||||
|
@ -10,6 +11,9 @@ export default class Title extends BaseUIElement {
|
|||
|
||||
constructor(embedded: string | BaseUIElement, level: number = 3) {
|
||||
super()
|
||||
if(embedded === undefined){
|
||||
throw "A title should have some content. Undefined is not allowed"
|
||||
}
|
||||
if (typeof embedded === "string") {
|
||||
this.title = new FixedUiElement(embedded)
|
||||
} else {
|
||||
|
@ -23,7 +27,11 @@ export default class Title extends BaseUIElement {
|
|||
}else if(embedded instanceof FixedUiElement){
|
||||
innerText = embedded.content
|
||||
}else{
|
||||
this.title.ConstructElement()?.innerText
|
||||
if(Utils.runningFromConsole){
|
||||
console.log("Not constructing an anchor for title with embedded content of "+embedded)
|
||||
}else{
|
||||
innerText = embedded.ConstructElement()?.innerText
|
||||
}
|
||||
}
|
||||
|
||||
this.id = innerText?.replace(/ /g, '-')
|
||||
|
|
|
@ -140,6 +140,20 @@ ${Utils.Special_visualizations_tagsToApplyHelpText}
|
|||
defaultValue: "5"
|
||||
}]
|
||||
|
||||
getLayerDependencies(args: string[]){
|
||||
const dependsOnLayers: string[] = []
|
||||
|
||||
// The target layer
|
||||
dependsOnLayers.push(args[0])
|
||||
|
||||
const snapOntoLayers = args[5]?.trim() ?? "";
|
||||
if(args[5] !== ""){
|
||||
dependsOnLayers.push(...snapOntoLayers.split(";"))
|
||||
}
|
||||
|
||||
return dependsOnLayers
|
||||
}
|
||||
|
||||
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"),
|
||||
|
|
|
@ -44,7 +44,8 @@ export interface SpecialVisualization {
|
|||
constr: ((state: State, tagSource: UIEventSource<any>, argument: string[], guistate: DefaultGuiState,) => BaseUIElement),
|
||||
docs: string,
|
||||
example?: string,
|
||||
args: { name: string, defaultValue?: string, doc: string }[]
|
||||
args: { name: string, defaultValue?: string, doc: string }[],
|
||||
getLayerDependencies?: (argument: string[]) => string[]
|
||||
}
|
||||
|
||||
export default class SpecialVisualizations {
|
||||
|
@ -477,6 +478,7 @@ export default class SpecialVisualizations {
|
|||
}
|
||||
},
|
||||
new ImportButtonSpecialViz(),
|
||||
|
||||
{
|
||||
funcName: "multi_apply",
|
||||
docs: "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",
|
||||
|
@ -688,7 +690,7 @@ export default class SpecialVisualizations {
|
|||
return [arg.name, defaultArg, arg.doc];
|
||||
})
|
||||
) : undefined,
|
||||
new Title("Example usage", 4),
|
||||
new Title("Example usage of "+viz.funcName, 4),
|
||||
new FixedUiElement(
|
||||
viz.example ?? "`{" + viz.funcName + "(" + viz.args.map(arg => arg.defaultValue).join(",") + ")}`"
|
||||
).SetClass("literal-code"),
|
||||
|
|
|
@ -112,6 +112,10 @@ export class Translation extends BaseUIElement {
|
|||
}
|
||||
return langs;
|
||||
}
|
||||
|
||||
public AllValues(): string[]{
|
||||
return this.SupportedLanguages().map(lng => this.translations[lng]);
|
||||
}
|
||||
|
||||
public Subs(text: any): Translation {
|
||||
const newTranslations = {};
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
}
|
||||
},
|
||||
"tagRenderings": [
|
||||
"images",
|
||||
{
|
||||
"id": "Entrance type",
|
||||
"question": {
|
||||
|
@ -167,6 +168,67 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "automatic_door",
|
||||
"question": "Is this door automated?",
|
||||
"condition": "door!=no",
|
||||
"mappings": [
|
||||
{
|
||||
"if": "automatic_door=yes",
|
||||
"then": {
|
||||
"en": "This is an automatic door"
|
||||
},
|
||||
"hideInAnswer": true
|
||||
},
|
||||
{
|
||||
"if": "automatic_door=no",
|
||||
"then": {
|
||||
"en": "This door is <b>not</b> automated"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "automatic_door=motion",
|
||||
"then": {
|
||||
"en": "This door will open automatically when <b>motion</b> is detected"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "automatic_door=floor",
|
||||
"then": {
|
||||
"en": "This door will open automatically when a <b>sensor in the floor</b> is triggered"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "automatic_door=button",
|
||||
"then": {
|
||||
"en": "This door will open automatically when a <b>button is pressed</b>"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "automatic_door=slowdown_button",
|
||||
"then": {
|
||||
"en": "This door revolves automatically all the time, but has a <b>button to slow it down</b>, e.g. for wheelchair users"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "automatic_door=continuous",
|
||||
"then": {
|
||||
"en": "This door revolves automatically all the time"
|
||||
}
|
||||
},{
|
||||
"if": "automatic_door=serviced_on_button_press",
|
||||
"then": {
|
||||
"en": "This door will be opened by staff when requested by <b>pressing a button</b"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "automatic_door=serviced_on_request",
|
||||
"then": {
|
||||
"en": "This door will be opened by staff when requested"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "width",
|
||||
"render": {
|
||||
|
@ -188,11 +250,11 @@
|
|||
"centroid"
|
||||
],
|
||||
"icon": {
|
||||
"render": "./assets/layers/entrance/door.svg",
|
||||
"render": "circle:white;./assets/layers/entrance/door.svg",
|
||||
"mappings": [
|
||||
{
|
||||
"if": "entrance=emergency",
|
||||
"then": "./assets/layers/entrance/emergency_door.svg"
|
||||
"then": "circle:white;./assets/layers/entrance/emergency_door.svg"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -20,13 +20,22 @@
|
|||
"passAllFeatures": true,
|
||||
"shownByDefault": false,
|
||||
"mapRendering": [
|
||||
{
|
||||
"color": {
|
||||
"render": "#fff"
|
||||
},
|
||||
"fill": "no",
|
||||
"width": {
|
||||
"render": "3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"color": {
|
||||
"render": "#333"
|
||||
},
|
||||
"fill": "no",
|
||||
"width": {
|
||||
"render": "5"
|
||||
"render": "2"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
@ -60,7 +60,6 @@
|
|||
"startLon": 0,
|
||||
"startZoom": 12,
|
||||
"layers": [
|
||||
"walls_and_buildings",
|
||||
"defibrillator"
|
||||
]
|
||||
}
|
|
@ -16,7 +16,6 @@
|
|||
"startLon": 3.195682,
|
||||
"startZoom": 12,
|
||||
"layers": [
|
||||
"walls_and_buildings",
|
||||
"defibrillator",
|
||||
{
|
||||
"id": "Brugge",
|
||||
|
|
|
@ -692,7 +692,7 @@
|
|||
"render": {
|
||||
"en": "<h3>Length overview</h3>{histogram(_length_hist)}",
|
||||
"fr": "<h3>Résumé de longueur</h3>{histogram(_length_hist)}",
|
||||
"de": "<h3>Längenübersicht</h3>{histogramm(_length_hist)}",
|
||||
"de": "<h3>Längenübersicht</h3>{histogram(_length_hist)}",
|
||||
"it": "<h3>Riassunto della lunghezza</h3>{histogram(_length_hist)}"
|
||||
},
|
||||
"condition": "_length_hist!~\\[\\]",
|
||||
|
|
22
assets/themes/entrances/entrances.json
Normal file
22
assets/themes/entrances/entrances.json
Normal file
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"id": "entrances",
|
||||
"title": {
|
||||
"en":"Entrances"
|
||||
},
|
||||
"icon": "./assets/layers/entrance/door.svg",
|
||||
"description": {
|
||||
"en":"A map showing all entrances, which surveys for important aspects for wheelchair users"
|
||||
},
|
||||
"shortDescription": {"en":
|
||||
"Survey entrances to help wheelchair routing"
|
||||
},
|
||||
"language": ["en"],
|
||||
"version": "2021-12-04",
|
||||
"maintainer": "MapComplete",
|
||||
"layers": [
|
||||
"entrance"
|
||||
],
|
||||
"startZoom": 15,
|
||||
"startLat": 51.0490,
|
||||
"startLon": 3.7297
|
||||
}
|
|
@ -49,7 +49,6 @@
|
|||
"socialImage": "",
|
||||
"defaultBackgroundId": "osm",
|
||||
"layers": [
|
||||
"walls_and_buildings",
|
||||
"direction",
|
||||
"surveillance_camera"
|
||||
]
|
||||
|
|
8177
dependencies.svg
Normal file
8177
dependencies.svg
Normal file
File diff suppressed because it is too large
Load diff
After Width: | Height: | Size: 803 KiB |
|
@ -2,7 +2,7 @@ import {Utils} from "../Utils";
|
|||
import SpecialVisualizations from "../UI/SpecialVisualizations";
|
||||
import SimpleMetaTagger from "../Logic/SimpleMetaTagger";
|
||||
import Combine from "../UI/Base/Combine";
|
||||
import {ExtraFunction} from "../Logic/ExtraFunction";
|
||||
import {ExtraFunctions} from "../Logic/ExtraFunctions";
|
||||
import ValidatedTextField from "../UI/Input/ValidatedTextField";
|
||||
import BaseUIElement from "../UI/BaseUIElement";
|
||||
import Translations from "../UI/i18n/Translations";
|
||||
|
@ -39,7 +39,7 @@ function WriteFile(filename, html: BaseUIElement, autogenSource: string[]): void
|
|||
}
|
||||
|
||||
WriteFile("./Docs/SpecialRenderings.md", SpecialVisualizations.HelpMessage(), ["UI/SpecialVisualisations.ts"])
|
||||
WriteFile("./Docs/CalculatedTags.md", new Combine([new Title("Metatags", 1), SimpleMetaTagger.HelpText(), ExtraFunction.HelpText()]).SetClass("flex-col"),
|
||||
WriteFile("./Docs/CalculatedTags.md", new Combine([new Title("Metatags", 1), SimpleMetaTagger.HelpText(), ExtraFunctions.HelpText()]).SetClass("flex-col"),
|
||||
["SimpleMetaTagger", "ExtraFunction"])
|
||||
WriteFile("./Docs/SpecialInputElements.md", ValidatedTextField.HelpText(), ["ValidatedTextField.ts"]);
|
||||
WriteFile("./Docs/BuiltinLayers.md", AllKnownLayouts.GenLayerOverviewText(), ["AllKnownLayers.ts"])
|
||||
|
|
|
@ -7,6 +7,7 @@ import {LayerConfigJson} from "../Models/ThemeConfig/Json/LayerConfigJson";
|
|||
import LayerConfig from "../Models/ThemeConfig/LayerConfig";
|
||||
import {Translation} from "../UI/i18n/Translation";
|
||||
import {Utils} from "../Utils";
|
||||
import AllKnownLayers from "../Customizations/AllKnownLayers";
|
||||
|
||||
// This scripts scans 'assets/layers/*.json' for layer definition files and 'assets/themes/*.json' for theme definition files.
|
||||
// It spits out an overview of those to be used to load them
|
||||
|
@ -89,6 +90,7 @@ class LayerOverviewUtils {
|
|||
|
||||
main(args: string[]) {
|
||||
|
||||
AllKnownLayers.runningGenerateScript = true;
|
||||
const layerFiles = ScriptUtils.getLayerFiles();
|
||||
const themeFiles = ScriptUtils.getThemeFiles();
|
||||
|
||||
|
|
Loading…
Reference in a new issue