/******************************************************************************* * 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 3.0 * which accompanies this distribution, and is available at * http://www.gnu.org/licenses/lgpl.html * ******************************************************************************/ package com.opendoorlogistics.core.geometry.operations; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import org.geotools.geometry.jts.JTS; import com.opendoorlogistics.api.geometry.ODLGeom; import com.opendoorlogistics.core.cache.ApplicationCache; import com.opendoorlogistics.core.cache.RecentlyUsedCache; import com.opendoorlogistics.core.geometry.ODLGeomImpl; import com.opendoorlogistics.core.geometry.ODLLoadedGeometry; import com.opendoorlogistics.core.geometry.Spatial; import com.vividsolutions.jts.geom.Geometry; import com.vividsolutions.jts.geom.GeometryCollection; import com.vividsolutions.jts.geom.GeometryFactory; import com.vividsolutions.jts.geom.PrecisionModel; import com.vividsolutions.jts.precision.GeometryPrecisionReducer; /** * Perform union on multiple geomtries, caching the result. Union is performed in the input grid system. * * @author Phil * */ public class GeomUnion { private Geometry combineIntoOneGeometry(Collection<com.vividsolutions.jts.geom.Geometry> geometryCollection) { if (geometryCollection.size() == 1) { return geometryCollection.iterator().next(); } GeometryFactory factory = new GeometryFactory(); // note the following geometry collection may be invalid (say with overlapping polygons) GeometryCollection gc = (GeometryCollection) factory.buildGeometry(geometryCollection); return gc.union(); } private Object createCacheKey(Iterable<ODLGeom> inputGeoms, String ESPGCode) { class CacheKey { HashSet<ODLGeom> set = new HashSet<>(); String espg; @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((espg == null) ? 0 : espg.hashCode()); result = prime * result + ((set == null) ? 0 : set.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 (set == null) { if (other.set != null) return false; } else if (!set.equals(other.set)) return false; return true; } } CacheKey key = new CacheKey(); key.espg = ESPGCode; // put all geoms in a hashset for (ODLGeom geom : inputGeoms) { key.set.add(geom); } return key; } private static final String INVALID_UNION = "invalid-union"; public ODLGeom union(Iterable<ODLGeom> inputGeoms, String ESPGCode) { Object key = createCacheKey(inputGeoms, ESPGCode); RecentlyUsedCache cache = ApplicationCache.singleton().get(ApplicationCache.GEOMETRY_MERGER_CACHE); Object cached = cache.get(key); if (cached == INVALID_UNION) { return null; } ODLGeom ret = (ODLGeom) cached; if (ret == null) { // estimate the key size, assuming the input geometries are stored elsewhere int estimatedKeySize = 8 + com.opendoorlogistics.core.utils.iterators.IteratorUtils.size(inputGeoms) * 8 + 20; try { ret = calculateUnion(inputGeoms, ESPGCode); } catch (Exception e) { // record the union as invalid in-case we do it again cache.put(key, INVALID_UNION, estimatedKeySize); throw new RuntimeException(e); } if (ret != null) { // estimate size of geometry in bytes long size = ((ODLGeomImpl) ret).getEstimatedSizeInBytes(); size += estimatedKeySize; cache.put(key, ret, size); } } return ret; } private ODLGeom calculateUnion(Iterable<ODLGeom> inputGeoms, String ESPGCode) { try { Spatial.initSpatial(); GridTransforms transforms = new GridTransforms(ESPGCode); PrecisionModel pm = new PrecisionModel(PrecisionModel.FLOATING_SINGLE); GeometryPrecisionReducer reducer = new GeometryPrecisionReducer(pm); // process shapes into grid with reduced precision ArrayList<Geometry> gridGeoms = new ArrayList<>(); for (ODLGeom geom : inputGeoms) { if (geom != null) { ODLGeomImpl gimpl = (ODLGeomImpl) geom; if (gimpl.getJTSGeometry() != null) { com.vividsolutions.jts.geom.Geometry g = gimpl.getJTSGeometry(); // convert to grid g = JTS.transform(g, transforms.getWGS84ToGrid().getMathTransform()); // reduce precision as it stops holes appearing with our UK postcode data g = reducer.reduce(g); gridGeoms.add(g); } } } if (gridGeoms.size() == 0) { return null; } // combine Geometry combinedGrid = combineIntoOneGeometry(gridGeoms); // transform back Geometry combinedWGS84 = JTS.transform(combinedGrid, transforms.getGridToWGS84().getMathTransform()); return new ODLLoadedGeometry(combinedWGS84); } catch (Exception e) { throw new RuntimeException(e); } } }