Skip to content

v0.2.48..v0.2.49 changeset ChangesetReplacementCreator.cpp

Garret Voltz edited this page Oct 2, 2019 · 1 revision
diff --git a/hoot-core/src/main/cpp/hoot/core/algorithms/changeset/ChangesetReplacementCreator.cpp b/hoot-core/src/main/cpp/hoot/core/algorithms/changeset/ChangesetReplacementCreator.cpp
index 4fc65c6..f39e472 100644
--- a/hoot-core/src/main/cpp/hoot/core/algorithms/changeset/ChangesetReplacementCreator.cpp
+++ b/hoot-core/src/main/cpp/hoot/core/algorithms/changeset/ChangesetReplacementCreator.cpp
@@ -27,126 +27,397 @@
 #include "ChangesetReplacementCreator.h"
 
 // Hoot
-#include <hoot/core/util/GeometryUtils.h>
-#include <hoot/core/util/ConfigOptions.h>
-#include <hoot/core/util/IoUtils.h>
-#include <hoot/core/visitors/RemoveElementsVisitor.h>
 #include <hoot/core/algorithms/alpha-shape/AlphaShapeGenerator.h>
+#include <hoot/core/algorithms/ReplacementSnappedWayJoiner.h>
+#include <hoot/core/algorithms/WayJoinerAdvanced.h>
+#include <hoot/core/algorithms/WayJoinerBasic.h>
+
 #include <hoot/core/conflate/CookieCutter.h>
 #include <hoot/core/conflate/UnifyingConflator.h>
-#include <hoot/core/ops/UnconnectedWaySnapper.h>
-#include <hoot/core/util/Factory.h>
-#include <hoot/core/algorithms/ReplacementSnappedWayJoiner.h>
-#include <hoot/core/ops/NamedOp.h>
-#include <hoot/core/visitors/RemoveUnknownVisitor.h>
-#include <hoot/core/ops/MapCropper.h>
-#include <hoot/core/io/OsmMapWriterFactory.h>
-#include <hoot/core/util/MapProjector.h>
-#include <hoot/core/ops/RecursiveSetTagValueOp.h>
+#include <hoot/core/conflate/network/NetworkMatchCreator.h>
+
+#include <hoot/core/criterion/ConflatableElementCriterion.h>
+#include <hoot/core/criterion/ElementTypeCriterion.h>
 #include <hoot/core/criterion/InBoundsCriterion.h>
-#include <hoot/core/criterion/ChainCriterion.h>
+#include <hoot/core/criterion/LinearCriterion.h>
 #include <hoot/core/criterion/NotCriterion.h>
-#include <hoot/core/criterion/ElementTypeCriterion.h>
-#include <hoot/core/ops/SuperfluousNodeRemover.h>
-#include <hoot/core/io/OsmMapReaderFactory.h>
-#include <hoot/core/util/Boundable.h>
-#include <hoot/core/io/OsmMapReader.h>
-#include <hoot/core/criterion/WayNodeCriterion.h>
-#include <hoot/core/ops/ElementIdToVersionMapper.h>
-#include <hoot/core/conflate/network/NetworkMatchCreator.h>
-#include <hoot/core/algorithms/WayJoinerAdvanced.h>
-#include <hoot/core/algorithms/WayJoinerBasic.h>
-#include <hoot/core/visitors/SetTagValueVisitor.h>
-#include <hoot/core/visitors/FilteredVisitor.h>
-#include <hoot/core/criterion/TagKeyCriterion.h>
-#include <hoot/core/visitors/RemoveElementsVisitor.h>
+#include <hoot/core/criterion/OrCriterion.h>
+#include <hoot/core/criterion/PointCriterion.h>
+#include <hoot/core/criterion/PolygonCriterion.h>
 #include <hoot/core/criterion/TagCriterion.h>
+#include <hoot/core/criterion/TagKeyCriterion.h>
+#include <hoot/core/criterion/WayNodeCriterion.h>
+
+#include <hoot/core/elements/OsmUtils.h>
 #include <hoot/core/io/OsmGeoJsonReader.h>
+#include <hoot/core/io/OsmMapReaderFactory.h>
+#include <hoot/core/io/OsmMapWriterFactory.h>
+
+#include <hoot/core/ops/ElementIdToVersionMapper.h>
+#include <hoot/core/ops/UnconnectedWaySnapper.h>
+
+#include <hoot/core/ops/MapCropper.h>
+#include <hoot/core/ops/NamedOp.h>
+#include <hoot/core/ops/PointsToPolysConverter.h>
+#include <hoot/core/ops/SuperfluousNodeRemover.h>
+#include <hoot/core/ops/RecursiveSetTagValueOp.h>
 #include <hoot/core/ops/WayJoinerOp.h>
+
+#include <hoot/core/util/Boundable.h>
+#include <hoot/core/util/ConfigOptions.h>
+#include <hoot/core/util/Factory.h>
+#include <hoot/core/util/GeometryUtils.h>
+#include <hoot/core/util/IoUtils.h>
+#include <hoot/core/util/MapProjector.h>
+
 #include <hoot/core/visitors/ApiTagTruncateVisitor.h>
