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 }