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;
}
}
}