com.opendoorlogistics.core.scripts.formulae.FmLookupNearest.java Source code

Java tutorial

Introduction

Here is the source code for com.opendoorlogistics.core.scripts.formulae.FmLookupNearest.java

Source

/*******************************************************************************
 * 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");

        }
    }
}