GeoTools : How to render a shape file, style with SLD, zoom, pan -- jdk 1.5 geotools 2.1.M4

The example I wished I had had when I started to work with Geotools.
It tries to show the basic things needed to render a shape file.
I adapted the LiteRendererTest2.java example.

This example

  • reads polylines from a shape file,
  • uses a LiteRenderer to render the polylines,
  • uses SLD (Styled Layer Descriptor) to describe the style to use,
  • includes a few buttons to zoom and pan the rendered image, and
  • additionally renders the image to a .png file.

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.io.StringReader;

import javax.imageio.ImageIO;
import javax.swing.AbstractAction;
import javax.swing.JButton;
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 org.geotools.data.FeatureSource;
import org.geotools.data.shapefile.ShapefileDataStore;
import org.geotools.map.DefaultMapContext;
import org.geotools.map.MapContext;
import org.geotools.map.MapLayer;
import org.geotools.renderer.lite.LiteRenderer;
import org.geotools.styling.Rule;
import org.geotools.styling.SLDParser;
import org.geotools.styling.Stroke;
import org.geotools.styling.Style;
import org.geotools.styling.StyleAttributeExtractor;
import org.geotools.styling.StyleFactory;

/**
 * <p>
 * The
 * <span style="color: green;">How to render a shape file, style with SLD, zoom, pan</span>
 * geotools snippet.
 * </p>
 *
 * <p>
 * The example I wished I had had when I started to work with Geotools.
 * It tries to show the basic things needed to render a shape file.
 * I adapted the <code>LiteRendererTest2.java</code> example.
 * This example
 * <ul>
 *  <li>
 *   reads polylines from a shape file,
 *  </li>
 *  <li>
 *   uses a {@link org.geotools.renderer.lite.LiteRenderer} to render the
 *   polylines,
 *  </li>
 *  <li>
 *   uses SLD (Styled Layer Descriptor) to describe
 *   the {@link org.geotools.styling.Style} to use,
 *  </li>
 *  <li>
 *   includes a few buttons to zoom and pan the rendered image, and
 *  </li>
 *  <li>
 *   additionally renders the image to a .png file.
 *  </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:gt2-shapefile.jar LiteRendererJFrame.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:gt2-shapefile.jar:. LiteRendererJFrame</pre>
 * </p>
 *
 * <p>
 * Good luck.
 * </p>
 *
 * @author Arjan Vermeij
 */
public class LiteRendererJFrame extends JFrame
{
  public static void main (final String [] arguments)
  {
    SwingUtilities.invokeLater
       (new Runnable ()
        {
          public void run ()
          {
            try {
              new LiteRendererJFrame ();
            }
            catch (final Exception e) {
              e.printStackTrace ();
            }
          }
        });
  }

  private final LiteRendererJPanel liteRendererJPanel;

  public LiteRendererJFrame ()
  throws IOException
  {
    this.liteRendererJPanel = new LiteRendererJPanel ();
    setDefaultCloseOperation (JFrame.EXIT_ON_CLOSE);
    getContentPane ().add (this.liteRendererJPanel, BorderLayout.CENTER);
    getContentPane ().add
       (new JButton
           (new AbstractAction ("<<")
            {
              public void actionPerformed (final ActionEvent actionEvent)
              {
                LiteRendererJFrame.this.liteRendererJPanel.panLeft ();
              }
            }),
        BorderLayout.WEST);
    getContentPane ().add
       (new JButton
           (new AbstractAction (">>")
            {
              public void actionPerformed (final ActionEvent actionEvent)
              {
                LiteRendererJFrame.this.liteRendererJPanel.panRight ();
              }
            }),
        BorderLayout.EAST);
    getContentPane ().add
       (new JButton
           (new AbstractAction ("Zoom Out")
            {
              public void actionPerformed (final ActionEvent actionEvent)
              {
                LiteRendererJFrame.this.liteRendererJPanel.zoomOut ();
              }
            }),
        BorderLayout.NORTH);
    getContentPane ().add
       (new JButton
           (new AbstractAction ("Zoom In")
            {
              public void actionPerformed (final ActionEvent actionEvent)
              {
                LiteRendererJFrame.this.liteRendererJPanel.zoomIn ();
              }
            }),
        BorderLayout.SOUTH);
    setSize (400, 400);
    setVisible (true);
  }

