Commit 9a1f70fc authored by Matija Obreza's avatar Matija Obreza
Browse files

Leaflet.js and Genesys tileserver

parent 27ca95c4
......@@ -23,8 +23,10 @@ import org.genesys2.server.model.genesys.Accession;
import org.genesys2.server.service.impl.GenesysFilterServiceImpl.LabelValue;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.jdbc.core.RowCallbackHandler;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
public interface GenesysFilterService {
......@@ -53,4 +55,8 @@ public interface GenesysFilterService {
Collection<GenesysFilter> generateFilters(Collection<Long> methodIds);
List<LabelValue<String>> autocomplete(String filter, String ac);
void listGeo(ObjectNode jsonTree, RowCallbackHandler rowHandler);
void listGeoTile(boolean distinct, ObjectNode jsonTree, int zoom, int x, int y, RowCallbackHandler rowCallbackHandler);
}
......@@ -115,4 +115,6 @@ public interface GeoService {
long countActive();
String filteredKml(String jsonFilter);
}
/**
* 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.genesys2.server.service;
public interface MappingService {
String filteredKml(String jsonFilter);
byte[] getTile(String jsonFilter, int zoom, int xtile, int ytile);
public static class CoordUtil {
public static double tileToLon(int zoom, int xtile) {
return Math.floor(((double) xtile / (1 << zoom) * 360.0) - 180.0);
}
public static double tileToLat(int zoom, int ytile) {
double n = Math.PI - (2.0 * Math.PI * ytile) / (1 << zoom);
return Math.toDegrees(Math.atan(Math.sinh(n)));
}
public static int tileToLon1(int zoom, int xtile) {
// n = 2 ^ zoom
double n = 1 << zoom;
// lon_deg = xtile / n * 360.0 - 180.0
double lon = xtile / n * 360.0 - 180.0;
return (int) Math.floor(lon);
}
public static int tileToLat1(int zoom, int ytile) {
// n = 2 ^ zoom
double n = 1 << zoom;
// lat_rad = arctan(sinh(π * (1 - 2 * ytile / n)))
double latr = Math.atan(Math.sinh(Math.PI * (1.0d - 2.0d * ytile / n)));
// lat_deg = lat_rad * 180.0 / π
double lat = latr * 180.0 / Math.PI;
return (int) Math.floor(lat);
}
public static int lonToTile(int zoom, double lon) {
return (int) Math.floor((lon + 180.0) / 360.0 * (1 << zoom));
}
public static int latToTile(int zoom, double lat) {
double latr = Math.toRadians(lat);
return (int) Math.floor((1 - Math.log(Math.tan(latr) + 1 / Math.cos(latr)) / Math.PI) / 2 * (1 << zoom));
}
public static int lonToImg(int zoom, double lon) {
// n = 2 ^ zoom
int xtile = lonToTile(zoom, lon);
double a = tileToLon(zoom, xtile);
double b = tileToLon(zoom, xtile + 1);
// System.err.println("a=" + a + " b=" + b);
// Translate to start of tile
lon -= a;
// Scale by (ba)*256;
// System.err.println("b-a="+(b-a));
lon = 256.0 * lon / (b - a);
return (int) Math.floor(lon);
}
public static int latToImg(int zoom, double lat) {
// n = 2 ^ zoom
int ytile = latToTile(zoom, lat);
// System.err.println("ytile=" + ytile);
double a = tileToLat(zoom, ytile);
double b = tileToLat(zoom, ytile + 1);
// System.err.println("a=" + a + " b=" + b);
// Translate to start of tile
lat -= a;
// Scale by (ba)*256;
// System.err.println("b-a=" + (b - a));
lat = 256.0 * lat / (b - a);
return (int) Math.floor(lat);
}
public static int latToImg3(int zoom, int tile, double lat) {
double latr = Math.toRadians(lat);
double pixAtZoom = (1 << zoom) * 256;
double y1 = Math.floor((1 - Math.log(Math.tan(latr) + 1 / Math.cos(latr)) / Math.PI) / 2 * pixAtZoom);
return (int) y1 - tile * 256;
}
public static int lonToImg3(int zoom, int tile, double lng) {
double pixAtZoom = (1 << zoom) * 256;
return (int) ((Math.floor(((180.0 + lng) / 360.0) * pixAtZoom)) - tile * 256);
}
}
}
......@@ -16,6 +16,9 @@
package org.genesys2.server.service.impl;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
......@@ -43,6 +46,7 @@ import org.genesys2.server.persistence.domain.MethodRepository;
import org.genesys2.server.persistence.domain.TraitValueRepository;
import org.genesys2.server.service.GenesysFilterService;
import org.genesys2.server.service.GenesysFilterService.GenesysFilter.DataType;
import org.genesys2.server.service.MappingService.CoordUtil;
import org.genesys2.server.service.InstituteService;
import org.genesys2.server.service.SearchService;
import org.genesys2.server.service.TaxonomyService;
......@@ -53,12 +57,16 @@ import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Order;
import org.springframework.jdbc.core.ArgumentPreparedStatementSetter;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.PreparedStatementCreator;
import org.springframework.jdbc.core.RowCallbackHandler;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.JsonNodeType;
import com.fasterxml.jackson.databind.node.ObjectNode;
@Service
@Transactional(readOnly = true)
......@@ -655,4 +663,104 @@ public class GenesysFilterServiceImpl implements GenesysFilterService {
return autocompleteUrl;
}
}
@Override
public void listGeo(ObjectNode jsonTree, RowCallbackHandler rowHandler) {
listGeoTile(false, jsonTree, -1, 0, 0, rowHandler);
}
@Override
public void listGeoTile(final boolean distinct, ObjectNode jsonTree, int zoom, int xtile, int ytile, RowCallbackHandler rowHandler) {
Iterator<Entry<String, JsonNode>> fields = jsonTree.fields();
while (fields.hasNext()) {
Entry<String, JsonNode> entry = fields.next();
LOG.debug("Looking at " + entry.getKey() + " = " + entry.getValue());
}
final List<Object> params = new ArrayList<Object>();
final StringBuffer sb = new StringBuffer();
sb.append(" from accessiongeo geo inner join accession a on a.id=geo.accessionId ");
if (jsonTree.has("crop") || jsonTree.has("genus") || jsonTree.has("taxon")) {
sb.append(" inner join taxonomy t on t.id=a.taxonomyId ");
if (jsonTree.has("crop")) {
sb.append(" inner join croptaxonomy ct on ct.taxonomyId=t.id inner join crop on crop.id=ct.cropId ");
}
}
if (jsonTree.has("organization")) {
sb.append(" inner join faoinstitute fao on fao.id=a.instituteId inner join organizationinstitute oi on oi.instituteId=fao.id inner join organization org on org.id=oi.organizationId ");
}
StringBuffer sbf = new StringBuffer();
if (zoom >= 0) {
LOG.debug("ZOOM=" + zoom);
final double lonW = CoordUtil.tileToLon(zoom, xtile);
final double lonE = CoordUtil.tileToLon(zoom, xtile + 1);
final double diffLon = lonE - lonW;
final double latN = CoordUtil.tileToLat(zoom, ytile);
final double latS = CoordUtil.tileToLat(zoom, ytile + 1);
final double diffLat = latN - latS;
sbf.append(" where ((geo.longitude between ? and ?) and (geo.latitude between ? and ?)) ");
params.add(lonW - zoom * diffLon * .2);
params.add(lonE + zoom * diffLon * .2);
params.add(latS - zoom * diffLat * .2);
params.add(latN + zoom * diffLat * .2);
if (LOG.isDebugEnabled()) {
LOG.debug(lonW + " <= lon <= " + lonE + " corr=" + diffLon * .2);
LOG.debug(latS + " <= lat <= " + latN + " corr=" + diffLat * .2);
}
}
createQuery(sbf, "a.acceNumb", jsonTree.get("accenumb"), params);
createQuery(sbf, "a.orgCty", jsonTree.get("origin"), params);
createQuery(sbf, "a.instCode", jsonTree.get("institute"), params);
createQuery(sbf, "a.inSGSV", jsonTree.get("inSvalbard"), params);
createQuery(sbf, "a.mlsStat", jsonTree.get("mls"), params);
createQuery(sbf, "a.inTrust", jsonTree.get("inTrust"), params);
createQuery(sbf, "a.available", jsonTree.get("available"), params);
createQuery(sbf, "org.slug", jsonTree.get("organization"), params);
createQuery(sbf, "t.genus", jsonTree.get("genus"), params);
createQuery(sbf, "t.taxonName", jsonTree.get("taxon"), params);
createQuery(sbf, "geo.longitude", jsonTree.get("lon"), params);
createQuery(sbf, "geo.latitude", jsonTree.get("lat"), params);
createQuery(sbf, "geo.elevation", jsonTree.get("elevation"), params);
createQuery(sbf, "crop.shortName", jsonTree.get("crop"), params);
for (Iterator<String> it = jsonTree.fieldNames(); it.hasNext();) {
String fieldName = it.next();
if (fieldName.startsWith("gm:")) {
LOG.info("Filtering on C&E data, method=" + fieldName);
// Handle Genesys Method!
long methodId = Long.parseLong(fieldName.substring(3));
// Need to validate method
Method method = traitService.getMethod(methodId);
if (method != null) {
String alias = "gm" + methodId;
sb.append(" inner join `").append(methodId).append("` ").append(alias).append(" on ");
sb.append(alias).append(".accessionId=a.id");
createQuery(sbf, alias + ".`" + method.getFieldName() + "`", jsonTree.get(fieldName), params);
} else {
LOG.warn("No such method with id=" + methodId);
}
}
}
sb.append(sbf.toString());
LOG.info("Parameter count: " + params.size());
LOG.info("Count query:\n" + sb.toString());
this.jdbcTemplate.query(new PreparedStatementCreator() {
@Override
public PreparedStatement createPreparedStatement(Connection con) throws SQLException {
PreparedStatement stmt = con.prepareStatement("select "
+ (distinct ? "distinct geo.longitude, geo.latitude"
: "a.id, a.acceNumb, a.instCode, geo.longitude, geo.latitude, geo.datum, geo.uncertainty ") + sb.toString());
// Set mysql JConnector to stream results
stmt.setFetchSize(Integer.MIN_VALUE);
new ArgumentPreparedStatementSetter(params.toArray()).setValues(stmt);
return stmt;
}
}, rowHandler);
};
}
......@@ -139,7 +139,7 @@ public class GeoServiceImpl implements GeoService {
private Country findCountryByName(String name) {
ObjectMapper mapper = new ObjectMapper();
List<Country> countries = countryRepository.findWithI18N("%" + name.trim() + "%");
System.err.println("Found " + countries.size() + " that have " + name);
LOG.debug("Found " + countries.size() + " that have " + name);
for (Country c : countries) {
try {
JsonNode nameJ = mapper.readTree(c.getNameL());
......@@ -401,4 +401,9 @@ public class GeoServiceImpl implements GeoService {
return itpgrfaRepository.save(itpgrfaStatus);
}
@Override
public String filteredKml(String jsonFilter) {
return null;
}
}
/**
* 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.genesys2.server.service.impl;
import java.awt.Color;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.sql.ResultSet;
import java.sql.SQLException;
import javax.imageio.ImageIO;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.genesys2.server.service.GenesysFilterService;
import org.genesys2.server.service.MappingService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.RowCallbackHandler;
import org.springframework.stereotype.Service;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
@Service
public class MappingServiceImpl implements MappingService {
// 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 Log LOG = LogFactory.getLog(MappingServiceImpl.class);
@Autowired
private GenesysFilterService filterService;
private ObjectMapper mapper = new ObjectMapper();
@Override
public String filteredKml(String jsonFilter) {
try {
ObjectNode jsonTree = (ObjectNode) mapper.readTree(jsonFilter);
LOG.debug(jsonTree.toString());
final StringBuilder sb = new StringBuilder();
sb.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
sb.append("<kml xmlns=\"http://www.opengis.net/kml/2.2\">");
sb.append("<Document>");
filterService.listGeo(jsonTree, new RowCallbackHandler() {
@Override
public void processRow(ResultSet rs) throws SQLException {
try {
sb.append("<Placemark>");
sb.append("<name>").append(rs.getString("acceNumb")).append("</name>");
sb.append("<description></description>");
sb.append("<Point><coordinates>");
sb.append(rs.getDouble("longitude")).append(",").append(rs.getDouble("latitude"));
sb.append("</coordinates></Point>");
sb.append("</Placemark>");
} catch (SQLException e) {
LOG.warn(e.getMessage());
throw e;
}
}
});
sb.append("</Document>");
sb.append("</kml>");
return sb.toString();
} catch (IOException e) {
LOG.warn(e.getMessage(), e);
throw new RuntimeException(e);
}
}
// TODO Add caching
@Override
// @Cacheable(value="tileserver",
// key="'tile-' + #zoom + '-' + #xtile + '-' + #ytile + '-' + #jsonFilter")
public byte[] getTile(String jsonFilter, final int zoom, final int xtile, final int ytile) {
final BufferedImage bi = new BufferedImage(256, 256, BufferedImage.TYPE_INT_ARGB);
ObjectNode jsonTree;
try {
jsonTree = (ObjectNode) mapper.readTree(jsonFilter);
} catch (IOException e1) {
throw new RuntimeException("Error parsing jsonFilter: " + e1.getMessage());
}
// final int pixelSize = (int) Math.round(1.0 + (1 << (zoom / 2))) / 2;
final int[] pixelSizes = new int[] { 0, 0, 0, 1, 1, 2, 2, 2, 3, 2, 2, 3 };
final int pixelSize = pixelSizes[(zoom > pixelSizes.length ? pixelSizes.length : zoom)];
if (LOG.isDebugEnabled()) {
LOG.debug(jsonTree.toString());
LOG.debug("PIXELSIZE=" + pixelSize + " zoom=" + zoom);
}
// // Border
// for (int i = 0; i < 256; i++) {
// bi.setRGB(0, i, Color.red.getRGB());
// bi.setRGB(i, 0, Color.red.getRGB());
// bi.setRGB(255, i, Color.red.getRGB());
// bi.setRGB(i, 255, Color.red.getRGB());
// }
filterService.listGeoTile(true, jsonTree, zoom, xtile, ytile, new RowCallbackHandler() {
@Override
public void processRow(ResultSet rs) throws SQLException {
try {
// System.err.println("Adding dot " +
// rs.getDouble("longitude") + " " +
// rs.getDouble("latitude"));
int longitude = CoordUtil.lonToImg3(zoom, xtile, rs.getDouble("longitude"));
int latitude = CoordUtil.latToImg3(zoom, ytile, rs.getDouble("latitude"));
// System.err.println("Dotting " + longitude + "," +
// latitude);
for (int i = -pixelSize / 2; i <= pixelSize / 2; i++) {
for (int j = -pixelSize / 2; j <= pixelSize / 2; j++) {
if (longitude + i >= 0 && latitude + j >= 0 && longitude + i < 256 && latitude + j < 256)
bi.setRGB(longitude + i, latitude + j, Color.yellow.getRGB());
}
}
} catch (SQLException e) {
LOG.warn(e.getMessage());
throw e;
}
}
});
// for (int j = -80; j < 81; j += 10) {
// int latitude = CoordUtil.latToImg3(zoom, ytile, j);
// if (latitude >= 0 && latitude < 256)
// for (int i = 0; i < 256; i++) {
// bi.setRGB(i, latitude, Color.blue.getRGB());
// }
// }
//
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
// if (zoom < -20) {
// ImageIO.write(op.filter(bi, null), "png", baos);
// } else {
ImageIO.write(bi, "png", baos);
// }
return baos.toByteArray();
} catch (IOException e) {
LOG.warn(e);
throw new RuntimeException("Could not render image", e);
}
}
}
......@@ -32,6 +32,7 @@ import org.genesys2.server.service.CropService;
import org.genesys2.server.service.GenesysFilterService;
import org.genesys2.server.service.GenesysService;
import org.genesys2.server.service.InstituteService;
import org.genesys2.server.service.MappingService;
import org.genesys2.server.service.TaxonomyService;
import org.genesys2.server.service.TraitService;
import org.genesys2.server.service.impl.GenesysFilterServiceImpl.LabelValue;
......@@ -76,6 +77,9 @@ public class ExplorerController extends BaseController {
@Autowired
private TaxonomyService taxonomyService;
@Autowired
private MappingService mappingService;
private ObjectMapper mapper = new ObjectMapper();
/**
......@@ -329,4 +333,28 @@ public class ExplorerController extends BaseController {
public List<LabelValue<String>> autocomplete(@PathVariable("field") String filter, @RequestParam(value = "term", required = false) String ac) {
return filterService.autocomplete(filter, ac);
}
@RequestMapping(value = "/explore/map", method = RequestMethod.GET)
public String map(ModelMap model, @RequestParam(value = "filter", required = true) String jsonFilter) {
model.addAttribute("jsonFilter", jsonFilter);
return "/accession/map";
}
@RequestMapping(value = "/explore/kml", produces = "application/vnd.google-earth.kml+xml")
@ResponseBody
public String kml(@RequestParam(value = "filter", required = true) String jsonFilter) {
return mappingService.filteredKml(jsonFilter);
}
@RequestMapping(value = "/explore/tile/{zoom}/{x}/{y}", produces = MediaType.IMAGE_PNG_VALUE)
@ResponseBody
public byte[] tile(@RequestParam(value = "filter", required = true) String jsonFilter, @PathVariable("zoom") int zoom, @PathVariable("x") int x,
@PathVariable("y") int y) {
try {
return mappingService.getTile(jsonFilter, zoom, x, y);
} catch (Throwable e) {
_logger.error(e.getMessage(), e);
throw new ResourceNotFoundException(e.getMessage());
}
}
}
......@@ -166,13 +166,13 @@ public class DatasetController extends RestController {
// Check for 'WRITE' permission
datasetService.touch(metadata);
System.err.println(data);
LOG.debug(data);
ObjectMapper objectMapper = new ObjectMapper();
JsonNode json = null;
try {
json = objectMapper.readTree(data);
System.err.println(json);
LOG.debug(json);
} catch (IOException e) {
LOG.error(e);
throw e;
......
......@@ -463,3 +463,5 @@ autocomplete.genus=Find Genus...
stats.number-of-countries={0} Countries
stats.number-of-institutes={0} Institutes
stats.number-of-accessions={0} Accessions
navigate.back=Back
\ No newline at end of file
......@@ -53,7 +53,7 @@
</props>
</constructor-arg>
</bean>
<beans profile="aws">
<hz:hazelcast id="theHazelcast">
<hz:config>
......@@ -70,12 +70,14 @@
<hz:join>
<hz:multicast enabled="false" />
<hz:tcp-ip connection-timeout-seconds="20" enabled="false" />
<hz:aws connection-timeout-seconds="5" enabled="true" access-key="${hazelcast.aws.access-key}" region="${hazelcast.aws.region}"
<hz:aws connection-timeout-seconds="5" enabled="true"
access-key="${hazelcast.aws.access-key}" region="${hazelcast.aws.region}"
secret-key="${hazelcast.aws.secret-key}" security-group-name="${hazelcast.aws.security-group}" />
</hz:join>
</hz:network>