1818package org .aavso .tools .vstar .ui .mediator ;
1919
2020import java .awt .Window ;
21+ import java .time .LocalTime ;
22+ import java .time .format .DateTimeFormatter ;
2123import java .util .HashMap ;
24+ import java .util .IdentityHashMap ;
2225import java .util .Map ;
2326import java .util .TreeMap ;
2427
4649 */
4750@ SuppressWarnings ("serial" )
4851public class DocumentManager {
52+ private static final DateTimeFormatter MODEL_LABEL_TIME_FORMATTER = DateTimeFormatter .ofPattern ("HH:mm:ss" );
4953
5054 private Mediator mediator ;
5155
@@ -55,6 +59,7 @@ public class DocumentManager {
5559
5660 private Map <String , SyntheticObservationListPane <AbstractModelObservationTableModel >> rawDataResidualComponents ;
5761 private Map <String , SyntheticObservationListPane <AbstractModelObservationTableModel >> phasedResidualComponents ;
62+ private Map <IModel , String > uniqueModelKeys ;
5863
5964 private boolean phasePlotExists ;
6065 private double epoch ;
@@ -163,7 +168,8 @@ private void togglePlotControlState(Map<AnalysisType, Boolean> map) {
163168 public SyntheticObservationListPane <AbstractModelObservationTableModel > getModelListPane (AnalysisType type ,
164169 IModel model ) {
165170 SyntheticObservationListPane <AbstractModelObservationTableModel > pane = null ;
166- String key = model .getDescription ();
171+ String modelKey = getOrCreateUniqueModelKey (model );
172+ String key = modelKey ;
167173
168174 switch (type ) {
169175 case RAW_DATA :
@@ -183,7 +189,7 @@ public SyntheticObservationListPane<AbstractModelObservationTableModel> getModel
183189 break ;
184190
185191 case PHASE_PLOT :
186- key = getPhasedModelKey (model );
192+ key = getPhasedModelKey (modelKey );
187193
188194 if (!phasedModelComponents .containsKey (key )) {
189195 // Set the fit list's phases according to the last phase change.
@@ -211,7 +217,8 @@ public SyntheticObservationListPane<AbstractModelObservationTableModel> getModel
211217 public SyntheticObservationListPane <AbstractModelObservationTableModel > getResidualsListPane (AnalysisType type ,
212218 IModel model ) {
213219 SyntheticObservationListPane <AbstractModelObservationTableModel > pane = null ;
214- String key = model .getDescription ();
220+ String modelKey = getOrCreateUniqueModelKey (model );
221+ String key = modelKey ;
215222
216223 switch (type ) {
217224 case RAW_DATA :
@@ -229,7 +236,7 @@ public SyntheticObservationListPane<AbstractModelObservationTableModel> getResid
229236 break ;
230237
231238 case PHASE_PLOT :
232- key = getPhasedModelKey (model );
239+ key = getPhasedModelKey (modelKey );
233240
234241 if (!phasedResidualComponents .containsKey (key )) {
235242 // Set the residual list's phases according to the last phase
@@ -324,8 +331,55 @@ public String getNextUntitledFilterName() {
324331 * @param model The model whose description we will use as part of the key.
325332 * @return The unique key from the tuple: <description, epoch, period>.
326333 */
327- private String getPhasedModelKey (IModel model ) {
328- return String .format ("%s:e=%f,p=%f" , model .getDescription (), epoch , period );
334+ private String getPhasedModelKey (String modelKey ) {
335+ return String .format ("%s:e=%f,p=%f" , modelKey , epoch , period );
336+ }
337+
338+ /**
339+ * Return a stable unique key for this model instance for the life of the active
340+ * document. Keys are derived from model description, but disambiguated on
341+ * conflicts via " #N" suffixes to preserve models with identical descriptions.
342+ */
343+ private String getOrCreateUniqueModelKey (IModel model ) {
344+ if (uniqueModelKeys .containsKey (model )) {
345+ return uniqueModelKeys .get (model );
346+ }
347+
348+ String baseKey = buildModelLabel (model );
349+ String key = baseKey ;
350+ int suffix = 2 ;
351+
352+ while (isModelKeyInUse (key )) {
353+ key = String .format ("%s #%d" , baseKey , suffix ++);
354+ }
355+
356+ uniqueModelKeys .put (model , key );
357+ return key ;
358+ }
359+
360+ private boolean isModelKeyInUse (String key ) {
361+ return uniqueModelKeys .containsValue (key ) || rawDataModelComponents .containsKey (key )
362+ || rawDataResidualComponents .containsKey (key )
363+ || phasedModelComponents .containsKey (getPhasedModelKey (key ))
364+ || phasedResidualComponents .containsKey (getPhasedModelKey (key ));
365+ }
366+
367+ /**
368+ * Build a human-readable label for model and residual document components.
369+ * Includes model description, observation count, and creation time to reduce
370+ * collisions while preserving user context.
371+ */
372+ private String buildModelLabel (IModel model ) {
373+ int obsCount = model .getFit () == null ? 0 : model .getFit ().size ();
374+ String time = LocalTime .now ().format (MODEL_LABEL_TIME_FORMATTER );
375+ return String .format ("%s (%d obs, %s)" , model .getDescription (), obsCount , time );
376+ }
377+
378+ /**
379+ * Return the display label/key for this model instance.
380+ */
381+ public String getModelDisplayLabel (IModel model ) {
382+ return getOrCreateUniqueModelKey (model );
329383 }
330384
331385 /**
@@ -357,6 +411,11 @@ public void init() {
357411 }
358412 phasedResidualComponents .clear ();
359413
414+ if (uniqueModelKeys == null ) {
415+ uniqueModelKeys = new IdentityHashMap <IModel , String >();
416+ }
417+ uniqueModelKeys .clear ();
418+
360419 // Boolean maps
361420 if (showErrorBars == null ) {
362421 showErrorBars = new HashMap <AnalysisType , Boolean >();
0 commit comments