forked from MapComplete/MapComplete
		
	
		
			
				
	
	
		
			195 lines
		
	
	
		
			No EOL
		
	
	
		
			6.3 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			195 lines
		
	
	
		
			No EOL
		
	
	
		
			6.3 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| export default class AspectedRouting {
 | |
| 
 | |
|     public readonly name: string
 | |
|     public readonly description: string
 | |
|     public readonly units: string
 | |
|     public readonly program: any
 | |
| 
 | |
|     public constructor(program) {
 | |
|         this.name = program.name;
 | |
|         this.description = program.description;
 | |
|         this.units = program.unit
 | |
|         this.program = JSON.parse(JSON.stringify(program))
 | |
|         delete this.program.name
 | |
|         delete this.program.description
 | |
|         delete this.program.unit
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Interprets the given Aspected-routing program for the given properties
 | |
|      */
 | |
|     public static interpret(program: any, properties: any) {
 | |
|         if (typeof program !== "object") {
 | |
|             return program;
 | |
|         }
 | |
| 
 | |
|         let functionName /*: string*/ = undefined;
 | |
|         let functionArguments /*: any */ = undefined
 | |
|         let otherValues = {}
 | |
|         // @ts-ignore
 | |
|         Object.entries(program).forEach(tag => {
 | |
|                 const [key, value] = tag;
 | |
|                 if (key.startsWith("$")) {
 | |
|                     functionName = key
 | |
|                     functionArguments = value
 | |
|                 } else {
 | |
|                     otherValues[key] = value
 | |
|                 }
 | |
|             }
 | |
|         )
 | |
| 
 | |
|         if (functionName === undefined) {
 | |
|             return AspectedRouting.interpretAsDictionary(program, properties)
 | |
|         }
 | |
| 
 | |
|         if (functionName === '$multiply') {
 | |
|             return AspectedRouting.multiplyScore(properties, functionArguments);
 | |
|         } else if (functionName === '$firstMatchOf') {
 | |
|             return AspectedRouting.getFirstMatchScore(properties, functionArguments);
 | |
|         } else if (functionName === '$min') {
 | |
|             return AspectedRouting.getMinValue(properties, functionArguments);
 | |
|         } else if (functionName === '$max') {
 | |
|             return AspectedRouting.getMaxValue(properties, functionArguments);
 | |
|         } else if (functionName === '$default') {
 | |
|             return AspectedRouting.defaultV(functionArguments, otherValues, properties)
 | |
|         } else {
 | |
|             console.error(`Error: Program ${functionName} is not implemented yet. ${JSON.stringify(program)}`);
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Given a 'program' without function invocation, interprets it as a dictionary
 | |
|      *
 | |
|      * E.g., given the program
 | |
|      *
 | |
|      * {
 | |
|      *     highway: {
 | |
|      *         residential: 30,
 | |
|      *         living_street: 20
 | |
|      *     },
 | |
|      *     surface: {
 | |
|      *         sett : 0.9
 | |
|      *     }
 | |
|      *     
 | |
|      * }
 | |
|      *
 | |
|      * in combination with the tags {highway: residential},
 | |
|      *
 | |
|      * the result should be [30, undefined];
 | |
|      *
 | |
|      * For the tags {highway: residential, surface: sett} we should get [30, 0.9]
 | |
|      *
 | |
|      *
 | |
|      * @param program
 | |
|      * @param tags
 | |
|      * @return {(undefined|*)[]}
 | |
|      */
 | |
|     private static interpretAsDictionary(program, tags) {
 | |
|         // @ts-ignore
 | |
|         return Object.entries(tags).map(tag => {
 | |
|             const [key, value] = tag;
 | |
|             const propertyValue = program[key]
 | |
|             if (propertyValue === undefined) {
 | |
|                 return undefined
 | |
|             }
 | |
|             if (typeof propertyValue !== "object") {
 | |
|                 return propertyValue
 | |
|             }
 | |
|             // @ts-ignore
 | |
|             return propertyValue[value]
 | |
|         });
 | |
|     }
 | |
| 
 | |
|     private static defaultV(subProgram, otherArgs, tags) {
 | |
|         // @ts-ignore
 | |
|         const normalProgram = Object.entries(otherArgs)[0][1]
 | |
|         const value = AspectedRouting.interpret(normalProgram, tags)
 | |
|         if (value !== undefined) {
 | |
|             return value;
 | |
|         }
 | |
|         return AspectedRouting.interpret(subProgram, tags)
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Multiplies the default score with the proper values
 | |
|      * @param tags {object} the active tags to check against
 | |
|      * @param subprograms which should generate a list of values
 | |
|      * @returns score after multiplication
 | |
|      */
 | |
|     private static multiplyScore(tags, subprograms) {
 | |
|         let number = 1
 | |
| 
 | |
|         let subResults: any[]
 | |
|         if (subprograms.length !== undefined) {
 | |
|             subResults = AspectedRouting.concatMap(subprograms, subprogram => AspectedRouting.interpret(subprogram, tags))
 | |
|         } else {
 | |
|             subResults = AspectedRouting.interpret(subprograms, tags)
 | |
|         }
 | |
| 
 | |
|         subResults.filter(r => r !== undefined).forEach(r => number *= parseFloat(r))
 | |
|         return number.toFixed(2);
 | |
|     }
 | |
| 
 | |
|     private static getFirstMatchScore(tags, order: any) {
 | |
|         /*Order should be a list of arguments after evaluation*/
 | |
|         order = <string[]>AspectedRouting.interpret(order, tags)
 | |
|         for (let key of order) {
 | |
|             // @ts-ignore
 | |
|             for (let entry of Object.entries(JSON.parse(tags))) {
 | |
|                 const [tagKey, value] = entry;
 | |
|                 if (key === tagKey) {
 | |
|                     // We have a match... let's evaluate the subprogram
 | |
|                     const evaluated = AspectedRouting.interpret(value, tags)
 | |
|                     if (evaluated !== undefined) {
 | |
|                         return evaluated;
 | |
|                     }
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         // Not a single match found...
 | |
|         return undefined
 | |
|     }
 | |
| 
 | |
|     private static getMinValue(tags, subprogram) {
 | |
|         const minArr = subprogram.map(part => {
 | |
|             if (typeof (part) === 'object') {
 | |
|                 const calculatedValue = this.interpret(part, tags)
 | |
|                 return parseFloat(calculatedValue)
 | |
|             } else {
 | |
|                 return parseFloat(part);
 | |
|             }
 | |
|         }).filter(v => !isNaN(v));
 | |
|         return Math.min(...minArr);
 | |
|     }
 | |
| 
 | |
|     private static getMaxValue(tags, subprogram) {
 | |
|         const maxArr = subprogram.map(part => {
 | |
|             if (typeof (part) === 'object') {
 | |
|                 return parseFloat(AspectedRouting.interpret(part, tags))
 | |
|             } else {
 | |
|                 return parseFloat(part);
 | |
|             }
 | |
|         }).filter(v => !isNaN(v));
 | |
|         return Math.max(...maxArr);
 | |
|     }
 | |
| 
 | |
|     private static concatMap(list, f): any[] {
 | |
|         const result = []
 | |
|         list = list.map(f)
 | |
|         for (const elem of list) {
 | |
|             if (elem.length !== undefined) {
 | |
|                 // This is a list
 | |
|                 result.push(...elem)
 | |
|             } else {
 | |
|                 result.push(elem)
 | |
|             }
 | |
|         }
 | |
|         return result;
 | |
|     }
 | |
| 
 | |
|     public evaluate(properties) {
 | |
|         return AspectedRouting.interpret(this.program, properties)
 | |
|     }
 | |
| 
 | |
| } |