/******************************************************************************* * Copyright (c) 2014 Open Door Logistics (www.opendoorlogistics.com) * All rights reserved. This program and the accompanying materials * are made available under the terms of the GNU Lesser Public License v3 * which accompanies this distribution, and is available at http://www.gnu.org/licenses/lgpl.txt ******************************************************************************/ package com.opendoorlogistics.core.scripts.formulae; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import org.apache.commons.collections.BinaryHeap; import org.geotools.geometry.jts.JTS; import org.opengis.referencing.operation.MathTransform; import com.opendoorlogistics.api.ExecutionReport; import com.opendoorlogistics.api.geometry.LatLong; import com.opendoorlogistics.api.geometry.ODLGeom; import com.opendoorlogistics.api.tables.ODLColumnType; import com.opendoorlogistics.api.tables.ODLTableReadOnly; import com.opendoorlogistics.core.cache.ApplicationCache; import com.opendoorlogistics.core.cache.RecentlyUsedCache; import com.opendoorlogistics.core.formulae.Function; import com.opendoorlogistics.core.formulae.FunctionFactory; import com.opendoorlogistics.core.formulae.FunctionImpl; import com.opendoorlogistics.core.formulae.FunctionParameters; import com.opendoorlogistics.core.formulae.FunctionUtils; import com.opendoorlogistics.core.formulae.Functions; import com.opendoorlogistics.core.formulae.definitions.FunctionDefinition; import com.opendoorlogistics.core.formulae.definitions.FunctionDefinition.ArgumentType; import com.opendoorlogistics.core.formulae.definitions.FunctionDefinition.FunctionArgument; import com.opendoorlogistics.core.geometry.GreateCircle; import com.opendoorlogistics.core.geometry.ODLGeomImpl; import com.opendoorlogistics.core.geometry.Spatial; import com.opendoorlogistics.core.gis.map.data.LatLongImpl; import com.opendoorlogistics.core.scripts.execution.adapters.FunctionsBuilder; import com.opendoorlogistics.core.scripts.execution.adapters.IndexedDatastores; import com.opendoorlogistics.core.scripts.execution.adapters.FunctionsBuilder.ProcessedLookupReferences; import com.opendoorlogistics.core.scripts.execution.adapters.FunctionsBuilder.ToProcessLookupReferences; import com.opendoorlogistics.core.tables.ColumnValueProcessor; import com.opendoorlogistics.core.utils.Numbers; import com.opendoorlogistics.core.utils.Pair; import com.opendoorlogistics.core.utils.strings.Strings; import com.vividsolutions.jts.geom.Coordinate; import com.vividsolutions.jts.geom.Envelope; import com.vividsolutions.jts.geom.Geometry; import com.vividsolutions.jts.geom.GeometryFactory; /** * Looks up the closest object to the input geometry. For polygons the distance used is the distance to the boundary, not the polygon centre. When * comparing two objects which are both within the polygon, the closest distance to the centre is instead used. * * @author Phil * */ final public class FmLookupNearest extends FunctionImpl { private final MathTransform transform; private final String espg_srid; //private final Pair<Class<?>, String> cacheKey; private final LCType type; private ProcessedLookupReferences refs; private FmLookupNearest(LCType type, String espg_srid, MathTransform transform, Function... children) { super(children); this.type = type; this.espg_srid = espg_srid; this.transform = transform; // this.cacheKey = new Pair<Class<?>, String>(FmLookupNearest.class, this.espg_srid); } // public FmLookupClosest(Function foreignKeyValue, int datastoreIndex, int otherTableId, int geometryColumn, int otherTableReturnKeyColummn, // String espg_srid) { // super(foreignKeyValue, datastoreIndex, otherTableId, otherTableReturnKeyColummn); // this.espg_srid = espg_srid; // transform = Spatial.fromWGS84(this.espg_srid); // if (transform == null) { // throw new RuntimeException("Cannot find transform for coordinate system: " + espg_srid); // } // // cacheKey = new Pair<Class<?>, String>(FmLookupClosest.class, this.espg_srid); // } private static class BoundingCircle { final Coordinate envelopeCentre; final double envelopeRadius; BoundingCircle(Geometry transformedGeom) { Envelope env = transformedGeom.getEnvelopeInternal(); envelopeCentre = env.centre(); double halfWidth = 0.5 * env.getWidth(); double halfHeight = 0.5 * env.getHeight(); envelopeRadius = Math.sqrt(halfWidth * halfWidth + halfHeight * halfHeight); } /** * Calculate the minimum separation using the bounding circle * * @param cpg * @return */ double minimumSeparation(BoundingCircle cpg) { double ret = envelopeCentre.distance(cpg.envelopeCentre); ret -= envelopeRadius; ret -= cpg.envelopeRadius; return ret; } } /** * Cached geometry. This is the geometry converted to the coord system together with a bounding circle. * * @author Phil * */ private static class CachedProcessedGeom { public CachedProcessedGeom(LatLong ll, MathTransform transform) { Geometry geom = new GeometryFactory().createPoint(new Coordinate(ll.getLongitude(), ll.getLatitude())); if (transform != null) { try { geom = JTS.transform(geom, transform); } catch (Throwable e) { geom = null; } if (geom != null) { boundingCircle = new BoundingCircle(geom); } else { boundingCircle = null; } } else { boundingCircle = null; } geometry = geom; } CachedProcessedGeom(Geometry transformedGeom) { this.geometry = transformedGeom; boundingCircle = new BoundingCircle(transformedGeom); } final Geometry geometry; final BoundingCircle boundingCircle; public long getSizeInBytes(){ long bytes =8; if(geometry!=null){ bytes += Spatial.getEstimatedSizeInBytes(geometry); } // bounding circle bytes += 40; return bytes; } } /** * Get in the coord system, caching when possible * * @param geom * @return */ private CachedProcessedGeom toCoordSystem(ODLGeomImpl geom) { RecentlyUsedCache cache = ApplicationCache.singleton().get(ApplicationCache.LOOKUP_NEAREST_TRANSFORMED_GEOMS); class CacheKey{ final String espg; final ODLGeom geom; private CacheKey(String espg, ODLGeom geom) { super(); this.espg = espg; this.geom = geom; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((espg == null) ? 0 : espg.hashCode()); result = prime * result + ((geom == null) ? 0 : geom.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; CacheKey other = (CacheKey) obj; if (espg == null) { if (other.espg != null) return false; } else if (!espg.equals(other.espg)) return false; if (geom == null) { if (other.geom != null) return false; } else if (!geom.equals(other.geom)) return false; return true; } } CacheKey key = new CacheKey(espg_srid, geom); CachedProcessedGeom cached = (CachedProcessedGeom)cache.get(key); if (cached != null) { return cached; } try { Geometry wgs84 = geom.getJTSGeometry(); if (wgs84 == null) { return null; } cached = new CachedProcessedGeom(JTS.transform(wgs84, transform)); cache.put(key, cached, cached.getSizeInBytes()); return cached; } catch (Throwable e) { // return value will be null, so error reported later } return null; } private enum LCType { LL, LG, GG, GL, } private CachedProcessedGeom getSearchGeom(FunctionParameters parameters) { switch (type) { case LL: // untransformed return getLatLongFromExecution(parameters, false); case LG: // transformed to wgs84 return getLatLongFromExecution(parameters, true); case GG: case GL: { // read geometry object Object keyVal = child(0).execute(parameters); if (keyVal == null || keyVal == Functions.EXECUTION_ERROR) { return null; } // get transformed geometry ODLGeomImpl odlGeom = (ODLGeomImpl) ColumnValueProcessor.convertToMe(ODLColumnType.GEOM,keyVal); return toCoordSystem(odlGeom); } } return null; } // protected Object getTransformedLatLongGeometrySearchObject(FunctionParameters parameters){ // LatLong ll = getLatLongFromExecution(parameters); // if(ll==null){ // return Functions.EXECUTION_ERROR; // } // Geometry ret = toCoordSystem(new ODLGeom( new GeometryFactory().createPoint(new Coordinate(ll.getLongitude(), ll.getLatitude())))); // if(ret==null){ // return Functions.EXECUTION_ERROR; // } // return ret; // } private CachedProcessedGeom getLatLongFromExecution(FunctionParameters parameters, boolean transformed) { LatLong ll = getLatLongFromExecution(parameters); return new CachedProcessedGeom(ll, transformed ? transform : null); } private LatLong getLatLongFromExecution(FunctionParameters parameters) { Object[] longlat = executeChildFormulae(parameters, true); if (longlat == null) { return null; } LatLong ll = getLatLongFromObjects(longlat[0], longlat[1]); if (ll == null) { return null; } return ll; } private LatLong getLatLongFromObjects(Object oLng, Object oLat) { Double lng = Numbers.toDouble(oLng); Double lat = Numbers.toDouble(oLat); if (lng == null || lat == null) { return null; } LatLong ll = new LatLongImpl(lat, lng); return ll; } // protected Object getGeometrySearchObject(FunctionParameters parameters){ // Object keyVal = child(0).execute(parameters); // if (keyVal == null || keyVal == Functions.EXECUTION_ERROR) { // return Functions.EXECUTION_ERROR; // } // // // get transformed geometry // Geometry geom=toCoordSystem(keyVal); // if(geom==null){ // return Functions.EXECUTION_ERROR; // } // // return geom; // } private Object executeLL(FunctionParameters parameters, ODLTableReadOnly table) { LatLong ll = getLatLongFromExecution(parameters); if (ll == null) { return Functions.EXECUTION_ERROR; } int nr = table.getRowCount(); int closestRow = -1; double closest = Double.MAX_VALUE; for (int row = 0; row < nr; row++) { Pair<LatLong, Boolean> other = getLatLongFromRow(table, row); if (other.getSecond() == false) { // critical error return Functions.EXECUTION_ERROR; } else if (other.getFirst() != null) { double dist = GreateCircle.greatCircleApprox(ll, other.getFirst()); if (dist < closest) { closest = dist; closestRow = row; } } } if (closestRow != -1) { return getReturnObject(table, closestRow); } return null; } @Override public Object execute(FunctionParameters parameters) { TableParameters tp = (TableParameters) parameters; ODLTableReadOnly table = (ODLTableReadOnly) tp.getTableById(refs.datastoreIndx, refs.tableId); if (type == LCType.LL) { return executeLL(parameters, table); } CachedProcessedGeom searchObject = getSearchGeom(parameters); if (searchObject == null || searchObject.geometry == null) { return Functions.EXECUTION_ERROR; } class RowElement implements Comparable<RowElement>{ int row; CachedProcessedGeom geom; double minDistance; @Override public int compareTo(RowElement o) { int ret= Double.compare(minDistance, o.minDistance); if(ret==0){ ret= Integer.compare(row, o.row); } return ret; } } // Get all geometries, transformed into the coord system and with bounding circles. // Place them in a binary heap, sorted by their minimum possible distance according to bounding circle int nr = table.getRowCount(); BinaryHeap sortedHeap = new BinaryHeap(); for (int row = 0; row < nr; row++) { CachedProcessedGeom otherGeom = null; switch (type) { case GL: Pair<LatLong, Boolean> result = getLatLongFromRow(table, row); if (result.getSecond() == false) { // critical error return Functions.EXECUTION_ERROR; } else if (result.getFirst() != null) { LatLong ll = result.getFirst(); // put into our comparison object and convert otherGeom = new CachedProcessedGeom(ll, transform); } break; case GG: case LG: { Object val = table.getValueAt(row, refs.columnIndices[0]); if (val != null) { ODLGeomImpl odlGeom = (ODLGeomImpl) ColumnValueProcessor.convertToMe(ODLColumnType.GEOM,val); if (odlGeom == null) { // critical error return Functions.EXECUTION_ERROR; } otherGeom = toCoordSystem(odlGeom); if (otherGeom == null || otherGeom.geometry == null) { // critical error return Functions.EXECUTION_ERROR; } } } default: break; } if (otherGeom != null) { RowElement rowElement = new RowElement(); rowElement.row = row; rowElement.minDistance = searchObject.boundingCircle.minimumSeparation(otherGeom.boundingCircle); rowElement.geom = otherGeom; sortedHeap.add(rowElement); } } // loop over the table RowElement closest=null; double closestDistance = Double.MAX_VALUE; while(sortedHeap.size()>0){ RowElement row = (RowElement)sortedHeap.pop(); if(row.minDistance > closestDistance){ // We can stop searching now as the minimum possible distance is greater than our closest break; } // Explicitly get the distance double distance = searchObject.geometry.distance(row.geom.geometry); if(distance < closestDistance){ closestDistance = distance; closest = row; } } if(closest!=null){ return getReturnObject(table, closest.row); } return null; } private Object getReturnObject(ODLTableReadOnly table, int closestRow) { return table.getValueAt(closestRow, refs.columnIndices[refs.columnIndices.length - 1]); } /** * * @param searchGeom * @param table * @param row * @return Return false for critical error. */ private Pair<LatLong, Boolean> getLatLongFromRow(ODLTableReadOnly table, int row) { Object lng = table.getValueAt(row, refs.columnIndices[0]); Object lat = table.getValueAt(row, refs.columnIndices[1]); boolean result = true; LatLong ll = null; if (lng == null && lat == null) { // ok, both null result = true; } else { // try getting latlong object ll = getLatLongFromObjects(lng, lat); if (ll == null) { if (lng != null && lat != null && Strings.isEmpty(lng.toString()) && Strings.isEmpty(lat.toString())) { // both non-null but empty; this is OK result = true; } else { // corrupt data; critical error result = false; } } } return new Pair<LatLong, Boolean>(ll, result); } @Override public Function deepCopy() { throw new UnsupportedOperationException(); } public static Iterable<FunctionDefinition> createDefinitions(final IndexedDatastores<? extends ODLTableReadOnly> datastores, final int defaultDatastoreIndex, final ExecutionReport result) { ArrayList<FunctionDefinition> dfns = new ArrayList<>(); for (final LCType type : LCType.values()) { final FunctionDefinition dfn = new FunctionDefinition("lookupnearest" + type.name().toLowerCase()); dfn.setGroup("lookupNearest"); dfn.setDescription("Find the nearest object to the input value in the other table."); if (type != LCType.LL) { dfn.addArg("ESPG_SRID", ArgumentType.STRING_CONSTANT, "Spatial Reference System Identifier (SRID) from the ESPG SRID database " + "to use for performing the distance calculations. The reference system " + "must be a grid-based one allowing Pythagoras distance calculations (i.e. " + "a spherical longitude-latitude coord system will not work)."); } // add FROM arguments switch (type) { case LG: case LL: dfn.addArg("longitude", "Longitude to compare to geographic objects in the other table."); dfn.addArg("latitude", "Latitude to compare to geographic objects in the other table."); break; case GG: case GL: dfn.addArg("geometry", "Geometry to compare to geographic objects in the other table."); break; } dfn.addArg("table_reference", ArgumentType.TABLE_REFERENCE_CONSTANT, "Reference to the table to search in."); // add OTHER arguments switch (type) { case GL: case LL: dfn.addArg(new FunctionArgument("longitude_field_name", ArgumentType.STRING_CONSTANT, "Field name of the longitude field in the other table.", false)); dfn.addArg(new FunctionArgument("latitude_field_name", ArgumentType.STRING_CONSTANT, "Field name of the latitude field in the other table.", false)); break; case LG: case GG: dfn.addArg(new FunctionArgument("geometry_field_name", ArgumentType.STRING_CONSTANT, "Field name of the geometry field in the other table.", false)); break; } dfn.addArg("return_field_name", "Name of the field from the other table to return the value of."); // only build the factory if we have actual datastore to build against if (datastores != null) { dfn.setFactory(new FunctionFactory() { @Override public Function createFunction(Function... children) { int i = 0; // get math transform to grid unless working completely in longitude-latitude String srid = null; MathTransform transform = null; if (type != LCType.LL) { srid = FunctionUtils.getConstantString(get(children, i++)); transform = Spatial.fromWGS84(srid); } // get function(s) to get the geometry FmLookupNearest ret = null; switch (type) { case LL: case LG: ret = new FmLookupNearest(type, srid, transform, get(children, i++), get(children, i++)); break; case GL: case GG: ret = new FmLookupNearest(type, srid, transform, get(children, i++)); break; } // get the table reference - to do process this ToProcessLookupReferences toProcess = new ToProcessLookupReferences(); toProcess.tableReferenceFunction = children[i++]; int nbFieldnames = (type == LCType.LL || type == LCType.GL) ? 3 : 2; toProcess.fieldnameFunctions = new Function[nbFieldnames]; for (int j = 0; j < nbFieldnames; j++) { toProcess.fieldnameFunctions[j] = get(children, i++); } // check no more fields left if (i != children.length) { throwWrongNbArgs(); } // process table references ret.refs = FunctionsBuilder.processLookupReferenceNames(dfn.getName(), datastores, defaultDatastoreIndex, toProcess, result); return ret; } private Function get(Function[] children, int i) { if (i >= children.length) { throwWrongNbArgs(); } return children[i]; } private void throwWrongNbArgs() { throw new RuntimeException("Wrong number of arguments given for function: " + dfn.getName()); } }); } dfns.add(dfn); // } } // sort by number of arguments Collections.sort(dfns, new Comparator<FunctionDefinition>() { @Override public int compare(FunctionDefinition o1, FunctionDefinition o2) { return Integer.compare(o1.nbArgs(), o2.nbArgs()); } }); return dfns; } public static void main(String[] args) { for (FunctionDefinition dfn : createDefinitions(null, -1, null)) { System.out.println(dfn + " - " + dfn.nbArgs() + " arguments"); } } }