From 6c39f563b6bc26a7d8c1b4ae5b1421e4e7905612 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Fri, 29 Oct 2021 18:16:51 +0200 Subject: [PATCH] More work on GRB theme, add 'apply_action' button --- Docs/CalculatedTags.md | 2 +- Docs/SpecialRenderings.md | 67 ++++++++++- UI/SpecialVisualizations.ts | 182 +++++++++++++++++++++--------- assets/themes/grb_import/grb.json | 84 ++++++++++++-- langs/en.json | 4 + scripts/generateDocs.ts | 2 +- 6 files changed, 275 insertions(+), 66 deletions(-) diff --git a/Docs/CalculatedTags.md b/Docs/CalculatedTags.md index 5ca15250d..02345f1b7 100644 --- a/Docs/CalculatedTags.md +++ b/Docs/CalculatedTags.md @@ -183,7 +183,7 @@ Some advanced functions are available on **feat** as well: ### overlapWith Gives a list of features from the specified layer which this feature (partly) overlaps with. If the current feature is a point, all features that embed the point are given. The returned value is `{ feat: GeoJSONFeature, overlap: number}[]` where `overlap` is the overlapping surface are (in m²) for areas, the overlapping length (in meter) if the current feature is a line or `undefined` if the current feature is a point. - +The resulting list is sorted in descending order by overlap. The feature with the most overlap will thus be the first in the list For example to get all objects which overlap or embed from a layer, use `_contained_climbing_routes_properties=feat.overlapWith('climbing_route')` 0. ...layerIds - one or more layer ids of the layer from which every feature is checked for overlap) diff --git a/Docs/SpecialRenderings.md b/Docs/SpecialRenderings.md index 7480a053c..af4a3948b 100644 --- a/Docs/SpecialRenderings.md +++ b/Docs/SpecialRenderings.md @@ -23,6 +23,7 @@ General usage is `{func_name()}`, `{func_name(arg, someotherarg)}` or `{func_nam - [canonical](#canonical) - [import_button](#import_button) - [multi_apply](#multi_apply) + - [tag_apply](#tag_apply) @@ -191,6 +192,8 @@ key | _undefined_ | The key of the tag to give the canonical text for This button will copy the data from an external dataset into OpenStreetMap. It is only functional in official themes but can be tested in unofficial themes. +#### Importing a dataset into OpenStreetMap: requirements + If you want to import a dataset, make sure that: 1. The dataset to import has a suitable license @@ -199,17 +202,42 @@ If you want to import a dataset, make sure that: There are also some technicalities in your theme to keep in mind: -1. The new point will be added and will flow through the program as any other new point as if it came from OSM. +1. The new feature will be added and will flow through the program as any other new point as if it came from OSM. This means that there should be a layer which will match the new tags and which will display it. -2. The original point from your geojson layer will gain the tag '_imported=yes'. +2. The original feature from your geojson layer will gain the tag '_imported=yes'. This should be used to change the appearance or even to hide it (eg by changing the icon size to zero) 3. There should be a way for the theme to detect previously imported points, even after reloading. - A reference number to the original dataset is an excellen way to do this + A reference number to the original dataset is an excellent way to do this +4. When importing ways, the theme creator is also responsible of avoiding overlapping ways. + +#### Disabled in unofficial themes + +The import button can be tested in an unofficial theme by adding `test=true` or `backend=osm-test` as [URL-paramter](URL_Parameters.md). +The import button will show up then. If in testmode, you can read the changeset-XML directly in the web console. +In the case that MapComplete is pointed to the testing grounds, the edit will be made on https://master.apis.dev.openstreetmap.org + + +#### Specifying which tags to copy or add + +The first argument of the import button takes a `;`-seperated list of tags to add. + +These can either be a tag to add, such as `amenity=fast_food` or can use a substitution, e.g. `addr:housenumber=$number`. +This new point will then have the tags `amenity=fast_food` and `addr:housenumber` with the value that was saved in `number` in the original feature. + +If a value to substitute is undefined, empty string will be used instead. + +This supports multiple values, e.g. `ref=$source:geometry:type/$source:geometry:ref` + +Remark that the syntax is slightly different then expected; it uses '$' to note a value to copy, followed by a name (matched with `[a-zA-Z0-9_:]*`). Sadly, delimiting with `{}` as these already mark the boundaries of the special rendering... + +Note that these values can be prepare with javascript in the theme by using a [calculatedTag](calculatedTags.md#calculating-tags-with-javascript) + + name | default | description ------ | --------- | ------------- -tags | _undefined_ | Tags to copy-specification. This contains one or more pairs (seperated by a `;`), e.g. `amenity=fast_food; addr:housenumber=$number`. This new point will then have the tags `amenity=fast_food` and `addr:housenumber` with the value that was saved in `number` in the original feature. (Hint: prepare these values, e.g. with calculatedTags) +tags | _undefined_ | The tags to add onto the new object - see specification above text | Import this data into OpenStreetMap | The text to show on the button icon | ./assets/svg/addSmall.svg | A nice icon to show in the button minzoom | 18 | How far the contributor must zoom in before being able to import the point @@ -233,4 +261,33 @@ overwrite | _undefined_ | If set to 'true', the tags on the other objects will a #### Example usage - {multi_apply(_features_with_the_same_name_within_100m, name:etymology:wikidata;name:etymology, Apply etymology information on all nearby objects with the same name)} Generated from UI/SpecialVisualisations.ts \ No newline at end of file + {multi_apply(_features_with_the_same_name_within_100m, name:etymology:wikidata;name:etymology, Apply etymology information on all nearby objects with the same name)} + + +### tag_apply + + Shows a big button; clicking this button will apply certain tags onto the feature. + +The first argument takes a specification of which tags to add. +These can either be a tag to add, such as `amenity=fast_food` or can use a substitution, e.g. `addr:housenumber=$number`. +This new point will then have the tags `amenity=fast_food` and `addr:housenumber` with the value that was saved in `number` in the original feature. + +If a value to substitute is undefined, empty string will be used instead. + +This supports multiple values, e.g. `ref=$source:geometry:type/$source:geometry:ref` + +Remark that the syntax is slightly different then expected; it uses '$' to note a value to copy, followed by a name (matched with `[a-zA-Z0-9_:]*`). Sadly, delimiting with `{}` as these already mark the boundaries of the special rendering... + +Note that these values can be prepare with javascript in the theme by using a [calculatedTag](calculatedTags.md#calculating-tags-with-javascript) + + +name | default | description +------ | --------- | ------------- +tags_to_apply | _undefined_ | A specification of the tags to apply +message | _undefined_ | The text to show to the contributor +image | _undefined_ | An image to show to the contributor on the button +id_of_object_to_apply_this_one | _undefined_ | If specified, applies the the tags onto _another_ object. The id will be read from properties[id_of_object_to_apply_this_one] of the selected object. The tags are still calculated based on the tags of the _selected_ element + +#### Example usage + + `{tag_apply(survey_date:=$_now:date, Surveyed today!)}` Generated from UI/SpecialVisualisations.ts \ No newline at end of file diff --git a/UI/SpecialVisualizations.ts b/UI/SpecialVisualizations.ts index 72b0ece70..87af2ab7d 100644 --- a/UI/SpecialVisualizations.ts +++ b/UI/SpecialVisualizations.ts @@ -34,6 +34,10 @@ import ShowDataLayer from "./ShowDataLayer/ShowDataLayer"; import Link from "./Base/Link"; import List from "./Base/List"; import {OsmConnection} from "../Logic/Osm/OsmConnection"; +import {SubtleButton} from "./Base/SubtleButton"; +import ChangeTagAction from "../Logic/Osm/Actions/ChangeTagAction"; +import {And} from "../Logic/Tags/And"; +import Toggle from "./Input/Toggle"; export interface SpecialVisualization { funcName: string, @@ -45,6 +49,17 @@ export interface SpecialVisualization { export default class SpecialVisualizations { + private static tagsToApplyHelpText = `These can either be a tag to add, such as \`amenity=fast_food\` or can use a substitution, e.g. \`addr:housenumber=$number\`. +This new point will then have the tags \`amenity=fast_food\` and \`addr:housenumber\` with the value that was saved in \`number\` in the original feature. + +If a value to substitute is undefined, empty string will be used instead. + +This supports multiple values, e.g. \`ref=$source:geometry:type/$source:geometry:ref\` + +Remark that the syntax is slightly different then expected; it uses '$' to note a value to copy, followed by a name (matched with \`[a-zA-Z0-9_:]*\`). Sadly, delimiting with \`{}\` as these already mark the boundaries of the special rendering... + +Note that these values can be prepare with javascript in the theme by using a [calculatedTag](calculatedTags.md#calculating-tags-with-javascript) + ` public static specialVisualizations: SpecialVisualization[] = [ { @@ -222,7 +237,6 @@ export default class SpecialVisualizations { return minimap; } }, - { funcName: "sided_minimap", docs: "A small map showing _only one side_ the selected feature. *This features requires to have linerenderings with offset* as only linerenderings with a postive or negative offset will be shown. Note: in most cases, this map will be automatically introduced", @@ -305,14 +319,14 @@ export default class SpecialVisualizations { name: "key", defaultValue: "opening_hours", doc: "The tagkey from which the table is constructed." - },{ + }, { name: "prefix", defaultValue: "", - doc:"Remove this string from the start of the value before parsing. __Note: use `&LPARENs` to indicate `(` if needed__" - },{ + doc: "Remove this string from the start of the value before parsing. __Note: use `&LPARENs` to indicate `(` if needed__" + }, { name: "postfix", defaultValue: "", - doc:"Remove this string from the end of the value before parsing. __Note: use `&RPARENs` to indicate `)` if needed__" + doc: "Remove this string from the end of the value before parsing. __Note: use `&RPARENs` to indicate `)` if needed__" }], example: "A normal opening hours table can be invoked with `{opening_hours_table()}`. A table for e.g. conditional access with opening hours can be `{opening_hours_table(access:conditional, no @ &LPARENS, &RPARENS)}`", constr: (state: State, tagSource: UIEventSource, args) => { @@ -529,57 +543,21 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be The first argument of the import button takes a \`;\`-seperated list of tags to add. -These can either be a tag to add, such as \`amenity=fast_food\` or can use a substitution, e.g. \`addr:housenumber=$number\`. -This new point will then have the tags \`amenity=fast_food\` and \`addr:housenumber\` with the value that was saved in \`number\` in the original feature. - -If a value to substitute is undefined, empty string will be used instead. - -This supports multiple values, e.g. \`ref=$source:geometry:type/$source:geometry:ref\` - -Remark that the syntax is slightly different then expected; it uses '$' to note a value to copy, followed by a name (matched with \`[a-zA-Z0-9_:]*\`). Sadly, delimiting with \`{}\` as these already mark the boundaries of the special rendering... - -Note that these values can be prepare with javascript in the theme by using a [calculatedTag](calculatedTags.md#calculating-tags-with-javascript) - +${SpecialVisualizations.tagsToApplyHelpText} + `, constr: (state, tagSource, args) => { if (!state.layoutToUse.official && !(state.featureSwitchIsTesting.data || state.osmConnection._oauth_config.url === OsmConnection.oauth_configs["osm-test"].url)) { return new Combine([new FixedUiElement("The import button is disabled for unofficial themes to prevent accidents.").SetClass("alert"), new FixedUiElement("To test, add test=true or backend=osm-test to the URL. The changeset will be printed in the console. Please open a PR to officialize this theme to actually enable the import button.")]) } - const tgsSpec = args[0].split(";").map(spec => { - const kv = spec.split("=").map(s => s.trim()); - if (kv.length != 2) { - throw "Invalid key spec: multiple '=' found in " + spec - } - return kv - }) - const rewrittenTags: UIEventSource = tagSource.map(tags => { - const newTags: Tag [] = [] - for (const [key, value] of tgsSpec) { - if (value.indexOf('$') >= 0) { - - let parts = value.split("$") - // THe first of the split won't start with a '$', so no substitution needed - let actualValue = parts[0] - parts.shift() - - for (const part of parts) { - const [_, varName, leftOver] = part.match(/([a-zA-Z0-9_:]*)(.*)/) - actualValue += (tags[varName] ?? "") + leftOver - } - newTags.push(new Tag(key, actualValue)) - } else { - newTags.push(new Tag(key, value)) - } - } - return newTags - }) + const rewrittenTags = SpecialVisualizations.generateTagsToApply(args[0], tagSource) const id = tagSource.data.id; const feature = state.allElements.ContainingFeatures.get(id) const minzoom = Number(args[3]) - const message = args[1] + const message = args[1] const image = args[2] - + return new ImportButton( image, message, tagSource, rewrittenTags, feature, minzoom, state ) @@ -636,12 +614,113 @@ Note that these values can be prepare with javascript in the theme by using a [c ); } + }, + { + funcName: "tag_apply", + docs: "Shows a big button; clicking this button will apply certain tags onto the feature.\n\nThe first argument takes a specification of which tags to add.\n" + SpecialVisualizations.tagsToApplyHelpText, + args: [ + { + name: "tags_to_apply", + doc: "A specification of the tags to apply" + }, + { + name: "message", + doc: "The text to show to the contributor" + }, + { + name: "image", + doc: "An image to show to the contributor on the button" + }, + { + name: "id_of_object_to_apply_this_one", + defaultValue: undefined, + doc: "If specified, applies the the tags onto _another_ object. The id will be read from properties[id_of_object_to_apply_this_one] of the selected object. The tags are still calculated based on the tags of the _selected_ element" + } + ], + example: "`{tag_apply(survey_date:=$_now:date, Surveyed today!)}`", + constr: (state, tags, args) => { + const tagsToApply = SpecialVisualizations.generateTagsToApply(args[0], tags) + const msg = args[1] + let image = args[2]?.trim() + if (image === "" || image === "undefined") { + image = undefined + } + const targetIdKey = args[3] + const t = Translations.t.general.apply_button + + const tagsExplanation = new VariableUiElement(tagsToApply.map(tagsToApply => { + const tagsStr = tagsToApply.map(t => t.asHumanString(false, true)).join("&"); + let el: BaseUIElement = new FixedUiElement(tagsStr) + if(targetIdKey !== undefined){ + const targetId = tags.data[targetIdKey] ?? tags.data.id + el = t.appliedOnAnotherObject.Subs({tags: tagsStr , id: targetId }) + } + return el; + } + )).SetClass("subtle") + + const applied = new UIEventSource(false) + const applyButton = new SubtleButton(image, new Combine([msg, tagsExplanation]).SetClass("flex flex-col")) + .onClick(() => { + const targetId = tags.data[ targetIdKey] ?? tags.data.id + const changeAction = new ChangeTagAction(targetId, + new And(tagsToApply.data), + tags.data, // We pass in the tags of the selected element, not the tags of the target element! + { + theme: state.layoutToUse.id, + changeType: "answer" + } + ) + state.changes.applyAction(changeAction) + applied.setData(true) + }) + + + return new Toggle( + new Toggle( + t.isApplied.SetClass("thanks"), + applyButton, + applied + ) + , undefined, state.osmConnection.isLoggedIn) + } } ] - static HelpMessage: BaseUIElement = SpecialVisualizations.GenHelpMessage(); + private static generateTagsToApply(spec: string, tagSource: UIEventSource): UIEventSource { - private static GenHelpMessage() { + const tgsSpec = spec.split(";").map(spec => { + const kv = spec.split("=").map(s => s.trim()); + if (kv.length != 2) { + throw "Invalid key spec: multiple '=' found in " + spec + } + return kv + }) + return tagSource.map(tags => { + const newTags: Tag [] = [] + for (const [key, value] of tgsSpec) { + if (value.indexOf('$') >= 0) { + + let parts = value.split("$") + // THe first of the split won't start with a '$', so no substitution needed + let actualValue = parts[0] + parts.shift() + + for (const part of parts) { + const [_, varName, leftOver] = part.match(/([a-zA-Z0-9_:]*)(.*)/) + actualValue += (tags[varName] ?? "") + leftOver + } + newTags.push(new Tag(key, actualValue)) + } else { + newTags.push(new Tag(key, value)) + } + } + return newTags + }) + + } + + public static HelpMessage() { const helpTexts = SpecialVisualizations.specialVisualizations.map(viz => new Combine( @@ -651,7 +730,7 @@ Note that these values can be prepare with javascript in the theme by using a [c viz.args.length > 0 ? new Table(["name", "default", "description"], viz.args.map(arg => { let defaultArg = arg.defaultValue ?? "_undefined_" - if(defaultArg == ""){ + if (defaultArg == "") { defaultArg = "_empty string_" } return [arg.name, defaultArg, arg.doc]; @@ -665,9 +744,9 @@ Note that these values can be prepare with javascript in the theme by using a [c ] )); - + const toc = new List( - SpecialVisualizations.specialVisualizations.map(viz => new Link(viz.funcName, "#"+viz.funcName)) + SpecialVisualizations.specialVisualizations.map(viz => new Link(viz.funcName, "#" + viz.funcName)) ) return new Combine([ @@ -679,4 +758,5 @@ Note that these values can be prepare with javascript in the theme by using a [c ] ).SetClass("flex flex-col"); } + } \ No newline at end of file diff --git a/assets/themes/grb_import/grb.json b/assets/themes/grb_import/grb.json index 57d99c145..7979fe324 100644 --- a/assets/themes/grb_import/grb.json +++ b/assets/themes/grb_import/grb.json @@ -7,7 +7,8 @@ "nl": "Grb Fixup" }, "description": { - "nl": "GRB Fixup" + "nl": "GRB Fixup", + "en": "This theme is an attempt to help automating the GRB import.
Note that this is very hacky and 'steals' the GRB data from an external site; in order to do this, you need to install and activate this firefox extension for it to work." }, "language": [ "nl" @@ -23,6 +24,9 @@ "clustering": { "maxZoom": 15 }, + "overrideAll": { + "minzoom": 18 + }, "layers": [ { "id": "OSM-buildings", @@ -31,7 +35,6 @@ "osmTags": "building~*", "maxCacheAge": 0 }, - "minzoom": 16, "mapRendering": [ { "width": { @@ -67,6 +70,55 @@ ], "title": "OSM-gebouw", "tagRenderings": [ + { + "id": "building type", + "freeform": { + "key": "building" + }, + "render": "The building type is {building}", + "mappings": [ + { + "if": "building=house", + "then": "A normal house" + }, + { + "if": "building=detached", + "then": "A house detached from other building" + }, + { + "if": "building=semidetached_house", + "then": "A house sharing only one wall with another house" + }, + { + "if": "building=apartments", + "then": "An apartment building - highrise for living" + }, + { + "if": "building=office", + "then": "An office building - highrise for work" + }, + { + "if": "building=apartments", + "then": "An apartment building" + }, + { + "if": "building=shed", + "then": "A small shed, e.g. in a garden" + }, + { + "if": "building=garage", + "then": "A single garage to park a car" + }, + { + "if": "building=garages", + "then": "A building containing only garages; typically they are all identical" + }, + { + "if": "building=yes", + "then": "A building - no specification" + } + ] + }, "all_tags" ] }, @@ -99,7 +151,6 @@ }, "maxCacheAge": 0 }, - "minzoom": 18, "mapRendering": [ { "color": { @@ -124,7 +175,6 @@ "name": { "nl": "Fixmes op gebouwen" }, - "minzoom": 21, "source": { "maxCacheAge": 0, "osmTags": { @@ -314,7 +364,6 @@ "geoJsonZoomLevel": 18, "maxCacheAge": 0 }, - "minzoom": 16, "name": "CRAB-addressen", "title": "CRAB-adres", "mapRendering": [ @@ -372,7 +421,6 @@ "name": { "nl": "Fixmes op gebouwen" }, - "minzoom": 16, "source": { "maxCacheAge": 0, "osmTags": { @@ -563,7 +611,6 @@ }, "name": "GRB geometries", "title": "GRB outline", - "minzoom": 16, "calculatedTags": [ "_overlaps_with=feat.overlapWith('OSM-buildings').filter(f => f.overlap > 1 && (feat.get('_surface') < 20 || f.overlap / feat.get('_surface')) > 0.9)[0] ?? null", "_overlap_absolute=feat.get('_overlaps_with')?.overlap", @@ -587,6 +634,26 @@ "render": "
The overlapping openstreetmap-building is a {_osm_obj:building} and covers {_overlap_percentage}% of the GRB building

GRB geometry:

{minimap(21, id):height:10rem;border-radius:1rem;overflow:hidden}

OSM geometry:

{minimap(21,_osm_obj:id):height:10rem;border-radius:1rem;overflow:hidden}", "condition": "_overlaps_with!=null" }, + { + "id": "apply-id", + "render": "{tag_apply(source:geometry:date=$_grb_date; source:geometry:ref=$_grb_ref,Mark the OSM-building as imported,,_osm_obj:id)}", + "condition": { + "and": [ + "_overlaps_with!=null" + ] + } + }, + { + "id": "apply-building-type", + "render": "{tag_apply(building=$building,Use the building type from GRB,,_osm_obj:id)}", + "condition": { + "and": [ + "_overlaps_with!=null", + "_osm_obj:building=yes", + "building!=yes" + ] + } + }, { "id": "Import-button", "render": "{import_button(building=$building; source:geometry:date=$_grb_date; source:geometry:ref=$_grb_ref, Upload this building to OpenStreetMap)}", @@ -594,7 +661,8 @@ { "if": "_overlaps_with!=null", "then": "Cannot be imported directly, there is a nearly identical building geometry in OpenStreetMap" - }] + } + ] }, "all_tags" ], diff --git a/langs/en.json b/langs/en.json index 02dd71dbb..0e8640fde 100644 --- a/langs/en.json +++ b/langs/en.json @@ -131,6 +131,10 @@ "previouslyHiddenTitle": "Previously visited hidden themes", "hiddenExplanation": "These themes are only visible if you know the link. You have discovered {hidden_discovered} out of {total_hidden} hidden themes" }, + "apply_button": { + "isApplied": "The changes are applied", + "appliedOnAnotherObject": "The object {id} will receive {tags}" + }, "sharescreen": { "intro": "

Share this map

Share this map by copying the link below and sending it to friends and family:", "addToHomeScreen": "

Add to your home screen

You can easily add this website to your smartphone home screen for a native feel. Click the 'add to home screen' button in the URL bar to do this.", diff --git a/scripts/generateDocs.ts b/scripts/generateDocs.ts index 0ad24f43d..9d7951d0f 100644 --- a/scripts/generateDocs.ts +++ b/scripts/generateDocs.ts @@ -22,7 +22,7 @@ function WriteFile(filename, html: string | BaseUIElement, autogenSource: string ]).AsMarkdown()); } -WriteFile("./Docs/SpecialRenderings.md", SpecialVisualizations.HelpMessage, ["UI/SpecialVisualisations.ts"]) +WriteFile("./Docs/SpecialRenderings.md", SpecialVisualizations.HelpMessage(), ["UI/SpecialVisualisations.ts"]) WriteFile("./Docs/CalculatedTags.md", new Combine([SimpleMetaTagger.HelpText(), ExtraFunction.HelpText()]).SetClass("flex-col"), ["SimpleMetaTagger", "ExtraFunction"]) WriteFile("./Docs/SpecialInputElements.md", ValidatedTextField.HelpText(), ["ValidatedTextField.ts"]);