Your application needs to support text NOTAMs, so you want to decode and visualize text NOTAMs on the map.

Why do it?

A NOTAM or Notice to Airmen represents a temporary update to the aeronautical environment, relevant for aviation stakeholders, such as pilots, air traffic controllers, airlines, and so on. Common examples include:

  • Temporary airspace restrictions, for instance because of an important political event or sport manifestation, a helicopter training, or a UAV operation

  • Temporary obstacles, such as a crane being deployed somewhere

  • Navigation aid updates, about a broken radar, for instance

  • Airport updates, such as a taxiway that is closed for maintenance

Historically, those NOTAMs are represented as structured text messages. One example format is the one from Belgocontrol:

B0640/16
From:18 JUN 16 11:00 Till:26 JUN 16 20:00
Schedule:JUN 18 19 1100-SS, 23 26 1000-SS
Text:MEEUWEN-GRUITRODE PSN 510707N0053225E RIFLE SHOOTING CONTEST RADIUS 0.5NM
Lower limit:GND Upper limit:1300FT AGL

As the example shows, a NOTAM typically contains a time period, a description of the update, a location, and some information about the horizontal and vertical extent of the update. Although not all information is always present, we can choose a best-effort approach to decode and visualize such text NOTAMs on the map.

More about text NOTAMs and Digital NOTAMs

Text NOTAMs were originally designed to be readable by humans. This can make lots of NOTAMs fairly cumbersome. In addition, it is not that easy for a human being to read coordinates. Automatically interpreting and visualizing relevant NOTAMS on a map is much easier. Because of the partial free text, however, it is never guaranteed that all NOTAMs are successfully processed and visualized. Therefore, the aviation world is moving towards a new NOTAM structure, called Digital NOTAM. It is entirely based on AIXM 5.1 (XML), and readily supported by Luciad portfolio software.

How to do it?

This use case is illustrated by means of a LuciadLightspeed sample that shows how to decode and visualize a few text NOTAMs on the map. This sample consists of the following classes:

  • NotamDataTypes.java: definition of the structure of a NOTAM domain object, consisting of a set of properties and geometry

  • Notam.java: representation of a NOTAM domain object, adhering to the structure defined in NotamDataTypes

  • NotamDecoder.java: decoder to parse text NOTAMs into objects of type Notam

  • NotamSample.java: Lightspeed view sample showing the use of the classes above to decode and visualize a few publicly available text NOTAMs on the map.

The code can be found at the end of this article.

The following screenshot shows an overview of the decoded sample NOTAMs on the map, on top of a Bing Maps background layer. The NOTAMs are displayed by means of a cylinder and a label indicating its identifier.

notam overview 2D

The following screenshot shows a selected NOTAM in 3D. The label for a selected NOTAM show its title. Users can access all available NOTAM properties on a properties panel.

notam 3D

Code

NotamDataTypes.java

package samples.lightspeed.notam.model;

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;

/**
 * Represents the structure of a Notam object.
 */
public class NotamDataTypes {

  // The data model for the Notams, fully describing the structure of the data.
  private static final TLcdDataModel NOTAM_DATA_MODEL;

  // The data model contains a single data type - the Notam data type.
  public static final TLcdDataType NOTAM_DATA_TYPE;
  public static final String NOTAM_DATA_TYPE_AS_STRING = "NotamType"; //Starts with capital, same as Java class

  // The list of properties for a Notam object.
  public static final String ID = "name"; //Starts with lower case, same as Java property
  public static final String FROM = "from";
  public static final String TILL = "till";
  public static final String SCHEDULE = "schedule";
  public static final String TEXT = "text";
  public static final String TITLE = "title";
  public static final String LOWER_LIMIT = "lowerLimit";
  public static final String UPPER_LIMIT = "upperLimit";

  static {
    // Assign the constants
    NOTAM_DATA_MODEL = createDataModel();
    NOTAM_DATA_TYPE = NOTAM_DATA_MODEL.getDeclaredType(NOTAM_DATA_TYPE_AS_STRING);
  }

