diff --git a/Docs/FilterFunctionality.gif b/Docs/FilterFunctionality.gif
new file mode 100644
index 0000000000..69b826dc4b
Binary files /dev/null and b/Docs/FilterFunctionality.gif differ
diff --git a/Docs/FilteredByDepth.gif b/Docs/FilteredByDepth.gif
new file mode 100644
index 0000000000..3af61e8d3e
Binary files /dev/null and b/Docs/FilteredByDepth.gif differ
diff --git a/Docs/Making_Your_Own_Theme.md b/Docs/Making_Your_Own_Theme.md
index 5279b8b969..7848988d22 100644
--- a/Docs/Making_Your_Own_Theme.md
+++ b/Docs/Making_Your_Own_Theme.md
@@ -4,6 +4,11 @@ Making your own theme
In MapComplete, it is relatively simple to make your own theme. This guide will give some information on how you can do
this.
+Table of contents:
+
+1. [Requirements](#requirements) which lists what you should know before starting to create a theme
+2. [What is a good theme?](#what-is-a-good-theme)
+
Requirements
------------
@@ -15,14 +20,217 @@ Before you start, you should have the following qualifications:
- You are in contact with your local OpenStreetMap community and do know some other members to discuss tagging and to
help testing
-If you do not have those qualifications, reach out to the MapComplete community channel
+Please, do reach out to the MapComplete community channel
on [Telegram](https://t.me/MapComplete)
or [Matrix](https://app.element.io/#/room/#MapComplete:matrix.org).
+
+What is a good theme?
+---------------------
+
+A **theme** (or _layout_) is a single map showing one or more layers.
+The layers should work together in such a way that they serve a certain **audience**.
+You should be able to state in a few sentences whom would be the user of such a map, e.g.
+
+- a cyclist searching for bike repair
+- a thirsty person who needs water
+- someone who wants to know what their street is named after
+- ...
+
+Some layers will be useful for many themes (e.g. _drinking water_, _toilets_, _shops_, ...). Due to this, MapComplete supports to reuse already existing official layers into a theme.
+
+To include an already existing layer, simply type the layer id, e.g.:
+
+```json
+{
+ "id": "my-theme",
+ "title": "My theme for xyz",
+ "...": "...",
+ "layers": [
+ {
+ "id": "my super-awesome new layer"
+ },
+ "bench",
+ "shops",
+ "drinking_water",
+ "toilet"
+ ]
+}
+```
+
+Note that it is good practice to use an existing layer and to tweak it:
+
+```json
+{
+ "id": "my super awesome theme",
+ "...": "...",
+ "layers": [
+ {
+ "builtin": [
+ "toilet",
+ "bench"
+ ],
+ "override": {
+ "#": "Override is a section which copies all the keys here and 'pastes' them into the existing layers. For example, the 'minzoom' defined here will redifine the minzoom of 'toilet' and 'bench'",
+ "minzoom": 17,
+ "#0": "Appending to lists is supported to, e.g. to add an extra question",
+ "tagRenderings+": [
+ {
+ "id": "new-question",
+ "question": "What is ?",
+ "render": "{property}",
+ "...": "..."
+ }
+ ],
+ "#1": "Note that paths will be followed: the below block will add/change the icon of the layer, without changing the other properties of the first tag rendering. (Assumption: the first mapRendering is the icon rendering)",
+ "mapRendering": [
+ {
+ "icon": {
+ "render": "new-icon.svg"
+ }
+ }
+ ]
+ }
+ }
+ ]
+}
+
+```
+
+### What is a good layer?
+
+A good layer is layer which shows **all** objects of a certain type, e.g. **all** shops, **all** restaurants, ...
+
+It asks some relevant questions, with the most important and easiests questions first.
+
+#### Don't: use a layer to filter
+
+**Do not define a layer which filters on an attribute**, such as all restaurants with a vegetarian diet, all shops which accept bitcoin.
+This makes _addition_ of new points difficult as information might not yet be known. Conser the following situation:
+
+1. A theme defines a layer `vegetarian restaurants`, which matches `amenity=restaurant` & `diet:vegetarian=yes`.
+2. An object exists in OSM with `amenity=restaurant`;`name=Fancy Food`;`diet:vegan=yes`;`phone=...`;...
+3. A contributor visits the themes and will notice that _Fancy Food_ is missing
+4. The contributor will add _Fancy Food_
+5. There are now **two** Fancy Food objects in OSM.
+
+Instead, use the filter functionality instead. This can be used from the layer to hide some objects based on their properties.
+When the contributor wants to add a new point, they'll be notified that some features might be hidden and only be allowed to add a new point when the points are shown.
+
+
+
+```json
+{
+ "id": "my awesome layer",
+ "tagRenderings": "... some relevant attributes and questions ...",
+ "mapRenderings": "... display on the map ... ",
+ "filter": [
+ {
+ "id": "vegetarian",
+ "options": [
+ {
+ "question": {
+ "en": "Has a vegetarian menu"
+ },
+ "osmTags": {
+ "or": [
+ "diet:vegetarian=yes",
+ "diet:vegetarian=only",
+ "diet:vegan=yes",
+ "diet:vegan=only"
+ ]
+ }
+ }
+ ]
+ }
+ ]
+}
+```
+
+If you want to show only features of a certain type, there is a workaround.
+For example, the [fritures map](https://mapcomplete.osm.be/fritures.html?z=1&welcome-control-toggle=true) will show french fries shop, aka every `amenity~fast_food|restaurant` with `cuisine=friture`.
+However, quite a few fritures are already mapped as fastfood but have their `cuisine`-tag missing (or misspelled).
+
+There is a workaround for this: show **all** food related items at zoomlevel 19 (or higher), and only show the fritures when zoomed out.
+
+In order to achieve this:
+
+1. The layer 'food' is defined in a separate file and reused
+2. The layer food is imported in the theme 'fritures'. With 'override', some properties are changed, namely:
+ - The `osmTags` are overwritten: `cuisine=friture` is now required
+ - The presets are overwritten and _disabled_
+ - The _id_ and _name_ of the layer are changed
+3. The layer `food` is imported _a second time_, but now the minzoom is set to `19`. This will show _all_ restaurants.
+
+In case of a friture which is already added as fastfood, they'll see the fastfood popup instead of adding a new item:
+
+
+
+```json
+{
+ "layers": [
+ {
+ "builtin": "food",
+ "override": {
+ "id": "friture",
+ "name": {
+ "en": "Fries shop"
+ },
+ "=presets": [],
+ "source": {
+ "=osmTags": {
+ "and": [
+ "cuisine=friture",
+ {
+ "or": [
+ "amenity=fast_food",
+ "amenity=restaurant"
+ ]
+ }
+ ]
+ }
+ }
+ }
+ },
+ {
+ "builtin": "food",
+ "override": {
+ "minzoom": 19,
+ "filter": null,
+ "name": null
+ }
+ }
+ ]
+}
+```
+
+
+### What is a good question and tagrendering?
+
+A tagrendering maps an attribute onto a piece of human readable text.
+These should be **full sentences**, e.g. `"render": "The maximum speed of this road is {maxspeed} km/h"`
+
+In some cases, there might be some predifined special values as mappings, such as `"mappings": [{"if": "maxspeed=30", "then": "The maxspeed is 30km/h"}]`
+
+The question then follows logically: `{"question": "What is the maximum allowed speed for this road, in km/h?"}`
+At last, you'll also want to say that the user can type an answer too and that it has to be a number: `"freeform":{"key": "maxspeed","type":"pnat"}`.
+
+The entire tagRendering will thus be:
+
+```json
+{
+ "question": "What is the maximum allowed speed for this road, in km/h?",
+ "render": "The maximum speed of this road is {maxspeed} km/h",
+ "freeform":{"key": "maxspeed","type":"pnat"},
+ "mappings": [{"if": "maxspeed=30", "then": "The maxspeed is 30km/h"}]
+}
+```
+
+
The template
------------
-[A basic template is availalbe here](https://github.com/pietervdvn/MapComplete/blob/develop/Docs/theme-template.json)
+[A basic template is available here](https://github.com/pietervdvn/MapComplete/blob/develop/Docs/theme-template.json)
The custom theme generator
--------------------------
@@ -229,18 +437,21 @@ disregarding other properties.
One should not make one layer for benches with a backrest and one layer for benches without. This is confusing for users
and poses problems: what if the backrest status is unknown? What if it is some weird value? Also, it isn't possible to '
-move' an attribute to another layer.
+move' a feature to another layer.
Instead, make one layer for one kind of object and change the icon based on attributes.
-### Using layers as filters
-
-Using layers as filters - this doesn't work!
-
-Use the `filter`-functionality instead.
-
### Not reading the theme JSON specs
There are a few advanced features to do fancy stuff available, which are documented only in the spec above - for
example, reusing background images and substituting the colours or HTML rendering. If you need advanced stuff, read it
through!
+
+### Forgetting adjacent concepts
+
+Some new contributors might add a POI to indicate something that resembles it, but quite isn't.
+
+For example, if they are only offered a layer with public bookcases, they might map their local library with a public bookcase.
+The perfect solution for this is to provide both the library-layer and public bookcases layer - but this requires having both layers.
+
+A good solution is to clearly explain what a certain feature is and what it is not.
\ No newline at end of file
diff --git a/Docs/theme-template.json b/Docs/theme-template.json
index 9ead97d564..8d904d0aad 100644
--- a/Docs/theme-template.json
+++ b/Docs/theme-template.json
@@ -22,9 +22,8 @@
"#": "For more options and configuration, see the documentation in LayoutConfig.json",
"#layers": "The list of layers is where most of the content will be. Either reuse an already existing layer by simply calling it's ID or define a whole new layer. An overview of builtin layers is at https://github.com/pietervdvn/MapComplete/blob/develop/Docs/BuiltinLayers.md#normal-layers",
"layers": [
- "bench",
{
- "id": "a singular nound describing the feature, in english",
+ "id": "a singular noun describing the feature, in english",
"source": {
"osmTags": {
"#": "For a description on which tags are possible, see https://github.com/pietervdvn/MapComplete/blob/develop/Docs/Tags_format.md",
diff --git a/Logic/Actors/TitleHandler.ts b/Logic/Actors/TitleHandler.ts
index d20bea7b9e..8593ecf184 100644
--- a/Logic/Actors/TitleHandler.ts
+++ b/Logic/Actors/TitleHandler.ts
@@ -16,7 +16,7 @@ export default class TitleHandler {
const currentTitle: UIEventSource = state.selectedElement.map(
selected => {
const layout = state.layoutToUse
- const defaultTitle = Translations.WT(layout?.title)?.txt ?? "MapComplete"
+ const defaultTitle = layout?.title?.txt ?? "MapComplete"
if (selected === undefined) {
return defaultTitle
diff --git a/Logic/DetermineLayout.ts b/Logic/DetermineLayout.ts
index 734988583d..b935481e88 100644
--- a/Logic/DetermineLayout.ts
+++ b/Logic/DetermineLayout.ts
@@ -14,7 +14,6 @@ import {FixLegacyTheme} from "../Models/ThemeConfig/Conversion/LegacyJsonConvert
import {LayerConfigJson} from "../Models/ThemeConfig/Json/LayerConfigJson";
import SharedTagRenderings from "../Customizations/SharedTagRenderings";
import * as known_layers from "../assets/generated/known_layers.json"
-import {LayoutConfigJson} from "../Models/ThemeConfig/Json/LayoutConfigJson";
import {PrepareTheme} from "../Models/ThemeConfig/Conversion/PrepareTheme";
import * as licenses from "../assets/generated/license_info.json"
import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig";
@@ -43,10 +42,6 @@ export default class DetermineLayout {
}
let layoutId: string = undefined
- if (location.href.indexOf("buurtnatuur.be") >= 0) {
- layoutId = "buurtnatuur"
- }
-
const path = window.location.pathname.split("/").slice(-1)[0];
if (path !== "theme.html" && path !== "") {
@@ -72,7 +67,7 @@ export default class DetermineLayout {
public static LoadLayoutFromHash(
userLayoutParam: UIEventSource
- ): (LayoutConfig & {definition: LayoutConfigJson}) | null {
+ ): LayoutConfig | null {
let hash = location.hash.substr(1);
let json: any;
@@ -113,9 +108,7 @@ export default class DetermineLayout {
const layoutToUse = DetermineLayout.prepCustomTheme(json)
userLayoutParam.setData(layoutToUse.id);
- const config = new LayoutConfig(layoutToUse, false);
- config["definition"] = json
- return config
+ return layoutToUse
} catch (e) {
console.error(e)
if (hash === undefined || hash.length < 10) {
@@ -144,7 +137,7 @@ export default class DetermineLayout {
.AttachTo("centermessage");
}
- private static prepCustomTheme(json: any): LayoutConfigJson {
+ private static prepCustomTheme(json: any, sourceUrl?: string): LayoutConfig {
if(json.layers === undefined && json.tagRenderings !== undefined){
const iconTr = json.mapRendering.map(mr => mr.icon).find(icon => icon !== undefined)
@@ -161,7 +154,6 @@ export default class DetermineLayout {
}
}
-
const knownLayersDict = new Map()
for (const key in known_layers.layers) {
const layer = known_layers.layers[key]
@@ -172,10 +164,17 @@ export default class DetermineLayout {
sharedLayers: knownLayersDict
}
json = new FixLegacyTheme().convertStrict(json, "While loading a dynamic theme")
+ const raw = json;
+
json = new FixImages(DetermineLayout._knownImages).convertStrict(json, "While fixing the images")
json = new PrepareTheme(converState).convertStrict(json, "While preparing a dynamic theme")
console.log("The layoutconfig is ", json)
- return json
+
+
+ return new LayoutConfig(json, false, {
+ definitionRaw: JSON.stringify(raw, null, " "),
+ definedAtUrl: sourceUrl
+ })
}
private static async LoadRemoteTheme(link: string): Promise {
@@ -190,8 +189,7 @@ export default class DetermineLayout {
try {
parsed.id = link;
console.log("Loaded remote link:", link)
- const layoutToUse = DetermineLayout.prepCustomTheme(parsed)
- return new LayoutConfig(layoutToUse, false)
+ return DetermineLayout.prepCustomTheme(parsed, link)
} catch (e) {
console.error(e)
DetermineLayout.ShowErrorOnCustomTheme(
diff --git a/Logic/FeatureSource/Sources/RenderingMultiPlexerFeatureSource.ts b/Logic/FeatureSource/Sources/RenderingMultiPlexerFeatureSource.ts
index 5349d8d431..1f5d8cbb41 100644
--- a/Logic/FeatureSource/Sources/RenderingMultiPlexerFeatureSource.ts
+++ b/Logic/FeatureSource/Sources/RenderingMultiPlexerFeatureSource.ts
@@ -1,5 +1,5 @@
/**
- * This feature source helps the ShowDataLayer class: it introduces the necessary extra features and indiciates with what renderConfig it should be rendered.
+ * This feature source helps the ShowDataLayer class: it introduces the necessary extra features and indicates with what renderConfig it should be rendered.
*/
import {UIEventSource} from "../../UIEventSource";
import {GeoOperations} from "../../GeoOperations";
@@ -11,22 +11,25 @@ export default class RenderingMultiPlexerFeatureSource {
public readonly features: UIEventSource<(any & { pointRenderingIndex: number | undefined, lineRenderingIndex: number | undefined })[]>;
constructor(upstream: FeatureSource, layer: LayerConfig) {
+
+ 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 projectedCentroidRenderings = pointRenderObjects.filter(r => r.rendering.location.has("projected_centerpoint"))
+ const startRenderings = pointRenderObjects.filter(r => r.rendering.location.has("start"))
+ const endRenderings = pointRenderObjects.filter(r => r.rendering.location.has("end"))
+ const hasCentroid = centroidRenderings.length > 0 || projectedCentroidRenderings.length > 0
+ const lineRenderObjects = layer.lineRendering
+
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, multiLineStringIndex: number | undefined })[] = [];
@@ -55,12 +58,25 @@ export default class RenderingMultiPlexerFeatureSource {
}
} else {
// This is a a line: add the centroids
- for (const rendering of centroidRenderings) {
- addAsPoint(feat, rendering, GeoOperations.centerpointCoordinates(feat))
+ let centerpoint: [number, number] = undefined;
+ let projectedCenterPoint : [number, number] = undefined
+ if(hasCentroid){
+ centerpoint = GeoOperations.centerpointCoordinates(feat)
+ if(projectedCentroidRenderings.length > 0){
+ projectedCenterPoint = <[number,number]> GeoOperations.nearestPoint(feat, centerpoint).geometry.coordinates
+ }
}
+ for (const rendering of centroidRenderings) {
+ addAsPoint(feat, rendering, centerpoint)
+ }
+
if (feat.geometry.type === "LineString") {
+ for (const rendering of projectedCentroidRenderings) {
+ addAsPoint(feat, rendering, projectedCenterPoint)
+ }
+
// Add start- and endpoints
const coordinates = feat.geometry.coordinates
for (const rendering of startRenderings) {
@@ -71,6 +87,10 @@ export default class RenderingMultiPlexerFeatureSource {
addAsPoint(feat, rendering, coordinate)
}
+ }else{
+ for (const rendering of projectedCentroidRenderings) {
+ addAsPoint(feat, rendering, centerpoint)
+ }
}
// AT last, add it 'as is' to what we should render
diff --git a/Logic/Web/Wikidata.ts b/Logic/Web/Wikidata.ts
index dc4329ebd2..c32f6b6870 100644
--- a/Logic/Web/Wikidata.ts
+++ b/Logic/Web/Wikidata.ts
@@ -1,6 +1,6 @@
import {Utils} from "../../Utils";
import {UIEventSource} from "../UIEventSource";
-import * as wds from "wikibase-sdk"
+import * as wds from "wikidata-sdk"
export class WikidataResponse {
public readonly id: string
@@ -126,13 +126,22 @@ export interface WikidataSearchoptions {
maxCount?: 20 | number
}
+export interface WikidataAdvancedSearchoptions extends WikidataSearchoptions {
+ instanceOf?: number[];
+ notInstanceOf?: number[]
+}
+
+
/**
* Utility functions around wikidata
*/
export default class Wikidata {
private static readonly _identifierPrefixes = ["Q", "L"].map(str => str.toLowerCase())
- private static readonly _prefixesToRemove = ["https://www.wikidata.org/wiki/Lexeme:", "https://www.wikidata.org/wiki/", "Lexeme:"].map(str => str.toLowerCase())
+ private static readonly _prefixesToRemove = ["https://www.wikidata.org/wiki/Lexeme:",
+ "https://www.wikidata.org/wiki/",
+ "http://www.wikidata.org/entity/",
+ "Lexeme:"].map(str => str.toLowerCase())
private static readonly _cache = new Map>()
@@ -148,6 +157,52 @@ export default class Wikidata {
return src;
}
+ /**
+ * Given a search text, searches for the relevant wikidata entries, excluding pages "outside of the main tree", e.g. disambiguation pages.
+ * Optionally, an 'instance of' can be given to limit the scope, e.g. instanceOf:5 (humans) will only search for humans
+ */
+ public static async searchAdvanced(text: string, options: WikidataAdvancedSearchoptions): Promise<{
+ id: string,
+ relevance?: number,
+ label: string,
+ description?: string
+ }[]> {
+ let instanceOf = ""
+ if (options?.instanceOf !== undefined && options.instanceOf.length > 0) {
+ const phrases = options.instanceOf.map(q => `{ ?item wdt:P31/wdt:P279* wd:Q${q}. }`)
+ instanceOf = "{"+ phrases.join(" UNION ") + "}"
+ }
+ const forbidden = (options?.notInstanceOf ?? [])
+ .concat([17379835]) // blacklist 'wikimedia pages outside of the main knowledge tree', e.g. disambiguation pages
+ const minusPhrases = forbidden.map(q => `MINUS {?item wdt:P31/wdt:P279* wd:Q${q} .}`)
+ const sparql = `SELECT * WHERE {
+ SERVICE wikibase:mwapi {
+ bd:serviceParam wikibase:api "EntitySearch" .
+ bd:serviceParam wikibase:endpoint "www.wikidata.org" .
+ bd:serviceParam mwapi:search "${text}" .
+ bd:serviceParam mwapi:language "${options.lang}" .
+ ?item wikibase:apiOutputItem mwapi:item .
+ ?num wikibase:apiOrdinal true .
+ bd:serviceParam wikibase:limit ${Math.round((options.maxCount ?? 20) * 1.5) /*Some padding for disambiguation pages */} .
+ ?label wikibase:apiOutput mwapi:label .
+ ?description wikibase:apiOutput "@description" .
+ }
+ ${instanceOf}
+ ${minusPhrases.join("\n ")}
+ } ORDER BY ASC(?num) LIMIT ${options.maxCount ?? 20}`
+ const url = wds.sparqlQuery(sparql)
+
+ const result = await Utils.downloadJson(url)
+ /*The full uri of the wikidata-item*/
+
+ return result.results.bindings.map(({item, label, description, num}) => ({
+ relevance: num?.value,
+ id: item?.value,
+ label: label?.value,
+ description: description?.value
+ }))
+ }
+
public static async search(
search: string,
options?: WikidataSearchoptions,
@@ -195,39 +250,28 @@ export default class Wikidata {
public static async searchAndFetch(
search: string,
- options?: WikidataSearchoptions
+ options?: WikidataAdvancedSearchoptions
): Promise {
- const maxCount = options.maxCount
// We provide some padding to filter away invalid values
- options.maxCount = Math.ceil((options.maxCount ?? 20) * 1.5)
- const searchResults = await Wikidata.search(search, options)
- const maybeResponses = await Promise.all(searchResults.map(async r => {
- try {
- return await Wikidata.LoadWikidataEntry(r.id).AsPromise()
- } catch (e) {
- console.error(e)
- return undefined;
- }
- }))
- const responses = maybeResponses
- .map(r => r["success"])
- .filter(wd => {
- if (wd === undefined) {
- return false;
+ const searchResults = await Wikidata.searchAdvanced(search, options)
+ const maybeResponses = await Promise.all(
+ searchResults.map(async r => {
+ try {
+ console.log("Loading ", r.id)
+ return await Wikidata.LoadWikidataEntry(r.id).AsPromise()
+ } catch (e) {
+ console.error(e)
+ return undefined;
}
- if (wd.claims.get("P31" /*Instance of*/)?.has("Q4167410"/* Wikimedia Disambiguation page*/)) {
- return false;
- }
- return true;
- })
- responses.splice(maxCount, responses.length - maxCount)
- return responses
+ }))
+ return Utils.NoNull(maybeResponses.map(r => r["success"]))
}
/**
* Gets the 'key' segment from a URL
- *
+ *
* Wikidata.ExtractKey("https://www.wikidata.org/wiki/Lexeme:L614072") // => "L614072"
+ * Wikidata.ExtractKey("http://www.wikidata.org/entity/Q55008046") // => "Q55008046"
*/
public static ExtractKey(value: string | number): string {
if (typeof value === "number") {
@@ -271,6 +315,35 @@ export default class Wikidata {
return undefined;
}
+ /**
+ * Converts 'Q123' into 123, returns undefined if invalid
+ *
+ * Wikidata.QIdToNumber("Q123") // => 123
+ * Wikidata.QIdToNumber(" Q123 ") // => 123
+ * Wikidata.QIdToNumber(" X123 ") // => undefined
+ * Wikidata.QIdToNumber(" Q123X ") // => undefined
+ * Wikidata.QIdToNumber(undefined) // => undefined
+ * Wikidata.QIdToNumber(123) // => 123
+ */
+ public static QIdToNumber(q: string | number): number | undefined {
+ if(q === undefined || q === null){
+ return
+ }
+ if(typeof q === "number"){
+ return q
+ }
+ q = q.trim()
+ if (!q.startsWith("Q")) {
+ return
+ }
+ q = q.substr(1)
+ const n = Number(q)
+ if (isNaN(n)) {
+ return
+ }
+ return n
+ }
+
public static IdToArticle(id: string) {
if (id.startsWith("Q")) {
return "https://wikidata.org/wiki/" + id
@@ -305,4 +378,4 @@ export default class Wikidata {
return WikidataResponse.fromJson(response)
}
-}
\ No newline at end of file
+}
diff --git a/Models/Constants.ts b/Models/Constants.ts
index 431aacf2ce..e829a39de0 100644
--- a/Models/Constants.ts
+++ b/Models/Constants.ts
@@ -2,7 +2,7 @@ import {Utils} from "../Utils";
export default class Constants {
- public static vNumber = "0.19.0-alpha";
+ public static vNumber = "0.18.2";
public static ImgurApiKey = '7070e7167f0a25a'
public static readonly mapillary_client_token_v4 = "MLY|4441509239301885|b40ad2d3ea105435bd40c7e76993ae85"
diff --git a/Models/ThemeConfig/Conversion/Conversion.ts b/Models/ThemeConfig/Conversion/Conversion.ts
index 3d9aa1183a..5b6b72c008 100644
--- a/Models/ThemeConfig/Conversion/Conversion.ts
+++ b/Models/ThemeConfig/Conversion/Conversion.ts
@@ -216,13 +216,18 @@ export class Fuse extends DesugaringStep {
const information = []
for (let i = 0; i < this.steps.length; i++) {
const step = this.steps[i];
- let r = step.convert(json, "While running step " + step.name + ": " + context)
- errors.push(...r.errors ?? [])
- warnings.push(...r.warnings ?? [])
- information.push(...r.information ?? [])
- json = r.result
- if (errors.length > 0) {
- break;
+ try{
+ let r = step.convert(json, "While running step " + step.name + ": " + context)
+ errors.push(...r.errors ?? [])
+ warnings.push(...r.warnings ?? [])
+ information.push(...r.information ?? [])
+ json = r.result
+ if (errors.length > 0) {
+ break;
+ }
+ }catch(e){
+ console.error("Step "+step.name+" failed due to "+e);
+ throw e
}
}
return {
diff --git a/Models/ThemeConfig/Conversion/PrepareTheme.ts b/Models/ThemeConfig/Conversion/PrepareTheme.ts
index 7c1aa17211..8a9877e108 100644
--- a/Models/ThemeConfig/Conversion/PrepareTheme.ts
+++ b/Models/ThemeConfig/Conversion/PrepareTheme.ts
@@ -479,6 +479,7 @@ export class PrepareTheme extends Fuse {
}) {
super(
"Fully prepares and expands a theme",
+
new AddContextToTransltionsInLayout(),
new PreparePersonalTheme(state),
new WarnForUnsubstitutedLayersInTheme(),
diff --git a/Models/ThemeConfig/Conversion/Validation.ts b/Models/ThemeConfig/Conversion/Validation.ts
index 4bdf9c415c..b0c121953b 100644
--- a/Models/ThemeConfig/Conversion/Validation.ts
+++ b/Models/ThemeConfig/Conversion/Validation.ts
@@ -68,7 +68,7 @@ class ValidateTheme extends DesugaringStep {
const warnings = []
const information = []
- const theme = new LayoutConfig(json, true, "test")
+ const theme = new LayoutConfig(json, true)
{
// Legacy format checks
@@ -217,12 +217,17 @@ class MiscThemeChecks extends DesugaringStep{
convert(json: LayoutConfigJson, context: string): { result: LayoutConfigJson; errors?: string[]; warnings?: string[]; information?: string[] } {
const warnings = []
+ const errors = []
+ if(json.id !== "personal" && (json.layers === undefined || json.layers.length === 0)){
+ errors.push("The theme "+json.id+" has no 'layers' defined ("+context+")")
+ }
if(json.socialImage === ""){
warnings.push("Social image for theme "+json.id+" is the emtpy string")
}
return {
result :json,
- warnings
+ warnings,
+ errors
};
}
}
@@ -231,8 +236,8 @@ export class PrevalidateTheme extends Fuse {
constructor() {
super("Various consistency checks on the raw JSON",
- new OverrideShadowingCheck(),
- new MiscThemeChecks()
+ new MiscThemeChecks(),
+ new OverrideShadowingCheck()
);
}
diff --git a/Models/ThemeConfig/Json/PointRenderingConfigJson.ts b/Models/ThemeConfig/Json/PointRenderingConfigJson.ts
index 5170f89534..c9a4abc9d1 100644
--- a/Models/ThemeConfig/Json/PointRenderingConfigJson.ts
+++ b/Models/ThemeConfig/Json/PointRenderingConfigJson.ts
@@ -13,9 +13,10 @@ export default interface PointRenderingConfigJson {
/**
* All the locations that this point should be rendered at.
- * Using `location: ["point", "centroid"] will always render centerpoint
+ * Using `location: ["point", "centroid"] will always render centerpoint.
+ * 'projected_centerpoint' will show an item on the line itself, near the middle of the line. (LineStrings only)
*/
- location: ("point" | "centroid" | "start" | "end" | string)[]
+ location: ("point" | "centroid" | "start" | "end" | "projected_centerpoint" | string)[]
/**
* The icon for an element.
diff --git a/Models/ThemeConfig/Json/UnitConfigJson.ts b/Models/ThemeConfig/Json/UnitConfigJson.ts
index 5eba5eaf49..bde2683b23 100644
--- a/Models/ThemeConfig/Json/UnitConfigJson.ts
+++ b/Models/ThemeConfig/Json/UnitConfigJson.ts
@@ -18,9 +18,12 @@ export default interface UnitConfigJson {
export interface ApplicableUnitJson {
/**
- * The canonical value which will be added to the text.
+ * The canonical value which will be added to the value in OSM.
* e.g. "m" for meters
- * If the user inputs '42', the canonical value will be added and it'll become '42m'
+ * If the user inputs '42', the canonical value will be added and it'll become '42m'.
+ *
+ * Important: often, _no_ canonical values are expected, e.g. in the case of 'maxspeed' where 'km/h' is the default.
+ * In this case, an empty string should be used
*/
canonicalDenomination: string,
/**
diff --git a/Models/ThemeConfig/LayerConfig.ts b/Models/ThemeConfig/LayerConfig.ts
index b7863b4274..ade6d6b6df 100644
--- a/Models/ThemeConfig/LayerConfig.ts
+++ b/Models/ThemeConfig/LayerConfig.ts
@@ -134,6 +134,9 @@ export default class LayerConfig extends WithContextLoader {
this.allowSplit = json.allowSplit ?? false;
this.name = Translations.T(json.name, translationContext + ".name");
+ if(json.units!==undefined && !Array.isArray(json.units)){
+ throw "At "+context+".units: the 'units'-section should be a list; you probably have an object there"
+ }
this.units = (json.units ?? []).map(((unitJson, i) => Unit.fromJson(unitJson, `${context}.unit[${i}]`)))
if (json.description !== undefined) {
diff --git a/Models/ThemeConfig/LayoutConfig.ts b/Models/ThemeConfig/LayoutConfig.ts
index 402f288929..2d8598f6af 100644
--- a/Models/ThemeConfig/LayoutConfig.ts
+++ b/Models/ThemeConfig/LayoutConfig.ts
@@ -56,9 +56,17 @@ export default class LayoutConfig {
public readonly usedImages: string[]
public readonly extraLink?: ExtraLinkConfig
- constructor(json: LayoutConfigJson, official = true, context?: string) {
+ public readonly definedAtUrl? : string;
+ public readonly definitionRaw?: string;
+
+ constructor(json: LayoutConfigJson, official = true,options?: {
+ definedAtUrl?: string,
+ definitionRaw?: string
+ }) {
this.official = official;
this.id = json.id;
+ this.definedAtUrl = options?.definedAtUrl
+ this.definitionRaw = options?.definitionRaw
if (official) {
if (json.id.toLowerCase() !== json.id) {
throw "The id of a theme should be lowercase: " + json.id
@@ -67,11 +75,7 @@ export default class LayoutConfig {
throw "The id of a theme should match [a-z0-9-_]*: " + json.id
}
}
- if(context === undefined){
- context = this.id
- }else{
- context = context + "." + this.id;
- }
+ const context = this.id
this.maintainer = json.maintainer;
this.credits = json.credits;
this.version = json.version;
diff --git a/Models/ThemeConfig/PointRenderingConfig.ts b/Models/ThemeConfig/PointRenderingConfig.ts
index 0a02149b84..39dcec1563 100644
--- a/Models/ThemeConfig/PointRenderingConfig.ts
+++ b/Models/ThemeConfig/PointRenderingConfig.ts
@@ -15,8 +15,8 @@ import {VariableUiElement} from "../../UI/Base/VariableUIElement";
export default class PointRenderingConfig extends WithContextLoader {
- private static readonly allowed_location_codes = new Set(["point", "centroid", "start", "end"])
- public readonly location: Set<"point" | "centroid" | "start" | "end" | string>
+ private static readonly allowed_location_codes = new Set(["point", "centroid", "start", "end","projected_centerpoint"])
+ public readonly location: Set<"point" | "centroid" | "start" | "end" | "projected_centerpoint" | string>
public readonly icon: TagRenderingConfig;
public readonly iconBadges: { if: TagsFilter; then: TagRenderingConfig }[];
diff --git a/UI/BigComponents/AddNewMarker.ts b/UI/BigComponents/AddNewMarker.ts
index 9bd89e3f53..479098beab 100644
--- a/UI/BigComponents/AddNewMarker.ts
+++ b/UI/BigComponents/AddNewMarker.ts
@@ -14,7 +14,7 @@ export default class AddNewMarker extends Combine {
let last = undefined;
for (const filteredLayer of filteredLayers) {
const layer = filteredLayer.layerDef;
- if(layer.name === undefined){
+ if(layer.name === undefined && !filteredLayer.isDisplayed.data){
continue
}
for (const preset of filteredLayer.layerDef.presets) {
diff --git a/UI/BigComponents/LicensePicker.ts b/UI/BigComponents/LicensePicker.ts
index 19c608141a..619aad54a6 100644
--- a/UI/BigComponents/LicensePicker.ts
+++ b/UI/BigComponents/LicensePicker.ts
@@ -17,7 +17,10 @@ export default class LicensePicker extends DropDown {
{value: LicensePicker.ccbysa, shown: Translations.t.image.ccbs.Clone()},
{value: LicensePicker.ccby, shown: Translations.t.image.ccb.Clone()}
],
- state?.osmConnection?.GetPreference("pictures-license") ?? new UIEventSource("CC0")
+ state?.osmConnection?.GetPreference("pictures-license") ?? new UIEventSource("CC0"),
+ {
+ select_class:"w-min bg-indigo-100 p-1 rounded hover:bg-indigo-200"
+ }
)
this.SetClass("flex flex-col sm:flex-row").SetStyle("float:left");
}
diff --git a/UI/BigComponents/ShareScreen.ts b/UI/BigComponents/ShareScreen.ts
index a550627792..de96839bfd 100644
--- a/UI/BigComponents/ShareScreen.ts
+++ b/UI/BigComponents/ShareScreen.ts
@@ -14,6 +14,8 @@ import BaseLayer from "../../Models/BaseLayer";
import FilteredLayer from "../../Models/FilteredLayer";
import {InputElement} from "../Input/InputElement";
import CheckBoxes, {CheckBox} from "../Input/Checkboxes";
+import {SubtleButton} from "../Base/SubtleButton";
+import LZString from "lz-string";
export default class ShareScreen extends Combine {
@@ -24,14 +26,6 @@ export default class ShareScreen extends Combine {
const optionCheckboxes: InputElement[] = []
const optionParts: (UIEventSource)[] = [];
- function check() {
- return Svg.checkmark_svg().SetStyle("width: 1.5em; display:inline-block;");
- }
-
- function nocheck() {
- return Svg.no_checkmark_svg().SetStyle("width: 1.5em; display: inline-block;");
- }
-
const includeLocation = new CheckBox(tr.fsIncludeCurrentLocation, true)
optionCheckboxes.push(includeLocation);
@@ -49,6 +43,7 @@ export default class ShareScreen extends Combine {
} else {
return null;
}
+
}, [currentLocation]));
@@ -119,6 +114,9 @@ export default class ShareScreen extends Combine {
}
+ if(layout.definitionRaw !== undefined){
+ optionParts.push(new UIEventSource("userlayout="+(layout.definedAtUrl ?? layout.id)))
+ }
const options = new Combine(optionCheckboxes).SetClass("flex flex-col")
const url = (currentLocation ?? new UIEventSource(undefined)).map(() => {
@@ -126,13 +124,21 @@ export default class ShareScreen extends Combine {
const host = window.location.host;
let path = window.location.pathname;
path = path.substr(0, path.lastIndexOf("/"));
- let literalText = `https://${host}${path}/${layout.id.toLowerCase()}`
+ let id = layout.id.toLowerCase()
+ if(layout.definitionRaw !== undefined){
+ id="theme.html"
+ }
+ let literalText = `https://${host}${path}/${id}`
+ let hash = ""
+ if(layout.definedAtUrl === undefined && layout.definitionRaw !== undefined){
+ hash = "#"+ LZString.compressToBase64( Utils.MinifyJSON(layout.definitionRaw))
+ }
const parts = Utils.NoEmpty(Utils.NoNull(optionParts.map((eventSource) => eventSource.data)));
if (parts.length === 0) {
- return literalText;
+ return literalText + hash;
}
- return literalText + "?" + parts.join("&");
+ return literalText + "?" + parts.join("&") + hash;
}, optionParts);
@@ -184,13 +190,27 @@ export default class ShareScreen extends Combine {
});
+
+ let downloadThemeConfig: BaseUIElement = undefined;
+ if(layout.definitionRaw !== undefined){
+ downloadThemeConfig = new SubtleButton(Svg.download_svg(), new Combine([
+ tr.downloadCustomTheme,
+ tr.downloadCustomThemeHelp.SetClass("subtle")
+ ]).onClick(() => {
+ Utils.offerContentsAsDownloadableFile(layout.definitionRaw, layout.id+".mapcomplete-theme-definition.json", {
+ mimetype:"application/json"
+ })
+ })
+ .SetClass("flex flex-col"))
+ }
super([
- tr.intro.Clone(),
+ tr.intro,
link,
new VariableUiElement(linkStatus),
- tr.addToHomeScreen.Clone(),
- tr.embedIntro.Clone(),
+ downloadThemeConfig,
+ tr.addToHomeScreen,
+ tr.embedIntro,
options,
iframeCode,
])
diff --git a/UI/BigComponents/SimpleAddUI.ts b/UI/BigComponents/SimpleAddUI.ts
index 2e8371a0a9..9310928638 100644
--- a/UI/BigComponents/SimpleAddUI.ts
+++ b/UI/BigComponents/SimpleAddUI.ts
@@ -191,7 +191,7 @@ export default class SimpleAddUI extends Toggle {
preset.icon(),
new Combine([
title.SetClass("font-bold"),
- Translations.WT(preset.description)?.FirstSentence()
+ preset.description?.FirstSentence()
]).SetClass("flex flex-col")
)
}
@@ -208,15 +208,20 @@ export default class SimpleAddUI extends Toggle {
const allButtons = [];
for (const layer of state.filteredLayers.data) {
- if (layer.isDisplayed.data === false && !state.featureSwitchFilter.data) {
- // The layer is not displayed and we cannot enable the layer control -> we skip
- continue;
+ if (layer.isDisplayed.data === false) {
+ // The layer is not displayed...
+ if(!state.featureSwitchFilter.data){
+ // ...and we cannot enable the layer control -> we skip, as these presets can never be shown anyway
+ continue;
+ }
+
+ if (layer.layerDef.name === undefined) {
+ // this layer can never be toggled on in any case, so we skip the presets
+ continue;
+ }
}
- if (layer.layerDef.name === undefined) {
- // this is a parlty hidden layer
- continue;
- }
+
const presets = layer.layerDef.presets;
for (const preset of presets) {
diff --git a/UI/Image/ImageUploadFlow.ts b/UI/Image/ImageUploadFlow.ts
index 743b1b2c28..47e775289b 100644
--- a/UI/Image/ImageUploadFlow.ts
+++ b/UI/Image/ImageUploadFlow.ts
@@ -15,6 +15,7 @@ import {VariableUiElement} from "../Base/VariableUIElement";
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
import {OsmConnection} from "../../Logic/Osm/OsmConnection";
import {Changes} from "../../Logic/Osm/Changes";
+import Loading from "../Base/Loading";
export class ImageUploadFlow extends Toggle {
@@ -138,16 +139,16 @@ export class ImageUploadFlow extends Toggle {
if (l == 0) {
return undefined
}
- return t.uploadFailed.Clone().SetClass("alert");
+ return new Loading(t.uploadFailed).SetClass("alert");
})),
new VariableUiElement(uploadedCount.map(l => {
if (l == 0) {
return undefined;
}
if (l == 1) {
- return t.uploadDone.Clone().SetClass("thanks");
+ return t.uploadDone.Clone().SetClass("thanks block");
}
- return t.uploadMultipleDone.Subs({count: l}).SetClass("thanks")
+ return t.uploadMultipleDone.Subs({count: l}).SetClass("thanks block")
})),
fileSelector,
diff --git a/UI/ImportFlow/ConfirmProcess.ts b/UI/ImportFlow/ConfirmProcess.ts
index d7168a86d3..c8e3f570e8 100644
--- a/UI/ImportFlow/ConfirmProcess.ts
+++ b/UI/ImportFlow/ConfirmProcess.ts
@@ -13,19 +13,20 @@ export class ConfirmProcess extends Combine implements FlowStep<{ features: any[
constructor(v: { features: any[], theme: string }) {
const t = Translations.t.importHelper.confirmProcess;
- const toConfirm = [
+ const elements = [
new Link(t.readImportGuidelines, "https://wiki.openstreetmap.org/wiki/Import_guidelines", true),
t.contactedCommunity,
t.licenseIsCompatible,
t.wikipageIsMade
- ];
+ ]
+ const toConfirm = new CheckBoxes(elements);
super([
new Title(t.titleLong),
- new CheckBoxes(toConfirm),
+ toConfirm,
]);
this.SetClass("link-underline")
- this.IsValid = new CheckBoxes(toConfirm).GetValue().map(selected => toConfirm.length == selected.length)
+ this.IsValid = toConfirm.GetValue().map(selected => elements.length == selected.length)
this.Value = new UIEventSource<{ features: any[], theme: string }>(v)
}
}
\ No newline at end of file
diff --git a/UI/ImportFlow/ConflationChecker.ts b/UI/ImportFlow/ConflationChecker.ts
index 0f01ce3c96..3ae2103f8b 100644
--- a/UI/ImportFlow/ConflationChecker.ts
+++ b/UI/ImportFlow/ConflationChecker.ts
@@ -47,6 +47,27 @@ export default class ConflationChecker extends Combine implements FlowStep<{ fea
const toImport: {features: any[]} = params;
let overpassStatus = new UIEventSource<{ error: string } | "running" | "success" | "idle" | "cached">("idle")
const cacheAge = new UIEventSource(undefined);
+
+
+ function loadDataFromOverpass(){
+ // Load the data!
+ const url = Constants.defaultOverpassUrls[1]
+ const relationTracker = new RelationsTracker()
+ const overpass = new Overpass(params.layer.source.osmTags, [], url, new UIEventSource(180), relationTracker, true)
+ console.log("Loading from overpass!")
+ overpassStatus.setData("running")
+ overpass.queryGeoJson(bbox).then(
+ ([data, date]) => {
+ console.log("Received overpass-data: ", data.features.length, "features are loaded at ", date);
+ overpassStatus.setData("success")
+ fromLocalStorage.setData([data, date])
+ },
+ (error) => {
+ overpassStatus.setData({error})
+ })
+ }
+
+
const fromLocalStorage = IdbLocalStorage.Get<[any, Date]>("importer-overpass-cache-" + layer.id, {
whenLoaded: (v) => {
@@ -63,22 +84,7 @@ export default class ConflationChecker extends Combine implements FlowStep<{ fea
}
cacheAge.setData(-1)
}
- // Load the data!
- const url = Constants.defaultOverpassUrls[1]
- const relationTracker = new RelationsTracker()
- const overpass = new Overpass(params.layer.source.osmTags, [], url, new UIEventSource(180), relationTracker, true)
- console.log("Loading from overpass!")
- overpassStatus.setData("running")
- overpass.queryGeoJson(bbox).then(
- ([data, date]) => {
- console.log("Received overpass-data: ", data.features.length, "features are loaded at ", date);
- overpassStatus.setData("success")
- fromLocalStorage.setData([data, date])
- },
- (error) => {
- overpassStatus.setData({error})
- })
-
+ loadDataFromOverpass()
}
});
@@ -166,7 +172,7 @@ export default class ConflationChecker extends Combine implements FlowStep<{ fea
return osmData.features.filter(f =>
toImport.features.some(imp =>
maxDist >= GeoOperations.distanceBetween(imp.geometry.coordinates, GeoOperations.centerpointCoordinates(f))))
- }, [nearbyCutoff.GetValue()]), false);
+ }, [nearbyCutoff.GetValue().stabilized(500)]), false);
const paritionedImport = ImportUtils.partitionFeaturesIfNearby(toImport, geojson, nearbyCutoff.GetValue().map(Number));
// Featuresource showing OSM-features which are nearby a toImport-feature
@@ -211,13 +217,17 @@ export default class ConflationChecker extends Combine implements FlowStep<{ fea
if (age < 0) {
return t.cacheExpired
}
- return t.loadedDataAge.Subs({age: Utils.toHumanTime(age)})
+ return new Combine([t.loadedDataAge.Subs({age: Utils.toHumanTime(age)}),
+ new SubtleButton(Svg.reload_svg().SetClass("h-8"), t.reloadTheCache)
+ .onClick(loadDataFromOverpass)
+ .SetClass("h-12")
+ ])
})),
new Title(t.titleLive),
t.importCandidatesCount.Subs({count:toImport.features.length }),
new VariableUiElement(geojson.map(geojson => {
- if(geojson?.features?.length === undefined && geojson?.features?.length === 0){
+ if(geojson?.features?.length === undefined || geojson?.features?.length === 0){
return t.nothingLoaded.Subs(layer).SetClass("alert")
}
return new Combine([
@@ -233,7 +243,7 @@ export default class ConflationChecker extends Combine implements FlowStep<{ fea
new Combine([t.mapShowingNearbyIntro, nearbyCutoff]).SetClass("flex"),
new VariableUiElement(toImportWithNearby.features.map(feats =>
t.nearbyWarn.Subs({count: feats.length}).SetClass("alert"))),
- ,t.setRangeToZero,
+ t.setRangeToZero,
matchedFeaturesMap]).SetClass("flex flex-col")
super([
@@ -246,7 +256,7 @@ export default class ConflationChecker extends Combine implements FlowStep<{ fea
return new Loading(t.states.running)
}
if (d["error"] !== undefined) {
- return t.states.error.Subs(d).SetClass("alert")
+ return t.states.error.Subs({error: d["error"]}).SetClass("alert")
}
if (d === "cached") {
diff --git a/UI/ImportFlow/CreateNotes.ts b/UI/ImportFlow/CreateNotes.ts
index 8b8f1ffc40..249be836ec 100644
--- a/UI/ImportFlow/CreateNotes.ts
+++ b/UI/ImportFlow/CreateNotes.ts
@@ -9,12 +9,14 @@ import {FixedUiElement} from "../Base/FixedUiElement";
import {SubtleButton} from "../Base/SubtleButton";
import Svg from "../../Svg";
import Translations from "../i18n/Translations";
+import {Translation} from "../i18n/Translation";
export class CreateNotes extends Combine {
- public static createNoteContents(feature: {properties: any, geometry: {coordinates: [number,number]}},
- options: {wikilink: string; intro: string; source: string, theme: string }
- ): string[]{
+
+ public static createNoteContentsUi(feature: {properties: any, geometry: {coordinates: [number,number]}},
+ options: {wikilink: string; intro: string; source: string, theme: string }
+ ): (Translation | string)[]{
const src = feature.properties["source"] ?? feature.properties["src"] ?? options.source
delete feature.properties["source"]
delete feature.properties["src"]
@@ -41,14 +43,26 @@ export class CreateNotes extends Combine {
return [
options.intro,
extraNote,
- note.datasource.Subs({source: src}).txt,
- note.wikilink.Subs(options).txt,
+ note.datasource.Subs({source: src}),
+ note.wikilink.Subs(options),
'',
- note.importEasily.txt,
+ note.importEasily,
`https://mapcomplete.osm.be/${options.theme}.html?z=18&lat=${lat}&lon=${lon}#import`,
...tags]
}
+ public static createNoteContents(feature: {properties: any, geometry: {coordinates: [number,number]}},
+ options: {wikilink: string; intro: string; source: string, theme: string }
+ ): string[]{
+ return CreateNotes.createNoteContentsUi(feature, options).map(trOrStr => {
+ if(typeof trOrStr === "string"){
+ return trOrStr
+ }
+ return trOrStr.txt
+ })
+ }
+
+
constructor(state: { osmConnection: OsmConnection }, v: { features: any[]; wikilink: string; intro: string; source: string, theme: string }) {
const t = Translations.t.importHelper.createNotes;
const createdNotes: UIEventSource = new UIEventSource([])
@@ -83,7 +97,7 @@ export class CreateNotes extends Combine {
)))),
new Combine([
Svg.party_svg().SetClass("w-24"),
- t.done.Subs(v.features.length).SetClass("thanks"),
+ t.done.Subs({count: v.features.length}).SetClass("thanks"),
new SubtleButton(Svg.note_svg(),
t.openImportViewer , {
url: "import_viewer.html"
diff --git a/UI/ImportFlow/ImportViewerGui.ts b/UI/ImportFlow/ImportViewerGui.ts
index 2c427bd9c5..779214cd41 100644
--- a/UI/ImportFlow/ImportViewerGui.ts
+++ b/UI/ImportFlow/ImportViewerGui.ts
@@ -44,6 +44,47 @@ interface NoteState {
status: "imported" | "already_mapped" | "invalid" | "closed" | "not_found" | "open" | "has_comments"
}
+class DownloadStatisticsButton extends SubtleButton {
+ constructor(states: NoteState[][]) {
+ super(Svg.statistics_svg(), "Download statistics");
+ this.onClick(() => {
+
+ const st: NoteState[] = [].concat(...states)
+
+ const fields = [
+ "id",
+ "status",
+ "theme",
+ "date_created",
+ "date_closed",
+ "days_open",
+ "intro",
+ "...comments"
+ ]
+ const values : string[][] = st.map(note => {
+
+
+ return [note.props.id+"",
+ note.status,
+ note.theme,
+ note.props.date_created?.substr(0, note.props.date_created.length - 3),
+ note.props.closed_at?.substr(0, note.props.closed_at.length - 3) ?? "",
+ JSON.stringify( note.intro),
+ ...note.props.comments.map(c => JSON.stringify(c.user)+": "+JSON.stringify(c.text))
+ ]
+ })
+
+ Utils.offerContentsAsDownloadableFile(
+ [fields, ...values].map(c => c.join(", ")).join("\n"),
+ "mapcomplete_import_notes_overview.csv",
+ {
+ mimetype: "text/csv"
+ }
+ )
+ })
+ }
+}
+
class MassAction extends Combine {
constructor(state: UserRelatedState, props: NoteProperties[]) {
const textField = ValidatedTextField.ForType("text").ConstructInputElement()
@@ -303,7 +344,9 @@ class ImportInspector extends VariableUiElement {
contents.push(accordeon)
const content = new Combine(contents)
return new LeftIndex(
- [new TableOfContents(content, {noTopLevel: true, maxDepth: 1}).SetClass("subtle")],
+ [new TableOfContents(content, {noTopLevel: true, maxDepth: 1}).SetClass("subtle"),
+ new DownloadStatisticsButton(perBatch)
+ ],
content
)
diff --git a/UI/ImportFlow/Introdution.ts b/UI/ImportFlow/Introdution.ts
index e46dd3c9ff..d800901965 100644
--- a/UI/ImportFlow/Introdution.ts
+++ b/UI/ImportFlow/Introdution.ts
@@ -4,13 +4,14 @@ import {UIEventSource} from "../../Logic/UIEventSource";
import Translations from "../i18n/Translations";
import Title from "../Base/Title";
import {CreateNotes} from "./CreateNotes";
+import {FixedUiElement} from "../Base/FixedUiElement";
export default class Introdution extends Combine implements FlowStep {
readonly IsValid: UIEventSource;
readonly Value: UIEventSource;
constructor() {
- const example = CreateNotes.createNoteContents({
+ const example = CreateNotes.createNoteContentsUi({
properties:{
"some_key":"some_value",
"note":"a note in the original dataset"
@@ -23,7 +24,7 @@ export default class Introdution extends Combine implements FlowStep {
intro: "There might be an XYZ here",
theme: "theme",
source: "source of the data"
- })
+ }).map(el => el === "" ? new FixedUiElement("").SetClass("block") : el)
super([
new Title(Translations.t.importHelper.introduction.title),
diff --git a/UI/Input/DropDown.ts b/UI/Input/DropDown.ts
index d7137a04bb..b9fb6a3d32 100644
--- a/UI/Input/DropDown.ts
+++ b/UI/Input/DropDown.ts
@@ -47,7 +47,7 @@ export class DropDown extends InputElement {
}
options = options ?? {}
- options.select_class = options.select_class ?? 'bg-indigo-100 p-1 rounded hover:bg-indigo-200'
+ options.select_class = options.select_class ?? 'w-full bg-indigo-100 p-1 rounded hover:bg-indigo-200'
{
diff --git a/UI/Input/ValidatedTextField.ts b/UI/Input/ValidatedTextField.ts
index 874e830bce..15fe25b7eb 100644
--- a/UI/Input/ValidatedTextField.ts
+++ b/UI/Input/ValidatedTextField.ts
@@ -250,13 +250,15 @@ class WikidataTextField extends TextFieldDef {
["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"],
+ ["instanceOf","A list of Q-identifier which indicates that the search results _must_ be an entity of this type, e.g. [`Q5`](https://www.wikidata.org/wiki/Q5) for humans"],
+ ["notInstanceof","A list of Q-identifiers which indicates that the search results _must not_ be an entity of this type, e.g. [`Q79007`](https://www.wikidata.org/wiki/Q79007) to filter away all streets from the search results"]
]
)])
]]),
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
-\`\`\`
+\`\`\`json
"freeform": {
"key": "name:etymology:wikidata",
"type": "wikidata",
@@ -269,11 +271,29 @@ class WikidataTextField extends TextFieldDef {
"path",
"square",
"plaza",
- ]
+ ],
+ "#": "Remove streets and parks from the search results:"
+ "notInstanceOf": ["Q79007","Q22698"]
}
+
]
}
-\`\`\``
+\`\`\`
+
+Another example is to search for species and trees:
+
+\`\`\`json
+ "freeform": {
+ "key": "species:wikidata",
+ "type": "wikidata",
+ "helperArgs": [
+ "species",
+ {
+ "instanceOf": [10884, 16521]
+ }]
+ }
+\`\`\`
+`
]));
}
@@ -304,9 +324,9 @@ class WikidataTextField extends TextFieldDef {
const args = inputHelperOptions.args ?? []
const searchKey = args[0] ?? "name"
- let searchFor = inputHelperOptions.feature?.properties[searchKey]?.toLowerCase()
+ let searchFor = (inputHelperOptions.feature?.properties[searchKey]?.toLowerCase() ?? "")
- const options = args[1]
+ const options: any = args[1]
if (searchFor !== undefined && options !== undefined) {
const prefixes = options["removePrefixes"]
const postfixes = options["removePostfixes"]
@@ -325,10 +345,18 @@ class WikidataTextField extends TextFieldDef {
}
}
+
+ let instanceOf : number[] = Utils.NoNull((options?.instanceOf ?? []).map(i => Wikidata.QIdToNumber(i)))
+ let notInstanceOf : number[] = Utils.NoNull((options?.notInstanceOf ?? []).map(i => Wikidata.QIdToNumber(i)))
+ console.log("Instance of", instanceOf)
+
+
return new WikidataSearchBox({
value: currentValue,
- searchText: new UIEventSource(searchFor)
+ searchText: new UIEventSource(searchFor),
+ instanceOf,
+ notInstanceOf
})
}
}
@@ -424,7 +452,7 @@ class UrlTextfieldDef extends TextFieldDef {
reformat(str: string): string {
try {
let url: URL
- str = str.toLowerCase()
+ // str = str.toLowerCase() // URLS are case sensitive. Lowercasing them might break some URLS. See #763
if (!str.startsWith("http://") && !str.startsWith("https://") && !str.startsWith("http:")) {
url = new URL("https://" + str)
} else {
diff --git a/UI/Popup/DeleteWizard.ts b/UI/Popup/DeleteWizard.ts
index ba289a2e24..1cfece179c 100644
--- a/UI/Popup/DeleteWizard.ts
+++ b/UI/Popup/DeleteWizard.ts
@@ -202,7 +202,7 @@ export default class DeleteWizard extends Toggle {
private static generateDeleteTagRenderingConfig(softDeletionTags: TagsFilter,
nonDeleteOptions: { if: TagsFilter; then: Translation }[],
extraDeleteReasons: { explanation: Translation; changesetMessage: string }[],
- currentTags: any) {
+ currentTags: any): TagRenderingConfig {
const t = Translations.t.delete
nonDeleteOptions = nonDeleteOptions ?? []
let softDeletionTagsStr = []
diff --git a/UI/Popup/ImportButton.ts b/UI/Popup/ImportButton.ts
index 4108c4e24c..8db42618de 100644
--- a/UI/Popup/ImportButton.ts
+++ b/UI/Popup/ImportButton.ts
@@ -194,7 +194,7 @@ ${Utils.special_visualizations_importRequirementDocs}
importFlow,
isImported
),
- t.zoomInMore.SetClass("alert"),
+ t.zoomInMore.SetClass("alert block"),
state.locationControl.map(l => l.zoom >= 18)
),
pleaseLoginButton,
@@ -613,7 +613,7 @@ export class ImportPointButton extends AbstractImportButton {
icon: () => new Img(args.icon),
layerToAddTo: state.filteredLayers.data.filter(l => l.layerDef.id === args.targetLayer)[0],
name: args.text,
- title: Translations.WT(args.text),
+ title: Translations.T(args.text),
preciseInput: preciseInputSpec, // must be explicitely assigned, if 'undefined' won't work otherwise
boundsFactor: 3
}
diff --git a/UI/Popup/MoveWizard.ts b/UI/Popup/MoveWizard.ts
index 6ed7d06575..b6eb6e7124 100644
--- a/UI/Popup/MoveWizard.ts
+++ b/UI/Popup/MoveWizard.ts
@@ -86,7 +86,7 @@ export default class MoveWizard extends Toggle {
moveReason.setData(reason)
moveButton = new SubtleButton(
reason.icon.SetStyle("height: 1.5rem; width: 1.5rem;"),
- Translations.WT(reason.invitingText)
+ Translations.T(reason.invitingText)
).onClick(() => {
currentStep.setData("pick_location")
})
diff --git a/UI/Popup/TagRenderingQuestion.ts b/UI/Popup/TagRenderingQuestion.ts
index 0348bccb9e..7e32b12638 100644
--- a/UI/Popup/TagRenderingQuestion.ts
+++ b/UI/Popup/TagRenderingQuestion.ts
@@ -11,7 +11,7 @@ import {SaveButton} from "./SaveButton";
import {VariableUiElement} from "../Base/VariableUIElement";
import Translations from "../i18n/Translations";
import {FixedUiElement} from "../Base/FixedUiElement";
-import {Translation} from "../i18n/Translation";
+import {Translation, TypedTranslation} from "../i18n/Translation";
import Constants from "../../Models/Constants";
import {SubstitutedTranslation} from "../SubstitutedTranslation";
import {TagsFilter} from "../../Logic/Tags/TagsFilter";
@@ -51,7 +51,7 @@ export default class TagRenderingQuestion extends Combine {
const applicableMappingsSrc =
UIEventSource.ListStabilized(tags.map(tags => {
- const applicableMappings: { if: TagsFilter, then: any, ifnot?: TagsFilter, addExtraTags: Tag[] }[] = []
+ const applicableMappings: { if: TagsFilter, icon?: string, then: TypedTranslation
OpenStreetMap es ese mapa. Los datos del mapa se pueden utilizar de forma gratuita (con atribución y publicación de cambios en esos datos). Además de eso, todos pueden agregar libremente nuevos datos y corregir errores. Este sitio web también usa OpenStreetMap. Todos los datos provienen de allí, y tus respuestas y correcciones también se añadirán allí.
Muchas personas y aplicaciones ya usan OpenStreetMap: Maps.me, OsmAnd, pero también los mapas de Facebook, Instagram, Apple y Bing son (en parte) impulsados por OpenStreetMap. Si cambias algo aquí, también se reflejará en esas aplicaciones, en su próxima actualización
sind hier vorhanden?",
+ "render": "Hier sind {socket:schuko} Stecker des Typs
Schuko-Stecker ohne Erdungsstift (CEE7/4 Typ F)
vorhanden"
+ },
+ "plugs-1": {
+ "question": "Wie viele Stecker des Typs
Europäischer Wandstecker mit Erdungsstift (CEE7/4 Typ E)
sind hier vorhanden?",
+ "render": "Hier sind {socket:typee} Stecker des Typs
Europäischer Wandstecker mit Erdungsstift (CEE7/4 Typ E)
vorhanden"
+ },
+ "voltage-12": {
+ "render": "
Tesla Supercharger (Destination) (Typ 2 mit Kabel von Tesla)
liefert {socket:tesla_destination:voltage} Volt"
+ },
+ "voltage-13": {
+ "mappings": {
+ "0": {
+ "then": "USB zum Aufladen von Handys und kleinen Elektrogeräten liefert 5 Volt"
+ }
+ },
+ "question": "Welche Spannung liefern die Stecker mit
USB zum Laden von Handys und kleinen Elektrogeräten
?",
+ "render": "
USB zum Aufladen von Telefonen und kleinen Elektrogeräten
liefert {socket:USB-A:voltage} Volt"
+ },
+ "voltage-14": {
+ "question": "Welche Spannung bieten die Stecker mit
Bosch Active Connect mit 3 Pins und Kabel
?"
}
}
},
@@ -2221,6 +2442,9 @@
"description": "Alle Objekte, die eine bekannte Namensherkunft haben",
"name": "Objekte mit Informationen zur Namensherkunft",
"tagRenderings": {
+ "etymology_multi_apply": {
+ "render": "{multi_apply(_same_name_ids, name:etymology:wikidata;name:etymology, Automatische Datenübernahme auf alle Segmente mit demselben Namen, true)}"
+ },
"simple etymology": {
"mappings": {
"0": {
@@ -2387,6 +2611,9 @@
},
"question": "Wenn Sie Ihr eigenes Behältnis mitbringen (z. B. einen Kochtopf und kleine Töpfe), wird es dann zum Verpacken Ihrer Bestellung verwendet? "
},
+ "friture-vegetarian": {
+ "question": "Hat dieser Frittenladen vegetarische Snacks?"
+ },
"halal (no friture)": {
"mappings": {
"0": {
@@ -2416,10 +2643,12 @@
"2": {
"then": "Schnellrestaurant"
}
- }
+ },
+ "render": "Restaurant"
}
},
"ghost_bike": {
+ "description": "Eine Ebene mit Gedenkstätten für Radfahrer, die bei Verkehrsunfällen ums Leben gekommen sind",
"name": "Geisterräder",
"presets": {
"0": {
@@ -2437,7 +2666,7 @@
"ghost_bike-name": {
"mappings": {
"0": {
- "then": "Auf dem Fahrrad ist kein Name angegeben"
+ "then": "Am Fahrrad ist kein Name angegeben"
}
},
"question": "An wen erinnert dieses Geisterrad? Bitte respektieren Sie die Privatsphäre - geben Sie den Namen nur an, wenn er weit verbreitet oder auf dem Fahrrad markiert ist. Den Familiennamen können Sie weglassen.",
@@ -2598,6 +2827,7 @@
}
},
"information_board": {
+ "description": "Eine Ebene mit touristischen, straßenseitigen Informationstafeln (z. B. mit Informationen über die Landschaft, ein Gebäude, ein Merkmal, eine Karte, ...)",
"name": "Informationstafeln",
"presets": {
"0": {
@@ -2663,6 +2893,9 @@
},
"1": {
"options": {
+ "0": {
+ "question": "Alle Naturschutzgebiete"
+ },
"1": {
"question": "Hunde dürfen frei herumlaufen"
},
@@ -2673,7 +2906,37 @@
}
},
"name": "Naturschutzgebiete",
+ "presets": {
+ "0": {
+ "description": "Ein fehlendes Naturschutzgebiet hinzufügen",
+ "title": "ein Naturschutzgebiet"
+ }
+ },
"tagRenderings": {
+ "Access tag": {
+ "mappings": {
+ "0": {
+ "then": "Öffentlich zugänglich"
+ },
+ "1": {
+ "then": "Nicht zugänglich"
+ },
+ "2": {
+ "then": "Nicht zugänglich, da dies ein privater Bereich ist"
+ },
+ "3": {
+ "then": "Zugänglich, obwohl es sich um ein privates Gebiet handelt"
+ },
+ "4": {
+ "then": "Nur mit einem Führer oder bei organisierten Aktivitäten zugänglich"
+ },
+ "5": {
+ "then": "Zugänglich gegen Gebühr"
+ }
+ },
+ "question": "Ist dieses Naturschutzgebiet für die Öffentlichkeit zugänglich?",
+ "render": "Zugang zu diesem Naturschutzgebiet: {access:description}"
+ },
"Curator": {
"question": "Wer ist der Verwalter dieses Naturschutzgebietes? Respektieren Sie die Privatsphäre - geben Sie nur dann einen Namen an, wenn dieser allgemein bekannt ist",
"render": "{curator} ist der Pfleger dieses Naturschutzgebietes"
@@ -2692,20 +2955,155 @@
},
"question": "Sind Hunde in diesem Naturschutzgebiet erlaubt?"
},
+ "Editable description": {
+ "question": "Gibt es zusätzliche Informationen?",
+ "render": "Zusätzliche Informationen: {description:0}"
+ },
"Email": {
"question": "An welche Email-Adresse kann man sich bei Fragen und Problemen zu diesem Naturschutzgebiet wenden? Respektieren Sie die Privatsphäre - geben Sie nur dann eine persönliche Email-Adresse an, wenn diese allgemein bekannt ist",
"render": "{email}"
},
+ "Name tag": {
+ "mappings": {
+ "0": {
+ "then": "Dieses Gebiet hat keinen Namen"
+ }
+ },
+ "question": "Wie heißt dieses Gebiet?",
+ "render": "Dieses Gebiet heißt {name}"
+ },
+ "Non-editable description": {
+ "render": "Zusätzliche Informationen: {description}"
+ },
+ "Operator tag": {
+ "mappings": {
+ "0": {
+ "then": "Betrieben von Natuurpunt"
+ },
+ "1": {
+ "then": "Betrieben von {operator}"
+ },
+ "2": {
+ "then": "Betrieben von Agentschap Natuur en Bos"
+ }
+ },
+ "question": "Wer betreibt dieses Gebiet?",
+ "render": "Betrieben von {operator}"
+ },
"Surface area": {
"render": "Grundfläche: {_surface:ha}ha"
},
"phone": {
"question": "Welche Telefonnummer kann man bei Fragen und Problemen zu diesem Naturschutzgebiet anrufen? Respektieren Sie die Privatsphäre - geben Sie nur eine Telefonnummer an, wenn diese allgemein bekannt ist"
}
+ },
+ "title": {
+ "render": "Naturschutzgebiet"
}
},
"note": {
- "name": "OpenStreetMap-Hinweise"
+ "filter": {
+ "0": {
+ "options": {
+ "0": {
+ "question": "Sollte {search} im ersten Kommentar erwähnen"
+ }
+ }
+ },
+ "1": {
+ "options": {
+ "0": {
+ "question": "Sollte nicht {search} im ersten Kommentar erwähnen"
+ }
+ }
+ },
+ "2": {
+ "options": {
+ "0": {
+ "question": "Geöffnet vom Mitwirkenden {search}"
+ }
+ }
+ },
+ "3": {
+ "options": {
+ "0": {
+ "question": "Nicht vom Mitwirkenden {search} geöffnet"
+ }
+ }
+ },
+ "4": {
+ "options": {
+ "0": {
+ "question": "Zuletzt bearbeitet vom Mitwirkenden {search}"
+ }
+ }
+ },
+ "5": {
+ "options": {
+ "0": {
+ "question": "Geöffnet nach {search}"
+ }
+ }
+ },
+ "6": {
+ "options": {
+ "0": {
+ "question": "Erstellt vor {search}"
+ }
+ }
+ },
+ "7": {
+ "options": {
+ "0": {
+ "question": "Erstellt nach {search}"
+ }
+ }
+ },
+ "8": {
+ "options": {
+ "0": {
+ "question": "Nur Notizen anzeigen, die von anonymen Mitwirkenden geöffnet wurden"
+ }
+ }
+ },
+ "9": {
+ "options": {
+ "0": {
+ "question": "Nur offene Notizen anzeigen"
+ }
+ }
+ },
+ "10": {
+ "options": {
+ "0": {
+ "question": "Importnotizen ausblenden"
+ }
+ }
+ }
+ },
+ "name": "OpenStreetMap-Hinweise",
+ "tagRenderings": {
+ "report-contributor": {
+ "render": "{_first_user} als Spam melden"
+ },
+ "report-note": {
+ "render": "Diese Notiz als Spam oder unangemessen melden"
+ }
+ },
+ "title": {
+ "mappings": {
+ "0": {
+ "then": "Geschlossene Notiz"
+ }
+ },
+ "render": "Notiz"
+ }
+ },
+ "note_import": {
+ "name": "Mögliche Bücherschränke",
+ "title": {
+ "render": "Mögliches Objekt"
+ }
},
"observation_tower": {
"description": "Türme zur Aussicht auf die umgebende Landschaft",
@@ -2728,6 +3126,28 @@
"question": "Wer betreibt diesen Turm?",
"render": "Betrieben von {operator}"
},
+ "access": {
+ "mappings": {
+ "0": {
+ "then": "Dieser Turm ist öffentlich zugänglich"
+ },
+ "1": {
+ "then": "Dieser Turm kann nur mit einem Führer besichtigt werden"
+ }
+ },
+ "question": "Kann dieser Turm besichtigt werden?"
+ },
+ "elevator": {
+ "mappings": {
+ "0": {
+ "then": "Dieser Turm verfügt über einen Aufzug, der die Besucher nach oben bringt"
+ },
+ "1": {
+ "then": "Dieser Turm hat keinen Aufzug"
+ }
+ },
+ "question": "Hat dieser Turm einen Aufzug?"
+ },
"name": {
"mappings": {
"0": {
@@ -2736,6 +3156,10 @@
},
"question": "Wie heißt dieser Turm?",
"render": "Der Name dieses Turms lautet {name}"
+ },
+ "step_count": {
+ "question": "Wie viele einzelne Stufen muss man erklimmen, um die Spitze des Turms zu erreichen?",
+ "render": "Dieser Turm hat {step_count} Stufen, um die Spitze zu erreichen"
}
},
"title": {
@@ -2757,7 +3181,16 @@
}
},
"parking": {
- "name": "Parkplätze"
+ "description": "Eine Ebene mit Parkplätzen",
+ "name": "Parkplätze",
+ "presets": {
+ "0": {
+ "title": "ein Parkplatz"
+ }
+ },
+ "title": {
+ "render": "Parkplatz"
+ }
},
"pedestrian_path": {
"name": "Fußgängerwege"
@@ -2816,6 +3249,9 @@
"0": {
"then": "Zugänglich für die Allgemeinheit"
},
+ "1": {
+ "then": "Dies ist ein gebührenpflichtiger Spielplatz"
+ },
"2": {
"then": "Nur für Kunden des Betreibers zugänglich"
},
@@ -3024,6 +3460,7 @@
}
},
"recycling": {
+ "description": "Eine Ebene mit Recyclingcontainern und -zentren",
"filter": {
"0": {
"options": {
@@ -3031,11 +3468,217 @@
"question": "Derzeit geöffnet"
}
}
+ },
+ "1": {
+ "options": {
+ "0": {
+ "question": "Alle Recyclingarten"
+ },
+ "1": {
+ "question": "Recycling von Batterien"
+ },
+ "2": {
+ "question": "Recycling von Getränkekartons"
+ },
+ "3": {
+ "question": "Recycling von Dosen"
+ },
+ "4": {
+ "question": "Recycling von Kleidung"
+ },
+ "5": {
+ "question": "Recycling von Speiseöl"
+ },
+ "6": {
+ "question": "Recycling von Motoröl"
+ },
+ "7": {
+ "question": "Recycling von Grünabfällen"
+ },
+ "8": {
+ "question": "Recycling von Glasflaschen"
+ },
+ "9": {
+ "question": "Recycling von Glas"
+ },
+ "10": {
+ "question": "Recycling von Zeitungen"
+ },
+ "11": {
+ "question": "Recycling von Papier"
+ },
+ "12": {
+ "question": "Recycling von Plastikflaschen"
+ },
+ "13": {
+ "question": "Recycling von Kunststoffverpackungen"
+ },
+ "14": {
+ "question": "Recycling von Kunststoffen"
+ },
+ "15": {
+ "question": "Recycling von Metallschrott"
+ },
+ "16": {
+ "question": "Recycling von Elektrokleingeräten"
+ },
+ "17": {
+ "question": "Recycling von Restabfällen"
+ }
+ }
}
},
- "name": "Recycling"
+ "name": "Recycling",
+ "presets": {
+ "0": {
+ "title": "ein Recyclingcontainer"
+ },
+ "1": {
+ "title": "ein Wertstoffhof"
+ }
+ },
+ "tagRenderings": {
+ "container-location": {
+ "mappings": {
+ "0": {
+ "then": "Dies ist ein Unterflurcontainer"
+ },
+ "1": {
+ "then": "Dieser Container befindet sich in einem Gebäude"
+ },
+ "2": {
+ "then": "Dieser Container befindet sich im Freien"
+ }
+ },
+ "question": "Wo befindet sich dieser Container?"
+ },
+ "opening_hours": {
+ "mappings": {
+ "0": {
+ "then": "24/7"
+ }
+ },
+ "question": "Wie sind die Öffnungszeiten dieser Recyclinganlage?"
+ },
+ "operator": {
+ "question": "Welches Unternehmen betreibt diese Recyclinganlage?",
+ "render": "Diese Recyclinganlage wird betrieben von {operator}"
+ },
+ "recycling-accepts": {
+ "mappings": {
+ "0": {
+ "then": "Batterien können hier recycelt werden"
+ },
+ "1": {
+ "then": "Getränkekartons können hier recycelt werden"
+ },
+ "2": {
+ "then": "Dosen können hier recycelt werden"
+ },
+ "3": {
+ "then": "Kleidung kann hier recycelt werden"
+ },
+ "4": {
+ "then": "Speiseöl kann hier recycelt werden"
+ },
+ "5": {
+ "then": "Motoröl kann hier recycelt werden"
+ },
+ "6": {
+ "then": "Grünabfälle können hier recycelt werden"
+ },
+ "7": {
+ "then": "Bio-Abfall kann hier recycelt werden"
+ },
+ "8": {
+ "then": "Glasflaschen können hier recycelt werden"
+ },
+ "9": {
+ "then": "Glas kann hier recycelt werden"
+ },
+ "10": {
+ "then": "Zeitungen können hier recycelt werden"
+ },
+ "11": {
+ "then": "Papier kann hier recycelt werden"
+ },
+ "12": {
+ "then": "Plastikflaschen können hier recycelt werden"
+ },
+ "13": {
+ "then": "Kunststoffverpackungen können hier recycelt werden"
+ },
+ "14": {
+ "then": "Kunststoff kann hier recycelt werden"
+ },
+ "15": {
+ "then": "Metallschrott kann hier recycelt werden"
+ },
+ "16": {
+ "then": "Schuhe können hier recycelt werden"
+ },
+ "17": {
+ "then": "Elektrokleingeräte können hier recycelt werden"
+ },
+ "18": {
+ "then": "Elektrokleingeräte können hier recycelt werden"
+ },
+ "19": {
+ "then": "Nadeln können hier recycelt werden"
+ },
+ "20": {
+ "then": "Restmüll kann hier recycelt werden"
+ }
+ },
+ "question": "Was kann hier recycelt werden?"
+ },
+ "recycling-centre-name": {
+ "mappings": {
+ "0": {
+ "then": "Dieser Wertstoffhof hat keinen bestimmten Namen"
+ }
+ },
+ "question": "Wie lautet der Name dieses Wertstoffhofs?",
+ "render": "Dieser Wertstoffhof heißt {name}"
+ },
+ "recycling-type": {
+ "mappings": {
+ "0": {
+ "then": "Dies ist ein Recycling-Container"
+ },
+ "1": {
+ "then": "Dies ist ein Wertstoffhof"
+ },
+ "2": {
+ "then": "Dies ist ein Abfallcontainer für Restmüll"
+ }
+ },
+ "question": "Um welche Recyclingeinrichtung handelt es sich?"
+ }
+ },
+ "title": {
+ "mappings": {
+ "0": {
+ "then": "Wertstoffhof"
+ },
+ "1": {
+ "then": "Wertstoffhof"
+ },
+ "2": {
+ "then": "Recyclingcontainer"
+ }
+ },
+ "render": "Recyclinganlage"
+ }
},
"shops": {
+ "deletion": {
+ "extraDeleteReasons": {
+ "0": {
+ "explanation": "{title()} wurde dauerhaft geschlossen"
+ }
+ }
+ },
"description": "Ein Geschäft",
"filter": {
"0": {
@@ -3062,13 +3705,15 @@
},
"tagRenderings": {
"shops-email": {
- "question": "Wie ist die Email-Adresse dieses Geschäfts?"
+ "question": "Wie ist die Email-Adresse dieses Geschäfts?",
+ "render": "{email}"
},
"shops-name": {
"question": "Wie ist der Name dieses Geschäfts?"
},
"shops-opening_hours": {
- "question": "Wie sind die Öffnungszeiten dieses Geschäfts?"
+ "question": "Wie sind die Öffnungszeiten dieses Geschäfts?",
+ "render": "{opening_hours_table(opening_hours)}"
},
"shops-phone": {
"question": "Wie ist die Telefonnummer?",
@@ -3119,6 +3764,7 @@
}
},
"slow_roads": {
+ "description": "Alle autofreien Straßen",
"tagRenderings": {
"slow_roads-surface": {
"mappings": {
@@ -3262,10 +3908,165 @@
}
},
"street_lamps": {
- "name": "Straßenlaternen"
+ "description": "Eine Ebene mit Straßenbeleuchtung",
+ "name": "Straßenlaternen",
+ "presets": {
+ "0": {
+ "title": "eine Straßenlaterne"
+ }
+ },
+ "tagRenderings": {
+ "colour": {
+ "mappings": {
+ "0": {
+ "then": "Diese Lampe strahlt weißes Licht aus"
+ },
+ "1": {
+ "then": "Diese Lampe strahlt grünes Licht aus"
+ },
+ "2": {
+ "then": "Diese Lampe strahlt orangefarbenes Licht aus"
+ }
+ },
+ "question": "Welche Lichtfarbe strahlt diese Lampe aus?",
+ "render": "Diese Lampe strahlt {light:colour} Licht aus"
+ },
+ "count": {
+ "mappings": {
+ "0": {
+ "then": "Diese Straßenlaterne hat 1 Leuchte"
+ },
+ "1": {
+ "then": "Diese Straßenlaterne hat 2 Leuchten"
+ }
+ },
+ "question": "Wie viele Leuchten hat diese Straßenlaterne?",
+ "render": "Diese Straßenlaterne hat {light:count} Leuchten"
+ },
+ "direction": {
+ "question": "Wohin leuchtet diese Straßenlaterne?",
+ "render": "Diese Straßenlaterne leuchtet in Richtung {light:direction}"
+ },
+ "lamp_mount": {
+ "mappings": {
+ "0": {
+ "then": "Diese Straßenlaterne sitzt auf einem geraden Mast"
+ },
+ "1": {
+ "then": "Diese Straßenlaterne sitzt am Ende eines gebogenen Mastes"
+ }
+ },
+ "question": "Wie ist diese Straßenlaterne am Mast befestigt?"
+ },
+ "lit": {
+ "mappings": {
+ "0": {
+ "then": "Diese Straßenlaterne leuchtet nachts"
+ },
+ "1": {
+ "then": "Diese Straßenlaterne leuchtet durchgehend"
+ },
+ "2": {
+ "then": "Diese Straßenlaterne leuchtet bewegungsgesteuert"
+ },
+ "3": {
+ "then": "Diese Straßenlaterne leuchtet bei Bedarf (z. B. mit einem Taster)"
+ }
+ },
+ "question": "Wann leuchtet diese Straßenlaterne?"
+ },
+ "method": {
+ "mappings": {
+ "0": {
+ "then": "Diese Straßenlaterne leuchtet elektrisch"
+ },
+ "1": {
+ "then": "Diese Straßenlaterne verwendet LEDs"
+ },
+ "2": {
+ "then": "Diese Straßenlaterne verwendet Glühlampenlicht"
+ },
+ "3": {
+ "then": "Diese Straßenlaterne verwendet Halogenlicht"
+ },
+ "4": {
+ "then": "Diese Straßenlaterne verwendet Entladungslampen (unbekannter Typ)"
+ },
+ "5": {
+ "then": "Diese Straßenlaterne verwendet eine Quecksilberdampflampe (leicht bläulich)"
+ },
+ "6": {
+ "then": "Diese Straßenlaterne verwendet Halogen-Metalldampflampen (hellweiß)"
+ },
+ "7": {
+ "then": "Diese Straßenlaterne verwendet Leuchtstoffröhren"
+ },
+ "8": {
+ "then": "Diese Straßenlaterne verwendet Natriumdampflampen (unbekannter Typ)"
+ },
+ "9": {
+ "then": "Diese Straßenlaterne verwendet Niederdruck-Natriumdampflampen (einfarbig orange)"
+ },
+ "10": {
+ "then": "Diese Straßenlaterne verwendet Hochdruck-Natriumdampflampen (orange mit weiß)"
+ },
+ "11": {
+ "then": "Diese Straßenlaterne wird mit Gas beleuchtet"
+ }
+ },
+ "question": "Mit welcher Art von Beleuchtung arbeitet diese Straßenlaterne?"
+ },
+ "ref": {
+ "question": "Wie lautet die Referenznummer dieser Straßenlaterne?",
+ "render": "Diese Straßenlaterne hat die Referenznummer {ref}"
+ },
+ "support": {
+ "mappings": {
+ "0": {
+ "then": "Diese Straßenlaterne ist an einem Kabel aufgehängt"
+ },
+ "1": {
+ "then": "Diese Straßenlaterne ist an einer Decke montiert"
+ },
+ "2": {
+ "then": "Diese Straßenlaterne ist im Boden montiert"
+ },
+ "3": {
+ "then": "Diese Straßenlaterne ist an einem kurzen Mast (< 1,5m) montiert"
+ },
+ "4": {
+ "then": "Diese Straßenlaterne ist an einem Mast montiert"
+ },
+ "5": {
+ "then": "Diese Straßenlaterne ist direkt an der Wand montiert"
+ },
+ "6": {
+ "then": "Diese Straßenlaterne ist mit einer Metallstange an der Wand montiert"
+ }
+ },
+ "question": "Wie ist diese Straßenlaterne befestigt?"
+ }
+ },
+ "title": {
+ "mappings": {
+ "0": {
+ "then": "Straßenlaterne {ref}"
+ }
+ },
+ "render": "Straßenlaterne"
+ }
},
"surveillance_camera": {
+ "description": "Diese Ebene zeigt die Überwachungskameras an und ermöglicht es, Informationen zu aktualisieren und neue Kameras hinzuzufügen",
"name": "Überwachungskameras",
+ "presets": {
+ "0": {
+ "title": "eine Überwachungskamera"
+ },
+ "1": {
+ "title": "eine an einer Wand montierte Überwachungskamera"
+ }
+ },
"tagRenderings": {
"Camera type: fixed; panning; dome": {
"mappings": {
@@ -3343,7 +4144,13 @@
"render": "Montageart: {camera:mount}"
},
"camera_direction": {
- "question": "In welche Himmelsrichtung ist diese Kamera ausgerichtet?"
+ "mappings": {
+ "0": {
+ "then": "filmt in Himmelsrichtung {direction}"
+ }
+ },
+ "question": "In welche Himmelsrichtung ist diese Kamera ausgerichtet?",
+ "render": "filmt in Himmelsrichtung {camera:direction}"
},
"is_indoor": {
"mappings": {
@@ -3365,6 +4172,7 @@
}
},
"toilet": {
+ "description": "Eine Ebene mit (öffentlichen) Toiletten",
"filter": {
"0": {
"options": {
@@ -3406,6 +4214,14 @@
}
},
"tagRenderings": {
+ "Opening-hours": {
+ "mappings": {
+ "0": {
+ "then": "Durchgehend geöffnet"
+ }
+ },
+ "question": "Wann sind diese Toiletten geöffnet?"
+ },
"toilet-access": {
"mappings": {
"0": {
@@ -3462,6 +4278,9 @@
},
"toilet-has-paper": {
"mappings": {
+ "0": {
+ "then": "Diese Toilette ist mit Toilettenpapier ausgestattet"
+ },
"1": {
"then": "Für diese Toilette müssen Sie Ihr eigenes Toilettenpapier mitbringen"
}
@@ -3551,6 +4370,7 @@
}
},
"tree_node": {
+ "description": "Eine Ebene, die Bäume zeigt",
"name": "Bäume",
"presets": {
"0": {
@@ -3592,9 +4412,15 @@
"3": {
"then": "Der Baum steht in einem Park oder ähnlichem (Friedhof, Schulgelände, ...)."
},
+ "4": {
+ "then": "Der Baum steht in einem Wohngarten."
+ },
"5": {
"then": "Dieser Baum steht entlang einer Straße."
},
+ "6": {
+ "then": "Der Baum steht in einem städtischen Gebiet."
+ },
"7": {
"then": "Dieser Baum steht außerhalb eines städtischen Gebiets."
}
@@ -3653,7 +4479,8 @@
"render": "Name: {name}"
},
"tree_node-ref:OnroerendErfgoed": {
- "question": "Wie lautet die Kennung der Onroerend Erfgoed Flanders?"
+ "question": "Wie lautet die Kennung der Onroerend Erfgoed Flanders?",
+ "render": " Onroerend Erfgoed Kennung: {ref:OnroerendErfgoed}"
},
"tree_node-wikidata": {
"question": "Was ist das passende Wikidata Element zu diesem Baum?",
@@ -3681,6 +4508,9 @@
"render": "Aussichtspunkt"
}
},
+ "village_green": {
+ "description": "Eine Ebene mit Dorfangern (kommunale Grünflächen, aber nicht wirklich Parks)"
+ },
"visitor_information_centre": {
"description": "Ein Besucherzentrum bietet Informationen über eine bestimmte Attraktion oder Sehenswürdigkeit, an der es sich befindet.",
"name": "Besucherinformationszentrum",
@@ -3693,6 +4523,12 @@
"render": "{name}"
}
},
+ "walls_and_buildings": {
+ "description": "Spezielle eingebaute Ebene, die alle Wände und Gebäude bereitstellt. Diese Ebene ist in Voreinstellungen für Objekte nützlich, die an Wänden platziert werden können (z. B. AEDs, Briefkästen, Eingänge, Adressen, Überwachungskameras, ...). Diese Ebene ist standardmäßig unsichtbar und kann vom Benutzer nicht umgeschaltet werden.",
+ "title": {
+ "render": "Wand oder Gebäude"
+ }
+ },
"waste_basket": {
"description": "Dies ist ein öffentlicher Abfalleimer, in den Sie Ihren Müll entsorgen können.",
"filter": {
@@ -3792,6 +4628,59 @@
"render": "Abfalleimer"
}
},
+ "waste_disposal": {
+ "description": "Entsorgungsbehälter, mittlerer bis großer Behälter zur Entsorgung von (Haushalts-)Abfällen",
+ "filter": {
+ "0": {
+ "options": {
+ "0": {
+ "question": "Nur öffentlich zugänglich"
+ }
+ }
+ }
+ },
+ "name": "Mülleimer",
+ "presets": {
+ "0": {
+ "description": "Mittlere bis große Mülltonne für die Entsorgung von (Haushalts-)Abfällen",
+ "title": "ein Abfalleimer"
+ }
+ },
+ "tagRenderings": {
+ "access": {
+ "mappings": {
+ "0": {
+ "then": "Dieser Behälter kann von jedem benutzt werden"
+ },
+ "1": {
+ "then": "Dieser Behälter ist privat"
+ },
+ "2": {
+ "then": "Diese Mülltonne ist nur für Anwohner"
+ }
+ },
+ "question": "Wer kann diese Mülltonne benutzen?",
+ "render": "Zugang: {access}"
+ },
+ "disposal-location": {
+ "mappings": {
+ "0": {
+ "then": "Dies ist ein unterirdischer Container"
+ },
+ "1": {
+ "then": "Dieser Container befindet sich in einem Gebäude"
+ },
+ "2": {
+ "then": "Dieser Container befindet sich im Freien"
+ }
+ },
+ "question": "Wo befindet sich dieser Container?"
+ }
+ },
+ "title": {
+ "render": "Abfallentsorgung"
+ }
+ },
"watermill": {
"name": "Wassermühle"
},
diff --git a/langs/layers/en.json b/langs/layers/en.json
index f66ce48feb..aaf6f81d83 100644
--- a/langs/layers/en.json
+++ b/langs/layers/en.json
@@ -3422,7 +3422,7 @@
"title": "a fastfood"
},
"2": {
- "description": "A fastfood-buisiness focused on french fries",
+ "description": "A fastfood-business focused on french fries",
"title": "a fries shop"
}
},
@@ -5454,6 +5454,9 @@
},
"question": "Is this a broadleaved or needleleaved tree?"
},
+ "tree-species-wikidata": {
+ "question": "What species is this tree?"
+ },
"tree_node-name": {
"mappings": {
"0": {
@@ -5670,6 +5673,7 @@
"name": "Watermill"
},
"windturbine": {
+ "description": "Modern windmills generating electricity",
"name": "wind turbine",
"presets": {
"0": {
diff --git a/langs/layers/es.json b/langs/layers/es.json
index 961033fe75..482a0f017e 100644
--- a/langs/layers/es.json
+++ b/langs/layers/es.json
@@ -12,16 +12,67 @@
"then": "Esta edificación no tiene número"
}
},
- "question": "¿Cuál es el número de esta casa?"
+ "question": "¿Cuál es el número de esta casa?",
+ "render": "La numeración de la casa es {addr:housenumber}"
},
"street": {
- "question": "¿En qué calle se encuentra esta dirección?"
+ "question": "¿En qué calle se encuentra esta dirección?",
+ "render": "La dirección está en la calle {addr:street}"
}
},
"title": {
"render": "Domicilio conocido"
}
},
+ "ambulancestation": {
+ "description": "Una estación de ambulancias es una zona para almacenar vehículos de ambulancia, equipamiento médico, equipos de protección personal y otros suministros médicos.",
+ "name": "Mapa de estaciones de ambulancias",
+ "presets": {
+ "0": {
+ "description": "Añadir una estación de ambulancias al mapa",
+ "title": "una estación de ambulancias"
+ }
+ },
+ "tagRenderings": {
+ "ambulance-agency": {
+ "question": "¿Qué agencia opera esta estación?",
+ "render": "Esta estación la opera {operator}."
+ },
+ "ambulance-name": {
+ "question": "¿Cual es el nombre de esta estación de ambulancias?",
+ "render": "Esta estación se llama {name}."
+ },
+ "ambulance-operator-type": {
+ "mappings": {
+ "0": {
+ "then": "La estación la opera el govierno."
+ },
+ "1": {
+ "then": "La estación la opera una organización basada en la comunidad o informal."
+ },
+ "2": {
+ "then": "La estación la opera un grupo formal de voluntarios."
+ },
+ "3": {
+ "then": "La estación es de gestión privada."
+ }
+ },
+ "question": "¿Como está clasificada la operadora de la estación?",
+ "render": "La operador a no es una entidad de tipo {operator:type}."
+ },
+ "ambulance-place": {
+ "question": "¿Dónde se encuentra la estación? (ej. nombre del barrio, pueblo o ciudad)",
+ "render": "Esta estación se encuentra en {addr:place}."
+ },
+ "ambulance-street": {
+ "question": " ¿Cual es el nombre de la calle en la que se encuentra la estación?",
+ "render": "Esta estación se encuentra al lado de una autovía llamada {addr:street}."
+ }
+ },
+ "title": {
+ "render": "Estación de Ambulancias"
+ }
+ },
"artwork": {
"description": "Diversas piezas de obras de arte",
"name": "Obras de arte",
@@ -31,6 +82,10 @@
}
},
"tagRenderings": {
+ "artwork-artist_name": {
+ "question": "¿Que artista creó esto?",
+ "render": "Creado por {artist_name}"
+ },
"artwork-artwork_type": {
"mappings": {
"0": {
@@ -51,15 +106,35 @@
"5": {
"then": "Busto"
},
+ "6": {
+ "then": "Piedra"
+ },
"7": {
"then": "Instalación"
},
"8": {
"then": "Grafiti"
+ },
+ "9": {
+ "then": "Relieve"
+ },
+ "10": {
+ "then": "Azulejo (azulejos decorativos españoles)"
+ },
+ "11": {
+ "then": "Cerámica"
}
},
"question": "¿Qué tipo de obra es esta pieza?",
"render": "Esta es un {artwork_type}"
+ },
+ "artwork-website": {
+ "question": "¿Hay un sitio web con más información sobre esta obra de arte?",
+ "render": "Más información en este sitio web"
+ },
+ "artwork-wikidata": {
+ "question": "¿Qué entrada de Wikidata se corresponde con esta obra de arte?",
+ "render": "Se corresponde con {wikidata}"
}
},
"title": {
@@ -75,6 +150,7 @@
"name": "Barreras",
"presets": {
"0": {
+ "description": "Un bolardo en la carretera",
"title": "una bolardo"
}
},
@@ -92,6 +168,35 @@
}
},
"question": "¿Qué tipo de bolardo es este?"
+ },
+ "Cycle barrier type": {
+ "question": "¿Qué tipo de barrera ciclista es esta?"
+ },
+ "MaxWidth": {
+ "question": "¿Cómo de ancho es el hueco dejado fuera de la barrera?",
+ "render": "Anchura máxima: {maxwidth:physical} m"
+ },
+ "Overlap (cyclebarrier)": {
+ "question": "¿Cuánto se solapan las barreras?",
+ "render": "Solapado: {overlap} m"
+ },
+ "Space between barrier (cyclebarrier)": {
+ "question": "¿Cuánto espacio hay entre las barreras (a lo largo de la longitud de la carretera)?",
+ "render": "Espacio entre barreras (a lo largo de la longitud de la carretera): {width:separation} m"
+ },
+ "Width of opening (cyclebarrier)": {
+ "question": "¿Cómo de año es la apertura más pequeña al lado de las barreras?",
+ "render": "Anchura de la apertura: {width:opening} m"
+ },
+ "barrier_type": {
+ "mappings": {
+ "0": {
+ "then": "Este es un único bolardo en la carretera"
+ },
+ "1": {
+ "then": "Esta es una barrera ciclista que ralentiza a los ciclistas"
+ }
+ }
}
},
"title": {
@@ -104,6 +209,7 @@
}
},
"bench": {
+ "description": "Un banco es una superficie de madera, metal, piedra, ... donde un humano se puede sentar. Estas capas los visualizan y preguntan algunas preguntas sobre ellos.",
"name": "Bancos",
"presets": {
"0": {
@@ -124,6 +230,9 @@
},
"bench-colour": {
"mappings": {
+ "0": {
+ "then": "Color: marrón"
+ },
"1": {
"then": "Color: verde"
},
@@ -150,7 +259,8 @@
"render": "Color: {colour}"
},
"bench-direction": {
- "question": "¿En qué dirección se mira al sentarse en el banco?"
+ "question": "¿En qué dirección se mira al sentarse en el banco?",
+ "render": "¿Cuando está sentado en el banco, uno mira hacia {direction}º."
},
"bench-material": {
"mappings": {
@@ -173,11 +283,16 @@
"then": "Material: acero"
}
},
+ "question": "¿De que está hecho el banco (asiento)?",
"render": "Material: {material}"
},
"bench-seats": {
"question": "¿Cuántos asientos tiene este banco?",
"render": "{seats} asientos"
+ },
+ "bench-survey:date": {
+ "question": "¿Cuándo fue la última vez que se inspeccionó este banco?",
+ "render": "Este banco se inspeccionó por última vez el {survey:date}"
}
},
"title": {
@@ -189,6 +304,11 @@
"name": "Bancos en una parada de transporte público",
"tagRenderings": {
"bench_at_pt-bench_type": {
+ "mappings": {
+ "2": {
+ "then": "No hay ningún banco aquí"
+ }
+ },
"question": "¿Qué tipo de banco es este?"
},
"bench_at_pt-name": {
@@ -196,21 +316,662 @@
}
},
"title": {
+ "mappings": {
+ "0": {
+ "then": "Banco en una parada de transporte público"
+ }
+ },
"render": "Banco"
}
},
+ "bicycle_library": {
+ "tagRenderings": {
+ "bicycle-library-target-group": {
+ "mappings": {
+ "0": {
+ "then": "Bicicletas para niños disponibles"
+ },
+ "1": {
+ "then": "Bicicletas para adultos disponibles"
+ },
+ "2": {
+ "then": "Bicicletas para discapacitados disponibles"
+ }
+ }
+ },
+ "bicycle_library-charge": {
+ "render": "Alquilar una bicicleta cuesta {charge}"
+ }
+ }
+ },
"bicycle_rental": {
+ "deletion": {
+ "extraDeleteReasons": {
+ "0": {
+ "explanation": "{title()} ha cerrado permanentemente"
+ }
+ },
+ "nonDeleteMappings": {
+ "0": {
+ "then": "Esta tienda de bicicletas alquilaba bicis, pero ya no lo hace"
+ }
+ }
+ },
+ "description": "Estaciones de alquiler de bicicletas",
+ "presets": {
+ "0": {
+ "title": "una tienda de alquiler de bicicletas"
+ }
+ },
+ "tagRenderings": {
+ "9": {
+ "rewrite": {
+ "into": {
+ "0": {
+ "1": "bicis de ciudad"
+ },
+ "1": {
+ "1": "bicis eléctricas"
+ },
+ "2": {
+ "1": "bicis infantiles"
+ },
+ "3": {
+ "1": "bicis BMX"
+ },
+ "4": {
+ "1": "bicis de montaña"
+ }
+ }
+ }
+ },
+ "bicycle-types": {
+ "mappings": {
+ "0": {
+ "then": "Aquí se pueden alquilar bicis normales"
+ },
+ "1": {
+ "then": "Aquí se pueden alquilar bicis eléctricas"
+ },
+ "2": {
+ "then": "Aquí se pueden alquilar bicis BMX"
+ },
+ "3": {
+ "then": "Aquí se pueden alquilar bicis de montaña"
+ },
+ "4": {
+ "then": "Aquí se pueden alquilar bicis infantiles"
+ },
+ "6": {
+ "then": "Aquí se pueden alquilar bicicletas de carreras"
+ }
+ },
+ "question": "¿Qué tipo de bicicletas y accesorios se alquilan aquí?",
+ "render": "{rental} se alquilan aquí"
+ },
+ "bicycle_rental_type": {
+ "mappings": {
+ "0": {
+ "then": "Esta es una tienda que se centra en el alquiler de bicicletas"
+ },
+ "1": {
+ "then": "Este es un negocio de alquileres que alquila varios objetos y/o vehículos. También alquila bicicletas, pero este no es el enfoque principal"
+ },
+ "2": {
+ "then": "Esta es una tienda que vende o alquila bicicletas, pero también las alquila"
+ },
+ "3": {
+ "then": "Esta es una estación automática, en la que una bici se asegura mecánicamente en una estructura"
+ }
+ },
+ "question": "¿Qué tipo de alquiler de bicicletas es este?"
+ }
+ },
"title": {
+ "mappings": {
+ "0": {
+ "then": "{name}"
+ }
+ },
"render": "Alquiler de bicicletas"
}
},
+ "bicycle_tube_vending_machine": {
+ "tagRenderings": {
+ "Still in use?": {
+ "mappings": {
+ "1": {
+ "then": "Esta máquina exprendedora está rota"
+ },
+ "2": {
+ "then": "Esta máquina exprendedora está cerrada"
+ }
+ },
+ "question": "¿Todavía es operacional esta máquina exprendedora?",
+ "render": "El estado operacional es {operational_status}"
+ }
+ }
+ },
"bike_cafe": {
"tagRenderings": {
+ "bike_cafe-email": {
+ "question": "¿Cual es la dirección de correo electrónico de {name}?"
+ },
+ "bike_cafe-phone": {
+ "question": "¿Cual es el número de teléfono de {name}?"
+ },
"bike_cafe-repair-tools": {
"question": "¿Hay herramientas para reparar su propia bicicleta?"
}
}
},
+ "bike_cleaning": {
+ "description": "Una capa que muestra facilidades en las que uno puede limpiar su bici",
+ "name": "Servicio de limpieza de bicis",
+ "presets": {
+ "0": {
+ "title": "un servicio de limpieza de bicis"
+ }
+ },
+ "tagRenderings": {
+ "bike_cleaning-charge": {
+ "mappings": {
+ "0": {
+ "then": "Un servicio de limpieza gratis"
+ },
+ "1": {
+ "then": "Gratis"
+ },
+ "2": {
+ "then": "El servicio de limpieza tiene una tarifa"
+ }
+ },
+ "question": "¿Cuánto cuesta utilizar el servicio de limpieza?",
+ "render": "Utilizar el servicio de limpieza cuesta {charge}"
+ },
+ "bike_cleaning-service:bicycle:cleaning:charge": {
+ "mappings": {
+ "0": {
+ "then": "El servicio de limpieza es gratis"
+ },
+ "1": {
+ "then": "Gratis"
+ },
+ "2": {
+ "then": "El servicio de limpieza tiene una tasa, pero la cantidad se desconoce"
+ }
+ },
+ "question": "¿Cuánto cuesta utilizar el servicio de limpieza?",
+ "render": "Utilizar el servicio de limpieza cuesta {service:bicycle:cleaning:charge}"
+ }
+ },
+ "title": {
+ "mappings": {
+ "0": {
+ "then": "Servicio de limpieza de bicis {name}"
+ }
+ },
+ "render": "Servicio de limpieza de bicis"
+ }
+ },
+ "bike_parking": {
+ "description": "Una capa que muestra donde puedes aparcar tu bici",
+ "name": "Aparcamiento de bicis",
+ "presets": {
+ "0": {
+ "title": "un aparcamiento de bicis"
+ }
+ },
+ "tagRenderings": {
+ "Access": {
+ "mappings": {
+ "0": {
+ "then": "Accesible públicamente"
+ },
+ "1": {
+ "then": "El acceso es primariamente para visitantes a un negocio"
+ },
+ "2": {
+ "then": "El acceso se limita a miembros de una escuela, compañía u organización"
+ }
+ },
+ "question": "¿Quién puede utilizar este aparcamiento de bicicletas?",
+ "render": "{access}"
+ },
+ "Bicycle parking type": {
+ "mappings": {
+ "5": {
+ "then": "Caseta"
+ },
+ "6": {
+ "then": "Bolardo"
+ },
+ "7": {
+ "then": "Una área en el suelo que está marcada para el aparcamiento de bicicletas"
+ }
+ },
+ "question": "¿Cual es el tipo de este aparcamiento de bicicletas?",
+ "render": "Este es un aparcamiento de bicicletas del tipo: {bicycle_parking}"
+ },
+ "Capacity": {
+ "question": "¿Cuántas bicicletas caben en este aparcamiento de bicicletas (incluyendo posibles bicicletas de carga)?",
+ "render": "Espacio para {capacity} bicis"
+ },
+ "Cargo bike capacity?": {
+ "question": "¿Cuántas bicicletas de carga caben en este aparcamiento de bicicletas?",
+ "render": "En este aparcamiento caben {capacity:cargo_bike} bicis de carga"
+ },
+ "Cargo bike spaces?": {
+ "mappings": {
+ "0": {
+ "then": "Este aparcamiento tiene espacio para bicicletas de carga"
+ },
+ "1": {
+ "then": "Este aparcamiento tiene huecos (oficialmente) designados para bicicletas de carga."
+ },
+ "2": {
+ "then": "No se permite aparcar bicicletas de carga"
+ }
+ },
+ "question": "¿Este aparcamiento de bicicletas tiene huevos para bicicletas de carga?"
+ },
+ "Is covered?": {
+ "mappings": {
+ "0": {
+ "then": "Este aparcamiento está cubierto (tiene un tejado)"
+ },
+ "1": {
+ "then": "Este aparcamiento no está cubierto"
+ }
+ },
+ "question": "¿Está cubierto este aparcamiento? Selecciona \"cubierto\" también para aparcamientos interiores."
+ },
+ "Underground?": {
+ "mappings": {
+ "0": {
+ "then": "Aparcamiento subterráneo"
+ },
+ "1": {
+ "then": "Aparcamiento a nivel de calle"
+ },
+ "2": {
+ "then": "Aparcamiento de azotea"
+ }
+ },
+ "question": "¿Cual es la localización relativa de este aparcamiento de bicicletas?"
+ }
+ },
+ "title": {
+ "render": "Aparcamiento de bicis"
+ }
+ },
+ "bike_repair_station": {
+ "description": "Una capa que muestra bombas de bicicletas y puestos de herramientas de reparación de bicicletas",
+ "name": "Bomba y reparación de bicicletas",
+ "presets": {
+ "0": {
+ "description": "Un dispositivo para inflar tus ruedas en una posición fija en el espacio público.",
+ "title": "una bomba de bicicletas"
+ },
+ "1": {
+ "description": "Una bomba de bicicletas y herramientas para reparar tu bicicleta en el espacio público. Las herramientas habitualmente están aseguradas con cadenas contra el robo.",
+ "title": "En estación de reparación de bicicletas y bomba"
+ },
+ "2": {
+ "description": "Herramientas para reparar tu bici en el espacio público (sin bomba).Las herramientas están aseguradas contra el robo.",
+ "title": "una estación de reparación de bicicletas sin bomba"
+ }
+ },
+ "tagRenderings": {
+ "Operational status": {
+ "mappings": {
+ "0": {
+ "then": "La bomba de bicicletas está rota"
+ },
+ "1": {
+ "then": "La bomba de bicicletas está operativa"
+ }
+ },
+ "question": "¿Todavía está operativa la bomba de bicicletas?"
+ },
+ "access": {
+ "mappings": {
+ "0": {
+ "then": "Accesible públicamente"
+ },
+ "1": {
+ "then": "Accesible públicamente"
+ },
+ "2": {
+ "then": "Solo para clientes"
+ },
+ "3": {
+ "then": "No accesible para el público general"
+ },
+ "4": {
+ "then": "No accesible para el público general"
+ }
+ },
+ "question": "¿A quién se le permite utilizar esta estación de reparación?"
+ },
+ "bike_repair_station-available-services": {
+ "mappings": {
+ "0": {
+ "then": "Solo hay una bomba presente"
+ },
+ "1": {
+ "then": "Solo hay herramientas (destornilladores, pinzas...) presentes"
+ },
+ "2": {
+ "then": "Hay tanto herramientas como bombas"
+ }
+ },
+ "question": "¿Qué servicios están disponibles en esta localización?"
+ },
+ "bike_repair_station-bike-chain-tool": {
+ "mappings": {
+ "0": {
+ "then": "Hay una herramienta de cadenas"
+ },
+ "1": {
+ "then": "No hay herramienta de cadenas"
+ }
+ },
+ "question": "¿Esta estación de reparación tiene una herramienta especial para reparar la cadena de tu bici?"
+ },
+ "bike_repair_station-bike-stand": {
+ "mappings": {
+ "0": {
+ "then": "Hay un gancho o soporte"
+ },
+ "1": {
+ "then": "No hay ningún gancho o soporte"
+ }
+ },
+ "question": "¿Esta estación tiene un gancho para colgar tu bici o un soporte para elevarla?"
+ },
+ "bike_repair_station-electrical_pump": {
+ "mappings": {
+ "0": {
+ "then": "Bomba manual"
+ },
+ "1": {
+ "then": "Bomba eléctrica"
+ }
+ },
+ "question": "¿Hay una bomba eléctrica para bicis?"
+ },
+ "bike_repair_station-email": {
+ "question": "¿Es esta la dirección de correo electrónico del mantenedor?"
+ },
+ "bike_repair_station-manometer": {
+ "mappings": {
+ "0": {
+ "then": "Hay un manómetro"
+ },
+ "1": {
+ "then": "No hay ningún manometro"
+ },
+ "2": {
+ "then": "Hay un manómetro pero está roto"
+ }
+ },
+ "question": "¿La bomba tiene un indicador de presión o manómetro?"
+ },
+ "bike_repair_station-opening_hours": {
+ "mappings": {
+ "0": {
+ "then": "Siempre abierto"
+ }
+ },
+ "question": "¿Cuándo está abierto este punto de reparación de bicicletas?"
+ },
+ "bike_repair_station-operator": {
+ "question": "¿Quién mantiene esta bomba para bicicletas?",
+ "render": "Mantenido por {operator}"
+ },
+ "bike_repair_station-phone": {
+ "question": "¿Cual es el número de teléfono del mantenedor?"
+ },
+ "bike_repair_station-valves": {
+ "question": "¿Que válvulas se soportan?",
+ "render": "Esta bomba soporta las siguiente válvulas: {valves}"
+ }
+ },
+ "title": {
+ "mappings": {
+ "0": {
+ "then": "Estación de reparación de bicis"
+ },
+ "1": {
+ "then": "Estación de reparación de bicis"
+ },
+ "2": {
+ "then": "Bomba rota"
+ },
+ "3": {
+ "then": "Bomba de bicicletas {name}"
+ },
+ "4": {
+ "then": "Bomba para bicicletas"
+ }
+ },
+ "render": "Estación de bicis (bomba y reparación)"
+ }
+ },
+ "bike_shop": {
+ "description": "Una tiene que vende específicamente bicis u objetos relacionados",
+ "tagRenderings": {
+ "bike_repair_bike-pump-service": {
+ "mappings": {
+ "0": {
+ "then": "Esta tienda ofrece una bomba para cualquiera"
+ },
+ "1": {
+ "then": "Esta tienda no ofrece una bomba para cualquiera"
+ },
+ "2": {
+ "then": "Hay una bomba para bicicletas, se muestra como un punto separado "
+ }
+ },
+ "question": "¿Esta tienda ofrece una bomba para que la utilice cualquiera?"
+ },
+ "bike_repair_bike-wash": {
+ "mappings": {
+ "0": {
+ "then": "Esta tienda limpia bicicletas"
+ },
+ "1": {
+ "then": "Esta tienda tiene una instalación donde uno puede limpiar bicicletas por si mismo"
+ },
+ "2": {
+ "then": "Esta tienda no ofrece limpieza de bicicletas"
+ }
+ },
+ "question": "¿Aquí se lavan bicicletas?"
+ },
+ "bike_repair_rents-bikes": {
+ "mappings": {
+ "0": {
+ "then": "Esta tienda alquila bicis"
+ },
+ "1": {
+ "then": "Esta tienda no alquila bicis"
+ }
+ },
+ "question": "¿Alquila bicicis esta tienda?"
+ },
+ "bike_repair_repairs-bikes": {
+ "mappings": {
+ "0": {
+ "then": "Esta tienda repara bicis"
+ },
+ "1": {
+ "then": "Esta tienda no repara bicis"
+ },
+ "2": {
+ "then": "Esta tienda solo repara bicis compradas aquí"
+ },
+ "3": {
+ "then": "Esta tienda solo repara bicis de una cierta marca"
+ }
+ },
+ "question": "¿Repara bicis esta tienda?"
+ },
+ "bike_repair_second-hand-bikes": {
+ "mappings": {
+ "0": {
+ "then": "Esta tienda vende bicis de segunda mano"
+ },
+ "1": {
+ "then": "Esta tienda no vende bicis de segunda mano"
+ },
+ "2": {
+ "then": "Esta tienda solo vende bicis de segunda mano"
+ }
+ },
+ "question": "¿Vende bicis de segunda mano esta tienda?"
+ },
+ "bike_repair_sells-bikes": {
+ "mappings": {
+ "0": {
+ "then": "Esta tienda vende bicis"
+ },
+ "1": {
+ "then": "Esta tienda no vende bicis"
+ }
+ },
+ "question": "¿Vende bicis esta tienda?"
+ },
+ "bike_repair_tools-service": {
+ "question": "¿Hay herramientas para reparar tu propia bici?"
+ },
+ "bike_shop-access": {
+ "render": "Solo accesible a {access}"
+ },
+ "bike_shop-email": {
+ "question": "¿Cual es la dirección de correo electrónico de {name}?"
+ },
+ "bike_shop-is-bicycle_shop": {
+ "render": "Esta tienda está especializada en vender {shop} y hace actividades relacionadas con bicicletas"
+ },
+ "bike_shop-name": {
+ "question": "¿Cual es el nombre de esta tienda de bicicletas?",
+ "render": "Esta tienda de bicicletas se llama {name}"
+ },
+ "bike_shop-phone": {
+ "question": "¿Cual es el número de teléfono de {name}?"
+ },
+ "bike_shop-website": {
+ "question": "¿Cual es el sitio web de {name}?"
+ }
+ },
+ "title": {
+ "mappings": {
+ "2": {
+ "then": "Alquiler de bicicletas {name}"
+ },
+ "3": {
+ "then": "Reparación de bicis {name}"
+ },
+ "4": {
+ "then": "Tienda de bicis {name}"
+ }
+ }
+ }
+ },
+ "bike_themed_object": {
+ "description": "Una capa con los objetos relacionados con bicis pero que no coinciden con ninguna otra capa",
+ "name": "Objeto relacionada con bicis",
+ "title": {
+ "mappings": {
+ "1": {
+ "then": "Carril bici"
+ }
+ },
+ "render": "Objeto relacionado con bicis"
+ }
+ },
+ "binocular": {
+ "tagRenderings": {
+ "binocular-charge": {
+ "question": "¿Cuánto hay que pagar para utilizar estos binoculares?",
+ "render": "Utilizar estos binoculares cuesta {charge}"
+ },
+ "binocular-direction": {
+ "question": "¿Cuándo uno mira a través de este binocular, en qué dirección lo hace?",
+ "render": "Mira hacia {direction}º"
+ }
+ },
+ "title": {
+ "render": "Binoculares"
+ }
+ },
+ "birdhide": {
+ "filter": {
+ "0": {
+ "options": {
+ "0": {
+ "question": "Accesible con sillas de ruedas"
+ }
+ }
+ }
+ },
+ "name": "Lugares para ver pájaros",
+ "presets": {
+ "0": {
+ "description": "Un refugio cubierto donde se pueden ver pájaros confortablemente"
+ },
+ "1": {
+ "description": "Una pantalla o pared con aperturas para ver pájaros"
+ }
+ },
+ "tagRenderings": {
+ "bird-hide-wheelchair": {
+ "mappings": {
+ "0": {
+ "then": "Hay provisiones especiales para usuarios de sillas de ruedas"
+ },
+ "3": {
+ "then": "No accesible a usuarios con sillas de ruedas"
+ }
+ }
+ },
+ "birdhide-operator": {
+ "mappings": {
+ "0": {
+ "then": "Operado por Natuurpunt"
+ }
+ },
+ "render": "Operado por {operator}"
+ }
+ }
+ },
+ "cafe_pub": {
+ "deletion": {
+ "extraDeleteReasons": {
+ "0": {
+ "explanation": "{title()} ha cerrado permanentemente"
+ }
+ }
+ },
+ "description": "Una capa que muestra cafeterías y bares donde uno se puede reunir con una bebida. La capa pregunta algunas preguntas relevantes",
+ "filter": {
+ "0": {
+ "options": {
+ "0": {
+ "question": "Abiert oahora"
+ }
+ }
+ }
+ },
+ "name": "Cafeterías y bares",
+ "presets": {
+ "0": {
+ "description": "Un bar, principalmente para beber cervezas en un interior templado y relajado"
+ }
+ }
+ },
"charging_station": {
"tagRenderings": {
"Authentication": {
@@ -332,5 +1093,125 @@
"question": "¿Se puede visitar esta torre?"
}
}
+ },
+ "tree_node": {
+ "description": "Una capa que muestra árboles",
+ "name": "Árbol",
+ "presets": {
+ "0": {
+ "description": "Un árbol de hojas como el Roble o el Álamo.",
+ "title": "árbol de hoja ancha"
+ },
+ "1": {
+ "description": "Un árbol de hojas agujas, como el Pino o el Abeto.",
+ "title": "Árbol tipo Conífera"
+ },
+ "2": {
+ "description": "Si no estás seguro de si es un árbol de hoja ancha o de hoja de aguja.",
+ "title": "un árbol"
+ }
+ },
+ "tagRenderings": {
+ "tree-decidouous": {
+ "mappings": {
+ "0": {
+ "then": "Caduco o Deciduo: el árbol pierde las hojas en un período del año"
+ },
+ "1": {
+ "then": "Siempreverde."
+ }
+ },
+ "question": "¿El árbol es Siempreverde o Caduco?"
+ },
+ "tree-denotation": {
+ "mappings": {
+ "0": {
+ "then": "El árbol es notable debido a su tamaño o ubicación prominente. Es útil para la navegación."
+ },
+ "1": {
+ "then": "El árbol es un monumento natural, por ejemplo, porque es especialmente antiguo, o de una especie valiosa."
+ },
+ "2": {
+ "then": "El árbol se utiliza con fines agrícolas, por ejemplo, en un huerto."
+ },
+ "3": {
+ "then": "El árbol está en un parque o similar (cementerio, recinto escolar, ...)."
+ },
+ "4": {
+ "then": "El árbol está en un jardín privado o residencial."
+ },
+ "5": {
+ "then": "El árbol está en bandejón de una avenida."
+ },
+ "6": {
+ "then": "El árbol está en un zona urbana."
+ },
+ "7": {
+ "then": "El árbol está fuera de una zona urbana."
+ }
+ },
+ "question": "¿Qué importancia tiene este árbol? Elige la primera respuesta que corresponda."
+ },
+ "tree-height": {
+ "mappings": {
+ "0": {
+ "then": "Altura: {height} m"
+ }
+ },
+ "render": "Altura: {height}"
+ },
+ "tree-heritage": {
+ "mappings": {
+ "0": {
+ "then": "Registrado como patrimonio por Onroerend Erfgoed Flandes"
+ },
+ "1": {
+ "then": "Registrado como patrimonio por la Dirección de Patrimonio Cultural de Bruselas"
+ },
+ "2": {
+ "then": "Registrado como patrimonio por una organización diferente"
+ },
+ "3": {
+ "then": "No registrado como patrimonio"
+ },
+ "4": {
+ "then": "Registrado como patrimonio por un organización diferente"
+ }
+ },
+ "question": "¿Este árbol es patrimonio registrado?"
+ },
+ "tree-leaf_type": {
+ "mappings": {
+ "0": {
+ "then": "Latifoliada"
+ },
+ "1": {
+ "then": "Hoja aguja"
+ },
+ "2": {
+ "then": "Permanentemente sin hojas"
+ }
+ },
+ "question": "¿Es un árbol de hoja ancha o de hoja aguja?"
+ },
+ "tree_node-name": {
+ "mappings": {
+ "0": {
+ "then": "No identificas la especie."
+ }
+ },
+ "question": "El árbol no tiene nombre?."
+ },
+ "tree_node-ref:OnroerendErfgoed": {
+ "question": "¿Cuál es la identificación emitida por Onroerend Erfgoed Flandes?"
+ },
+ "tree_node-wikidata": {
+ "question": "¿Cuál es el ID de Wikidata para este árbol?",
+ "render": " Wikidata: {wikidata}"
+ }
+ },
+ "title": {
+ "render": "Árbol"
+ }
}
}
\ No newline at end of file
diff --git a/langs/layers/nb_NO.json b/langs/layers/nb_NO.json
index cb4dce0aeb..487780cc5a 100644
--- a/langs/layers/nb_NO.json
+++ b/langs/layers/nb_NO.json
@@ -1,4 +1,25 @@
{
+ "address": {
+ "description": "Adresser",
+ "name": "Kjente adresser i OSM",
+ "title": {
+ "render": "Kjent adresse"
+ }
+ },
+ "ambulancestation": {
+ "presets": {
+ "0": {
+ "description": "Legg til en ambulansestasjon på kartet",
+ "title": "en ambulansestasjon"
+ }
+ },
+ "tagRenderings": {
+ "ambulance-name": {
+ "question": "Hva er navnet på denne ambulansestasjonen?",
+ "render": "Denne stasjonen heter {name}."
+ }
+ }
+ },
"artwork": {
"name": "Kunstverk",
"presets": {
diff --git a/langs/nl.json b/langs/nl.json
index 92573b6ba1..a42df19945 100644
--- a/langs/nl.json
+++ b/langs/nl.json
@@ -244,7 +244,7 @@
"doSearch": "Zoek hierboven om resultaten te zien",
"failed": "Het Wikipedia-artikel inladen is mislukt",
"loading": "Wikipedia aan het laden...",
- "noResults": "Niet gevonden voor {search}",
+ "noResults": "Geen relevante items gevonden voor {search}",
"noWikipediaPage": "Dit Wikidata-item heeft nog geen overeenkomstig Wikipedia-artikel",
"previewbox": {
"born": "Geboren: {value}",
@@ -276,6 +276,9 @@
"willBePublished": "Jouw foto wordt gepubliceerd "
},
"importHelper": {
+ "compareToAlreadyExistingNotes": {
+ "nothingNearby": "Geen enkel te importeren punt heeft een importeer-kaartnota in de buurt"
+ },
"introduction": {
"description": "De importeer-helper converteert een externe dataset in OSM-kaartnotas. De externe data moet overeenkomen met een bestaande MapComplete-laag. Voor elk item wordt er een kaartnota gemaakt. Deze notas worden dan samen met de relevante POI getoond en kunnen dan (via MapComplete) snel en eenvoudig toegevoegd worden.",
"importFormat": "Een kaartnota moet het volgende formaat hebben om gedetecteerd te worden binnen een laag:
[Een introductietekst] https://mapcomplete.osm.be/[themename].html?[parameters waaronder lon en lat]#import [alle tags van het te importeren object]
"
@@ -294,6 +297,11 @@
"selectLayer": "Met welke laag komt deze te importeren dataset overeen?",
"title": "Voorbeeldkaart"
},
+ "noteParts": {
+ "datasource": "Oorspronkelijke data van {source}",
+ "importEasily": "Voeg dit punt eenvoudig toe met MapComplete:",
+ "wikilink": "Meer informatie over deze import: {wikilink}"
+ },
"previewAttributes": {
"allAttributesSame": "Alle kaart-objecten om te importeren hebben deze tag",
"inspectDataTitle": "Bekijk de data van {count} te importeren objecten",
diff --git a/langs/shared-questions/da.json b/langs/shared-questions/da.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/langs/shared-questions/da.json
@@ -0,0 +1 @@
+{}
diff --git a/langs/shared-questions/es.json b/langs/shared-questions/es.json
index e513f074e8..f6b0d6ad56 100644
--- a/langs/shared-questions/es.json
+++ b/langs/shared-questions/es.json
@@ -10,8 +10,122 @@
},
"1": {
"then": "No se permiten perros"
+ },
+ "2": {
+ "then": "Los perros están permitidos, pero tienen que llevar correa"
+ },
+ "3": {
+ "then": "Los perros están permitidos y pueden estar sueltos"
+ }
+ },
+ "question": "¿Están permitidos los perros en este negocio?"
+ },
+ "email": {
+ "question": "¿Cual es la direccióm de correo electrónico de {title()}?"
+ },
+ "level": {
+ "mappings": {
+ "0": {
+ "then": "Localizado bajo tierra"
+ },
+ "1": {
+ "then": "Localizado en la planta baja"
+ },
+ "2": {
+ "then": "Localizado en la planta baja"
+ },
+ "3": {
+ "then": "Localizado en la primera planta"
+ },
+ "4": {
+ "then": "Situado en el primer nivel del sótano"
+ }
+ },
+ "question": "¿En qué nivel se encuentra esta característica?",
+ "render": "Localizada en la {level}° planta"
+ },
+ "opening_hours": {
+ "question": "¿Cuales son las horas de apertura de {title()}?",
+ "render": "
Horas de apertura
{opening_hours_table(opening_hours)}"
+ },
+ "payment-options": {
+ "mappings": {
+ "0": {
+ "then": "Aquí se acepta el pago en efectivo"
+ },
+ "1": {
+ "then": "Aquí se acepta el pago por tarjeta"
+ }
+ },
+ "question": "¿Qué métodos de pago se aceptan aquí?"
+ },
+ "payment-options-advanced": {
+ "override": {
+ "mappings+": {
+ "0": {
+ "then": "El pago se realiza con una aplicación dedicada"
+ },
+ "1": {
+ "then": "El pago se realiza con una tarjeta de membresía"
+ }
}
}
+ },
+ "phone": {
+ "question": "¿Cuál es el número de teléfono de {title()}?"
+ },
+ "service:electricity": {
+ "mappings": {
+ "0": {
+ "then": "Hay numerosos enchufes domésticos disponibles para los clientes sentados en el interior, donde pueden cargar sus dispositivos electrónicos"
+ },
+ "1": {
+ "then": "Hay algunos enchufes domésticos disponibles para los clientes sentados en el interior, donde pueden cargar sus dispositivos electrónicos"
+ },
+ "2": {
+ "then": "No hay enchufes disponibles en el interior para los clientes, pero cargar puede ser posible si se pregunta al personal"
+ },
+ "3": {
+ "then": "No hay enchufes domésticos disponibles para los clientes sentados en el interior"
+ }
+ },
+ "question": "¿Esta facilidad tiene enchufes eléctricos, disponibles para los clientes cuando están dentro?"
+ },
+ "website": {
+ "question": "¿Cual es el sitio web de {title()}?"
+ },
+ "wheelchair-access": {
+ "mappings": {
+ "0": {
+ "then": "Este lugar está especialmente adaptado para usuarios en sillas de ruedas"
+ },
+ "1": {
+ "then": "Este lugar es fácilmente accesible con una silla de ruedas"
+ },
+ "2": {
+ "then": "Es posible llegar a este lugar con una silla de ruedas, pero no es fácil"
+ },
+ "3": {
+ "then": "No es posible llegar a este lugar con una silla de ruedas"
+ }
+ },
+ "question": "¿Este lugar es accesible con una silla de ruedas?"
+ },
+ "wikipedia": {
+ "mappings": {
+ "0": {
+ "then": "Todavía no se ha enlazado una página de wikipedia"
+ }
+ },
+ "question": "¿Cual es la entidad de Wikidata que se corresponde?"
+ },
+ "wikipedialink": {
+ "mappings": {
+ "0": {
+ "then": "No enlazado con Wikipedia"
+ }
+ },
+ "question": "¿Cual es el ítem correspondiente en Wikipedia?"
}
}
}
\ No newline at end of file
diff --git a/langs/shared-questions/nb_NO.json b/langs/shared-questions/nb_NO.json
index 7b1497c6e3..e768c96d88 100644
--- a/langs/shared-questions/nb_NO.json
+++ b/langs/shared-questions/nb_NO.json
@@ -54,6 +54,15 @@
},
"question": "Hvilke betalingsmetoder godtas her?"
},
+ "payment-options-advanced": {
+ "override": {
+ "mappings+": {
+ "1": {
+ "then": "Betaling utføres med et medlemskort"
+ }
+ }
+ }
+ },
"phone": {
"question": "Hva er telefonnummeret til {title()}?"
},
diff --git a/langs/shared-questions/zh_Hans.json b/langs/shared-questions/zh_Hans.json
new file mode 100644
index 0000000000..a6422ac1b4
--- /dev/null
+++ b/langs/shared-questions/zh_Hans.json
@@ -0,0 +1,73 @@
+{
+ "shared_questions": {
+ "dog-access": {
+ "mappings": {
+ "0": {
+ "then": "允许犬只"
+ },
+ "1": {
+ "then": "不允许犬只"
+ },
+ "2": {
+ "then": "允许犬只,但必须拴绳"
+ },
+ "3": {
+ "then": "允许犬只,且可自由放养"
+ }
+ },
+ "question": "犬只是否在这个商业体中允许?"
+ },
+ "email": {
+ "question": "{title()} 的电子邮箱地址为何?"
+ },
+ "opening_hours": {
+ "question": "{title()} 的开放时间为何?",
+ "render": "
开放时间
{opening_hours_table(opening_hours)}"
+ },
+ "payment-options": {
+ "mappings": {
+ "0": {
+ "then": "可用现金"
+ },
+ "1": {
+ "then": "可用信用卡"
+ }
+ },
+ "question": "这里支持哪些支付方式?"
+ },
+ "payment-options-advanced": {
+ "override": {
+ "mappings+": {
+ "0": {
+ "then": "使用专用APP支付"
+ },
+ "1": {
+ "then": "使用会员卡支付"
+ }
+ }
+ }
+ },
+ "phone": {
+ "question": "{title()} 的电话号码为何?"
+ },
+ "website": {
+ "question": "{title()} 的网站为何?"
+ },
+ "wikipedia": {
+ "mappings": {
+ "0": {
+ "then": "尚未有连接到的维基百科页面"
+ }
+ },
+ "question": "在Wikidata上对应的实体是什么?"
+ },
+ "wikipedialink": {
+ "mappings": {
+ "0": {
+ "then": "不要连接到维基百科"
+ }
+ },
+ "question": "在维基百科上对应的条目是什么?"
+ }
+ }
+}
\ No newline at end of file
diff --git a/langs/themes/da.json b/langs/themes/da.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/langs/themes/da.json
@@ -0,0 +1 @@
+{}
diff --git a/langs/themes/de.json b/langs/themes/de.json
index 98f62021aa..56f22bcfe9 100644
--- a/langs/themes/de.json
+++ b/langs/themes/de.json
@@ -8,7 +8,7 @@
"title": "Freie Kunstwerk-Karte"
},
"benches": {
- "description": "Diese Karte zeigt alle Sitzbänke, die in OpenStreetMap eingetragen sind: Einzeln stehende Bänke und Bänke, die zu Haltestellen oder Unterständen gehören. Mit einem OpenStreetMap-Account können Sie neue Bänke eintragen oder Detailinformationen existierender Bänke bearbeiten.",
+ "description": "Diese Karte zeigt alle Sitzbänke, die in OpenStreetMap eingetragen sind: Alleinstehende Bänke und Bänke, die zu Haltestellen oder Unterständen gehören. Mit einem OpenStreetMap-Konto können Sie neue Bänke eintragen oder Details existierender Bänke bearbeiten.",
"shortDescription": "Eine Karte aller Sitzbänke",
"title": "Sitzbänke"
},
@@ -677,7 +677,7 @@
"title": "Pommes-frites-Läden"
},
"ghostbikes": {
- "description": "Ein Geisterrad ist ein weißes Fahrrad, dass zum Gedenken eines tödlich verunglückten Radfahrers vor Ort aufgestellt wurde.
Auf dieser Karte kann man alle Geisterräder sehen, die in OpenStreetMap eingetragen sind. Fehlt ein Geisterrad? Jeder kann hier Informationen hinzufügen oder aktualisieren - Sie benötigen lediglich einen (kostenlosen) OpenStreetMap-Account.",
+ "description": "Ein Geisterrad ist ein weißes Fahrrad, dass zum Gedenken eines tödlich verunglückten Radfahrers vor Ort aufgestellt wurde.
Auf dieser Karte sehen Sie alle Geisterräder, die in OpenStreetMap eingetragen sind. Fehlt ein Geisterrad? Jeder kann hier Informationen hinzufügen oder aktualisieren - Sie benötigen nur ein (kostenloses) OpenStreetMap-Konto.",
"title": "Geisterräder"
},
"grb": {
@@ -797,7 +797,7 @@
"title": "In die Natur"
},
"notes": {
- "description": "Ein Hinweis ist eine Stecknadel auf der Karte mit einer Fehlerbeschreibung.
Über die Filteransicht kann nach Benutzern und Text gesucht werden.",
+ "description": "Eine Notiz ist eine Stecknadel auf der Karte mit einer Fehlerbeschreibung.
Über die Filteransicht kann nach Benutzer und Text gesucht werden.",
"title": "Notizen von OpenStreetMap"
},
"observation_towers": {
diff --git a/langs/themes/es.json b/langs/themes/es.json
index 8d15340174..b1af01d8aa 100644
--- a/langs/themes/es.json
+++ b/langs/themes/es.json
@@ -6,11 +6,143 @@
"artwork": {
"description": "Bienvenido a Open Artwork Map, un mapa de estatuas, bustos, grafitis y otras obras de arte de todo el mundo"
},
+ "benches": {
+ "description": "Este mapa muestra todos los bancos que están grabados en OpenStreetMap: Bancos individuales, bancos que pertenecen a paradas o marquesinas del transporte público. Con una cuenta de OpenStreetMap, puedes mapear nuevos bancos o editar detalles de bancos existentes.",
+ "shortDescription": "Un mapa de bancos",
+ "title": "Bancos"
+ },
+ "bicycle_rental": {
+ "description": "En este mapa, encontrarás numerosas estaciones de alquiler de bicicletas que son conocidas por OpenStreetMap",
+ "shortDescription": "Un mapa con estaciones de alquiler de bicicletas y tiendas de alquiler de bicicletas",
+ "title": "Alquiler de bicicletas"
+ },
+ "binoculars": {
+ "description": "Un mapa con prismáticos fijos en un poste. Suele encontrarse en lugares turísticos, miradores, en lo alto de torres panorámicas u ocasionalmente en una reserva natural.",
+ "shortDescription": "Un mapa con prismáticos fijos",
+ "title": "Binoculares"
+ },
+ "bookcases": {
+ "description": "Una librería pública es un pequeño armario en la calle, una caja, una vieja cabina telefónica o algún otro objeto donde se guardan libros. Todo el mundo puede colocar o coger un libro. Este mapa pretende recoger todas estas librerías. Puedes descubrir nuevas librerías cercanas y, con una cuenta gratuita de OpenStreetMap, añadir rápidamente tus librerías favoritas.",
+ "title": "Mapa de Librerías Abiertas"
+ },
+ "cafes_and_pubs": {
+ "description": "Pubs y bares",
+ "title": "Cafeterías y pubs"
+ },
+ "campersite": {
+ "description": "Este sitio recoge todos los lugares oficiales de parada de caravanas y los lugares donde se pueden verter las aguas grises y negras. Puedes añadir detalles sobre los servicios prestados y el coste. Añade fotos y reseñas. Este es un sitio web y una aplicación web. Los datos se almacenan en OpenStreetMap, por lo que serán gratuitos para siempre y podrán ser reutilizados por cualquier aplicación.",
+ "layers": {
+ "0": {
+ "description": "Sitios de acampada",
+ "name": "Sitios de Acampada",
+ "presets": {
+ "0": {
+ "description": "Añade un nuevo sitio de acampada oficial. Son lugares designados para pasar la noche con tu caravana. Pueden parecerse a un camping real o simplemente a un aparcamiento. Puede que no estén señalizados en absoluto, sino que simplemente estén definidos en una decisión municipal. Un aparcamiento normal destinado a los campistas en el que no se espera que se pase la noche, no es un camping. ",
+ "title": "Un camping"
+ }
+ },
+ "tagRenderings": {
+ "caravansites-capacity": {
+ "question": "¿Cuántos campistas pueden alojarse aquí? (omitir si no hay un número evidente de plazas o vehículos permitidos)",
+ "render": "{capacity} los campistas pueden utilizar este lugar al mismo tiempo"
+ },
+ "caravansites-charge": {
+ "question": "¿Cuánto cobra este lugar?",
+ "render": "Este lugar cobra {charge}"
+ },
+ "caravansites-description": {
+ "question": "¿Te gustaría añadir una descripción general de este lugar? (No repitas información previamente preguntada o mostrada arriba. Por favor mantenla objetiva - las opiniones van en la de opiniones)",
+ "render": "Más detalles sobre este lugar:{description}"
+ },
+ "caravansites-fee": {
+ "mappings": {
+ "0": {
+ "then": "Necesitas pagar por su uso"
+ },
+ "1": {
+ "then": "Se puede usar de manera gratuita"
+ }
+ },
+ "question": "¿Este lugar cobra una tarifa?"
+ },
+ "caravansites-internet": {
+ "mappings": {
+ "0": {
+ "then": "Hay acceso a internet"
+ },
+ "1": {
+ "then": "Hay acceso a internet"
+ },
+ "2": {
+ "then": "No hay acceso a internet"
+ }
+ },
+ "question": "¿Este lugar tiene acceso a internet?"
+ },
+ "caravansites-internet-fee": {
+ "mappings": {
+ "0": {
+ "then": "Tienes que pagar a mayores por el acceso a internet"
+ },
+ "1": {
+ "then": "No tienes que pagar a mayores por el acceso a internet"
+ }
+ },
+ "question": "¿Tienes que pagar por el acceso a internet?"
+ },
+ "caravansites-long-term": {
+ "mappings": {
+ "0": {
+ "then": "Sí, hay algunas plazas de alquiler a largo plazo, pero también puedes alojarte por días"
+ },
+ "1": {
+ "then": "No, no hay huéspedes permanentes aquí"
+ },
+ "2": {
+ "then": "Solo es posible permanecer aquí si tienes un contrato a largo plazo (este lugar desaparecerá de este mapa si escoges esto)"
+ }
+ },
+ "question": "¿Este lugar ofrece huecos para alquilar a largo plazo?"
+ },
+ "caravansites-name": {
+ "question": "¿Cómo se llama este lugar?",
+ "render": "Este lugar se llama {name}"
+ },
+ "caravansites-sanitary-dump": {
+ "mappings": {
+ "0": {
+ "then": "Este lugar tiene un vertedero sanitario"
+ },
+ "1": {
+ "then": "Este lugar no tiene vertedero sanitario"
+ }
+ },
+ "question": "¿Este lugar tiene un vertedero sanitario?"
+ },
+ "caravansites-toilets": {
+ "mappings": {
+ "0": {
+ "then": "Este lugar cuenta con sanitarios"
+ },
+ "1": {
+ "then": "Este lugar no tiene sanitarios"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
"ghostbikes": {
"title": "Bicicleta blanca"
},
"personal": {
"description": "Crea una interficie basada en todas las capas disponibles de todas las interficies",
"title": "Interficie personal"
+ },
+ "trees": {
+ "description": "Mapa de todos los Árboles",
+ "shortDescription": "Mapa de los Árboles",
+ "title": "Árboles"
}
}
\ No newline at end of file
diff --git a/langs/themes/fr.json b/langs/themes/fr.json
index c87ff74c52..eeb226c080 100644
--- a/langs/themes/fr.json
+++ b/langs/themes/fr.json
@@ -21,10 +21,19 @@
"description": "Une vélothèque est un endroit où on peut emprunter des vélos, souvent moyennant une petite somme annuelle. Un cas d'utilisation notable est celui des vélothèques pour les enfants, qui leur permettent de passer à un vélo plus grand quand ils sont trop grands pour leur vélo actuel",
"title": "Vélothèques"
},
+ "binoculars": {
+ "description": "Une carte des longue-vues fixes. Se trouve typiquement sur les sites touristiques, les points de vue, les tours panoramiques ou dans les réserves naturelles.",
+ "shortDescription": "Une carte de jumelles panoramiques",
+ "title": "Jumelles"
+ },
"bookcases": {
"description": "Une microbibliothèques, également appelée boite à livre, est un élément de mobilier urbain (étagère, armoire, etc) dans lequel sont stockés des livres et autres objets en accès libre. Découvrez les boites à livres prêt de chez vous, ou ajouter en une nouvelle à l'aide de votre compte OpenStreetMap.",
"title": "Carte des microbibliothèques"
},
+ "cafes_and_pubs": {
+ "description": "Bars et pubs",
+ "title": "Cafés et pubs"
+ },
"campersite": {
"description": "Ce site collecte les zones de camping officielles ainsi que les aires de vidange. Il est possible d’ajouter des détails à propos des services proposés ainsi que leurs coûts. Ajoutez vos images et avis. C’est un site et une application. Les données sont stockées sur OpenStreetMap, elles seront toujours gratuites et peuvent être réutilisées par n’importe quelle application.",
"layers": {
@@ -252,6 +261,11 @@
"shortDescription": "Trouver des sites pour passer la nuit avec votre camping-car",
"title": "Sites pour camping-cars"
},
+ "charging_stations": {
+ "description": "Sur cette carte l’on trouve et ajoute des informations sur les points de recharge",
+ "shortDescription": "Une carte mondiale des points de recharge",
+ "title": "Points de recharge"
+ },
"climbing": {
"description": "Cette carte indique les sites d’escalades comme les salles d’escalade ou les sites naturels.",
"descriptionTail": "La carte des sites d'escalade a été créée par Christian Neumann. Merci de le contacter pour des avis ou des questions.
",
@@ -335,6 +349,16 @@
}
},
"question": "Est-il possible d’escalader à la moulinette ?"
+ },
+ "9": {
+ "mappings": {
+ "0": {
+ "then": "De l’escalade est possible ici"
+ },
+ "1": {
+ "then": "L’escalade est impossible ici"
+ }
+ }
}
},
"units+": {
@@ -352,6 +376,21 @@
},
"title": "Open Climbing Map"
},
+ "cycle_highways": {
+ "description": "Cette carte affiche les aménagements cyclables",
+ "layers": {
+ "0": {
+ "name": "Aménagements cyclables",
+ "title": {
+ "render": "Aménagement cyclable"
+ }
+ }
+ },
+ "title": "Aménagements cyclables"
+ },
+ "cycle_infra": {
+ "description": "Une carte montrant les aménagements cyclables et où l’on peut rajouter des informations. Réalisée durant #osoc21."
+ },
"cyclofix": {
"description": "Le but de cette carte est de présenter aux cyclistes une solution facile à utiliser pour trouver l'infrastructure appropriée à leurs besoins.
Vous pouvez suivre votre localisation précise (mobile uniquement) et sélectionner les couches qui vous concernent dans le coin inférieur gauche. Vous pouvez également utiliser cet outil pour ajouter ou modifier des épingles (points d'intérêt) sur la carte et fournir plus de données en répondant aux questions.
Toutes les modifications que vous apportez seront automatiquement enregistrées dans la base de données mondiale d'OpenStreetMap et peuvent être librement réutilisées par d'autres.
Pour plus d'informations sur le projet cyclofix, rendez-vous sur cyclofix.osm.be.",
"title": "Cyclofix - Une carte ouverte pour les cyclistes"
@@ -516,6 +555,10 @@
"description": "Une carte indiquant les éoliennes et permettant leur édition.",
"title": "OpenWindPowerMap"
},
+ "parkings": {
+ "description": "Cette carte affiche différents lieux de stationnement",
+ "title": "Stationnement"
+ },
"personal": {
"description": "Crée un thème personnalisé basé sur toutes les couches disponibles de tous les thèmes",
"title": "Thème personnalisé"
diff --git a/langs/themes/nb_NO.json b/langs/themes/nb_NO.json
index 1d541c65a4..12fd79fccf 100644
--- a/langs/themes/nb_NO.json
+++ b/langs/themes/nb_NO.json
@@ -11,6 +11,9 @@
"shortDescription": "Et benkekart",
"title": "Benker"
},
+ "bicycle_rental": {
+ "title": "Sykkelutleie"
+ },
"bicyclelib": {
"title": "Sykkelbibliotek"
},
diff --git a/langs/themes/zh_Hans.json b/langs/themes/zh_Hans.json
new file mode 100644
index 0000000000..f124b98086
--- /dev/null
+++ b/langs/themes/zh_Hans.json
@@ -0,0 +1,14 @@
+{
+ "aed": {
+ "description": "在这份地图上可以寻找和标记附近的除颤器",
+ "title": "Open AED Map"
+ },
+ "artwork": {
+ "description": "欢迎使用Open Artwork Map,一个雕塑、半身像、涂鸦和其他全球艺术品的地图",
+ "title": "Open Artwork Map"
+ },
+ "benches": {
+ "shortDescription": "长椅地图",
+ "title": "长椅"
+ }
+}
\ No newline at end of file
diff --git a/langs/zh_Hans.json b/langs/zh_Hans.json
index 9e26dfeeb6..380347ac9f 100644
--- a/langs/zh_Hans.json
+++ b/langs/zh_Hans.json
@@ -1 +1,29 @@
-{}
\ No newline at end of file
+{
+ "centerMessage": {
+ "loadingData": "加载数据中……",
+ "ready": "完成!",
+ "retrying": "加载数据失败。将在 {count} 秒后重试……",
+ "zoomIn": "放大以查看或编辑数据"
+ },
+ "delete": {
+ "cancel": "取消",
+ "delete": "删除",
+ "explanations": {
+ "hardDelete": "这个点将在OpenStreetMap中被删除。它可以被有经验的贡献者恢复"
+ },
+ "notEnoughExperience": "这个点由其他人创建。",
+ "onlyEditedByLoggedInUser": "这个点仅被您所编辑,您可以安全的删除它。",
+ "useSomethingElse": "使用其他OpenStreetMap编辑器来删除它",
+ "whyDelete": "为什么这个点需要被删除?"
+ },
+ "favourite": {
+ "reload": "重新加载数据"
+ },
+ "general": {
+ "aboutMapcomplete": "
关于MapComplete
使用它在特定主题上追加OpenStreetMap信息。 Answer questions, and within minutes your contributions are available everywhere. 主题维护者为它定义元素、问题和语言。
发现更多
MapComplete always offers the next step to learn more about OpenStreetMap.
当嵌入在网站的时候,iframe连接到一个全屏的MapComplete
全屏版本提供OpenStreetMap信息
无需登录即可查看,但编辑需要一个OSM账号。
若您未登录则将被要求登录
每次当您回答一条问题,您都可以向地图添加新的点
After a while, actual OSM-tags are shown, later linking to the wiki
Did you notice an issue? Do you have a feature request? Want to help translate? Head over to the source code orissue tracker.