Prerequisites

This tutorial assumes the reader already knows how to support a custom vector format.

Goal

The goal of this tutorial is to create an ILcdModelDecoder for a realtime data format. A realtime data format is a format in which the data values change over time.

The ILcdMultiDimensionalModel interface

Creating a realtime format works almost exactly the same as adding support for a static format. The only difference is that the ILcdModel created by the ILcdModelDecoder must implement the ILcdMultiDimensionalModel interface as well, and expose a time dimension.

The ILcdMultiDimensionalModel interface exposes that a model not only contains georeferenced data, but that the data may change over certain dimensions as well. For example, data may:

  • Change over time, recordings of air plane positions over time for instance

  • Depend on elevation, such as measurements of sea water temperature at several depths. Sea water temperature not only depends on the location, but also on the depth at which the measurement was taken. It is not uncommon that one data set contains measurements at multiple depths.

The ILcdMultiDimensionalModel interface also allows you to set a dimension to a specific value. This mechanism can for example be used to create a time slider UI: when the user drags the slider, the UI updates the time dimension on the model. It is up to the ILcdMultiDimensionalModel to update its contents to match that time.

Structure of the custom format

For this tutorial, we invented a realtime data format. Our custom realtime format represents tracks data: the position and elevation of an airplane recorded at regular intervals.

The data of our track format consists of CSV files with a *.trc extension and a ; value separator. Each file contains a:

  • Flight ID: a unique identifier for the flight. All entries with the same flight ID are considered recordings of the same flight from points A to B.

  • Call sign: the call sign of the plane

  • Longitude: the longitude of the plane, at the time of the recording

  • Latitude: the latitude of the plane, at the time of the recording

  • Altitude: the altitude of the plane, at the time of the recording

  • Time stamp: the time of the recording

An example file can be downloaded here.

Creating the model decoder

For our ILcdModelDecoder implementation, we start from a model decoder which accepts .trc files. Seeing as the .trc file is a regular CSV file, we delegate the actual reading and parsing of the .trc file to the TLcdCSVModelDecoder:

public class TracksModelDecoder implements ILcdModelDecoder {

  private static final String TYPE_NAME = "com.luciad.model.realtimetutorial.TracksModelDecoder.tracks";
  private static final String EXTENSION = ".trc";

  private final TLcdCSVModelDecoder fCSVModelDecoder = new TLcdCSVModelDecoder();

  public TracksModelDecoder() {
    fCSVModelDecoder.setExtensions(new String[]{EXTENSION});
    fCSVModelDecoder.setDefaultModelReference(new TLcdGeodeticReference());
  }

  @Override
  public String getDisplayName() {
    return "Tracks";
  }

  @Override
  public boolean canDecodeSource(String aSourceName) {
    return aSourceName.endsWith(EXTENSION);
  }

  @Override
  public ILcdModel decode(String aSourceName) throws IOException {
    ILcdModel csvModel = fCSVModelDecoder.decodeSource(TLcdCSVDataSource.newBuilder()
                                                                        .source(aSourceName)
                                                                        .separator(";")
                                                                        .firstRowContainsNames()
                                                                        .geometry(2, 3, 4)
                                                                        .build());
  }
}

The result is an ILcdModel containing all the flight recordings in the file as individual points. Now, we need to convert this model to a ILcdMultiDimensionalModel.

For that, we rely on the ALcdTimeIndexedSimulatorModel class: a support class that facilitates the creation of an ILcdModel with a time dependency. We need to initialize the ALcdTimeIndexedSimulatorModel class with an ILcdModel and a collection of tracks. The class offers a method to set the current date.

Each time the date changes, the ALcdTimeIndexedSimulatorModel class checks:

  • For each track whether it should be displayed

  • If the track needs to be displayed and was not displayed previously. If that is the case, it is added to the ILcdModel.

  • If the track does not need to be displayed and was displayed previously. If that is the case, it is removed from the ILcdModel

  • If the track was already being displayed and still needs to be displayed. If that is the case, it is updated with the new date.

When you create your own ILcdMultiDimensionalModel implementation, there is no need to use ALcdTimeIndexedSimulatorModel. You can implement the interface from scratch.

However, for realtime data representing moving objects, using this convenience class can avoid having to write a lot of boilerplate code.

