Java geotools implements heat maps and generates tiff files

Foreword

Heatmap is a data visualization technology used to represent the relative density or weight distribution of different areas in the data. It generally displays the popularity of data by using different colors to provide an intuitive understanding of data distribution and trends.

Heat maps are commonly used in the following areas:

  • Data density visualization: Heat maps can display dense and sparse areas in the data set, helping people understand the distribution of data more intuitively. For example, show population density or crime rates in different areas of a city on a map.

  • Heat trend analysis: Heat maps can show the changing trend of data in time or space. By comparing heatmaps over different time periods or locations, you can discover patterns in the evolution and change of your data. For example, it is used to analyze changes in trading activity of different stocks in the stock market over time.

  • Click heat analysis: In web design and user experience, heat maps can be used to display the heat distribution of users clicking, touching or browsing on the page. This helps optimize page layout and content, improve user experience and user behavior analysis.

  • Event and behavior analysis: Heat maps can be used to analyze the behavior patterns and interaction heat of the crowd. For example, in market research, you can learn about consumers’ interests and preferences by observing how they move around and stay in a store.

geoServer heat map

Use

geoServer provides WPS service for heat maps. In geoServer’s WPS request builder, select the “gs:Heatmap” process and enter the parameters to execute.

Parameter meaning

Related parameter meanings:

  • Input features: input feature collection.

  • radiusPixels: The radius used to define the influence range of each data point in the heat map, in pixels. A larger radiusPixel will cause a wider range of pixels to be affected, and the heat map will show a smoother transition, but some detail information may also be lost.

  • weightAttr: weight field, if not, a heat map will be generated based on the location of the data.

  • pixelsPerCell: Define the resolution of the heat map, how many pixels each cell is long and wide. A smaller pixelsPerCell will cause more detailed information to be retained, but will also increase the complexity and calculation amount of the heat map.

  • outputBBOX: The geographical range of the output heat map. If the coordinate system of the BBOX is inconsistent with the data source coordinate system, coordinate conversion will be performed.

  • outputWidth: output image width.

  • outputHeight: output image height.

Code implementation

There is currently a requirement for the front-end to pass in geoJSON data, and the back-end generates geotiff images based on this data and returns it to the front-end. After investigation, we learned that the WPS service of geoServer’s HeatMap is actually executed by org.geotools.process.vector.HeatMapProcess.

So there are three main steps to implement this interface:

1. Parse geoJSON data to construct request parameters.

2. Execute HeatMapProcess to generate raster images.

3. Return the image to the front end in tiff format.

Get geoJSON data from the request stream

The front end will put geoJSON into the request body, and only needs to read the data in the body of HttpServletRequest.

public static String getPostData(HttpServletRequest request) {<!-- -->
StringBuilder data = new StringBuilder();
String line;
BufferedReader reader;
try {<!-- -->
reader = request.getReader();
while (null != (line = reader.readLine())) {<!-- -->
data.append(line);
}
} catch (IOException e) {<!-- -->
return null;
}
return data.toString();
}

Parse geoJSON into SimpleFeatureCollection

FeatureJSON featureJSON = new FeatureJSON();
JSONParser jsonParser = new JSONParser();
JSONObject json = (JSONObject)
//The inputHeatMapFeatures here is the string obtained by parsing the request body
jsonParser.parse(inputHeatMapFeatures);
SimpleFeatureCollection featureCollection = null;
featureCollection = (SimpleFeatureCollection) featureJSON.readFeatureCollection(json.toJSONString());
SimpleFeatureType featureType = featureCollection.getSchema();
// Get SRID
String srs = CRS.lookupIdentifier(featureType.getCoordinateReferenceSystem(),true);
//The order of coordinates parsed by FeatureJSON may be reversed. For example, EPSG:4326 will reverse the longitude and latitude, resulting in an error in the generated image, so another coordinate conversion is required.
featureCollection = new ForceCoordinateSystemFeatureResults(featureCollection, CRS.decode(srs, true));

There is a pitfall here, that is, the coordinate order parsed by FeatureJSON is longitude first, latitude last, while the reading order of geotools is latitude first, longitude last. This will cause the longitude and latitude of the generated image to be reversed, so another coordinate transformation is required. .

Construct request scope