  private class LiteRendererJPanel extends JPanel
  {
    private final LiteRenderer liteRenderer;
    private final Envelope envelope;

    private final Coordinate center;
    private double zoomFactor = 1.0;

    public LiteRendererJPanel ()
    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 first paints lines with a purple stroke 3 pixels wide, then
      // it paints the same lines with a white stroke 1.5 pixels wide, to
      // create a 'cased' effect.
      final Style style
         = loadStyle
              ("  <UserStyle>"
               + " <FeatureTypeStyle>"
               + "  <Rule>"
               + "   <LineSymbolizer>"
               + "    <Stroke>"
               + "     <CssParameter name=\"stroke\">#ff00ff</CssParameter>"
               + "     <CssParameter name=\"width\">3.0</CssParameter>"
               + "    </Stroke>"
               + "   </LineSymbolizer>"
               + "  </Rule>"
               + "  <Rule>"
               + "   <LineSymbolizer>"
               + "    <Stroke>"
               + "     <CssParameter name=\"stroke\">#ffffff</CssParameter>"
               + "     <CssParameter name=\"width\">1.5</CssParameter>"
               + "    </Stroke>"
               + "   </LineSymbolizer>"
               + "  </Rule>"
               + " </FeatureTypeStyle>"
               + "</UserStyle>");

      // Shows some information about the style we just read.
      visit (style);

      final MapContext mapContext
         = new DefaultMapContext ()
           {
             {
               // The "Boundary Lines.shp" shape file I got through
               // http://www.esri.com, see
               // http://www.esri.com/data/download/basemap/index.html
               // But any shape file defining polylines should do.
               _addLayer
                  (new ShapefileDataStore
                      (new File ("Boundary Lines.shp").toURI ().toURL ())
                      .getFeatureSource ());
             }

             private void _addLayer (final FeatureSource featureSource)
             {
               System.out.println
                  ("schema is " + featureSource.getSchema ());
               addLayer (featureSource, style);
             }
           };

      this.liteRenderer
         = new LiteRenderer (mapContext)
           {
             {
               // 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);
             }
           };
      this.envelope
         = new Envelope ()
           {
             {
               for (final MapLayer mapLayer : mapContext.getLayers ()) {
                 expandToInclude
                    (mapLayer.getFeatureSource ().getFeatures ().getBounds ());
               }
             }
           };
      this.center
         = new Coordinate
              (((this.envelope.getMinX () + this.envelope.getMaxX ()) / 2),
               ((this.envelope.getMinY () + this.envelope.getMaxY ()) / 2));
    }

    public void panLeft ()
    {
      this.center.x -= ((this.envelope.getWidth () / 3) / this.zoomFactor);
      repaint ();
    }

    public void panRight ()
    {
      this.center.x += ((this.envelope.getWidth () / 3) / this.zoomFactor);
      repaint ();
    }

    public void zoomIn ()
    {
      this.zoomFactor *= 1.25;
      repaint ();
    }

    public void zoomOut ()
    {
      this.zoomFactor /= 1.25;
      repaint ();
    }

