Skip to content

v0.2.55..v0.2.56 changeset ChangesetReplacementCreator.cpp

Garret Voltz edited this page Aug 14, 2020 · 3 revisions
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 c2f23e0..ce6d265 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
@@ -36,10 +36,10 @@
 #include <hoot/core/conflate/CookieCutter.h>
 #include <hoot/core/conflate/SuperfluousConflateOpRemover.h>
 #include <hoot/core/conflate/UnifyingConflator.h>
-#include <hoot/core/conflate/network/NetworkMatchCreator.h>
 
 #include <hoot/core/criterion/ConflatableElementCriterion.h>
 #include <hoot/core/criterion/ElementTypeCriterion.h>
+#include <hoot/core/criterion/HighwayCriterion.h>
 #include <hoot/core/criterion/InBoundsCriterion.h>
 #include <hoot/core/criterion/LinearCriterion.h>
 #include <hoot/core/criterion/NotCriterion.h>
@@ -90,7 +90,9 @@
 #include <hoot/core/visitors/FilteredVisitor.h>
 #include <hoot/core/visitors/RemoveElementsVisitor.h>
 #include <hoot/core/visitors/RemoveDuplicateRelationMembersVisitor.h>
+#include <hoot/core/visitors/RemoveInvalidMultilineStringMembersVisitor.h>
 #include <hoot/core/visitors/RemoveMissingElementsVisitor.h>
+#include <hoot/core/visitors/RemoveTagsVisitor.h>
 #include <hoot/core/visitors/ReportMissingElementsVisitor.h>
 #include <hoot/core/visitors/SetTagValueVisitor.h>
 
@@ -100,14 +102,15 @@ namespace hoot
 ChangesetReplacementCreator::ChangesetReplacementCreator(
   const bool printStats, const QString& statsOutputFile, const QString osmApiDbUrl) :
 _fullReplacement(false),
-_lenientBounds(true),
+_boundsInterpretation(BoundsInterpretation::Lenient),
 _geometryFiltersSpecified(false),
 _chainReplacementFilters(false),
 _chainRetainmentFilters(false),
 _waySnappingEnabled(true),
 _conflationEnabled(true),
 _cleaningEnabled(true),
