View Javadoc
1   package org.opentrafficsim.editor.extensions.map.edit;
2   
3   import java.awt.BasicStroke;
4   import java.awt.Color;
5   import java.awt.Graphics2D;
6   import java.awt.Shape;
7   import java.awt.image.ImageObserver;
8   import java.util.LinkedHashSet;
9   import java.util.Set;
10  import java.util.function.Consumer;
11  import java.util.function.Function;
12  import java.util.function.UnaryOperator;
13  
14  import org.djutils.draw.bounds.Bounds2d;
15  import org.djutils.draw.line.PolyLine2d;
16  import org.djutils.draw.line.Polygon2d;
17  import org.djutils.draw.point.DirectedPoint2d;
18  import org.djutils.draw.point.Point2d;
19  import org.opentrafficsim.animation.DrawLevel;
20  import org.opentrafficsim.animation.PaintLine;
21  import org.opentrafficsim.base.geometry.OtsShape;
22  import org.opentrafficsim.editor.OtsEditor;
23  import org.opentrafficsim.editor.OtsEditorProperties;
24  import org.opentrafficsim.editor.XsdTreeNode;
25  import org.opentrafficsim.editor.extensions.map.EditorMap;
26  import org.opentrafficsim.editor.extensions.map.edit.DraggableAnnotation.Draggable;
27  
28  import nl.tudelft.simulation.dsol.animation.d2.RenderableScale;
29  import nl.tudelft.simulation.naming.context.Contextualized;
30  
31  /**
32   * Draggable by which the user can influence a value in the data structure.
33   * <p>
34   * Copyright (c) 2026-2026 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.<br>
35   * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
36   * @author Wouter Schakel
37   */
38  public class DraggableAnnotation extends MapAnnotation<Draggable<?>>
39  {
40  
41      /** Fill color. */
42      private static final Color FILL_COLOR =
43              OtsEditorProperties.PROPERTIES_STORE.getColorOrDefault("map.draggableFillColor", Color.WHITE);
44  
45      /** Expression color. */
46      private static final Color EXPRESSION_COLOR =
47              OtsEditorProperties.PROPERTIES_STORE.getColorOrDefault("map.draggableExpressionColor", Color.RED);
48  
49      /** Edge color. */
50      private static final Color EDGE_COLOR =
51              OtsEditorProperties.PROPERTIES_STORE.getColorOrDefault("map.draggableEdgeColor", Color.BLACK);
52  
53      /** Pixel margin within which draggables are selected. */
54      private static final int DRAGGABLE_SELECT_MARGIN_PX =
55              OtsEditorProperties.PROPERTIES_STORE.getIntegerOrDefault("map.draggableSelectMarginPx", 3);
56  
57      /** Annotations that are static in the life cycle up to a mouse release. */
58      private final Set<HelperAnnotation> staticAnnotations = new LinkedHashSet<>();
59  
60      /** Annotations that are continuously updated while dragging. */
61      private final Set<HelperAnnotation> dynamicAnnotations = new LinkedHashSet<>();
62  
63      /** Whether the draggable is highlighted. */
64      private boolean highlight;
65  
66      /**
67       * Constructor.
68       * @param source source
69       * @param map map
70       */
71      public DraggableAnnotation(final Draggable<?> source, final EditorMap map)
72      {
73          super(source, map);
74          setScale(false);
75          setRotate(false);
76      }
77  
78      /**
79       * Sets whether the draggable is highlighted based on whether it is selected, and it is deletable.
80       * @param selected whether the draggable is selected
81       */
82      public void setHighlightIfDeletable(final boolean selected)
83      {
84          this.highlight = selected && isDeletable();
85      }
86  
87      /**
88       * Returns whether the draggable is deletable.
89       * @return whether the draggable is deletable
90       */
91      public boolean isDeletable()
92      {
93          return getSource().deleter != null;
94      }
95  
96      /**
97       * Shows the attribute and/or node in the tree that is affected by the draggable.
98       * @param editor editor
99       */
100     public void showValue(final OtsEditor editor)
101     {
102         if (getSource().node != null)
103         {
104             editor.show(getSource().node, getSource().attribute);
105         }
106     }
107 
108     /**
109      * Delete represented data of draggable.
110      */
111     public void delete()
112     {
113         if (isDeletable())
114         {
115             getSource().deleter.run();
116         }
117     }
118 
119     @Override
120     public void paint(final Graphics2D graphics, final ImageObserver observer)
121     {
122         setRendering(graphics);
123         graphics.setStroke(new BasicStroke(1.0f));
124         graphics.setColor(getSource().isExpression ? EXPRESSION_COLOR : FILL_COLOR);
125         Shape shape = PaintLine.getPath(ZERO, getSource().annotation);
126         graphics.fill(shape);
127         graphics.setColor(this.highlight ? SelectionAnnotation.SELECTION_COLOR : EDGE_COLOR);
128         graphics.draw(shape);
129         resetRendering(graphics);
130     }
131 
132     /**
133      * Mouse was dragged.
134      * @param mousePoint world coordinate of mouse
135      */
136     public void mouseDragged(final Point2d mousePoint)
137     {
138         getSource().setPointSnapped(mousePoint);
139         for (HelperAnnotation annotation : this.dynamicAnnotations)
140         {
141             annotation.getSource().setLocation(getSource().getLocation());
142         }
143     }
144 
145     /**
146      * Mouse was released.
147      * @param mousePoint world coordinate of mouse
148      */
149     public void mouseReleased(final Point2d mousePoint)
150     {
151         getSource().setPointSnapped(mousePoint);
152         getSource().setValue();
153         // there is no need to update this.staticAnnotations, all annotations are reloaded on MapData.MAP_DATA_CHANGED event
154     }
155 
156     /**
157      * Add helper annotation. The update mode determines when the location is updated, which is set to the location of the
158      * animated draggable.
159      * @param annotation annotation
160      * @param updateMode whether the annotation is update while dragging or only on mouse release
161      */
162     public void addHelperAnnotation(final HelperAnnotation annotation, final UpdateMode updateMode)
163     {
164         (updateMode == UpdateMode.DYNAMIC ? this.dynamicAnnotations : this.staticAnnotations).add(annotation);
165     }
166 
167     @Override
168     public synchronized void destroy(final Contextualized contextProvider)
169     {
170         super.destroy(contextProvider);
171         this.staticAnnotations.forEach((a) ->
172         {
173             a.destroy(contextProvider);
174         });
175         this.dynamicAnnotations.forEach((a) ->
176         {
177             a.destroy(contextProvider);
178         });
179     }
180 
181     @Override
182     public boolean contains(final Point2d pointRelativeTo00, final RenderableScale scale, final double worldMargin,
183             final double pixelMargin, final double xScale, final double yScale)
184     {
185         return getSource().signedDistance(pointRelativeTo00.x / xScale,
186                 pointRelativeTo00.y / yScale) < DRAGGABLE_SELECT_MARGIN_PX;
187     }
188 
189     /**
190      * Data for draggable renderable.
191      * @param <T> value type that the draggable sets
192      */
193     public static class Draggable<T> implements OtsShape
194     {
195 
196         /** Location of draggable. */
197         private DirectedPoint2d location;
198 
199         /** Annotation shape around point. */
200         private final PolyLine2d annotation;
201 
202         /** Logic to snap mouse world coordinate to world point of draggable. */
203         private final UnaryOperator<Point2d> snapper;
204 
205         /** Function that determines the value from the draggable point. */
206         private final Function<Point2d, T> valueFunction;
207 
208         /** Writes the value typically in an {@link XsdTreeNode}. */
209         private final Consumer<T> valueWriter;
210 
211         /** Tree node of which this draggable affects the value (or attribute), if any. */
212         private XsdTreeNode node;
213 
214         /** Attribute of tree node that this draggable affects, if any. */
215         private String attribute;
216 
217         /** Deletes something in the data structure. */
218         private Runnable deleter = null;
219 
220         /** Last value. */
221         private T value;
222 
223         /** Whether a default value was set. */
224         private boolean defaultValueSet = false;
225 
226         /** Default value. */
227         private T defaultValue = null;
228 
229         /** Whether this draggable concerns a value that is currently an expression. */
230         private boolean isExpression;
231 
232         /**
233          * Constructor.
234          * @param initialLocation initial location
235          * @param annotation annotation shape around point
236          * @param snapper logic to snap mouse world coordinate to world point of draggable
237          * @param valueFunction function that determines the value from the draggable point
238          * @param valueWriter set the value typically in an {@link XsdTreeNode}
239          */
240         public Draggable(final Point2d initialLocation, final PolyLine2d annotation, final UnaryOperator<Point2d> snapper,
241                 final Function<Point2d, T> valueFunction, final Consumer<T> valueWriter)
242         {
243             this.location = new DirectedPoint2d(initialLocation, 0.0);
244             this.annotation = annotation;
245             this.snapper = snapper;
246             this.valueFunction = valueFunction;
247             this.valueWriter = valueWriter;
248         }
249 
250         /**
251          * Set default value (which is set after the user double clicks the draggable).
252          * @param defaultValue default value
253          * @return this, for method chaining
254          */
255         public Draggable<T> setDefaultValue(final T defaultValue)
256         {
257             this.defaultValue = defaultValue;
258             this.defaultValueSet = true;
259             return this;
260         }
261 
262         /**
263          * Sets node of which this draggable affects the value. This is for display purposes (shift-click draggable to show in
264          * tree).
265          * @param node node of which this draggable affects the value
266          * @return this, for method chaining
267          */
268         public Draggable<T> setNode(final XsdTreeNode node)
269         {
270             this.node = node;
271             this.attribute = null;
272             this.isExpression = node.valueIsExpression();
273             return this;
274         }
275 
276         /**
277          * Returns the affected node.
278          * @return affected node.
279          */
280         XsdTreeNode getNode()
281         {
282             return this.node;
283         }
284 
285         /**
286          * Sets node and attribute of the node of which this draggable affects the value. This is for display purposes
287          * (shift-click draggable to show in tree).
288          * @param node node of which this draggable affects an attribute value
289          * @param attribute attribute name
290          * @return this, for method chaining
291          */
292         @SuppressWarnings("hiddenfield")
293         public Draggable<T> setNodeAttribute(final XsdTreeNode node, final String attribute)
294         {
295             this.node = node;
296             this.attribute = attribute;
297             this.isExpression = node.attributeIsExpression(node.getAttributeIndexByName(attribute));
298             return this;
299         }
300 
301         /**
302          * Sets a deleter which deletes something in the data structure when the draggable is deleted by the user.
303          * @param deleter deletes something in the data structure
304          * @return this, for method chaining
305          */
306         public Draggable<T> setDeleter(final Runnable deleter)
307         {
308             this.deleter = deleter;
309             return this;
310         }
311 
312         /**
313          * Writes the default value.
314          */
315         public void writeDefaultValue()
316         {
317             if (this.defaultValueSet && !this.isExpression)
318             {
319                 this.value = this.defaultValue;
320                 this.valueWriter.accept(this.value);
321             }
322         }
323 
324         /**
325          * Set the point based on mouse world coordinate. Also updates the internal draggable value, but not the value in the
326          * data tree.
327          * @param mousePoint mouse world coordinate
328          */
329         public void setPointSnapped(final Point2d mousePoint)
330         {
331             if (this.isExpression)
332             {
333                 return;
334             }
335             Point2d point = this.snapper.apply(mousePoint);
336             this.location = new DirectedPoint2d(point, 0.0);
337             this.value = this.valueFunction.apply(point);
338         }
339 
340         /**
341          * Sets the value typically in an {@link XsdTreeNode}.
342          */
343         public void setValue()
344         {
345             if (this.isExpression)
346             {
347                 return;
348             }
349             this.valueWriter.accept(this.value);
350         }
351 
352         /**
353          * Returns the current value based on the draggable location. That can be regarded as a continuous and temporary value
354          * that is coupled to the live location of the draggable. This live value may be, and likely is, inconsistent with the
355          * current value in the data structure (e.g. value in a {@link XsdTreeNode}). The live value is intended to be used by
356          * live visual aides that indicate to the user what the draggable location will result in. For example, a dashed line
357          * indicating a new centerline.
358          * @return current value based on the draggable location
359          */
360         public T getValue()
361         {
362             return this.value;
363         }
364 
365         @Override
366         public double getZ()
367         {
368             return DrawLevel.ANNOTATION.getZ();
369         }
370 
371         @Override
372         public DirectedPoint2d getLocation()
373         {
374             return this.location;
375         }
376 
377         @Override
378         public Bounds2d getRelativeBounds()
379         {
380             return this.annotation.getAbsoluteBounds();
381         }
382 
383         @Override
384         public Polygon2d getRelativeContour()
385         {
386             return new Polygon2d(this.annotation.getPointList());
387         }
388 
389     }
390 
391     /**
392      * Update mode option.
393      */
394     public enum UpdateMode
395     {
396         /** Update draggable while dragging. */
397         DYNAMIC,
398 
399         /** Update draggable only on mouse release. */
400         STATIC;
401     }
402 
403 }