diff --git a/Docs/Integrating_Maproulette.md b/Docs/Integrating_Maproulette.md index cc0e73c96..701b44e3d 100644 --- a/Docs/Integrating_Maproulette.md +++ b/Docs/Integrating_Maproulette.md @@ -4,11 +4,11 @@ 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) -(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 -**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. 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": { "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 A challenge can be created on https://maproulette.org/admin/projects 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. -Furthermore, setting the property `blurb` can be useful. +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 + +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.) @@ -117,8 +124,8 @@ Furthermore, setting the property `blurb` can be useful. "type": "Feature", "geometry": {"type": "Point", "coordinates": [1.234, 5.678]}, "properties": { + "id": ... "tags": "foo=bar;name=xyz", - "blurb": "Please review this item and add it..." } } diff --git a/Docs/Layers/maproulette.md b/Docs/Layers/maproulette.md index 9ebb96e48..57bd91009 100644 --- a/Docs/Layers/maproulette.md +++ b/Docs/Layers/maproulette.md @@ -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 + + diff --git a/Docs/Layers/maproulette_challenge.md b/Docs/Layers/maproulette_challenge.md index e7941024d..05162e8ae 100644 --- a/Docs/Layers/maproulette_challenge.md +++ b/Docs/Layers/maproulette_challenge.md @@ -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 diff --git a/Docs/SpecialRenderings.md b/Docs/SpecialRenderings.md index fd905f74e..bac04e00b 100644 --- a/Docs/SpecialRenderings.md +++ b/Docs/SpecialRenderings.md @@ -118,6 +118,8 @@ In other words: use `{ "before": ..., "after": ..., "special": {"type": ..., "ar * [Example usage of title](#example-usage-of-title) + [maproulette_task](#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) * [Example usage of statistics](#example-usage-of-statistics) + [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 - `{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 Show general statistics about the elements currently in view. Intended to use on the `current_view`-layer diff --git a/Logic/Maproulette.ts b/Logic/Maproulette.ts index abaf9e65b..15e11ccba 100644 --- a/Logic/Maproulette.ts +++ b/Logic/Maproulette.ts @@ -1,7 +1,27 @@ import Constants from "../Models/Constants" 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 */ 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 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 { - const response = await fetch(`${this.endpoint}/task/${taskId}/1`, { + async closeTask( + taskId: number, + status = Maproulette.STATUS_FIXED, + options?: { + comment?: string + tags?: string + requestReview?: boolean + completionResponses?: Record + } + ): Promise { + const response = await fetch(`${this.endpoint}/task/${taskId}/${status}`, { method: "PUT", headers: { "Content-Type": "application/json", 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}`) + throw `Failed to close task: ${response.status}` } } } diff --git a/UI/Popup/ImportButton.ts b/UI/Popup/ImportButton.ts index bedb8f5fc..d8ade6184 100644 --- a/UI/Popup/ImportButton.ts +++ b/UI/Popup/ImportButton.ts @@ -702,7 +702,7 @@ export class ImportPointButton extends AbstractImportButton { Hash.hash.setData(newElementAction.newElementId) 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.ping() } @@ -720,7 +720,7 @@ export class ImportPointButton extends AbstractImportButton { ) } else { 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.ping() } diff --git a/UI/SpecialVisualizations.ts b/UI/SpecialVisualizations.ts index d97074927..60444bead 100644 --- a/UI/SpecialVisualizations.ts +++ b/UI/SpecialVisualizations.ts @@ -52,10 +52,90 @@ import StatisticsPanel from "./BigComponents/StatisticsPanel" import AutoApplyButton from "./Popup/AutoApplyButton" import { LanguageElement } from "./Popup/LanguageElement" import FeatureReviews from "../Logic/Web/MangroveReviews" +import Maproulette from "../Logic/Maproulette" export default class SpecialVisualizations { 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[] { const specialVisualizations: SpecialVisualization[] = [ new HistogramViz(), @@ -417,6 +497,24 @@ export default class SpecialVisualizations { 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) => { const isUploading = new UIEventSource(false) 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.", }, + { + 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", 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 } - - 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") - } } diff --git a/assets/layers/maproulette/maproulette.json b/assets/layers/maproulette/maproulette.json index 84e91bced..7e0e36a4a 100644 --- a/assets/layers/maproulette/maproulette.json +++ b/assets/layers/maproulette/maproulette.json @@ -53,7 +53,7 @@ } ] }, - "iconSize": "40,40,center" + "iconSize": "40,40,bottom" } ], "tagRenderings": [ @@ -128,9 +128,41 @@ ] }, { - "id": "blurb", - "condition": "blurb~*", - "render": "{blurb}" + "id": "mark_fixed", + "render": { + "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, @@ -266,4 +298,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/assets/layers/maproulette_challenge/maproulette_challenge.json b/assets/layers/maproulette_challenge/maproulette_challenge.json index ea13601a2..3f0cf546c 100644 --- a/assets/layers/maproulette_challenge/maproulette_challenge.json +++ b/assets/layers/maproulette_challenge/maproulette_challenge.json @@ -144,11 +144,6 @@ } } ] - }, - { - "id": "blurb", - "condition": "blurb~*", - "render": "{blurb}" } ], "filter": [ @@ -229,4 +224,4 @@ ] } ] -} \ No newline at end of file +}