We are going to do this in multiple steps:

  1. Because the ALcdTimeIndexedSimulatorModel needs track objects, the first step is to group all recordings with the same flight ID into a track in the decode method:

    Map<Integer, List<ILcdDataObject>> flightIDToRecordings = groupByFlightID(csvModel);

    where the groupByFlightID method is implemented by simply looking a the "FlightID" property and grouping the points in an ArrayList:

    private Map<Integer, List<ILcdDataObject>> groupByFlightID(ILcdModel aCSVModel) {
      Enumeration elements = aCSVModel.elements();
    
      Map<Integer, List<ILcdDataObject>> flightIDToRecordingsMap = new HashMap<>();
    
      while (elements.hasMoreElements()) {
        ILcdDataObject domainObject = (ILcdDataObject) elements.nextElement();
    
        int flightID = Integer.valueOf((String) domainObject.getValue("FlightID"));
        List<ILcdDataObject> recordingsForFlightID = flightIDToRecordingsMap.computeIfAbsent(flightID, key -> new ArrayList<>());
        recordingsForFlightID.add(domainObject);
      }
      return flightIDToRecordingsMap;
    }
  2. As our resulting ILcdModel contains tracks, we need to create a domain object representing a track. For this, we use an ILcdDataObject with the following properties:

    • The flight ID

    • The call sign

    • A list of all recorded positions, ordered by their timestamp

    • A list of all timestamps at which the position recording took place

    • The current position of the track: it is this position that

    package samples.lucy.showcase.tracks;
    
    import com.luciad.datamodel.TLcdCoreDataTypes;
    import com.luciad.datamodel.TLcdDataModel;
    import com.luciad.datamodel.TLcdDataModelBuilder;
    import com.luciad.datamodel.TLcdDataProperty;
    import com.luciad.datamodel.TLcdDataType;
    import com.luciad.datamodel.TLcdDataTypeBuilder;
    import com.luciad.shape.shape3D.TLcdLonLatHeightPoint;
    import com.luciad.util.TLcdHasGeometryAnnotation;
    
    final class TracksDataTypes {
    
      private static final TLcdDataModel TRACKS_DATA_MODEL;
    
      public static final TLcdDataType TRACK_RECORDING_DATA_TYPE;
    
      public static final String FLIGHT_ID = "flightID";
      public static final String CALL_SIGN = "callSign";
      public static final String CURRENT_POSITION = "currentPosition";
    
      public static final String RECORDED_POSITIONS = "recordedPositions";
      public static final String TIME_STAMPS = "timeStamps";
    
      static final String TRACK_RECORDING_TYPE = "TrackRecordingType";
    
      static {
        TRACKS_DATA_MODEL = createDataModel();
        TRACK_RECORDING_DATA_TYPE = TRACKS_DATA_MODEL.getDeclaredType(TRACK_RECORDING_TYPE);
      }
    
      private static TLcdDataModel createDataModel() {
        TLcdDataModelBuilder builder = new TLcdDataModelBuilder("http://www.mydomain.com/datamodel/TracksModel");
    
        TLcdDataTypeBuilder geometryType = builder.typeBuilder("GeometryType");
        geometryType.primitive(true).instanceClass(TLcdLonLatHeightPoint.class);
    
        TLcdDataTypeBuilder trackRecordingBuilder = builder.typeBuilder(TRACK_RECORDING_TYPE);
        trackRecordingBuilder.addProperty(FLIGHT_ID, TLcdCoreDataTypes.INTEGER_TYPE);
        trackRecordingBuilder.addProperty(CALL_SIGN, TLcdCoreDataTypes.STRING_TYPE);
        trackRecordingBuilder.addProperty(CURRENT_POSITION, geometryType);
    
        trackRecordingBuilder.addProperty(RECORDED_POSITIONS, geometryType).collectionType(TLcdDataProperty.CollectionType.LIST);
        trackRecordingBuilder.addProperty(TIME_STAMPS, TLcdCoreDataTypes.LONG_TYPE).collectionType(TLcdDataProperty.CollectionType.LIST);
    
        TLcdDataModel dataModel = builder.createDataModel();
    
        TLcdDataType type = dataModel.getDeclaredType(TRACK_RECORDING_TYPE);
        type.addAnnotation(new TLcdHasGeometryAnnotation(type.getProperty(CURRENT_POSITION)));
    
        return dataModel;
      }
    
      public static TLcdDataModel getDataModel() {
        return TRACKS_DATA_MODEL;
      }
    }
  3. Now that we have the TLcdDataModel, we can create our track domain objects. For each of the flight IDs we already have all recorded points. We just need to sort them according to their timestamp, and store all the information in an ILcdDataObject:

    List<ILcdDataObject> tracks = new ArrayList<>();
    
    for (Map.Entry<Integer, List<ILcdDataObject>> flightIDToRecordingsMap : flightIDToRecordings.entrySet()) {
    
      Integer flightID = flightIDToRecordingsMap.getKey();
      List<ILcdDataObject> allRecordingsForFlight = flightIDToRecordingsMap.getValue();
      allRecordingsForFlight.sort(Comparator.comparingLong(firstRecording -> Long.valueOf((String) firstRecording.getValue("TimeStamp"))));
    
      List<TLcdLonLatHeightPoint> allLocations = new ArrayList<>();
      List<Long> timeStamps = new ArrayList<>(allRecordingsForFlight.size());
    
      for (ILcdDataObject recording : allRecordingsForFlight) {
        timeStamps.add(Long.valueOf((String) recording.getValue("TimeStamp")));
        allLocations.add((TLcdLonLatHeightPoint) ALcdShape.fromDomainObject(recording));
      }
    
      Object callSign = allRecordingsForFlight.get(0).getValue("CallSign");
      TLcdLonLatHeightPoint currentLocation = allLocations.get(0);
    
      // Group all the individual points together into a track
      ILcdDataObject track = TracksDataTypes.TRACK_RECORDING_DATA_TYPE.newInstance();
      track.setValue(TracksDataTypes.FLIGHT_ID, flightID);
      track.setValue(TracksDataTypes.CALL_SIGN, callSign);
      track.setValue(TracksDataTypes.CURRENT_POSITION, currentLocation);
      track.setValue(TracksDataTypes.RECORDED_POSITIONS, allLocations);
      track.setValue(TracksDataTypes.TIME_STAMPS, timeStamps);
      tracks.add(track);
    }
  4. The final step is storing all this information in an ILcdModel and returning that model:

    TLcdDataModelDescriptor tracksModelDescriptor = new TLcdDataModelDescriptor(
        aSourceName,
        TYPE_NAME,
        "Tracks",
        TracksDataTypes.getDataModel(),
        Collections.singleton(TracksDataTypes.TRACK_RECORDING_DATA_TYPE),
        Collections.singleton(TracksDataTypes.TRACK_RECORDING_DATA_TYPE)
    );
    
    ILcdModelReference modelReference = csvModel.getModelReference();
    return new TracksModel(modelReference, tracksModelDescriptor, tracks);

    In this step, we return a TracksModel instance. This is our own custom model implementation based on an ALcdTimeIndexedSimulatorModel.

