triggers/PropertyTriggers.js

/**
 *  PropertyTriggers - triggers on property changes
 *  
 *  This code is licensed under the MIT License (MIT).
 *  
 *  Copyright 2020, 2021, 2022 Rolf Bagge, Janus B. Kristensen, CAVI,
 *  Center for Advanced Visualization and Interaction, Aarhus University
 *  
 *  Permission is hereby granted, free of charge, to any person obtaining a copy
 *  of this software and associated documentation files (the “Software”), to deal
 *  in the Software without restriction, including without limitation the rights 
 *  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 
 *  copies of the Software, and to permit persons to whom the Software is 
 *  furnished to do so, subject to the following conditions:
 *  
 *  The above copyright notice and this permission notice shall be included in 
 *  all copies or substantial portions of the Software.
 *  
 *  THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
 *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
 *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 
 *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
 *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 
 *  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 
 *  THE SOFTWARE.
 *  
 */

/**
 * A trigger "stateChanged" that listens for property state changes
 * @memberOf Triggers
 * @example
 * //Triggers the stateChanged event when myProperty has changed, only the first concept that has the property is checked.
 * {
 *     "stateChanged": "myProperty"
 * }
 *
 * @example
 * //Triggers the stateChanged event when any property on myConcept changes
 * {
 *     "stateChanged": "myConcept"
 * }
 *
 * @example
 * //Triggers the stateChanged event when any property on myConcept changes, or if myProperty changes, on the first concept it was found on
 * {
 *     "stateChanged": ["myConcept", "myProperty"]
 * }
 *
 * @example
 * //Triggers the stateChanged event when property myProperty changes on myConcept
 * {
 *     "stateChanged": {"myConcept": "myProperty"}
 * }
 *
 * @example
 * //Triggers the stateChanged event when property myProperty changes on myConcept
 * {
 *     "stateChanged": {
 *         "concept": "myConcept",
 *         "property": "myProperty"
 *     }
 * }
 *
 * @example
 * //Triggers the stateChanged event when property myProperty changes
 * {
 *     "stateChanged": {
 *         "property": "myProperty"
 *     }
 * }
 *
 * @example
 * //Triggers the stateChanged event when a property on myConcept changes
 * {
 *     "stateChanged": {
 *         "concept": "myConcept"
 *     }
 * }
 */
class StateChangedTrigger extends Trigger {
    constructor(name, options, concept) {
        if(typeof options === "string") {
            //Shorthand options string
            options = {
                runtimeLookup: [options]
            };
        } else if(Array.isArray(options)) {
            //Shorthand options concept array
            options = {
                runtimeLookup: []
            }
            for(let i = 0; i<options.length; i++) {
                options.runtimeLookup.push(options[i]);
            }
        } else if(Object.keys(options).length === 1) {
            //Shorthand options {"concept": "property"}
            let possibleConceptType = Object.keys(options)[0];
            let concept = VarvEngine.getConceptFromType(possibleConceptType);

            if(concept != null) {
                try {
                    let property = concept.getProperty(options[possibleConceptType]);

                    //We have both concept and property, so shorthand was correct
                    options = {
                        "concept": concept.name,
                        "property": property.name
                    }
                } catch(e) {
                    //Ignore
                }
            }
        }

        super(name, options, concept);
        this.triggerDelete = null;
    }

    enable() {
        const self = this;

        this.triggerDelete = Trigger.registerTriggerEvent("stateChanged", async (context)=>{
            //Always only 1 entry in array
            context = context[0];

            let options = Object.assign({}, self.options);

            if(options.runtimeLookup != null) {
                let lookedUpReferences = [];
                options.runtimeLookup.forEach((reference)=>{
                    let lookup = VarvEngine.lookupReference(reference, self.concept);
                    lookedUpReferences.push(lookup);
                });

                //Set options to the looked up references
                options = Object.assign(options, lookedUpReferences);
            }

            //Check if options array shorthand
            if(Array.isArray(options)) {
                let temp = options;
                options = {
                    concept: [],
                    property: []
                }

                temp.forEach((entry)=>{
                    if(entry.concept != null) {
                        options.concept.push(entry.concept);
                    }

                    if(entry.property != null) {
                        options.property.push(entry.property);
                    }
                })
            }

            let clonedContext = Action.cloneContext(context);

            if(Trigger.DEBUG) {
                console.log("StateChangedTrigger:", self.name, options, ""+context.target);
            }

            let triggeringConcept = await VarvEngine.getConceptFromUUID(context.target);

            if(triggeringConcept == null) {
                throw new Error("Unknown concept for UUID: "+context.target);
            }

            if(options.exactConceptMatch && options.concept == null) {
                //We are matching excact on concept, but have no concept, use owning concept
                options.concept = self.concept.name;
            }

            if(options.concept != null) {
                let filterConcepts = options.concept;
                if(!Array.isArray(filterConcepts)) {
                    filterConcepts = [filterConcepts];
                }

                let found = filterConcepts.length === 0;

                for(let filterConcept of filterConcepts) {
                    if(options.exactConceptMatch) {
                        if (triggeringConcept.name === filterConcept) {
                            found = true;
                            break;
                        }
                    } else {
                        if (triggeringConcept.isA(filterConcept)) {
                            found = true;
                            break;
                        }
                    }
                }

                if(!found) {
                    //Skip based on wrong concept
                    if(Trigger.DEBUG) {
                        console.log("Skipping based on wrong concept");
                    }
                    return;
                }
            }

            if(options.property != null) {
                let filterProperties = options.property;
                if(!Array.isArray(filterProperties)) {
                    filterProperties = [filterProperties];
                }

                let found = filterProperties.length === 0;

                for(let filterProperty of filterProperties) {
                    if(context.property === filterProperty) {
                        found = true;
                        break;
                    }
                }

                if(!found) {
                    //Skip based on wrong property
                    if (Trigger.DEBUG) {
                        console.log("Skipping based on wrong property")
                    }
                    return;
                }
            }

            await Trigger.trigger(self.name, clonedContext);
        });
    }

    disable() {
        if(this.triggerDelete != null) {
            this.triggerDelete.delete();
        }
        this.triggerDelete = null;
    }
}
Trigger.registerTrigger("stateChanged", StateChangedTrigger);
window.StateChangedTrigger = StateChangedTrigger;