forked from MapComplete/MapComplete
		
	Add ifnot-tags for multianswers, in order to indicate something is _not_ possible (e.g.: playment:coins=no)
This commit is contained in:
		
							parent
							
								
									972128516b
								
							
						
					
					
						commit
						ae9d93138b
					
				
					 8 changed files with 99 additions and 131 deletions
				
			
		|  | @ -4,6 +4,7 @@ import Translations from "../../UI/i18n/Translations"; | |||
| import {FromJSON} from "./FromJSON"; | ||||
| import ValidatedTextField from "../../UI/Input/ValidatedTextField"; | ||||
| import {Translation} from "../../UI/i18n/Translation"; | ||||
| import {Utils} from "../../Utils"; | ||||
| 
 | ||||
| /*** | ||||
|  * The parsed version of TagRenderingConfigJSON | ||||
|  | @ -24,9 +25,10 @@ export default class TagRenderingConfig { | |||
|     readonly multiAnswer: boolean; | ||||
| 
 | ||||
|     readonly mappings?: { | ||||
|         readonly    if: TagsFilter, | ||||
|         readonly   then: Translation | ||||
|         readonly   hideInAnswer: boolean | TagsFilter | ||||
|         readonly if: TagsFilter, | ||||
|         readonly ifnot?: TagsFilter, | ||||
|         readonly then: Translation | ||||
|         readonly hideInAnswer: boolean | TagsFilter | ||||
|     }[] | ||||
|     readonly roaming: boolean; | ||||
| 
 | ||||
|  | @ -74,7 +76,10 @@ export default class TagRenderingConfig { | |||
|             this.mappings = json.mappings.map((mapping, i) => { | ||||
| 
 | ||||
|                 if (mapping.then === undefined) { | ||||
|                     throw "Invalid mapping: if without body" | ||||
|                     throw `${context}.mapping[${i}]: Invalid mapping: if without body` | ||||
|                 } | ||||
|                 if (mapping.ifnot !== undefined && !this.multiAnswer) { | ||||
|                     throw `${context}.mapping[${i}]: Invalid mapping: ifnot defined, but the tagrendering is not a multianswer` | ||||
|                 } | ||||
|                 let hideInAnswer: boolean | TagsFilter = false; | ||||
|                 if (typeof mapping.hideInAnswer === "boolean") { | ||||
|  | @ -82,25 +87,53 @@ export default class TagRenderingConfig { | |||
|                 } else if (mapping.hideInAnswer !== undefined) { | ||||
|                     hideInAnswer = FromJSON.Tag(mapping.hideInAnswer, `${context}.mapping[${i}].hideInAnswer`); | ||||
|                 } | ||||
|                 return { | ||||
|                 const mp = { | ||||
|                     if: FromJSON.Tag(mapping.if, `${context}.mapping[${i}].if`), | ||||
|                     ifnot: (mapping.ifnot !== undefined ? FromJSON.Tag(mapping.ifnot, `${context}.mapping[${i}].ifnot`) : undefined), | ||||
|                     then: Translations.T(mapping.then), | ||||
|                     hideInAnswer: hideInAnswer | ||||
|                 }; | ||||
|                 if (this.question) { | ||||
|                     if (hideInAnswer !== true && !mp.if.isUsableAsAnswer()) { | ||||
|                         throw `${context}.mapping[${i}].if: This value cannot be used to answer a question, probably because it contains a regex or an OR. Either change it or set 'hideInAnswer'` | ||||
|                     } | ||||
| 
 | ||||
|                     if (hideInAnswer !== true && !(mp.ifnot?.isUsableAsAnswer() ?? true)) { | ||||
|                         throw `${context}.mapping[${i}].ifnot: This value cannot be used to answer a question, probably because it contains a regex or an OR. Either change it or set 'hideInAnswer'` | ||||
|                     } | ||||
|                 } | ||||
| 
 | ||||
|                 return mp; | ||||
|             }); | ||||
|         } | ||||
| 
 | ||||
|         if (this.question && this.freeform?.key === undefined && this.mappings === undefined) { | ||||
|             throw `A question is defined, but no mappings nor freeform (key) are. The question is ${this.question.txt} at ${context}` | ||||
|         } | ||||
|          | ||||
|         if(this.freeform && this.render === undefined){ | ||||
|             throw `Detected a freeform key without rendering... Key: ${this.freeform.key} in ${context}` | ||||
|             throw `${context}: A question is defined, but no mappings nor freeform (key) are. The question is ${this.question.txt} at ${context}` | ||||
|         } | ||||
| 
 | ||||
|         if (json.multiAnswer) { | ||||
|         if (this.freeform && this.render === undefined) { | ||||
|             throw `${context}: Detected a freeform key without rendering... Key: ${this.freeform.key} in ${context}` | ||||
|         } | ||||
| 
 | ||||
|         if (this.question !== undefined && json.multiAnswer) { | ||||
|             if ((this.mappings?.length ?? 0) === 0) { | ||||
|                 throw "MultiAnswer is set, but no mappings are defined" | ||||
|                 throw `${context} MultiAnswer is set, but no mappings are defined` | ||||
|             } | ||||
| 
 | ||||
|             let allKeys = []; | ||||
|             let allHaveIfNot = true; | ||||
|             for (const mapping of this.mappings) { | ||||
|                 if (mapping.hideInAnswer) { | ||||
|                     continue; | ||||
|                 } | ||||
|                 if (mapping.ifnot === undefined) { | ||||
|                     allHaveIfNot = false; | ||||
|                 } | ||||
|                 allKeys = allKeys.concat(mapping.if.usedKeys()); | ||||
|             } | ||||
|             allKeys = Utils.Dedup(allKeys); | ||||
|             if (allKeys.length > 1 && !allHaveIfNot) { | ||||
|                 throw `${context}: A multi-answer is defined, which generates values over multiple keys. Please define ifnot-tags too on every mapping` | ||||
|             } | ||||
| 
 | ||||
|         } | ||||
|  |  | |||
|  | @ -50,6 +50,11 @@ export interface TagRenderingConfigJson { | |||
|      */ | ||||
|     mappings?: { | ||||
|         if: AndOrTagConfigJson | string, | ||||
|         /** | ||||
|          * Only applicable if 'multiAnswer' is set. | ||||
|          * This tag is applied if the respective checkbox is unset | ||||
|          */ | ||||
|         ifnot?: AndOrTagConfigJson | string, | ||||
|         then: string | any | ||||
|         hideInAnswer?: boolean | ||||
|     }[] | ||||
|  |  | |||
|  | @ -294,7 +294,7 @@ export default class MetaTagging { | |||
|         MetaTagging.isOpen, | ||||
|         MetaTagging.carriageWayWidth, | ||||
|         MetaTagging.directionSimplified, | ||||
|         MetaTagging.currentTime | ||||
|     //    MetaTagging.currentTime
 | ||||
| 
 | ||||
|     ]; | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,4 +1,5 @@ | |||
| import {Utils} from "../Utils"; | ||||
| import {type} from "os"; | ||||
| 
 | ||||