    private double getZoomFactor ()
    {
      return this.zoomFactor;
    }

    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 ()
                          / LiteRendererJPanel.this.envelope.getWidth ()),
                         (getHeight ()
                          / LiteRendererJPanel.this.envelope.getHeight ()))
                     * getZoomFactor ());

               // 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
                  (- LiteRendererJPanel.this.center.x,
                   - LiteRendererJPanel.this.center.y);
             }
           };

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

      // Render to an image buffer.
      BufferedImage bufferedImage
         = new BufferedImage
              (getWidth (), getHeight (), BufferedImage.TYPE_INT_RGB)
           {
             {
               paint ((Graphics2D) getGraphics ());
             }

             private void paint (final Graphics2D graphics2D)
             {
               graphics2D.setRenderingHints
                  (new RenderingHints
                      (RenderingHints.KEY_ANTIALIASING,
                       RenderingHints.VALUE_ANTIALIAS_ON));
               graphics2D.setColor (Color.white);
               graphics2D.fillRect (0, 0, getWidth (), getHeight ());
               LiteRendererJPanel.this.liteRenderer.paint
                  (graphics2D,
                   new Rectangle (0, 0, getWidth (), getHeight ()),
                   affineTransform);
             }
           };

      try {
        ImageIO.write
           (bufferedImage, "png", new File ("boundary-lines.png"));
      }
      catch (final IOException e) {
        e.printStackTrace ();
      }
    }

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

    private void visit (final Style style)
    {
      style.accept
         (new StyleAttributeExtractor ()
          {
            public void visit (final Stroke stroke)
            {
              System.out.println ("start of stroke " + stroke);
              super.visit (stroke);
              System.out.println ("end of stroke " + stroke);
            }
          });
    }
  };
}

Comments:

I found the exmple very useful, so thought I'd tweak it for gt2-2.3.0-M0 & JTS 1.7

  • uses no deprecated code
  • uses a StreamingRender to render the polylines
  • uses a file chooser to open a polyline shape file
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Envelope;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.io.StringReader;
import java.net.MalformedURLException;
import java.net.URL;

import javax.imageio.ImageIO;
import javax.swing.AbstractAction;
import javax.swing.JButton;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;

import org.geotools.data.FeatureSource;
import org.geotools.data.shapefile.ShapefileDataStore;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.graph.util.SimpleFileFilter;
import org.geotools.map.DefaultMapContext;
import org.geotools.map.MapContext;
import org.geotools.map.MapLayer;
import org.geotools.referencing.crs.DefaultGeographicCRS;
import org.geotools.renderer.lite.StreamingRenderer;
import org.geotools.styling.SLDParser;
import org.geotools.styling.Stroke;
import org.geotools.styling.Style;
import org.geotools.styling.StyleAttributeExtractor;
import org.geotools.styling.StyleFactoryFinder;
import org.opengis.referencing.crs.CoordinateReferenceSystem;

/**
 * <p>
 * The <span style="color: green;">How to render a shape file, style with SLD,
 * zoom, pan</span> geotools snippet.
 * </p>
 *
 * <p>
 * The example I wished I had had when I started to work with Geotools. It tries
 * to show the basic things needed to render a shape file. I adapted the
 * <code>LiteRendererTest2.java</code> example. This example
 * <ul>
 * <li> reads polylines from a shape file, </li>
 * <li> uses a {@link org.geotools.renderer.lite.StreamingRenderer} to render
 * the polylines, </li>
 * <li> uses SLD (Styled Layer Descriptor) to describe the
 * {@link org.geotools.styling.Style} to use, </li>
 * <li> includes a few buttons to zoom and pan the rendered image, and </li>
 * <li> additionally renders the image to a .png file. </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.3.0-M0</span>.
 * </p>
 *
 * <p>
 * Compile with
 *
 * <pre style="color: blue;">
 *           javac -cp JTS-1.7.jar:geoapi-nogenerics-2.1-M1.jar:gt2-main.jar:gt2-shapefile.jar StreamingRendererJFrame.java
 * </pre>
 *
 * </p>
 *
 * <p>
 * Run with
 *
 * <pre style="color: blue;">
 *           java -cp JTS-1.7.jar:units-0.01.jar:geoapi-nogenerics-2.1-M1.jar:gt2-main.jar:gt2-shapefile.jar:. StreamingRendererJFrame
 * </pre>
 *
 * </p>
 *
 * <p>
 * Good luck.
 * </p>
 *
 * @author Arjan Vermeij
 *
 * Updated by Maurice Walton for gt2-2.3.0-M0, also included file chooser code
 * for polyline shape file selection
 */
@SuppressWarnings("serial")
public class StreamingRendererJFrame extends JFrame {
    private class StreamingRendererJPanel extends JPanel {
        /**
         * Comment for <code>serialVersionUID</code>.
         */
        private static final long serialVersionUID = 1199749163815737039L;