Creating the tracks model

For our TracksModel, we start from a regular TLcd2DBoundsIndexedModel which we will let implement the ILcdMultiDimensionalModel interface:

final class TracksModel extends TLcd2DBoundsIndexedModel implements ILcdMultiDimensionalModel {
}

First we expose the time dimension. For this, we need to loop over all the tracks and see what time interval they span. All these time intervals must be combined so that we know the time interval the whole model spans:

private final List<ILcdDimension<Date>> fDimensions = new ArrayList<>();

private TLcdDimensionFilter fDimensionFilter = TLcdDimensionFilter.EMPTY_FILTER;

TracksModel(ILcdModelReference aModelReference,
            ILcdModelDescriptor aModelDescriptor,
            Collection<ILcdDataObject> aTracks) {
  super(aModelReference, aModelDescriptor);

  fDimensions.add(buildDateDimension(aTracks));
}
@Override
public TLcdDimensionFilter getDimensionFilter() {
  return fDimensionFilter;
}

@Override
public List<? extends ILcdDimension<?>> getDimensions() {
  return Collections.unmodifiableList(fDimensions);
}

The last remaining piece of functionality is updating the model when the time changes. This functionality is offered by the ALcdTimeIndexedSimulatorModel class.

This class can update the contents of our TracksModel, and update the individual tracks when the time changes. We add a field to our TracksModel class

private final ALcdTimeIndexedSimulatorModel fTimeIndexedSimulatorModel;

and in the constructor, we instantiate the field:

