MCIndexPointSnapper.java

/*
 * Copyright (c) 2016 Vivid Solutions.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * and Eclipse Distribution License v. 1.0 which accompanies this distribution.
 * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html
 * and the Eclipse Distribution License is available at
 *
 * http://www.eclipse.org/org/documents/edl-v10.php.
 */

package org.locationtech.jts.noding.snapround;

import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.Envelope;
import org.locationtech.jts.index.ItemVisitor;
import org.locationtech.jts.index.SpatialIndex;
import org.locationtech.jts.index.chain.MonotoneChain;
import org.locationtech.jts.index.chain.MonotoneChainSelectAction;
import org.locationtech.jts.noding.NodedSegmentString;
import org.locationtech.jts.noding.SegmentString;

/**
 * "Snaps" all {@link SegmentString}s in a {@link SpatialIndex} containing
 * {@link MonotoneChain}s to a given {@link HotPixel}.
 *
 * @version 1.7
 */
public class MCIndexPointSnapper
{
  //public static final int nSnaps = 0;

  private SpatialIndex index;

  public MCIndexPointSnapper(SpatialIndex index) {
    this.index = index;
  }

  /**
   * Snaps (nodes) all interacting segments to this hot pixel.
   * The hot pixel may represent a vertex of an edge,
   * in which case this routine uses the optimization
   * of not noding the vertex itself
   *
   * @param hotPixel the hot pixel to snap to
   * @param parentEdge the edge containing the vertex, if applicable, or <code>null</code>
   * @param hotPixelVertexIndex the index of the hotPixel vertex, if applicable, or -1
   * @return <code>true</code> if a node was added for this pixel
   */
  public boolean snap(HotPixel hotPixel, SegmentString parentEdge, int hotPixelVertexIndex)
  {
    final Envelope pixelEnv = getSafeEnvelope(hotPixel);
    final HotPixelSnapAction hotPixelSnapAction = new HotPixelSnapAction(hotPixel, parentEdge, hotPixelVertexIndex);

    index.query(pixelEnv, new ItemVisitor() {
      public void visitItem(Object item) {
        MonotoneChain testChain = (MonotoneChain) item;
        testChain.select(pixelEnv, hotPixelSnapAction);
      }
    }
    );
    return hotPixelSnapAction.isNodeAdded();
  }

  public boolean snap(HotPixel hotPixel)
  {
    return snap(hotPixel, null, -1);
  }

  private static final double SAFE_ENV_EXPANSION_FACTOR = 0.75;
  
  /**
   * Returns a "safe" envelope that is guaranteed to contain the hot pixel.
   * The envelope returned is larger than the exact envelope of the 
   * pixel by a safe margin.
   * 
   * @return an envelope which contains the hot pixel
   */
  public Envelope getSafeEnvelope(HotPixel hp)
  {
    double safeTolerance = SAFE_ENV_EXPANSION_FACTOR / hp.getScaleFactor();
    Envelope safeEnv = new Envelope(hp.getCoordinate());
    safeEnv.expandBy(safeTolerance);
    return safeEnv;
  }
  
  public static class HotPixelSnapAction
      extends MonotoneChainSelectAction
  {
    private HotPixel hotPixel;
    private SegmentString parentEdge;
    // is -1 if hotPixel is not a vertex
    private int hotPixelVertexIndex;
    private boolean isNodeAdded = false;

    public HotPixelSnapAction(HotPixel hotPixel, SegmentString parentEdge, int hotPixelVertexIndex)
    {
      this.hotPixel = hotPixel;
      this.parentEdge = parentEdge;
      this.hotPixelVertexIndex = hotPixelVertexIndex;
    }

    /**
     * Reports whether the HotPixel caused a
     * node to be added in any target segmentString (including its own).
     * If so, the HotPixel must be added as a node as well.
     * @return true if a node was added in any target segmentString.
     */
    public boolean isNodeAdded() { return isNodeAdded; }

    /**
     * Check if a segment of the monotone chain intersects
     * the hot pixel vertex and introduce a snap node if so.
     * Optimized to avoid noding segments which
     * contain the vertex (which otherwise
     * would cause every vertex to be noded).
     */
    public void select(MonotoneChain mc, int startIndex)
    {
    	NodedSegmentString ss = (NodedSegmentString) mc.getContext();
      /**
       * Check to avoid snapping a hotPixel vertex to the its orginal vertex.
       * This method is called on segments which intersect the
       * hot pixel.
       * If either end of the segment is equal to the hot pixel
       * do not snap.
       */
      if (parentEdge != null && ss == parentEdge) {
        if (startIndex == hotPixelVertexIndex
              || startIndex + 1 == hotPixelVertexIndex
            )
          return;
      }
      // records if this HotPixel caused any node to be added
      isNodeAdded |= addSnappedNode(hotPixel, ss, startIndex);
    }

    /**
     * Adds a new node (equal to the snap pt) to the specified segment
     * if the segment passes through the hot pixel
     *
     * @param segStr
     * @param segIndex
     * @return true if a node was added to the segment
     */
    public boolean addSnappedNode(HotPixel hotPixel, 
        NodedSegmentString segStr,
        int segIndex
        )
    {
      Coordinate p0 = segStr.getCoordinate(segIndex);
      Coordinate p1 = segStr.getCoordinate(segIndex + 1);

      if (hotPixel.intersects(p0, p1)) {
        //System.out.println("snapped: " + snapPt);
        //System.out.println("POINT (" + snapPt.x + " " + snapPt.y + ")");
        segStr.addIntersection(hotPixel.getCoordinate(), segIndex);

        return true;
      }
      return false;
    }
  }

}