        private final Coordinate center;

        private final CoordinateReferenceSystem crs = DefaultGeographicCRS.WGS84;

        private final Envelope envelope;

        private final StreamingRenderer streamingRenderer;

        private double zoomFactor = 1.0;

        /**
         * Constructor comment.
         *
         * @throws IOException
         */
        public StreamingRendererJPanel() 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 first paints lines with a purple stroke 3 pixels wide,
            // then it paints the same lines with a white stroke 1.5 pixels
            // wide, to
            // create a 'cased' effect.
            final Style style = loadStyle("  <UserStyle>"
                    + " <FeatureTypeStyle>"
                    + "  <Rule>"
                    + "   <LineSymbolizer>"
                    + "    <Stroke>"
                    + "     <CssParameter name=\"stroke\">#ff00ff</CssParameter>"
                    + "     <CssParameter name=\"width\">3.0</CssParameter>"
                    + "    </Stroke>"
                    + "   </LineSymbolizer>"
                    + "  </Rule>"
                    + "  <Rule>"
                    + "   <LineSymbolizer>"
                    + "    <Stroke>"
                    + "     <CssParameter name=\"stroke\">#ffffff</CssParameter>"
                    + "     <CssParameter name=\"width\">1.5</CssParameter>"
                    + "    </Stroke>" + "   </LineSymbolizer>" + "  </Rule>"
                    + " </FeatureTypeStyle>" + "</UserStyle>");

            // Shows some information about the style we just read.
            visit(style);

            // final MapContext mapContext = new DefaultMapContext()
            final MapContext mapContext = new DefaultMapContext(crs) {
                {
                    // This was tested with "Boundary Lines.shp" shape file I
                    // got through
                    // http://www.esri.com, see
                    // http://www.esri.com/data/download/basemap/index.html
                    // But any shape file defining polylines should do.
                    // _addLayer(new ShapefileDataStore(new File(
                    // "Boundary Lines.shp").toURI().toURL())
                    // .getFeatureSource());
                    _addLayer(new ShapefileDataStore(openURL())
                            .getFeatureSource());
                }

                private void _addLayer(final FeatureSource featureSource) {
                    System.out
                            .println("schema is " + featureSource.getSchema());
                    addLayer(featureSource, style);
                }
            };

