GeoTools : How to create a custom MapLayer -- jdk 1.5 geotools 2.1.M4

An example of how to create your own MapLayer.

I adapted the original How to create a simple FeatureCollection from Scratch example by knudsen, because it no longer compiled with 2.1.M4, which I was using.

This example

  • creates a custom Feature,
  • creates a custom FeatureType,
  • uses SLD (Styled Layer Descriptor) to describe the style to use, and
  • creates and shows a custom MapLayer.

import java.awt.BorderLayout;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.AffineTransform;
import java.io.IOException;
import java.io.StringReader;

import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;

import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Envelope;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.Point;

import org.opengis.referencing.crs.CoordinateReferenceSystem;

import org.geotools.feature.AttributeTypeFactory;
import org.geotools.feature.DefaultFeature;
import org.geotools.feature.DefaultFeatureCollection;
import org.geotools.feature.DefaultFeatureType;
import org.geotools.feature.FeatureTypeFactory;
import org.geotools.feature.GeometryAttributeType;
import org.geotools.feature.IllegalAttributeException;
import org.geotools.feature.SchemaException;
import org.geotools.map.DefaultMapContext;
import org.geotools.map.DefaultMapLayer;
import org.geotools.referencing.crs.GeographicCRS;
import org.geotools.renderer.lite.LiteRenderer;
import org.geotools.styling.SLDParser;
import org.geotools.styling.Style;
import org.geotools.styling.StyleFactory;

/**
 * <p>
 * The <span style="color: green;">How to create your own MapLayer</span>
 * geotools snippet.
 * </p>
 *
 * <p>
 * I adapted the original <span style="color: green;">How to create a simple
 * FeatureCollection from Scratch</span> example by knudsen, because it no
 * longer compiled with geotools 2.1.M4, which I was using.
 * </p>
 *
 * <p>
 * This example
 * <ul>
 *  <li>
 *   creates a custom {@link org.geotools.feature.Feature},
 *  </li>
 *  <li>
 *   creates a custom {@link org.geotools.feature.FeatureType},
 *  </li>
 *  <li>
 *   uses SLD (Styled Layer Descriptor) to describe
 *   the {@link org.geotools.styling.Style} to use, and
 *  </li>
 *  <li>
 *   creates and shows a custom {@link org.geotools.map.MapLayer}.
 *  </li>
 * </ul>
 * </p>
 *
 * <p>
 * I developed this snippet with
 * <span style="color: gray; font-weight: bold;">jdk 1.5</span>
 * and <span style="color: gray; font-weight: bold;">geotools 2.1.M4</span>.
 * </p>
 *
 * <p>
 * Compile with
 * <pre style="color: blue;">
 *   javac -cp JTS-1.6.jar:geoapi-20050403.jar:gt2-main.jar HouseMapLayer.java</pre>
 * </p>
 *
 * <p>
 * Run with
 * <pre style="color: blue;">
 *   java -cp JTS-1.6.jar:units-0.01.jar:geoapi-20050403.jar:gt2-main.jar:. HouseMapLayer</pre>
 * </p>
 *
 * <p>
 * Good luck.
 * </p>
 *
 * @author knudsen (the author of the original example)
 *
 * @author Arjan Vermeij (the author of this adapted example)
 */
public class HouseMapLayer extends DefaultMapLayer
{
  private static final House keithsHouse
     = new House ("Keith's House", "Oxnard", "CA", -119.1761, 34.1975);

  public static void main (final String [] arguments)
  {
    SwingUtilities.invokeLater
       (new Runnable ()
        {
          public void run ()
          {
            new JFrame ()
            {
              {
                setDefaultCloseOperation (EXIT_ON_CLOSE);
                getContentPane ().add (new HouseJPanel (), BorderLayout.CENTER);
                setSize (400, 400);
                setVisible (true);
              }
            };
          }
        });
  }

