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