ddf.catalog.transformer.OverlayMetacardTransformer.java Source code

Java tutorial

Introduction

Here is the source code for ddf.catalog.transformer.OverlayMetacardTransformer.java

Source

/**
 * Copyright (c) Codice Foundation
 *
 * <p>This is free software: you can redistribute it and/or modify it under the terms of the GNU
 * Lesser General Public License as published by the Free Software Foundation, either version 3 of
 * the License, or any later version.
 *
 * <p>This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Lesser General Public License for more details. A copy of the GNU Lesser General Public
 * License is distributed along with this program and can be found at
 * <http://www.gnu.org/licenses/lgpl.html>.
 */
package ddf.catalog.transformer;

import static ddf.catalog.transformer.GeometryUtils.canHandleGeometry;
import static ddf.catalog.transformer.GeometryUtils.parseGeometry;

import com.jhlabs.image.PerspectiveFilter;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Geometry;
import ddf.catalog.data.BinaryContent;
import ddf.catalog.data.Metacard;
import ddf.catalog.data.impl.BinaryContentImpl;
import ddf.catalog.transform.CatalogTransformerException;
import ddf.catalog.transform.MetacardTransformer;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.BiFunction;
import javax.activation.MimeType;
import javax.activation.MimeTypeParseException;
import javax.imageio.ImageIO;
import org.apache.commons.lang.Validate;
import org.la4j.Vector;
import org.la4j.vector.dense.BasicVector;

public class OverlayMetacardTransformer implements MetacardTransformer {
    private static final String PNG = "png";

    private static final MimeType MIME_TYPE;

    static {
        try {
            MIME_TYPE = new MimeType("image", PNG);
        } catch (MimeTypeParseException e) {
            throw new ExceptionInInitializerError(e);
        }
    }

    private final BiFunction<Metacard, Map<String, Serializable>, Optional<BufferedImage>> imageSupplier;

    public OverlayMetacardTransformer(
            BiFunction<Metacard, Map<String, Serializable>, Optional<BufferedImage>> imageSupplier) {
        Validate.notNull(imageSupplier, "The image supplier cannot be null.");
        this.imageSupplier = imageSupplier;
    }

    @Override
    public BinaryContent transform(Metacard metacard, Map<String, Serializable> arguments)
            throws CatalogTransformerException {
        Validate.notNull(metacard, "The metacard cannot be null.");
        return overlay(metacard, arguments);
    }

    private BinaryContent overlay(Metacard metacard, Map<String, Serializable> arguments)
            throws CatalogTransformerException {
        final Optional<BufferedImage> bufferedImageOptional = imageSupplier.apply(metacard, arguments);
        final BufferedImage image = bufferedImageOptional.orElseThrow(
                () -> new CatalogTransformerException("Did not receive an image from the image supplier."));

        List<Vector> boundary = parseBoundary(metacard.getLocation());
        BufferedImage tile = createTileFromImageAndBoundary(image, boundary);
        return createBinaryContent(tile);
    }

    private List<Vector> parseBoundary(String location) throws CatalogTransformerException {
        final Geometry geometry = parseGeometry(location);
        if (!canHandleGeometry(geometry)) {
            throw new CatalogTransformerException("The Image boundary is not a rectangle");
        }

        final Coordinate[] coordinates = geometry.getCoordinates();

        List<Vector> boundary = new ArrayList<>();

        // Using indices rather than for-each because the first coordinate is duplicated.
        for (int i = 0; i < 4; i++) {
            boundary.add(new BasicVector(new double[] { coordinates[i].x, coordinates[i].y }));
        }
        return boundary;
    }

    private BufferedImage createTileFromImageAndBoundary(BufferedImage image, List<Vector> boundary) {
        /*
         * We transform the image by moving the corners and applying
         * transparency so that it looks right when laid down as a north-up
         * rectangular tile.
         */

        // Scaling by latitude so our x and y axes have roughly equal units.
        double lat = boundary.get(0).get(1);
        boundary.replaceAll(v -> scaleByLatitude(v, lat));

        // We are putting the image into a north-up rectangle, so we need
        // to get the minimum rectangle surrounding the boundary.
        List<Vector> boundingBox = calculateBoundingBox(boundary);
        Vector origin = boundingBox.get(0).copy();
        boundary.replaceAll(v -> v.subtract(origin));
        boundingBox.replaceAll(v -> v.subtract(origin));

        // The image may be stretched in weird ways, but we do our best to preserve
        // the resolution by scaling by the width of the image when going from lon/lat
        // to pixel space.
        double scaleFactor = calculateScaleFactor(boundary, image.getWidth());
        boundary.replaceAll(v -> v.multiply(scaleFactor));
        boundingBox.replaceAll(v -> v.multiply(scaleFactor));

        return createImage(image, boundary, (int) boundingBox.get(1).get(0), (int) -boundingBox.get(1).get(1));
    }

    private BinaryContent createBinaryContent(BufferedImage image) throws CatalogTransformerException {
        try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
            ImageIO.write(image, PNG, baos);
            return new BinaryContentImpl(new ByteArrayInputStream(baos.toByteArray()), MIME_TYPE);
        } catch (IOException e) {
            throw new CatalogTransformerException(e);
        }
    }

    private static BufferedImage createImage(BufferedImage original, List<Vector> boundary, int width, int height) {
        BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);

        int[] xs = new int[4];
        int[] ys = new int[4];
        for (int i = 0; i < boundary.size(); i++) {
            xs[i] = (int) boundary.get(i).get(0);
            ys[i] = (int) -boundary.get(i).get(1);
        }

        // We have a rectangular image and we have boundary points that the image should
        // be in. This transform moves the corners of the image to be at those boundary points.
        PerspectiveFilter perspectiveFilter = new PerspectiveFilter();
        perspectiveFilter.setCorners(xs[0], ys[0], xs[1], ys[1], xs[2], ys[2], xs[3], ys[3]);
        img = perspectiveFilter.filter(original, img);

        return img;
    }

    private static double calculateScaleFactor(List<Vector> boundary, int width) {
        return width / boundary.get(1).subtract(boundary.get(0)).euclideanNorm();
    }

    private static Vector scaleByLatitude(Vector v, double lat) {
        double lon = v.get(0) * Math.cos(Math.toRadians(lat));
        return new BasicVector(new double[] { lon, v.get(1) });
    }

    public static List<Vector> calculateBoundingBox(List<Vector> boundary) {
        double maxLon = Collections.max(boundary, Comparator.comparing(v -> v.get(0))).get(0);
        double minLon = Collections.min(boundary, Comparator.comparing(v -> v.get(0))).get(0);
        double maxLat = Collections.max(boundary, Comparator.comparing(v -> v.get(1))).get(1);
        double minLat = Collections.min(boundary, Comparator.comparing(v -> v.get(1))).get(1);

        List<Vector> boundingBox = new ArrayList<>();
        boundingBox.add(new BasicVector(new double[] { minLon, maxLat }));
        boundingBox.add(new BasicVector(new double[] { maxLon, minLat }));

        return boundingBox;
    }
}