fTimeIndexedSimulatorModel = new ALcdTimeIndexedSimulatorModel() {
  {
    init(TracksModel.this, aTracks);
  }

  @Override
  protected long getBeginTime(Object aTrack) {
    List<Long> timeStamps = (List<Long>) ((ILcdDataObject) aTrack).getValue(TracksDataTypes.TIME_STAMPS);
    return timeStamps.get(0);
  }

  @Override
  protected long getEndTime(Object aTrack) {
    List<Long> timeStamps = (List<Long>) ((ILcdDataObject) aTrack).getValue(TracksDataTypes.TIME_STAMPS);
    return timeStamps.get(timeStamps.size() - 1);
  }

  @Override
  protected boolean updateTrackForDateSFCT(ILcdModel aILcdModel, Object aTrack, Date aDate) {
    List<Long> timeStamps = (List<Long>) ((ILcdDataObject) aTrack).getValue(TracksDataTypes.TIME_STAMPS);
    List<TLcdLonLatHeightPoint> locations = (List<TLcdLonLatHeightPoint>) ((ILcdDataObject) aTrack).getValue(TracksDataTypes.RECORDED_POSITIONS);

    //Find the index of the time stamp matching with the new date
    int binarySearchIndex = Collections.binarySearch(timeStamps, aDate.getTime());
    int index;
    if (binarySearchIndex >= 0) {
      index = binarySearchIndex;
    } else {
      index = Math.min(timeStamps.size() - 1, Math.max(0, -1 * (binarySearchIndex + 1) - 1));
    }

    Object oldPosition = ((ILcdDataObject) aTrack).getValue(TracksDataTypes.CURRENT_POSITION);
    //Retrieve the position matching the new time stamp
    TLcdLonLatHeightPoint newPosition = locations.get(index);
    //Store this position in the object
    ((ILcdDataObject) aTrack).setValue(TracksDataTypes.CURRENT_POSITION, newPosition);
    //Compare the new and old position to indicate whether the track was updated or not
    return Objects.equals(oldPosition, newPosition);
  }
};

The getBeginTime and getEndTime implementations are straightforward. These methods indicate the valid time interval for a single track, which is a matter of retrieving the correct property from track domain object.

The updateTrackForDataSFCT method is a bit more complicated. That method needs to update the current position of the track, based on the current time. It does this by:

  1. finding the timestamp closest to the new date

  2. find the recorded position matching that timestamp

  3. update the current position property with this new position

Once this position is calculated, the method needs to indicate whether this is a new position or not. This is required so that the ALcdTimeIndexedSimulatorModel can ensure that our TracksModel only fires model events for elements that actually changed.

The last method to implement is the ILcdMultiDimensionalModel#applyDimensionFilter method. In this method, we retrieve the date from the filter and set on the ALcdTimeIndexedSimulatorModel:

@Override
public void applyDimensionFilter(TLcdDimensionFilter aFilter, int aEventMode) {
  fDimensionFilter = aFilter;

  Set<TLcdDimensionAxis<?>> axes = aFilter.getAxes();
  for (TLcdDimensionAxis<?> ax : axes) {
    if (ax.getType().equals(Date.class)) {
      TLcdDimensionInterval<Date> interval = (TLcdDimensionInterval<Date>) aFilter.getInterval(ax);
      //We opted for a simple implementation, where we only use the max value of the interval
      //instead of the whole interval
      Date date = interval.getMax();
      if (date == null) {
        //when removing the data from the view or closing the previewer, the filter is reset resulting in an unbounded interval
        //reset the date to the begin data in that case
        date = fDimensions.get(0).getUnionOfValues().getMin();
      }
      fTimeIndexedSimulatorModel.setDate(date);
    }
  }
}

The ALcdTimeIndexedSimulatorModel will now update our TracksModel and trigger the firing of the necessary model change events.

Full code

The TracksDataTypes class

package samples.lucy.showcase.tracks;

import com.luciad.datamodel.TLcdCoreDataTypes;
import com.luciad.datamodel.TLcdDataModel;
import com.luciad.datamodel.TLcdDataModelBuilder;
import com.luciad.datamodel.TLcdDataProperty;
import com.luciad.datamodel.TLcdDataType;
import com.luciad.datamodel.TLcdDataTypeBuilder;
import com.luciad.shape.shape3D.TLcdLonLatHeightPoint;
import com.luciad.util.TLcdHasGeometryAnnotation;