  public HouseMapLayer ()
  throws IOException
  {
    // Load a style using the Styled Layer Descriptor xml format
    // defined by the OGC. Styles will usually be loaded from an
    // external file.
    // This style renders Keith's House as a fairly large red star, with two
    // labels, its name and city.
    this
       (createHouseFeatureType (GeographicCRS.WGS84),
        loadStyle
           ("  <UserStyle xmlns:ogc=\"http://www.opengis.net/ows\">"
            + " <FeatureTypeStyle>"
            + "  <Rule>"
            + "   <PointSymbolizer>"
            + "     <Graphic>"
            + "       <Mark>"
            + "         <WellKnownName>star</WellKnownName>"
            + "         <Fill>"
            + "           <CssParameter name=\"fill\">#ff0000</CssParameter>"
            + "         </Fill>"
            + "       </Mark>"
            + "       <Size>80.0</Size>"
            + "     </Graphic>"
            + "   </PointSymbolizer>"
            + "  </Rule>"
            + "  <Rule>"
            + "    <TextSymbolizer>"
            + "      <Label>"
            + "        <ogc:PropertyName>NAME</ogc:PropertyName>"
            + "      </Label>"
            + "      <LabelPlacement>"
            + "        <PointPlacement>"
            + "          <AnchorPoint>"
            + "            <AnchorPointX>1.0</AnchorPointX>"
            + "            <AnchorPointY>0.5</AnchorPointY>"
            + "          </AnchorPoint>"
            + "          <Displacement>"
            + "            <DisplacementX>-20</DisplacementX>"
            + "            <DisplacementY>-20</DisplacementY>"
            + "          </Displacement>"
            + "        </PointPlacement>"
            + "      </LabelPlacement>"
            + "      <Font/>"
            + "      <Fill>"
            + "        <CssParameter name=\"fill\">#0000ff</CssParameter>"
            + "      </Fill>"
            + "      <Halo/>"
            + "    </TextSymbolizer>"
            + "  </Rule>"
            + "  <Rule>"
            + "    <TextSymbolizer>"
            + "      <Label>"
            + "        <ogc:PropertyName>CITY</ogc:PropertyName>"
            + "      </Label>"
            + "      <LabelPlacement>"
            + "        <PointPlacement>"
            + "          <Displacement>"
            + "            <DisplacementX>30</DisplacementX>"
            + "            <DisplacementY>20</DisplacementY>"
            + "          </Displacement>"
            + "        </PointPlacement>"
            + "      </LabelPlacement>"
            + "      <Font/>"
            + "      <Fill>"
            + "        <CssParameter name=\"fill\">#00ff00</CssParameter>"
            + "      </Fill>"
            + "      <Halo/>"
            + "    </TextSymbolizer>"
            + "  </Rule>"
            + " </FeatureTypeStyle>"
            + "</UserStyle>"));
  }

  private
  HouseMapLayer
     (final DefaultFeatureType houseFeatureType,
      final Style style)
  {
    super
       (new DefaultFeatureCollection
           ("house-feature-collection", houseFeatureType)
        {
          {
            try {
              add
                 (new HouseFeature
                     (houseFeatureType, HouseMapLayer.keithsHouse));
            }
            catch (final IllegalAttributeException e) {
              e.printStackTrace ();
            }
          }
        },
        style,
        "House Map Layer");
  }

  private static Style loadStyle (final String xml)
  throws IOException
  {
    return new SLDParser (StyleFactory.createStyleFactory ())
    {
      {
        setInput (new StringReader (xml));
      }
    }.readXML () [0];
  }

  private static DefaultFeatureType
  createHouseFeatureType
     (final CoordinateReferenceSystem coordinateReferenceSystem)
  {
    final FeatureTypeFactory featureTypeFactory
       = FeatureTypeFactory.newInstance ("House");

    // The default geometry attribute type. This attribute type must be an
    // instance of GeometryAttributeType. To make this happen, the
    // coordinateReferenceSystem is passed in as the metaData parameter of the
    // newAttributeType method. The other requirement is, that an instance of
    // com.vividsolutions.jts.geom.Geometry is passed through the clazz
    // parameter of the newAttributeType method.
    //
    // The cast to GeometryAttributeType is not essential, it is there to check
    // whether the returned attribute type is in fact an instance of
    // GeometryAttributeType. I prefer things to fail early.
    featureTypeFactory.addType
       ((GeometryAttributeType) AttributeTypeFactory.newAttributeType
           ("the_geom", Point.class, true, 1, null, coordinateReferenceSystem));

    featureTypeFactory.addType
       (AttributeTypeFactory.newAttributeType
           ("NAME", String.class, true, 48, null));
    featureTypeFactory.addType
       (AttributeTypeFactory.newAttributeType
           ("CITY", String.class, true, 48, null));
    featureTypeFactory.addType
       (AttributeTypeFactory.newAttributeType
           ("STATE", String.class, true, 48, null));

    DefaultFeatureType result = null;

    try {
      result = (DefaultFeatureType) featureTypeFactory.getFeatureType ();
    }
    catch (final SchemaException e) {
      e.printStackTrace ();
    }
    return result;
  }

