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