MappingServiceImpl.java

/**
 * Copyright 2014 Global Crop Diversity Trust
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 **/

package org.genesys.server.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.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

import javax.imageio.ImageIO;

import org.apache.commons.lang3.time.StopWatch;
import org.genesys.server.exception.SearchException;
import org.genesys.server.service.GenesysFilterService;
import org.genesys.server.service.MappingService;
import org.genesys.server.service.filter.AccessionFilter;
import org.genesys.util.CoordUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;

@Service
public class MappingServiceImpl implements MappingService, InitializingBean {

	// A 3x3 kernel that blurs an image
	// Kernel kernel = new Kernel(3, 3, new float[] { 0.05f, 0.1f, 0.05f, 0.05f,
	// 0.5f, 0.05f, 0.05f, 0.01f, 0.05f });
	// Copied from wikipedia
	// private final Kernel kernel = new Kernel(7, 7, new float[] { 0.00000067f,
	// 0.00002292f, 0.00019117f, 0.00038771f, 0.00019117f, 0.00002292f,
	// 0.00000067f,
	// 0.00002292f, 0.00078634f, 0.00655965f, 0.01330373f, 0.00655965f,
	// 0.00078633f, 0.00002292f, 0.00019117f, 0.00655965f, 0.05472157f,
	// 0.11098164f,
	// 0.05472157f, 0.00655965f, 0.00019117f, 0.00038771f, 0.01330373f,
	// 0.11098164f, 0.22508352f, 0.11098164f, 0.01330373f, 0.00038771f,
	// 0.00019117f,
	// 0.00655965f, 0.05472157f, 0.11098164f, 0.05472157f, 0.00655965f,
	// 0.00019117f, 0.00002292f, 0.00078633f, 0.00655965f, 0.01330373f,
	// 0.00655965f,
	// 0.00078633f, 0.00002292f, 0.00000067f, 0.00002292f, 0.00019117f,
	// 0.00038771f, 0.00019117f, 0.00002292f, 0.00000067f });
	// private final BufferedImageOp op = new ConvolveOp(kernel);

	private static final Logger LOG = LoggerFactory.getLogger(MappingServiceImpl.class);

	@Autowired
	private GenesysFilterService filterService;

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

	@PreAuthorize("hasRole('ADMINISTRATOR')")
	@Override
	@CacheEvict(value = "tileserver", allEntries = true)
	public void clearCache() {
		LOG.warn("Cleared tiles cache");
	}

	@Override
	@Cacheable(value = "tileserver", key = "'tile-' + #zoom + '-' + #xtile + '-' + #ytile + '-' + #filters")
	public byte[] getTile(AccessionFilter filters, int zoom, int xtile, int ytile) throws IOException {
		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;
		
		StopWatch stopWatch = StopWatch.createStarted();
		List<Double[]> geoTiles = null;
		try {
			geoTiles = filterService.listGeoTile(filters, null, zoom, xtile, ytile);
		} catch (SearchException e) {
			LOG.error("Error occurred during search", e);
			geoTiles = Collections.emptyList();
		}
        stopWatch.split();
        LOG.debug("Got {} geo in {}", geoTiles.size(), stopWatch.toSplitString());

		AtomicInteger paints = new AtomicInteger(0);
		AtomicInteger outsidesX = new AtomicInteger(0);
		AtomicInteger outsidesY = new AtomicInteger(0);
		geoTiles.parallelStream().map(item -> new TilePos(zoom, xtile, ytile, item)).distinct().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();
		}
	}

	@Override
	public void afterPropertiesSet() throws Exception {
		for (int i = 0; i < 20; 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());
			}
		}
	}
	
	/**
	 * Allows us to generate distinct dot locations
	 */
	private static class TilePos {

		private int longitude;
		private int latitude;

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

		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result + latitude;
			result = prime * result + longitude;
			return result;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			TilePos other = (TilePos) obj;
			if (latitude != other.latitude)
				return false;
			if (longitude != other.longitude)
				return false;
			return true;
		}
	}
}