final class TracksDataTypes {

  private static final TLcdDataModel TRACKS_DATA_MODEL;

  public static final TLcdDataType TRACK_RECORDING_DATA_TYPE;

  public static final String FLIGHT_ID = "flightID";
  public static final String CALL_SIGN = "callSign";
  public static final String CURRENT_POSITION = "currentPosition";

  public static final String RECORDED_POSITIONS = "recordedPositions";
  public static final String TIME_STAMPS = "timeStamps";

  static final String TRACK_RECORDING_TYPE = "TrackRecordingType";

  static {
    TRACKS_DATA_MODEL = createDataModel();
    TRACK_RECORDING_DATA_TYPE = TRACKS_DATA_MODEL.getDeclaredType(TRACK_RECORDING_TYPE);
  }

  private static TLcdDataModel createDataModel() {
    TLcdDataModelBuilder builder = new TLcdDataModelBuilder("http://www.mydomain.com/datamodel/TracksModel");

    TLcdDataTypeBuilder geometryType = builder.typeBuilder("GeometryType");
    geometryType.primitive(true).instanceClass(TLcdLonLatHeightPoint.class);

    TLcdDataTypeBuilder trackRecordingBuilder = builder.typeBuilder(TRACK_RECORDING_TYPE);
    trackRecordingBuilder.addProperty(FLIGHT_ID, TLcdCoreDataTypes.INTEGER_TYPE);
    trackRecordingBuilder.addProperty(CALL_SIGN, TLcdCoreDataTypes.STRING_TYPE);
    trackRecordingBuilder.addProperty(CURRENT_POSITION, geometryType);

    trackRecordingBuilder.addProperty(RECORDED_POSITIONS, geometryType).collectionType(TLcdDataProperty.CollectionType.LIST);
    trackRecordingBuilder.addProperty(TIME_STAMPS, TLcdCoreDataTypes.LONG_TYPE).collectionType(TLcdDataProperty.CollectionType.LIST);

    TLcdDataModel dataModel = builder.createDataModel();

    TLcdDataType type = dataModel.getDeclaredType(TRACK_RECORDING_TYPE);
    type.addAnnotation(new TLcdHasGeometryAnnotation(type.getProperty(CURRENT_POSITION)));

    return dataModel;
  }

  public static TLcdDataModel getDataModel() {
    return TRACKS_DATA_MODEL;
  }
}

The TracksModelDecoder class

package samples.lucy.showcase.tracks;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.luciad.datamodel.ILcdDataObject;
import com.luciad.format.csv.TLcdCSVDataSource;
import com.luciad.format.csv.TLcdCSVModelDecoder;
import com.luciad.model.ILcdModel;
import com.luciad.model.ILcdModelDecoder;
import com.luciad.model.ILcdModelReference;
import com.luciad.model.TLcdDataModelDescriptor;
import com.luciad.reference.TLcdGeodeticReference;
import com.luciad.shape.ALcdShape;
import com.luciad.shape.shape3D.TLcdLonLatHeightPoint;

public class TracksModelDecoder implements ILcdModelDecoder {

  private static final String TYPE_NAME = "com.luciad.model.realtimetutorial.TracksModelDecoder.tracks";
  private static final String EXTENSION = ".trc";

  private final TLcdCSVModelDecoder fCSVModelDecoder = new TLcdCSVModelDecoder();

  public TracksModelDecoder() {
    fCSVModelDecoder.setExtensions(new String[]{EXTENSION});
    fCSVModelDecoder.setDefaultModelReference(new TLcdGeodeticReference());
  }

  @Override
  public String getDisplayName() {
    return "Tracks";
  }

  @Override
  public boolean canDecodeSource(String aSourceName) {
    return aSourceName.endsWith(EXTENSION);
  }