-#include <hoot/core/visitors/ElementCountVisitor.h>
-#include <hoot/core/criterion/AttributeValueCriterion.h>
-#include <hoot/core/elements/OsmUtils.h>
+#include <hoot/core/visitors/FilteredVisitor.h>
+#include <hoot/core/visitors/RemoveElementsVisitor.h>
+#include <hoot/core/visitors/SetTagValueVisitor.h>
 
 namespace hoot
 {
 
 ChangesetReplacementCreator::ChangesetReplacementCreator(const bool printStats,
-                                                         const QString osmApiDbUrl)
+                                                         const QString osmApiDbUrl) :
+_fullReplacement(false),
+_lenientBounds(true),
+_chainReplacementFilters(false),
+_chainRetainmentFilters(false)
 {
   _changesetCreator.reset(new ChangesetCreator(printStats, osmApiDbUrl));
+  setGeometryFilters(QStringList());
+}
+
+void ChangesetReplacementCreator::setGeometryFilters(const QStringList& filterClassNames)
+{
+  LOG_VARD(filterClassNames);
+  if (!filterClassNames.isEmpty())
+  {
+    _geometryTypeFilters.clear();
+    _linearFilterClassNames.clear();
+
+    for (int i = 0; i < filterClassNames.size(); i++)
+    {
+      const QString filterClassName = filterClassNames.at(i);
+      LOG_VARD(filterClassName);
+
+      // Fail if the filter doesn't map to a geometry type.
+      std::shared_ptr<GeometryTypeCriterion> filter =
+        std::dynamic_pointer_cast<GeometryTypeCriterion>(
+          std::shared_ptr<ElementCriterion>(
+            Factory::getInstance().constructObject<ElementCriterion>(filterClassName)));
+      if (!filter)
+      {
+        throw IllegalArgumentException(
+          "Invalid feature geometry type filter: " + filterClassName +
+          ". Filter must be a GeometryTypeCriterion.");
+      }
+
+      ElementCriterionPtr currentFilter = _geometryTypeFilters[filter->getGeometryType()];
+      if (!currentFilter)
+      {
+        _geometryTypeFilters[filter->getGeometryType()] = filter;
+      }
+      else
+      {
+        _geometryTypeFilters[filter->getGeometryType()] =
+          std::shared_ptr<OrCriterion>(new OrCriterion(currentFilter, filter));
+      }
+
+      if (filter->getGeometryType() == GeometryTypeCriterion::GeometryType::Line)
+      {
+        _linearFilterClassNames.append(filterClassName);
+      }
+    }
+  }
+
+  LOG_VARD(_geometryTypeFilters.size());
+  if (_geometryTypeFilters.isEmpty())
+  {
+    _geometryTypeFilters = _getDefaultGeometryFilters();
+    _linearFilterClassNames =
+      ConflatableElementCriterion::getCriterionClassNamesByType(
+        GeometryTypeCriterion::GeometryType::Line);
+  }
+
+  LOG_VARD(_geometryTypeFilters.size());
+  LOG_VARD(_linearFilterClassNames);
+}
+
+void ChangesetReplacementCreator::_setInputFilter(
+  std::shared_ptr<ChainCriterion>& inputFilter, const QStringList& filterClassNames,
+  const bool chainFilters)
+{
+  LOG_VARD(filterClassNames.size());
+  if (!filterClassNames.isEmpty())
+  {
+    LOG_VARD(chainFilters);
+    if (!chainFilters)
+    {
+      inputFilter.reset(new OrCriterion());
+    }
+    else
+    {
+      inputFilter.reset(new ChainCriterion());
+    }
+
+    for (int i = 0; i < filterClassNames.size(); i++)
+    {
+      const QString filterClassName = filterClassNames.at(i);
+      LOG_VARD(filterClassName);
+
+      ElementCriterionPtr crit;
+      try
+      {
+        crit.reset(Factory::getInstance().constructObject<ElementCriterion>(filterClassName));
+      }
+      catch (const boost::bad_any_cast&)
+      {
+      }
+      if (!crit)
+      {
+        throw IllegalArgumentException(
+          "Invalid additional input filter: " + filterClassName +
+          ". Filter must be a ElementCriterion.");
+      }
+
+      // Fail if the filter maps to a geometry type.
+      std::shared_ptr<GeometryTypeCriterion> geometryTypeFilter;
+      try
+      {
+        geometryTypeFilter = std::dynamic_pointer_cast<GeometryTypeCriterion>(crit);
+      }
+      catch (const boost::bad_any_cast&)
+      {
+      }
+      if (geometryTypeFilter)
+      {
+        throw IllegalArgumentException(
+          "Invalid additional input filter: " + filterClassName +
+          ". May not be a GeometryTypeCriterion.");
+      }
+
+      inputFilter->addCriterion(crit);
+    }
+
+    LOG_VARD(inputFilter->toString());
+  }
+}
+
+void ChangesetReplacementCreator::setReplacementFilters(const QStringList& filterClassNames)
+{
+  LOG_VARD(filterClassNames.size());
+  if (filterClassNames.size() > 0)
+  {
+    LOG_DEBUG("Creating replacement filter...");
+    _setInputFilter(_replacementFilter, filterClassNames, _chainReplacementFilters);
+  }
+}
+
+void ChangesetReplacementCreator::setRetainmentFilters(const QStringList& filterClassNames)
+{
+  LOG_VARD(filterClassNames.size());
+  if (filterClassNames.size() > 0)
+  {
+    LOG_DEBUG("Creating retainment filter...");
+    _setInputFilter(_retainmentFilter, filterClassNames, _chainRetainmentFilters);
+  }
+}
+
+void ChangesetReplacementCreator::_setInputFilterOptions(Settings& opts,
+                                                         const QStringList& optionKvps)
+{
+  LOG_VARD(optionKvps.size());
+  opts = conf();
+  LOG_DEBUG("Opts size before adding custom opts: " << opts.size());
+  for (int i = 0; i < optionKvps.size(); i++)
+  {
+    const QString kvp = optionKvps.at(i);
+    // split on the first occurrence of '=' since the opt value itself could have an '=' in it
+    const int firstEqualOccurrence = kvp.indexOf("=");
+    if (firstEqualOccurrence == -1)
+    {
+      throw IllegalArgumentException("Invalid filter configuration option: " + kvp);
+    }
+    const QString key = kvp.mid(0, firstEqualOccurrence).trimmed().remove("\"").remove("'");
+    LOG_VARD(key);
+    const QString val = kvp.mid(firstEqualOccurrence + 2).trimmed().remove("\"").remove("'");
+    LOG_VARD(val);
+    opts.set(key, val);
+  }
+  LOG_DEBUG("Opts size after adding custom opts: " << opts.size());
+}
+
+void ChangesetReplacementCreator::setReplacementFilterOptions(const QStringList& optionKvps)
+{
+  LOG_DEBUG("Creating replacement filter options...");
+  _setInputFilterOptions(_replacementFilterOptions, optionKvps);
+}
+
+void ChangesetReplacementCreator::setRetainmentFilterOptions(const QStringList& optionKvps)
+{
+  LOG_DEBUG("Creating retainment filter options...");
+  _setInputFilterOptions(_retainmentFilterOptions, optionKvps);
 }
 
 void ChangesetReplacementCreator::create(
   const QString& input1, const QString& input2, const geos::geom::Envelope& bounds,
-  const QString& featureTypeFilterClassName, const bool lenientBounds, const QString& output)
+  const QString& output)
 {
+  LOG_VARD(_chainReplacementFilters);
+  LOG_VARD(_lenientBounds);
+  LOG_VARD(_fullReplacement);
+
   // INPUT VALIDATION AND SETUP
 
   _validateInputs(input1, input2);
-  std::shared_ptr<ConflatableElementCriterion> featureFilter =
-    _validateFilter(featureTypeFilterClassName);
-  const bool isLinearCrit =
-    featureFilter->getGeometryType() == ConflatableElementCriterion::ConflatableGeometryType::Line;
   const QString boundsStr = GeometryUtils::envelopeToConfigString(bounds);
-  _parseConfigOpts(lenientBounds, featureFilter, boundsStr);
+  _setGlobalOpts(boundsStr);
+  // If a retainment filter was specified, we'll AND it together with each geometry type filter to
+  // further restrict what reference data gets replaced in the final changeset.
+  const QMap<GeometryTypeCriterion::GeometryType, ElementCriterionPtr> refFilters =
+   _getCombinedFilters(_retainmentFilter);
+  // If a replacement filter was specified, we'll AND it together with each geometry type filter to
+  // further restrict what secondary replacement data goes into the final changeset.
+  const QMap<GeometryTypeCriterion::GeometryType, ElementCriterionPtr> secFilters =
+    _getCombinedFilters(_replacementFilter);
 
   const int maxFilePrintLength = ConfigOptions().getProgressVarPrintLengthMax();
   QString lenientStr = "Bounds calculation is ";
-  if (! lenientBounds)
+  if (!_lenientBounds)
   {
     lenientStr += "not ";
   }
   lenientStr += "lenient.";
   LOG_INFO(
     "Deriving replacement output changeset: ..." << output.right(maxFilePrintLength) <<
-    " from inputs: ..." << input1.right(maxFilePrintLength) + " and ..." <<
-    input2.right(maxFilePrintLength) << ", with filter: " << featureTypeFilterClassName <<
-    ", at bounds: " << boundsStr << ". " << lenientStr);
+    " from inputs: ..." << input1.right(maxFilePrintLength) << " and ..." <<
+    input2.right(maxFilePrintLength) << "" << ", at bounds: " << boundsStr << ". " << lenientStr);
+
+  // CHANGESET CALCULATION
+
+  // Since data with different geometry types require different settings, we'll calculate a separate
+  // pair of before/after maps for each geometry type.
+
+  QList<OsmMapPtr> refMaps;
+  QList<OsmMapPtr> conflatedMaps;
+  int passCtr = 1;
+  for (QMap<GeometryTypeCriterion::GeometryType, ElementCriterionPtr>::const_iterator itr =
+         refFilters.begin(); itr != refFilters.end(); ++itr)
+  {
+    LOG_DEBUG(
+      "Preparing maps for changeset derivation given geometry type: "<<
+      GeometryTypeCriterion::typeToString(itr.key()) << ". Pass: " << passCtr << " / " <<
+      refFilters.size() << "...");
+
+    OsmMapPtr refMap;
+    OsmMapPtr conflatedMap;
+    QStringList linearFilterClassNames;
+    LOG_VARD(itr.value().get());
+    if (itr.key() == GeometryTypeCriterion::GeometryType::Line)
+    {
+      linearFilterClassNames = _linearFilterClassNames;
+    }
+    _getMapsForGeometryType(
+      refMap, conflatedMap, input1, input2, boundsStr, itr.value(), secFilters[itr.key()],
+      itr.key(), linearFilterClassNames);
+
+    LOG_VARD(refMap.get());
+    LOG_VARD(conflatedMap.get());
+    if (refMap && conflatedMap && conflatedMap->size() > 0)
+    {
+      LOG_VARD(refMap->size());
+      LOG_VARD(conflatedMap.get());
+      refMaps.append(refMap);
+      conflatedMaps.append(conflatedMap);
+    }
+
+    passCtr++;
+  }
+
+  LOG_VARD(refMaps.size());
+  LOG_VARD(conflatedMaps.size());
+  if (refMaps.size() == 0 || conflatedMaps.size() == 0)
+  {
+    LOG_DEBUG("No features remain after filtering. Skipping changeset generation...");
+    return;
+  }
+  assert(refMaps.size() == conflatedMaps.size());
+
+  // CHANGESET GENERATION
+
+  // Derive a changeset between the ref and conflated maps that replaces ref features with
+  // secondary features within the bounds and write it out.
+
+  _changesetCreator->create(refMaps, conflatedMaps, output);
+
+  LOG_INFO("Derived replacement changeset: " << output.right(maxFilePrintLength));
+}
+
+void ChangesetReplacementCreator::_getMapsForGeometryType(
+  OsmMapPtr& refMap, OsmMapPtr& conflatedMap, const QString& input1, const QString& input2,
+  const QString& boundsStr, const ElementCriterionPtr& refFeatureFilter,
+  const ElementCriterionPtr& secFeatureFilter,
+  const GeometryTypeCriterion::GeometryType& geometryType,
+  const QStringList& linearFilterClassNames)
+{
+  LOG_VARD(linearFilterClassNames);
+  LOG_VARD(secFeatureFilter);
+
+  // INPUT VALIDATION AND SETUP
+
+  _parseConfigOpts(_lenientBounds, geometryType);
 
   // DATA LOAD AND INITIAL PREP
 
   // Load the ref dataset and crop to the specified aoi.
 
-  OsmMapPtr refMap = _loadRefMap(input1);
+  refMap = _loadRefMap(input1);
 
   // We want to alert the user to the fact their ref versions *could* be being populated incorectly
   // to avoid difficulties during changeset application at the end. Its likely if they are
   // incorrect at this point the changeset derivation will fail at the end anyway, but let's warn
   // now to give the chance to back out earlier.
 
-  const int numberOfRefElementsWithVersionLessThan1 = _versionLessThanOneCount(refMap);
-  if (numberOfRefElementsWithVersionLessThan1 > 0)
-  {
-    LOG_WARN(
-      StringUtils::formatLargeNumber(numberOfRefElementsWithVersionLessThan1) << " features in " <<
-      "the reference map have a version less than one. This could lead to difficulties when " <<
-      "applying the resulting changeset back to an authoritative data store. Are the versions " <<
-      "on the features being populated correctly?")
-  }
+  OsmUtils::checkVersionLessThanOneCountAndLogWarning(refMap);
 
   // Keep a mapping of the original ref element ids to versions, as we'll need the original
   // versions later.
 
   const QMap<ElementId, long> refIdToVersionMappings = _getIdToVersionMappings(refMap);
-  if (lenientBounds && isLinearCrit)
+  const bool isLinearCrit = !linearFilterClassNames.isEmpty();
+  LOG_VARD(isLinearCrit);
+  if (_lenientBounds && isLinearCrit)
   {
     // If we have a lenient bounds requirement and linear features, we need to exclude all ways
     // outside of the bounds but immediately connected to a way crossing the bounds from deletion.
     _addChangesetDeleteExclusionTags(refMap);
   }
 
-  // Prune down the elements to just the feature types specified by the filter.
+  // Prune the ref dataset down to just the geometry types specified by the filter, so we don't end
+  // up modifying anything else.
 
-  _filterFeatures(refMap, featureFilter, "ref-after-type-pruning");
+  _filterFeatures(refMap, refFeatureFilter, conf(), "ref-after-type-pruning");
 
   // Load the sec dataset and crop to the specified aoi.
 
   OsmMapPtr secMap = _loadSecMap(input2);
 
-  // Prune down the elements to just the feature types specified by the filter.
+  // Prune the sec dataset down to just the feature types specified by the filter, so we don't end
+  // up modifying anything else.
+
+  _filterFeatures(secMap, secFeatureFilter, _replacementFilterOptions, "sec-after-type-pruning");
 
-  _filterFeatures(secMap, featureFilter, "sec-after-type-pruning");
+  LOG_VARD(refMap->getElementCount());
+  LOG_VARD(secMap->getElementCount());
+  // If we're empty here, then the filtering must have removed everything...no changeset to
+  // calculate.
+  if (refMap->getElementCount() == 0 && secMap->getElementCount() == 0)
+  {
+    LOG_DEBUG("Both input maps empty after filtering. Skipping changeset generation...");
+    return;
+  }
 
   // COOKIE CUT
 
@@ -161,18 +432,19 @@ void ChangesetReplacementCreator::create(
   // situation again, we can go back in the history to resurrect the use of the ElementIdRemapper
   // for relations here, which has since been removed from the codebase.
 
-  // Combine the cookie cut map back with the secondary map, so we can conflate it with the ref map.
+  // Combine the cookie cut ref map back with the secondary map, so we can conflate the two
+  // together.
 
   _combineMaps(cookieCutRefMap, secMap, false, "combined-before-conflation");
   secMap.reset();
 
   // CONFLATE
 
-  // Conflate the cookie cut ref map with the cropped sec map.
+  // Conflate the cookie cut ref map with the sec map.
 
-  OsmMapPtr conflatedMap = cookieCutRefMap;
+  conflatedMap = cookieCutRefMap;
   // TODO: do something with reviews - #3361
-  _conflate(conflatedMap, lenientBounds);
+  _conflate(conflatedMap, _lenientBounds);
 
   if (isLinearCrit)
   {
@@ -180,9 +452,19 @@ void ChangesetReplacementCreator::create(
     // ref features may have been cut along the bounds. We're being lenient here by snapping
     // secondary to reference *and* allowing conflated data to be snapped to either dataset.
 
-    _snapUnconnectedWays(
-      conflatedMap, "Input2;Conflated", "Input1;Conflated", featureTypeFilterClassName, false,
-      "conflated-snapped-sec-to-ref-1");
+    // We only want to snap ways of like types together, so we'll loop through each applicable
+    // linear type and snap them separately.
+
+    QStringList snapWayStatuses("Input2");
+    snapWayStatuses.append("Conflated");
+    QStringList snapToWayStatuses("Input1");
+    snapToWayStatuses.append("Conflated");
+    for (int i = 0; i < linearFilterClassNames.size(); i++)
+    {
+      _snapUnconnectedWays(
+        conflatedMap, snapWayStatuses, snapToWayStatuses, linearFilterClassNames.at(i), false,
+        "conflated-snapped-sec-to-ref-1");
+    }
 
     // After snapping, perform joining to prevent unnecessary create/delete statements for the ref
     // data in the resulting changeset and generate modify statements instead.
@@ -194,7 +476,7 @@ void ChangesetReplacementCreator::create(
   // PRE-CHANGESET DERIVATION DATA PREP
 
   OsmMapPtr immediatelyConnectedOutOfBoundsWays;
-  if (lenientBounds && isLinearCrit)
+  if (_lenientBounds && isLinearCrit)
   {
     // If we're conflating linear features with the lenient bounds requirement, copy the
     // immediately connected out of bounds ways to a new temp map. We'll lose those ways once we
@@ -203,26 +485,40 @@ void ChangesetReplacementCreator::create(
 
     immediatelyConnectedOutOfBoundsWays = _getImmediatelyConnectedOutOfBoundsWays(refMap);
   }
-  // Crop the ref and conflated maps appropriately for changeset derivation.
 
+  // Crop the original ref and conflated maps appropriately for changeset derivation.
+
+  const geos::geom::Envelope bounds = GeometryUtils::envelopeFromConfigString(boundsStr);
   _cropMapForChangesetDerivation(
-    refMap, bounds, _changesetRefKeepEntireCrossingBounds, _changesetRefKeepOnlyInsideBounds,
-    isLinearCrit, "ref-cropped-for-changeset");
+    refMap, bounds, _boundsOpts.changesetRefKeepEntireCrossingBounds,
+    _boundsOpts.changesetRefKeepOnlyInsideBounds, isLinearCrit, "ref-cropped-for-changeset");
   _cropMapForChangesetDerivation(
-    conflatedMap, bounds, _changesetSecKeepEntireCrossingBounds, _changesetSecKeepOnlyInsideBounds,
-    isLinearCrit, "sec-cropped-for-changeset");
-  if (lenientBounds && isLinearCrit)
+    conflatedMap, bounds, _boundsOpts.changesetSecKeepEntireCrossingBounds,
+    _boundsOpts.changesetSecKeepOnlyInsideBounds, isLinearCrit, "sec-cropped-for-changeset");
+  LOG_VARD(_lenientBounds);
+  LOG_VARD(isLinearCrit);
+  if (_lenientBounds && isLinearCrit)
   {
     // The non-strict way replacement workflow benefits from a second snapping run right before
     // changeset derivation due to there being ways connected to replacement ways that fall
     // completely outside of the bounds. However, joining after this snapping caused changeset
     // errors with some datasets and hasn't seem to be needed for now...so skipping it. Note that
     // we're being as lenient as possible with the snapping here, allowing basically anything to
-    // join to anything else...could end up causing problems...but we'll go with it for now.
-
-    _snapUnconnectedWays(
-      conflatedMap, "Input2;Conflated;Input1", "Input1;Conflated;Input2",
-      featureTypeFilterClassName, false, "conflated-snapped-sec-to-ref-2");
+    // join to anything else, which could end up causing problems...we'll go with it for now.
+
+    QStringList snapWayStatuses("Input2");
+    snapWayStatuses.append("Conflated");
+    snapWayStatuses.append("Input1");
+    QStringList snapToWayStatuses("Input1");
+    snapToWayStatuses.append("Conflated");
+    snapToWayStatuses.append("Input2");
+    LOG_VARD(linearFilterClassNames);
+    for (int i = 0; i < linearFilterClassNames.size(); i++)
+    {
+      _snapUnconnectedWays(
+        conflatedMap, snapWayStatuses, snapToWayStatuses, linearFilterClassNames.at(i), false,
+        "conflated-snapped-sec-to-ref-2");
+    }
 
     // Combine the conflated map with the immediately connected out of bounds ways.
 
@@ -232,9 +528,13 @@ void ChangesetReplacementCreator::create(
     // Snap only the connected ways to other ways in the conflated map. Mark the ways that were
     // snapped, as we'll need that info in the next step.
 
-    _snapUnconnectedWays(
-      conflatedMap, "Input1", "Input1", featureTypeFilterClassName, true,
-      "conflated-snapped-immediately-connected-out-of-bounds");
+    LOG_VARD(linearFilterClassNames);
+    for (int i = 0; i < linearFilterClassNames.size(); i++)
+    {
+      _snapUnconnectedWays(
+        conflatedMap, QStringList("Input1"), QStringList("Input1"), linearFilterClassNames.at(i),
+        true, "conflated-snapped-immediately-connected-out-of-bounds");
+    }
 
     // Remove any ways that weren't snapped.
 
@@ -256,12 +556,8 @@ void ChangesetReplacementCreator::create(
     _excludeFeaturesFromChangesetDeletion(refMap, boundsStr);
   }
 
-  // CHANGESET DERIVATION
-
-  // Derive a changeset between the ref and conflated maps that completely replaces ref features
-  // with secondary features within the bounds and write it out.
-
-  _changesetCreator->create(refMap, conflatedMap, output);
+  LOG_VARD(refMap->getElementCount());
+  LOG_VARD(conflatedMap->getElementCount());
 }
 
 void ChangesetReplacementCreator::_validateInputs(const QString& input1, const QString& input2)
@@ -271,12 +567,14 @@ void ChangesetReplacementCreator::_validateInputs(const QString& input1, const Q
     std::dynamic_pointer_cast<Boundable>(OsmMapReaderFactory::createReader(input1));
   if (!boundable)
   {
-    throw IllegalArgumentException("Reader for " + input1 + " must implement Boundable.");
+    throw IllegalArgumentException(
+      "Reader for " + input1 + " must implement Boundable for replacement changeset derivation.");
   }
   boundable = std::dynamic_pointer_cast<Boundable>(OsmMapReaderFactory::createReader(input2));
   if (!boundable)
   {
-    throw IllegalArgumentException("Reader for " + input2 + " must implement Boundable.");
+    throw IllegalArgumentException(
+      "Reader for " + input2 + " must implement Boundable for replacement changeset derivation.");
   }
 
   // Fail for GeoJSON - GeoJSON coming from Overpass does not have way nodes, so their versions
@@ -285,23 +583,59 @@ void ChangesetReplacementCreator::_validateInputs(const QString& input1, const Q
   OsmGeoJsonReader geoJsonReader;
   if (geoJsonReader.isSupported(input1) || geoJsonReader.isSupported(input2))
   {
-    throw IllegalArgumentException("GeoJSON inputs are not supported.");
+    throw IllegalArgumentException(
+      "GeoJSON inputs are not supported by replacement changeset derivation.");
+  }
+
+  if (_fullReplacement && _retainmentFilter)
+  {
+    throw IllegalArgumentException(
+      "Both full reference data replacement and a reference data retainment filter may not "
+      "be specified for replacement changeset derivation.");
+  }
+
+  if (ConfigOptions().getConvertOps().size())
+  {
+    throw IllegalArgumentException(
+      "Replacement changeset derivation does not support convert operations.");
   }
 }
 
-std::shared_ptr<ConflatableElementCriterion> ChangesetReplacementCreator::_validateFilter(
-  const QString& featureTypeFilterClassName)
+QMap<GeometryTypeCriterion::GeometryType, ElementCriterionPtr>
+  ChangesetReplacementCreator::_getDefaultGeometryFilters() const
 {
-  // Fail if a filter with an unconflatable feature type was specified.
-  std::shared_ptr<ConflatableElementCriterion> featureFilter =
-    std::dynamic_pointer_cast<ConflatableElementCriterion>(
-      std::shared_ptr<ElementCriterion>(
-        Factory::getInstance().constructObject<ElementCriterion>(featureTypeFilterClassName)));
-  if (!featureFilter)
+  QMap<GeometryTypeCriterion::GeometryType, ElementCriterionPtr> featureFilters;
+  featureFilters[GeometryTypeCriterion::GeometryType::Point] =
+    std::shared_ptr<ElementCriterion>(new PointCriterion());
+  featureFilters[GeometryTypeCriterion::GeometryType::Line] =
+    std::shared_ptr<ElementCriterion>(new LinearCriterion());
+  featureFilters[GeometryTypeCriterion::GeometryType::Polygon] =
+    std::shared_ptr<ElementCriterion>(new PolygonCriterion());
+  return featureFilters;
+}
+
+QMap<GeometryTypeCriterion::GeometryType, ElementCriterionPtr>
+  ChangesetReplacementCreator::_getCombinedFilters(
+    std::shared_ptr<ChainCriterion> nonGeometryFilter)
+{
+  QMap<GeometryTypeCriterion::GeometryType, ElementCriterionPtr> combinedFilters;
+  LOG_VARD(nonGeometryFilter.get());
+  if (nonGeometryFilter)
   {
-    throw IllegalArgumentException("Invalid feature type filter: " + featureTypeFilterClassName);
+    for (QMap<GeometryTypeCriterion::GeometryType, ElementCriterionPtr>::const_iterator itr =
+         _geometryTypeFilters.begin(); itr != _geometryTypeFilters.end(); ++itr)
+    {
+      combinedFilters[itr.key()] =
+        std::shared_ptr<ChainCriterion>(new ChainCriterion(itr.value(), nonGeometryFilter));
+      LOG_DEBUG("New combined filter: " << combinedFilters[itr.key()]->toString());
+    }
   }
-  return featureFilter;
+  else
+  {
+    combinedFilters = _geometryTypeFilters;
+  }
+  LOG_VARD(combinedFilters.size());
+  return combinedFilters;
 }
 
 OsmMapPtr ChangesetReplacementCreator::_loadRefMap(const QString& input)
@@ -310,38 +644,27 @@ OsmMapPtr ChangesetReplacementCreator::_loadRefMap(const QString& input)
 
   conf().set(
     ConfigOptions::getConvertBoundingBoxKeepEntireFeaturesCrossingBoundsKey(),
-    _loadRefKeepEntireCrossingBounds);
+    _boundsOpts.loadRefKeepEntireCrossingBounds);
   conf().set(
     ConfigOptions::getConvertBoundingBoxKeepOnlyFeaturesInsideBoundsKey(),
-    _loadRefKeepOnlyInsideBounds);
+    _boundsOpts.loadRefKeepOnlyInsideBounds);
   conf().set(
     ConfigOptions::getConvertBoundingBoxKeepImmediatelyConnectedWaysOutsideBoundsKey(),
-    _loadRefKeepImmediateConnectedWaysOutsideBounds);
+    _boundsOpts.loadRefKeepImmediateConnectedWaysOutsideBounds);
 
   OsmMapPtr refMap(new OsmMap());
   refMap->setName("ref");
   IoUtils::loadMap(refMap, input, true, Status::Unknown1);
 
-  LOG_VARD(MapProjector::toWkt(refMap->getProjection()));
+  LOG_VART(MapProjector::toWkt(refMap->getProjection()));
   OsmMapWriterFactory::writeDebugMap(refMap, "ref-after-cropped-load");
 
   return refMap;
 }
 
-int ChangesetReplacementCreator::_versionLessThanOneCount(const OsmMapPtr& map) const
-{
-  std::shared_ptr<AttributeValueCriterion> attrCrit(
-    new AttributeValueCriterion(
-      ElementAttributeType(ElementAttributeType::Version), 1, NumericComparisonType::LessThan));
-  return
-    (int)FilteredVisitor::getStat(
-      attrCrit, std::shared_ptr<ElementCountVisitor>(new ElementCountVisitor()), map);
-}
-
 QMap<ElementId, long> ChangesetReplacementCreator::_getIdToVersionMappings(
   const OsmMapPtr& map) const
 {
-  LOG_DEBUG("Recording ID to version mappings for: " << map->getName() << "...");
   ElementIdToVersionMapper idToVersionMapper;
   LOG_DEBUG(idToVersionMapper.getInitStatusMessage());
   idToVersionMapper.apply(map);
@@ -391,10 +714,10 @@ OsmMapPtr ChangesetReplacementCreator::_loadSecMap(const QString& input)
 
   conf().set(
     ConfigOptions::getConvertBoundingBoxKeepEntireFeaturesCrossingBoundsKey(),
-    _loadSecKeepEntireCrossingBounds);
+    _boundsOpts.loadSecKeepEntireCrossingBounds);
   conf().set(
     ConfigOptions::getConvertBoundingBoxKeepOnlyFeaturesInsideBoundsKey(),
-    _loadSecKeepOnlyInsideBounds);
+    _boundsOpts.loadSecKeepOnlyInsideBounds);
   conf().set(
     ConfigOptions::getConvertBoundingBoxKeepImmediatelyConnectedWaysOutsideBoundsKey(), false);
 
@@ -402,52 +725,124 @@ OsmMapPtr ChangesetReplacementCreator::_loadSecMap(const QString& input)
   secMap->setName("sec");
   IoUtils::loadMap(secMap, input, false, Status::Unknown2);
 
-  LOG_VARD(MapProjector::toWkt(secMap->getProjection()));
+  LOG_VART(MapProjector::toWkt(secMap->getProjection()));
   OsmMapWriterFactory::writeDebugMap(secMap, "sec-after-cropped-load");
 
   return secMap;
 }
 
 void ChangesetReplacementCreator::_filterFeatures(
-  OsmMapPtr& map, const std::shared_ptr<ConflatableElementCriterion>& featureFilter,
+  OsmMapPtr& map, const ElementCriterionPtr& featureFilter, const Settings& config,
   const QString& debugFileName)
 {
-  LOG_DEBUG("Filtering features for: " << map->getName() << " based on input filter...");
+  LOG_DEBUG(
+    "Filtering features for: " << map->getName() << " based on input filter: " +
+    featureFilter->toString() << "...");
+
   RemoveElementsVisitor elementPruner(true);
+  // The criteria must be added before the config or map is set. We may want to change
+  // MultipleCriterionConsumerVisitor and RemoveElementsVisitor to make this behavior less brittle.
   elementPruner.addCriterion(featureFilter);
+  elementPruner.setConfiguration(config);
+  elementPruner.setOsmMap(map.get());
   elementPruner.setRecursive(true);
   LOG_DEBUG(elementPruner.getInitStatusMessage());
   map->visitRw(elementPruner);
   LOG_DEBUG(elementPruner.getCompletedStatusMessage());
-  LOG_VARD(MapProjector::toWkt(map->getProjection()));
+
+  LOG_VART(MapProjector::toWkt(map->getProjection()));
   OsmMapWriterFactory::writeDebugMap(map, debugFileName);
 }
 
 OsmMapPtr ChangesetReplacementCreator::_getCookieCutMap(OsmMapPtr doughMap, OsmMapPtr cutterMap)
 {
-  LOG_VARD(MapProjector::toWkt(doughMap->getProjection()));
-  LOG_VARD(MapProjector::toWkt(cutterMap->getProjection()));
+  // TODO: could use some refactoring here after the addition of _fullReplacement
 
-  // Generate a cutter shape based on the cropped secondary map.
+  LOG_VARD(doughMap->getElementCount());
+  LOG_VART(MapProjector::toWkt(doughMap->getProjection()));
+  OsmMapWriterFactory::writeDebugMap(doughMap, "dough-map");
+  LOG_VARD(cutterMap->getElementCount());
+  LOG_VART(MapProjector::toWkt(cutterMap->getProjection()));
 
-  LOG_DEBUG("Generating cutter shape map from: " << cutterMap->getName() << "...");
-  OsmMapPtr cutterShapeOutlineMap = AlphaShapeGenerator(1000.0, 0.0).generateMap(cutterMap);
-  // not exactly sure yet why this needs to be done
+  OsmMapPtr cookieCutMap(new OsmMap(doughMap));
+  cookieCutMap->setName("cookie-cut");
+  LOG_VART(MapProjector::toWkt(cookieCutMap->getProjection()));
+  LOG_DEBUG("Preparing to cookie cut: " << cookieCutMap->getName() << "...");
+
+  OsmMapPtr cutterMapToUse;
+  LOG_VARD(cutterMap->getElementCount());
+  ConfigOptions opts(conf());
+  LOG_VARD(OsmUtils::mapIsPointsOnly(cutterMap));
+  double cookieCutterAlpha = opts.getCookieCutterAlpha();
+  double cookieCutterAlphaShapeBuffer = opts.getCookieCutterAlphaShapeBuffer();
+  LOG_VARD(_fullReplacement);
+  if (_fullReplacement)
+  {
+    // Generate a cutter shape based on the ref map, which will cause all the ref data to be
+    // replaced.
+    cutterMapToUse = doughMap;
+    cookieCutterAlphaShapeBuffer = 10.0;
+  }
+  else if (cutterMap->getElementCount() < 3 && OsmUtils::mapIsPointsOnly(cutterMap))
+  {
+    // Generate a cutter shape based on a transformation of the cropped secondary map.
+
+    // Found that if a map only has a couple points or less, generating an alpha shape from them may
+    // not be possible (or at least I don't know how to yet). So instead, go through the points in
+    // the map and replace them with small square polys...from that we can generate the alpha shape.
+
+    cutterMapToUse.reset(new OsmMap(cutterMap));
+    PointsToPolysConverter pointConverter;
+    LOG_DEBUG(pointConverter.getInitStatusMessage());
+    pointConverter.apply(cutterMapToUse);
+    LOG_DEBUG(pointConverter.getCompletedStatusMessage());
+    MapProjector::projectToWgs84(cutterMapToUse);
+  }
+  else
+  {
+    // Generate a cutter shape based on the cropped secondary map.
+    cutterMapToUse = cutterMap;
+  }
+  LOG_VARD(cutterMapToUse->getElementCount());
+  OsmMapWriterFactory::writeDebugMap(cutterMapToUse, "cutter-map");
+
+  LOG_DEBUG("Generating cutter shape map from: " << cutterMapToUse->getName() << "...");
+
+  OsmMapPtr cutterShapeOutlineMap;
+  try
+  {
+    cutterShapeOutlineMap =
+      AlphaShapeGenerator(cookieCutterAlpha, cookieCutterAlphaShapeBuffer)
+        .generateMap(cutterMapToUse);
+  }
+  catch (const HootException& e)
+  {
+    if (e.getWhat().contains("Alpha Shape area is zero"))
+    {
+      LOG_ERROR(
+        "No cut shape generated from secondary data. Is your secondary data empty or have you " <<
+        "filtered it to be empty?");
+    }
+    throw e;
+  }
+
+  // not exactly sure yet why this projection needs to be done
   MapProjector::projectToWgs84(cutterShapeOutlineMap);
-  LOG_VARD(MapProjector::toWkt(cutterShapeOutlineMap->getProjection()));
+  LOG_VART(MapProjector::toWkt(cutterShapeOutlineMap->getProjection()));
   OsmMapWriterFactory::writeDebugMap(cutterShapeOutlineMap, "cutter-shape");
 
   // Cookie cut the shape of the cutter shape map out of the cropped ref map.
+  LOG_DEBUG("Cookie cutting cutter shape out of: " << cookieCutMap->getName() << "...");
 
-  LOG_DEBUG("Cookie cutting cutter shape out of: " << doughMap->getName() << "...");
-  OsmMapPtr cookieCutMap(new OsmMap(doughMap));
-  //OsmMapPtr cookieCutMap(new OsmMap(doughMap, MapProjector::createWgs84Projection()));
-  LOG_VARD(MapProjector::toWkt(cookieCutMap->getProjection()));
-  cookieCutMap->setName("cookie-cut");
-  CookieCutter(false, 0.0, _cookieCutKeepEntireCrossingBounds, _cookieCutKeepOnlyInsideBounds)
+  CookieCutter(
+    false, 0.0, _boundsOpts.cookieCutKeepEntireCrossingBounds,
+    _boundsOpts.cookieCutKeepOnlyInsideBounds)
     .cut(cutterShapeOutlineMap, cookieCutMap);
   MapProjector::projectToWgs84(cookieCutMap); // not exactly sure yet why this needs to be done
-  LOG_VARD(MapProjector::toWkt(cookieCutMap->getProjection()));
+  LOG_VARD(cookieCutMap->getElementCount());
+  MapProjector::projectToWgs84(doughMap);
+  LOG_VARD(doughMap->getElementCount());
+  LOG_VART(MapProjector::toWkt(cookieCutMap->getProjection()));
   OsmMapWriterFactory::writeDebugMap(cookieCutMap, "cookie-cut");
 
   return cookieCutMap;
@@ -465,7 +860,7 @@ void ChangesetReplacementCreator::_combineMaps(OsmMapPtr& map1, OsmMapPtr& map2,
   MapProjector::projectToWgs84(map2);   // not exactly sure yet why this needs to be done
 
   map1->append(map2, throwOutDupes);
-  LOG_VARD(MapProjector::toWkt(map1->getProjection()));
+  LOG_VART(MapProjector::toWkt(map1->getProjection()));
 
   OsmMapWriterFactory::writeDebugMap(map1, debugFileName);
 }
@@ -494,35 +889,36 @@ void ChangesetReplacementCreator::_conflate(OsmMapPtr& map, const bool lenientBo
   NamedOp postOps(ConfigOptions().getConflatePostOps());
   postOps.apply(map);
   MapProjector::projectToWgs84(map);  // conflation works in planar
-  LOG_VARD(MapProjector::toWkt(map->getProjection()));
+  LOG_VART(MapProjector::toWkt(map->getProjection()));
   OsmMapWriterFactory::writeDebugMap(map, "conflated");
 }
 
-void ChangesetReplacementCreator::_snapUnconnectedWays(OsmMapPtr& map, const QString& snapWayStatus,
-                                                       const QString& snapToWayStatus,
-                                                       const QString& featureTypeFilterClassName,
-                                                       const bool markSnappedWays,
-                                                       const QString& debugFileName)
+void ChangesetReplacementCreator::_snapUnconnectedWays(
+  OsmMapPtr& map, const QStringList& snapWayStatuses, const QStringList& snapToWayStatuses,
+  const QString& typeCriterionClassName, const bool markSnappedWays, const QString& debugFileName)
 {
-  LOG_DEBUG("Snapping ways for map: " << map->getName() <<" ...");
+  LOG_DEBUG(
+    "Snapping ways for map: " << map->getName() << ", with filter type: " <<
+    typeCriterionClassName << ", snap way statuses: " << snapWayStatuses <<
+    ", snap to way statuses: " << snapToWayStatuses << " ...");
 
   UnconnectedWaySnapper lineSnapper;
   lineSnapper.setConfiguration(conf());
   // override some of the default config
-  lineSnapper.setSnapToWayStatus(snapToWayStatus);
-  lineSnapper.setSnapWayStatus(snapWayStatus);
+  lineSnapper.setSnapToWayStatuses(snapToWayStatuses);
+  lineSnapper.setSnapWayStatuses(snapWayStatuses);
   lineSnapper.setMarkSnappedWays(markSnappedWays);
-  // TODO: hack - need a way to derive the way node crit from the input feature filter crit
+  // TODO: Do we need a way to derive the way node crit from the input feature filter crit?
   lineSnapper.setWayNodeToSnapToCriterionClassName(
     QString::fromStdString(WayNodeCriterion::className()));
-  lineSnapper.setWayToSnapCriterionClassName(featureTypeFilterClassName);
-  lineSnapper.setWayToSnapToCriterionClassName(featureTypeFilterClassName);
+  lineSnapper.setWayToSnapCriterionClassName(typeCriterionClassName);
+  lineSnapper.setWayToSnapToCriterionClassName(typeCriterionClassName);
   LOG_DEBUG(lineSnapper.getInitStatusMessage());
   lineSnapper.apply(map);
   LOG_DEBUG(lineSnapper.getCompletedStatusMessage());
 
   MapProjector::projectToWgs84(map);   // snapping works in planar
-  LOG_VARD(MapProjector::toWkt(map->getProjection()));
+  LOG_VART(MapProjector::toWkt(map->getProjection()));
 
   OsmMapWriterFactory::writeDebugMap(map, debugFileName);
 }
@@ -542,7 +938,7 @@ OsmMapPtr ChangesetReplacementCreator::_getImmediatelyConnectedOutOfBoundsWays(
         new TagKeyCriterion(MetadataTags::HootConnectedWayOutsideBounds()))));
   OsmMapPtr connectedWays = OsmUtils::getMapSubset(map, copyCrit);
   connectedWays->setName(outputMapName);
