Improve maproulette documentation, add possibility to change the maproulette state with a special rendering

This commit is contained in:
Pieter Vander Vennet 2023-02-14 00:09:04 +01:00
parent 509b237d02
commit 2e5aef35b8
9 changed files with 334 additions and 118 deletions

View file

@ -4,11 +4,11 @@
tasks which can be solved in a few minutes. tasks which can be solved in a few minutes.
A perfect example of this is to setup such a challenge to e.g. import new points. [Important: always follow the import guidelines if you want to import data.](https://wiki.openstreetmap.org/wiki/Import/Guidelines) A perfect example of this is to setup such a challenge to e.g. import new points. [Important: always follow the import guidelines if you want to import data.](https://wiki.openstreetmap.org/wiki/Import/Guidelines)
(Another approach to set up a guided import is to create a map note for every point with the [import helper](https://mapcomplete.osm.be/import_helper). This however litters the map and will upset mappers if used with to much points.) (Another approach to set up a guided import is to create a map note for every point with the [import helper](https://mapcomplete.osm.be/import_helper). This however litters the map notes and will upset mappers if used with to much points. However, this flow is easier to setup as no changes to theme files are needed, nor is a maproulette-account needed)
## The API ## The API
**Most of the heavy lifting is done in layer `maproulette`. Extend this layer with your needs** **Most of the heavy lifting is done in [layer `maproulette-challenge`](./Docs/Layers/maproulette_challenge.md). Extend this layer with your needs.**
The API is shortly discussed here for future reference only. The API is shortly discussed here for future reference only.
There is an API-endpoint at `https://maproulette.org/api/v2/tasks/box/{x_min}/{y_min}/{x_max}/{y_max}` which can be used There is an API-endpoint at `https://maproulette.org/api/v2/tasks/box/{x_min}/{y_min}/{x_max}/{y_max}` which can be used
@ -90,21 +90,28 @@ The following example uses the calculated tags `_has_closeby_feature` and `_clos
"message": { "message": {
"en": "Add all the suggested tags" "en": "Add all the suggested tags"
}, },
"image": "./assets/svg/addSmall.svg", "image": "./assets/svg/addSmall.svg"
} }
} }
} }
``` ```
### Changing the status of the task
The easiest way is to reuse a tagrendering from the [Maproulette-layer](./Docs/Layers/maproulette.md) (_not_ the `maproulette-challenge`-layer!), such as [`maproulette.mark_fixed`](./Docs/Layers/maproulette.md#markfixed),[`maproulette.mark_duplicate`](./Docs/Layers/maproulette.md#markduplicate),[`maproulette.mark_too_hard`](./Docs/Layers/maproulette.md#marktoohard).
In the background, these use the special visualisation [`maproulette_set_status`](./Docs/SpecialRenderings.md#maproulettesetstatus) - which allows to apply different status codes or different messages/icons.
## Creating a maproulette challenge ## Creating a maproulette challenge
A challenge can be created on https://maproulette.org/admin/projects A challenge can be created on https://maproulette.org/admin/projects
This can be done with a geojson-file (or by other means). This can be done with a geojson-file (or by other means).
To create an import dataset, make a geojson file where every feature has a `tags`-field with ';'-seperated tags to add. MapRoulette works as a geojson-store with status fields added. As such, you have a bit of freedom in creating the data, but an **id** field is mandatory. A **name** tag is recommended
Furthermore, setting the property `blurb` can be useful.
To setup a guided import, add a `tags`-field with tags formatted in such a way that they are compatible with the [import-button](./Docs/SpecialRenderings.md#specifying-which-tags-to-copy-or-add)
(The following example is not tested and might be wrong.) (The following example is not tested and might be wrong.)
@ -117,8 +124,8 @@ Furthermore, setting the property `blurb` can be useful.
"type": "Feature", "type": "Feature",
"geometry": {"type": "Point", "coordinates": [1.234, 5.678]}, "geometry": {"type": "Point", "coordinates": [1.234, 5.678]},
"properties": { "properties": {
"id": ...
"tags": "foo=bar;name=xyz", "tags": "foo=bar;name=xyz",
"blurb": "Please review this item and add it..."
} }
} }

View file

@ -76,7 +76,7 @@ This tagrendering has no question and is thus read-only
### blurb ### mark_fixed
@ -84,7 +84,15 @@ This tagrendering has no question and is thus read-only
This tagrendering is only visible in the popup if the following condition is met: `blurb~.+`
### mark_duplicate
This tagrendering has no question and is thus read-only

View file

@ -99,18 +99,6 @@ This tagrendering has no question and is thus read-only
### blurb
This tagrendering has no question and is thus read-only
This tagrendering is only visible in the popup if the following condition is met: `blurb~.+`
#### Filters #### Filters

View file

@ -118,6 +118,8 @@ In other words: use `{ "before": ..., "after": ..., "special": {"type": ..., "ar
* [Example usage of title](#example-usage-of-title) * [Example usage of title](#example-usage-of-title)
+ [maproulette_task](#maproulette_task) + [maproulette_task](#maproulette_task)
* [Example usage of maproulette_task](#example-usage-of-maproulette_task) * [Example usage of maproulette_task](#example-usage-of-maproulette_task)
+ [maproulette_set_status](#maproulette_set_status)
* [Example usage of maproulette_set_status](#example-usage-of-maproulette_set_status)
+ [statistics](#statistics) + [statistics](#statistics)
* [Example usage of statistics](#example-usage-of-statistics) * [Example usage of statistics](#example-usage-of-statistics)
+ [send_email](#send_email) + [send_email](#send_email)
@ -785,7 +787,23 @@ Id-key | id | The property name where the ID of the note to close can be found
#### Example usage of add_image_to_note #### Example usage of add_image_to_note
`{add_image_to_note(id)}` The following example sets the status to '2' (false positive)
```json
{
"id": "mark_duplicate",
"render": {
"special": {
"type": "maproulette_set_status",
"message": {
"en": "Mark as not found or false positive"
},
"status": "2",
"image": "close"
}
}
}
```
@ -811,6 +829,25 @@ This reads the property `mr_challengeId` to detect the parent campaign.
### maproulette_set_status
Change the status of the given MapRoulette task
name | default | description
------ | --------- | -------------
message | _undefined_ | A message to show to the user
image | confirm | Image to show
message_confirm | _undefined_ | What to show when the task is closed, either by the user or was already closed.
status | 1 | A statuscode to apply when the button is clicked. 1 = `close`, 2 = `false_positive`, 3 = `skip`, 4 = `deleted`, 5 = `already fixed` (on the map, e.g. for duplicates), 6 = `too hard`
maproulette_id | mr_taskId | The property name containing the maproulette id
#### Example usage of maproulette_set_status
`{maproulette_set_status(,confirm,,1,mr_taskId)}`
### statistics ### statistics
Show general statistics about the elements currently in view. Intended to use on the `current_view`-layer Show general statistics about the elements currently in view. Intended to use on the `current_view`-layer

View file

@ -1,7 +1,27 @@
import Constants from "../Models/Constants" import Constants from "../Models/Constants"
export default class Maproulette { export default class Maproulette {
/** public static readonly STATUS_OPEN = 0
public static readonly STATUS_FIXED = 1
public static readonly STATUS_FALSE_POSITIVE = 2
public static readonly STATUS_SKIPPED = 3
public static readonly STATUS_DELETED = 4
public static readonly STATUS_ALREADY_FIXED = 5
public static readonly STATUS_TOO_HARD = 6
public static readonly STATUS_DISABLED = 9
public static readonly STATUS_MEANING = {
0: "Open",
1: "Fixed",
2: "False positive",
3: "Skipped",
4: "Deleted",
5: "Already fixed",
6: "Too hard",
9: "Disabled",
}
/*
* The API endpoint to use * The API endpoint to use
*/ */
endpoint: string endpoint: string
@ -21,19 +41,34 @@ export default class Maproulette {
} }
/** /**
* Close a task * Close a task; might throw an error
*
* Also see:https://maproulette.org/docs/swagger-ui/index.html?url=/assets/swagger.json&docExpansion=none#/Task/setTaskStatus
* @param taskId The task to close * @param taskId The task to close
* @param status A number indicating the status. Use MapRoulette.STATUS_*
* @param options Additional settings to pass. Refer to the API-docs for more information
*/ */
async closeTask(taskId: number): Promise<void> { async closeTask(
const response = await fetch(`${this.endpoint}/task/${taskId}/1`, { taskId: number,
status = Maproulette.STATUS_FIXED,
options?: {
comment?: string
tags?: string
requestReview?: boolean
completionResponses?: Record<string, string>
}
): Promise<void> {
const response = await fetch(`${this.endpoint}/task/${taskId}/${status}`, {
method: "PUT", method: "PUT",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
apiKey: this.apiKey, apiKey: this.apiKey,
}, },
body: options !== undefined ? JSON.stringify(options) : undefined,
}) })
if (response.status !== 304) { if (response.status !== 204) {
console.log(`Failed to close task: ${response.status}`) console.log(`Failed to close task: ${response.status}`)
throw `Failed to close task: ${response.status}`
} }
} }
} }

View file

@ -702,7 +702,7 @@ export class ImportPointButton extends AbstractImportButton {
Hash.hash.setData(newElementAction.newElementId) Hash.hash.setData(newElementAction.newElementId)
if (note_id !== undefined) { if (note_id !== undefined) {
state.osmConnection.closeNote(note_id, "imported") await state.osmConnection.closeNote(note_id, "imported")
originalFeatureTags.data["closed_at"] = new Date().toISOString() originalFeatureTags.data["closed_at"] = new Date().toISOString()
originalFeatureTags.ping() originalFeatureTags.ping()
} }
@ -720,7 +720,7 @@ export class ImportPointButton extends AbstractImportButton {
) )
} else { } else {
console.log("Marking maproulette task as fixed") console.log("Marking maproulette task as fixed")
state.maprouletteConnection.closeTask(Number(maproulette_id)) await state.maprouletteConnection.closeTask(Number(maproulette_id))
originalFeatureTags.data["mr_taskStatus"] = "Fixed" originalFeatureTags.data["mr_taskStatus"] = "Fixed"
originalFeatureTags.ping() originalFeatureTags.ping()
} }

View file

@ -52,10 +52,90 @@ import StatisticsPanel from "./BigComponents/StatisticsPanel"
import AutoApplyButton from "./Popup/AutoApplyButton" import AutoApplyButton from "./Popup/AutoApplyButton"
import { LanguageElement } from "./Popup/LanguageElement" import { LanguageElement } from "./Popup/LanguageElement"
import FeatureReviews from "../Logic/Web/MangroveReviews" import FeatureReviews from "../Logic/Web/MangroveReviews"
import Maproulette from "../Logic/Maproulette"
export default class SpecialVisualizations { export default class SpecialVisualizations {
public static specialVisualizations: SpecialVisualization[] = SpecialVisualizations.initList() public static specialVisualizations: SpecialVisualization[] = SpecialVisualizations.initList()
public static DocumentationFor(viz: string | SpecialVisualization): BaseUIElement | undefined {
if (typeof viz === "string") {
viz = SpecialVisualizations.specialVisualizations.find((sv) => sv.funcName === viz)
}
if (viz === undefined) {
return undefined
}
return new Combine([
new Title(viz.funcName, 3),
viz.docs,
viz.args.length > 0
? new Table(
["name", "default", "description"],
viz.args.map((arg) => {
let defaultArg = arg.defaultValue ?? "_undefined_"
if (defaultArg == "") {
defaultArg = "_empty string_"
}
return [arg.name, defaultArg, arg.doc]
})
)
: undefined,
new Title("Example usage of " + viz.funcName, 4),
new FixedUiElement(
viz.example ??
"`{" +
viz.funcName +
"(" +
viz.args.map((arg) => arg.defaultValue).join(",") +
")}`"
).SetClass("literal-code"),
])
}
public static HelpMessage() {
const helpTexts = SpecialVisualizations.specialVisualizations.map((viz) =>
SpecialVisualizations.DocumentationFor(viz)
)
return new Combine([
new Combine([
new Title("Special tag renderings", 1),
"In a tagrendering, some special values are substituted by an advanced UI-element. This allows advanced features and visualizations to be reused by custom themes or even to query third-party API's.",
"General usage is `{func_name()}`, `{func_name(arg, someotherarg)}` or `{func_name(args):cssStyle}`. Note that you _do not_ need to use quotes around your arguments, the comma is enough to separate them. This also implies you cannot use a comma in your args",
new Title("Using expanded syntax", 4),
`Instead of using \`{"render": {"en": "{some_special_visualisation(some_arg, some other really long message, more args)} , "nl": "{some_special_visualisation(some_arg, een boodschap in een andere taal, more args)}}\`, one can also write`,
new FixedUiElement(
JSON.stringify(
{
render: {
special: {
type: "some_special_visualisation",
argname: "some_arg",
message: {
en: "some other really long message",
nl: "een boodschap in een andere taal",
},
other_arg_name: "more args",
},
before: {
en: "Some text to prefix before the special element (e.g. a title)",
nl: "Een tekst om voor het element te zetten (bv. een titel)",
},
after: {
en: "Some text to put after the element, e.g. a footer",
},
},
},
null,
" "
)
).SetClass("code"),
'In other words: use `{ "before": ..., "after": ..., "special": {"type": ..., "argname": ...argvalue...}`. The args are in the `special` block; an argvalue can be a string, a translation or another value. (Refer to class `RewriteSpecial` in case of problems)',
]).SetClass("flex flex-col"),
...helpTexts,
]).SetClass("flex flex-col")
}
private static initList(): SpecialVisualization[] { private static initList(): SpecialVisualization[] {
const specialVisualizations: SpecialVisualization[] = [ const specialVisualizations: SpecialVisualization[] = [
new HistogramViz(), new HistogramViz(),
@ -417,6 +497,24 @@ export default class SpecialVisualizations {
defaultValue: "id", defaultValue: "id",
}, },
], ],
example:
" The following example sets the status to '2' (false positive)\n" +
"\n" +
"```json\n" +
"{\n" +
' "id": "mark_duplicate",\n' +
' "render": {\n' +
' "special": {\n' +
' "type": "maproulette_set_status",\n' +
' "message": {\n' +
' "en": "Mark as not found or false positive"\n' +
" },\n" +
' "status": "2",\n' +
' "image": "close"\n' +
" }\n" +
" }\n" +
"}\n" +
"```",
constr: (state, tags, args) => { constr: (state, tags, args) => {
const isUploading = new UIEventSource(false) const isUploading = new UIEventSource(false)
const t = Translations.t.notes const t = Translations.t.notes
@ -517,6 +615,101 @@ export default class SpecialVisualizations {
}, },
docs: "Fetches the metadata of MapRoulette campaign that this task is part of and shows those details (namely `title`, `description` and `instruction`).\n\nThis reads the property `mr_challengeId` to detect the parent campaign.", docs: "Fetches the metadata of MapRoulette campaign that this task is part of and shows those details (namely `title`, `description` and `instruction`).\n\nThis reads the property `mr_challengeId` to detect the parent campaign.",
}, },
{
funcName: "maproulette_set_status",
docs: "Change the status of the given MapRoulette task",
args: [
{
name: "message",
doc: "A message to show to the user",
},
{
name: "image",
doc: "Image to show",
defaultValue: "confirm",
},
{
name: "message_confirm",
doc: "What to show when the task is closed, either by the user or was already closed.",
},
{
name: "status",
doc: "A statuscode to apply when the button is clicked. 1 = `close`, 2 = `false_positive`, 3 = `skip`, 4 = `deleted`, 5 = `already fixed` (on the map, e.g. for duplicates), 6 = `too hard`",
defaultValue: "1",
},
{
name: "maproulette_id",
doc: "The property name containing the maproulette id",
defaultValue: "mr_taskId",
},
],
constr: (state, tagsSource, args, guistate) => {
let [message, image, message_closed, status, maproulette_id_key] = args
if (image === "") {
image = "confirm"
}
if (Svg.All[image] !== undefined || Svg.All[image + ".svg"] !== undefined) {
if (image.endsWith(".svg")) {
image = image.substring(0, image.length - 4)
}
image = Svg[image + "_ui"]()
}
const failed = new UIEventSource(false)
const closeButton = new SubtleButton(image, message).OnClickWithLoading(
Translations.t.general.loading,
async () => {
const maproulette_id =
tagsSource.data[maproulette_id_key] ?? tagsSource.data.id
try {
await state.maprouletteConnection.closeTask(
Number(maproulette_id),
Number(status),
{
tags: `MapComplete MapComplete:${state.layoutToUse.id}`,
}
)
tagsSource.data["mr_taskStatus"] =
Maproulette.STATUS_MEANING[Number(status)]
tagsSource.data.status = status
tagsSource.ping()
} catch (e) {
console.error(e)
failed.setData(true)
}
}
)
let message_closed_element = undefined
if (message_closed !== undefined && message_closed !== "") {
message_closed_element = new FixedUiElement(message_closed)
}
return new VariableUiElement(
tagsSource
.map(
(tgs) =>
tgs["status"] ??
Maproulette.STATUS_MEANING[tgs["mr_taskStatus"]]
)
.map(Number)
.map(
(status) => {
if (failed.data) {
return new FixedUiElement(
"ERROR - could not close the MapRoulette task"
).SetClass("block alert")
}
if (status === Maproulette.STATUS_OPEN) {
return closeButton
}
return message_closed_element ?? "Closed!"
},
[failed]
)
)
},
},
{ {
funcName: "statistics", funcName: "statistics",
docs: "Show general statistics about the elements currently in view. Intended to use on the `current_view`-layer", docs: "Show general statistics about the elements currently in view. Intended to use on the `current_view`-layer",
@ -674,83 +867,4 @@ export default class SpecialVisualizations {
return specialVisualizations return specialVisualizations
} }
public static DocumentationFor(viz: string | SpecialVisualization): BaseUIElement | undefined {
if (typeof viz === "string") {
viz = SpecialVisualizations.specialVisualizations.find((sv) => sv.funcName === viz)
}
if (viz === undefined) {
return undefined
}
return new Combine([
new Title(viz.funcName, 3),
viz.docs,
viz.args.length > 0
? new Table(
["name", "default", "description"],
viz.args.map((arg) => {
let defaultArg = arg.defaultValue ?? "_undefined_"
if (defaultArg == "") {
defaultArg = "_empty string_"
}
return [arg.name, defaultArg, arg.doc]
})
)
: undefined,
new Title("Example usage of " + viz.funcName, 4),
new FixedUiElement(
viz.example ??
"`{" +
viz.funcName +
"(" +
viz.args.map((arg) => arg.defaultValue).join(",") +
")}`"
).SetClass("literal-code"),
])
}
public static HelpMessage() {
const helpTexts = SpecialVisualizations.specialVisualizations.map((viz) =>
SpecialVisualizations.DocumentationFor(viz)
)
return new Combine([
new Combine([
new Title("Special tag renderings", 1),
"In a tagrendering, some special values are substituted by an advanced UI-element. This allows advanced features and visualizations to be reused by custom themes or even to query third-party API's.",
"General usage is `{func_name()}`, `{func_name(arg, someotherarg)}` or `{func_name(args):cssStyle}`. Note that you _do not_ need to use quotes around your arguments, the comma is enough to separate them. This also implies you cannot use a comma in your args",
new Title("Using expanded syntax", 4),
`Instead of using \`{"render": {"en": "{some_special_visualisation(some_arg, some other really long message, more args)} , "nl": "{some_special_visualisation(some_arg, een boodschap in een andere taal, more args)}}\`, one can also write`,
new FixedUiElement(
JSON.stringify(
{
render: {
special: {
type: "some_special_visualisation",
argname: "some_arg",
message: {
en: "some other really long message",
nl: "een boodschap in een andere taal",
},
other_arg_name: "more args",
},
before: {
en: "Some text to prefix before the special element (e.g. a title)",
nl: "Een tekst om voor het element te zetten (bv. een titel)",
},
after: {
en: "Some text to put after the element, e.g. a footer",
},
},
},
null,
" "
)
).SetClass("code"),
'In other words: use `{ "before": ..., "after": ..., "special": {"type": ..., "argname": ...argvalue...}`. The args are in the `special` block; an argvalue can be a string, a translation or another value. (Refer to class `RewriteSpecial` in case of problems)',
]).SetClass("flex flex-col"),
...helpTexts,
]).SetClass("flex flex-col")
}
} }

View file

@ -53,7 +53,7 @@
} }
] ]
}, },
"iconSize": "40,40,center" "iconSize": "40,40,bottom"
} }
], ],
"tagRenderings": [ "tagRenderings": [
@ -128,9 +128,41 @@
] ]
}, },
{ {
"id": "blurb", "id": "mark_fixed",
"condition": "blurb~*", "render": {
"render": "{blurb}" "special": {
"type": "maproulette_set_status",
"message": {
"en": "Mark as fixed"
}
}
}
},
{
"id": "mark_duplicate",
"render": {
"special": {
"type": "maproulette_set_status",
"message": {
"en": "Mark as not found or false positive"
},
"status": "2",
"image": "close"
}
}
},
{
"id": "mark_too_hard",
"render": {
"special": {
"type": "maproulette_set_status",
"message": {
"en": "Mark as too hard"
},
"status": "6",
"image": "not_found"
}
}
} }
], ],
"minzoom": 15, "minzoom": 15,
@ -266,4 +298,4 @@
] ]
} }
] ]
} }

View file

@ -144,11 +144,6 @@
} }
} }
] ]
},
{
"id": "blurb",
"condition": "blurb~*",
"render": "{blurb}"
} }
], ],
"filter": [ "filter": [
@ -229,4 +224,4 @@
] ]
} }
] ]
} }