TileMaker.java

package org.gringlobal.service.impl;

import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
import java.util.stream.Stream;

import javax.imageio.ImageIO;

import org.apache.commons.lang3.time.StopWatch;
import org.gringlobal.util.CoordUtil;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Component;

import lombok.Data;
import lombok.extern.slf4j.Slf4j;

/**
 * Handles accession tile generation for maps.
 */
@Component
@Slf4j
public class TileMaker implements InitializingBean {

	private List<BufferedImage> zoomTemplates = new ArrayList<>();

	@Override
	public void afterPropertiesSet() throws Exception {
		for (int i = 0; i < 15; i++) {
			String accessionDotSource = "/tileserver/accessionDot" + i + ".png";
			try (InputStream sourceStream = this.getClass().getResourceAsStream(accessionDotSource)) {
				BufferedImage accessionAtZoom = ImageIO.read(sourceStream);
				zoomTemplates.add(accessionAtZoom);
			} catch (Throwable e) {
				log.warn("Could not read accession time template {}: {}", accessionDotSource, e.getMessage());
			}
		}
	}

	/**
	 * Generate a tile from source
	 * @param zoom
	 * @param xtile
	 * @param ytile
	 * @param coordinateSource Make sure coordinates are ordered by lat,lon
	 * @return
	 */
	public byte[] makeTile(int zoom, int xtile, int ytile, Supplier<Stream<Double[]>> coordinateSource) {
		StopWatch stopWatch = StopWatch.createStarted();

		final BufferedImage bufferedImage = new BufferedImage(256, 256, BufferedImage.TYPE_INT_ARGB);
		Graphics2D g2d = (Graphics2D) bufferedImage.getGraphics();

		BufferedImage accessionDot = zoomTemplates.get(zoomTemplates.size() > zoom ? zoom : zoomTemplates.size() - 1);
		int dotHalfW = accessionDot.getWidth() / 2;
		int dotHalfH = accessionDot.getHeight() / 2;

		AtomicInteger paints = new AtomicInteger(0);
		AtomicInteger outsidesX = new AtomicInteger(0);
		AtomicInteger outsidesY = new AtomicInteger(0);
		coordinateSource.get().map(item -> new TilePos(zoom, xtile, ytile, item)).forEach(item -> {

			// calculates the coordinate where the image is painted
			int topLeftX = item.longitude - dotHalfW;
			int topLeftY = item.latitude - dotHalfH;

			if (log.isDebugEnabled()) {
				paints.incrementAndGet();
				if (topLeftX < -dotHalfW || topLeftX > 255 + dotHalfW) {
					log.trace("Longitude {},{} outside 0 - 255", item.longitude, item.latitude);
					outsidesX.incrementAndGet();
				}
				if (topLeftY < -dotHalfH || topLeftY > 255 + dotHalfH) {
					log.trace("Latitude {},{} outside 0 - 255", item.longitude, item.latitude);
					outsidesY.incrementAndGet();
				}
			}

			// paints the image watermark
			g2d.drawImage(accessionDot, topLeftX, topLeftY, null);
		});

		stopWatch.split();
		log.debug("Painted outX={} outY={} {} in {}", outsidesX.get(), outsidesY.get(), paints.get(), stopWatch.toSplitString());

		try {
			final ByteArrayOutputStream baos = new ByteArrayOutputStream();
			ImageIO.write(bufferedImage, "png", baos);
			return baos.toByteArray();
		} catch (final IOException e) {
			log.warn(e.getMessage(), e);
			throw new RuntimeException("Couldn't render image", e);
		} finally {
			g2d.dispose();
		}
	}

	/**
	 * Allows us to generate distinct dot locations
	 */
	@Data
	private static class TilePos {

		private int longitude;
		private int latitude;

		public TilePos(int zoom, int xtile, int ytile, Double[] item) {
			this.latitude = CoordUtil.latToImg3(zoom, ytile, item[0]);
			this.longitude = CoordUtil.lonToImg3(zoom, xtile, item[1]);
		}
	}
}