-  LOG_VARD(MapProjector::toWkt(connectedWays->getProjection()));
+  LOG_VART(MapProjector::toWkt(connectedWays->getProjection()));
   OsmMapWriterFactory::writeDebugMap(connectedWays, "connected-ways");
   return connectedWays;
 }
@@ -552,11 +948,13 @@ void ChangesetReplacementCreator::_cropMapForChangesetDerivation(
   const bool keepOnlyFeaturesInsideBounds, const bool isLinearMap, const QString& debugFileName)
 {
   LOG_DEBUG("Cropping map: " << map->getName() << " for changeset derivation...");
+  LOG_VART(MapProjector::toWkt(map->getProjection()));
+  //LOG_VART(map->getProjection()->GetName());
 
   MapCropper cropper(bounds);
   cropper.setKeepEntireFeaturesCrossingBounds(keepEntireFeaturesCrossingBounds);
   cropper.setKeepOnlyFeaturesInsideBounds(keepOnlyFeaturesInsideBounds);
-  LOG_DEBUG(cropper.getInitStatusMessage());
+  //LOG_DEBUG(cropper.getInitStatusMessage());
   cropper.apply(map);
   LOG_DEBUG(cropper.getCompletedStatusMessage());
 
@@ -565,7 +963,7 @@ void ChangesetReplacementCreator::_cropMapForChangesetDerivation(
   // with no information.
   SuperfluousNodeRemover::removeNodes(map, isLinearMap);
 
-  LOG_VARD(MapProjector::toWkt(map->getProjection()));
+  LOG_VART(MapProjector::toWkt(map->getProjection()));
   OsmMapWriterFactory::writeDebugMap(map, debugFileName);
 }
 
@@ -589,7 +987,7 @@ void ChangesetReplacementCreator::_removeUnsnappedImmediatelyConnectedOutOfBound
   LOG_DEBUG(removeVis.getInitStatusMessage());
   map->visitRw(removeVis);
   LOG_DEBUG(removeVis.getCompletedStatusMessage());
-  LOG_VARD(MapProjector::toWkt(map->getProjection()));
+  LOG_VART(MapProjector::toWkt(map->getProjection()));
   OsmMapWriterFactory::writeDebugMap(map, map->getName() + "-unsnapped-removed");
 }
 