  @Override
  public ILcdModel decode(String aSourceName) throws IOException {
    ILcdModel csvModel = fCSVModelDecoder.decodeSource(TLcdCSVDataSource.newBuilder()
                                                                        .source(aSourceName)
                                                                        .separator(";")
                                                                        .firstRowContainsNames()
                                                                        .geometry(2, 3, 4)
                                                                        .build());

    Map<Integer, List<ILcdDataObject>> flightIDToRecordings = groupByFlightID(csvModel);
    List<ILcdDataObject> tracks = new ArrayList<>();

    for (Map.Entry<Integer, List<ILcdDataObject>> flightIDToRecordingsMap : flightIDToRecordings.entrySet()) {

      Integer flightID = flightIDToRecordingsMap.getKey();
      List<ILcdDataObject> allRecordingsForFlight = flightIDToRecordingsMap.getValue();
      allRecordingsForFlight.sort(Comparator.comparingLong(firstRecording -> Long.valueOf((String) firstRecording.getValue("TimeStamp"))));

      List<TLcdLonLatHeightPoint> allLocations = new ArrayList<>();
      List<Long> timeStamps = new ArrayList<>(allRecordingsForFlight.size());

      for (ILcdDataObject recording : allRecordingsForFlight) {
        timeStamps.add(Long.valueOf((String) recording.getValue("TimeStamp")));
        allLocations.add((TLcdLonLatHeightPoint) ALcdShape.fromDomainObject(recording));
      }

      Object callSign = allRecordingsForFlight.get(0).getValue("CallSign");
      TLcdLonLatHeightPoint currentLocation = allLocations.get(0);

      // Group all the individual points together into a track
      ILcdDataObject track = TracksDataTypes.TRACK_RECORDING_DATA_TYPE.newInstance();
      track.setValue(TracksDataTypes.FLIGHT_ID, flightID);
      track.setValue(TracksDataTypes.CALL_SIGN, callSign);
      track.setValue(TracksDataTypes.CURRENT_POSITION, currentLocation);
      track.setValue(TracksDataTypes.RECORDED_POSITIONS, allLocations);
      track.setValue(TracksDataTypes.TIME_STAMPS, timeStamps);
      tracks.add(track);
    }

    TLcdDataModelDescriptor tracksModelDescriptor = new TLcdDataModelDescriptor(
        aSourceName,
        TYPE_NAME,
        "Tracks",
        TracksDataTypes.getDataModel(),
        Collections.singleton(TracksDataTypes.TRACK_RECORDING_DATA_TYPE),
        Collections.singleton(TracksDataTypes.TRACK_RECORDING_DATA_TYPE)
    );

    ILcdModelReference modelReference = csvModel.getModelReference();
    return new TracksModel(modelReference, tracksModelDescriptor, tracks);
  }

  private Map<Integer, List<ILcdDataObject>> groupByFlightID(ILcdModel aCSVModel) {
    Enumeration elements = aCSVModel.elements();

    Map<Integer, List<ILcdDataObject>> flightIDToRecordingsMap = new HashMap<>();

    while (elements.hasMoreElements()) {
      ILcdDataObject domainObject = (ILcdDataObject) elements.nextElement();

      int flightID = Integer.valueOf((String) domainObject.getValue("FlightID"));
      List<ILcdDataObject> recordingsForFlightID = flightIDToRecordingsMap.computeIfAbsent(flightID, key -> new ArrayList<>());
      recordingsForFlightID.add(domainObject);
    }
    return flightIDToRecordingsMap;
  }
}

The TracksModel class

package samples.lucy.showcase.tracks;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.Set;

import com.luciad.datamodel.ILcdDataObject;
import com.luciad.model.ILcdModel;
import com.luciad.model.ILcdModelDescriptor;
import com.luciad.model.ILcdModelReference;
import com.luciad.model.TLcd2DBoundsIndexedModel;
import com.luciad.multidimensional.ILcdDimension;
import com.luciad.multidimensional.ILcdMultiDimensionalModel;
import com.luciad.multidimensional.TLcdDimension;
import com.luciad.multidimensional.TLcdDimensionAxis;
import com.luciad.multidimensional.TLcdDimensionFilter;
import com.luciad.multidimensional.TLcdDimensionInterval;
import com.luciad.realtime.ALcdTimeIndexedSimulatorModel;
import com.luciad.shape.shape3D.TLcdLonLatHeightPoint;

final class TracksModel extends TLcd2DBoundsIndexedModel implements ILcdMultiDimensionalModel {

  private final ALcdTimeIndexedSimulatorModel fTimeIndexedSimulatorModel;

  private final List<ILcdDimension<Date>> fDimensions = new ArrayList<>();

  private TLcdDimensionFilter fDimensionFilter = TLcdDimensionFilter.EMPTY_FILTER;

