/* Copyright (c) 2014 Boundless and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Distribution License v1.0 * which accompanies this distribution, and is available at * https://www.eclipse.org/org/documents/edl-v10.html * * Contributors: * Jillian Crossley (Cornell University) - initial implementation */ package org.locationtech.geogig.api.plumbing; import static com.google.common.base.Optional.fromNullable; import static com.google.common.base.Preconditions.checkArgument; import java.util.List; import java.util.Map; import javax.annotation.Nullable; import org.geotools.geometry.jts.JTS; import org.geotools.geometry.jts.ReferencedEnvelope; import org.geotools.referencing.CRS; import org.locationtech.geogig.api.AbstractGeoGigOp; import org.locationtech.geogig.api.Bounded; import org.locationtech.geogig.api.Bucket; import org.locationtech.geogig.api.Node; import org.locationtech.geogig.api.NodeRef; import org.locationtech.geogig.api.ObjectId; import org.locationtech.geogig.api.Ref; import org.locationtech.geogig.api.RevFeatureType; import org.locationtech.geogig.api.RevTree; import org.locationtech.geogig.api.plumbing.diff.DiffSummary; import org.locationtech.geogig.api.plumbing.diff.PreOrderDiffWalk; import org.locationtech.geogig.api.plumbing.diff.PathFilteringDiffConsumer; import org.locationtech.geogig.storage.ObjectDatabase; import org.opengis.feature.type.FeatureType; import org.opengis.geometry.BoundingBox; import org.opengis.referencing.FactoryException; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.operation.MathTransform; import org.opengis.referencing.operation.TransformException; import com.google.common.base.Optional; import com.google.common.base.Preconditions; import com.google.common.base.Throwables; import com.google.common.collect.ImmutableList; import com.google.common.collect.Maps; import com.vividsolutions.jts.geom.Envelope; /** * Computes the bounds of the difference between the two trees instead of the actual diffs. * */ public class DiffBounds extends AbstractGeoGigOp<DiffSummary<BoundingBox, BoundingBox>> { private String oldVersion; private String newVersion; private boolean cached; private List<String> pathFilters; private CoordinateReferenceSystem crs; public DiffBounds setOldVersion(String oldVersion) { this.oldVersion = oldVersion; this.pathFilters = ImmutableList.of(); return this; } public DiffBounds setNewVersion(String newVersion) { this.newVersion = newVersion; return this; } public DiffBounds setCompareIndex(boolean cached) { this.cached = cached; return this; } public DiffBounds setPathFilters(@Nullable final List<String> pathFilters) { if (null == pathFilters) { this.pathFilters = ImmutableList.of(); } else { this.pathFilters = ImmutableList.copyOf(pathFilters); } return this; } /** * @param crs the CRS to compute the bounds in. Defaults to {@code EPSG:4326} with long/lat axis * order if not set. */ public DiffBounds setCRS(@Nullable CoordinateReferenceSystem crs) { this.crs = crs; return this; } @Override protected DiffSummary<BoundingBox, BoundingBox> _call() { checkArgument(cached && oldVersion == null || !cached, String.format( "compare index allows only one revision to check against, got %s / %s", oldVersion, newVersion)); checkArgument(newVersion == null || oldVersion != null, "If new rev spec is specified then old rev spec is mandatory"); final String leftRefSpec = fromNullable(oldVersion).or(Ref.HEAD); final String rightRefSpec = fromNullable(newVersion).or( cached ? Ref.STAGE_HEAD : Ref.WORK_HEAD); RevTree left = resolveTree(leftRefSpec); RevTree right = resolveTree(rightRefSpec); ObjectDatabase leftSource = resolveSafeDb(leftRefSpec); ObjectDatabase rightSource = resolveSafeDb(rightRefSpec); PreOrderDiffWalk visitor = new PreOrderDiffWalk(left, right, leftSource, rightSource); CoordinateReferenceSystem crs = resolveCrs(); BoundsWalk walk = new BoundsWalk(crs, stagingDatabase()); PreOrderDiffWalk.Consumer consumer = walk; if (!pathFilters.isEmpty()) { consumer = new PathFilteringDiffConsumer(pathFilters, walk); } visitor.walk(consumer); DiffSummary<BoundingBox, BoundingBox> diffBounds = walk.getResult(); return diffBounds; } private CoordinateReferenceSystem resolveCrs() { if (this.crs != null) { return this.crs; } CoordinateReferenceSystem defaultCrs; try { defaultCrs = CRS.decode("EPSG:4326", true); } catch (Exception e) { throw Throwables.propagate(e); } return defaultCrs; } /** * If {@code refSpec} can easily be determined to be on the object database (e.g. its a ref), * then returns the repository object database, otherwise the staging database, just to be safe */ private ObjectDatabase resolveSafeDb(String refSpec) { Optional<Ref> ref = command(RefParse.class).setName(refSpec).call(); if (ref.isPresent()) { ObjectId id = ref.get().getObjectId(); return objectDatabase().exists(id) ? objectDatabase() : stagingDatabase(); } return stagingDatabase(); } private RevTree resolveTree(String refSpec) { Optional<ObjectId> id = command(ResolveTreeish.class).setTreeish(refSpec).call(); Preconditions.checkState(id.isPresent(), "%s did not resolve to a tree", refSpec); return stagingDatabase().getTree(id.get()); } private static class BoundsWalk implements PreOrderDiffWalk.Consumer { private DiffSummary<BoundingBox, BoundingBox> result; private ReferencedEnvelope leftEnv; private ReferencedEnvelope rightEnv; private final CoordinateReferenceSystem crs; private final ReferencedEnvelope leftHelper, rightHelper; private final ObjectDatabase source; private final Map<ObjectId, MathTransform> transformsByMetadataId; private Optional<ObjectId> currentDefaultLefMetadataId = Optional.absent(); private Optional<ObjectId> currentDefaultRightMetadataId = Optional.absent(); public BoundsWalk(CoordinateReferenceSystem crs, ObjectDatabase source) { this.crs = crs; this.source = source; this.transformsByMetadataId = Maps.newHashMap(); leftEnv = new ReferencedEnvelope(this.crs); rightEnv = new ReferencedEnvelope(this.crs); leftHelper = new ReferencedEnvelope(this.crs); rightHelper = new ReferencedEnvelope(this.crs); } @Override public void feature(@Nullable Node left, @Nullable Node right) { setEnv(left, leftHelper, md(left).or(currentDefaultLefMetadataId)); setEnv(right, rightHelper, md(right).or(currentDefaultRightMetadataId)); if (!leftHelper.equals(rightHelper)) { leftEnv.expandToInclude(leftHelper); rightEnv.expandToInclude(rightHelper); } } @Override public boolean tree(@Nullable Node left, @Nullable Node right) { Optional<ObjectId> leftMd = md(left); Optional<ObjectId> rightMd = md(right); if (leftMd.isPresent()) { currentDefaultLefMetadataId = leftMd; } if (rightMd.isPresent()) { currentDefaultRightMetadataId = rightMd; } setEnv(left, leftHelper, leftMd.or(leftMd)); setEnv(right, rightHelper, rightMd.or(rightMd)); if (leftHelper.isNull() && rightHelper.isNull()) { return false; } if (leftHelper.isNull()) { rightEnv.expandToInclude(rightHelper); return false; } else if (rightHelper.isNull()) { leftEnv.expandToInclude(leftHelper); return false; } return true; } private Optional<ObjectId> md(@Nullable Node node) { return null == node ? Optional.<ObjectId> absent() : node.getMetadataId(); } @Override public boolean bucket(final int bucketIndex, final int bucketDepth, @Nullable Bucket left, @Nullable Bucket right) { setEnv(left, leftHelper, currentDefaultLefMetadataId); setEnv(right, rightHelper, currentDefaultRightMetadataId); if (leftHelper.isNull() && rightHelper.isNull()) { return false; } if (leftHelper.isNull()) { rightEnv.expandToInclude(rightHelper); return false; } else if (rightHelper.isNull()) { leftEnv.expandToInclude(leftHelper); return false; } return true; } private void setEnv(@Nullable Bounded bounded, ReferencedEnvelope env, Optional<ObjectId> metadataId) { env.setToNull(); if (bounded == null) { return; } bounded.expand(env); if (env.isNull()) { return; } ObjectId mdid; if (metadataId.isPresent()) { mdid = metadataId.get(); MathTransform transform = getMathTransform(mdid); if (transform.isIdentity()) { return; } Envelope targetEnvelope = new ReferencedEnvelope(crs); try { int densifyPoints = isPoint(env) ? 1 : 5; JTS.transform(env, targetEnvelope, transform, densifyPoints); env.init(targetEnvelope); } catch (TransformException e) { throw Throwables.propagate(e); } } } private boolean isPoint(Envelope env) { return env.getWidth() == 0D && env.getHeight() == 0D; } private MathTransform getMathTransform(ObjectId mdid) { MathTransform transform = this.transformsByMetadataId.get(mdid); if (transform == null) { RevFeatureType revtype = source.getFeatureType(mdid); FeatureType type = revtype.type(); CoordinateReferenceSystem sourceCrs = type.getCoordinateReferenceSystem(); CoordinateReferenceSystem targetCrs = this.crs; if (sourceCrs == null) { sourceCrs = targetCrs; } try { boolean lenient = true; transform = CRS.findMathTransform(sourceCrs, targetCrs, lenient); } catch (FactoryException e) { throw Throwables.propagate(e); } this.transformsByMetadataId.put(mdid, transform); } return transform; } @Override public void endTree(Node left, Node right) { String name = left == null ? right.getName() : left.getName(); if (NodeRef.ROOT.equals(name)) { BoundingBox lbounds = new ReferencedEnvelope(this.leftEnv); BoundingBox rbounds = new ReferencedEnvelope(this.rightEnv); BoundingBox merged; if (lbounds.isEmpty()) { merged = rbounds; } else if (rbounds.isEmpty()) { merged = lbounds; } else { merged = new ReferencedEnvelope(lbounds); merged.include(rbounds); } this.result = new DiffSummary<BoundingBox, BoundingBox>(lbounds, rbounds, merged); } } @Override public void endBucket(int bucketIndex, int bucketDepth, Bucket left, Bucket right) { // nothing to do } public DiffSummary<BoundingBox, BoundingBox> getResult() { DiffSummary<BoundingBox, BoundingBox> r = this.result; if (r == null) { BoundingBox empty = new ReferencedEnvelope(crs); r = new DiffSummary<BoundingBox, BoundingBox>(empty, empty, empty); } return r; } } }