forked from MapComplete/MapComplete
		
	
		
			
				
	
	
		
			200 lines
		
	
	
	
		
			6.4 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			200 lines
		
	
	
	
		
			6.4 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)
 | |
|     }
 | |
| }
 |