  private static TLcdDataModel createDataModel() {
    // Create the builder for the data model.
    // Use some unique name space, to prevent name clashes.  This isn't really needed
    // for the sample but might be useful when exposing it externally.
    TLcdDataModelBuilder builder = new TLcdDataModelBuilder(
        "http://www.mydomain.com/datamodel/NotamModel");

    // Define the types and their properties (only one type and one property here)
    TLcdDataTypeBuilder notamBuilder = builder.typeBuilder(NOTAM_DATA_TYPE_AS_STRING);
    notamBuilder.addProperty(ID, TLcdCoreDataTypes.STRING_TYPE);
    notamBuilder.addProperty(FROM, TLcdCoreDataTypes.DATE_TYPE);
    notamBuilder.addProperty(TILL, TLcdCoreDataTypes.DATE_TYPE);
    notamBuilder.addProperty(SCHEDULE, TLcdCoreDataTypes.STRING_TYPE);
    notamBuilder.addProperty(TEXT, TLcdCoreDataTypes.STRING_TYPE);
    notamBuilder.addProperty(TITLE, TLcdCoreDataTypes.STRING_TYPE);
    notamBuilder.addProperty(LOWER_LIMIT, TLcdCoreDataTypes.STRING_TYPE);
    notamBuilder.addProperty(UPPER_LIMIT, TLcdCoreDataTypes.STRING_TYPE);

    // Define the instance class, so that TLcdDataType.newInstance creates a Notam
    notamBuilder.instanceClass(Notam.class);

    // Finalize the creation
    return builder.createDataModel();
  }

  public static TLcdDataModel getDataModel() {
    return NOTAM_DATA_MODEL;
  }

  private NotamDataTypes() {
  }
}

Notam.java

package samples.lightspeed.notam.model;

import com.luciad.datamodel.ILcdDataObject;
import com.luciad.datamodel.TLcdDataObject;
import com.luciad.datamodel.TLcdDataProperty;
import com.luciad.datamodel.TLcdDataType;
import com.luciad.shape.TLcdShapeList;

/***
 * Represents a Notam object.
 * The structure of the object, i.e. its available properties and their types, is defined by {@link NotamDataTypes}.
 * The geometry is defined as a list of shapes, by means of an extension of {@link TLcdShapeList}.
 * Although any type of shape can be added, a commonly used shape is a cilinder, as it gives the users an indication
 * of the location, horizontal and vertical extent.
 */
public final class Notam extends TLcdShapeList implements ILcdDataObject {

  private final TLcdDataObject fDataObject = new TLcdDataObject(NotamDataTypes.NOTAM_DATA_TYPE);

  public Notam() {
  }

  //The methods below simply delegate to fDataObject
  @Override
  public TLcdDataType getDataType() {
    return fDataObject.getDataType();
  }

  @Override
  public Object getValue(TLcdDataProperty aProperty) {
    return fDataObject.getValue(aProperty);
  }

  @Override
  public Object getValue(String aPropertyName) {
    return fDataObject.getValue(aPropertyName);
  }

  @Override
  public void setValue(TLcdDataProperty aProperty, Object aValue) {
    fDataObject.setValue(aProperty, aValue);
  }

  @Override
  public void setValue(String aPropertyName, Object aValue) {
    fDataObject.setValue(aPropertyName, aValue);
  }

  @Override
  public boolean hasValue(TLcdDataProperty aProperty) {
    return fDataObject.hasValue(aProperty);
  }

  @Override
  public boolean hasValue(String aPropertyName) {
    return fDataObject.hasValue(aPropertyName);
  }

  @Override
  public String toString() {
    return (String) getValue(NotamDataTypes.TITLE);
  }

  @Override
  public boolean equals(Object aObject) {
    return aObject == this;
  }

  @Override
  public synchronized int hashCode() {
    return System.identityHashCode(this);
  }
}

NotamDecoder.java

package samples.lightspeed.notam.model;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
import java.text.Format;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.StringTokenizer;

import com.luciad.geodesy.TLcdEllipsoid;
import com.luciad.shape.shape2D.ILcd2DEditablePoint;
import com.luciad.shape.shape2D.TLcdLonLatCircle;
import com.luciad.shape.shape3D.TLcdExtrudedShape;
import com.luciad.text.TLcdAltitudeFormat;
import com.luciad.text.TLcdDistanceFormat;
import com.luciad.text.TLcdLonLatPointFormat;

/**
 * Decodes text notams into {@link Notam} objects.
 * For each text notam, an attempt is done to represent its geometry as a cilinder with a location,
 * horizontal and vertical extent.
 */
final class NotamDecoder {

