diff --git a/Docs/Integrating_Maproulette.md b/Docs/Integrating_Maproulette.md new file mode 100644 index 000000000..cc0e73c96 --- /dev/null +++ b/Docs/Integrating_Maproulette.md @@ -0,0 +1,129 @@ +# Integrating MapRoulette + +[MapRoulette](https://www.maproulette.org/) is a website which has challenges. A challenge is a collection of _microtasks_, i.e. mapping +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.) + +## The API + +**Most of the heavy lifting is done in layer `maproulette`. 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 +to query _all_ tasks in a bbox and returns this as geojson. Hint: +use [the maproulette theme in debug mode](https://mapcomplete.osm.be/maproulette?debug=true) to inspect all properties. + +To view the overview a single challenge, visit `https://maproulette.org/browse/challenges/` with your +browser. +The API endpoint for a single challenge is `https://maproulette.org/api/v2/challenge/view/` which returns a +geojson. + +## Displaying MapRoulette data in MapComplete + +As you'll probably want to link MapComplete to your challenge, reuse [maproulette_challenge](Docs/Layers/maproulette_challenge.md). +It has a basic framework already to load the tags. + +Of course, interacting with the tasks is the next step. + +### Detecting nearby features + +You can use [`calculatedTags`](./Docs/CalculatedTags.md) to add a small piece of code to e.g. detect nearby entities. + +The following example is to match hotels: + +``` + "calculatedTags": [ + "_closest_osm_hotel=feat.closest('hotel')?.properties?.id", + "_closest_osm_hotel_distance=feat.distanceTo(feat.properties._closest_osm_hotel)", + "_has_closeby_feature=Number(feat.properties._closest_osm_hotel_distance) < 50 ? 'yes' : 'no'" + ], +``` + +This can be used to decide if tags should be applied on an existing object or a new point should be created. + + +### Creating a new point based on a maproulette challenge (Guided import) + +**Requirement**: the MapRoulette task should have `tags` added. + +One can add `import`-button in the featureInfoBox ([documentation here](./Docs/SpecialRenderings.md#importbutton)). +Note that the import button has support for MapRoulette and is able to close the task if the property containing the maproulette-id is given: + +```json +{ + "id": "import-button", + "render": { + "special": { + "type": "import_button", + "targetLayer": "", + "tags": "tags", -- should stay 'tags' + "maproulette_id": "mr_taskId", -- important to get the task closed + "text": { + "en": "Import this point" -- or a nice message + }, + "icon": "./assets/svg/addSmall.svg", -- optional, feel free to change + "location_picker": "photo", -- optional, background layer to pinpoint the hotel + } + } +} +``` + + +### Applying tags to already existing features + +For this, [the `tag_apply`-button can be used](./Docs/SpecialRenderings.md#tagapply). + +The following example uses the calculated tags `_has_closeby_feature` and `_closest_osm_hotel`. These were added by a small extra script using `calculatedTagss`. + +```json + + { + "id": "tag-apply-button", + "condition": "_has_closeby_feature=yes", -- don't show if no feature to add to + "render": { + "special": { + "type": "tag_apply", + "tags_to_apply": "$tags", -- note the '$', property containing the tags + "id_of_object_to_apply_this_one": "_closest_osm_hotel" -- id of the feature to add those tags to + "message": { + "en": "Add all the suggested tags" + }, + "image": "./assets/svg/addSmall.svg", + } + } + } + +``` + +## 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. + + +(The following example is not tested and might be wrong.) + +``` +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": {"type": "Point", "coordinates": [1.234, 5.678]}, + "properties": { + "tags": "foo=bar;name=xyz", + "blurb": "Please review this item and add it..." + } + + } + + ] +} + +``` diff --git a/UI/Base/FixedUiElement.ts b/UI/Base/FixedUiElement.ts index c3cd33c87..aa8e41c27 100644 --- a/UI/Base/FixedUiElement.ts +++ b/UI/Base/FixedUiElement.ts @@ -14,6 +14,9 @@ export class FixedUiElement extends BaseUIElement { AsMarkdown(): string { if (this.HasClass("code")) { + if (this.content.indexOf("\n") > 0 || this.HasClass("block")) { + return "\n```\n" + this.content + "\n```\n" + } return "`" + this.content + "`" } if (this.HasClass("font-bold")) { diff --git a/UI/Popup/ImportButton.ts b/UI/Popup/ImportButton.ts index 037ec6be6..bedb8f5fc 100644 --- a/UI/Popup/ImportButton.ts +++ b/UI/Popup/ImportButton.ts @@ -644,7 +644,7 @@ export class ImportPointButton extends AbstractImportButton { }, { name: "maproulette_id", - doc: "If given, the maproulette challenge will be marked as fixed", + doc: "The property name of the maproulette_id - this is probably `mr_taskId`. If given, the maproulette challenge will be marked as fixed. Only use this if part of a maproulette-layer.", }, ], { showRemovedTags: false } diff --git a/UI/SpecialVisualizations.ts b/UI/SpecialVisualizations.ts index b9b16a878..d97074927 100644 --- a/UI/SpecialVisualizations.ts +++ b/UI/SpecialVisualizations.ts @@ -30,7 +30,6 @@ import WikipediaBox from "./Wikipedia/WikipediaBox" import Wikidata, { WikidataResponse } from "../Logic/Web/Wikidata" import { Translation } from "./i18n/Translation" import Translations from "./i18n/Translations" -import MangroveReviews from "../Logic/Web/MangroveReviews" import ReviewForm from "./Reviews/ReviewForm" import ReviewElement from "./Reviews/ReviewElement" import OpeningHoursVisualization from "./OpeningHours/OpeningHoursVisualization" @@ -480,6 +479,10 @@ export default class SpecialVisualizations { args: [], constr(state, tagSource, argument, guistate) { let parentId = tagSource.data.mr_challengeId + if (parentId === undefined) { + console.warn("Element ", tagSource.data.id, " has no mr_challengeId") + return undefined + } let challenge = Stores.FromPromise( Utils.downloadJsonCached( `https://maproulette.org/api/v2/challenge/${parentId}`, @@ -512,7 +515,7 @@ export default class SpecialVisualizations { }) ) }, - docs: "Show details of a MapRoulette task", + 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: "statistics", @@ -725,13 +728,6 @@ export default class SpecialVisualizations { render: { special: { type: "some_special_visualisation", - 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", - }, argname: "some_arg", message: { en: "some other really long message", @@ -739,12 +735,20 @@ export default class SpecialVisualizations { }, 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 b12af4b80..84e91bced 100644 --- a/assets/layers/maproulette/maproulette.json +++ b/assets/layers/maproulette/maproulette.json @@ -5,6 +5,11 @@ "geoJsonZoomLevel": 16, "osmTags": "title~*" }, + "description": { + "en": "Layer showing all tasks in MapRoulette", + "de": "Ebene, die alle MapRoulette-Aufgaben zeigt", + "nl": "Laag met alle taken uit MapRoulette" + }, "mapRendering": [ { "location": [ @@ -128,11 +133,6 @@ "render": "{blurb}" } ], - "description": { - "en": "Layer showing all tasks in MapRoulette", - "de": "Ebene, die alle MapRoulette-Aufgaben zeigt", - "nl": "Laag met alle taken uit MapRoulette" - }, "minzoom": 15, "name": { "en": "MapRoulette Tasks", diff --git a/scripts/generateDocs.ts b/scripts/generateDocs.ts index b92d519fd..a66cf8e3d 100644 --- a/scripts/generateDocs.ts +++ b/scripts/generateDocs.ts @@ -64,7 +64,14 @@ function WriteFile( md.replace(/\n\n\n+/g, "\n\n") - writeFileSync(filename, md) + if (!md.endsWith("\n")) { + md += "\n" + } + + const warnAutomated = + "[//]: # (WARNING: this file is automatically generated. Please find the sources at the bottom and edit those sources)" + + writeFileSync(filename, warnAutomated + md) } /**