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