View Javadoc
1   package org.djunits.quantity.def;
2   
3   import java.util.LinkedHashMap;
4   import java.util.Map;
5   import java.util.Objects;
6   
7   import org.djutils.base.Identifiable;
8   import org.djutils.exceptions.Throw;
9   
10  /**
11   * Reference contains information about the reference point or origin / zero point of an absolute quantity.
12   * <p>
13   * Copyright (c) 2025-2026 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved. See
14   * for project information <a href="https://djunits.org" target="_blank">https://djunits.org</a>. The DJUNITS project is
15   * distributed under a <a href="https://djunits.org/docs/license.html" target="_blank">three-clause BSD-style license</a>.
16   * @author Alexander Verbraeck
17   * @param <R> the reference type itself
18   * @param <Q> the relative quantity for the offset
19   */
20  public abstract class AbstractReference<R extends AbstractReference<R, Q>, Q extends Quantity<Q, ?>> implements Identifiable
21  {
22      /**
23       * Master registry: per concrete Reference subclass we keep a map of id to reference. This prevents name collisions between
24       * different absolute quantities.
25       */
26      @SuppressWarnings("checkstyle:visibilitymodifier")
27      protected static final Map<Class<?>, Map<String, AbstractReference<?, ?>>> REFERENCES = new LinkedHashMap<>();
28  
29      /** The id. */
30      private final String id;
31  
32      /** The explanation. */
33      private final String name;
34  
35      /** The offset w.r.t. the offset reference, is ZERO when offsetReference is null. */
36      private final Q offset;
37  
38      /** The reference to which the offset is relative, can be null. */
39      private final R offsetReference;
40  
41      /**
42       * Define a new reference point for the absolute quantity. Prevent duplicate registration of the same id within the same
43       * Reference subclass.
44       * @param id the id
45       * @param name the name or explanation
46       * @param offset the offset w.r.t. the offsetReference, should be ZERO when offsetReference is null
47       * @param offsetReference the reference to which the offset is relative, can be null
48       * @throws IllegalArgumentException if an id is already registered for this Reference subclass
49       */
50      public AbstractReference(final String id, final String name, final Q offset, final R offsetReference)
51      {
52          Throw.whenNull(id, "id");
53          Throw.whenNull(name, "name");
54          Throw.whenNull(offset, "offset");
55  
56          this.id = id;
57          this.name = name;
58          this.offset = offset;
59          this.offsetReference = offsetReference;
60  
61          // Register in the per-class map for THIS concrete Reference subclass.
62          final Class<?> refClass = getClass();
63          final Map<String, AbstractReference<?, ?>> map = mapFor(refClass);
64  
65          Throw.when(map.containsKey(id), IllegalArgumentException.class, "Reference id '%s' already registered for %s", id,
66                  refClass.getSimpleName());
67  
68          map.put(id, this);
69      }
70  
71      /**
72       * Get or create the inner map for a specific Reference subclass.
73       * @param referenceClass the reference class to look up
74       * @return the existing or new reference map for the the Reference subclass
75       */
76      protected static Map<String, AbstractReference<?, ?>> mapFor(final Class<?> referenceClass)
77      {
78          return REFERENCES.computeIfAbsent(referenceClass, k -> new LinkedHashMap<>());
79      }
80  
81      /**
82       * Fetch a reference by class and id. Returns null when not found.
83       * @param referenceClass the concrete Reference subclass
84       * @param id the id
85       * @return the reference instance or null
86       * @param <R> the reference subclass type
87       */
88      @SuppressWarnings("unchecked")
89      public static <R extends AbstractReference<R, ?>> R get(final Class<R> referenceClass, final String id)
90      {
91          final Map<String, AbstractReference<?, ?>> map = REFERENCES.get(referenceClass);
92          if (map == null)
93          {
94              return null;
95          }
96          return (R) map.get(id);
97      }
98  
99      /**
100      * Check existence of id in a specific Reference subclass.
101      * @param referenceClass the reference subclass to check
102      * @param id the id to check
103      * @return whether the id exists for the Reference subclass
104      */
105     public static boolean containsId(final Class<?> referenceClass, final String id)
106     {
107         final Map<String, AbstractReference<?, ?>> map = REFERENCES.get(referenceClass);
108         return map != null && map.containsKey(id);
109     }
110 
111     /**
112      * Return a safe copy (snapshot) of the registry for a Reference subclass.
113      * @param referenceClass the reference subclass to retrieve
114      * @return a safe copy of the reference map
115      */
116     public static Map<String, AbstractReference<?, ?>> snapshotMap(final Class<?> referenceClass)
117     {
118         final Map<String, AbstractReference<?, ?>> map = REFERENCES.get(referenceClass);
119         return map == null ? Map.of() : new LinkedHashMap<>(map);
120     }
121 
122     /**
123      * Return a safe copy of the static reference map for this Reference subclass.
124      * @return a safe copy of the static reference map for this subclass
125      */
126     public Map<String, AbstractReference<?, ?>> getReferenceMap()
127     {
128         return snapshotMap(getClass());
129     }
130 
131     /**
132      * Instance-level unregister; removes this reference from the per-class registry. Intended primarily for unit tests to clean
133      * up temporary references. Existing objects that hold a direct pointer to this instance continue to work.
134      * @return true if this reference was removed from the registry; false if it was not present
135      */
136     public boolean unregister()
137     {
138         final Map<String, AbstractReference<?, ?>> map = mapFor(getClass());
139         synchronized (map)
140         {
141             // remove(key, value): only remove if the map still points at *this* instance
142             return map.remove(getId(), this);
143         }
144     }
145 
146     /**
147      * Return the offset w.r.t. the offset reference, or zero when the offset is not defined.
148      * @return the offset expressed in the relative quantity
149      */
150     public Q getOffset()
151     {
152         return this.offset;
153     }
154 
155     /**
156      * Return the offset reference for the offset, or null when the offset reference is not defined.
157      * @return the offset reference
158      */
159     public R getOffsetReference()
160     {
161         return this.offsetReference;
162     }
163 
164     @Override
165     public String getId()
166     {
167         return this.id;
168     }
169 
170     /** @return description of this reference point */
171     public String getName()
172     {
173         return this.name;
174     }
175 
176     @Override
177     public int hashCode()
178     {
179         return Objects.hash(this.id);
180     }
181 
182     @SuppressWarnings("checkstyle:needbraces")
183     @Override
184     public boolean equals(final Object obj)
185     {
186         if (this == obj)
187             return true;
188         if (obj == null)
189             return false;
190         if (getClass() != obj.getClass())
191             return false;
192         AbstractReference<?, ?> other = (AbstractReference<?, ?>) obj;
193         return Objects.equals(this.id, other.id);
194     }
195 
196     @Override
197     public String toString()
198     {
199         return this.id;
200     }
201 }