@@ -599,7 +997,7 @@ void ChangesetReplacementCreator::_excludeFeaturesFromChangesetDeletion(OsmMapPt
   LOG_DEBUG(
     "Marking reference features in: " << map->getName() << " for exclusion from deletion...");
 
-  std::shared_ptr<InBoundsCriterion> boundsCrit(new InBoundsCriterion(_inBoundsStrict));
+  std::shared_ptr<InBoundsCriterion> boundsCrit(new InBoundsCriterion(_boundsOpts.inBoundsStrict));
   boundsCrit->setBounds(GeometryUtils::envelopeFromConfigString(boundsStr));
   boundsCrit->setOsmMap(map.get());
   std::shared_ptr<NotCriterion> notInBoundsCrit(new NotCriterion(boundsCrit));
@@ -611,7 +1009,7 @@ void ChangesetReplacementCreator::_excludeFeaturesFromChangesetDeletion(OsmMapPt
   tagSetter.apply(map);
   LOG_DEBUG(tagSetter.getCompletedStatusMessage());
 
-  LOG_VARD(MapProjector::toWkt(map->getProjection()));
+  LOG_VART(MapProjector::toWkt(map->getProjection()));
   OsmMapWriterFactory::writeDebugMap(map, map->getName() + "-after-delete-exclude-tags");
 }
 
@@ -622,12 +1020,8 @@ bool ChangesetReplacementCreator::_isNetworkConflate() const
       QString::fromStdString(NetworkMatchCreator::className()));
 }
 