  TracksModel(ILcdModelReference aModelReference,
              ILcdModelDescriptor aModelDescriptor,
              Collection<ILcdDataObject> aTracks) {
    super(aModelReference, aModelDescriptor);

    fDimensions.add(buildDateDimension(aTracks));

    fTimeIndexedSimulatorModel = new ALcdTimeIndexedSimulatorModel() {
      {
        init(TracksModel.this, aTracks);
      }

      @Override
      protected long getBeginTime(Object aTrack) {
        List<Long> timeStamps = (List<Long>) ((ILcdDataObject) aTrack).getValue(TracksDataTypes.TIME_STAMPS);
        return timeStamps.get(0);
      }

      @Override
      protected long getEndTime(Object aTrack) {
        List<Long> timeStamps = (List<Long>) ((ILcdDataObject) aTrack).getValue(TracksDataTypes.TIME_STAMPS);
        return timeStamps.get(timeStamps.size() - 1);
      }

      @Override
      protected boolean updateTrackForDateSFCT(ILcdModel aILcdModel, Object aTrack, Date aDate) {
        List<Long> timeStamps = (List<Long>) ((ILcdDataObject) aTrack).getValue(TracksDataTypes.TIME_STAMPS);
        List<TLcdLonLatHeightPoint> locations = (List<TLcdLonLatHeightPoint>) ((ILcdDataObject) aTrack).getValue(TracksDataTypes.RECORDED_POSITIONS);

        //Find the index of the time stamp matching with the new date
        int binarySearchIndex = Collections.binarySearch(timeStamps, aDate.getTime());
        int index;
        if (binarySearchIndex >= 0) {
          index = binarySearchIndex;
        } else {
          index = Math.min(timeStamps.size() - 1, Math.max(0, -1 * (binarySearchIndex + 1) - 1));
        }

        Object oldPosition = ((ILcdDataObject) aTrack).getValue(TracksDataTypes.CURRENT_POSITION);
        //Retrieve the position matching the new time stamp
        TLcdLonLatHeightPoint newPosition = locations.get(index);
        //Store this position in the object
        ((ILcdDataObject) aTrack).setValue(TracksDataTypes.CURRENT_POSITION, newPosition);
        //Compare the new and old position to indicate whether the track was updated or not
        return Objects.equals(oldPosition, newPosition);
      }
    };
  }

  private TLcdDimension<Date> buildDateDimension(Collection<ILcdDataObject> aTracks) {
    Long startDate = Long.MAX_VALUE;
    Long endDate = Long.MIN_VALUE;
    for (ILcdDataObject track : aTracks) {
      List<Long> timeStamps = (List<Long>) track.getValue(TracksDataTypes.TIME_STAMPS);

      startDate = Math.min(startDate, timeStamps.get(0));
      endDate = Math.max(endDate, timeStamps.get(timeStamps.size() - 1));
    }

    return TLcdDimension.<Date>newBuilder()
        .axis(TLcdDimensionAxis.TIME_AXIS)
        .addInterval(TLcdDimensionInterval.create(Date.class, new Date(startDate), new Date(endDate)))
        .build();
  }

  @Override
  public void applyDimensionFilter(TLcdDimensionFilter aFilter, int aEventMode) {
    fDimensionFilter = aFilter;

    Set<TLcdDimensionAxis<?>> axes = aFilter.getAxes();
    for (TLcdDimensionAxis<?> ax : axes) {
      if (ax.getType().equals(Date.class)) {
        TLcdDimensionInterval<Date> interval = (TLcdDimensionInterval<Date>) aFilter.getInterval(ax);
        //We opted for a simple implementation, where we only use the max value of the interval
        //instead of the whole interval
        Date date = interval.getMax();
        if (date == null) {
          //when removing the data from the view or closing the previewer, the filter is reset resulting in an unbounded interval
          //reset the date to the begin data in that case
          date = fDimensions.get(0).getUnionOfValues().getMin();
        }
        fTimeIndexedSimulatorModel.setDate(date);
      }
    }
  }

  @Override
  public TLcdDimensionFilter getDimensionFilter() {
    return fDimensionFilter;
  }

  @Override
  public List<? extends ILcdDimension<?>> getDimensions() {
    return Collections.unmodifiableList(fDimensions);
  }
}