Article
The traditional MapServer CGI (mapserv) is a powerful, C-based workhorse for serving web maps. But what if your ecosystem is predominantly Java, or you need to tightly integrate mapping functionality into an existing Spring Boot or Jakarta EE application? You can build the core functionality of a map server directly in Java.
This article guides you through creating a simple Java-based web application that mimics the key function of the MapServer CGI: accepting a request with parameters (like bounding box and layers) and returning a dynamic map image.
Core Concept: Replacing the CGI with a Servlet
Instead of a standalone CGI executable, we will use a Java Servlet (or a Spring @RestController) as our endpoint. This servlet will parse incoming HTTP GET parameters, use a spatial library to render the map, and stream the resulting image back to the client.
The Technology Stack
- Java Servlet Container: Tomcat, Jetty, or a full-fledged application server.
- Spatial Libraries:
- GeoTools: The de facto standard open source Java library for geospatial data. It provides the rendering engine we need.
- JTS Topology Suite: The geometry model that GeoTools builds upon.
Step-by-Step Implementation
Let's build a minimal MapServler (a servlet that acts like mapserv).
Step 1: Project Setup and Dependencies
If you're using Maven, include these critical dependencies in your pom.xml:
<dependencies> <!-- GeoTools Core --> <dependency> <groupId>org.geotools</groupId> <artifactId>gt-main</artifactId> <version>28.2</version> </dependency> <!-- GeoTools Rendering --> <dependency> <groupId>org.geotools</groupId> <artifactId>gt-render</artifactId> <version>28.2</version> </dependency> <!-- GeoTools Shapefile Module --> <dependency> <groupId>org.geotools</groupId> <artifactId>gt-shapefile</artifactId> <version>28.2</version> </dependency> <!-- Servlet API --> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency> </dependencies> <!-- Add the GeoTools repository --> <repositories> <repository> <id>osgeo</id> <name>OSGeo Release Repository</name> <url>https://repo.osgeo.org/repository/release/</url> </repository> </repositories>
Step 2: The Map Servlet Code
This servlet handles requests to, for example, /map?WIDTH=800&HEIGHT=600&BBOX=-180,-90,180,90&LAYERS=countries.
package com.example.mapserv;
import org.geotools.data.FileDataStore;
import org.geotools.data.FileDataStoreFinder;
import org.geotools.data.simple.SimpleFeatureSource;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.map.FeatureLayer;
import org.geotools.map.MapContent;
import org.geotools.referencing.crs.DefaultGeographicCRS;
import org.geotools.renderer.GTRenderer;
import org.geotools.renderer.lite.StreamingRenderer;
import org.geotools.styling.Style;
import org.geotools.styling.StyleFactory;
import org.geotools.styling.StyleBuilder;
import javax.imageio.ImageIO;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
@WebServlet("/map")
public class MapServler extends HttpServlet {
private static final String SHAPEFILE_PATH = "/data/ne_110m_admin_0_countries.shp";
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
// 1. Parse CGI-like parameters
int width = Integer.parseInt(request.getParameter("WIDTH"));
int height = Integer.parseInt(request.getParameter("HEIGHT"));
String[] bboxCoords = request.getParameter("BBOX").split(",");
double minX = Double.parseDouble(bboxCoords[0]);
double minY = Double.parseDouble(bboxCoords[1]);
double maxX = Double.parseDouble(bboxCoords[2]);
double maxY = Double.parseDouble(bboxCoords[3]);
String layers = request.getParameter("LAYERS"); // Could be used to select different data sources
ReferencedEnvelope mapArea = new ReferencedEnvelope(minX, maxX, minY, maxY, DefaultGeographicCRS.WGS84);
// 2. Set up the response as a PNG image
response.setContentType("image/png");
try {
// 3. Load the spatial data source (e.g., a Shapefile)
File shapefile = new File(getServletContext().getRealPath(SHAPEFILE_PATH));
FileDataStore store = FileDataStoreFinder.getDataStore(shapefile);
SimpleFeatureSource featureSource = store.getFeatureSource();
// 4. Create a default style for the features
StyleFactory styleFactory = new StyleBuilder();
Style style = styleFactory.createStyle(styleFactory.createPolygonSymbolizer());
// 5. Create a MapContent and add the layer
MapContent mapContent = new MapContent();
mapContent.setTitle("Java MapServer");
mapContent.addLayer(new FeatureLayer(featureSource, style));
// 6. Create a buffered image and its graphics context
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
Graphics2D graphics = image.createGraphics();
// 7. Set up the renderer and paint the map
GTRenderer renderer = new StreamingRenderer();
renderer.setMapContent(mapContent);
renderer.paint(graphics, new Rectangle(width, height), mapArea);
// 8. Write the image to the response output stream
ImageIO.write(image, "png", response.getOutputStream());
// 9. Clean up resources
graphics.dispose();
mapContent.dispose();
store.dispose();
} catch (Exception e) {
// Handle errors more gracefully in production
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
e.printStackTrace();
}
}
}
Key Components Explained
- Parameter Parsing: The servlet's
doGetmethod starts by extracting and parsing standard WMS/CGI parameters likeWIDTH,HEIGHT, andBBOX. - MapContent: This is the GeoTools equivalent of a map file. It holds the layers, the rendering context, and other map metadata.
- FeatureLayer: This connects your data source (e.g., a Shapefile, a PostGIS database) with a
Stylethat defines how it should be drawn. - StreamingRenderer: This is the workhorse that performs the geometric calculations and drawing, translating features in the
BBOXinto pixels on ourBufferedImage. - Image Streaming: Finally, the rendered
BufferedImageis encoded as a PNG and written directly to theHttpServletResponse's output stream.
Advantages of the Java Approach
- Integration: Seamlessly embed mapping into larger Java applications (e.g., add user authentication, logging, business logic).
- Leverage the JVM: Benefit from Java's memory management, performance, and extensive ecosystem.
- Modern Web Frameworks: Easily wrap this functionality in a REST API using Spring Boot, making it consumable by modern JavaScript frameworks like React or Vue.js.
- Data Source Flexibility: GeoTools supports a vast array of data sources beyond Shapefiles, including PostGIS, WFS, and databases.
Moving to Production
This example is simplistic. A production-ready service would need:
- Caching: Aggressively cache rendered tiles to avoid reprocessing identical requests.
- Error Handling: Robust error handling and returning service exceptions as XML, as mandated by the OGC WMS standard.
- Configuration: An external configuration file to define layers, styles, and data sources, similar to a MapServer
.mapfile. - Full OGC Compliance: Implementing the entire WMS specification (GetCapabilities, GetFeatureInfo, etc.) is a significant undertaking but entirely possible.
Conclusion
By leveraging the powerful GeoTools library, you can successfully build a high-performance, integrated map server within the Java ecosystem. While it requires more initial setup than the classic MapServer CGI, it offers unparalleled flexibility and control, making it an excellent choice for complex enterprise geospatial applications.