-_tagOobConnectedWays(false)
+_tagOobConnectedWays(false),
+_currentChangeDerivationPassIsLinear(false)
 {
   _changesetCreator.reset(new ChangesetCreator(printStats, statsOutputFile, osmApiDbUrl));
 
@@ -285,17 +288,32 @@ void ChangesetReplacementCreator::setReplacementFilterOptions(const QStringList&
   _setInputFilterOptions(_replacementFilterOptions, optionKvps);
 }
 
-QString ChangesetReplacementCreator::_getJobDescription(
-  const QString& input1, const QString& input2, const QString& bounds,
-  const QString& output) const
+QString ChangesetReplacementCreator::_boundsInterpretationToString(
+  const BoundsInterpretation& boundsInterpretation) const
 {
-  const int maxFilePrintLength = ConfigOptions().getProgressVarPrintLengthMax();
-  QString lenientStr = "Bounds calculation is ";
-  if (!_lenientBounds)
+  switch (boundsInterpretation)
   {
-    lenientStr += "not ";
+    case BoundsInterpretation::Lenient:
+      return "lenient";
+
+    case BoundsInterpretation::Strict:
+      return "strict";
+
+    case BoundsInterpretation::Hybrid:
+      return "hybrid";
+
+    default:
+      return "";
   }
-  lenientStr += "lenient.";
+}
+
+void ChangesetReplacementCreator::_printJobDescription(
+  const QString& input1, const QString& input2, const QString& bounds,
+  const QString& output) const
+{
+  const int maxFilePrintLength = ConfigOptions().getProgressVarPrintLengthMax() * 2;
+  QString boundsStr = "Bounds calculation is " +
+    _boundsInterpretationToString(_boundsInterpretation);
   const QString replacementTypeStr = _fullReplacement ? "full" : "overlapping only";
   QString geometryFiltersStr = "are ";
   if (!_geometryFiltersSpecified)
@@ -345,7 +363,10 @@ QString ChangesetReplacementCreator::_getJobDescription(
   str += "\nBeing replaced: ..." + input1.right(maxFilePrintLength);
   str += "\nReplacing with ..." + input2.right(maxFilePrintLength);
   str += "\nOutput Changeset: ..." + output.right(maxFilePrintLength);
-  str += "\nBounds: " + bounds + lenientStr;
+  LOG_STATUS(str);
+
+  str = "";
+  str += "\nBounds interpretation: " + bounds + "; " + boundsStr;
   str += "\nReplacement is: " + replacementTypeStr;
   str += "\nGeometry filters: " + geometryFiltersStr;
   str += "\nReplacement filter: " + replacementFiltersStr;
@@ -354,7 +375,7 @@ QString ChangesetReplacementCreator::_getJobDescription(
   str += "\nCleaning: " + cleaningStr;
   str += "\nWay snapping: " + waySnappingStr;
   str += "\nOut of bounds way handling: " + oobWayHandlingStr;
-  return str;
+  LOG_DEBUG(str);
 }
 
 void ChangesetReplacementCreator::setRetainmentFilterOptions(const QStringList& optionKvps)
@@ -369,16 +390,15 @@ void ChangesetReplacementCreator::create(
 {
   // INPUT VALIDATION AND SETUP
 
-  _validateInputs(input1, input2);
+  _validateInputs(input1, input2, output);
   const QString boundsStr = GeometryUtils::envelopeToConfigString(bounds);
   _setGlobalOpts(boundsStr);
-
-  LOG_INFO(_getJobDescription(input1, input2, boundsStr, output));
+  _printJobDescription(input1, input2, boundsStr, output);
 
   // 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);
+    _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 =
@@ -395,8 +415,8 @@ void ChangesetReplacementCreator::create(
   for (QMap<GeometryTypeCriterion::GeometryType, ElementCriterionPtr>::const_iterator itr =
          refFilters.begin(); itr != refFilters.end(); ++itr)
   {
-    LOG_INFO("******************************************");
-    LOG_INFO(
+    LOG_STATUS("******************************************");
+    LOG_STATUS(
       "Preparing maps for changeset derivation given geometry type: "<<
       GeometryTypeCriterion::typeToString(itr.key()) << ". Pass: " << passCtr << " / " <<
       refFilters.size() << "...");
@@ -412,9 +432,7 @@ void ChangesetReplacementCreator::create(
     }
 
     ElementCriterionPtr refFilter = itr.value();
-    LOG_VARD(refFilter->toString());
     ElementCriterionPtr secFilter = secFilters[itr.key()];
-    LOG_VARD(secFilter->toString());
 
     _getMapsForGeometryType(
       refMap, conflatedMap, input1, input2, boundsStr, refFilter, secFilter, itr.key(),
@@ -442,6 +460,8 @@ void ChangesetReplacementCreator::create(
         "Adding ref map of size: " << refMap->size() << " and conflated map of size: " <<
         conflatedMap->size() << " to changeset derivation queue for geometry type: " <<
         GeometryTypeCriterion::typeToString(itr.key()) << "...");
+      // TODO: move set name here to inside _getMapsForGeometryType, so we can see the geometry type
+      // in the name??
       refMap->setName(refMap->getName() + "-" + GeometryTypeCriterion::typeToString(itr.key()));
       refMaps.append(refMap);
       conflatedMap->setName(
@@ -502,19 +522,26 @@ void ChangesetReplacementCreator::_getMapsForGeometryType(
   const GeometryTypeCriterion::GeometryType& geometryType,
   const QStringList& linearFilterClassNames)
 {
-  LOG_VART(linearFilterClassNames);
-  LOG_VART(secFeatureFilter);
+  LOG_VARD(linearFilterClassNames);
+  LOG_VARD(refFeatureFilter->toString());
+  LOG_VARD(secFeatureFilter->toString());
 
   // INPUT VALIDATION AND SETUP
 
-  _parseConfigOpts(_lenientBounds, geometryType);
+  _parseConfigOpts(geometryType);
 
   // DATA LOAD AND INITIAL PREP
 
-  // load the ref dataset and crop to the specified aoi
+  // load the ref dataset and crop to the specified aoi; can't cache here b/c the map is cropped
+  // during reading differently based on the geometry type
+  // TODO: we could maybe load the full map the first time and then crop the raw map only based
+  // on geometry type each time, rather than reload it from the source
   refMap = _loadRefMap(input1);
   MemoryUsageChecker::getInstance().check();
 
+  // always remove any existing missing child tags
+  RemoveTagsVisitor missingChildTagRemover(QStringList(MetadataTags::HootMissingChild()));
+  refMap->visitRw(missingChildTagRemover);
   const bool markMissing =
     ConfigOptions().getChangesetReplacementMarkElementsWithMissingChildren();
   if (markMissing)
@@ -530,9 +557,9 @@ void ChangesetReplacementCreator::_getMapsForGeometryType(
   // versions later.
   const QMap<ElementId, long> refIdToVersionMappings = _getIdToVersionMappings(refMap);
 
-  const bool isLinearCrit = !linearFilterClassNames.isEmpty();
-  LOG_VART(isLinearCrit);
-  if (_tagOobConnectedWays && _lenientBounds && isLinearCrit)
+  _currentChangeDerivationPassIsLinear = !linearFilterClassNames.isEmpty();
+  LOG_VART(_currentChangeDerivationPassIsLinear);
+  if (_tagOobConnectedWays && _currentChangeDerivationPassIsLinear)
   {
     // 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.
@@ -545,10 +572,11 @@ void ChangesetReplacementCreator::_getMapsForGeometryType(
     refMap, refFeatureFilter, conf(),
     "ref-after-" + GeometryTypeCriterion::typeToString(geometryType) + "-pruning");
 
-  // load the sec dataset and crop to the specified aoi
+  // load the sec dataset and crop to the specified aoi; see note for ref map load
   OsmMapPtr secMap = _loadSecMap(input2);
   MemoryUsageChecker::getInstance().check();
 
+  secMap->visitRw(missingChildTagRemover);
   if (markMissing)
   {
     _markElementsWithMissingChildren(secMap);
@@ -556,8 +584,10 @@ void ChangesetReplacementCreator::_getMapsForGeometryType(
 
   // Prune the sec dataset down to just the feature types specified by the filter, so we don't end
   // up modifying anything else.
+  const Settings secFilterSettings =
+    _replacementFilterOptions.size() == 0 ? conf() : _replacementFilterOptions;
   _filterFeatures(
-    secMap, secFeatureFilter, _replacementFilterOptions,
+    secMap, secFeatureFilter, secFilterSettings,
     "sec-after-" + GeometryTypeCriterion::typeToString(geometryType) + "-pruning");
 
   const int refMapSize = refMap->size();
@@ -569,8 +599,10 @@ void ChangesetReplacementCreator::_getMapsForGeometryType(
 
   // CUT
 
+  const geos::geom::Envelope replacementBounds = GeometryUtils::envelopeFromConfigString(boundsStr);
+
   // cut the secondary data out of the reference data
-  OsmMapPtr cookieCutRefMap = _getCookieCutMap(refMap, secMap, geometryType);
+  OsmMapPtr cookieCutRefMap = _getCookieCutMap(refMap, secMap, geometryType, replacementBounds);
 
   // At one point it was necessary to re-number the relations in the sec map, as they could have ID
   // overlap with those in the cookie cut ref map at this point. It seemed that this was due to the
@@ -588,13 +620,15 @@ void ChangesetReplacementCreator::_getMapsForGeometryType(
 
   // conflate the cookie cut ref map with the sec map if conflation is enabled
 
+  // TODO: rename var since this map isn't necessary conflated; also rename everything in terms of
+  // "toReplace" and "replacement"
   conflatedMap = cookieCutRefMap;
   if (secMapSize > 0)
   {
     if (_conflationEnabled)
     {
       // conflation cleans beforehand
-      _conflate(conflatedMap, _lenientBounds);
+      _conflate(conflatedMap);
       conflatedMap->setName("conflated");
 
       if (!ConfigOptions().getChangesetReplacementPassConflateReviews())
@@ -614,7 +648,7 @@ void ChangesetReplacementCreator::_getMapsForGeometryType(
 
   // SNAP
 
-  if (isLinearCrit && _waySnappingEnabled)
+  if (_currentChangeDerivationPassIsLinear && _waySnappingEnabled)
   {
     // Snap secondary features back to reference features if dealing with linear features where
     // ref features may have been cut along the bounds. We're being lenient here by snapping
@@ -622,6 +656,7 @@ void ChangesetReplacementCreator::_getMapsForGeometryType(
     // want to snap ways of like types together, so we'll loop through each applicable linear type
     // and snap them separately.
 
+    LOG_STATUS("Snapping unconnected ways to each other...");
     QStringList snapWayStatuses("Input2");
     snapWayStatuses.append("Conflated");
     QStringList snapToWayStatuses("Input1");
@@ -642,7 +677,8 @@ void ChangesetReplacementCreator::_getMapsForGeometryType(
   // PRE-CHANGESET DERIVATION DATA PREP
 
   OsmMapPtr immediatelyConnectedOutOfBoundsWays;
-  if (_lenientBounds && isLinearCrit)
+  if (_boundsInterpretation == BoundsInterpretation::Lenient &&
+      _currentChangeDerivationPassIsLinear)
   {
     // 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
@@ -653,17 +689,15 @@ void ChangesetReplacementCreator::_getMapsForGeometryType(
   }
 
   // Crop the original ref and conflated maps appropriately for changeset derivation.
-  const geos::geom::Envelope bounds = GeometryUtils::envelopeFromConfigString(boundsStr);
   _cropMapForChangesetDerivation(
-    refMap, bounds, _boundsOpts.changesetRefKeepEntireCrossingBounds,
+    refMap, replacementBounds, _boundsOpts.changesetRefKeepEntireCrossingBounds,
     _boundsOpts.changesetRefKeepOnlyInsideBounds, "ref-cropped-for-changeset");
   _cropMapForChangesetDerivation(
-    conflatedMap, bounds, _boundsOpts.changesetSecKeepEntireCrossingBounds,
+    conflatedMap, replacementBounds, _boundsOpts.changesetSecKeepEntireCrossingBounds,
     _boundsOpts.changesetSecKeepOnlyInsideBounds, "sec-cropped-for-changeset");
-  LOG_VART(_lenientBounds);
-  LOG_VART(isLinearCrit);
 
-  if (_lenientBounds && isLinearCrit)
+  if (_boundsInterpretation == BoundsInterpretation::Lenient &&
+      _currentChangeDerivationPassIsLinear)
   {
     if (_waySnappingEnabled)
     {
@@ -674,6 +708,7 @@ void ChangesetReplacementCreator::_getMapsForGeometryType(
       // we're being as lenient as possible with the snapping here, allowing basically anything to
       // join to anything else, which could end up causing problems...we'll go with it for now.
 
+      LOG_STATUS("Snapping unconnected ways to each other...");
       QStringList snapWayStatuses("Input2");
       snapWayStatuses.append("Conflated");
       snapWayStatuses.append("Input1");
@@ -693,15 +728,21 @@ void ChangesetReplacementCreator::_getMapsForGeometryType(
     _combineMaps(
       conflatedMap, immediatelyConnectedOutOfBoundsWays, true, "conflated-connected-combined");
 
-    // Snap only the connected ways to other ways in the conflated map. Mark the ways that were
+    // Snap 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.
     if (_waySnappingEnabled)
     {
       LOG_VART(linearFilterClassNames);
       for (int i = 0; i < linearFilterClassNames.size(); i++)
       {
+        QStringList snapWayStatuses("Input2");
+        snapWayStatuses.append("Conflated");
+        snapWayStatuses.append("Input1");
+        QStringList snapToWayStatuses("Input1");
+        snapToWayStatuses.append("Conflated");
+        snapToWayStatuses.append("Input2");
         _snapUnconnectedWays(
-          conflatedMap, QStringList("Input1"), QStringList("Input1"), linearFilterClassNames.at(i),
+          conflatedMap, snapWayStatuses, snapToWayStatuses, linearFilterClassNames.at(i),
           true, "conflated-snapped-immediately-connected-out-of-bounds");
       }
     }
@@ -723,7 +764,7 @@ void ChangesetReplacementCreator::_getMapsForGeometryType(
     _excludeFeaturesFromChangesetDeletion(refMap, boundsStr);
   }
 
-  // clean up introduced mistakes
+  // clean up any mistakes introduced
   _cleanup(refMap);
   _cleanup(conflatedMap);
 
@@ -731,7 +772,8 @@ void ChangesetReplacementCreator::_getMapsForGeometryType(
   LOG_VART(conflatedMap->getElementCount());
 }
 
-void ChangesetReplacementCreator::_validateInputs(const QString& input1, const QString& input2)
+void ChangesetReplacementCreator::_validateInputs(const QString& input1, const QString& input2,
+                                                  const QString& output)
 {
   // Fail if the reader that supports either input doesn't implement Boundable.
   std::shared_ptr<Boundable> boundable =
@@ -758,6 +800,16 @@ void ChangesetReplacementCreator::_validateInputs(const QString& input1, const Q
       "GeoJSON inputs are not supported by replacement changeset derivation.");
   }
 
+  QFile outputFile(output);
+  if (outputFile.exists())
+  {
+    if (!outputFile.remove())
+    {
+      throw HootException("Unable to remove changeset output file: " + output);
+    }
+  }
+
+  LOG_VARD(_fullReplacement);
   if (_fullReplacement && _retainmentFilter)
   {
     throw IllegalArgumentException(
@@ -765,7 +817,7 @@ void ChangesetReplacementCreator::_validateInputs(const QString& input1, const Q
       "be specified for replacement changeset derivation.");
   }
 
-  if (ConfigOptions().getConvertOps().size())
+  if (ConfigOptions().getConvertOps().size() > 0)
   {
     throw IllegalArgumentException(
       "Replacement changeset derivation does not support convert operations.");
@@ -800,9 +852,38 @@ void ChangesetReplacementCreator::_setGlobalOpts(const QString& boundsStr)
   ConfigUtils::removeListOpEntry(
     ConfigOptions::getConflatePostOpsKey(),
     QString::fromStdString(RemoveMissingElementsVisitor::className()));
+  // Having to set multiple different settings to prevent missing elements from being dropped here
+  // is convoluted...may need to look into changing at some point.
   conf().set(ConfigOptions::getConvertBoundingBoxRemoveMissingElementsKey(), false);
+  conf().set(ConfigOptions::getMapReaderAddChildRefsWhenMissingKey(), true);
+  conf().set(ConfigOptions::getLogWarningsForMissingElementsKey(), false);
 
-  // These don't change between scenarios (or at least we haven't needed to yet).
+  // If we're adding missing child element tags to parents, then we need to explicitly specify that
+  // they are allowed to pass through to the changeset output. See notes where
+  // _markElementsWithMissingChildren is called for more info on why this tag is added.
+  if (ConfigOptions().getChangesetReplacementMarkElementsWithMissingChildren())
+  {
+    QStringList metadataAllowTagKeys(MetadataTags::HootMissingChild());
+    conf().set(ConfigOptions::getChangesetMetadataAllowedTagKeysKey(), metadataAllowTagKeys);
+  }
+
+  // Came across a very odd bug in #4101, where if RemoveInvalidMultilineStringMembersVisitor ran
+  // as part of the pre-conflate map cleaning during replacement with conflation enabled, the match
+  // conflict resolution would slow down to a crawl. When it was removed from the cleaning ops, the
+  // conflate operation ran very quickly. So as a not so great workaround (aka hack), removing that
+  // pre-op here when running conflation. It still will run post conflate, though. This change had
+  // a very minor affect on changeset replacement test output where one test got slightly better
+  // output after the change and another slightly worse. See more details in #4101, which is closed,
+  // but if we can figure out what's going on at some point maybe this situation can be handled
+  // properly.
+  if (_conflationEnabled)
+  {
+    ConfigUtils::removeListOpEntry(
+      ConfigOptions::getMapCleanerTransformsKey(),
+      QString::fromStdString(RemoveInvalidMultilineStringMembersVisitor::className()));
+  }
+
+  // These don't change between scenarios (or at least we haven't needed to change them yet).
   _boundsOpts.loadRefKeepOnlyInsideBounds = false;
   _boundsOpts.cookieCutKeepOnlyInsideBounds = false;
   _boundsOpts.changesetRefKeepOnlyInsideBounds = false;
@@ -812,11 +893,13 @@ void ChangesetReplacementCreator::_setGlobalOpts(const QString& boundsStr)
 }
 
 void ChangesetReplacementCreator::_parseConfigOpts(
-  const bool lenientBounds, const GeometryTypeCriterion::GeometryType& geometryType)
+  const GeometryTypeCriterion::GeometryType& geometryType)
 {
   if (!_cleaningEnabled && _conflationEnabled)
   {
-    throw IllegalArgumentException("If conflation is enabled, cleaning cannot be disabled.");
+    throw IllegalArgumentException(
+      "If conflation is enabled during changeset replacement derivation, cleaning cannot be "
+      "disabled.");
   }
 
   // These settings have been are customized for each geometry type and bounds handling preference.
@@ -825,19 +908,6 @@ void ChangesetReplacementCreator::_parseConfigOpts(
 
   if (geometryType == GeometryTypeCriterion::GeometryType::Point)
   {
-    if (lenientBounds)
-    {
-      const QString msg = "--lenient-bounds option ignored with point datasets.";
-      if (_geometryFiltersSpecified)
-      {
-        LOG_WARN(msg);
-      }
-      else
-      {
-        LOG_DEBUG(msg);
-      }
-    }
-
     _boundsOpts.loadRefKeepEntireCrossingBounds = false;
     _boundsOpts.loadRefKeepImmediateConnectedWaysOutsideBounds = false;
     _boundsOpts.loadSecKeepEntireCrossingBounds = false;
@@ -851,7 +921,7 @@ void ChangesetReplacementCreator::_parseConfigOpts(
   }
   else if (geometryType == GeometryTypeCriterion::GeometryType::Line)
   {
-    if (lenientBounds)
+    if (_boundsInterpretation == BoundsInterpretation::Lenient)
     {
       _boundsOpts.loadRefKeepEntireCrossingBounds = true;
       _boundsOpts.loadRefKeepImmediateConnectedWaysOutsideBounds = true;
@@ -878,7 +948,7 @@ void ChangesetReplacementCreator::_parseConfigOpts(
       _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
+      // Changing the default ordering of the post ops to accommodate this had detrimental effects
       // on other conflation. The best location seems to be at the end just before tag truncation.
       // would like to get rid of this...isn't a foolproof fix by any means if the conflate post
       // ops end up getting reordered for some reason.
@@ -896,7 +966,8 @@ void ChangesetReplacementCreator::_parseConfigOpts(
   }
   else if (geometryType == GeometryTypeCriterion::GeometryType::Polygon)
   {
-    if (lenientBounds)
+    if (_boundsInterpretation == BoundsInterpretation::Lenient ||
+        _boundsInterpretation == BoundsInterpretation::Hybrid)
     {
       _boundsOpts.loadRefKeepEntireCrossingBounds = true;
       _boundsOpts.loadRefKeepImmediateConnectedWaysOutsideBounds = false;
@@ -965,8 +1036,9 @@ QMap<GeometryTypeCriterion::GeometryType, ElementCriterionPtr>
   // duplicated in the output. This is why we run a de-duplication routine just before changeset
   // derivation...kind of a band-aid unfortunately :-(
 
-  // The map will get set on this point crit by the RemoveElementsVisitor later on, right before its
-  // needed.
+  // The maps will get set on the crits here that need them by the RemoveElementsVisitor later on,
+  // right before its needed.
+
   ElementCriterionPtr pointCrit(new PointCriterion());
   std::shared_ptr<RelationWithPointMembersCriterion> relationPointCrit(
     new RelationWithPointMembersCriterion());
@@ -981,12 +1053,11 @@ QMap<GeometryTypeCriterion::GeometryType, ElementCriterionPtr>
   OrCriterionPtr lineOr(new OrCriterion(lineCrit, relationLinearCrit));
   featureFilters[GeometryTypeCriterion::GeometryType::Line] = lineOr;
 
-  ElementCriterionPtr polyCrit(new PolygonCriterion());
-  std::shared_ptr<RelationWithPolygonMembersCriterion> relationPolyCrit(
-    new RelationWithPolygonMembersCriterion());
-  relationPolyCrit->setAllowMixedChildren(false);
-  OrCriterionPtr polyOr(new OrCriterion(polyCrit, relationPolyCrit));
-  featureFilters[GeometryTypeCriterion::GeometryType::Polygon] = polyOr;
+  // Poly crit has been converted over to encapsulate RelationWithGeometryMembersCriterion, while
+  // the other types have not yet (#4151).
+  std::shared_ptr<PolygonCriterion> polyCrit(new PolygonCriterion());
+  polyCrit->setAllowMixedChildren(false);
+  featureFilters[GeometryTypeCriterion::GeometryType::Polygon] = polyCrit;
 
   return featureFilters;
 }
@@ -1063,7 +1134,7 @@ QMap<GeometryTypeCriterion::GeometryType, ElementCriterionPtr>
   }
   else
   {
-    LOG_VART(_geometryTypeFilters.size());
+    LOG_VARD(_geometryTypeFilters.size());
     if (_geometryTypeFilters.isEmpty())
     {
       _geometryTypeFilters = _getDefaultGeometryFilters();
@@ -1114,13 +1185,13 @@ QMap<GeometryTypeCriterion::GeometryType, ElementCriterionPtr>
       LOG_TRACE("New combined filter: " << combinedFilters[geomType]->toString());
     }
   }
-  LOG_VART(combinedFilters.size());
+  LOG_VARD(combinedFilters.size());
   return combinedFilters;
 }
 
 OsmMapPtr ChangesetReplacementCreator::_loadRefMap(const QString& input)
 { 
-  LOG_INFO("Loading reference map: " << input << "...");
+  LOG_STATUS("Loading reference map: " << input << "...");
 
   // We want to alert the user to the fact their ref versions *could* be being populated incorrectly
   // to avoid difficulties during changeset application at the end. Its likely if they are incorrect
@@ -1139,7 +1210,7 @@ OsmMapPtr ChangesetReplacementCreator::_loadRefMap(const QString& input)
     _boundsOpts.loadRefKeepImmediateConnectedWaysOutsideBounds);
 
   // Here and with sec map loading, attempted to cache the initial map to avoid unnecessary
-  // reloading, but it wreaked havoc on the element IDs. May try doing it again later.
+  // reloading, but it wreaked havoc on the element IDs. May try debugging it again later.
   OsmMapPtr refMap;
   refMap.reset(new OsmMap());
   refMap->setName("ref");
@@ -1155,7 +1226,7 @@ OsmMapPtr ChangesetReplacementCreator::_loadRefMap(const QString& input)
 
 OsmMapPtr ChangesetReplacementCreator::_loadSecMap(const QString& input)
 {
-  LOG_INFO("Loading secondary map: " << input << "...");
+  LOG_STATUS("Loading secondary map: " << input << "...");
 
   conf().set(
     ConfigOptions::getConvertBoundingBoxKeepEntireFeaturesCrossingBoundsKey(),
@@ -1180,15 +1251,19 @@ OsmMapPtr ChangesetReplacementCreator::_loadSecMap(const QString& input)
 void ChangesetReplacementCreator::_markElementsWithMissingChildren(OsmMapPtr& map)
 {
   ReportMissingElementsVisitor elementMarker;
-  // Originally, this was going to add reviews rather than tagging elements but there was an ID
-  // provenance problem with reviews.
+  // Originally, this was going to add reviews rather than tagging elements, but there was an ID
+  // provenance problem when using reviews.
   elementMarker.setMarkRelationsForReview(false);
   elementMarker.setMarkWaysForReview(false);
   elementMarker.setRelationKvp(MetadataTags::HootMissingChild() + "=yes");
   elementMarker.setWayKvp(MetadataTags::HootMissingChild() + "=yes");
-  LOG_STATUS("\t" << elementMarker.getInitStatusMessage());
+  LOG_STATUS("Marking elements with missing child elements...");
   map->visitRelationsRw(elementMarker);
-  LOG_STATUS("\t" << elementMarker.getCompletedStatusMessage());
+  LOG_DEBUG(
+    "Marked " << elementMarker.getNumWaysTagged() << " ways with missing child elements.");
+  LOG_DEBUG(
+    "Marked " << elementMarker.getNumRelationsTagged() <<
+    " relations with missing child elements.");
 
   OsmMapWriterFactory::writeDebugMap(map, map->getName() + "-after-missing-marked");
 }
@@ -1197,7 +1272,7 @@ void ChangesetReplacementCreator::_filterFeatures(
   OsmMapPtr& map, const ElementCriterionPtr& featureFilter, const Settings& config,
   const QString& debugFileName)
 {
-  LOG_INFO(
+  LOG_STATUS(
     "Filtering features for: " << map->getName() << " based on input filter: " +
     featureFilter->toString() << "...");
 
@@ -1208,54 +1283,147 @@ void ChangesetReplacementCreator::_filterFeatures(
   elementPruner.addCriterion(featureFilter);
   elementPruner.setConfiguration(config);
   elementPruner.setOsmMap(map.get());
-  // If recursion isn't used here, nasty crashes that are hard to track down occur at times. I'm
-  // not completely convinced recursion should be used here, though.
+  // If recursion isn't used here, nasty crashes that are hard to track down occur at times. Not
+  // completely convinced recursion should be used here, though.
   elementPruner.setRecursive(true);
-  LOG_STATUS("\t" << elementPruner.getInitStatusMessage());
+  //LOG_STATUS("\t" << elementPruner.getInitStatusMessage());
   map->visitRw(elementPruner);
-  LOG_STATUS("\t" << elementPruner.getCompletedStatusMessage());
+  LOG_INFO(elementPruner.getCompletedStatusMessage());
 
   LOG_VART(MapProjector::toWkt(map->getProjection()));
   OsmMapWriterFactory::writeDebugMap(map, debugFileName);
 }
 
 OsmMapPtr ChangesetReplacementCreator::_getCookieCutMap(
-  OsmMapPtr doughMap, OsmMapPtr cutterMap, const GeometryTypeCriterion::GeometryType& geometryType)
+  OsmMapPtr doughMap, OsmMapPtr cutterMap, const GeometryTypeCriterion::GeometryType& geometryType,
+  const geos::geom::Envelope& replacementBounds)
 {
-  // could use some refactoring here after the addition of _fullReplacement
-
-  // If the passed in dough map is empty, there's nothing to be cut out.
-  if (doughMap->getElementCount() == 0)
+  // This logic has become extremely complicated over time to handle all the different cut
+  // and replace use cases. There may be way to simplify some of this logic related to
+  // strict/lenient bounds in here by changing some of the initial crop related opts set in
+  // _parseConfigOpts...not sure.
+
+  LOG_VARD(_fullReplacement);
+  LOG_VARD(_boundsInterpretationToString(_boundsInterpretation));
+  LOG_VARD(_currentChangeDerivationPassIsLinear);
+  LOG_VARD(doughMap->size());
+  LOG_VARD(cutterMap->size());
+  OsmMapWriterFactory::writeDebugMap(doughMap, "dough-map-input");
+  OsmMapWriterFactory::writeDebugMap(cutterMap, "cutter-map-input");
+
+  // It could just be an byproduct of how data is being read out by core scripts during testing, but
+  // when doing adjacent cell updates I'm getting cropped data with a bunch of empty relations in
+  // them as input. That eventually needs to be dealt with, but regardless, checking node/way count
+  // and not including relations in the size count is a better check for the total map size so that
+  // the alpha shape gets calculated correctly.
+  const int doughMapInputSize = doughMap->getWayCount() + doughMap->getNodeCount();
+  const int cutterMapInputSize = cutterMap->getWayCount() + cutterMap->getNodeCount();
+
+  /*
+   * lenient/overlapping - cutter shape is all overlapping sec data inside the bounds and
+   *                       immediately connected outside the bounds OR all ref data inside the
+   *                       bounds and immediately connected outside the bounds (if linear) if sec
+   *                       map is empty
+     lenient/full        - cutter shape is all ref data inside the bounds and immediately connected
+                           outside the bounds (if linear)
+     strict/overlapping  - cutter shape is all overlapping sec data inside the bounds
+     strict/full         - cutter shape is all ref data inside the bounds
+
+     hybrid bounds acts like strict for linear features and lenient for polygon features.
+   */
+
+  // If the passed in dough map is empty, there's nothing to be cut out. So, just return the empty
+  // ref map.
+  if (doughMapInputSize == 0)
   {
-    LOG_DEBUG("Nothing to cut from dough map, so returning the dough map as the cut map...");
+    LOG_DEBUG(
+      "Nothing to cut from dough map, so returning the empty dough map as the map after " <<
+      "cutting: " << doughMap->getName() << "...");
+    OsmMapWriterFactory::writeDebugMap(doughMap, "cookie-cut");
     return doughMap;
   }
-  else if (cutterMap->size() == 0)
+  else if (cutterMapInputSize == 0)
   {
-    if (_fullReplacement)
+    // Linear features need to be handled slightly differently, due to the need to snap cut features
+    // back to reference features when the strict bounds interpretation is enabled.
+    if (!_currentChangeDerivationPassIsLinear)
     {
-      // If the sec map is empty and we're doing full replacement, we want everything deleted out
-      // of the ref for the current feature type in the changeset. So, return an empty map.
-      LOG_DEBUG(
-        "Nothing in cutter map. Full replacement not enabled, so returning an empty map " <<
-        "as the cut map...");
-      return OsmMapPtr(new OsmMap());
+      // The bounds interpretation doesn't seem to matter here for non-linear features.
+      if (_fullReplacement)
+      {
+        // If the sec map is empty and we're doing full replacement on non-linear features, we want
+        // everything deleted out of the ref inside the the replacement bounds. So, return an empty
+        // map.
+        LOG_DEBUG(
+          "Nothing in cutter map. Full replacement enabled, so returning an empty map " <<
+          "as the map after cutting...");
+        return OsmMapPtr(new OsmMap());
+      }
+      else
+      {
+        // If the sec map is empty and we're not doing full replacement, there's nothing in the sec
+        // to overlap with the ref, so leave the ref untouched.
+        LOG_DEBUG(
+          "Nothing in cutter map. Full replacement not enabled, so returning the entire dough " <<
+          "map as the map after cutting: " << doughMap->getName() << "...");
+        OsmMapWriterFactory::writeDebugMap(doughMap, "cookie-cut");
+        return doughMap;
+      }
     }
     else
     {
-      // If the sec map is empty and we're not doing full replacement, there's nothing in the sec
-      // to overlap with the ref, so leave the ref untouched.
-      LOG_DEBUG(
-        "Nothing in cutter map. Full replacement enabled, so returning the entire dough map " <<
-        "as the cut map...");
-      return doughMap;
+      if (_fullReplacement && _boundsInterpretation == BoundsInterpretation::Lenient)
+      {
+        // If our map contains linear features only, the sec map is empty, we're doing full
+        // replacement, AND there isn't a strict interpretation of the bounds, we want everything
+        // deleted out of the ref inside the replacement bounds and features immediately connected
+        // outside of the bounds. So, return an empty map.
+        LOG_DEBUG(
+          "Nothing in cutter map for linear features. Full replacement and lenient bounds "
+          "interpretation, so returning an empty map as the map after cutting...");
+        return OsmMapPtr(new OsmMap());
+      }
+      else if (_fullReplacement && _boundsInterpretation != BoundsInterpretation::Lenient )
+      {
+        // With the strict bounds interpretation, full replacement, and an empty secondary map,
+        // we want simply the rectangular replacement bounds cut out. No need to use the cookie
+        // cutter here. Just use the map cropper.
+        LOG_DEBUG(
+          "Nothing in cutter map. Full replacement with strict bounds enabled, so cropping out " <<
+          "the rectangular bounds area of the dough map to be the map after cutting: " <<
+          doughMap->getName() << "...");
+        OsmMapPtr cookieCutMap(new OsmMap(doughMap));
+        cookieCutMap->setName("cookie-cut");
+        MapCropper cropper(replacementBounds);
+        cropper.setRemoveSuperflousFeatures(false);
+        cropper.setKeepEntireFeaturesCrossingBounds(false);
+        cropper.setKeepOnlyFeaturesInsideBounds(false);
+        cropper.setInvert(true);
+        // We're not going to remove missing elements, as we want to have as minimal of an impact on
+        // the resulting changeset as possible.
+        cropper.setRemoveMissingElements(false);
+        LOG_STATUS(cropper.getInitStatusMessage());
+        cropper.apply(cookieCutMap);
+        LOG_INFO(cropper.getCompletedStatusMessage());
+        OsmMapWriterFactory::writeDebugMap(cookieCutMap, "cookie-cut");
+        return cookieCutMap;
+      }
+      else
+      {
+        // If the sec map is empty and we're not doing full replacement, there's nothing in the sec
+        // to overlap with the ref, so leave the ref untouched.
+        LOG_DEBUG(
+          "Nothing in cutter map for linear features. Full replacement not enabled, so returning the "
+          "entire dough map as the map after cutting: " << doughMap->getName() << "...");
+        OsmMapWriterFactory::writeDebugMap(doughMap, "cookie-cut");
+        return doughMap;
+      }
     }
   }
 
-  LOG_VART(doughMap->getElementCount());
+  LOG_VART(doughMap->size());
   LOG_VART(MapProjector::toWkt(doughMap->getProjection()));
-  OsmMapWriterFactory::writeDebugMap(doughMap, "dough-map");
-  LOG_VART(cutterMap->getElementCount());
+  OsmMapWriterFactory::writeDebugMap(doughMap, "dough-map");;
   LOG_VART(MapProjector::toWkt(cutterMap->getProjection()));
 
   OsmMapPtr cookieCutMap(new OsmMap(doughMap));
@@ -1264,50 +1432,71 @@ OsmMapPtr ChangesetReplacementCreator::_getCookieCutMap(
   LOG_DEBUG("Preparing to cookie cut: " << cookieCutMap->getName() << "...");
 
   OsmMapPtr cutterMapToUse;
-  LOG_VART(cutterMap->getElementCount());
   ConfigOptions opts(conf());
   LOG_VART(MapUtils::mapIsPointsOnly(cutterMap));
   const double cookieCutterAlpha = opts.getCookieCutterAlpha();
   double cookieCutterAlphaShapeBuffer = opts.getCookieCutterAlphaShapeBuffer();
-  LOG_VART(_fullReplacement);
-  if (_fullReplacement)
+  if (_currentChangeDerivationPassIsLinear) // See related note above when the cutter map is empty.
   {
-    // Generate a cutter shape based on the ref map, which will cause all the ref data to be
-    // replaced.
-    LOG_DEBUG("Using dough map as cutter shape map...");
-    cutterMapToUse = doughMap;
-    // TODO: riverbank test fails with missing POIs without this and the single point test has
-    // extra POIs in output without this; explain
-    cookieCutterAlphaShapeBuffer = 10.0;
+    if (_boundsInterpretation == BoundsInterpretation::Lenient && _fullReplacement)
+    {
+      // Generate a cutter shape based on the ref map, which will cause all the ref data to be
+      // replaced.
+      LOG_DEBUG("Using dough map: " << doughMap->getName() << " as cutter shape map...");
+      cutterMapToUse = doughMap;
+      // TODO: riverbank test fails with missing POIs without this and the single point test has
+      // extra POIs in output without this; explain
+      cookieCutterAlphaShapeBuffer = 10.0;
+    }
+    else
+    {
+      LOG_DEBUG("Using cutter map: " << cutterMap->getName() << " as cutter shape map...");
+      cutterMapToUse = cutterMap;
+    }
   }
   else
   {
-    // Generate a cutter shape based on the cropped secondary map, which will cause only
-    // overlapping data between the two datasets to be replaced.
-    LOG_DEBUG("Using cutter map as cutter shape map...");
-    cutterMapToUse = cutterMap;
+    if (_fullReplacement)
+    {
+      // Generate a cutter shape based on the ref map, which will cause all the ref data to be
+      // replaced.
+      LOG_DEBUG("Using dough map: " << doughMap->getName() << " as cutter shape map...");
+      cutterMapToUse = doughMap;
+      // TODO: riverbank test fails with missing POIs without this and the single point test has
+      // extra POIs in output without this; explain
+      cookieCutterAlphaShapeBuffer = 10.0;
+    }
+    else
+    {
+      // Generate a cutter shape based on the cropped secondary map, which will cause only
+      // overlapping data between the two datasets to be replaced.
+      LOG_DEBUG("Using cutter map: " << cutterMap->getName() << " as cutter shape map...");
+      cutterMapToUse = cutterMap;
+    }
   }
 
   // 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 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.
-  if ((int)cutterMapToUse->getElementCount() < 3 && MapUtils::mapIsPointsOnly(cutterMapToUse))
+  // not be possible (or at least don't know how to yet). So instead, go through the points in the
+  // map and replace them with small square shaped polys...from that we can generate the alpha
+  // shape.
+  const int cutterMapToUseSize = cutterMapToUse->getNodeCount();
+  if ((int)cutterMapToUseSize < 3 && MapUtils::mapIsPointsOnly(cutterMapToUse))
   {
     LOG_DEBUG("Creating a cutter shape map transformation for point map...");
     // Make a copy here since we're making destructive changes to the geometry here for alpha shape
     // generation purposes only.
     cutterMapToUse.reset(new OsmMap(cutterMap));
-    PointsToPolysConverter pointConverter/*(1.0)*/;
-    LOG_STATUS("\t" << pointConverter.getInitStatusMessage());
+    PointsToPolysConverter pointConverter;
+    LOG_STATUS(pointConverter.getInitStatusMessage());
     pointConverter.apply(cutterMapToUse);
-    LOG_STATUS("\t" << pointConverter.getCompletedStatusMessage());
+    LOG_INFO(pointConverter.getCompletedStatusMessage());
     MapProjector::projectToWgs84(cutterMapToUse);
   }
 
-  LOG_VART(cutterMapToUse->getElementCount());
-  OsmMapWriterFactory::writeDebugMap(cutterMapToUse, "cutter-map");
+  LOG_VART(cutterMapToUse->size());
+  OsmMapWriterFactory::writeDebugMap(cutterMapToUse, "cutter-map-to-use");
 
-  LOG_INFO("Generating cutter shape map from: " << cutterMapToUse->getName() << "...");
+  LOG_STATUS("Generating cutter shape map from: " << cutterMapToUse->getName() << "...");
 
   LOG_VART(cookieCutterAlpha);
   LOG_VART(cookieCutterAlphaShapeBuffer);
@@ -1337,7 +1526,7 @@ OsmMapPtr ChangesetReplacementCreator::_getCookieCutMap(
   OsmMapWriterFactory::writeDebugMap(cutterShapeOutlineMap, "cutter-shape");
 
   // Cookie cut the shape of the cutter shape map out of the cropped ref map.
-  LOG_INFO("Cookie cutting cutter shape out of: " << cookieCutMap->getName() << "...");
+  LOG_STATUS("Cutting cutter shape out of: " << cookieCutMap->getName() << "...");
 
   // We're not going to remove missing elements, as we want to have as minimal of an impact on
   // the resulting changeset as possible.
@@ -1346,9 +1535,8 @@ OsmMapPtr ChangesetReplacementCreator::_getCookieCutMap(
     _boundsOpts.cookieCutKeepOnlyInsideBounds, false)
     .cut(cutterShapeOutlineMap, cookieCutMap);
   MapProjector::projectToWgs84(cookieCutMap); // not exactly sure yet why this needs to be done
-  LOG_VARD(cookieCutMap->getElementCount());
+  LOG_VARD(cookieCutMap->size());
   MapProjector::projectToWgs84(doughMap);
-  LOG_VART(doughMap->getElementCount());
   LOG_VART(MapProjector::toWkt(cookieCutMap->getProjection()));
   MemoryUsageChecker::getInstance().check();
   OsmMapWriterFactory::writeDebugMap(cookieCutMap, "cookie-cut");
@@ -1359,13 +1547,13 @@ OsmMapPtr ChangesetReplacementCreator::_getCookieCutMap(
 QMap<ElementId, long> ChangesetReplacementCreator::_getIdToVersionMappings(
   const OsmMapPtr& map) const
 {
-  LOG_DEBUG("Mapping element IDs to element versions for: " << map->getName() << "...");
+  LOG_STATUS("Mapping element IDs to element versions for: " << map->getName() << "...");
 
   ElementIdToVersionMapper idToVersionMapper;
-  LOG_STATUS("\t" << idToVersionMapper.getInitStatusMessage());
+  //LOG_STATUS("\t" << idToVersionMapper.getInitStatusMessage());
   idToVersionMapper.apply(map);
   MemoryUsageChecker::getInstance().check();
-  LOG_STATUS("\t" << idToVersionMapper.getCompletedStatusMessage());
+  LOG_DEBUG(idToVersionMapper.getCompletedStatusMessage());
   const QMap<ElementId, long> idToVersionMappings = idToVersionMapper.getMappings();
   LOG_VART(idToVersionMappings.size());
   return idToVersionMappings;
@@ -1373,21 +1561,21 @@ QMap<ElementId, long> ChangesetReplacementCreator::_getIdToVersionMappings(
 
 void ChangesetReplacementCreator::_addChangesetDeleteExclusionTags(OsmMapPtr& map)
 {
-  LOG_INFO(
+  LOG_STATUS(
     "Setting connected way features outside of bounds to be excluded from deletion for: " <<
     map->getName() << "...");
 
   // Add the changeset deletion exclusion tag to all connected ways previously tagged upon load.
 
   SetTagValueVisitor addTagVis(MetadataTags::HootChangeExcludeDelete(), "yes");
-  LOG_STATUS("\t" << addTagVis.getInitStatusMessage());
+  //LOG_STATUS("\t" << addTagVis.getInitStatusMessage());
   ChainCriterion addTagCrit(
     std::shared_ptr<WayCriterion>(new WayCriterion()),
     std::shared_ptr<TagKeyCriterion>(
       new TagKeyCriterion(MetadataTags::HootConnectedWayOutsideBounds())));
   FilteredVisitor deleteExcludeTagVis(addTagCrit, addTagVis);
   map->visitRw(deleteExcludeTagVis);
-  LOG_STATUS("\t" << addTagVis.getCompletedStatusMessage());
+  LOG_DEBUG(addTagVis.getCompletedStatusMessage());
 
   // Add the changeset deletion exclusion tag to all children of those connected ways.
 
@@ -1398,9 +1586,9 @@ void ChangesetReplacementCreator::_addChangesetDeleteExclusionTags(OsmMapPtr& ma
         new TagKeyCriterion(MetadataTags::HootChangeExcludeDelete()))));
   RecursiveSetTagValueOp childDeletionExcludeTagOp(
     MetadataTags::HootChangeExcludeDelete(), "yes", childAddTagCrit);
-  LOG_STATUS("\t" << childDeletionExcludeTagOp.getInitStatusMessage());
+  //LOG_STATUS("\t" << childDeletionExcludeTagOp.getInitStatusMessage());
   childDeletionExcludeTagOp.apply(map);
-  LOG_STATUS("\t" << childDeletionExcludeTagOp.getCompletedStatusMessage());
+  LOG_DEBUG(childDeletionExcludeTagOp.getCompletedStatusMessage());
 
   MemoryUsageChecker::getInstance().check();
   OsmMapWriterFactory::writeDebugMap(map, map->getName() + "-after-delete-exclusion-tagging");
@@ -1421,7 +1609,7 @@ void ChangesetReplacementCreator::_combineMaps(
     return;
   }
 
-  LOG_INFO("Combining maps: " << map1->getName() << " and " << map2->getName() << "...");
+  LOG_STATUS("Combining maps: " << map1->getName() << " and " << map2->getName() << "...");
 
   map1->append(map2, throwOutDupes);
   LOG_VART(MapProjector::toWkt(map1->getProjection()));
@@ -1431,23 +1619,26 @@ void ChangesetReplacementCreator::_combineMaps(
   OsmMapWriterFactory::writeDebugMap(map1, debugFileName);
 }
 
-void ChangesetReplacementCreator::_conflate(OsmMapPtr& map, const bool lenientBounds)
+void ChangesetReplacementCreator::_conflate(OsmMapPtr& map)
 {
   map->setName("conflated");
-  LOG_INFO(
+  LOG_STATUS(
     "Conflating the cookie cut reference map with the secondary map into " << map->getName() <<
     "...");
 
   conf().set(ConfigOptions::getWayJoinerLeaveParentIdKey(), true);
-  if (!lenientBounds) // not exactly sure yet why this needs to be done
+  if (_boundsInterpretation != BoundsInterpretation::Lenient)
   {
+    // not exactly sure yet why this needs to be done
     conf().set(ConfigOptions::getWayJoinerKey(), WayJoinerAdvanced::className());
   }
   else
   {
     conf().set(ConfigOptions::getWayJoinerKey(), WayJoinerBasic::className());
   }
-  conf().set(ConfigOptions::getWayJoinerAdvancedStrictNameMatchKey(), !_isNetworkConflate());
+  conf().set(
+    ConfigOptions::getWayJoinerAdvancedStrictNameMatchKey(),
+    !UnifyingConflator::isNetworkConflate());
 
   if (ConfigOptions().getConflateRemoveSuperfluousOps())
   {
@@ -1470,7 +1661,7 @@ void ChangesetReplacementCreator::_conflate(OsmMapPtr& map, const bool lenientBo
 
 void ChangesetReplacementCreator::_removeConflateReviews(OsmMapPtr& map)
 {
-  LOG_INFO("Removing reviews added during conflation from " << map->getName() << "...");
+  LOG_STATUS("Removing reviews added during conflation from " << map->getName() << "...");
 
   RemoveElementsVisitor removeVis;
   removeVis.addCriterion(ElementCriterionPtr(new RelationCriterion("review")));
@@ -1483,9 +1674,9 @@ void ChangesetReplacementCreator::_removeConflateReviews(OsmMapPtr& map)
             QString::fromStdString(ReportMissingElementsVisitor::className()))))));
   removeVis.setChainCriteria(true);
   removeVis.setRecursive(false);
-  LOG_STATUS("\t" << removeVis.getInitStatusMessage());
+  //LOG_STATUS("\t" << removeVis.getInitStatusMessage());
   map->visitRw(removeVis);
-  LOG_STATUS("\t" << removeVis.getCompletedStatusMessage());
+  LOG_DEBUG(removeVis.getCompletedStatusMessage());
 
   MemoryUsageChecker::getInstance().check();
   LOG_VART(MapProjector::toWkt(map->getProjection()));
@@ -1495,7 +1686,7 @@ void ChangesetReplacementCreator::_removeConflateReviews(OsmMapPtr& map)
 void ChangesetReplacementCreator::_clean(OsmMapPtr& map)
 {
   map->setName("cleaned");
-  LOG_INFO(
+  LOG_STATUS(
     "Cleaning the combined cookie cut reference and secondary maps: " << map->getName() << "...");
 
   // TODO: since we're never conflating when we call clean, should we remove cleaning ops like
@@ -1512,7 +1703,7 @@ void ChangesetReplacementCreator::_snapUnconnectedWays(
   OsmMapPtr& map, const QStringList& snapWayStatuses, const QStringList& snapToWayStatuses,
   const QString& typeCriterionClassName, const bool markSnappedWays, const QString& debugFileName)
 {
-  LOG_INFO(
+  LOG_DEBUG(
     "Snapping ways for map: " << map->getName() << ", with filter type: " <<
     typeCriterionClassName << ", snap way statuses: " << snapWayStatuses <<
     ", snap to way statuses: " << snapToWayStatuses << " ...");
@@ -1528,9 +1719,9 @@ void ChangesetReplacementCreator::_snapUnconnectedWays(
     QString::fromStdString(WayNodeCriterion::className()));
   lineSnapper.setWayToSnapCriterionClassName(typeCriterionClassName);
   lineSnapper.setWayToSnapToCriterionClassName(typeCriterionClassName);
-  LOG_STATUS("\t" << lineSnapper.getInitStatusMessage());
+  //LOG_STATUS("\t" << lineSnapper.getInitStatusMessage());
   lineSnapper.apply(map);
-  LOG_STATUS("\t" << lineSnapper.getCompletedStatusMessage());
+  LOG_DEBUG(lineSnapper.getCompletedStatusMessage());
 
   MapProjector::projectToWgs84(map);   // snapping works in planar
   LOG_VART(MapProjector::toWkt(map->getProjection()));
@@ -1542,7 +1733,7 @@ OsmMapPtr ChangesetReplacementCreator::_getImmediatelyConnectedOutOfBoundsWays(
   const ConstOsmMapPtr& map) const
 {
   const QString outputMapName = "connected-ways";
-  LOG_INFO(
+  LOG_STATUS(
     "Copying immediately connected out of bounds ways from: " << map->getName() <<
     " to new map: " << outputMapName << "...");
 
@@ -1569,7 +1760,7 @@ void ChangesetReplacementCreator::_cropMapForChangesetDerivation(
     return;
   }
 
-  LOG_INFO("Cropping map: " << map->getName() << " for changeset derivation...");
+  LOG_STATUS("Cropping map: " << map->getName() << " for changeset derivation...");
   LOG_VART(MapProjector::toWkt(map->getProjection()));
 
   MapCropper cropper(bounds);
@@ -1578,9 +1769,10 @@ void ChangesetReplacementCreator::_cropMapForChangesetDerivation(
   // We're not going to remove missing elements, as we want to have as minimal of an impact on
   // the resulting changeset as possible.
   cropper.setRemoveMissingElements(false);
-  LOG_STATUS("\t" << cropper.getInitStatusMessage());
+  // TODO: should removing superfluous features be suppressed here?
+  //LOG_STATUS("\t" << cropper.getInitStatusMessage());
   cropper.apply(map);
-  LOG_STATUS("\t" << cropper.getCompletedStatusMessage());
+  LOG_DEBUG(cropper.getCompletedStatusMessage());
 
   MemoryUsageChecker::getInstance().check();
   LOG_VART(MapProjector::toWkt(map->getProjection()));
@@ -1591,7 +1783,7 @@ void ChangesetReplacementCreator::_cropMapForChangesetDerivation(
 void ChangesetReplacementCreator::_removeUnsnappedImmediatelyConnectedOutOfBoundsWays(
   OsmMapPtr& map)
 {
-  LOG_INFO(
+  LOG_STATUS(
     "Removing any immediately connected ways that were not previously snapped in: " <<
     map->getName() << "...");
 
@@ -1606,9 +1798,9 @@ void ChangesetReplacementCreator::_removeUnsnappedImmediatelyConnectedOutOfBound
           new TagCriterion(MetadataTags::HootSnapped(), "snapped_way")))));
   removeVis.setChainCriteria(true);
   removeVis.setRecursive(true);
-  LOG_STATUS("\t" << removeVis.getInitStatusMessage());
+  //LOG_STATUS("\t" << removeVis.getInitStatusMessage());
   map->visitRw(removeVis);
-  LOG_STATUS("\t" << removeVis.getCompletedStatusMessage());
+  LOG_DEBUG(removeVis.getCompletedStatusMessage());
 
   MemoryUsageChecker::getInstance().check();
   LOG_VART(MapProjector::toWkt(map->getProjection()));
@@ -1623,7 +1815,7 @@ void ChangesetReplacementCreator::_excludeFeaturesFromChangesetDeletion(
     return;
   }
 
-  LOG_INFO(
+  LOG_STATUS(
     "Marking reference features in: " << map->getName() << " for exclusion from deletion...");
 
   std::shared_ptr<InBoundsCriterion> boundsCrit(new InBoundsCriterion(_boundsOpts.inBoundsStrict));
@@ -1634,9 +1826,9 @@ void ChangesetReplacementCreator::_excludeFeaturesFromChangesetDeletion(
     new ChainCriterion(std::shared_ptr<WayCriterion>(new WayCriterion()), notInBoundsCrit));
 
   RecursiveSetTagValueOp tagSetter(MetadataTags::HootChangeExcludeDelete(), "yes", elementCrit);
-  LOG_STATUS("\t" << tagSetter.getInitStatusMessage());
+  //LOG_STATUS("\t" << tagSetter.getInitStatusMessage());
   tagSetter.apply(map);
-  LOG_STATUS("\t" << tagSetter.getCompletedStatusMessage());
+  LOG_DEBUG(tagSetter.getCompletedStatusMessage());
 
   MemoryUsageChecker::getInstance().check();
   LOG_VART(MapProjector::toWkt(map->getProjection()));
@@ -1687,42 +1879,30 @@ void ChangesetReplacementCreator::_dedupeMaps(const QList<OsmMapPtr>& maps)
 
 void ChangesetReplacementCreator::_cleanup(OsmMapPtr& map)
 {
-  LOG_INFO("Cleaning up missing elements for " << map->getName() << "...");
-
-  // This will handle removing refs in relation members we've cropped out.
-//  RemoveMissingElementsVisitor missingElementsRemover;
-//  LOG_STATUS("\t" << missingElementsRemover.getInitStatusMessage());
-//  map->visitRw(missingElementsRemover);
-//  LOG_STATUS("\t" << missingElementsRemover.getCompletedStatusMessage());
+  LOG_STATUS("Cleaning up duplicated elements for " << map->getName() << "...");
 
   // Due to mixed geometry type relations explained in _getDefaultGeometryFilters, we may have
-  // introduced some duplicate relation members.
+  // introduced some duplicate relation members by this point.
   RemoveDuplicateRelationMembersVisitor dupeMembersRemover;
-  LOG_STATUS("\t" << dupeMembersRemover.getInitStatusMessage());
+  //LOG_STATUS("\t" << dupeMembersRemover.getInitStatusMessage());
   map->visitRw(dupeMembersRemover);
-  LOG_STATUS("\t" << dupeMembersRemover.getCompletedStatusMessage());
+  LOG_DEBUG(dupeMembersRemover.getCompletedStatusMessage());
 
   // get rid of straggling nodes
   SuperfluousNodeRemover orphanedNodeRemover;
-  LOG_STATUS("\t" << orphanedNodeRemover.getInitStatusMessage());
+  //LOG_STATUS("\t" << orphanedNodeRemover.getInitStatusMessage());
   orphanedNodeRemover.apply(map);
-  LOG_STATUS("\t" << orphanedNodeRemover.getCompletedStatusMessage());
+  LOG_DEBUG(orphanedNodeRemover.getCompletedStatusMessage());
 
-  // This will remove any relations that were already empty or became empty after we removed missing
-  // members.
+  // This will remove any relations that were already empty or became empty after we removed
+  // duplicated members.
   RemoveEmptyRelationsOp emptyRelationRemover;
-  LOG_STATUS("\t" << emptyRelationRemover.getInitStatusMessage());
+  //LOG_STATUS("\t" << emptyRelationRemover.getInitStatusMessage());
   emptyRelationRemover.apply(map);
-  LOG_STATUS("\t" << emptyRelationRemover.getCompletedStatusMessage());
+  LOG_DEBUG(emptyRelationRemover.getCompletedStatusMessage());
 
+  // get out of orthographic
   MapProjector::projectToWgs84(map);
 }
 
-bool ChangesetReplacementCreator::_isNetworkConflate() const
-{
-  return
-    ConfigOptions().getMatchCreators().contains(
-      QString::fromStdString(NetworkMatchCreator::className()));
-}
-
 }
Clone this wiki locally