/** * This file is hereby placed into the Public Domain. This means anyone is * free to do whatever they wish with this file. */ package mil.nga.giat.process.elasticsearch; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.logging.Logger; import org.geotools.coverage.CoverageFactoryFinder; import org.geotools.coverage.grid.GridCoverage2D; import org.geotools.coverage.grid.GridCoverageFactory; import org.geotools.data.simple.SimpleFeatureCollection; import org.geotools.data.simple.SimpleFeatureIterator; import org.geotools.factory.GeoTools; import org.geotools.geometry.jts.ReferencedEnvelope; import org.geotools.referencing.crs.DefaultGeographicCRS; import org.geotools.util.logging.Logging; import org.opengis.feature.simple.SimpleFeature; import org.opengis.referencing.FactoryException; import org.opengis.referencing.NoSuchAuthorityCodeException; import org.opengis.referencing.operation.TransformException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.github.davidmoten.geo.GeoHash; import com.github.davidmoten.geo.LatLong; import com.vividsolutions.jts.geom.Envelope; public abstract class GeoHashGrid { private final static Logger LOGGER = Logging.getLogger(GeoHashGrid.class); private static final int DEFAULT_PRECISION = 2; public static final String BUCKET_NAME_KEY = "key"; public static final String BUCKETS_KEY = "buckets"; public static final String DOC_COUNT_KEY = "doc_count"; public static final String VALUE_KEY = "value"; private double cellWidth; private double cellHeight; private double lonOffset; private Envelope envelope; private ReferencedEnvelope boundingBox; private List<Map<String, Object>> buckets; private float emptyCellValue; private float[][] grid; private RasterScale scale; public GeoHashGrid() { this.emptyCellValue = 0; this.scale = new RasterScale(); } public GeoHashGrid initalize(ReferencedEnvelope srcEnvelope, SimpleFeatureCollection features) throws NoSuchAuthorityCodeException, TransformException, FactoryException { this.buckets = readFeatures(features); final String firstGeohash = buckets.isEmpty() ? null : (String) buckets.get(0).get("key"); final int precision; if (firstGeohash == null || !isValid(firstGeohash)) { LOGGER.fine("No aggregations found or missing/invalid geohash key"); precision = DEFAULT_PRECISION; } else { precision = ((String) buckets.get(0).get("key")).length(); } cellWidth = GeoHash.widthDegrees(precision); cellHeight = GeoHash.heightDegrees(precision); if (srcEnvelope.getCoordinateReferenceSystem() != null) { srcEnvelope = srcEnvelope.transform(DefaultGeographicCRS.WGS84,false); } computeMinLonOffset(srcEnvelope); envelope = computeEnvelope(srcEnvelope, precision); boundingBox = new ReferencedEnvelope(envelope.getMinX()-cellWidth/2.0, envelope.getMaxX()+cellWidth/2.0, envelope.getMinY()-cellHeight/2.0, envelope.getMaxY()+cellHeight/2.0, DefaultGeographicCRS.WGS84); final int numCol = (int) Math.round((envelope.getMaxX()-envelope.getMinX())/cellWidth+1); final int numRow = (int) Math.round((envelope.getMaxY()-envelope.getMinY())/cellHeight+1); grid = new float[numRow][numCol]; LOGGER.fine("Created grid with size (" + numCol + ", " + numRow + ")"); if (emptyCellValue != 0) { for (float[] row: grid) Arrays.fill(row, emptyCellValue); } List<GridCell> cells = new ArrayList<>(); buckets.stream().forEach(bucket -> { Number rasterValue = computeCellValue(bucket); cells.add(new GridCell((String) bucket.get("key"), rasterValue)); scale.prepareScale(rasterValue.floatValue()); }); cells.stream().forEach(cell -> updateGrid(cell.getGeohash(), cell.getValue())); LOGGER.fine("Read " + cells.size() + " aggregation buckets"); return this; } public abstract Number computeCellValue(Map<String,Object> bucket); protected void updateGrid(String geohash, Number value) { if (geohash != null && value != null) { final LatLong latLon = GeoHash.decodeHash(geohash); final double lat = latLon.getLat(); double lon = latLon.getLon() + lonOffset; if (isValid(lat, lon-360)) updateGrid(lat, lon-360, value); if (isValid(lat, lon)) updateGrid(lat, lon, value); while (isValid(lat, lon+=360)) { updateGrid(lat, lon, value); } } } private void updateGrid(double lat, double lon, Number value) { final int row = grid.length-(int) Math.round((lat-envelope.getMinY())/cellHeight)-1; final int col = (int) Math.round((lon-envelope.getMinX())/cellWidth); grid[Math.min(row,grid.length-1)][Math.min(col,grid[0].length-1)] = scale.scaleValue(value.floatValue()); } public GridCoverage2D toGridCoverage2D() { final GridCoverageFactory coverageFactory = CoverageFactoryFinder.getGridCoverageFactory(GeoTools.getDefaultHints()); return coverageFactory.create("geohashGridAgg", grid, boundingBox); } private List<Map<String, Object>> readFeatures(SimpleFeatureCollection features) { final ObjectMapper mapper = new ObjectMapper(); final List<Map<String, Object>> buckets = new ArrayList<>(); try (SimpleFeatureIterator iterator = features.features()) { while (iterator.hasNext()) { final SimpleFeature feature = iterator.next(); if (feature.getAttribute("_aggregation") != null) { final byte[] data = (byte[]) feature.getAttribute("_aggregation"); try { final Map<String,Object> aggregation = mapper.readValue(data, new TypeReference<Map<String,Object>>() {}); buckets.add(aggregation); } catch (IOException e) { LOGGER.fine("Failed to parse aggregation value: " + e); } } } } return buckets; } private Envelope computeEnvelope(ReferencedEnvelope outEnvelope, int precision) throws NoSuchAuthorityCodeException, TransformException, FactoryException { final String minHash = GeoHash.encodeHash(Math.max(-90,outEnvelope.getMinY()), outEnvelope.getMinX(), precision); final LatLong minLatLon = GeoHash.decodeHash(minHash); final double minLon = minLatLon.getLon() + lonOffset; final double width = Math.ceil(outEnvelope.getWidth()/cellWidth)*cellWidth; final double maxLon = minLon + width - cellWidth; final String maxHash = GeoHash.encodeHash(Math.min(90, outEnvelope.getMaxY()), maxLon, precision); final LatLong maxLatLon = GeoHash.decodeHash(maxHash); return new Envelope(minLon, maxLon, minLatLon.getLat(), maxLatLon.getLat()); } private void computeMinLonOffset(ReferencedEnvelope env) { double minLon; if (env.getMinX() > 180) { minLon = env.getMinX() % 360; } else if (env.getMinX() < -180) { minLon = 360 - Math.abs(env.getMinX()) % 360; } else { minLon = env.getMinX() % 360; } if (minLon > 180) { minLon -= 360; } lonOffset = env.getMinX() - minLon; } private boolean isValid(final double lat, final double lon) { return lon>=envelope.getMinX() && lon<=envelope.getMaxX() && lat>=envelope.getMinY() && lat<=envelope.getMaxY(); } private boolean isValid(String geohash) { return geohash != null && GeoHash.encodeHash(GeoHash.decodeHash(geohash), geohash.length()).equals(geohash); } protected String pluckBucketName(Map<String,Object> bucket) { if (!bucket.containsKey(BUCKET_NAME_KEY)) { LOGGER.warning("Unable to pluck key, bucket does not contain required field:" + BUCKET_NAME_KEY); throw new IllegalArgumentException(); } return bucket.get(BUCKET_NAME_KEY) + ""; } protected Number pluckDocCount(Map<String,Object> bucket) { if (!bucket.containsKey(DOC_COUNT_KEY)) { LOGGER.warning("Unable to pluck document count, bucket does not contain required key:" + DOC_COUNT_KEY); throw new IllegalArgumentException(); } return (Number) bucket.get(DOC_COUNT_KEY); } protected Number pluckMetricValue(Map<String,Object> bucket, String metricKey, String valueKey) { Number value; if (null == metricKey || metricKey.trim().length() == 0) { value = pluckDocCount(bucket); } else { if (!bucket.containsKey(metricKey)) { LOGGER.warning("Unable to pluck metric, bucket does not contain required key:" + metricKey); throw new IllegalArgumentException(); } Map<String,Object> metric = (Map<String,Object>) bucket.get(metricKey); if (!metric.containsKey(valueKey)) { LOGGER.warning("Unable to pluck value, metric does not contain required key:" + valueKey); throw new IllegalArgumentException(); } value = (Number) metric.get(valueKey); } return value; } protected List<Map<String,Object>> pluckAggBuckets(Map<String,Object> parentBucket, String aggKey) { if (!parentBucket.containsKey(aggKey)) { LOGGER.warning("Unable to pluck aggregation results, parent bucket does not contain required key:" + aggKey); throw new IllegalArgumentException(); } Map<String,Object> aggResults = (Map<String,Object>) parentBucket.get(aggKey); if (!aggResults.containsKey(BUCKETS_KEY)) { LOGGER.warning("Unable to pluck buckets, aggregation results bucket does not contain required key:" + BUCKETS_KEY); throw new IllegalArgumentException(); } return (List<Map<String,Object>>) aggResults.get(BUCKETS_KEY); } public void setParams(List<String> params) { //ignore params } public void setEmptyCellValue(Float value) { if (null != value) { this.emptyCellValue = value; } } public double getCellWidth() { return cellWidth; } public double getCellHeight() { return cellHeight; } public Envelope getEnvelope() { return envelope; } public ReferencedEnvelope getBoundingBox() { return boundingBox; } public float[][] getGrid() { return grid; } public void setScale(RasterScale scale) { this.scale = scale; } }