View Javadoc
1   package org.opentrafficsim.editor;
2   
3   import java.lang.reflect.Constructor;
4   import java.rmi.RemoteException;
5   import java.util.ArrayList;
6   import java.util.LinkedHashMap;
7   import java.util.LinkedHashSet;
8   import java.util.List;
9   import java.util.Map;
10  import java.util.Objects;
11  import java.util.Set;
12  
13  import org.djutils.eval.Eval;
14  import org.djutils.event.Event;
15  import org.djutils.reflection.ClassUtil;
16  import org.opentrafficsim.editor.decoration.AbstractNodeDecoratorRemove;
17  import org.opentrafficsim.road.network.factory.xml.parser.ScenarioParser;
18  import org.opentrafficsim.road.network.factory.xml.parser.ScenarioParser.ParameterWrapper;
19  import org.opentrafficsim.road.network.factory.xml.parser.ScenarioParser.ScenariosWrapper;
20  import org.opentrafficsim.xml.bindings.ExpressionAdapter;
21  
22  /**
23   * Wraps an evaluator for the editor. Any editor component that has content that depends on evaluation, may listen to this
24   * object via the editor and be notified of any change. In particular, changes involve changes in the input parameters. This
25   * wrapper makes sure that it returns an evaluator based on the current input parameters and selected scenario in the editor.
26   * <p>
27   * Copyright (c) 2023-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
28   * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
29   * </p>
30   * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
31   */
32  public class EvalWrapper extends AbstractNodeDecoratorRemove
33  {
34  
35      /** */
36      private static final long serialVersionUID = 20231005L;
37  
38      /** Mask of full class names where type adapters are to be found, depending on node name for %s. */
39      private static final String ADAPTER_MASK = "org.opentrafficsim.xml.bindings.%sAdapter";
40  
41      /** Whether the evaluator is dirty, i.e. a parameter was added, removed, or changed. */
42      private boolean dirty = true;
43  
44      /** Scenario for which the most recent evaluator was returned. */
45      private ScenarioWrapper lastScenario;
46  
47      /** Last valid evaluator. */
48      private Eval eval;
49  
50      /** List of parameter wrappers for default parameters. */
51      private final List<ParameterWrapper> defaultParamaters = new ArrayList<>();
52  
53      /** List of parameter wrappers per scenario tree node. */
54      private final Map<XsdTreeNode, List<ParameterWrapper>> scenarioParameters = new LinkedHashMap<>();
55  
56      /** Parameter wrapper per parameter tree node. */
57      private final Map<XsdTreeNode, ParameterWrapper> parameterMap = new LinkedHashMap<>();
58  
59      /** Listeners for a dirt evaluator. */
60      private final Set<EvalListener> listeners = new LinkedHashSet<>();
61  
62      /**
63       * Constructor.
64       * @param editor OtsEditor; editor.
65       */
66      public EvalWrapper(final OtsEditor editor)
67      {
68          super(editor);
69      }
70  
71      /**
72       * Returns expression evaluator.
73       * @param scenario ScenarioWrapper; selected scenario (of type as listed in dropdown menu).
74       * @return Eval; expression evaluator.
75       */
76      public Eval getEval(final ScenarioWrapper scenario)
77      {
78          boolean becomesDirty = this.dirty || !Objects.equals(this.lastScenario, scenario);
79          if (becomesDirty)
80          {
81              this.lastScenario = scenario;
82              try
83              {
84                  this.eval = ScenarioParser.parseInputParameters(new ScenariosWrapper()
85                  {
86                      /** {@inheritDoc} */
87                      @Override
88                      public Iterable<ParameterWrapper> getDefaultInputParameters()
89                      {
90                          return EvalWrapper.this.defaultParamaters;
91                      }
92  
93                      /** {@inheritDoc} */
94                      @Override
95                      public Iterable<ParameterWrapper> getScenarioInputParameters()
96                      {
97                          return scenario == null ? null : EvalWrapper.this.scenarioParameters.get(scenario.getScenarioNode());
98                      }
99                  });
100             }
101             catch (RuntimeException ex)
102             {
103                 this.dirty = false;
104                 this.listeners.forEach((listener) -> listener.evalChanged());
105                 throw ex;
106             }
107             this.dirty = false;
108             this.listeners.forEach((listener) -> listener.evalChanged());
109         }
110         return this.eval;
111     }
112 
113     /**
114      * Returns the last evaluator that was valid, i.e. did not have a circular dependency between input parameters.
115      * @return Eval; last valid evaluator.
116      */
117     public Eval getLastValidEval()
118     {
119         if (this.eval == null)
120         {
121             return new Eval();
122         }
123         return this.eval;
124     }
125 
126     /** {@inheritDoc} */
127     @Override
128     public void notifyCreated(final XsdTreeNode node)
129     {
130         if (node.getPathString().equals(XsdPaths.SCENARIO))
131         {
132             this.scenarioParameters.put(node, new ArrayList<>());
133             setDirty();
134         }
135         else if (node.getPathString().equals(XsdPaths.INPUT_PARAMETERS)
136                 || node.getPathString().equals(XsdPaths.DEFAULT_INPUT_PARAMETERS))
137         {
138             node.addListener(this, XsdTreeNode.ACTIVATION_CHANGED);
139         }
140         else if ((node.getPathString().startsWith(XsdPaths.INPUT_PARAMETERS + ".")
141                 || node.getPathString().startsWith(XsdPaths.DEFAULT_INPUT_PARAMETERS + "."))
142                 && !node.getNodeName().equals("xsd:choice"))
143         {
144             node.addListener(this, XsdTreeNode.ATTRIBUTE_CHANGED);
145             node.addListener(this, XsdTreeNode.VALUE_CHANGED);
146             node.addListener(this, XsdTreeNode.ACTIVATION_CHANGED);
147             registerParameter(node); // node may be valid when its created as part of an undo, so we need to register it if so
148             setDirty();
149         }
150     }
151 
152     /** {@inheritDoc} */
153     @Override
154     public void notifyRemoved(final XsdTreeNode node)
155     {
156         if (node.getPathString().equals(XsdPaths.SCENARIO))
157         {
158             this.scenarioParameters.remove(node);
159             setDirty();
160         }
161         else if (node.getPathString().equals(XsdPaths.INPUT_PARAMETERS)
162                 || node.getPathString().equals(XsdPaths.DEFAULT_INPUT_PARAMETERS))
163         {
164             node.removeListener(this, XsdTreeNode.ACTIVATION_CHANGED);
165         }
166         else if (node.getPathString().startsWith(XsdPaths.DEFAULT_INPUT_PARAMETERS + "."))
167         {
168             this.defaultParamaters.remove(this.parameterMap.remove(node));
169             node.removeListener(this, XsdTreeNode.ATTRIBUTE_CHANGED);
170             node.removeListener(this, XsdTreeNode.VALUE_CHANGED);
171             setDirty();
172         }
173         else if (node.getPathString().startsWith(XsdPaths.INPUT_PARAMETERS + "."))
174         {
175             this.scenarioParameters.forEach((s, list) -> list.remove(this.parameterMap.remove(node)));
176             node.removeListener(this, XsdTreeNode.ATTRIBUTE_CHANGED);
177             node.removeListener(this, XsdTreeNode.VALUE_CHANGED);
178             setDirty();
179         }
180     }
181 
182     /** {@inheritDoc} */
183     @Override
184     public void notify(final Event event) throws RemoteException
185     {
186         if (event.getType().equals(XsdTreeNode.ATTRIBUTE_CHANGED) || event.getType().equals(XsdTreeNode.VALUE_CHANGED))
187         {
188             XsdTreeNode node = (XsdTreeNode) ((Object[]) event.getContent())[0];
189             registerParameter(node);
190             setDirty();
191         }
192         else if (event.getType().equals(XsdTreeNode.ACTIVATION_CHANGED))
193         {
194             Object[] content = (Object[]) event.getContent();
195             XsdTreeNode node = (XsdTreeNode) content[0];
196             boolean activated = (boolean) content[1];
197             if (node.getPathString().equals(XsdPaths.INPUT_PARAMETERS)
198                     || node.getPathString().equals(XsdPaths.DEFAULT_INPUT_PARAMETERS))
199             {
200                 if (activated)
201                 {
202                     node.getChildren().forEach((child) -> notifyCreated(child));
203                 }
204                 else
205                 {
206                     node.getChildren().forEach((child) -> notifyRemoved(child));
207                 }
208             }
209             else if ((node.getPathString().startsWith(XsdPaths.INPUT_PARAMETERS + ".")
210                     || node.getPathString().startsWith(XsdPaths.DEFAULT_INPUT_PARAMETERS + "."))
211                     && !node.getNodeName().equals("xsd:choice"))
212             {
213                 if (activated)
214                 {
215                     notifyCreated(node);
216                 }
217                 else
218                 {
219                     notifyRemoved(node);
220                 }
221             }
222         }
223         else
224         {
225             super.notify(event);
226         }
227     }
228 
229     /**
230      * Register the given node as a parameter.
231      * @param node XsdTreeNode; node (default or scenario input parameter).
232      */
233     private void registerParameter(final XsdTreeNode node)
234     {
235         if (node.getPathString().startsWith(XsdPaths.DEFAULT_INPUT_PARAMETERS + "."))
236         {
237             this.defaultParamaters.remove(this.parameterMap.remove(node));
238             if (node.isValid())
239             {
240                 ParameterWrapper parameter = wrap(node);
241                 this.parameterMap.put(node, parameter);
242                 this.defaultParamaters.add(parameter);
243             }
244         }
245         else if (node.getPathString().startsWith(XsdPaths.INPUT_PARAMETERS + "."))
246         {
247             this.scenarioParameters.forEach((s, list) -> list.remove(this.parameterMap.remove(node)));
248             if (node.isValid())
249             {
250                 ParameterWrapper parameter = wrap(node);
251                 this.parameterMap.put(node, parameter);
252                 XsdTreeNode scenarioNode = node.getParent().getParent();
253                 if (scenarioNode != null)
254                 {
255                     this.scenarioParameters.get(scenarioNode).add(parameter);
256                 }
257             }
258         }
259     }
260 
261     /**
262      * Sets the evaluator as being dirty, i.e. some input parameter was added, removed or changed. All listeners are notified.
263      */
264     public void setDirty()
265     {
266         this.dirty = true;
267         this.listeners.forEach((listener) -> listener.evalChanged());
268     }
269 
270     /**
271      * Adds listener to changes in the evaluator, i.e. added, removed or changed input parameters.
272      * @param listener EvalListener; listener.
273      */
274     public void addListener(final EvalListener listener)
275     {
276         this.listeners.add(listener);
277     }
278 
279     /**
280      * Removes listener to changes in the evaluator, i.e. added, removed or changed input parameters.
281      * @param listener EvalListener; listener.
282      */
283     public void removeListener(final EvalListener listener)
284     {
285         this.listeners.remove(listener);
286     }
287 
288     /**
289      * Parameter representation of a node suitable for parsing an {@code Eval}.
290      * @param node XsdTreeNode; node, must be a default or scenario input parameter node.
291      * @return ParameterWrapper; parameter representation of a node suitable for parsing an {@code Eval}.
292      */
293     private ParameterWrapper wrap(final XsdTreeNode node)
294     {
295         try
296         {
297             Class<?> clazz = Class.forName(String.format(ADAPTER_MASK, node.getNodeName()));
298             Constructor<?> constructor = ClassUtil.resolveConstructor(clazz, new Object[0]);
299             ExpressionAdapter<?, ?> adapter = (ExpressionAdapter<?, ?>) constructor.newInstance();
300             return new ParameterWrapper(node.getId(), adapter.unmarshal(node.getValue()));
301         }
302         catch (Exception e)
303         {
304             throw new RuntimeException("Unable to wrap node " + node + " as a parameter for Eval.");
305         }
306     }
307 
308     /**
309      * Interface for listeners that need to know when evaluation results may have changed.
310      * <p>
311      * Copyright (c) 2023-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
312      * <br>
313      * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
314      * </p>
315      * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
316      */
317     @FunctionalInterface
318     public interface EvalListener
319     {
320         /**
321          * Notifies the listener that evaluation results may have changed.
322          */
323         public void evalChanged();
324     }
325 
326 }