  // Default radius in case no horizontal extent information can be found.
  private static final int DEFAULT_RADIUS = 500; //meters
  // Default lower limit in case no vertical extent information can be found.
  private static final int DEFAULT_LOWER_LIMIT = 0; //meters
  // Default upper limit in case no vertical extent information can be found.
  private static final int DEFAULT_UPPER_LIMIT = 500; //meters

  private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("dd MMM yy HH:mm", Locale.US);
  private static final TLcdAltitudeFormat ALTITUDE_FORMAT = new TLcdAltitudeFormat();
  private static final TLcdDistanceFormat DISTANCE_FORMAT = new TLcdDistanceFormat();
  private static final TLcdLonLatPointFormat LONLAT_POINT_FORMAT = new TLcdLonLatPointFormat();

  static Notam decodeNotam(String aNotam) throws IOException {
    // 1. Read the Notam String into a String array, with 1 String per line.
    BufferedReader reader = new BufferedReader(new StringReader(aNotam));
    List<String> lines = new ArrayList<>();
    for (String line = reader.readLine(); line != null; line = reader.readLine()) {
      lines.add(line);
    }
    reader.close();

    // 2. Create a new Notam instance.
    Notam notam = new Notam();

    // 3. Parse each line.
    String id, schedule = null, lowerLimitAsString = null, upperLimitAsString = null, text = null, title = null;
    Date from = null, till = null;
    Number lowerLimit = null, upperLimit = null, radius = null;

    // An id is mandatory.
    id = lines.get(0);

    // All the rest can be optional.
    for (int i = 1; i < lines.size(); i++) {
      String line = lines.get(i);
      if (line.contains("From")) {
        try {
          from = DATE_FORMAT.parse(line.substring(5, line.indexOf("Till:")).trim());
        } catch (ParseException e) {
          throw new IOException("Unexpected NOTAM from date: " + line, e);
        }
      }
      if (line.contains("Till")) {
        try {
          String tillAsString = line.substring(line.indexOf("Till:") + 5, line.length()).trim();
          till = "PERM".equals(tillAsString) ? null : DATE_FORMAT.parse(tillAsString);
        } catch (ParseException e) {
          throw new IOException("Unexpected NOTAM till date: " + line, e);
        }
      }
      if (line.contains("Schedule")) {
        schedule = line.substring(9, line.length());
      }
      if (line.contains("Text")) {
        text = line.substring(5, line.length());
        title = parseTitle(text);
      }
      if (line.contains("Lower limit")) {
        lowerLimitAsString = line.substring(12, line.indexOf("Upper limit:")).trim();
        List<Number> lowerLimits = (List<Number>) parse(lowerLimitAsString, ALTITUDE_FORMAT, false);
        if (lowerLimits.size() > 0) {
          lowerLimit = lowerLimits.get(0);
        }
      }
      if (line.contains("Upper limit")) {
        upperLimitAsString = line.substring(line.indexOf("Upper limit:") + 12, line.length()).trim();
        List<Number> upperLimits = (List<Number>) parse(upperLimitAsString, ALTITUDE_FORMAT, false);
        if (upperLimits.size() > 0) {
          upperLimit = upperLimits.get(0);
        }
      }
    }

    // 4. Store the Notam's geometry, if available.
    List<ILcd2DEditablePoint> locations = (List<ILcd2DEditablePoint>) parse(text, LONLAT_POINT_FORMAT, true);
    if (text.contains("RADIUS")) {
      List<Double> radiuses = (List<Double>) parse(text, DISTANCE_FORMAT, false);
      if (radiuses.size() > 0) {
        radius = radiuses.get(0);
      }
    }
    for (ILcd2DEditablePoint location : locations) {
      TLcdExtrudedShape notamShape = new TLcdExtrudedShape();
      TLcdLonLatCircle circle = new TLcdLonLatCircle(location, radius != null ? radius.doubleValue() : DEFAULT_RADIUS, new TLcdEllipsoid());
      notamShape.setBaseShape(circle);
      if (lowerLimit != null) {
        notamShape.setMinimumZ(lowerLimit.doubleValue());
      } else {
        notamShape.setMinimumZ(DEFAULT_LOWER_LIMIT);
      }
      if (upperLimit != null) {
        notamShape.setMaximumZ(upperLimit.doubleValue());
      } else {
        notamShape.setMaximumZ(DEFAULT_UPPER_LIMIT);
      }
      notam.addShape(notamShape);
    }

    // 5. Store the Notam's properties.
    notam.setValue(NotamDataTypes.ID, id);
    notam.setValue(NotamDataTypes.FROM, from);
    notam.setValue(NotamDataTypes.TILL, till);
    notam.setValue(NotamDataTypes.SCHEDULE, schedule);
    notam.setValue(NotamDataTypes.TEXT, text);
    notam.setValue(NotamDataTypes.TITLE, title);
    notam.setValue(NotamDataTypes.LOWER_LIMIT, lowerLimitAsString);
    notam.setValue(NotamDataTypes.UPPER_LIMIT, upperLimitAsString);

    return notam;
  }