The request range here is the range of geoJSON, and all are converted to the WGS 84 coordinate system.

 /**
     * @Description Get the boundary range of SimpleFeatureCollection
     * @param featureCollection: feature collection
     * @return org.geotools.geometry.jts.ReferencedEnvelope
     **/
    public static ReferencedEnvelope getReferencedEnvelopeOfFeatureCollection(SimpleFeatureCollection featureCollection) throws FactoryException, TransformException {<!-- -->
        // Get the coordinate reference system of FeatureCollection
        CoordinateReferenceSystem crs = featureCollection.getSchema().getCoordinateReferenceSystem();
        //Initialize an initial range
        Envelope envelope = new Envelope();
        // Traverse each Feature in FeatureCollection
        try (SimpleFeatureIterator it = featureCollection.features()) {<!-- -->
            while (it.hasNext()) {<!-- -->
                SimpleFeature feature = it.next();
                Geometry geometry = (Geometry) feature.getDefaultGeometry();

                // Merge the boundary range of the geometry of each Feature
                envelope.expandToInclude(geometry.getEnvelopeInternal());
            }
        }
        //Convert the coordinate system of the output range to WGS 84
        CoordinateReferenceSystem targetCRS;
        Envelope targetEnvelope;
        if(("EPSG:WGS 84").equals(crs.getName().toString())){<!-- -->
            return new ReferencedEnvelope(envelope, crs);
        }else{<!-- -->
            targetCRS = CRS.decode("EPSG:4326");
            targetEnvelope = convertEnvelope(envelope, crs, targetCRS);
        }
        // Convert bounding range to referenced range
        return new ReferencedEnvelope(targetEnvelope, targetCRS);
    }

    /**
     * @Description Convert Envelope to target coordinate system
     * @param sourceEnvelope: Envelope to be converted
     * @param sourceCRS: source coordinate system
     * @param targetCRS: target coordinate system
     * @return org.locationtech.jts.geom.Envelope
     **/
    public static Envelope convertEnvelope(Envelope sourceEnvelope, CoordinateReferenceSystem sourceCRS, CoordinateReferenceSystem targetCRS) throws FactoryException, TransformException {<!-- -->
        Envelope targetEnvelope = null;
        return JTS.transform(sourceEnvelope, targetEnvelope, CRS.findMathTransform(sourceCRS, targetCRS), 0);
    }

Construct HeatMapProcess and execute the execute method

HeatmapProcess heatmapProcess = new HeatmapProcess();
GridCoverage2D coverage = heatmapProcess.execute(featureCollection, radiusPixels, weightAttr, pixelPerCell, referencedEnvelope, outputWidth, outputHeight, null);

Output the results as a geotiff file

 /**
     * @param radiusPixels Radius of the density kernel in pixels Kernel density radius (unit: pixels)
     * @param pixelPerCell Resolution at which to compute the heatmap (in pixels) Heatmap resolution, how many pixels per pixel
     * @param outputHeight the height of the output tiff image
     * @param outputWidth The width of the output tiff image
     * @param servletRequest body passes geoJSON parameters
     * @param weightAttr weight field
     * @param response
     * @throwsIOException
     * @Description Pass in geojson elements, generate heat map, and return tiff image
     */
    @PostMapping(value = "/heatMap")
    public void heatMap(@RequestParam(defaultValue = "5", required = false) int radiusPixels,
                        @RequestParam(defaultValue = "1", required = false) int pixelPerCell,
                        @RequestParam(defaultValue = "100", required = false) int outputHeight,
                        @RequestParam(defaultValue = "100", required = false) int outputWidth,
                        @RequestParam(defaultValue = "value", required = false) String weightAttr,
                        HttpServletRequest servletRequest,
                        HttpServletResponse response) throws Exception {<!-- -->
        String inputHeatMapFeatures = HttpUtil.getPostData(servletRequest);
        GridCoverage2D coverage2D = HeatMap.localHeatMapExecute(radiusPixels, pixelPerCell, outputHeight, outputWidth, inputHeatMapFeatures, weightAttr);

        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        GeoTiffWriter writer = new GeoTiffWriter(byteArrayOutputStream);
        writer.write(coverage2D, null);
        writer.dispose();

        //output geotiff
        response.setContentType("image/tiff");
        ServletOutputStream outputStream = response.getOutputStream();
        byteArrayOutputStream.writeTo(outputStream);
        outputStream.flush();
        outputStream.close();
        byteArrayOutputStream.close();
    }

This interface finally generates tiff images. HeatMapProcess is a class in geotools, sourced from the gt-process package:

 <dependency>
      <groupId>org.geotools</groupId>
      <artifactId>gt-process-raster</artifactId>
      <version>${gt.version}</version>
    </dependency>