clean up and refactor half-baked development commits and squash them into a new version.

Changes:

* support for multiple GTFS feeds as input in filtering, read default global and local configuration files
* be a bit more memory conservative
* make caching optional
* dont delete orphan edge if it would transform a degree 3 node with a possible full turn into a degree 2 node eligible for contraction
* dedicated filters for funicular and gondola
* make max snap level option more intuitive
* allow filter rules for level 0
* additional fallback for station snapping
* dont try to route for MOT unequal to trip in -T mode, force-snap to orphaned OSM station if not snap was possible
* write bounds to filtered osm
* remove unused surrounding heuristic
* use bus lanes info
* be a bit more tolerant for bus oneway streets
* create missing directories
* error if no cfg is present, clean up evaluation Makefile
This commit is contained in:
Patrick Brosi 2019-01-10 16:52:59 +01:00
parent 2cc2d2dc23
commit 63f0b61ea1
60 changed files with 4532 additions and 1576 deletions

View file

@ -37,6 +37,31 @@ BOX BBoxIdx::getFullWebMercBox() const {
_root.box.getUpperRight().getY(), _root.box.getUpperRight().getX()));
}
// _____________________________________________________________________________
BOX BBoxIdx::getFullBox() const { return _root.box; }
// _____________________________________________________________________________
std::vector<util::geo::Box<double>> BBoxIdx::getLeafs() const {
std::vector<util::geo::Box<double>> ret;
getLeafsRec(_root, &ret);
return ret;
}
// _____________________________________________________________________________
void BBoxIdx::getLeafsRec(const BBoxIdxNd& nd,
std::vector<util::geo::Box<double>>* ret) const {
if (!nd.childs.size()) {
ret->push_back(nd.box);
return;
}
for (const auto& child : nd.childs) {
getLeafsRec(child, ret);
}
return;
}
// _____________________________________________________________________________
bool BBoxIdx::treeHas(const Point<double>& p, const BBoxIdxNd& nd) const {
if (!nd.childs.size()) return util::geo::contains(p, nd.box);

View file

@ -38,9 +38,15 @@ class BBoxIdx {
// Return the full total bounding box of this index
BOX getFullWebMercBox() const;
// Return the full total bounding box of this index
BOX getFullBox() const;
// Return the size of this index
size_t size() const;
// return the leaf bounding boxes of this idx
std::vector<Box<double>> getLeafs() const;
private:
double _padding;
size_t _size;
@ -50,6 +56,9 @@ class BBoxIdx {
void addToTree(const Box<double>& box, BBoxIdxNd* nd, size_t lvl);
bool treeHas(const Point<double>& p, const BBoxIdxNd& nd) const;
void getLeafsRec(const BBoxIdxNd& nd,
std::vector<util::geo::Box<double>>* ret) const;
static const size_t MAX_LVL = 5;
static constexpr double MIN_COM_AREA = 0.0;
};

View file

@ -43,8 +43,20 @@ using pfaedle::osm::OsmRel;
using pfaedle::osm::OsmNode;
using pfaedle::osm::EdgeGrid;
using pfaedle::osm::NodeGrid;
using pfaedle::osm::EqSearch;
using pfaedle::osm::BlockSearch;
using ad::cppgtfs::gtfs::Stop;
// _____________________________________________________________________________
bool EqSearch::operator()(const Node* cand, const StatInfo* si) const {
if (orphanSnap && cand->pl().getSI() &&
(!cand->pl().getSI()->getGroup() ||
cand->pl().getSI()->getGroup()->getStops().size() == 0)) {
return true;
}
return cand->pl().getSI() && cand->pl().getSI()->simi(si) > minSimi;
}
// _____________________________________________________________________________
OsmBuilder::OsmBuilder() {}
@ -140,7 +152,8 @@ void OsmBuilder::read(const std::string& path, const OsmReadOpts& opts,
POINT geom = *s->pl().getGeom();
NodePL pl = s->pl();
pl.getSI()->setIsFromOsm(false);
const auto& r = snapStation(g, &pl, &eg, &sng, opts, res, false, d);
const auto& r =
snapStation(g, &pl, &eg, &sng, opts, res, false, false, d);
groupStats(r);
for (auto n : r) {
// if the snapped station is very near to the original OSM
@ -153,32 +166,70 @@ void OsmBuilder::read(const std::string& path, const OsmReadOpts& opts,
}
}
std::vector<const Stop*> notSnapped;
for (size_t i = 0; i < opts.maxSnapDistances.size(); i++) {
double d = opts.maxSnapDistances[i];
for (auto& s : *fs) {
auto pl = plFromGtfs(s.first, opts);
StatGroup* group =
groupStats(snapStation(g, &pl, &eg, &sng, opts, res,
i == opts.maxSnapDistances.size() - 1, d));
StatGroup* group = groupStats(
snapStation(g, &pl, &eg, &sng, opts, res,
i == opts.maxSnapDistances.size() - 1, false, d));
if (group) {
group->addStop(s.first);
(*fs)[s.first] = *group->getNodes().begin();
} else if (i == opts.maxSnapDistances.size() - 1) {
LOG(VDEBUG) << "Could not snap station "
<< "(" << pl.getSI()->getName() << ")"
<< " (" << s.first->getLat() << "," << s.first->getLng()
<< ") in normal run, trying again later in orphan mode.";
notSnapped.push_back(s.first);
}
}
}
if (notSnapped.size())
LOG(VDEBUG) << notSnapped.size() << " stations could not be snapped in "
"normal run, trying again in orphan "
"mode.";
// try again, but aggressively snap to orphan OSM stations which have
// not been assigned to any GTFS stop yet
for (size_t i = 0; i < opts.maxSnapDistances.size(); i++) {
double d = opts.maxSnapDistances[i];
for (auto& s : notSnapped) {
auto pl = plFromGtfs(s, opts);
StatGroup* group = groupStats(
snapStation(g, &pl, &eg, &sng, opts, res,
i == opts.maxSnapDistances.size() - 1, true, d));
if (group) {
group->addStop(s);
// add the added station name as an alt name to ensure future
// similarity
for (auto n : group->getNodes()) {
if (n->pl().getSI())
n->pl().getSI()->addAltName(pl.getSI()->getName());
}
(*fs)[s] = *group->getNodes().begin();
} else if (i ==
opts.maxSnapDistances.size() - 1) { // only fail on last
// finally give up
// add a group with only this stop in it
StatGroup* dummyGroup = new StatGroup();
Node* dummyNode = g->addNd(pl);
dummyNode->pl().getSI()->setGroup(dummyGroup);
dummyGroup->addNode(dummyNode);
dummyGroup->addStop(s.first);
(*fs)[s.first] = dummyNode;
dummyGroup->addStop(s);
(*fs)[s] = dummyNode;
LOG(WARN) << "Could not snap station "
<< "(" << pl.getSI()->getName() << ")"
<< " (" << s.first->getLat() << "," << s.first->getLng()
<< ")";
<< " (" << s->getLat() << "," << s->getLng() << ")";
}
}
}
@ -188,7 +239,7 @@ void OsmBuilder::read(const std::string& path, const OsmReadOpts& opts,
deleteOrphNds(g);
LOG(VDEBUG) << "Deleting orphan edges...";
deleteOrphEdgs(g);
deleteOrphEdgs(g, opts);
LOG(VDEBUG) << "Collapsing edges...";
collapseEdges(g);
@ -197,7 +248,7 @@ void OsmBuilder::read(const std::string& path, const OsmReadOpts& opts,
deleteOrphNds(g);
LOG(VDEBUG) << "Deleting orphan edges...";
deleteOrphEdgs(g);
deleteOrphEdgs(g, opts);
LOG(VDEBUG) << "Writing graph components...";
// the restrictor is needed here to prevent connections in the graph
@ -223,6 +274,89 @@ void OsmBuilder::read(const std::string& path, const OsmReadOpts& opts,
<< " edges and " << comps << " connected component(s)";
}
// _____________________________________________________________________________
void OsmBuilder::overpassQryWrite(std::ostream* out,
const std::vector<OsmReadOpts>& opts,
const BBoxIdx& latLngBox) const {
OsmIdSet bboxNodes, noHupNodes;
MultAttrMap emptyF;
RelLst rels;
OsmIdList ways;
RelMap nodeRels, wayRels;
// TODO(patrick): not needed here!
Restrictions rests;
NIdMap nodes;
// always empty
NIdMultMap multNodes;
util::xml::XmlWriter wr(out, true, 4);
*out << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
wr.openComment();
wr.writeText(" - written by pfaedle -");
wr.closeTag();
wr.openTag("osm-script",
{{"timeout", "99999"}, {"element-limit", "1073741824"}});
OsmFilter filter;
for (const OsmReadOpts& o : opts) {
filter = filter.merge(OsmFilter(o.keepFilter, o.dropFilter));
}
wr.openTag("union");
size_t c = 0;
for (auto box : latLngBox.getLeafs()) {
if (box.getLowerLeft().getX() > box.getUpperRight().getX()) continue;
c++;
wr.openComment();
wr.writeText(std::string("Bounding box #") + std::to_string(c) + " (" +
std::to_string(box.getLowerLeft().getY()) + ", " +
std::to_string(box.getLowerLeft().getX()) + ", " +
std::to_string(box.getUpperRight().getY()) + ", " +
std::to_string(box.getUpperRight().getX()) + ")");
wr.closeTag();
for (auto t : std::vector<std::string>{"way", "node", "relation"}) {
for (auto r : filter.getKeepRules()) {
for (auto val : r.second) {
if (t == "way" && (val.second & OsmFilter::WAY)) continue;
if (t == "relation" && (val.second & OsmFilter::REL)) continue;
if (t == "node" && (val.second & OsmFilter::NODE)) continue;
wr.openTag("query", {{"type", t}});
if (val.first == "*")
wr.openTag("has-kv", {{"k", r.first}});
else
wr.openTag("has-kv", {{"k", r.first}, {"v", val.first}});
wr.closeTag();
wr.openTag("bbox-query",
{{"s", std::to_string(box.getLowerLeft().getY())},
{"w", std::to_string(box.getLowerLeft().getX())},
{"n", std::to_string(box.getUpperRight().getY())},
{"e", std::to_string(box.getUpperRight().getX())}});
wr.closeTag();
wr.closeTag();
}
}
}
}
wr.closeTag();
wr.openTag("union");
wr.openTag("item");
wr.closeTag();
wr.openTag("recurse", {{"type", "down"}});
wr.closeTag();
wr.closeTag();
wr.openTag("print");
wr.closeTags();
}
// _____________________________________________________________________________
void OsmBuilder::filterWrite(const std::string& in, const std::string& out,
const std::vector<OsmReadOpts>& opts,
@ -250,8 +384,15 @@ void OsmBuilder::filterWrite(const std::string& in, const std::string& out,
outstr << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
wr.openTag("osm");
// TODO(patrick): write bounding box tag
wr.openTag(
"bounds",
{{"minlat", std::to_string(latLngBox.getFullBox().getLowerLeft().getY())},
{"minlon", std::to_string(latLngBox.getFullBox().getLowerLeft().getX())},
{"maxlat",
std::to_string(latLngBox.getFullBox().getUpperRight().getY())},
{"maxlon",
std::to_string(latLngBox.getFullBox().getUpperRight().getX())}});
wr.closeTag();
OsmFilter filter;
AttrKeySet attrKeys[3] = {};
@ -1071,8 +1212,7 @@ EdgeGrid OsmBuilder::buildEdgeIdx(Graph* g, size_t size,
}
// _____________________________________________________________________________
NodeGrid OsmBuilder::buildNodeIdx(Graph* g, size_t size,
const BOX& webMercBox,
NodeGrid OsmBuilder::buildNodeIdx(Graph* g, size_t size, const BOX& webMercBox,
bool which) const {
NodeGrid ret(size, size, webMercBox, false);
for (auto* n : *g->getNds()) {
@ -1158,9 +1298,10 @@ bool OsmBuilder::isBlocked(const Edge* e, const StatInfo* si, const POINT& p,
// _____________________________________________________________________________
Node* OsmBuilder::eqStatReach(const Edge* e, const StatInfo* si, const POINT& p,
double maxD, int maxFullTurns,
double minAngle) const {
return depthSearch(e, si, p, maxD, maxFullTurns, minAngle, EqSearch());
double maxD, int maxFullTurns, double minAngle,
bool orphanSnap) const {
return depthSearch(e, si, p, maxD, maxFullTurns, minAngle,
EqSearch(orphanSnap));
}
// _____________________________________________________________________________
@ -1187,8 +1328,7 @@ std::set<Node*> OsmBuilder::getMatchingNds(const NodePL& s, NodeGrid* ng,
std::set<Node*> ret;
double distor = webMercDistFactor(*s.getGeom());
std::set<Node*> neighs;
BOX box =
util::geo::pad(util::geo::getBoundingBox(*s.getGeom()), d / distor);
BOX box = util::geo::pad(util::geo::getBoundingBox(*s.getGeom()), d / distor);
ng->get(box, &neighs);
for (auto* n : neighs) {
@ -1205,8 +1345,7 @@ std::set<Node*> OsmBuilder::getMatchingNds(const NodePL& s, NodeGrid* ng,
Node* OsmBuilder::getMatchingNd(const NodePL& s, NodeGrid* ng, double d) const {
double distor = webMercDistFactor(*s.getGeom());
std::set<Node*> neighs;
BOX box =
util::geo::pad(util::geo::getBoundingBox(*s.getGeom()), d / distor);
BOX box = util::geo::pad(util::geo::getBoundingBox(*s.getGeom()), d / distor);
ng->get(box, &neighs);
Node* ret = 0;
@ -1229,7 +1368,7 @@ Node* OsmBuilder::getMatchingNd(const NodePL& s, NodeGrid* ng, double d) const {
std::set<Node*> OsmBuilder::snapStation(Graph* g, NodePL* s, EdgeGrid* eg,
NodeGrid* sng, const OsmReadOpts& opts,
Restrictor* restor, bool surrHeur,
double d) const {
bool orphSnap, double d) const {
assert(s->getSI());
std::set<Node*> ret;
@ -1239,10 +1378,14 @@ std::set<Node*> OsmBuilder::snapStation(Graph* g, NodePL* s, EdgeGrid* eg,
if (pq.empty() && surrHeur) {
// no station found in the first round, try again with the nearest
// surrounding
// station with matching name
// surrounding station with matching name
const Node* best = getMatchingNd(*s, sng, opts.maxSnapFallbackHeurDistance);
if (best) getEdgCands(*best->pl().getGeom(), &pq, eg, d);
if (best) {
getEdgCands(*best->pl().getGeom(), &pq, eg, d);
} else {
// if still no luck, get edge cands in fallback snap distance
getEdgCands(*s->getGeom(), &pq, eg, opts.maxSnapFallbackHeurDistance);
}
}
while (!pq.empty()) {
@ -1254,7 +1397,7 @@ std::set<Node*> OsmBuilder::snapStation(Graph* g, NodePL* s, EdgeGrid* eg,
Node* eq = 0;
if (!(eq = eqStatReach(e, s->getSI(), geom, 2 * d, 0,
opts.maxAngleSnapReach))) {
opts.maxAngleSnapReach, orphSnap))) {
if (e->pl().lvl() > opts.maxSnapLevel) continue;
if (isBlocked(e, s->getSI(), geom, opts.maxBlockDistance, 0,
opts.maxAngleSnapReach)) {
@ -1309,11 +1452,6 @@ std::set<Node*> OsmBuilder::snapStation(Graph* g, NodePL* s, EdgeGrid* eg,
}
}
// get surrounding nodes
// TODO(patrick): own distance configuration for this!
const auto& sur = getMatchingNds(*s, sng, opts.maxGroupSearchDistance);
ret.insert(sur.begin(), sur.end());
return ret;
}
@ -1336,7 +1474,10 @@ StatGroup* OsmBuilder::groupStats(const NodeSet& s) const {
}
}
if (!used) delete ret;
if (!used) {
delete ret;
return 0;
}
return ret;
}
@ -1507,7 +1648,7 @@ void OsmBuilder::getKeptAttrKeys(const OsmReadOpts& opts,
}
// _____________________________________________________________________________
void OsmBuilder::deleteOrphEdgs(Graph* g) const {
void OsmBuilder::deleteOrphEdgs(Graph* g, const OsmReadOpts& opts) const {
size_t ROUNDS = 3;
for (size_t c = 0; c < ROUNDS; c++) {
for (auto i = g->getNds()->begin(); i != g->getNds()->end();) {
@ -1515,6 +1656,15 @@ void OsmBuilder::deleteOrphEdgs(Graph* g) const {
++i;
continue;
}
// check if the removal of this edge would transform a steep angle
// full turn at an intersection into a node 2 eligible for contraction
// if so, dont delete
if (keepFullTurn(*i, opts.fullTurnAngle)) {
++i;
continue;
}
i = g->delNd(*i);
continue;
i++;
@ -1706,3 +1856,43 @@ void OsmBuilder::writeSelfEdgs(Graph* g) const {
}
}
}
// _____________________________________________________________________________
bool OsmBuilder::keepFullTurn(const trgraph::Node* n, double ang) const {
if (n->getInDeg() + n->getOutDeg() != 1) return false;
const trgraph::Edge* e = 0;
if (n->getOutDeg())
e = n->getAdjListOut().front();
else
e = n->getAdjListIn().front();
auto other = e->getOtherNd(n);
if (other->getInDeg() + other->getOutDeg() == 3) {
const trgraph::Edge* a = 0;
const trgraph::Edge* b = 0;
for (auto f : other->getAdjListIn()) {
if (f != e && !a)
a = f;
else if (f != e && !b)
b = f;
}
for (auto f : other->getAdjListOut()) {
if (f != e && !a)
a = f;
else if (f != e && !b)
b = f;
}
auto ap = a->pl().backHop();
auto bp = b->pl().backHop();
if (a->getTo() != other) ap = a->pl().frontHop();
if (b->getTo() != other) bp = b->pl().frontHop();
return router::angSmaller(ap, *other->pl().getGeom(), bp, ang);
}
return false;
}

View file

@ -59,10 +59,10 @@ struct SearchFunc {
};
struct EqSearch : public SearchFunc {
explicit EqSearch(bool orphanSnap) : orphanSnap(orphanSnap) {}
double minSimi = 0.9;
bool operator()(const Node* cand, const StatInfo* si) const {
return cand->pl().getSI() && cand->pl().getSI()->simi(si) > minSimi;
}
bool orphanSnap;
bool operator()(const Node* cand, const StatInfo* si) const;
};
struct BlockSearch : public SearchFunc {
@ -91,6 +91,11 @@ class OsmBuilder {
const BBoxIdx& box, size_t gridSize, router::FeedStops* fs,
Restrictor* res);
// Based on the list of options, output an overpass XML query for getting
// the data needed for routing
void overpassQryWrite(std::ostream* out, const std::vector<OsmReadOpts>& opts,
const BBoxIdx& latLngBox) const;
// Based on the list of options, read an OSM file from in and output an
// OSM file to out which contains exactly the entities that are needed
// from the file at in
@ -170,7 +175,7 @@ class OsmBuilder {
void writeGeoms(Graph* g) const;
void deleteOrphNds(Graph* g) const;
void deleteOrphEdgs(Graph* g) const;
void deleteOrphEdgs(Graph* g, const OsmReadOpts& opts) const;
double dist(const Node* a, const Node* b) const;
double webMercDist(const Node* a, const Node* b) const;
double webMercDistFactor(const POINT& a) const;
@ -198,13 +203,14 @@ class OsmBuilder {
NodeSet snapStation(Graph* g, NodePL* s, EdgeGrid* eg, NodeGrid* sng,
const OsmReadOpts& opts, Restrictor* restor, bool surHeur,
double maxD) const;
bool orphSnap, double maxD) const;
// Checks if from the edge e, a station similar to si can be reach with less
// than maxD distance and less or equal to "maxFullTurns" full turns. If
// such a station exists, it is returned. If not, 0 is returned.
Node* eqStatReach(const Edge* e, const StatInfo* si, const POINT& p,
double maxD, int maxFullTurns, double maxAng) const;
double maxD, int maxFullTurns, double maxAng,
bool orph) const;
Node* depthSearch(const Edge* e, const StatInfo* si, const POINT& p,
double maxD, int maxFullTurns, double minAngle,
@ -243,6 +249,8 @@ class OsmBuilder {
bool relKeep(osmid id, const RelMap& rels, const FlatRels& fl) const;
bool keepFullTurn(const trgraph::Node* n, double ang) const;
std::map<TransitEdgeLine, TransitEdgeLine*> _lines;
std::map<size_t, TransitEdgeLine*> _relLines;
};

View file

@ -102,12 +102,12 @@ uint64_t OsmFilter::contained(const AttrMap& attrs, const Attr& attr) {
// _____________________________________________________________________________
uint8_t OsmFilter::level(const AttrMap& attrs) const {
// the best matching level is always returned
for (int16_t i = 0; i < 7; i++) {
for (int16_t i = 0; i < 8; i++) {
for (const auto& kv : attrs) {
const auto& lkv = (_levels + i)->find(kv.first);
if (lkv != (_levels + i)->end()) {
for (const auto& val : lkv->second) {
if (valMatches(kv.second, val.first)) return i + 1;
if (valMatches(kv.second, val.first)) return i;
}
}
}
@ -169,7 +169,7 @@ std::vector<std::string> OsmFilter::getAttrKeys() const {
for (const auto& kv : _noRestr) {
ret.push_back(kv.first);
}
for (uint8_t i = 0; i < 7; i++) {
for (uint8_t i = 0; i < 8; i++) {
for (const auto& kv : *(_levels + i)) {
ret.push_back(kv.first);
}
@ -191,27 +191,6 @@ OsmFilter OsmFilter::merge(const OsmFilter& other) const {
keep[kv.first].insert(kv.second.begin(), kv.second.end());
}
// TODO(patrick): multi-level combination for filters. otherwise
// filter drop filters meant as a refinement for keep filters
// interfere with other keeps
// const auto* d = &_drop;
// for (size_t i = 0; i < 2; i++) {
// for (const auto& kv : *d) {
// if (keep.find(kv.first) != keep.end()) {
// for (const auto& val : kv.second) {
// if (keep[kv.first].find(val.first) == keep[kv.first].end()) {
// drop[kv.first].insert(val);
// }
// }
// } else {
// drop[kv.first].insert(kv.second.begin(), kv.second.end());
// }
// }
// d = &other._drop;
// }
return OsmFilter(keep, drop);
}
@ -258,3 +237,13 @@ uint64_t OsmFilter::posRestr(const AttrMap& attrs) const {
if (contained(attrs, _noRestr, ALL)) return false;
return (contained(attrs, _posRestr, ALL));
}
// _____________________________________________________________________________
const pfaedle::osm::MultAttrMap& OsmFilter::getKeepRules() const {
return _keep;
}
// _____________________________________________________________________________
const pfaedle::osm::MultAttrMap& OsmFilter::getDropRules() const {
return _drop;
}

View file

@ -33,6 +33,9 @@ class OsmFilter {
OsmFilter merge(const OsmFilter& other) const;
const MultAttrMap& getKeepRules() const;
const MultAttrMap& getDropRules() const;
std::string toString() const;
static bool valMatches(const std::string& a, const std::string& b, bool m);

View file

@ -113,7 +113,7 @@ struct OsmReadOpts {
MultAttrMap noHupFilter;
MultAttrMap keepFilter;
MultAttrMap levelFilters[7];
MultAttrMap levelFilters[8];
MultAttrMap dropFilter;
MultAttrMap oneWayFilter;
MultAttrMap oneWayFilterRev;
@ -136,7 +136,6 @@ struct OsmReadOpts {
double maxAngleSnapReach;
std::vector<double> maxSnapDistances;
double maxSnapFallbackHeurDistance;
double maxGroupSearchDistance;
double maxBlockDistance;
double maxOsmStationDistance;
@ -144,6 +143,8 @@ struct OsmReadOpts {
// TODO(patrick): this is not implemented yet
double levelSnapPunishFac[7] = {0, 0, 0, 0, 0, 0, 0};
double fullTurnAngle;
// restriction system
MultAttrMap restrPosRestr;
MultAttrMap restrNegRestr;
@ -179,7 +180,6 @@ inline bool operator==(const OsmReadOpts& a, const OsmReadOpts& b) {
fabs(a.maxOsmStationDistance - b.maxOsmStationDistance) < 0.1 &&
fabs(a.maxSnapFallbackHeurDistance - b.maxSnapFallbackHeurDistance) <
0.1 &&
fabs(a.maxGroupSearchDistance - b.maxGroupSearchDistance) < 0.1 &&
fabs(a.maxBlockDistance - b.maxBlockDistance) < 0.1 &&
fabs(a.levelSnapPunishFac[0] - b.levelSnapPunishFac[0]) < 0.1 &&
fabs(a.levelSnapPunishFac[1] - b.levelSnapPunishFac[1]) < 0.1 &&
@ -188,6 +188,7 @@ inline bool operator==(const OsmReadOpts& a, const OsmReadOpts& b) {
fabs(a.levelSnapPunishFac[4] - b.levelSnapPunishFac[4]) < 0.1 &&
fabs(a.levelSnapPunishFac[5] - b.levelSnapPunishFac[5]) < 0.1 &&
fabs(a.levelSnapPunishFac[6] - b.levelSnapPunishFac[6]) < 0.1 &&
fabs(a.fullTurnAngle - b.fullTurnAngle) < 0.1 &&
a.restrPosRestr == b.restrPosRestr &&
a.restrNegRestr == b.restrNegRestr &&
a.noRestrFilter == b.noRestrFilter;