  private static String parseTitle(String aString) {
    int startIndex = -1, endIndex = -1;
    StringTokenizer tokenizer = new StringTokenizer(aString);
    while (tokenizer.hasMoreTokens()) {
      String element = tokenizer.nextToken();
      try {
        if (element.endsWith(",") || element.endsWith(".")) {
          element = element.substring(0, element.length() - 1);
        }
        element = element.replace(".", ",");
        if ((element.endsWith("W") || element.endsWith("E")) && LONLAT_POINT_FORMAT.parse(element) != null) {
          startIndex = aString.indexOf(element) + element.length();
        } else if (endIndex == -1 && "RADIUS".equals(element)) {
          endIndex = aString.indexOf(element);
        }

      } catch (Exception e) {
        // Ignore - we parse the tokens through trial and error.
      }
    }
    startIndex = startIndex != -1 ? startIndex : 0;
    endIndex = endIndex != -1 ? endIndex : aString.length();

    return aString.substring(startIndex, endIndex);
  }

  /**
   * Parses the given String using the given Format instance.
   * If coordinates are parsed, a List containing ILcd2DEditablePoint instances is returned,
   * one for each parsed coordinate.
   * If distances are parsed, a List containing Double instances is returned.
   * If altitudes are parsed, a List containing Number instances is returned.
   * If nothing can be parsed, an empty list is returned.
   *
   * @param aString            a String to be parsed
   * @param aFormat            a Format to be used for parsing
   * @param aLookForCoordinate a boolean indicating whether coordinates are being parsed. Setting this to false will
   *                           prevent coordinates to be interpreted as other measures.
   * @return the parsed String
   */
  private static List parse(String aString, Format aFormat, boolean aLookForCoordinate) {
    List values = new ArrayList();
    StringTokenizer tokenizer = new StringTokenizer(aString);
    while (tokenizer.hasMoreTokens()) {
      String element = tokenizer.nextToken();
      if (element.endsWith(",")) {
        element = element.substring(0, element.length() - 1);
      }
      element = element.replace(".", ",");
      if ((aLookForCoordinate && (element.endsWith("W") || element.endsWith("E"))) ||
          (!aLookForCoordinate && !element.endsWith("W") && !element.endsWith("E") &&
           ((Character.isDigit(element.charAt(0)) && !Character.isDigit(element.charAt(element.length() - 1))) || "GND".equals(element) || "SFC".equals(element)))) {
        try {
          if ("GND".equals(element) || "SFC".equals(element)) {
            values.add(0);
          } else {
            Object value = aFormat.parseObject(element);
            if (value != null) {
              values.add(value);
            }
          }
        } catch (Exception e) {
          // Ignore - we parse the tokens through trial and error.
        }
      }

    }
    return values;
  }
}

NotamSample.java

package samples.lightspeed.notam.model;

import static samples.lightspeed.common.FitUtil.fitOnLayers;

import java.io.IOException;
import java.util.Collections;

import com.luciad.model.ILcdModel;
import com.luciad.model.TLcd2DBoundsIndexedModel;
import com.luciad.model.TLcdDataModelDescriptor;
import com.luciad.reference.TLcdGeodeticReference;
import com.luciad.view.lightspeed.layer.ILspLayer;
import com.luciad.view.lightspeed.layer.TLspPaintState;
import com.luciad.view.lightspeed.layer.shape.TLspShapeLayerBuilder;
import com.luciad.view.lightspeed.painter.label.TLspLabelPainter;
import com.luciad.view.lightspeed.painter.label.style.TLspDataObjectLabelTextProviderStyle;
import com.luciad.view.lightspeed.style.TLspFillStyle;
import com.luciad.view.lightspeed.style.TLspLineStyle;
import com.luciad.view.lightspeed.style.TLspTextStyle;

import samples.lightspeed.common.LightspeedSample;

/**
 * Lightspeed sample illustrating the decoding and visualization of text notams.
 */
public class NotamSample extends LightspeedSample {

  // A few publicly example text notams.
  private static final String[] EXAMPLE_NOTAMS = new String[]{
      "B1223/16\n" +
      "From:21 MAR 16 05:42 Till:25 MAR 16 18:04\n" +
      "Schedule:DAILY SR-SS\n" +
      "Text:ASSENEDE PSN 511401N0034651E CIVIL UNMANNED AERIAL VEHICLE (UAV) OPS RADIUS 0,5NM. EHAA FIR NOT INCLUDED\n" +
      "Lower limit:GND Upper limit:500FT AGL",
      "B0798/15 \n" +
      "From:30 MAR 15 09:13 Till:PERM\n" +
      "Text:FLOBECQ PSN 504549N0034439E OBST LIGHTS U/S ON MAST 103M AGL/242M AMSL\n",
      "B0852/15 \n" +
      "From:02 APR 15 18:36 Till:31 MAR 16 15:00 EST\n" +
      "Text:ZELZATE PSN 511107N0034738E NO OBST LIGHTS AVBL ON CHIMNEY 150M AGL/160M AMSL\n",
      "B3018/15 \n" +
      "From:04 JAN 16 05:00 Till:PERM\n" +
      "Text:EVERGEM PSN 511103N0034607E, PSN 511102N0034638E, PSN 511113N0034632E AND PSN 511116N0034618E ERECTION OF 4 WINDTURBINES HGT 591FT AGL. MARKING OR OBSTACLE LIGHTING MAY NOT BE PRESENT\n",
      "B0464/16 \n" +
      "From:04 FEB 16 09:00 Till:31 MAR 16 00:00 \n" +
      "Text:LOVERVAL NEW HOSPITAL HELIPORT PSN 502233N 0042911E OPRATOR GRAND HOPITAL DE CHARLEROI TEL 3271106000 OPR HR HJ DIMENSIONS DIAMETER 21M ELEVATION 636 FT ARRIVAL ROUTE 250 DEG MAG PPR. ONLY HELICOPTERS OPERATING IN PERFORMANCE CLASS I PERFORMING HEMS AND AIR AMBULANCE FLIGHTS ARE ALLOWED OBSTACLE TREE IN ARRIVAL ROUTE 250 DEG ENE 75M FROM FATO\n",
      "B0528/16 \n" +
      "From:02 APR 16 14:00 Till:02 APR 16 15:00 \n" +
      "Text:PSN 493642N0060744E ASCENT OF 500 TOY BALLOONS WITH MAX DIAMETER OF 30CM\n" +
      "Lower limit:GND Upper limit:4500FT AGL\n",
      "B0546/16 \n" +
      "From:10 FEB 16 09:30 Till:PERM\n" +
      "Text:AIP AMDT CORRECTION WITHIN AIP ENR 5.1-8 TABLE EBR19 UNDER VERTICAL LIMITS READ 2500FT AGL INSTEAD OF 1000FT AGL AMEND ENR 06 ACCORDINGLY\n" +
      "Lower limit:GND Upper limit:2500FT AGL\n",
      "B0640/16 \n" +
      "From:18 JUN 16 11:00 Till:26 JUN 16 20:00 \n" +
      "Schedule:JUN 18 19 1100-SS, 23 26 1000-SS\n" +
      "Text:MEEUWEN-GRUITRODE PSN 510707N0053225E RIFLE SHOOTING CONTEST RADIUS 0.5NM\n" +
      "Lower limit:GND Upper limit:1300FT AGL\n",
      "B0728/16 \n" +
      "From:20 FEB 16 14:29 Till:25 MAR 16 10:00 EST\n" +
      "Text:VILLERS-LE-BOUILLET PSN 503452N0051410E OBST LGT ON MAST NR 1, 2, 3, 4, 5, 6 AND 7 U/S 125M AGL/319M AMSL\n",
      "B0738/16 \n" +
      "From:03 MAR 16 00:01 Till:PERM\n" +
      "Text:EBR49 - ZUTENDAAL, CORRECT AIRAC AIP AMDT 002/2016 SECTION ENR 5.1. READ EXCEPT STATE AIRCRAFT IN REAL-LIFE OPERATIONS AND GLIDER-AIRCRAFT FROM AND TO EBSL INSTEAD OF EXCEPT STATE AIRCRAFT IN REAL-LIFE OPERATIONS\n",
      "B0754/16 \n" +
      "From:04 APR 16 07:00 Till:09 JUN 16 07:00 \n" +
      "Text:OUTSIDE NML OPN HR OF EBFS AD, A COMPULSORY LISTENING WATCH WITH BRUSSELS FIC FREQ 126.900MHZ HAS TO BE MAINTAINED FOR TRAFFIC CROSSING EBFS TMA,CTR AND EBR06B. TRAFFIC UNABLE TO COMPLY HAS TO REMAIN CLEAR OF EBFS TMA,CTR AND EBR06B.\n" +
      "Lower limit:GND Upper limit:FL095\n",
      "B0812/16 \n" +
      "From:25 FEB 16 13:15 Till:04 APR 16 08:00 \n" +
      "Text:OUTSIDE NML OPENING HOURS OF EBBL AD, A COMPULSORY LISTENING WATCH WITH BRUSSELS FIC FREQ 126.900MHZ HAS TO BE MAINTAINED FOR TRAFFIC CROSSING EBBL TMA, CTR AND EBR07B. TRAFFIC UNABLE TO COMPLY HAS TO REMAIN CLEAR OF EBBL TMA, CTR AND EBR07B\n" +
      "Lower limit:GND Upper limit:FL075\n",
      "B0844/16 \n" +
      "From:26 FEB 16 13:37 Till:25 MAR 16 09:00 EST\n" +
      "Text:PERWEZ PSN 503646N0044740E OBST LIGHTS WIND TURBINES 2 AND 3 U/S 123M AGL\n",
      "B0866/16 \n" +
      "From:29 FEB 16 08:28 Till:28 MAR 16 08:28 EST\n" +
      "Text:CHECKLIST YEAR=2015 0144 0145 0146 0147 0148 0151 0152 0798 0852 2168 2739 2902 2958 2979 3018 3048 3110 3111 3175 3226 3322 3380 3444 3446 3450 3451 3478 3511 3513 3516 3521 YEAR=2016 0017 0410 0418 0440 0464 0528 0546 0604 0610 0611 0626 0637 0638 0640 0728 0738 0753 0754 0757 0758 0783 0797 0798 0801 0802 0808 0809 0810 0812 0814 0826 0832 0836 0837 0838 0839 0841 0842 0844 0846 0848 0849 0850 0851 0852 0853 0854 0855 0856 0857 0858 0860 0861 0865 LATEST PUBLICATIONS AIP AIRAC AMDT 003/2016 EFFECTIVE DATE 31 MAR 16 AIP AMDT 003/2016 EFFECTIVE DATE 03 MAR 16 AIP SUP 002/2016 EFFECTIVE DATE 07 JAN 16 AIC 002/2016 EFFECTIVE DATE 04 FEB 16\n",
      "B0871/16 \n" +
      "From:29 FEB 16 10:51 Till:29 MAR 16 10:51 EST\n" +
      "Text:WAVRE PSN 504452N0043439E OBST LGT TV MAST U/S 249M AGL/334M AMSL\n",
      "B0924/16 \n" +
      "From:02 MAR 16 10:39 Till:02 APR 16 10:39 \n" +
      "Text:FROIDMONT PSN 503526N0031857E TV MAST OBST LGT U/S 165M AGL/245M AMSL\n",
      "B1032/16 \n" +
      "From:21 MAR 16 07:00 Till:25 MAR 16 15:00 \n" +
      "Schedule:MAR 21-23 25 0700-1500\n" +
      "Text:EBD20-BRASSCHAAT ACT\n" +
      "Lower limit:GND Upper limit:FL070\n",
      "B1038/16 \n" +
      "From:03 APR 16 09:00 Till:03 APR 16 17:00 \n" +
      "Text:DUE TO CYCLING EVENT RONDE VAN VLAANDEREN ACFT ARE PROHIBITED TO LAND OR TAKE-OFF IN AREA CENTERED ON PSN 504857N00033355E RADIUS 05NM EXC TO AND FROM EBNK AND EXC FOR SAR, MEDICAL, HUMANITARIAN, NATIONAL MIL, NATO AND FLIGHTS WITH SPECIFIC APPROVAL FROM THE CIVIL AVIATION AUTHORITY.\n" +
      "Lower limit:GND Upper limit:2500FT AMSL\n",
      "B1041/16 \n" +
      "From:24 MAR 16 07:00 Till:24 MAR 16 15:00 \n" +
      "Text:EBR20-BRASSCHAAT ACT\n" +
      "Lower limit:GND Upper limit:FL140\n",
      "B1060/16 \n" +
      "From:26 MAR 16 09:00 Till:26 MAR 16 18:00 \n" +
      "Text:SINT-LENAARTS PSN 512232N0044146E RADIO CONTROLLED MODEL ACFT EVENT. RADIUS 400M\n" +
      "Lower limit:GND Upper limit:700FT AGL\n",
      "B1093/16 \n" +
      "From:30 APR 16 06:00 Till:30 APR 16 19:03 \n" +
      "Text:BOSSIERE PSN 503146N0044032E RADIO CONTROLLED MODEL ACFT EVENT. RADIUS 400M\n" +
      "Lower limit:GND Upper limit:700FT AGL\n",
      "B1098/16 \n" +
      "From:11 MAR 16 12:00 Till:PERM\n" +
      "Text:EBR11 - TIHANGE. ENTRY PROHIBITED, UNLESS INSTRUCTED BY ATC. POLICE FLT EXEMPTED\n",
      "B1101/16 \n" +
      "From:11 MAR 16 13:00 Till:PERM\n" +
      "Text:EBR16 - MOL. ENTRY PROHIBITED, UNLESS INSTRUCTED BY ATC. POLICE FLT EXEMPTED\n",
      "B1120/16 \n" +
      "From:14 MAR 16 14:12 Till:04 APR 16 09:00 EST\n" +
      "Text:GEMBLOUX SOMBREFFE PSN 503239N0043812E WIND TURBINE NR 2 OBST LGT U/S 123M AGL/294M AMSL\n",
      "B1132/16 \n" +
      "From:15 MAR 16 14:02 Till:16 MAY 16 09:00 EST\n" +
      "Text:RIVIERE PSN 502118N0045133E TOP OBST LGT U/S ON MAST 164M AGL/421M AMSL\n",
      "B1134/16 \n" +
      "From:03 APR 16 08:00 Till:03 APR 16 17:00 \n" +
      "Text:DURING THE BICYCLE RACE RONDE VAN VLAANDEREN TEMPORARY SEGREGATED AIRSPACE RESERVED FOR OOHSK, OOHSM, OOTTD, SAR, STATE, MEDICAL, HUMANITARIAN, NATIONAL MIL AND NATO FLIGHTS WITHIN AN AREA EXTENDED VERTICALLY FROM GROUND UP TO 1300FT AGL, LATERALLY 0,5NM ON BOTH SIDES OF THE ROUTE FOLLOWED AND LONGITUDINALLY FROM 0,5NM IN FRONT OF THE LEADING CAR TO 0,5NM BEHIND THE REAR CAR, WITH EXCEPTION OF CONTROLLED AIRSPACE. BOTH LEADING AND REAR CARS WILL BE MADE CONSPICIOUS TO AIRCRAFT BY AN ORANGE SQUARE PANEL OF 1 SQUARE METER ON THEIR ROOF.\n" +
      "Lower limit:GND Upper limit:1300FT AGL\n",
      "B1152/16 \n" +
      "From:21 MAR 16 06:30 Till:25 MAR 16 13:30 \n" +
      "Schedule:MAR 21 24 25 0630-1330\n" +
      "Text:EBR17A-LOMBARDSIJDE SECTOR ALPHA ACT\n" +
      "Lower limit:SFC Upper limit:2500FT AMSL\n",
      "B1174/16 \n" +
      "From:31 MAR 16 00:01 Till:14 APR 16 23:59 \n" +
      "Text:TRIGGER NOTAM - PERM AIRAC 003/2016. EBD37 LATERAL LIMITS UPDATED.\n" +
      "Lower limit:GND Upper limit:2500FT AMSL\n",
      "B1206/16 \n" +
      "From:21 MAR 16 16:30 Till:22 MAR 16 23:00 \n" +
      "Schedule:MAR 21 22 1630-2300\n" +
      "Text:SEMMERZAKE ATCC OPR, PPR ONLY\n",
      "B1219/16 \n" +
      "From:21 MAR 16 14:00 Till:24 MAR 16 14:50 \n" +
      "Schedule:MAR 21 1400-1450, 22 1100-1150, 23 1400-1450, 24 0900-0950 1300-1350 1400-1450\n" +
      "Text:EBD26-ARDENNES 05 ACT\n" +
      "Lower limit:1000FT AGL Upper limit:4500FT AMSL\n",
      "B1220/16 \n" +
      "From:21 MAR 16 14:00 Till:24 MAR 16 14:50 \n" +
      "Schedule:MAR 21 1400-1450 1900-1950, 22 1100-1150 2100-2150, 23 1400-1450, 24 0900-0950 1300-1350 1400-1450\n" +
      "Text:TSA26B-ARDENNES 04 ACT\n" +
      "Lower limit:4500FT AMSL Upper limit:FL095\n",
      "B1222/16 \n" +
      "From:22 MAR 16 07:00 Till:25 MAR 16 10:30 \n" +
      "Schedule:MAR 22-24 0700-1600, 25 0700-1030\n" +
      "Text:CONDITIONAL ROUTE SEGMENT CDR1 CLSD : L607 LNO-SUXIM\n",
      "B1223/16 \n" +
      "From:21 MAR 16 05:42 Till:25 MAR 16 18:04 \n" +
      "Schedule:DAILY SR-SS\n" +
      "Text:ASSENEDE PSN 511401N0034651E CIVIL UNMANNED AERIAL VEHICLE (UAV) OPS RADIUS 0,5NM. EHAA FIR NOT INCLUDED\n" +
      "Lower limit:GND Upper limit:500FT AGL\n",
      "B1237/16 \n" +
      "From:22 MAR 16 08:00 Till:22 MAR 16 23:00 \n" +
      "Text:HTA01-ARDENNES 01 AREA ACT\n" +
      "Lower limit:GND Upper limit:250FT AGL"};

  @Override
  protected void addData() throws IOException {
    super.addData();

    // 1. Create Notam model with sample notams.
    TLcd2DBoundsIndexedModel model = new TLcd2DBoundsIndexedModel();
    model.setModelReference(new TLcdGeodeticReference());
    model.setModelDescriptor(new TLcdDataModelDescriptor(NotamDataTypes.getDataModel(),
                                                         Collections.singleton(NotamDataTypes.NOTAM_DATA_TYPE),
                                                         null));

    try {
      for (String notamString : EXAMPLE_NOTAMS) {
        Notam notam = NotamDecoder.decodeNotam(notamString);
        if (notam.getShapeCount() > 0) {
          model.addElement(notam, ILcdModel.FIRE_LATER);
        }
      }
      model.fireCollectedModelChanges();
    } catch (IOException e) {
      e.printStackTrace();
    }

    // 2. Create Notam layer.
    TLspLabelPainter labelPainter = new TLspLabelPainter();
    labelPainter.setOverlayLabels(true);
    ILspLayer layer = TLspShapeLayerBuilder.newBuilder().model(model).
        bodyStyles(TLspPaintState.REGULAR, TLspLineStyle.newBuilder().build(), TLspFillStyle.newBuilder().build()).
                                               bodyStyles(TLspPaintState.SELECTED, TLspLineStyle.newBuilder().build(), TLspFillStyle.newBuilder().build()).
                                               labelStyles(TLspPaintState.REGULAR, TLspDataObjectLabelTextProviderStyle.newBuilder().expressions(NotamDataTypes.ID).build(), TLspTextStyle.newBuilder().alignment(TLspTextStyle.Alignment.RIGHT).build()).
                                               labelStyles(TLspPaintState.SELECTED, TLspDataObjectLabelTextProviderStyle.newBuilder().expressions(NotamDataTypes.TITLE).build(), TLspTextStyle.newBuilder().alignment(TLspTextStyle.Alignment.RIGHT).build()).
                                               labelPainter(labelPainter).
                                               label("NOTAM").build();

    // 3. Add Notam layer to the view.
    getView().addLayer(layer);

    // 4. Fit on the Notam layer.
    fitOnLayers(this, layer);
  }

  public static void main(final String[] aArgs) {
    startSample(NotamSample.class, "Notam sample");
  }
}