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 airplane 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 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:
-
Because the
ALcdTimeIndexedSimulatorModel
needs track objects, the first step is to group all recordings with the same flight ID into a track in thedecode
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 anArrayList
: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.parseInt((String) domainObject.getValue("FlightID")); List<ILcdDataObject> recordingsForFlightID = flightIDToRecordingsMap.computeIfAbsent(flightID, key -> new ArrayList<>()); recordingsForFlightID.add(domainObject); } return flightIDToRecordingsMap; }
-
As our resulting
ILcdModel
contains tracks, we need to create a domain object representing a track. For this, we use anILcdDataObject
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
-
Will be updated when the time set on the
ILcdMultiDimensionalModel
changes -
Will be drawn on the map. For this purpose, we annotate that property with the
TLcdHasGeometryAnnotation
so that the visualization code picks this up.
-
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; } }
-
-
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 anILcdDataObject
: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.parseLong((String) firstRecording.getValue("TimeStamp")))); List<TLcdLonLatHeightPoint> allLocations = new ArrayList<>(); List<Long> timeStamps = new ArrayList<>(allRecordingsForFlight.size()); for (ILcdDataObject recording : allRecordingsForFlight) { // the trc format uses seconds, not milliseconds Long timeStampSeconds = Long.valueOf((String) recording.getValue("TimeStamp")); long epochMillis = timeStampSeconds * 1000; timeStamps.add(epochMillis); 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); }
-
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 anALcdTimeIndexedSimulatorModel
.
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:
-
finding the timestamp closest to the new date
-
find the recorded position matching that timestamp
-
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.parseLong((String) firstRecording.getValue("TimeStamp"))));
List<TLcdLonLatHeightPoint> allLocations = new ArrayList<>();
List<Long> timeStamps = new ArrayList<>(allRecordingsForFlight.size());
for (ILcdDataObject recording : allRecordingsForFlight) {
// the trc format uses seconds, not milliseconds
Long timeStampSeconds = Long.valueOf((String) recording.getValue("TimeStamp"));
long epochMillis = timeStampSeconds * 1000;
timeStamps.add(epochMillis);
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.parseInt((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);
}
}