| export abstract class TagsFilter { | ||||
|     abstract matches(tags: { k: string, v: string }[]): boolean | ||||
|  | @ -16,6 +17,8 @@ export abstract class TagsFilter { | |||
|     } | ||||
| 
 | ||||
|     abstract asHumanString(linkToWiki: boolean, shorten: boolean); | ||||
| 
 | ||||
|     abstract usedKeys(): string[]; | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
|  | @ -86,6 +89,13 @@ export class RegexTag extends TagsFilter { | |||
|         } | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     usedKeys(): string[] { | ||||
|         if (typeof this.key === "string") { | ||||
|             return [this.key]; | ||||
|         } | ||||
|         throw "Key cannot be determined as it is a regex" | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
|  | @ -163,6 +173,10 @@ export class Tag extends TagsFilter { | |||
|         } | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     usedKeys(): string[] { | ||||
|         return [this.key]; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
|  | @ -228,6 +242,10 @@ export class Or extends TagsFilter { | |||
|         } | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     usedKeys(): string[] { | ||||
|         return [].concat(this.or.map(subkeys => subkeys.usedKeys())); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
|  | @ -343,6 +361,10 @@ export class And extends TagsFilter { | |||
| 
 | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     usedKeys(): string[] { | ||||
|         return [].concat(this.and.map(subkeys => subkeys.usedKeys())); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
|  | @ -456,10 +478,10 @@ export class TagUtils { | |||
|         const splitted = TagUtils.SplitKeys([tag]); | ||||
|         for (const splitKey in splitted) { | ||||
|             const neededValues = splitted[splitKey]; | ||||
|             if(tags[splitKey] === undefined) { | ||||
|             if (tags[splitKey] === undefined) { | ||||
|                 return false; | ||||
|             } | ||||
|              | ||||
| 
 | ||||
|             const actualValue = tags[splitKey].split(";"); | ||||
|             for (const neededValue of neededValues) { | ||||
|                 if (actualValue.indexOf(neededValue) < 0) { | ||||
|  |  | |||
|  | @ -107,14 +107,14 @@ export default class TagRenderingQuestion extends UIElement { | |||
|         } | ||||
| 
 | ||||
|         if (this._configuration.multiAnswer) { | ||||
|             return this.GenerateMultiAnswer(mappings, ff) | ||||
|             return this.GenerateMultiAnswer(mappings, ff, this._configuration.mappings.map(mp => mp.ifnot)) | ||||
|         } else { | ||||
|             return new RadioButton(mappings, false) | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     private GenerateMultiAnswer(elements: InputElement<TagsFilter>[], freeformField: InputElement<TagsFilter>): InputElement<TagsFilter> { | ||||
|     private GenerateMultiAnswer(elements: InputElement<TagsFilter>[], freeformField: InputElement<TagsFilter>, ifNotSelected: TagsFilter[]): InputElement<TagsFilter> { | ||||
|         const checkBoxes = new CheckBoxes(elements); | ||||
|         const inputEl = new InputElementMap<number[], TagsFilter>( | ||||
|             checkBoxes, | ||||
|  | @ -126,6 +126,18 @@ export default class TagRenderingQuestion extends UIElement { | |||
|                     return undefined; | ||||
|                 } | ||||
|                 const tags: TagsFilter[] = indices.map(i => elements[i].GetValue().data); | ||||
|                 const oppositeTags: TagsFilter[] = []; | ||||
|                 for (let i = 0; i < ifNotSelected.length; i++) { | ||||
|                     if(indices.indexOf(i) >= 0){ | ||||
|                         continue; | ||||
|                     } | ||||
|                     const notSelected = ifNotSelected[i]; | ||||
|                     if(notSelected === undefined){ | ||||
|                         continue; | ||||
|                     } | ||||
|                     oppositeTags.push(notSelected); | ||||
|                 } | ||||
|                 tags.push(TagUtils.FlattenMultiAnswer(oppositeTags)); | ||||
|                 return TagUtils.FlattenMultiAnswer(tags); | ||||
|             }, | ||||
|             (tags: TagsFilter) => { | ||||
|  |  | |||
							
								
								
									
										47
									
								
								Utils.ts
									
										
									
									
									
								
							
							
						
						
									
										47
									
								
								Utils.ts
									
										
									
									
									
								
							|  | @ -141,21 +141,9 @@ export class Utils { | |||
|     } | ||||
| 
 | ||||
|     // Date will be undefined on failure
 | ||||
|     public static changesetDate(id: number, action: ((isFound: Date) => void)): void { | ||||
|         $.getJSON("https://www.openstreetmap.org/api/0.6/changeset/" + id, | ||||
|             function (data) { | ||||
|                 console.log(data) | ||||
|                 action(new Date(data.elements[0].created_at)); | ||||
|             }) | ||||
|             .fail(() => { | ||||
|                 action(undefined); | ||||
|             }); | ||||
| 
 | ||||
|     } | ||||
|      | ||||
|     public static LoadCustomCss(location: string){ | ||||
|         var head = document.getElementsByTagName('head')[0]; | ||||
|         var link = document.createElement('link'); | ||||
|         const head = document.getElementsByTagName('head')[0]; | ||||
|         const link = document.createElement('link'); | ||||
|         link.id = "customCss"; | ||||
|         link.rel = 'stylesheet'; | ||||
|         link.type = 'text/css'; | ||||
|  | @ -164,17 +152,6 @@ export class Utils { | |||
|         head.appendChild(link); | ||||
|         console.log("Added custom layout ",location) | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     static MatchKeys(object: any, prototype: any, context?: string){ | ||||
| 
 | ||||
|         for (const objectKey in object) { | ||||
|             if(prototype[objectKey] === undefined){ | ||||
|                 console.error("Key ", objectKey, "might be not supported (in context",context,")") | ||||
|             }    | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     static Merge(source: any, target: any){ | ||||
|         target = JSON.parse(JSON.stringify(target)); | ||||
|         source = JSON.parse(JSON.stringify(source)); | ||||
|  | @ -195,24 +172,4 @@ export class Utils { | |||
|         } | ||||
|         return target; | ||||
|     } | ||||
|      | ||||
|     static ToMuchTags(source: any, toCheck: any, context: string){ | ||||
| 
 | ||||
|         for (const key in toCheck) { | ||||
|             const toCheckV = toCheck[key]; | ||||
|             const sourceV = source[key]; | ||||
|             if(sourceV === undefined){ | ||||
|                 console.error("Probably a wrong tag in ", context, ": ", key, "might be wrong") | ||||
|             } | ||||
|             if(typeof toCheckV === "object"){ | ||||
|                 if(typeof sourceV !== "object"){ | ||||
|                     console.error("Probably a wrong value in ", context, ": ", key, "is a fixed value in the source") | ||||
|                 }else{ | ||||
|                     Utils.ToMuchTags(sourceV, toCheckV, context+"."+key); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|          | ||||
|     } | ||||
|      | ||||
| } | ||||
|  |  | |||
|  | @ -104,14 +104,17 @@ | |||
|       "mappings": [ | ||||
|         { | ||||
|           "if": "payment:coins=yes", | ||||
|           "ifnot":  "payment:coins=no", | ||||
|           "then": "Payment with coins is possible" | ||||
|         }, | ||||
|         { | ||||
|           "if": "payment:notes=yes", | ||||
|           "ifnot": "payment:notes=no", | ||||
|           "then": "Payment with notes is possible" | ||||
|         }, | ||||
|         { | ||||
|           "if": "payment:cards=yes", | ||||
|           "ifnot": "payment:cards=no", | ||||
|           "then": "Payment with cards is possible" | ||||
|         } | ||||
|       ], | ||||
|  | @ -157,22 +160,27 @@ | |||
|       "mappings": [ | ||||
|         { | ||||
|           "if": "vending:bicycle_light=yes", | ||||
|           "ifnot": "vending:bicycle_light=no", | ||||
|           "then": "Bicycle lights are sold here" | ||||
|         }, | ||||
|         { | ||||
|           "if": "vending:gloves=yes", | ||||
|           "ifnot": "vending:gloves=no", | ||||
|           "then": "Gloves are sold here" | ||||
|         }, | ||||
|         { | ||||
|           "if": "vending:bicycle_repair_kit=yes", | ||||
|           "ifnot": "vending:bicycle_repair_kit=no", | ||||
|           "then": "Bicycle repair kits are sold here" | ||||
|         }, | ||||
|         { | ||||
|           "if": "vending:bicycle_pump=yes", | ||||
|           "ifnot": "vending:bicycle_pump=no", | ||||
|           "then": "Bicycle pumps are sold here" | ||||
|         }, | ||||
|         { | ||||
|           "if": "vending:bicycle_lock=yes", | ||||
|           "ifnot": "vending:bicycle_lock=no", | ||||
|           "then": "Bicycle locks are sold here" | ||||
|         } | ||||
|       ], | ||||
|  |  | |||
							
								
								
									
										69
									
								
								test.ts
									
										
									
									
									
								
							
							
						
						
									
										69
									
								
								test.ts
									
										
									
									
									
								
							|  | @ -13,72 +13,3 @@ const images = new UIEventSource<{ url: string, key: string }[]>( | |||
| new ImageCarousel(images, new UIEventSource<any>({"image:1":"https://2.bp.blogspot.com/-fQiZkz9Zlzg/T_xe2X2Ia3I/AAAAAAAAA0Q/VPS8Mb8xtIQ/s1600/cat+15.jpg"})) | ||||
|     .AttachTo("maindiv")     | ||||
| 
 | ||||
| /*/ | ||||
| import {Utils} from "./Utils"; | ||||
| import {FixedUiElement} from "./UI/Base/FixedUiElement"; | ||||
| 
 | ||||
| 
 | ||||
| function generateStats(action: (stats: string) => void) { | ||||
|     // Binary searches the latest changeset
 | ||||
|     function search(lowerBound: number, | ||||
|                     upperBound: number, | ||||
|                     onCsFound: ((id: number, lastDate: Date) => void), | ||||
|                     depth = 0) { | ||||
|         if (depth > 30) { | ||||
|             return; | ||||
|         } | ||||
|         const tested = Math.floor((lowerBound + upperBound) / 2); | ||||
|         console.log("Testing", tested) | ||||
|         Utils.changesetDate(tested, (createdAtDate: Date) => { | ||||
|             new FixedUiElement(`Searching, value between ${lowerBound} and ${upperBound}. Queries till now: ${depth}`).AttachTo('maindiv') | ||||
|             if (lowerBound + 1 >= upperBound) { | ||||
|                 onCsFound(lowerBound, createdAtDate); | ||||
|                 return; | ||||
|             } | ||||
|             if (createdAtDate !== undefined) { | ||||
|                 search(tested, upperBound, onCsFound, depth + 1) | ||||
|             } else { | ||||
|                 search(lowerBound, tested, onCsFound, depth + 1); | ||||
|             } | ||||
|         }) | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     search(91000000, 100000000, (last, lastDate: Date) => { | ||||
|             const link = "http://osm.org/changeset/" + last; | ||||
| 
 | ||||
|             const delta = 100000; | ||||
| 
 | ||||
|             Utils.changesetDate(last - delta, (prevDate) => { | ||||
| 
 | ||||
| 
 | ||||
|                 const diff = (lastDate.getTime() - prevDate.getTime()) / 1000; | ||||
| 
 | ||||
|                 // Diff: seconds needed/delta changesets
 | ||||
|                 const secsPerCS = diff / delta; | ||||
| 
 | ||||
|                 const stillNeeded = 1000000 - (last % 1000000); | ||||
|                 const timeNeededSeconds = Math.floor(secsPerCS * stillNeeded); | ||||
| 
 | ||||
|                 const secNeeded = timeNeededSeconds % 60; | ||||
|                 const minNeeded = Math.floor(timeNeededSeconds / 60) % 60; | ||||
|                 const hourNeeded = Math.floor(timeNeededSeconds / (60 * 60)) % 24; | ||||
|                 const daysNeeded = Math.floor(timeNeededSeconds / (24 * 60 * 60)); | ||||
| 
 | ||||
|                 const result = `Last changeset: <a href='${link}'>${link}</a><br/>We needed ${(Math.floor(diff / 60))} minutes for the last ${delta} changesets.<br/>
 | ||||
| This is around ${secsPerCS} seconds/changeset.<br/> The next million (still ${stillNeeded} away) will be broken in around ${daysNeeded} days ${hourNeeded}:${minNeeded}:${secNeeded}` | ||||
|                 action(result); | ||||
|             }) | ||||
| 
 | ||||
|         } | ||||
|     ); | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| generateStats((stats) => { | ||||
|     new FixedUiElement(stats).AttachTo('maindiv') | ||||
| }) | ||||
| 
 | ||||
| 
 | ||||
| //*/
 | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue