-
Notifications
You must be signed in to change notification settings - Fork 74
v0.2.51..v0.2.52 changeset ChangesetReplacementCreator.cpp
Garret Voltz edited this page Jan 15, 2020
·
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
new file mode 100644
index 0000000..4e030b6
--- /dev/null
+++ b/hoot-core/src/main/cpp/hoot/core/algorithms/changeset/ChangesetReplacementCreator.cpp
@@ -0,0 +1,1211 @@
+/*
+ * This file is part of Hootenanny.
+ *
+ * Hootenanny is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * --------------------------------------------------------------------
+ *
+ * The following copyright notices are generated automatically. If you
+ * have a new notice to add, please use the format:
+ * " * @copyright Copyright ..."
+ * This will properly maintain the copyright information. DigitalGlobe
+ * copyrights will be updated automatically.
+ *
+ * @copyright Copyright (C) 2019 DigitalGlobe (http://www.digitalglobe.com/)
+ */
+#include "ChangesetReplacementCreator.h"
+
+// Hoot
+#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/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/LinearCriterion.h>
+#include <hoot/core/criterion/NotCriterion.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/io/IoUtils.h>
+#include <hoot/core/util/MapProjector.h>
+
+#include <hoot/core/visitors/ApiTagTruncateVisitor.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) :
+_fullReplacement(false),
+_lenientBounds(true),
+_geometryFiltersSpecified(false),
+_chainReplacementFilters(false),
+_chainRetainmentFilters(false),
+_waySnappingEnabled(true),
+_conflationEnabled(true)
+{
+ _changesetCreator.reset(new ChangesetCreator(printStats, osmApiDbUrl));
+ setGeometryFilters(QStringList());
+}
+
+void ChangesetReplacementCreator::setGeometryFilters(const QStringList& filterClassNames)
+{
+ LOG_VARD(filterClassNames);
+ if (!filterClassNames.isEmpty())
+ {
+ _geometryFiltersSpecified = true;
+ _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);
+ }
+ }
+ }
+
+ // TODO: have to call this method to keep filtering from erroring...shouldn't have to...should
+ // init itself internally when no geometry filters are specified
+ LOG_VARD(_geometryTypeFilters.size());
+ if (_geometryTypeFilters.isEmpty())
+ {
+ _geometryFiltersSpecified = false;
+ _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& output)
+{
+ LOG_VARD(_chainReplacementFilters);
+ LOG_VARD(_lenientBounds);
+ LOG_VARD(_fullReplacement);
+
+ // INPUT VALIDATION AND SETUP
+
+ _validateInputs(input1, input2);
+ const QString boundsStr = GeometryUtils::envelopeToConfigString(bounds);
+ _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)
+ {
+ lenientStr += "not ";
+ }
+ lenientStr += "lenient.";
+ LOG_INFO(
+ "Deriving replacement output changeset: ..." << output.right(maxFilePrintLength) <<
+ " 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_INFO(
+ "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_INFO("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.
+
+ refMap = _loadRefMap(input1);
+
+ // 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);
+ 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 the ref dataset down to just the geometry types specified by the filter, so we don't end
+ // up modifying anything else.
+
+ _filterFeatures(refMap, refFeatureFilter, conf(), "ref-after-type-pruning");
+
+ // Load the sec dataset and crop to the specified aoi.
+
+ OsmMapPtr secMap = _loadSecMap(input2);
+
+ // 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");
+
+ LOG_VARD(refMap->getElementCount());
+ LOG_VARD(secMap->getElementCount());
+ // If the secondary dataset is empty here, then the filtering must have removed everything from
+ // it...no changeset to calculate. Note, the ref map could be empty by this point, and that will
+ // just result in an all add changeset with secondary features in it for the current geometry
+ // type being replaced
+ if (secMap->getElementCount() == 0)
+ {
+ LOG_INFO("Secondary input map empty after filtering. Skipping changeset generation...");
+ return;
+ }
+
+ // COOKIE CUT
+
+ // Cut the secondary data out of the reference data.
+
+ OsmMapPtr cookieCutRefMap = _getCookieCutMap(refMap, secMap);
+
+ // 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
+ // fact that relations in the two maps had been added independently of each other during cropping.
+ // However, after some refactoring this doesn't seem to be the case anymore. If we run into this
+ // 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 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 sec map.
+
+ conflatedMap = cookieCutRefMap;
+ if (_conflationEnabled)
+ {
+ // TODO: do something with reviews - #3361
+ _conflate(conflatedMap, _lenientBounds);
+ }
+
+ if (isLinearCrit && _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
+ // secondary to reference *and* allowing conflated data to be snapped to either dataset.
+
+ // 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.
+
+ ReplacementSnappedWayJoiner(refIdToVersionMappings).join(conflatedMap);
+ LOG_VARD(MapProjector::toWkt(conflatedMap->getProjection()));
+ }
+
+ // PRE-CHANGESET DERIVATION DATA PREP
+
+ OsmMapPtr immediatelyConnectedOutOfBoundsWays;
+ 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
+ // crop in preparation for changeset derivation. If we don't introduce them back during
+ // changeset derivation, they may not end up being snapped back to the replacement data.
+
+ immediatelyConnectedOutOfBoundsWays = _getImmediatelyConnectedOutOfBoundsWays(refMap);
+ }
+
+ // Crop the original ref and conflated maps appropriately for changeset derivation.
+
+ const geos::geom::Envelope bounds = GeometryUtils::envelopeFromConfigString(boundsStr);
+ _cropMapForChangesetDerivation(
+ refMap, bounds, _boundsOpts.changesetRefKeepEntireCrossingBounds,
+ _boundsOpts.changesetRefKeepOnlyInsideBounds, isLinearCrit, "ref-cropped-for-changeset");
+ _cropMapForChangesetDerivation(
+ conflatedMap, bounds, _boundsOpts.changesetSecKeepEntireCrossingBounds,
+ _boundsOpts.changesetSecKeepOnlyInsideBounds, isLinearCrit, "sec-cropped-for-changeset");
+ LOG_VARD(_lenientBounds);
+ LOG_VARD(isLinearCrit);
+ if (_lenientBounds && isLinearCrit)
+ {
+ if (_waySnappingEnabled)
+ {
+ // 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, 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.
+
+ _combineMaps(
+ conflatedMap, immediatelyConnectedOutOfBoundsWays, true, "conflated-connected-combined");
+
+ // 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.
+
+ if (_waySnappingEnabled)
+ {
+ 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.
+
+ _removeUnsnappedImmediatelyConnectedOutOfBoundsWays(conflatedMap);
+
+ // Copy the connected ways back into the ref map as well, so the changeset will derive
+ // properly.
+
+ _combineMaps(refMap, immediatelyConnectedOutOfBoundsWays, true, "ref-connected-combined");
+
+ immediatelyConnectedOutOfBoundsWays.reset();
+ }
+ if (!ConfigOptions().getChangesetReplacementAllowDeletingReferenceFeaturesOutsideBounds())
+ {
+ // If we're not allowing the changeset deriver to generate delete statements for reference
+ // features outside of the bounds, we need to mark all corresponding ref ways with a custom
+ // tag that will cause the deriver to skip deleting them.
+
+ _excludeFeaturesFromChangesetDeletion(refMap, boundsStr);
+ }
+
+ LOG_VARD(refMap->getElementCount());
+ LOG_VARD(conflatedMap->getElementCount());
+}
+
+void ChangesetReplacementCreator::_validateInputs(const QString& input1, const QString& input2)
+{
+ // Fail if the reader that supports either input doesn't implement Boundable.
+ std::shared_ptr<Boundable> boundable =
+ std::dynamic_pointer_cast<Boundable>(OsmMapReaderFactory::createReader(input1));
+ if (!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 for replacement changeset derivation.");
+ }
+
+ // Fail for GeoJSON - GeoJSON coming from Overpass does not have way nodes, so their versions
+ // are lost when new way nodes are added to existing ways. For that reason, we can't support it
+ // (or at least not sure how to yet).
+ OsmGeoJsonReader geoJsonReader;
+ if (geoJsonReader.isSupported(input1) || geoJsonReader.isSupported(input2))
+ {
+ 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.");
+ }
+}
+
+QMap<GeometryTypeCriterion::GeometryType, ElementCriterionPtr>
+ ChangesetReplacementCreator::_getDefaultGeometryFilters() const
+{
+ 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)
+ {
+ 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());
+ }
+ }
+ else
+ {
+ if (_geometryTypeFilters.isEmpty())
+ {
+ _geometryTypeFilters = _getDefaultGeometryFilters();
+ _linearFilterClassNames =
+ ConflatableElementCriterion::getCriterionClassNamesByType(
+ GeometryTypeCriterion::GeometryType::Line);
+ }
+ combinedFilters = _geometryTypeFilters;
+ }
+ LOG_VARD(combinedFilters.size());
+ return combinedFilters;
+}
+
+OsmMapPtr ChangesetReplacementCreator::_loadRefMap(const QString& input)
+{
+ LOG_INFO("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
+ // 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.
+ conf().set(ConfigOptions::getReaderWarnOnZeroVersionElementKey(), true);
+
+ conf().set(
+ ConfigOptions::getConvertBoundingBoxKeepEntireFeaturesCrossingBoundsKey(),
+ _boundsOpts.loadRefKeepEntireCrossingBounds);
+ conf().set(
+ ConfigOptions::getConvertBoundingBoxKeepOnlyFeaturesInsideBoundsKey(),
+ _boundsOpts.loadRefKeepOnlyInsideBounds);
+ conf().set(
+ ConfigOptions::getConvertBoundingBoxKeepImmediatelyConnectedWaysOutsideBoundsKey(),
+ _boundsOpts.loadRefKeepImmediateConnectedWaysOutsideBounds);
+
+ OsmMapPtr refMap(new OsmMap());
+ refMap->setName("ref");
+ IoUtils::loadMap(refMap, input, true, Status::Unknown1);
+
+ conf().set(ConfigOptions::getReaderWarnOnZeroVersionElementKey(), false);
+
+ LOG_VART(MapProjector::toWkt(refMap->getProjection()));
+ OsmMapWriterFactory::writeDebugMap(refMap, "ref-after-cropped-load");
+
+ return refMap;
+}
+
+QMap<ElementId, long> ChangesetReplacementCreator::_getIdToVersionMappings(
+ const OsmMapPtr& map) const
+{
+ LOG_INFO("Mapping element IDs to element versions for: " << map->getName() << "...");
+
+ ElementIdToVersionMapper idToVersionMapper;
+ LOG_DEBUG(idToVersionMapper.getInitStatusMessage());
+ idToVersionMapper.apply(map);
+ LOG_DEBUG(idToVersionMapper.getCompletedStatusMessage());
+ const QMap<ElementId, long> idToVersionMappings = idToVersionMapper.getMappings();
+ LOG_VART(idToVersionMappings.size());
+ return idToVersionMappings;
+}
+
+void ChangesetReplacementCreator::_addChangesetDeleteExclusionTags(OsmMapPtr& map)
+{
+ LOG_INFO(
+ "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_DEBUG(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_DEBUG(addTagVis.getCompletedStatusMessage());
+
+ // Add the changeset deletion exclusion tag to all children of those connected ways.
+
+ std::shared_ptr<ChainCriterion> childAddTagCrit(
+ new ChainCriterion(
+ std::shared_ptr<WayCriterion>(new WayCriterion()),
+ std::shared_ptr<TagKeyCriterion>(
+ new TagKeyCriterion(MetadataTags::HootChangeExcludeDelete()))));
+ RecursiveSetTagValueOp childDeletionExcludeTagOp(
+ MetadataTags::HootChangeExcludeDelete(), "yes", childAddTagCrit);
+ LOG_DEBUG(childDeletionExcludeTagOp.getInitStatusMessage());
+ childDeletionExcludeTagOp.apply(map);
+ LOG_DEBUG(childDeletionExcludeTagOp.getCompletedStatusMessage());
+
+ OsmMapWriterFactory::writeDebugMap(map, map->getName() + "-after-delete-exclusion-tagging");
+}
+
+OsmMapPtr ChangesetReplacementCreator::_loadSecMap(const QString& input)
+{
+ LOG_INFO("Loading secondary map: " << input << "...");
+
+ conf().set(
+ ConfigOptions::getConvertBoundingBoxKeepEntireFeaturesCrossingBoundsKey(),
+ _boundsOpts.loadSecKeepEntireCrossingBounds);
+ conf().set(
+ ConfigOptions::getConvertBoundingBoxKeepOnlyFeaturesInsideBoundsKey(),
+ _boundsOpts.loadSecKeepOnlyInsideBounds);
+ conf().set(
+ ConfigOptions::getConvertBoundingBoxKeepImmediatelyConnectedWaysOutsideBoundsKey(), false);
+
+ OsmMapPtr secMap(new OsmMap());
+ secMap->setName("sec");
+ IoUtils::loadMap(secMap, input, false, Status::Unknown2);
+
+ LOG_VART(MapProjector::toWkt(secMap->getProjection()));
+ OsmMapWriterFactory::writeDebugMap(secMap, "sec-after-cropped-load");
+
+ return secMap;
+}
+
+void ChangesetReplacementCreator::_filterFeatures(
+ OsmMapPtr& map, const ElementCriterionPtr& featureFilter, const Settings& config,
+ const QString& debugFileName)
+{
+ LOG_INFO(
+ "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_VART(MapProjector::toWkt(map->getProjection()));
+ OsmMapWriterFactory::writeDebugMap(map, debugFileName);
+}
+
+OsmMapPtr ChangesetReplacementCreator::_getCookieCutMap(OsmMapPtr doughMap, OsmMapPtr cutterMap)
+{
+ // TODO: 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)
+ {
+ return doughMap;
+ }
+
+ 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()));
+
+ 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_INFO(pointConverter.getInitStatusMessage());
+ pointConverter.apply(cutterMapToUse);
+ LOG_INFO(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_INFO("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. " << e.getWhat() <<
+ " Is your secondary data empty or have you filtered it to be empty?");
+ }
+ // Rethrow the original exception
+ throw;
+ }
+
+ // not exactly sure yet why this projection needs to be done
+ MapProjector::projectToWgs84(cutterShapeOutlineMap);
+ 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_INFO("Cookie cutting cutter shape out of: " << cookieCutMap->getName() << "...");
+
+ 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(cookieCutMap->getElementCount());
+ MapProjector::projectToWgs84(doughMap);
+ LOG_VARD(doughMap->getElementCount());
+ LOG_VART(MapProjector::toWkt(cookieCutMap->getProjection()));
+ OsmMapWriterFactory::writeDebugMap(cookieCutMap, "cookie-cut");
+
+ return cookieCutMap;
+}
+
+void ChangesetReplacementCreator::_combineMaps(OsmMapPtr& map1, OsmMapPtr& map2,
+ const bool throwOutDupes,
+ const QString& debugFileName)
+{
+ LOG_VART(map1.get());
+ LOG_VART(map2.get());
+ LOG_INFO("Combining maps: " << map1->getName() << " and " << map2->getName() << "...");
+
+ MapProjector::projectToWgs84(map1);
+ MapProjector::projectToWgs84(map2); // not exactly sure yet why this needs to be done
+
+ map1->append(map2, throwOutDupes);
+ LOG_VART(MapProjector::toWkt(map1->getProjection()));
+
+ OsmMapWriterFactory::writeDebugMap(map1, debugFileName);
+}
+
+void ChangesetReplacementCreator::_conflate(OsmMapPtr& map, const bool lenientBounds)
+{
+ map->setName("conflated");
+ LOG_INFO(
+ "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
+ {
+ conf().set(ConfigOptions::getWayJoinerKey(), WayJoinerAdvanced::className());
+ }
+ else
+ {
+ conf().set(ConfigOptions::getWayJoinerKey(), WayJoinerBasic::className());
+ }
+ conf().set(ConfigOptions::getWayJoinerAdvancedStrictNameMatchKey(), !_isNetworkConflate());
+
+ NamedOp preOps(ConfigOptions().getConflatePreOps());
+ preOps.apply(map);
+
+ UnifyingConflator().apply(map);
+
+ NamedOp postOps(ConfigOptions().getConflatePostOps());
+ postOps.apply(map);
+
+ MapProjector::projectToWgs84(map); // conflation works in planar
+ LOG_VART(MapProjector::toWkt(map->getProjection()));
+ OsmMapWriterFactory::writeDebugMap(map, "conflated");
+}
+
+void ChangesetReplacementCreator::_snapUnconnectedWays(
+ OsmMapPtr& map, const QStringList& snapWayStatuses, const QStringList& snapToWayStatuses,
+ const QString& typeCriterionClassName, const bool markSnappedWays, const QString& debugFileName)
+{
+ LOG_INFO(
+ "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.setSnapToWayStatuses(snapToWayStatuses);
+ lineSnapper.setSnapWayStatuses(snapWayStatuses);
+ lineSnapper.setMarkSnappedWays(markSnappedWays);
+ // 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(typeCriterionClassName);
+ lineSnapper.setWayToSnapToCriterionClassName(typeCriterionClassName);
+ LOG_DEBUG(lineSnapper.getInitStatusMessage());
+ lineSnapper.apply(map);
+ LOG_DEBUG(lineSnapper.getCompletedStatusMessage());
+
+ MapProjector::projectToWgs84(map); // snapping works in planar
+ LOG_VART(MapProjector::toWkt(map->getProjection()));
+
+ OsmMapWriterFactory::writeDebugMap(map, debugFileName);
+}
+
+OsmMapPtr ChangesetReplacementCreator::_getImmediatelyConnectedOutOfBoundsWays(
+ const ConstOsmMapPtr& map) const
+{
+ const QString outputMapName = "connected-ways";
+ LOG_INFO(
+ "Copying immediately connected out of bounds ways from: " << map->getName() <<
+ " to new map: " << outputMapName << "...");
+
+ std::shared_ptr<ChainCriterion> copyCrit(
+ new ChainCriterion(
+ std::shared_ptr<WayCriterion>(new WayCriterion()),
+ std::shared_ptr<TagKeyCriterion>(
+ new TagKeyCriterion(MetadataTags::HootConnectedWayOutsideBounds()))));
+ OsmMapPtr connectedWays = OsmUtils::getMapSubset(map, copyCrit);
+ connectedWays->setName(outputMapName);
+ LOG_VART(MapProjector::toWkt(connectedWays->getProjection()));
+ OsmMapWriterFactory::writeDebugMap(connectedWays, "connected-ways");
+ return connectedWays;
+}
+
+void ChangesetReplacementCreator::_cropMapForChangesetDerivation(
+ OsmMapPtr& map, const geos::geom::Envelope& bounds, const bool keepEntireFeaturesCrossingBounds,
+ const bool keepOnlyFeaturesInsideBounds, const bool isLinearMap, const QString& debugFileName)
+{
+ LOG_INFO("Cropping map: " << map->getName() << " for changeset derivation...");
+ LOG_VART(MapProjector::toWkt(map->getProjection()));
+
+ MapCropper cropper(bounds);
+ cropper.setKeepEntireFeaturesCrossingBounds(keepEntireFeaturesCrossingBounds);
+ cropper.setKeepOnlyFeaturesInsideBounds(keepOnlyFeaturesInsideBounds);
+ LOG_DEBUG(cropper.getInitStatusMessage());
+ cropper.apply(map);
+ LOG_DEBUG(cropper.getCompletedStatusMessage());
+
+ // Clean up straggling nodes in that are the result of cropping. Its ok to ignore info tags when
+ // dealing with only linear features, as all nodes in the data being conflated should be way nodes
+ // with no information.
+ // TODO: This can be removed now, since its already happening in MapCropper, right?
+ SuperfluousNodeRemover::removeNodes(map, isLinearMap);
+
+ LOG_VART(MapProjector::toWkt(map->getProjection()));
+ OsmMapWriterFactory::writeDebugMap(map, debugFileName);
+}
+
+void ChangesetReplacementCreator::_removeUnsnappedImmediatelyConnectedOutOfBoundsWays(
+ OsmMapPtr& map)
+{
+ LOG_INFO(
+ "Removing any immediately connected ways that were not previously snapped in: " <<
+ map->getName() << "...");
+ RemoveElementsVisitor removeVis;
+ removeVis.addCriterion(ElementCriterionPtr(new WayCriterion()));
+ removeVis.addCriterion(
+ ElementCriterionPtr(new TagKeyCriterion(MetadataTags::HootConnectedWayOutsideBounds())));
+ removeVis.addCriterion(
+ ElementCriterionPtr(
+ new NotCriterion(
+ std::shared_ptr<TagCriterion>(
+ new TagCriterion(MetadataTags::HootSnapped(), "snapped_way")))));
+ removeVis.setChainCriteria(true);
+ removeVis.setRecursive(true);
+ LOG_DEBUG(removeVis.getInitStatusMessage());
+ map->visitRw(removeVis);
+ LOG_DEBUG(removeVis.getCompletedStatusMessage());
+ LOG_VART(MapProjector::toWkt(map->getProjection()));
+ OsmMapWriterFactory::writeDebugMap(map, map->getName() + "-unsnapped-removed");
+}
+
+void ChangesetReplacementCreator::_excludeFeaturesFromChangesetDeletion(OsmMapPtr& map,
+ const QString& boundsStr)
+{
+ LOG_INFO(
+ "Marking reference features in: " << map->getName() << " for exclusion from deletion...");
+
+ 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));
+ std::shared_ptr<ChainCriterion> elementCrit(
+ new ChainCriterion(std::shared_ptr<WayCriterion>(new WayCriterion()), notInBoundsCrit));
+
+ RecursiveSetTagValueOp tagSetter(MetadataTags::HootChangeExcludeDelete(), "yes", elementCrit);
+ LOG_DEBUG(tagSetter.getInitStatusMessage());
+ tagSetter.apply(map);
+ LOG_DEBUG(tagSetter.getCompletedStatusMessage());
+
+ LOG_VART(MapProjector::toWkt(map->getProjection()));
+ OsmMapWriterFactory::writeDebugMap(map, map->getName() + "-after-delete-exclude-tags");
+}
+
+bool ChangesetReplacementCreator::_isNetworkConflate() const
+{
+ return
+ ConfigOptions().getMatchCreators().contains(
+ QString::fromStdString(NetworkMatchCreator::className()));
+}
+
+void ChangesetReplacementCreator::_setGlobalOpts(const QString& boundsStr)
+{
+ conf().set(ConfigOptions::getChangesetXmlWriterAddTimestampKey(), false);
+ conf().set(ConfigOptions::getReaderAddSourceDatetimeKey(), false);
+ conf().set(ConfigOptions::getWriterIncludeCircularErrorTagsKey(), false);
+ conf().set(ConfigOptions::getConvertBoundingBoxKey(), boundsStr);
+ // 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);
+
+ // These don't change between scenarios (or at least haven't needed to yet).
+ _boundsOpts.loadRefKeepOnlyInsideBounds = false;
+ _boundsOpts.cookieCutKeepOnlyInsideBounds = false;
+ _boundsOpts.changesetRefKeepOnlyInsideBounds = false;
+}
+
+void ChangesetReplacementCreator::_parseConfigOpts(
+ const bool lenientBounds, const GeometryTypeCriterion::GeometryType& geometryType)
+{
+ // These settings have been are customized for each geometry type and bounds handling preference.
+ // They were derived from small test cases, so we may need to do some tweaking as we encounter
+ // real world data.
+
+ 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;
+ _boundsOpts.loadSecKeepOnlyInsideBounds = false;
+ _boundsOpts.cookieCutKeepEntireCrossingBounds = false;
+ _boundsOpts.changesetRefKeepEntireCrossingBounds = false;
+ _boundsOpts.changesetSecKeepEntireCrossingBounds = false;
+ _boundsOpts.changesetSecKeepOnlyInsideBounds = true;
+ _boundsOpts.changesetAllowDeletingRefOutsideBounds = true;
+ _boundsOpts.inBoundsStrict = false;
+ }
+ else if (geometryType == GeometryTypeCriterion::GeometryType::Line)
+ {
+ if (lenientBounds)
+ {
+ _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
+ {
+ _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
+ // 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.
+
+ LOG_VARD(conf().getList(ConfigOptions::getConflatePostOpsKey()));
+ QStringList conflatePostOps = conf().getList(ConfigOptions::getConflatePostOpsKey());
+ conflatePostOps.removeAll(QString::fromStdString(WayJoinerOp::className()));
+ const int indexOfTagTruncater =
+ conflatePostOps.indexOf(QString::fromStdString(ApiTagTruncateVisitor::className()));
+ conflatePostOps.insert(
+ indexOfTagTruncater - 1, QString::fromStdString(WayJoinerOp::className()));
+ conf().set(ConfigOptions::getConflatePostOpsKey(), conflatePostOps);
+ LOG_VARD(conf().getList(ConfigOptions::getConflatePostOpsKey()));
+ }
+ }
+ else if (geometryType == GeometryTypeCriterion::GeometryType::Polygon)
+ {
+ if (lenientBounds)
+ {
+ _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
+ {
+ _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
+ {
+ // shouldn't ever get here
+ throw IllegalArgumentException("Invalid geometry type.");
+ }
+
+ conf().set(
+ ConfigOptions::getChangesetReplacementAllowDeletingReferenceFeaturesOutsideBoundsKey(),
+ _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);
+}
+
+}