void RemoveDuplicateReviewsOp::apply(shared_ptr<OsmMap>& map)
{
    _map = map;

    // go through all the relations to get duplicate reviews
    const RelationMap& relations = map->getRelationMap();
    QMap< set<ElementId>, QList<ReviewMarker::ReviewUid> > membersToReview;
    for (RelationMap::const_iterator it = relations.begin(); it != relations.end(); it++)
    {
        ElementId eid = ElementId::relation(it->first);
        if (ReviewMarker::isReviewUid(map, eid))
        {
            membersToReview[ReviewMarker::getReviewElements(map, eid)].append(eid);
        }
    }

    //loop through dupplicate reviews
    QMap< set<ElementId>, QList<ReviewMarker::ReviewUid> >::iterator it = membersToReview.begin();
    while (it != membersToReview.end())
    {
        set<ElementId> eids = it.key();

        //remove duplicate reviews
        QList<ReviewMarker::ReviewUid> duplicateReviews = it.value();

        //Only remove reviews and process if there is more than one review
        // See discussion here: https://github.com/ngageoint/hootenanny/issues/81#issuecomment-162980656
        if (eids.size() == 2 && duplicateReviews.size() > 1)
        {
            for (int i = 0; i < duplicateReviews.size(); i++)
            {
                ReviewMarker::removeElement(map, duplicateReviews[i]);
            }

            ElementId beid = *eids.begin();
            ElementId eeid = *eids.rbegin();

            OsmMapPtr copy(new OsmMap());
            CopySubsetOp(map, beid, eeid).apply(copy);
            copy->getElement(beid)->setStatus(Status::Unknown1);
            copy->getElement(eeid)->setStatus(Status::Unknown2);

            Match* match = MatchFactory::getInstance().createMatch(copy, beid, eeid);
            if (match && match->getType() != MatchType::Miss)
            {
                QString explain = match->explain();
                if (match->getType() == MatchType::Match)
                {
                    if (explain.isEmpty())
                    {
                        explain = "Multiple overlapping high confidence reviews";
                    }
                    else
                    {
                        explain = "Multiple overlapping high confidence reviews: " + explain;
                    }
                }
                ReviewMarker::mark(map, map->getElement(beid), map->getElement(eeid),
                                   explain, match->getMatchName(), match->getClassification().getReviewP());
            }
        }
        ++it;
    }
}
Handle<Value> SublineStringMatcherJs::extractMatchingSublines(const Arguments& args)
{
    HandleScope scope;

    SublineStringMatcherJs* smJs = ObjectWrap::Unwrap<SublineStringMatcherJs>(args.This());
    SublineStringMatcherPtr sm = smJs->getSublineStringMatcher();

    OsmMapJs* mapJs = ObjectWrap::Unwrap<OsmMapJs>(args[0]->ToObject());
    ElementJs* e1Js = ObjectWrap::Unwrap<ElementJs>(args[1]->ToObject());
    ElementJs* e2Js = ObjectWrap::Unwrap<ElementJs>(args[2]->ToObject());
    ConstOsmMapPtr m = mapJs->getConstMap();
    ConstElementPtr e1 = e1Js->getConstElement();
    ConstElementPtr e2 = e2Js->getConstElement();

    Handle<Value> result;

    try
    {
        WaySublineMatchString match = sm->findMatch(m, e1, e2);

        if (match.isEmpty())
        {
            return Undefined();
        }

        // convert match into elements in a new map.
        set<ElementId> eids;
        eids.insert(e1->getElementId());
        eids.insert(e2->getElementId());
        OsmMapPtr copiedMap(new OsmMap(m->getProjection()));
        CopySubsetOp(m, eids).apply(copiedMap);
        WaySublineMatchString copiedMatch(match, copiedMap);

        // split the shared line based on the matching subline
        ElementPtr match1, scraps1;
        ElementPtr match2, scraps2;
        WaySublineString string1 = copiedMatch.getSublineString1();
        WaySublineString string2 = copiedMatch.getSublineString2();

        try
        {
            MultiLineStringSplitter().split(copiedMap, string1, copiedMatch.getReverseVector1(), match1,
                                            scraps1);
            MultiLineStringSplitter().split(copiedMap, string2, copiedMatch.getReverseVector2(), match2,
                                            scraps2);
        }
        catch (const IllegalArgumentException& e)
        {
            // this is unusual print out some information useful to debugging.
            MapReprojector::reprojectToWgs84(copiedMap);
            LOG_WARN(OsmWriter::toString(copiedMap));
            throw e;
        }

        if (!match1 || !match2)
        {
            result = Undefined();
        }
        else
        {
            Handle<Object> obj = Object::New();
            obj->Set(String::NewSymbol("map"), OsmMapJs::create(copiedMap));
            obj->Set(String::NewSymbol("match1"), ElementJs::New(match1));
            obj->Set(String::NewSymbol("match2"), ElementJs::New(match2));
            result = obj;
        }
    }
    catch (const HootException& e)
    {
        return v8::ThrowException(HootExceptionJs::create(e));
    }

    return scope.Close(result);
}
MaximalSublineStringMatcher::ScoredMatch MaximalSublineStringMatcher::_evaluateMatch(
  const ConstOsmMapPtr &map, Meters maxDistance, const vector<ConstWayPtr>& ways1,
  const vector<ConstWayPtr> &ways2, const vector<bool>& reversed1,
  const vector<bool> &reversed2) const
{
  vector<WaySublineMatch> matches;

  // make a copy of the map and the ways we need so we can reverse the ways as needed.
  set<ElementId> eids;
  _insertElementIds(ways1, eids);
  _insertElementIds(ways2, eids);
  OsmMapPtr copiedMap(new OsmMap(map->getProjection()));
  CopySubsetOp(map, eids).apply(copiedMap);

  vector<WayPtr> prep1 = _changeMap(ways1, copiedMap);
  vector<WayPtr> prep2 = _changeMap(ways2, copiedMap);

  // reversed ways as appropriate
  _reverseWays(prep1, reversed1);
  _reverseWays(prep2, reversed2);

  double scoreSum = 0;

  // go through and match each way against every other way
  for (size_t i = 0; i < prep1.size(); i++)
  {
    for (size_t j = 0; j < prep2.size(); j++)
    {
      double score;
      WaySublineMatchString m = _sublineMatcher->findMatch(copiedMap, prep1[i], prep2[j], score,
        maxDistance);

      scoreSum += score;
      matches.insert(matches.end(), m.getMatches().begin(), m.getMatches().end());
    }
  }

  HashMap<long, bool> wayIdToReversed1, wayIdToReversed2;
  // create a map from way id to reverse status
  for (size_t i = 0; i < prep1.size(); i++)
  {
    wayIdToReversed1[prep1[i]->getId()] = reversed1[i];
  }
  for (size_t i = 0; i < prep2.size(); i++)
  {
    wayIdToReversed2[prep2[i]->getId()] = reversed2[i];
  }

  // go through all the matches
  for (size_t i = 0; i < matches.size(); i++)
  {
    WaySubline ws1, ws2;
    // if the direction is reversed on one but not both ways then mark the match as reversed.
    long m1Id = matches[i].getSubline1().getStart().getWay()->getId();
    long m2Id = matches[i].getSubline2().getStart().getWay()->getId();

    if (wayIdToReversed1[m1Id])
    {
      // make sure the way subline is pointed to the right way (not reversed)
      ConstWayPtr w = map->getWay(matches[i].getSubline1().getElementId());
      ws1 = matches[i].getSubline1().reverse(w);
    }
    else
    {
      ws1 = WaySubline(matches[i].getSubline1(), map);
    }

    if (wayIdToReversed2[m2Id])
    {
      // make sure the way subline is pointed to the right way (not reversed)
      ConstWayPtr w = map->getWay(matches[i].getSubline2().getElementId());
      ws2 = matches[i].getSubline2().reverse(w);
    }
    else
    {
      ws2 = WaySubline(matches[i].getSubline2(), map);
    }

    if (wayIdToReversed1[m1Id] != wayIdToReversed2[m2Id])
    {
      matches[i] = WaySublineMatch(ws1, ws2, true);
    }
    else
    {
      matches[i] = WaySublineMatch(ws1, ws2, false);
    }
  }

  return ScoredMatch(scoreSum, matches);
}