-void ChangesetReplacementCreator::_parseConfigOpts(
-  const bool lenientBounds, const std::shared_ptr<ConflatableElementCriterion>& featureFilter,
-  const QString& boundsStr)
+void ChangesetReplacementCreator::_setGlobalOpts(const QString& boundsStr)
 {
-  // global opts
-
   conf().set(ConfigOptions::getChangesetXmlWriterAddTimestampKey(), false);
   conf().set(ConfigOptions::getReaderAddSourceDatetimeKey(), false);
   conf().set(ConfigOptions::getWriterIncludeCircularErrorTagsKey(), false);
@@ -635,64 +1029,65 @@ void ChangesetReplacementCreator::_parseConfigOpts(
   // For this being enabled to have any effect,
   // convert.bounding.box.keep.immediately.connected.ways.outside.bounds must be enabled as well.
   conf().set(ConfigOptions::getConvertBoundingBoxTagImmediatelyConnectedOutOfBoundsWaysKey(), true);
+  // turn on for testing only
   //conf().set(ConfigOptions::getDebugMapsWriteKey(), true);
 
-  // dataset specific opts
-
   // These don't change between scenarios (or at least haven't needed to yet).
-  _loadRefKeepOnlyInsideBounds = false;
-  _cookieCutKeepOnlyInsideBounds = false;
-  _changesetRefKeepOnlyInsideBounds = false;
+  _boundsOpts.loadRefKeepOnlyInsideBounds = false;
+  _boundsOpts.cookieCutKeepOnlyInsideBounds = false;
+  _boundsOpts.changesetRefKeepOnlyInsideBounds = false;
+}
 
+void ChangesetReplacementCreator::_parseConfigOpts(
+  const bool lenientBounds, const GeometryTypeCriterion::GeometryType& geometryType)
+{
   // only one of these should ever be true
 
-  if (featureFilter->getGeometryType() ==
-        ConflatableElementCriterion::ConflatableGeometryType::Point)
+  if (geometryType == GeometryTypeCriterion::GeometryType::Point)
   {
     if (lenientBounds)
     {
       LOG_WARN("--lenient-bounds option ignored with point datasets.");
     }
 
-    _loadRefKeepEntireCrossingBounds = false;
-    _loadRefKeepImmediateConnectedWaysOutsideBounds = false;
-    _loadSecKeepEntireCrossingBounds = false;
-    _loadSecKeepOnlyInsideBounds = false;
-    _cookieCutKeepEntireCrossingBounds = false;
-    _changesetRefKeepEntireCrossingBounds = false;
-    _changesetSecKeepEntireCrossingBounds = false;
-    _changesetSecKeepOnlyInsideBounds = true;
-    _changesetAllowDeletingRefOutsideBounds = true;
-    _inBoundsStrict = false;
+    _boundsOpts.loadRefKeepEntireCrossingBounds = false;
+    _boundsOpts.loadRefKeepImmediateConnectedWaysOutsideBounds = false;
+    _boundsOpts.loadSecKeepEntireCrossingBounds = false;
+    _boundsOpts.loadSecKeepOnlyInsideBounds = false;
+    _boundsOpts.cookieCutKeepEntireCrossingBounds = false;
+    _boundsOpts.changesetRefKeepEntireCrossingBounds = false;
+    _boundsOpts.changesetSecKeepEntireCrossingBounds = false;
+    _boundsOpts.changesetSecKeepOnlyInsideBounds = true;
+    _boundsOpts.changesetAllowDeletingRefOutsideBounds = true;
+    _boundsOpts.inBoundsStrict = false;
   }
-  else if (featureFilter->getGeometryType() ==
-             ConflatableElementCriterion::ConflatableGeometryType::Line)
+  else if (geometryType == GeometryTypeCriterion::GeometryType::Line)
   {
     if (lenientBounds)
     {
-      _loadRefKeepEntireCrossingBounds = true;
-      _loadRefKeepImmediateConnectedWaysOutsideBounds = true;
-      _loadSecKeepEntireCrossingBounds = true;
-      _loadSecKeepOnlyInsideBounds = false;
-      _cookieCutKeepEntireCrossingBounds = false;
-      _changesetRefKeepEntireCrossingBounds = true;
-      _changesetSecKeepEntireCrossingBounds = true;
-      _changesetSecKeepOnlyInsideBounds = false;
-      _changesetAllowDeletingRefOutsideBounds = true;
-      _inBoundsStrict = false;
+      _boundsOpts.loadRefKeepEntireCrossingBounds = true;
+      _boundsOpts.loadRefKeepImmediateConnectedWaysOutsideBounds = true;
+      _boundsOpts.loadSecKeepEntireCrossingBounds = true;
+      _boundsOpts.loadSecKeepOnlyInsideBounds = false;
+      _boundsOpts.cookieCutKeepEntireCrossingBounds = false;
+      _boundsOpts.changesetRefKeepEntireCrossingBounds = true;
+      _boundsOpts.changesetSecKeepEntireCrossingBounds = true;
+      _boundsOpts.changesetSecKeepOnlyInsideBounds = false;
+      _boundsOpts.changesetAllowDeletingRefOutsideBounds = true;
+      _boundsOpts.inBoundsStrict = false;
     }
     else
     {
-      _loadRefKeepEntireCrossingBounds = true;
-      _loadRefKeepImmediateConnectedWaysOutsideBounds = false;
-      _loadSecKeepEntireCrossingBounds = false;
-      _loadSecKeepOnlyInsideBounds = false;
-      _cookieCutKeepEntireCrossingBounds = false;
-      _changesetRefKeepEntireCrossingBounds = true;
-      _changesetSecKeepEntireCrossingBounds = true;
-      _changesetSecKeepOnlyInsideBounds = false;
-      _changesetAllowDeletingRefOutsideBounds = false;
-      _inBoundsStrict = false;
+      _boundsOpts.loadRefKeepEntireCrossingBounds = true;
+      _boundsOpts.loadRefKeepImmediateConnectedWaysOutsideBounds = false;
+      _boundsOpts.loadSecKeepEntireCrossingBounds = false;
+      _boundsOpts.loadSecKeepOnlyInsideBounds = false;
+      _boundsOpts.cookieCutKeepEntireCrossingBounds = false;
+      _boundsOpts.changesetRefKeepEntireCrossingBounds = true;
+      _boundsOpts.changesetSecKeepEntireCrossingBounds = true;
+      _boundsOpts.changesetSecKeepOnlyInsideBounds = false;
+      _boundsOpts.changesetAllowDeletingRefOutsideBounds = false;
+      _boundsOpts.inBoundsStrict = false;
 
       // Conflate way joining needs to happen later in the post ops for strict linear replacements.
       // Changing the default ordering of the post ops to accomodate this had detrimental effects
@@ -711,34 +1106,33 @@ void ChangesetReplacementCreator::_parseConfigOpts(
       LOG_VARD(conf().getList(ConfigOptions::getConflatePostOpsKey()));
     }
   }
-  else if (featureFilter->getGeometryType() ==
-             ConflatableElementCriterion::ConflatableGeometryType::Polygon)
+  else if (geometryType == GeometryTypeCriterion::GeometryType::Polygon)
   {
     if (lenientBounds)
     {
-      _loadRefKeepEntireCrossingBounds = true;
-      _loadRefKeepImmediateConnectedWaysOutsideBounds = false;
-      _loadSecKeepEntireCrossingBounds = true;
-      _loadSecKeepOnlyInsideBounds = false;
-      _cookieCutKeepEntireCrossingBounds = true;
-      _changesetRefKeepEntireCrossingBounds = true;
-      _changesetSecKeepEntireCrossingBounds = true;
-      _changesetSecKeepOnlyInsideBounds = false;
-      _changesetAllowDeletingRefOutsideBounds = true;
-      _inBoundsStrict = false;
+      _boundsOpts.loadRefKeepEntireCrossingBounds = true;
+      _boundsOpts.loadRefKeepImmediateConnectedWaysOutsideBounds = false;
+      _boundsOpts.loadSecKeepEntireCrossingBounds = true;
+      _boundsOpts.loadSecKeepOnlyInsideBounds = false;
+      _boundsOpts.cookieCutKeepEntireCrossingBounds = true;
+      _boundsOpts.changesetRefKeepEntireCrossingBounds = true;
+      _boundsOpts.changesetSecKeepEntireCrossingBounds = true;
+      _boundsOpts.changesetSecKeepOnlyInsideBounds = false;
+      _boundsOpts.changesetAllowDeletingRefOutsideBounds = true;
+      _boundsOpts.inBoundsStrict = false;
     }
     else
     {
-      _loadRefKeepEntireCrossingBounds = true;
-      _loadRefKeepImmediateConnectedWaysOutsideBounds = false;
-      _loadSecKeepEntireCrossingBounds = false;
-      _loadSecKeepOnlyInsideBounds = true;
-      _cookieCutKeepEntireCrossingBounds = true;
-      _changesetRefKeepEntireCrossingBounds = true;
-      _changesetSecKeepEntireCrossingBounds = false;
-      _changesetSecKeepOnlyInsideBounds = true;
-      _changesetAllowDeletingRefOutsideBounds = false;
-      _inBoundsStrict = true;
+      _boundsOpts.loadRefKeepEntireCrossingBounds = true;
+      _boundsOpts.loadRefKeepImmediateConnectedWaysOutsideBounds = false;
+      _boundsOpts.loadSecKeepEntireCrossingBounds = false;
+      _boundsOpts.loadSecKeepOnlyInsideBounds = true;
+      _boundsOpts.cookieCutKeepEntireCrossingBounds = true;
+      _boundsOpts.changesetRefKeepEntireCrossingBounds = true;
+      _boundsOpts.changesetSecKeepEntireCrossingBounds = false;
+      _boundsOpts.changesetSecKeepOnlyInsideBounds = true;
+      _boundsOpts.changesetAllowDeletingRefOutsideBounds = false;
+      _boundsOpts.inBoundsStrict = true;
     }
   }
   else
@@ -749,21 +1143,21 @@ void ChangesetReplacementCreator::_parseConfigOpts(
 
   conf().set(
     ConfigOptions::getChangesetReplacementAllowDeletingReferenceFeaturesOutsideBoundsKey(),
-    _changesetAllowDeletingRefOutsideBounds);
-
-  LOG_VARD(_loadRefKeepEntireCrossingBounds);
-  LOG_VARD(_loadRefKeepOnlyInsideBounds);
-  LOG_VARD(_loadRefKeepImmediateConnectedWaysOutsideBounds);
-  LOG_VARD(_loadSecKeepEntireCrossingBounds);
-  LOG_VARD(_loadSecKeepOnlyInsideBounds);
-  LOG_VARD(_cookieCutKeepEntireCrossingBounds);
-  LOG_VARD(_cookieCutKeepOnlyInsideBounds);
-  LOG_VARD(_changesetRefKeepEntireCrossingBounds);
-  LOG_VARD(_changesetRefKeepOnlyInsideBounds);
-  LOG_VARD(_changesetSecKeepEntireCrossingBounds);
-  LOG_VARD(_changesetSecKeepOnlyInsideBounds);
-  LOG_VARD(_changesetAllowDeletingRefOutsideBounds);
-  LOG_VARD(_inBoundsStrict);
+    _boundsOpts.changesetAllowDeletingRefOutsideBounds);
+
+  LOG_VARD(_boundsOpts.loadRefKeepEntireCrossingBounds);
+  LOG_VARD(_boundsOpts.loadRefKeepOnlyInsideBounds);
+  LOG_VARD(_boundsOpts.loadRefKeepImmediateConnectedWaysOutsideBounds);
+  LOG_VARD(_boundsOpts.loadSecKeepEntireCrossingBounds);
+  LOG_VARD(_boundsOpts.loadSecKeepOnlyInsideBounds);
+  LOG_VARD(_boundsOpts.cookieCutKeepEntireCrossingBounds);
+  LOG_VARD(_boundsOpts.cookieCutKeepOnlyInsideBounds);
+  LOG_VARD(_boundsOpts.changesetRefKeepEntireCrossingBounds);
+  LOG_VARD(_boundsOpts.changesetRefKeepOnlyInsideBounds);
+  LOG_VARD(_boundsOpts.changesetSecKeepEntireCrossingBounds);
+  LOG_VARD(_boundsOpts.changesetSecKeepOnlyInsideBounds);
+  LOG_VARD(_boundsOpts.changesetAllowDeletingRefOutsideBounds);
+  LOG_VARD(_boundsOpts.inBoundsStrict);
 }
 
 }
Clone this wiki locally