forked from MapComplete/MapComplete
		
	Allow closing Maproulette tasks
This commit is contained in:
		
							parent
							
								
									a1bffc7b7f
								
							
						
					
					
						commit
						65997291bb
					
				
					 12 changed files with 309 additions and 165 deletions
				
			
		|  | @ -347,11 +347,12 @@ snap_onto_layers | _undefined_ | If a way of the given layer(s) is closeby, will | |||
| max_snap_distance | 5 | The maximum distance that the imported point will be moved to snap onto a way in an already existing layer (in meters). This is previewed to the contributor, similar to the 'add new point'-action of MapComplete | ||||
| note_id | _undefined_ | If given, this key will be read. The corresponding note on OSM will be closed, stating 'imported' | ||||
| location_picker | photo | Chooses the background for the precise location picker, options are 'map', 'photo' or 'osmbasedmap' or 'none' if the precise input picker should be disabled | ||||
| maproulette_id | _undefined_ | If given, the maproulette challenge will be marked as fixed | ||||
|   | ||||
| 
 | ||||
| #### Example usage of import_button  | ||||
| 
 | ||||
|  `{import_button(,,Import this data into OpenStreetMap,./assets/svg/addSmall.svg,,5,,photo)}` | ||||
|  `{import_button(,,Import this data into OpenStreetMap,./assets/svg/addSmall.svg,,5,,photo,)}` | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										39
									
								
								Logic/Maproulette.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								Logic/Maproulette.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,39 @@ | |||
| import Constants from "../Models/Constants"; | ||||
| 
 | ||||