            this.streamingRenderer = new StreamingRenderer();
            this.streamingRenderer.setContext(mapContext);
            this.envelope = new Envelope() {
                {
                    for (final MapLayer mapLayer : mapContext.getLayers()) {
                        expandToInclude(mapLayer.getFeatureSource()
                                .getFeatures().getBounds());
                    }
                }
            };
            this.center = new Coordinate(
                    ((this.envelope.getMinX() + this.envelope.getMaxX()) / 2),
                    ((this.envelope.getMinY() + this.envelope.getMaxY()) / 2));
        }

        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() / StreamingRendererJPanel.this.envelope
                                    .getWidth()),
                            (getHeight() / StreamingRendererJPanel.this.envelope
                                    .getHeight())) * getZoomFactor());

                    // 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(-StreamingRendererJPanel.this.center.x,
                            -StreamingRendererJPanel.this.center.y);
                }
            };

            this.streamingRenderer.paint(graphics2D, getBounds(),
                    new ReferencedEnvelope(this.envelope, this.crs),
                    affineTransform);

            // Render to an image buffer (for output to a file).
            BufferedImage bufferedImage = new BufferedImage(getWidth(),
                    getHeight(), BufferedImage.TYPE_INT_RGB) {
                {
                    paint((Graphics2D) getGraphics());
                }

                private void paint(final Graphics2D graphics2D) {
                    graphics2D.setRenderingHints(new RenderingHints(
                            RenderingHints.KEY_ANTIALIASING,
                            RenderingHints.VALUE_ANTIALIAS_ON));
                    graphics2D.setColor(Color.white);
                    graphics2D.fillRect(0, 0, getWidth(), getHeight());

                    StreamingRendererJPanel.this.streamingRenderer.paint(graphics2D,
                            new Rectangle(0, 0, getWidth(), getHeight()),
                            new ReferencedEnvelope(envelope, crs),
                            affineTransform);
                }
            };

            // output image buffer to a file
            try {
                ImageIO.write(bufferedImage, "png", new File(
                        "boundary-lines.png"));
            } catch (final IOException e) {
                e.printStackTrace();
            }
        }

        private double getZoomFactor() {
            return this.zoomFactor;
        }

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

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

        /**
         * Method comment.
         */
        public void panLeft() {
            this.center.x -= ((this.envelope.getWidth() / 3) / this.zoomFactor);
            repaint();
        }

        /**
         * Method comment.
         */
        public void panRight() {
            this.center.x += ((this.envelope.getWidth() / 3) / this.zoomFactor);
            repaint();
        }

        private void visit(final Style style) {
            style.accept(new StyleAttributeExtractor() {
                public void visit(final Stroke stroke) {
                    System.out.println("start of stroke " + stroke);
                    super.visit(stroke);
                    System.out.println("end of stroke " + stroke);
                }
            });
        }

        /**
         * Method comment.
         */
        public void zoomIn() {
            this.zoomFactor *= 1.25;
            repaint();
        }

        /**
         * Method comment.
         */
        public void zoomOut() {
            this.zoomFactor /= 1.25;
            repaint();
        }
    }

    /**
     * Method comment.
     *
     * @param arguments
     */
    public static void main(final String[] arguments) {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                try {
                    new StreamingRendererJFrame();
                } catch (final Exception e) {
                    e.printStackTrace();
                }
            }
        });
    }

    /**
     * Copied from org.geotools.demo.data.ShapeReader.java so as not to be
     * dependant on demo code. This method will prompt the user for a shapefile.
     * <p>
     * This method will call System.exit() if the user presses cancel.
     * </p>
     *
     * @return url to selected shapefile.
     * @throws MalformedURLException
     */
    public static URL openURL() throws MalformedURLException {
        URL shapeURL = null;
        JFileChooser fileChooser = new JFileChooser();
        fileChooser.setFileFilter(new SimpleFileFilter("shp", "Shapefile"));

        int result = fileChooser.showOpenDialog(null);

        if (result == JFileChooser.APPROVE_OPTION) {
            File f = fileChooser.getSelectedFile();
            shapeURL = f.toURL();
        } else {
            System.out.println("Goodbye");
            System.exit(0);
        }
        return shapeURL;
    }

    private final StreamingRendererJPanel liteRendererJPanel;

    /**
     * Constructor comment.
     *
     * @throws IOException
     */
    @SuppressWarnings("serial")
    public StreamingRendererJFrame() throws IOException {
        this.liteRendererJPanel = new StreamingRendererJPanel();
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        getContentPane().add(this.liteRendererJPanel, BorderLayout.CENTER);
        getContentPane().add(new JButton(new AbstractAction("<<") {
            public void actionPerformed(final ActionEvent actionEvent) {
                StreamingRendererJFrame.this.liteRendererJPanel.panLeft();
            }
        }), BorderLayout.WEST);
        getContentPane().add(new JButton(new AbstractAction(">>") {
            public void actionPerformed(final ActionEvent actionEvent) {
                StreamingRendererJFrame.this.liteRendererJPanel.panRight();
            }
        }), BorderLayout.EAST);
        getContentPane().add(new JButton(new AbstractAction("Zoom Out") {
            public void actionPerformed(final ActionEvent actionEvent) {
                StreamingRendererJFrame.this.liteRendererJPanel.zoomOut();
            }
        }), BorderLayout.NORTH);
        getContentPane().add(new JButton(new AbstractAction("Zoom In") {
            public void actionPerformed(final ActionEvent actionEvent) {
                StreamingRendererJFrame.this.liteRendererJPanel.zoomIn();
            }
        }), BorderLayout.SOUTH);
        setSize(400, 400);
        setVisible(true);
    }
}
Posted by maurice.walton@infoterra-global.com at Nov 09, 2006 09:10