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 editor.
65       */
66      public EvalWrapper(final OtsEditor editor)
67      {
68          super(editor);
69      }
70  
71      /**
72       * Returns expression evaluator.
73       * @param scenario selected scenario (of type as listed in dropdown menu).
74       * @return 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                      @Override
87                      public Iterable<ParameterWrapper> getDefaultInputParameters()
88                      {
89                          return EvalWrapper.this.defaultParamaters;
90                      }
91  
92                      @Override
93                      public Iterable<ParameterWrapper> getScenarioInputParameters()
94                      {
95                          return scenario == null ? null : EvalWrapper.this.scenarioParameters.get(scenario.getScenarioNode());
96                      }
97                  });
98              }
99              catch (RuntimeException ex)
100             {
101                 this.dirty = false;
102                 this.listeners.forEach((listener) -> listener.evalChanged());
103                 throw ex;
104             }
105             this.dirty = false;
106             this.listeners.forEach((listener) -> listener.evalChanged());
107         }
108         return this.eval;
109     }
110 
111     /**
112      * Returns the last evaluator that was valid, i.e. did not have a circular dependency between input parameters.
113      * @return last valid evaluator.
114      */
115     public Eval getLastValidEval()
116     {
117         if (this.eval == null)
118         {
119             return new Eval();
120         }
121         return this.eval;
122     }
123 
124     @Override
125     public void notifyCreated(final XsdTreeNode node)
126     {
127         if (node.getPathString().equals(XsdPaths.SCENARIO))
128         {
129             this.scenarioParameters.put(node, new ArrayList<>());
130             setDirty();
131         }
132         else if (node.getPathString().equals(XsdPaths.INPUT_PARAMETERS)
133                 || node.getPathString().equals(XsdPaths.DEFAULT_INPUT_PARAMETERS))
134         {
135             node.addListener(this, XsdTreeNode.ACTIVATION_CHANGED);
136         }
137         else if ((node.getPathString().startsWith(XsdPaths.INPUT_PARAMETERS + ".")
138                 || node.getPathString().startsWith(XsdPaths.DEFAULT_INPUT_PARAMETERS + "."))
139                 && !node.getNodeName().equals("xsd:choice"))
140         {
141             node.addListener(this, XsdTreeNode.ATTRIBUTE_CHANGED);
142             node.addListener(this, XsdTreeNode.VALUE_CHANGED);
143             node.addListener(this, XsdTreeNode.ACTIVATION_CHANGED);
144             registerParameter(node); // node may be valid when its created as part of an undo, so we need to register it if so
145             setDirty();
146         }
147     }
148 
149     @Override
150     public void notifyRemoved(final XsdTreeNode node)
151     {
152         if (node.getPathString().equals(XsdPaths.SCENARIO))
153         {
154             this.scenarioParameters.remove(node);
155             setDirty();
156         }
157         else if (node.getPathString().equals(XsdPaths.INPUT_PARAMETERS)
158                 || node.getPathString().equals(XsdPaths.DEFAULT_INPUT_PARAMETERS))
159         {
160             node.removeListener(this, XsdTreeNode.ACTIVATION_CHANGED);
161         }
162         else if (node.getPathString().startsWith(XsdPaths.DEFAULT_INPUT_PARAMETERS + "."))
163         {
164             this.defaultParamaters.remove(this.parameterMap.remove(node));
165             node.removeListener(this, XsdTreeNode.ATTRIBUTE_CHANGED);
166             node.removeListener(this, XsdTreeNode.VALUE_CHANGED);
167             setDirty();
168         }
169         else if (node.getPathString().startsWith(XsdPaths.INPUT_PARAMETERS + "."))
170         {
171             this.scenarioParameters.forEach((s, list) -> list.remove(this.parameterMap.remove(node)));
172             node.removeListener(this, XsdTreeNode.ATTRIBUTE_CHANGED);
173             node.removeListener(this, XsdTreeNode.VALUE_CHANGED);
174             setDirty();
175         }
176     }
177 
178     @Override
179     public void notify(final Event event) throws RemoteException
180     {
181         if (event.getType().equals(XsdTreeNode.ATTRIBUTE_CHANGED) || event.getType().equals(XsdTreeNode.VALUE_CHANGED))
182         {
183             XsdTreeNode node = (XsdTreeNode) ((Object[]) event.getContent())[0];
184             registerParameter(node);
185             setDirty();
186         }
187         else if (event.getType().equals(XsdTreeNode.ACTIVATION_CHANGED))
188         {
189             Object[] content = (Object[]) event.getContent();
190             XsdTreeNode node = (XsdTreeNode) content[0];
191             boolean activated = (boolean) content[1];
192             if (node.getPathString().equals(XsdPaths.INPUT_PARAMETERS)
193                     || node.getPathString().equals(XsdPaths.DEFAULT_INPUT_PARAMETERS))
194             {
195                 if (activated)
196                 {
197                     node.getChildren().forEach((child) -> notifyCreated(child));
198                 }
199                 else
200                 {
201                     node.getChildren().forEach((child) -> notifyRemoved(child));
202                 }
203             }
204             else if ((node.getPathString().startsWith(XsdPaths.INPUT_PARAMETERS + ".")
205                     || node.getPathString().startsWith(XsdPaths.DEFAULT_INPUT_PARAMETERS + "."))
206                     && !node.getNodeName().equals("xsd:choice"))
207             {
208                 if (activated)
209                 {
210                     notifyCreated(node);
211                 }
212                 else
213                 {
214                     notifyRemoved(node);
215                 }
216             }
217         }
218         else
219         {
220             super.notify(event);
221         }
222     }
223 
224     /**
225      * Register the given node as a parameter.
226      * @param node node (default or scenario input parameter).
227      */
228     private void registerParameter(final XsdTreeNode node)
229     {
230         if (node.getPathString().startsWith(XsdPaths.DEFAULT_INPUT_PARAMETERS + "."))
231         {
232             this.defaultParamaters.remove(this.parameterMap.remove(node));
233             if (node.isValid())
234             {
235                 ParameterWrapper parameter = wrap(node);
236                 this.parameterMap.put(node, parameter);
237                 this.defaultParamaters.add(parameter);
238             }
239         }
240         else if (node.getPathString().startsWith(XsdPaths.INPUT_PARAMETERS + "."))
241         {
242             this.scenarioParameters.forEach((s, list) -> list.remove(this.parameterMap.remove(node)));
243             if (node.isValid())
244             {
245                 ParameterWrapper parameter = wrap(node);
246                 this.parameterMap.put(node, parameter);
247                 XsdTreeNode scenarioNode = node.getParent().getParent();
248                 if (scenarioNode != null)
249                 {
250                     this.scenarioParameters.get(scenarioNode).add(parameter);
251                 }
252             }
253         }
254     }
255 
256     /**
257      * Sets the evaluator as being dirty, i.e. some input parameter was added, removed or changed. All listeners are notified.
258      */
259     public void setDirty()
260     {
261         this.dirty = true;
262         this.listeners.forEach((listener) -> listener.evalChanged());
263     }
264 
265     /**
266      * Adds listener to changes in the evaluator, i.e. added, removed or changed input parameters.
267      * @param listener listener.
268      */
269     public void addListener(final EvalListener listener)
270     {
271         this.listeners.add(listener);
272     }
273 
274     /**
275      * Removes listener to changes in the evaluator, i.e. added, removed or changed input parameters.
276      * @param listener listener.
277      */
278     public void removeListener(final EvalListener listener)
279     {
280         this.listeners.remove(listener);
281     }
282 
283     /**
284      * Parameter representation of a node suitable for parsing an {@code Eval}.
285      * @param node node, must be a default or scenario input parameter node.
286      * @return parameter representation of a node suitable for parsing an {@code Eval}.
287      */
288     private ParameterWrapper wrap(final XsdTreeNode node)
289     {
290         try
291         {
292             Class<?> clazz = Class.forName(String.format(ADAPTER_MASK, node.getNodeName()));
293             Constructor<?> constructor = ClassUtil.resolveConstructor(clazz, new Object[0]);
294             ExpressionAdapter<?, ?> adapter = (ExpressionAdapter<?, ?>) constructor.newInstance();
295             return new ParameterWrapper(node.getId(), adapter.unmarshal(node.getValue()));
296         }
297         catch (Exception e)
298         {
299             throw new RuntimeException("Unable to wrap node " + node + " as a parameter for Eval.");
300         }
301     }
302 
303     /**
304      * Interface for listeners that need to know when evaluation results may have changed.
305      * <p>
306      * Copyright (c) 2023-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
307      * <br>
308      * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
309      * </p>
310      * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
311      */
312     @FunctionalInterface
313     public interface EvalListener
314     {
315         /**
316          * Notifies the listener that evaluation results may have changed.
317          */
318         public void evalChanged();
319     }
320 
321 }