Prerequisites
In this tutorial, it is assumed that you are familiar with:
-
The LuciadLightspeed concepts introduced in the Create your first Lightspeed application tutorial:
-
Lightspeed views:
ILspView
-
Lightspeed layers:
ILspLayer
-
Model and model decoders:
ILcdModel
andILcdModelDecoder
-
-
The basics of the data modeling API, as introduced in the Introduction to the data modeling API tutorial:
-
The services mechanism, as introduced in Introduction to the services mechanism.
Goal
In this tutorial, we are going to add support for a custom vector format.
Most of the time, it suffices to use one of the many model decoders provided by the LuciadLightspeed API to read business data from an external source. For example, to decode SHP data, you can use the model decoders available in the LuciadLightspeed API.
However, sometimes you have data defined in a custom format for which LuciadLightspeed does not provide a default model decoder. In that case, you need to create the model and the decoder yourself.
To create a model decoder, you need to choose or define LuciadLightspeed classes for:
-
The domain objects (the model data)
-
The model reference
-
The model descriptor
-
The model
In this tutorial, we are going to perform all those steps.
Once we have the model decoder, we can visualize the data using the standard LuciadLightspeed API classes.
The custom file format
The file format we are going to support is a fictitious file format for waypoints, geographical locations used for aircraft navigation.
Each waypoint is represented by:
-
A line with the identifier of the waypoint
-
A line containing a set of coordinates for the waypoint, specifying its longitude, latitude and height.
The coordinates are expressed as double values, and are separated by a space.
There is an empty line between each waypoint.
For example:
WayPoint WP001
-94.63313085049567 40.01131142211313 5000
Waypoint WP002
-96.32255011830625 39.78181694038106 5000
WayPoint WP003
-101.42797384139817 38.90782811827928 5000
You can download the example file custom.cwp
here.
Modeling the domain objects
We are going to use ILcdDataObject
instances for our domain objects, because it offers us the benefit of re-using the standard LuciadLightspeed classes, when
we are labeling the data on the map, for example.
Similar to what we did in the
Introduction to the data modeling API
tutorial, we create the TLcdDataModel
and the TLcdDataType
for our domain objects first:
TLcdDataModel
public static final TLcdDataModel DATA_MODEL;
public static final TLcdDataType WAYPOINT_TYPE;
static {
//Create the TLcdDataModel for our waypoints
TLcdDataModelBuilder dataModelBuilder =
new TLcdDataModelBuilder("http://www.luciad.com/tutorial/waypoints");
//Create a TLcdDataType for the geometry
TLcdDataTypeBuilder pointTypeBuilder =
dataModelBuilder.typeBuilder("PointType")
.superType(TLcdShapeDataTypes.SHAPE_TYPE)
.primitive(true)
.instanceClass(TLcdLonLatHeightPoint.class);
TLcdDataTypeBuilder wayPointTypeBuilder =
dataModelBuilder.typeBuilder("WayPointType");
wayPointTypeBuilder.addProperty("identifier", TLcdCoreDataTypes.STRING_TYPE);
wayPointTypeBuilder.addProperty("location", pointTypeBuilder);
TLcdDataModel dataModel = dataModelBuilder.createDataModel();
//Specify which property holds the geometry
TLcdDataType wayPointType = dataModel.getDeclaredType("WayPointType");
wayPointType.addAnnotation(new TLcdHasGeometryAnnotation(wayPointType.getProperty("location")));
DATA_MODEL = dataModel;
WAYPOINT_TYPE = wayPointType;
}
For the geometry, we use TLcdLonLatHeightPoint
instances.
This is a point shape with a longitude, latitude and elevation, an exact match to our waypoints.
Creating the model decoder
Creating a model decoder comes down to implementing the ILcdModelDecoder
interface.
We start by implementing the getDisplayName
method.
This method should return a human-readable name for the format that the model decoder can decode.
@Override
public String getDisplayName() {
return "Way Points";
}
A model decoder should also indicate which sources it can handle. This check needs to be a fast one. A typical implementation checks the extension:
@Override
public boolean canDecodeSource(String aSourceName) {
return aSourceName != null && aSourceName.endsWith(".cwp");
}
The last method to implement is the method that performs the actual decoding. In this method, we:
-
Create an
ILcdModel
and configure it with:-
An
ILcdModelReference
: the georeference in which the coordinates of the shapes are expressed. In the case of our waypoint data, this is a WGS84 reference. -
An
ILcdDataModelDescriptor
: because we useILcdDataObject
instances as domain objects, ourILcdModelDescriptor
must be anILcdDataModelDescriptor
instance.
-
-
Create the model elements by reading the file.
-
Add the model elements to the model.
-
Return the model.
In code, this looks like:
@Override
public ILcdModel decode(String aSourceName) throws IOException {
//Sanity check to see whether we can decode the data
if (!canDecodeSource(aSourceName)) {
throw new IOException("Cannot decode " + aSourceName);
}
//Create an empty model
TLcd2DBoundsIndexedModel model = createEmptyModel();
//With the correct reference
//The reference of the model expresses how the coordinates of the
//shapes should be interpreted
model.setModelReference(createModelReference());
//And a ILcdDataModelDescriptor to expose the data model
ILcdDataModelDescriptor modelDescriptor = createModelDescriptor(aSourceName);
model.setModelDescriptor(modelDescriptor);
//Read the file and create the domain objects (=ILcdDataObjects of the WayPointType)
List<ILcdDataObject> wayPoints = createWayPoints(aSourceName);
//Add them to the model
model.addElements(new Vector<>(wayPoints), ILcdModel.NO_EVENT);
//Return the created model
return model;
}
We use a TLcd2DBoundsIndexedModel
as the model class.
This model implementation provides a spatial index, allowing for fast queries of the data.
private TLcd2DBoundsIndexedModel createEmptyModel() {
return new TLcd2DBoundsIndexedModel();
}
For the model reference, we use a WGS84 reference.
private ILcdModelReference createModelReference() {
return new TLcdGeodeticReference();//WGS-84 reference
}
For the ILcdDataModelDescriptor
, we use the default implementation of the interface: TLcdDataModelDescriptor
.
private ILcdDataModelDescriptor createModelDescriptor(String aSourceName) {
return new TLcdDataModelDescriptor(aSourceName,
"CWP", //Typename: identifier of the format
"Way Points",//Display name for the model
DATA_MODEL,
Collections.singleton(WAYPOINT_TYPE),//The type(s) of our model elements
DATA_MODEL.getTypes());//All types used in the data model
}
The constructor needs to pass in the TLcdDataModel
as well as the types of the domain objects.
In our case, the model contains only waypoints, so we pass in a collection with a single element WAYPOINT_TYPE
.
The last argument is a collection of all types used in the data model.
This includes the WAYPOINT_TYPE
, but also the TLcdCoreDataTypes.STRING_TYPE
used for the identifier property of the waypoints.
Finally, we need to implement the reading of the file, and the creation of the ILcdDataObject
instances.
private List<ILcdDataObject> createWayPoints(String aSourceName) throws IOException {
List<ILcdDataObject> result = new ArrayList<>();
//Use TLcdInputStreamFactory to create an InputStream for aSourceName
//This allows to read files on the class path, absolute file paths, files embedded in a jar, ...
TLcdInputStreamFactory isf = new TLcdInputStreamFactory();
try (InputStream is = isf.createInputStream(aSourceName);
BufferedReader reader = new BufferedReader(new InputStreamReader(is))) {
String line;
while ((line = reader.readLine()) != null) {
//Skip empty lines
if (line.trim().isEmpty()) {
continue;
}
// The first non-empty line contains the way point name.
String wayPointIdentifier = line;
//The next line is the coordinates line
String coordinatesLine = reader.readLine();
if (coordinatesLine == null) {
throw new IOException("Unexpected end of file encountered.");
}
String[] coordinates = coordinatesLine.split("\\s+");//split the line based on whitespace
if (coordinates.length != 3) {
throw new IOException("Expected <lon lat height>, but found " + coordinatesLine);
}
//Parse the coordinates and the elevation
try {
double lon = Double.parseDouble(coordinates[0]);
double lat = Double.parseDouble(coordinates[1]);
double height = Double.parseDouble(coordinates[2]);
if (lon < -180 || lon > 180 || lat < -90 || lat > 90) {
throw new NumberFormatException("The longitude and latitude must be in the interval " +
"[-180, 180] and [-90, 90], respectively");
}
if (height < 0) {
throw new NumberFormatException("The altitude of the way point must be positive");
}
//Create the waypoint and set the properties
ILcdDataObject wayPoint = WAYPOINT_TYPE.newInstance();
wayPoint.setValue("identifier", wayPointIdentifier);
wayPoint.setValue("location", new TLcdLonLatHeightPoint(lon, lat, height));
result.add(wayPoint);
} catch (NumberFormatException ex) {
throw new IOException(ex);
}
}
}
return result;
}
The model decoder can now be used to read waypoint files. The full code of the model decoder is available at the end of this tutorial.
Strictly speaking, there is no need to implement the |
Visualizing the data
The ILcdModel
created by the ILcdModelDecoder
is a regular model with vector data.
As such, it can be visualized and styled just like any other vector data model, SHP, GeoPackage, or GeoJSON models for instance.
See the Introduction to styling vector data tutorial for an introduction to visualizing and styling vector data on a Lightspeed map, or the Introduction to styling vector data tutorial for an introduction to visualizing and styling vector data on a GXY map.
Optional: Registering the model decoder as service
In your application, you typically want:
-
The same code path for opening waypoint files and files of any of the standard formats like SHP and GeoJSON. This makes your application code easier to write and maintain.
-
The same UI for the end-user of your application to load data. The end user should not know which data formats are custom and which are supported by the LuciadLightspeed API out-of-the-box.
Therefore, it is recommended to register your model decoder as a service. You register a decoder as a service by adding an annotation to the class:
@LcdService(service = ILcdModelDecoder.class)
public final class WayPointsModelDecoder implements ILcdModelDecoder {
Now, when you need to decode data, you can use a composite model decoder instance that collects all registered model decoders, and can decode formats like SHP or GeoJSON, as well as your custom waypoint format.
ILcdModelDecoder decoder =
new TLcdCompositeModelDecoder(TLcdServiceLoader.getInstance(ILcdModelDecoder.class));
Full code
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Vector;
import com.luciad.datamodel.ILcdDataObject;
import com.luciad.datamodel.TLcdCoreDataTypes;
import com.luciad.datamodel.TLcdDataModel;
import com.luciad.datamodel.TLcdDataModelBuilder;
import com.luciad.datamodel.TLcdDataType;
import com.luciad.datamodel.TLcdDataTypeBuilder;
import com.luciad.io.TLcdInputStreamFactory;
import com.luciad.model.ILcdDataModelDescriptor;
import com.luciad.model.ILcdModel;
import com.luciad.model.ILcdModelDecoder;
import com.luciad.model.ILcdModelReference;
import com.luciad.model.TLcd2DBoundsIndexedModel;
import com.luciad.model.TLcdDataModelDescriptor;
import com.luciad.reference.TLcdGeodeticReference;
import com.luciad.shape.TLcdShapeDataTypes;
import com.luciad.shape.shape3D.TLcdLonLatHeightPoint;
import com.luciad.util.TLcdHasGeometryAnnotation;
import com.luciad.util.service.LcdService;
@LcdService(service = ILcdModelDecoder.class)
public final class WayPointsModelDecoder implements ILcdModelDecoder {
public static final TLcdDataModel DATA_MODEL;
public static final TLcdDataType WAYPOINT_TYPE;
static {
//Create the TLcdDataModel for our waypoints
TLcdDataModelBuilder dataModelBuilder =
new TLcdDataModelBuilder("http://www.luciad.com/tutorial/waypoints");
//Create a TLcdDataType for the geometry
TLcdDataTypeBuilder pointTypeBuilder =
dataModelBuilder.typeBuilder("PointType")
.superType(TLcdShapeDataTypes.SHAPE_TYPE)
.primitive(true)
.instanceClass(TLcdLonLatHeightPoint.class);
TLcdDataTypeBuilder wayPointTypeBuilder =
dataModelBuilder.typeBuilder("WayPointType");
wayPointTypeBuilder.addProperty("identifier", TLcdCoreDataTypes.STRING_TYPE);
wayPointTypeBuilder.addProperty("location", pointTypeBuilder);
TLcdDataModel dataModel = dataModelBuilder.createDataModel();
//Specify which property holds the geometry
TLcdDataType wayPointType = dataModel.getDeclaredType("WayPointType");
wayPointType.addAnnotation(new TLcdHasGeometryAnnotation(wayPointType.getProperty("location")));
DATA_MODEL = dataModel;
WAYPOINT_TYPE = wayPointType;
}
@Override
public String getDisplayName() {
return "Way Points";
}
@Override
public boolean canDecodeSource(String aSourceName) {
return aSourceName != null && aSourceName.endsWith(".cwp");
}
@Override
public ILcdModel decode(String aSourceName) throws IOException {
//Sanity check to see whether we can decode the data
if (!canDecodeSource(aSourceName)) {
throw new IOException("Cannot decode " + aSourceName);
}
//Create an empty model
TLcd2DBoundsIndexedModel model = createEmptyModel();
//With the correct reference
//The reference of the model expresses how the coordinates of the
//shapes should be interpreted
model.setModelReference(createModelReference());
//And a ILcdDataModelDescriptor to expose the data model
ILcdDataModelDescriptor modelDescriptor = createModelDescriptor(aSourceName);
model.setModelDescriptor(modelDescriptor);
//Read the file and create the domain objects (=ILcdDataObjects of the WayPointType)
List<ILcdDataObject> wayPoints = createWayPoints(aSourceName);
//Add them to the model
model.addElements(new Vector<>(wayPoints), ILcdModel.NO_EVENT);
//Return the created model
return model;
}
private TLcd2DBoundsIndexedModel createEmptyModel() {
return new TLcd2DBoundsIndexedModel();
}
private ILcdModelReference createModelReference() {
return new TLcdGeodeticReference();//WGS-84 reference
}
private ILcdDataModelDescriptor createModelDescriptor(String aSourceName) {
return new TLcdDataModelDescriptor(aSourceName,
"CWP", //Typename: identifier of the format
"Way Points",//Display name for the model
DATA_MODEL,
Collections.singleton(WAYPOINT_TYPE),//The type(s) of our model elements
DATA_MODEL.getTypes());//All types used in the data model
}
private List<ILcdDataObject> createWayPoints(String aSourceName) throws IOException {
List<ILcdDataObject> result = new ArrayList<>();
//Use TLcdInputStreamFactory to create an InputStream for aSourceName
//This allows to read files on the class path, absolute file paths, files embedded in a jar, ...
TLcdInputStreamFactory isf = new TLcdInputStreamFactory();
try (InputStream is = isf.createInputStream(aSourceName);
BufferedReader reader = new BufferedReader(new InputStreamReader(is))) {
String line;
while ((line = reader.readLine()) != null) {
//Skip empty lines
if (line.trim().isEmpty()) {
continue;
}
// The first non-empty line contains the way point name.
String wayPointIdentifier = line;
//The next line is the coordinates line
String coordinatesLine = reader.readLine();
if (coordinatesLine == null) {
throw new IOException("Unexpected end of file encountered.");
}
String[] coordinates = coordinatesLine.split("\\s+");//split the line based on whitespace
if (coordinates.length != 3) {
throw new IOException("Expected <lon lat height>, but found " + coordinatesLine);
}
//Parse the coordinates and the elevation
try {
double lon = Double.parseDouble(coordinates[0]);
double lat = Double.parseDouble(coordinates[1]);
double height = Double.parseDouble(coordinates[2]);
if (lon < -180 || lon > 180 || lat < -90 || lat > 90) {
throw new NumberFormatException("The longitude and latitude must be in the interval " +
"[-180, 180] and [-90, 90], respectively");
}
if (height < 0) {
throw new NumberFormatException("The altitude of the way point must be positive");
}
//Create the waypoint and set the properties
ILcdDataObject wayPoint = WAYPOINT_TYPE.newInstance();
wayPoint.setValue("identifier", wayPointIdentifier);
wayPoint.setValue("location", new TLcdLonLatHeightPoint(lon, lat, height));
result.add(wayPoint);
} catch (NumberFormatException ex) {
throw new IOException(ex);
}
}
}
return result;
}
}