  private static class HouseJPanel extends JPanel
  {
    private final Envelope envelope;
    private final LiteRenderer liteRenderer;

    public HouseJPanel ()
    {
      this.envelope
         = new Envelope
              (new Coordinate
                  ((Math.floor (3 * HouseMapLayer.keithsHouse.longitude) / 3),
                   (Math.floor (3 * HouseMapLayer.keithsHouse.latitude) / 3)),
               new Coordinate
                  ((Math.ceil (3 * HouseMapLayer.keithsHouse.longitude) / 3),
                   (Math.ceil (3 * HouseMapLayer.keithsHouse.latitude) / 3)));
      this.liteRenderer
         = new LiteRenderer
              (new DefaultMapContext ()
               {
                 {
                   try {
                     addLayer (new HouseMapLayer ());
                   }
                   catch (final IOException e) {
                     e.printStackTrace ();
                   }
                 }
               })
           {
             {
               // If not set to true, partial repaints, for example when
               // the window becomes visible after having been partially
               // obscured by another window, go wrong. I do not
               // understand the purpose of not setting this property to
               // true.
               setConcatTransforms (true);
             }
           };
    }

    public void paintComponent (final Graphics graphics)
    {
      if (isOpaque ()) {
        graphics.setColor (getBackground ());
        graphics.fillRect (0, 0, getWidth (), getHeight ());
      }
      _paintComponent ((Graphics2D) graphics);
    }

    private void _paintComponent (final Graphics2D graphics2D)
    {
      graphics2D.setRenderingHints
         (new RenderingHints
             (RenderingHints.KEY_ANTIALIASING,
              RenderingHints.VALUE_ANTIALIAS_ON));

      // Create an affine transform that maps the feature collection onto
      // the center of the JPanel, maintaining aspect ratio.
      final AffineTransform affineTransform
         = new AffineTransform ()
           {
             {
               final double scaleFactor
                  = Math.min
                       ((getWidth ()
                         / HouseJPanel.this.envelope.getWidth ()),
                        (getHeight ()
                         / HouseJPanel.this.envelope.getHeight ()));

               // Translate to the center of the JPanel.
               translate ((getWidth () / 2), (getHeight () / 2));

               // Scale with negative y factor to correct the orientation.
               scale (scaleFactor, - scaleFactor);

               // Translate to the center of the feature collection.
               translate
                  (- ((HouseJPanel.this.envelope.getMinX ()
                       + HouseJPanel.this.envelope.getMaxX ())
                      / 2),
                   - ((HouseJPanel.this.envelope.getMinY ()
                       + HouseJPanel.this.envelope.getMaxY ())
                      / 2));
             }
           };

      this.liteRenderer.paint (graphics2D, getBounds (), affineTransform);
    }
  }

  private static class HouseFeature
  extends DefaultFeature
  {
    public
    HouseFeature (final DefaultFeatureType houseFeatureType, final House house)
    throws IllegalAttributeException
    {
      super
         (houseFeatureType,
          new Object []
          {
            new GeometryFactory ()
               .createPoint (new Coordinate (house.longitude, house.latitude)),
            house.name,
            house.city,
            house.state
          },
          "house-feature-id");
    }
  }

  private static class House
  {
    public final String name;
    public final String city;
    public final String state;
    public final double longitude;
    public final double latitude;

    public
    House
       (final String name,
        final String city,
        final String state,
        final double longitude,
        final double latitude)
    {
      this.name = name;
      this.city = city;
      this.state = state;
      this.longitude = longitude;
      this.latitude = latitude;
    }
  }
}

Comments:

Guys it would really, really, really help if you removed the examples that no longer compile under the current release.

This pretty much means all of them.

Instead, how about just 2 examples of code that takes some coordinates, and displays it on a screen. I think that's all people need to get them going, as you learn the underlying datastructures best when you can actually compile the examples!!!!

Pete

Posted by p2409 at Dec 25, 2008 10:22

Hi Pete - you are correct there is a new user guide (an entirely seperate wiki: http://docs.codehaus.org/display/GEOTDOC/Home) so we should remove these pages. I was waiting until the final section on rendering etc was done but yeah these pages can be removed.

Posted by jive at Dec 30, 2008 00:28