| export default class Maproulette { | ||||
|   /** | ||||
|    * The API endpoint to use | ||||
|    */ | ||||
|   endpoint: string; | ||||
| 
 | ||||
|   /** | ||||
|    * The API key to use for all requests | ||||
|    */ | ||||
|   private apiKey: string; | ||||
| 
 | ||||
|   /** | ||||
|    * Creates a new Maproulette instance | ||||
|    * @param endpoint The API endpoint to use | ||||
|    */ | ||||
|   constructor(endpoint: string = "https://maproulette.org/api/v2") { | ||||
|     this.endpoint = endpoint; | ||||
|     this.apiKey = Constants.MaprouletteApiKey; | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Close a task | ||||
|    * @param taskId The task to close | ||||
|    */ | ||||
|   async closeTask(taskId: number): Promise<void> { | ||||
|     const response = await fetch(`${this.endpoint}/task/${taskId}/1`, { | ||||
|       method: "PUT", | ||||
|       headers: { | ||||
|         "Content-Type": "application/json", | ||||
|         "apiKey": this.apiKey, | ||||
|       }, | ||||
|     }); | ||||
|     if (response.status !== 304) { | ||||
|       console.log(`Failed to close task: ${response.status}`); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | @ -13,6 +13,7 @@ import ChangeToElementsActor from "../Actors/ChangeToElementsActor"; | |||
| import PendingChangesUploader from "../Actors/PendingChangesUploader"; | ||||
| import * as translators from "../../assets/translators.json" | ||||
| import {post} from "jquery"; | ||||
| import Maproulette from "../Maproulette"; | ||||
|          | ||||
| /** | ||||
|  * The part of the state which keeps track of user-related stuff, e.g. the OSM-connection, | ||||
|  | @ -34,6 +35,11 @@ export default class UserRelatedState extends ElementsState { | |||
|      */ | ||||
|     public mangroveIdentity: MangroveIdentity; | ||||
| 
 | ||||
|     /** | ||||
|      * Maproulette connection | ||||
|      */ | ||||
|     public maprouletteConnection: Maproulette; | ||||
| 
 | ||||
|     public readonly isTranslator : Store<boolean>; | ||||
|      | ||||
|     public readonly installedUserThemes: Store<string[]> | ||||
|  | @ -80,6 +86,8 @@ export default class UserRelatedState extends ElementsState { | |||
|             this.osmConnection.GetLongPreference("identity", "mangrove") | ||||
|         ); | ||||
| 
 | ||||
|         this.maprouletteConnection = new Maproulette(); | ||||
| 
 | ||||
|         if (layoutToUse?.hideFromOverview) { | ||||
|             this.osmConnection.isLoggedIn.addCallbackAndRunD(loggedIn => { | ||||
|                 if (loggedIn) { | ||||
|  |  | |||
|  | @ -6,6 +6,8 @@ export default class Constants { | |||
|      | ||||
|     public static ImgurApiKey = '7070e7167f0a25a' | ||||
|     public static readonly mapillary_client_token_v4 = "MLY|4441509239301885|b40ad2d3ea105435bd40c7e76993ae85" | ||||
|     // Currently there is no user-friendly way to get the user's API key. See https://github.com/maproulette/maproulette2/issues/476 for more information.
 | ||||
|     public static readonly MaprouletteApiKey = ""; | ||||
| 
 | ||||
|     public static defaultOverpassUrls = [ | ||||
|         // The official instance, 10000 queries per day per project allowed
 | ||||
|  |  | |||
|  | @ -550,15 +550,21 @@ export class ImportPointButton extends AbstractImportButton { | |||
|                     name: "note_id", | ||||
|                     doc: "If given, this key will be read. The corresponding note on OSM will be closed, stating 'imported'" | ||||
|                 }, | ||||
|                 {name:"location_picker", | ||||
|                 { | ||||
|                     name:"location_picker", | ||||
|                     defaultValue: "photo", | ||||
|                 doc: "Chooses the background for the precise location picker, options are 'map', 'photo' or 'osmbasedmap' or 'none' if the precise input picker should be disabled"}], | ||||
|                     doc: "Chooses the background for the precise location picker, options are 'map', 'photo' or 'osmbasedmap' or 'none' if the precise input picker should be disabled" | ||||
|                 }, | ||||
|                 { | ||||
|                     name: "maproulette_id", | ||||
|                     doc: "If given, the maproulette challenge will be marked as fixed" | ||||
|                 }], | ||||
|             { showRemovedTags: false} | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     private static createConfirmPanelForPoint( | ||||
|         args: { max_snap_distance: string, snap_onto_layers: string, icon: string, text: string, newTags: UIEventSource<any>, targetLayer: string, note_id: string }, | ||||
|         args: { max_snap_distance: string, snap_onto_layers: string, icon: string, text: string, newTags: UIEventSource<any>, targetLayer: string, note_id: string, maproulette_id: string }, | ||||
|         state: FeaturePipelineState, | ||||
|         guiState: DefaultGuiState, | ||||
|         originalFeatureTags: UIEventSource<any>, | ||||
|  | @ -600,6 +606,14 @@ export class ImportPointButton extends AbstractImportButton { | |||
|                 originalFeatureTags.data["closed_at"] = new Date().toISOString() | ||||
|                 originalFeatureTags.ping() | ||||
|             } | ||||
| 
 | ||||
|             let maproulette_id = originalFeatureTags.data[args.maproulette_id]; | ||||
|             console.log("Checking if we need to mark a maproulette challenge as fixed (" + maproulette_id + ")") | ||||
|             if (maproulette_id !== undefined) { | ||||
|                 // Fetch MapRoulette API key, then use it to mark the challenge as fixed
 | ||||
|                 console.log("Marking maproulette challenge as fixed") | ||||
|                 state.maprouletteConnection.closeTask(Number(maproulette_id)); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         let preciseInputOption = args["location_picker"] | ||||
|  |  | |||
|  | @ -1,147 +1,147 @@ | |||
| { | ||||
|     "id": "doctors", | ||||
|     "name": { | ||||
|         "en": "doctors" | ||||
|   "id": "doctors", | ||||
|   "name": { | ||||
|     "en": "doctors" | ||||
|   }, | ||||
|   "description": { | ||||
|     "en": "This layer shows doctor offices, dentists and other healthcare facilities" | ||||
|   }, | ||||
|   "source": { | ||||
|     "osmTags": { | ||||
|       "or": [ | ||||
|         "amenity=doctors", | ||||
|         "amenity=dentist", | ||||
|         "healthcare=physiotherapist" | ||||
|       ] | ||||
|     } | ||||
|   }, | ||||
|   "title": { | ||||
|     "render": { | ||||
|       "en": "Doctors Office {name}" | ||||
|     }, | ||||
|     "description": { | ||||
|         "en": "This layer shows doctor offices, dentists and other healthcare facilities" | ||||
|     }, | ||||
|     "source": { | ||||
|         "osmTags": { | ||||
|             "or": [ | ||||
|                 "amenity=doctors", | ||||
|                 "amenity=dentist", | ||||
|                 "healthcare=physiotherapist" | ||||
|             ] | ||||
|         } | ||||
|     }, | ||||
|     "title": { | ||||
|         "render": { | ||||
|             "en": "Doctors Office {name}" | ||||
|         }, | ||||
|         "mappings": [ | ||||
|             { | ||||
|                 "if": "amenity=doctors", | ||||
|                 "then": "Doctors Office {name}" | ||||
|             }, | ||||
|             { | ||||
|                 "if": "amenity=dentist", | ||||
|                 "then": "Dentists office {name}" | ||||
|             }, | ||||
|             { | ||||
|                 "if": "healthcare=physiotherapist", | ||||
|                 "then": "Physiotherapists office {name}" | ||||
|             } | ||||
|         ] | ||||
|     }, | ||||
|     "minzoom": 13, | ||||
|     "tagRenderings": [ | ||||
|         "images", | ||||
|         "opening_hours", | ||||
|         "phone", | ||||
|         "email", | ||||
|         "website", | ||||
|         { | ||||
|             "question": { | ||||
|                 "en": "What is the name of this doctors place?" | ||||
|             }, | ||||
|             "render": { | ||||
|                 "en": "This doctors place is called {name}" | ||||
|             }, | ||||
|             "freeform": { | ||||
|                 "key": "name" | ||||
|             }, | ||||
|             "id": "name" | ||||
|         }, | ||||
|         { | ||||
|             "condition": "amenity=doctors", | ||||
|             "id": "specialty", | ||||
|             "render": { | ||||
|                 "en": "This doctor is specialized in {healthcare:speciality}" | ||||
|             }, | ||||
|             "question": { | ||||
|                 "en": "What is this doctor specialized in?" | ||||
|             }, | ||||
|             "freeform": { | ||||
|                 "key": "healthcare:speciality" | ||||
|             }, | ||||
|             "mappings": [ | ||||
|                 { | ||||
|                     "if": "healthcare:speciality=general", | ||||
|                     "then": { | ||||
|                         "en": "This is a general practitioner" | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     "if": "healthcare:speciality=gynaecology", | ||||
|                     "then": { | ||||
|                         "en": "This is a gynaecologist" | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     "if": "healthcare:speciality=psychiatry", | ||||
|                     "then": { | ||||
|                         "en": "This is a psychiatrist" | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     "if": "healthcare:speciality=paediatrics", | ||||
|                     "then": { | ||||
|                         "en": "This is a paediatrician" | ||||
|                     } | ||||
|                 } | ||||
|             ] | ||||
|         } | ||||
|     ], | ||||
|     "presets": [ | ||||
|         { | ||||
|             "title": { | ||||
|                 "en": "a doctors office" | ||||
|             }, | ||||
|             "tags": [ | ||||
|                 "amenity=doctors" | ||||
|             ] | ||||
|         }, | ||||
|         { | ||||
|             "title": { | ||||
|                 "en": "a dentists office" | ||||
|             }, | ||||
|             "tags": [ | ||||
|                 "amenity=dentist" | ||||
|             ] | ||||
|         }, | ||||
|         { | ||||
|             "title": { | ||||
|                 "en": "a physiotherapists office" | ||||
|             }, | ||||
|             "tags": [ | ||||
|                 "healthcare=physiotherapist" | ||||
|             ] | ||||
|         } | ||||
|     ], | ||||
|     "filter": [ | ||||
|         { | ||||
|             "id": "opened-now", | ||||
|             "options": [ | ||||
|                 { | ||||
|                     "question": { | ||||
|                         "en": "Opened now" | ||||
|                     }, | ||||
|                     "osmTags": "_isOpen=yes" | ||||
|                 } | ||||
|             ] | ||||
|         } | ||||
|     ], | ||||
|     "mapRendering": [ | ||||
|         { | ||||
|             "icon": { | ||||
|                 "render": "circle:white;./assets/layers/doctors/doctors.svg" | ||||
|             }, | ||||
|             "iconSize": "40,40,center", | ||||
|             "location": [ | ||||
|                 "point", | ||||
|                 "centroid" | ||||
|             ] | ||||
|         } | ||||
|     "mappings": [ | ||||
|       { | ||||
|         "if": "amenity=doctors", | ||||
|         "then": "Doctors Office {name}" | ||||
|       }, | ||||
|       { | ||||
|         "if": "amenity=dentist", | ||||
|         "then": "Dentists office {name}" | ||||
|       }, | ||||
|       { | ||||
|         "if": "healthcare=physiotherapist", | ||||
|         "then": "Physiotherapists office {name}" | ||||
|       } | ||||
|     ] | ||||
|   }, | ||||
|   "minzoom": 13, | ||||
|   "tagRenderings": [ | ||||
|     "images", | ||||
|     "opening_hours", | ||||
|     "phone", | ||||
|     "email", | ||||
|     "website", | ||||
|     { | ||||
|       "question": { | ||||
|         "en": "What is the name of this doctors place?" | ||||
|       }, | ||||
|       "render": { | ||||
|         "en": "This doctors place is called {name}" | ||||
|       }, | ||||
|       "freeform": { | ||||
|         "key": "name" | ||||
|       }, | ||||
|       "id": "name" | ||||
|     }, | ||||
|     { | ||||
|       "condition": "amenity=doctors", | ||||
|       "id": "specialty", | ||||
|       "render": { | ||||
|         "en": "This doctor is specialized in {healthcare:speciality}" | ||||
|       }, | ||||
|       "question": { | ||||
|         "en": "What is this doctor specialized in?" | ||||
|       }, | ||||
|       "freeform": { | ||||
|         "key": "healthcare:speciality" | ||||
|       }, | ||||
|       "mappings": [ | ||||
|         { | ||||
|           "if": "healthcare:speciality=general", | ||||
|           "then": { | ||||
|             "en": "This is a general practitioner" | ||||
|           } | ||||
|         }, | ||||
|         { | ||||
|           "if": "healthcare:speciality=gynaecology", | ||||
|           "then": { | ||||
|             "en": "This is a gynaecologist" | ||||
|           } | ||||
|         }, | ||||
|         { | ||||
|           "if": "healthcare:speciality=psychiatry", | ||||
|           "then": { | ||||
|             "en": "This is a psychiatrist" | ||||
|           } | ||||
|         }, | ||||
|         { | ||||
|           "if": "healthcare:speciality=paediatrics", | ||||
|           "then": { | ||||
|             "en": "This is a paediatrician" | ||||
|           } | ||||
|         } | ||||
|       ] | ||||
|     } | ||||
|   ], | ||||
|   "presets": [ | ||||
|     { | ||||
|       "title": { | ||||
|         "en": "a doctors office" | ||||
|       }, | ||||
|       "tags": [ | ||||
|         "amenity=doctors" | ||||
|       ] | ||||
|     }, | ||||
|     { | ||||
|       "title": { | ||||
|         "en": "a dentists office" | ||||
|       }, | ||||
|       "tags": [ | ||||
|         "amenity=dentist" | ||||
|       ] | ||||
|     }, | ||||
|     { | ||||
|       "title": { | ||||
|         "en": "a physiotherapists office" | ||||
|       }, | ||||
|       "tags": [ | ||||
|         "healthcare=physiotherapist" | ||||
|       ] | ||||
|     } | ||||
|   ], | ||||
|   "filter": [ | ||||
|     { | ||||
|       "id": "opened-now", | ||||
|       "options": [ | ||||
|         { | ||||
|           "question": { | ||||
|             "en": "Opened now" | ||||
|           }, | ||||
|           "osmTags": "_isOpen=yes" | ||||
|         } | ||||
|       ] | ||||
|     } | ||||
|   ], | ||||
|   "mapRendering": [ | ||||
|     { | ||||
|       "icon": { | ||||
|         "render": "circle:white;./assets/layers/doctors/doctors.svg" | ||||
|       }, | ||||
|       "iconSize": "40,40,center", | ||||
|       "location": [ | ||||
|         "point", | ||||
|         "centroid" | ||||
|       ] | ||||
|     } | ||||
|   ] | ||||
| } | ||||
|  | @ -7,8 +7,7 @@ | |||
|     "en": "A layer showing pedestrian crossings with rainbow paintings" | ||||
|   }, | ||||
|   "source": { | ||||
|     "osmTags":  | ||||
|       "highway=crossing" | ||||
|     "osmTags": "highway=crossing" | ||||
|   }, | ||||
|   "minzoom": 17, | ||||
|   "title": { | ||||
|  | @ -38,7 +37,7 @@ | |||
|   ], | ||||
|   "tagRenderings": [ | ||||
|     "images", | ||||
|     {  | ||||
|     { | ||||
|       "id": "crossing-with-rainbow", | ||||
|       "question": { | ||||
|         "en": "Does this crossing has rainbow paintings?" | ||||
|  | @ -77,10 +76,12 @@ | |||
|     { | ||||
|       "icon": { | ||||
|         "render": "./assets/themes/rainbow_crossings/crossing.svg", | ||||
|         "mappings": [{ | ||||
|           "if": "crossing:marking=rainbow", | ||||
|           "then": "./assets/themes/rainbow_crossings/logo.svg" | ||||
|         }] | ||||
|         "mappings": [ | ||||
|           { | ||||
|             "if": "crossing:marking=rainbow", | ||||
|             "then": "./assets/themes/rainbow_crossings/logo.svg" | ||||
|           } | ||||
|         ] | ||||
|       }, | ||||
|       "iconSize": "40,40,center", | ||||
|       "location": [ | ||||
|  | @ -89,4 +90,4 @@ | |||
|       ] | ||||
|     } | ||||
|   ] | ||||
| } | ||||
| } | ||||
|  | @ -1,10 +1,10 @@ | |||
| { | ||||
|   "id": "onwheels", | ||||
|   "title": { | ||||
|       "en": "OnWheels" | ||||
|     "en": "OnWheels" | ||||
|   }, | ||||
|   "description": { | ||||
|       "en": "On this map, publicly weelchair accessible places are shown and can be easily added" | ||||
|     "en": "On this map, publicly weelchair accessible places are shown and can be easily added" | ||||
|   }, | ||||
|   "maintainer": "MapComplete", | ||||
|   "icon": "./assets/themes/onwheels/crest.svg", | ||||
|  | @ -29,11 +29,11 @@ | |||
|     "viewpoint", | ||||
|     "doctors" | ||||
|   ], | ||||
|   "overrideAll" : { | ||||
|     "minzoom" : "15", | ||||
|     "mapRendering" : [ | ||||
|   "overrideAll": { | ||||
|     "minzoom": "15", | ||||
|     "mapRendering": [ | ||||
|       { | ||||
|         "label" : null | ||||
|         "label": null | ||||
|       } | ||||
|     ] | ||||
|   } | ||||
|  |  | |||
|  | @ -24,7 +24,9 @@ | |||
|         "=presets": [], | ||||
|         "source": { | ||||
|           "osmTags": { | ||||
|             "and+": ["crossing:marking=rainbow"] | ||||
|             "and+": [ | ||||
|               "crossing:marking=rainbow" | ||||
|             ] | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|  | @ -38,6 +40,4 @@ | |||
|       } | ||||
|     } | ||||
|   ] | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
|  | @ -51,6 +51,43 @@ | |||
|       "tagRenderings": [ | ||||
|         "all_tags" | ||||
|       ] | ||||
|     }, | ||||
|     { | ||||
|       "id": "maproulette", | ||||
|       "name": "Maproulette Tasks", | ||||
|       "source": { | ||||
|         "osmTags": "id~*", | ||||
|         "geoJson": "https://maproulette.org/api/v2/challenge/view/27971", | ||||
|         "isOsmCache": false | ||||
|       }, | ||||
|       "calculatedTags": [ | ||||
|         "_closest_osm_street_lamp=feat.closest('street_lamps')?.properties?.id", | ||||
|         "_closest_osm_street_lamp_distance=feat.distanceTo(feat.properties._closest_osm_street_lamp)", | ||||
|         "_has_closeby_feature=Number(feat.properties._closest_osm_street_lamp_distance) < 5 ? 'yes' : 'no'" | ||||
|       ], | ||||
|       "title": "Straatlantaarn in Maproulette", | ||||
|       "mapRendering": [ | ||||
|         { | ||||
|           "location": [ | ||||
|             "point", | ||||
|             "centroid" | ||||
|           ], | ||||
|           "icon": "circle:black", | ||||
|           "iconSize": "20,20,center" | ||||
|         } | ||||
|       ], | ||||
|       "tagRenderings": [ | ||||
|         "all_tags", | ||||
|         { | ||||
|           "id": "link", | ||||
|           "render": "<a href='https://maproulette.org/challenge/{mr_challengeId}/task/{mr_taskId}'>View this task</a>" | ||||
|         }, | ||||
|         { | ||||
|           "id": "import", | ||||
|            | ||||
|           "render": "{import_button(street_lamps,tags,Import,./assets/svg/addSmall.svg,,,,photo,mr_taskId)}" | ||||
|         } | ||||
|       ] | ||||
|     } | ||||
|   ], | ||||
|   "hideFromOverview": true | ||||
|  |  | |||
|  | @ -3256,6 +3256,7 @@ | |||
|         "name": "Direction visualization" | ||||
|     }, | ||||
|     "doctors": { | ||||
|         "description": "This layer shows doctor offices, dentists and other healthcare facilities", | ||||
|         "filter": { | ||||
|             "0": { | ||||
|                 "options": { | ||||
|  | @ -3278,6 +3279,10 @@ | |||
|             } | ||||
|         }, | ||||
|         "tagRenderings": { | ||||
|             "name": { | ||||
|                 "question": "What is the name of this doctors place?", | ||||
|                 "render": "This doctors place is called {name}" | ||||
|             }, | ||||
|             "specialty": { | ||||
|                 "mappings": { | ||||
|                     "0": { | ||||
|  | @ -5074,6 +5079,35 @@ | |||
|             "render": "Bookcase" | ||||
|         } | ||||
|     }, | ||||
|     "rainbow_crossings": { | ||||
|         "description": "A layer showing pedestrian crossings with rainbow paintings", | ||||
|         "name": "Crossings with rainbow paintings", | ||||
|         "presets": { | ||||
|             "0": { | ||||
|                 "description": "Pedestrian crossing", | ||||
|                 "title": "a crossing" | ||||
|             } | ||||
|         }, | ||||
|         "tagRenderings": { | ||||
|             "crossing-with-rainbow": { | ||||
|                 "mappings": { | ||||
|                     "0": { | ||||
|                         "then": "This crossing has rainbow paintings" | ||||
|                     }, | ||||
|                     "1": { | ||||
|                         "then": "No rainbow paintings here" | ||||
|                     }, | ||||
|                     "2": { | ||||
|                         "then": "No rainbow paintings here" | ||||
|                     } | ||||
|                 }, | ||||
|                 "question": "Does this crossing has rainbow paintings?" | ||||
|             } | ||||
|         }, | ||||
|         "title": { | ||||
|             "render": "Crossing" | ||||
|         } | ||||
|     }, | ||||
|     "recycling": { | ||||
|         "description": "A layer with recycling containers and centres", | ||||
|         "filter": { | ||||
|  |  | |||
|  | @ -745,6 +745,10 @@ | |||
|         "shortDescription": "Publicly accessible towers to enjoy the view", | ||||
|         "title": "Observation towers" | ||||
|     }, | ||||
|     "onwheels": { | ||||
|         "description": "On this map, publicly weelchair accessible places are shown and can be easily added", | ||||
|         "title": "OnWheels" | ||||
|     }, | ||||
|     "openwindpowermap": { | ||||
|         "description": "A map for showing and editing wind turbines.", | ||||
|         "title": "OpenWindPowerMap" | ||||
|  | @ -867,6 +871,10 @@ | |||
|         "shortDescription": "A map showing postboxes and post offices", | ||||
|         "title": "Postbox and Post Office Map" | ||||
|     }, | ||||
|     "rainbow_crossings": { | ||||
|         "description": "On this map, rainbow-painted pedestrian crossings are shown and can be easily added", | ||||
|         "title": "Rainbow pedestrian crossings" | ||||
|     }, | ||||
|     "shops": { | ||||
|         "description": "On this map, one can mark basic information about shops, add opening hours and phone numbers", | ||||
|         "shortDescription": "An editable map with basic shop information", | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue