Skip to content

Using The Tinfour Shapefile API

gwlucastrig edited this page Feb 27, 2026 · 12 revisions

Under Construction: More content coming soon

Introduction

Many interesting applications for the Delaunay triangulation involve geophysical data. And although Tinfour is not a Geographic Information System (GIS), it does support logic for reading and writing industry standard shapefile formatted data products. Utilities for accessing shapefiles are included as part of the TinfourGIS module. In this wiki article, we will provide examples of how to use the Tinfour shapefile access tools. The primary shapefile classes, which are implemented in the org.tinfour.gis.shapefile package, are given below:

  • ShapefileReader
  • ShapefileWriter

Interested readers can also look at the code for the following utilities which use the ShapefileReader class. They are implemented as part of the org.tinfour.gis.utils package:

  • VertexReaderShapefile
  • ConstraintReaderShapefile

Both of these utility classes are used in the Tinfour Viewer application and in other Tinfour demostration code.

For this wiki article, we created a simple application that reads a pair of shapefiles and creates Delaunay triangulation derived from their content. The edges from the triangulation are then written to an output shapefile. The application assumes that the first input shapefile uses a polygon geometry to define the overall area of a lake or pond. Typically, such polygons are digitized to follow the shoreline of the body of water. The application also assumes that the second shapefile provides either contours (polyline geometry) or soundings (point geometry) giving depth information for the interior of the lake or pond.

The figure below shows the results from the application. The shoreline edges from the Delaunay triangulation are rendered in black and the interior edges are drawn in white. The image was captured using the free QGIS program to plot the output shapefiles over an Open Street Map (OSM) background. For this example, we could have used any body of water for which we could find appropriate data. For this article, we chose Morrison Lake, a 332 acre (132 hectare) lake located in Ionia County, Michigan, USA.

The Data

The shapefiles for Morrison Lake was obtained from the Michigan Department of Natural Resources (DNR) Inland Lake Contours (DNR, 2023) and Lake Polygons (DNR, 2024) data sets. These data sets include information for bodies of water located throughout the entire state. To prepare shapefiles for processing, we used the QGIS tool to extract just the Morrison Lake features. Water depth contours are given at 5 foot intervals. Both products use a projected coordinate system (i.e. a Cartesian coordinate system rather than latitude and longitude). Position data is scaled to feet using the Michigan State Plane Coordinate System 2022 (ESPG:3078 Michigan Oblique Mercator). In effect, this means that if a pair of points appear 10 feet apart on the Earth's surface, the computed distance between their respective coordinates would also be close to 10 feet.

The Organization of a Shapefile

The use of the singular noun form for "shapefile" is a bit misleading since a shapefile product actually consists of more than one data file. These files must appear in the same folder (or directory) and are all given with the same basename (i.e. MorrisonPolygon.shp, MorrisonPolygon.dbf, MorrisonPolygon.shx, etc.). The file name suffix (extention) indicates the kind of content stored in each.

Suffix Deascription
.shp Shape geometry, one record per feature
.dbf Metadata attributes for each feature
.shx An index file for accessing records in random order
.prj Projected coordinate system definition, formatted text

The main file in the collection takes the .shp extention. It consists of a header, which defines the shapefile geometry (points, lines, or polygons), followed by a series of variable-length records that represent each feature in the data set. For example, a shapefile may contain polygons for several lakes with each being given a separate record. Each record contains sequences one or more georeferenced coordinates that describe the geometry of each feature.

In addition to the main file, the shapefile product includes a number of sidecar files such as those listed in the table above (.dbf, .shx, etc.). For each record in the main file, there is a corresponding fixed-length record in the .shx and .dbf files.

The metadata in the .dbf file is used to carry supporting information for the product. For example, if a shapefile represents a set of lakes, the .dbf file might include the name of each lake, its computed surface area, and the elevation of its shoreline. Records from a shapefile representing political entites might give local population and income statistics.

The .shx files provide an geo-referenced index that can be used to expedite rendering and analysis operations. The .prj file provides map projection information that allows the shapefile data to be transformed for plotting on maps and other geographic display surfaces.

For more detail on the organization of shapefiles, please refer to Wikipedia which offers an excellent discussion of their structure and organization.

Depth Contours versus Soundings

When building a Delaunay triangulation, we are actually interested in point features rather than contours. So we would have preferred an input data set that consisted of soundings (point depth measurements) rather than contours. Unfortunately, most of the available lake data on the Internet is available in contour form rather than soundings. Naturally, we have to work with what we've got. So for this example, we extracted the point coordinates that made up the contours to build vertices for the interior of our Delaunay triangulation.

The Application

Our example application was implemented as a class named TinfourShapefilesExample. The code for the main method follows:

public class TinfourShapefilesExample {

  public static void main(String[] args) throws IOException {

    File inputPolygon    = new File("MorrisonPolygon.shp");
    File inputContours   = new File("MorrisonContours.shp");
    File outputShapefile = new File("MorrisonDelaunay.shp");
    String depthField    = "depth";  // the name of the depth-attributes for contours

    TinfourShapefilesExample example = new TinfourShapefilesExample();

    IncrementalTin tin = example.readShapefiles(inputPolygon, inputContours, depthField);
    example.writeShapefile(outputShapefile, tin);
  }

For this example, we hard wired the names for our input and output files. The name of the attribute (.dbf data field) for contour depths was obtained by inspecting the source files using the QGIS Geographic Information System. In normal practice, we would have coded our application using a more general solution that would accept user input for selecting and configuring source data. But to simplify the example code for this dicussion, we avoided the extra logic needed to do so.

Reading the Shapefiles and Building the Delaunay Triangulation

The input-polygon shapefile provides the defining polygon for Morrison lake. We use that input as a contraint to create a Constrained Delaunay Triangulation. Tinfour's ConstraintReaderShapefile utility encapsulates a lot of the details and allows use to initialize the Delaunay triangulation with relatively simple code

IncrementalTin readShapefiles(
    File inputPolygonShapefile,
    File inputContoursShapefile,
    String contourDbfFieldName) throws IOException {

    // Load the polygon constraints defining the shoreline and area of coverage
    // for the lake or pond
    List<IConstraint> constraints;
     try(var constraintReader = new ConstraintReaderShapefile(inputPolygonShapefile)){
       constraints = constraintReader.read();
     }

     // Store the constraints in a Delaunay triangulation
     // (also called a "triangulated irregular network", or TIN).
     IncrementalTin tin = new IncrementalTin(1.0);
     tin.addConstraints(constraints, true);

The Tinfour API allows us to add vertices before or after we add constraints. Because the order doesn't matter, we elected to place the simpler code first and start with the enclosing polygons for the lake. The code that follows demonstates the use of the ShapefileReader class. Although we could have used the VertexReaderShapefile utility to hide some of the complexity, we chose to use the more basic API for purposes of illustration. The ShapefileReader class to extracts data from the input contour file one record at a time. The MorrisonContour shapefile gives contours as polyline features (a sequence of connected points). For our purposes, we just want the coordinates for individual points so that we can form Tinfour Vertices and add them to the Delaunay triangulation. The code is a bit more complicated than the constraint-reader snippet given above, but it is still relatively straightforward:

    // Read the vertices form the contour file.  Add the vertices
    // to the Delaunay triangulation.  Also read the text from the prj file.
    try (var reader = new ShapefileReader(inputContoursShapefile);
         DbfFileReader dbfReader = reader.getDbfFileReader())
    {
      // The element prjText is defined as a class member. It will
      // be needed to create a .prj file when writing the results later on.
      prjText = reader.getPrjContent();

      // Some shapefiles give 2D geometries (x and y coordinates)
      // while others have an additional z value (x, y, and z).
      // We need to know how many coordinate values there are per point
      // so that we can compute the index to access the array of coordinates.
      // The ShapefileType enumerated type has a method that lets us know
      // whether z coordinates are present.
      ShapefileType shapefileType = reader.getShapefileType();
      boolean hasZ = shapefileType.hasZ();
      int coordinatesPerVertex = hasZ ? 3 : 2;

      // The DbfFileReader class lets us access the metadata from the .dbf file.
      // An instance of DbfField can be used to access the data for an attibute.
      DbfField depthField = dbfReader.getFieldByName(contourDbfFieldName);

      // We are now ready to read the file one record at a time.
      // Because the file gives contours, each record will include multiple points
      ShapefileRecord record = null;  // a reusable object to accept content
      int kVertex = 0;
      while (reader.hasNext()) {
        // On the first read operation, record object will be null but will be
        // replaced by a new instance created by the reader.  On all
        // subsequent calls, it will be reused.
        record = reader.readNextRecord(record);

        // We want to get the depth value for the record and use it as
        // the z coordinate when constructing vertices.
        dbfReader.readField(record.recordNumber, depthField);
        double z = depthField.getDouble();
        for (int i = 0; i < record.nPoints; i++) {
          int xyzIndex = i * coordinatesPerVertex;
          double x = record.xyz[xyzIndex];
          double y = record.xyz[xyzIndex + 1];
          Vertex v = new Vertex(x, y, z, kVertex++);
          tin.add(v);
        }
      }
    } // end of try-with-resources block

    return tin;
}

Text for the Map Projection (prjText)

In the code above, the ShapefileReader was used to extract the content of the .prj file and save it in the member element prjText. Shapefiles use .prj files (projection files) to store a text-based description of the map projection that is used to transform the coordinates in the shapefile to geographic (latitude and longitude) coordinates and vice versa. The definition for projection files is well beyond the scope of both these notes and Tinfour itself. But we will need create a .prj file when writing our output to a new shapefile. We can do this by simply copying the content of the source .prj and adding it to our output. To support that use, the Tinfour utilities give us the ability to transcribe .prj files even if they do not implement the ability to understand them.

Just as a reference, the following text shows the content of the MichiganContours.prj file. The example code handles this information behind-the-scenes so that you do not have to deal with it unless you have specific requirements to do so.

PROJCS["NAD_1983_StatePlane_Michigan_South_FIPS_2113_Feet_Intl",
      GEOGCS["GCS_North_American_1983",
             DATUM["D_North_American_1983",SPHEROID["GRS_1980",6378137.0,298.257222101]],
                 PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]],
                 PROJECTION["Lambert_Conformal_Conic"],
                 PARAMETER["False_Easting",13123359.58],
                 PARAMETER["False_Northing",0.0],
                 PARAMETER["Central_Meridian",-84.3666666666667],
                 PARAMETER["Standard_Parallel_1",43.6666666666667],
                 PARAMETER["Standard_Parallel_2",42.1],
                 PARAMETER["Latitude_Of_Origin",41.5],UNIT["foot",0.3048]]

Writing a Delaunay Triangulation to a Shapefile

The first step in using the ShapefileWriter class to write content to a shapefile is to create a specification describing the structure of the file and providing metadata elements for the sidecar files (.prj and .dbf). The specification is then passed into the constructor for Tinfour's ShapefileWriter class. The constructor creates 4 new files to the computer's file system: the main file (the .shp file) and the sidecar files (.dbf, .shx, and .prj). The code snippet below shows an example:

    ShapefileWriterSpecification spec = new ShapefileWriterSpecification();
    spec.setShapefileType(ShapefileType.PolyLine);
    spec.setShapefilePrjContent(prjText);
    spec.addIntegerField("shoreline", 1);

    ShapefileWriter writer = new ShapefileWriter(outputShapefile, spec));

A shapefile is allowed to have a single geometry type. The Tinfour API supports points, polygons, and lines (polylines). These are indicated using the Java enumeration ShapefileType. Our example application writes the Delaunay triangulation to a shapefile as a set of simple line segments. To do so, it specifies an output geometry of type PolyLine.

In the Morrison Lake image shown above, the shoreline edges are rendered in black while the internal edges are drawn in white. To distinguish the two kinds of edges, we used a metadata attribute named "shoreline" which is recorded as a one-digit integer value in the .dbf sidecar file.

Finally, the specification accepts the text for the .prj file. In modern GIS programs (QGIS, ArcInfo), the .prj file is usually treated as a mandatory element. Generally, if a .prj file is missing, the programs will require that the user select a map projection when the shapefile is loaded.

Example code for writing the Delaunay triangulation shapefile

The example below provides the complete code for the shapefile-writing method. For this discussion, we chose to store only those edges from the Delaunay that were part of the constrained region defined by the lake polygon. Those edges that fall outside the lake or within its small island are ommitted. The code determines whether it includes an edge using the Java method edge.isConstraintRegionMember() .

The shapefile-writing code uses an instance of ShapefileRecord to store the geometry for each feature to be written to the output (in this case, individual Delaunay edges). When the writeRecord() method is called, the ShapefileWriter adds a single record to the main .shp file and corresponding records to the sidecar files (.dbf and .shx).

As noted above, the .dbf file is used to store metadata indicating whether the edge was part of the shoreline or was internal to the lake. The code example shows that in order to propagate the metadata into the .dbf record when it is written, the field values must be set before the wrteRecord() method is invoked.

  void writeShapefile(File outputShapefile, IncrementalTin tin) throws IOException {
  
    ShapefileWriterSpecification spec = new ShapefileWriterSpecification();
    spec.setShapefileType(ShapefileType.PolyLine);
    spec.setShapefilePrjContent(prjText);
    spec.addIntegerField("shoreline", 1);

    try (ShapefileWriter writer = new ShapefileWriter(outputShapefile, spec)) {
      double[] xyz = new double[4];

      // loop on all edges in the Delaunay. Those that are inside the
      // constrained region are written to the output file.
      for (IQuadEdge edge : tin.edges()) {
        if (edge.isConstraintRegionMember()) {
          Vertex A = edge.getA();
          Vertex B = edge.getB();
          xyz[0] = A.getX();
          xyz[1] = A.getY();
          xyz[2] = B.getX();
          xyz[3] = B.getY();
          ShapefileRecord record = writer.createRecord();
          record.addPolyLine(2, xyz);
          if (edge.isConstrained()) {
            // the edge comes from the original lake polygon input
            writer.setDbfFieldValue("shoreline", 1);
          } else {
            // the edge is from the interior of the lake
            writer.setDbfFieldValue("shoreline", 0);
          }
          writer.writeRecord(record);
        }
      }
    } // end of try-with-resources block
  }

Conclusion

Tinfour's ability to read shapefiles allows applications to access a large number of geophysical and geopolitical data sources available on the Internet. Its ability to write shapefiles allows it to export its analysis products (Delaunay triangulations or derived products) for use by Geographic Information Systems (GIS) and other map-based applications.

References

Michigan Department of Natural Resources [DNR]. (2023, September 8). Inland Lake Contours https://gis-michigan.opendata.arcgis.com/datasets/midnr::inland-lake-contours/about.

Michigan Department of Natural Resources [DNR]. (2024, April 4). Lake Polygons https://gis-michigan.opendata.arcgis.com/datasets/Michigan::lake-polygons/about

Clone this wiki locally