[generator] Discard invalid closed lines #5556

Open
pastk wants to merge 1 commit from pastk-generator-isolines-skip-resimplify into master
20 changed files with 211 additions and 86 deletions

View file

@ -51,7 +51,6 @@ std::string JoinPath(std::string const & folder, Args &&... args)
template <typename... Args>
std::string JoinPath(std::string const & dir, std::string const & fileOrDir, Args &&... args)
{
ASSERT(!dir.empty(), ("JoinPath dir is empty"));
ASSERT(!fileOrDir.empty(), ("JoinPath fileOrDir is empty"));
return impl::JoinPath(dir, fileOrDir, std::forward<Args>(args)...);
}

View file

@ -80,6 +80,17 @@ bool FeatureBuilder::IsGeometryClosed() const
return (poly.size() > 2 && poly.front() == poly.back());
}
bool FeatureBuilder::IsClosedLine() const
{
if (GetGeomType() == GeomType::Line)
{
PointSeq const & points = GetOuterGeometry();
CHECK(points.size() > 1, (*this));
return points.front() == points.back();
}
return false;
}
m2::PointD FeatureBuilder::GetGeometryCenter() const
{
m2::PointD ret(0.0, 0.0);
@ -567,14 +578,6 @@ bool FeatureBuilder::HasOsmId(base::GeoObjectId const & id) const
return false;
}
int FeatureBuilder::GetMinFeatureDrawScale() const
{
int const minScale = GetMinDrawableScale(GetTypesHolder(), m_limitRect);
// some features become invisible after merge processing, so -1 is possible
return (minScale == -1 ? 1000 : minScale);
}
bool FeatureBuilder::AddName(std::string_view lang, std::string_view name)
{
return m_params.AddName(lang, name);
@ -602,7 +605,7 @@ bool FeatureBuilder::IsDrawableInRange(int lowScale, int highScale) const
auto const types = GetTypesHolder();
while (lowScale <= highScale)
{
if (IsDrawableForIndex(types, m_limitRect, lowScale++))
if (IsDrawableForIndex(types, m_limitRect, IsClosedLine(), lowScale++))
return true;
}
}
@ -618,7 +621,7 @@ bool FeatureBuilder::PreSerializeAndRemoveUselessNamesForMwm(SupportingData cons
{
if (data.m_ptsMask == 0 && data.m_innerPts.empty())
{
LOG(LWARNING, ("Skip feature with empty geometry", GetMostGenericOsmId()));
LOG(LINFO, ("Skip feature with empty geometry", GetMostGenericOsmId()));
return false;
}
}
@ -626,7 +629,7 @@ bool FeatureBuilder::PreSerializeAndRemoveUselessNamesForMwm(SupportingData cons
{
if (data.m_trgMask == 0 && data.m_innerTrg.empty())
{
LOG(LWARNING, ("Skip feature with empty geometry", GetMostGenericOsmId()));
LOG(LINFO, ("Skip feature with empty geometry", GetMostGenericOsmId()));
return false;
}
}
@ -776,7 +779,7 @@ std::string DebugPrint(FeatureBuilder const & fb)
default: out << "ERROR: unknown geometry type"; break;
}
out << " " << DebugPrint(fb.GetLimitRect())
out << " " << DebugPrint(mercator::ToLatLon(fb.GetLimitRect()))
<< " " << DebugPrint(fb.GetParams())
<< " " << ::DebugPrint(fb.m_osmIds);
return out.str();

View file

@ -64,7 +64,10 @@ public:
Geometry const & GetGeometry() const { return m_polygons; }
PointSeq const & GetOuterGeometry() const { return m_polygons.front(); }
GeomType GetGeomType() const { return m_params.GetGeomType(); }
// Used for good areas and lines.
bool IsGeometryClosed() const;
// Used for lines incl. degenerate ones.
bool IsClosedLine() const;
m2::PointD GetGeometryCenter() const;
m2::PointD GetKeyPoint() const;
size_t GetPointsCount() const;
@ -161,7 +164,6 @@ public:
// Clear name if it's not visible in scale range [minS, maxS].
void RemoveNameIfInvisible(int minS = 0, int maxS = 1000);
void RemoveUselessNames();
int GetMinFeatureDrawScale() const;
bool IsDrawableInRange(int lowScale, int highScale) const;
/// @name Serialization.

View file

@ -13,7 +13,7 @@ CalculateMidPoints::CalculateMidPoints()
{
m_minDrawableScaleFn = [](FeatureBuilder const & fb)
{
return GetMinDrawableScale(fb.GetTypesHolder(), fb.GetLimitRect());
return GetMinDrawableScale(fb.GetTypesHolder(), fb.GetLimitRect(), fb.IsClosedLine());
};
}

View file

@ -53,6 +53,7 @@ bool FeatureMakerSimple::BuildFromWay(OsmElement & p, FeatureBuilderParams const
fb.SetOsmId(base::MakeOsmWay(p.m_id));
fb.SetParams(params);
// TODO : convert degenerate 3-points closed ways into 2-points lines?
if (fb.IsGeometryClosed())
fb.SetArea();
else

View file

@ -14,6 +14,7 @@
#include "indexer/feature_algo.hpp"
#include "indexer/feature_impl.hpp"
#include "indexer/feature_processor.hpp"
#include "indexer/ftypes_matcher.hpp"
#include "indexer/scales.hpp"
#include "indexer/scales_patch.hpp"
@ -151,21 +152,30 @@ public:
GeometryHolder holder([this](int i) -> FileWriter & { return *m_geoFile[i]; },
[this](int i) -> FileWriter & { return *m_trgFile[i]; }, fb, m_header);
bool const isLine = fb.IsLine();
bool const isArea = fb.IsArea();
CHECK(!isLine || !isArea, ("A feature can't have both points and triangles geometries:", fb.GetMostGenericOsmId()));
bool isValidLine = fb.IsLine();
bool isValidArea = fb.IsArea();
CHECK(!isValidLine || !isValidArea, ("A feature can't have both points and triangles geometries:", fb.GetMostGenericOsmId()));
bool isClosedLine = fb.IsClosedLine();
bool const isRoad = isValidLine && routing::IsRoad(fb.GetTypes());
bool const isCoast = fb.IsCoastCell();
m2::RectD const rect = fb.GetLimitRect();
int const scalesStart = static_cast<int>(m_header.GetScalesCount()) - 1;
for (int i = scalesStart; i >= 0; --i)
{
int const level = m_header.GetScale(i);
// TODO : re-checks geom limit rect size via IsDrawableForIndexGeometryOnly() which was checked already in CalculateMidPoints.
if (fb.IsDrawableInRange(scales::PatchMinDrawableScale(i > 0 ? m_header.GetScale(i - 1) + 1 : 0),
scales::PatchMaxDrawableScale(level)))
{
bool const isCoast = fb.IsCoastCell();
m2::RectD const rect = fb.GetLimitRect();
// Simplify and serialize geometry.
// Only valid geometry should come from the OSM data (feature_maker.cpp
// filters it to min 2 points for ways and min 3 points for closed ways)
// and *.isolines files (the topography_generator filters it
// to 2 points for lines and min 4 points for closed lines).
// But the geometry can degenerate during simplification so we re-filter it here.
// The same line simplification algo is used both for lines
// and areas. For the latter polygon's outline is simplified and
// tesselated afterwards. But e.g. simplifying a circle from 50
@ -174,19 +184,49 @@ public:
// is much higher than between trg1 and trg2.
Points points;
// Do not change linear geometry for the upper scale.
if (isLine && i == scalesStart && IsCountry() && routing::IsRoad(fb.GetTypes()))
points = holder.GetSourcePoints();
else if (isLine || holder.NeedProcessTriangles())
// Do not change the most detailed linear geometry for roads (for precise distances calculation)
// and for isolines (they had been simplified already while being generated).
bool isKeepSourceLine = false;
if (isValidLine && i == scalesStart && IsCountry() &&
(isRoad || ftypes::IsIsolineChecker::Instance()(fb.GetTypes())))
{
points = fb.GetOuterGeometry();
// As an exception we keep degenerate 3 points closed geom3 roads
// (to avoid breaking routing) in a form of a regular 2-points line.
if (isRoad && isClosedLine && points.size() == 3)
{
LOG(LWARNING, ("Convert a 3-points degenerate closed road line into a 2-points line", DebugPrint(fb)));
points.pop_back();
isClosedLine = false;
}
// TODO : keep filtering until *.isolines files are regenerated to be pre-filtered; remove the if afterwards.
if (!ftypes::IsIsolineChecker::Instance()(fb.GetTypes()))
isKeepSourceLine = true;
}
else if (isValidLine || holder.NeedProcessTriangles())
SimplifyPoints(level, isCoast, rect, holder.GetSourcePoints(), points);
if (isLine)
holder.AddPoints(points, i);
if (isArea && holder.NeedProcessTriangles())
if (isValidLine)
{
// simplify and serialize triangles
bool const good = isCoast || IsGoodArea(points, level);
// Discard closed lines which are degenerate (<=3 points, first == last)
// or too small for the current geom level (except roads).
if (isClosedLine && !isKeepSourceLine &&
(points.size() <= 3 || (!isRoad && !scales::IsGoodOutlineForLevel(level, points))))
{
LOG(LDEBUG, ("Closed line: too small or degenerate, points count", points.size(), "at scale", i, DebugPrint(fb)));
isValidLine = false;
}
else
{
CHECK(!isClosedLine && points.size() > 1 || points.size() > 3, (DebugPrint(fb)));
holder.AddPoints(points, i);
}
}
if (isValidArea && holder.NeedProcessTriangles())
{
// Simplify and serialize triangles.
bool const good = isCoast || scales::IsGoodOutlineForLevel(level, points);
// At this point we don't need last point equal to first.
CHECK_GREATER(points.size(), 0, ());
@ -202,6 +242,8 @@ public:
simplified.push_back({});
simplified.back().swap(points);
}
else
LOG(LDEBUG, ("Area: too small or degenerate 1st polygon of", polys.size(), ", points count", points.size(), "at scale", i, DebugPrint(fb)));
auto iH = polys.begin();
for (++iH; iH != polys.end(); ++iH)
@ -212,20 +254,25 @@ public:
// Increment level check for coastline polygons for the first scale level.
// This is used for better coastlines quality.
if (IsGoodArea(simplified.back(), (isCoast && i == 0) ? level + 1 : level))
if (scales::IsGoodOutlineForLevel((isCoast && i == 0) ? level + 1 : level, simplified.back()))
{
// At this point we don't need last point equal to first.
CHECK_GREATER(simplified.back().size(), 0, ());
simplified.back().pop_back();
}
else
{
// Remove small polygon.
// Remove small or degenerate polygon.
simplified.pop_back();
LOG(LDEBUG, ("Area: too small or degenerate 2nd+ polygon of", polys.size(), ", points count", simplified.back().size(), "at scale", i, DebugPrint(fb)));
}
}
if (!simplified.empty())
if (simplified.empty())
{
// Stop processing this area.
isValidArea = false;
}
else
holder.AddTriangles(simplified, i);
}
}
@ -266,19 +313,6 @@ private:
using TmpFiles = std::vector<std::unique_ptr<TmpFile>>;
static bool IsGoodArea(Points const & poly, int level)
{
// Area has the same first and last points. That's why minimal number of points for
// area is 4.
if (poly.size() < 4)
return false;
m2::RectD r;
CalcRect(poly, r);
return scales::IsGoodForLevel(level, r);
}
bool IsCountry() const { return m_header.GetType() == feature::DataHeader::MapType::Country; }
static void SimplifyPoints(int level, bool isCoast, m2::RectD const & rect, Points const & in, Points & out)
@ -320,6 +354,7 @@ bool GenerateFinalFeatures(feature::GenerateInfo const & info, std::string const
std::string const srcFilePath = info.GetTmpFileName(name);
std::string const dataFilePath = info.GetTargetFileName(name);
LOG(LINFO, ("Calculating middle points"));
// Store cellIds for middle points.
CalculateMidPoints midPoints;
ForEachFeatureRawFormat(srcFilePath, [&midPoints](FeatureBuilder const & fb, uint64_t pos) {
@ -357,6 +392,7 @@ bool GenerateFinalFeatures(feature::GenerateInfo const & info, std::string const
// FeaturesCollector2 will create temporary file `dataFilePath + FEATURES_FILE_TAG`.
// We cannot remove it in ~FeaturesCollector2(), we need to remove it in SCOPE_GUARD.
SCOPE_GUARD(_, [&]() { Platform::RemoveFileIfExists(info.GetTargetFileName(name, FEATURES_FILE_TAG)); });
LOG(LINFO, ("Simplifying and filtering geometry for all geom levels"));
FeaturesCollector2 collector(name, info, header, regionData, info.m_versionDate);
for (auto const & point : midPoints.GetVector())
{
@ -368,10 +404,14 @@ bool GenerateFinalFeatures(feature::GenerateInfo const & info, std::string const
collector(fb);
}
LOG(LINFO, ("Writing features' data to", dataFilePath));
// Update bounds with the limit rect corresponding to region borders.
// Bounds before update can be too big because of big invisible features like a
// relation that contains an entire country's border.
// Borders file may be unavailable when building test mwms.
// TODO: Borders file may be unavailable when building test mwms (why?).
// Probably its the reason resulting mwm sizes are unpredictable
// when building not the whole planet.
m2::RectD bordersRect;
if (borders::GetBordersRect(info.m_targetDir, name, bordersRect))
collector.SetBounds(bordersRect);

View file

@ -37,8 +37,11 @@ bool FilterWorld::IsInternationalAirport(feature::FeatureBuilder const & fb)
// static
bool FilterWorld::IsGoodScale(feature::FeatureBuilder const & fb)
{
// GetMinFeatureDrawScale also checks suitable size for AREA features
return scales::GetUpperWorldScale() >= fb.GetMinFeatureDrawScale();
// GetMinDrawableScale also checks suitable size for AREA features.
int const minScale = GetMinDrawableScale(fb.GetTypesHolder(), fb.GetLimitRect(), fb.IsClosedLine());
// Some features become invisible after merge processing, so -1 is possible.
return scales::GetUpperWorldScale() >= minScale && minScale != -1;
}
// static

View file

@ -73,15 +73,7 @@ public:
{
// Inner geometry: store inside feature's header and keep
// a simplification mask for scale-specific visibility of each point.
if (m_buffer.m_innerPts.empty())
{
// If geometry is added for the most detailed scale 3 only then
// the mask is never updated and left == 0, which works OK
// as the feature is not supposed to be using lower geometry scales anyway.
m_buffer.m_innerPts = points;
}
else
FillInnerPointsMask(points, scaleIndex);
FillInnerPoints(points, scaleIndex);
m_current = points;
}
else
@ -202,6 +194,7 @@ private:
auto cp = m_header.GetGeometryCodingParams(i);
// Optimization: Store first point once in header for outer linear features.
// @pastk: how does it help? why not store all points in the outer?
cp.SetBasePoint(points[0]);
// Can optimize here, but ... Make copy of vector.
Points toSave(points.begin() + 1, points.end());
@ -268,14 +261,24 @@ private:
return true;
}
void FillInnerPointsMask(Points const & points, uint32_t scaleIndex)
void FillInnerPoints(Points const & points, uint32_t scaleIndex)
{
auto const & src = m_buffer.m_innerPts;
CHECK(!src.empty(), ());
if (src.empty())
{
m_buffer.m_innerPts = points;
// Init the simplification mask to the scaleIndex.
// First and last points are present always, hence not stored in the mask.
for (size_t i = 1; i < points.size() - 1; ++i)
m_buffer.m_ptsSimpMask |= (scaleIndex << (2 * (i - 1)));
return;
}
CHECK(feature::ArePointsEqual(src.front(), points.front()), ());
CHECK(feature::ArePointsEqual(src.back(), points.back()), ());
uint32_t const mask = 0x3;
size_t j = 1;
for (size_t i = 1; i < points.size() - 1; ++i)
{
@ -283,8 +286,7 @@ private:
{
if (feature::ArePointsEqual(src[j], points[i]))
{
// set corresponding 2 bits for source point [j] to scaleIndex
uint32_t mask = 0x3;
// Update corresponding 2 bits for the source point [j] to the scaleIndex.
m_buffer.m_ptsSimpMask &= ~(mask << (2 * (j - 1)));
m_buffer.m_ptsSimpMask |= (scaleIndex << (2 * (j - 1)));
break;

View file

@ -62,7 +62,8 @@ class WorldMapGenerator
/// This function is called after merging linear features.
void operator()(feature::FeatureBuilder const & fb) override
{
// do additional check for suitable size of feature
// Do additional check for suitable size of feature, because
// same check in NeedPushToWorld() applies to areas and closed lines only.
if (NeedPushToWorld(fb) &&
scales::IsGoodForLevel(scales::GetUpperWorldScale(), fb.GetLimitRect()))
PushSure(fb);

View file

@ -22,7 +22,10 @@ PointD CalculatePolyLineCenter::GetResult() const
if (e == m_poly.begin())
{
/// @todo It's very strange, but we have linear objects with zero length.
// It's degenerate closed lines (size == 2, first == last), now we filter them
// in feature_sorter.cpp, but they could be present in older mwms still.
LOG(LWARNING, ("Zero length linear object"));
CHECK_GREATER(m_poly.size(), 0, ());
return e->m_p;
}

View file

@ -335,6 +335,13 @@ int8_t FeatureType::GetLayer()
return m_params.layer;
}
// TODO: there is a room to store more information in Header2 (geometry header),
// but it needs a mwm version change.
// 1st bit - inner / outer flag
// 4 more bits - inner points/triangles count or outer geom offsets mask
// (but actually its enough to store number of the first existing geom level only - 2 bits)
// 3-5 more bits are spare
// One of them could be used for a closed line flag to avoid storing identical first + last points.
void FeatureType::ParseHeader2()
{
if (m_parsed.m_header2)
@ -351,6 +358,7 @@ void FeatureType::ParseHeader2()
{
elemsCount = bitSource.Read(4);
// For outer geometry read the geom scales (offsets) mask.
// For inner geometry remaining 4 bits are not used.
if (elemsCount == 0)
geomScalesMask = bitSource.Read(4);
else
@ -475,6 +483,13 @@ void FeatureType::ParseGeometry(int scale)
}
points.emplace_back(m_points.back());
// Treat closed degenerate zero-length lines (2 points, first == last) as an empty geometry,
// because the first and the last points are not excludable via the simplification mask.
// Don't discard 3-points degenerate lines, because there could be unfiltered closed roads
// like this in the older mwms (new mwms will have them converted into regular 2-points lines).
if (points.size() == 2 && points.front() == points.back())
points.clear();
m_points.swap(points);
}
@ -736,6 +751,16 @@ bool FeatureType::IsEmptyGeometry(int scale)
}
}
bool FeatureType::IsClosedLine() const
{
if (GetGeomType() == GeomType::Line)
{
ASSERT(m_parsed.m_points, (m_id));
return m_points.size() > 1 && m_points.front() == m_points.back();
}
return false;
}
size_t FeatureType::GetPointsCount() const
{
ASSERT(m_parsed.m_points, ());

View file

@ -96,6 +96,7 @@ public:
m2::RectD GetLimitRect(int scale);
m2::RectD const & GetLimitRectChecked() const;
bool IsClosedLine() const;
bool IsEmptyGeometry(int scale);

View file

@ -10,6 +10,8 @@
#include "base/assert.hpp"
#include "base/checked_cast.hpp"
#include "routing/routing_helpers.hpp"
#include <algorithm>
#include <array>
@ -223,16 +225,25 @@ bool CanGenerateLike(vector<uint32_t> const & types, GeomType geomType)
namespace
{
bool IsDrawableForIndexGeometryOnly(TypesHolder const & types, m2::RectD const & limitRect, int level)
{
Classificator const & c = classif();
static uint32_t const buildingPartType = c.GetTypeByPath({"building:part"});
// Used by the generator when
// - building scale / visibility index (via FeatureShouldBeIndexed())
// - simplifying geometry (via CalculateMidPoints and IsDrawableInRange())
// - including features into the World map (via FilterWorld::IsAccepted() and NeedPushToWorld())
bool IsDrawableForIndexGeometryOnly(TypesHolder const & types, m2::RectD const & limitRect, bool isClosedLine, int level)
{
// Exclude too small closed lines (except roads).
if (isClosedLine && !routing::IsRoad(types) && !scales::IsGoodForLevel(level, limitRect))
return false;
// Exclude too small area features unless it's a part of a coast or a building.
if (types.GetGeomType() == GeomType::Area && !types.Has(c.GetCoastType()) &&
!types.Has(buildingPartType) && !scales::IsGoodForLevel(level, limitRect))
return false;
if (types.GetGeomType() == GeomType::Area)
{
Classificator const & c = classif();
static uint32_t const buildingPartType = c.GetTypeByPath({"building:part"});
if (!types.Has(c.GetCoastType()) && !types.Has(buildingPartType) && !scales::IsGoodForLevel(level, limitRect))
return false;
}
return true;
}
@ -259,15 +270,15 @@ bool IsDrawableForIndex(FeatureType & ft, int level)
IsDrawableForIndexClassifOnly(TypesHolder(ft), level);
}
bool IsDrawableForIndex(TypesHolder const & types, m2::RectD const & limitRect, int level)
bool IsDrawableForIndex(TypesHolder const & types, m2::RectD const & limitRect, bool isClosedLine, int level)
{
return IsDrawableForIndexGeometryOnly(types, limitRect, level) &&
return IsDrawableForIndexGeometryOnly(types, limitRect, isClosedLine, level) &&
IsDrawableForIndexClassifOnly(types, level);
}
bool IsDrawableForIndexGeometryOnly(FeatureType & ft, int level)
{
return IsDrawableForIndexGeometryOnly(TypesHolder(ft), ft.GetLimitRectChecked(), level);
return IsDrawableForIndexGeometryOnly(TypesHolder(ft), ft.GetLimitRectChecked(), ft.IsClosedLine(), level);
}
bool IsUsefulType(uint32_t t, GeomType geomType, bool emptyName)
@ -303,16 +314,16 @@ bool RemoveUselessTypes(vector<uint32_t> & types, GeomType geomType, bool emptyN
int GetMinDrawableScale(FeatureType & ft)
{
return GetMinDrawableScale(TypesHolder(ft), ft.GetLimitRectChecked());
return GetMinDrawableScale(TypesHolder(ft), ft.GetLimitRectChecked(), ft.IsClosedLine());
}
int GetMinDrawableScale(TypesHolder const & types, m2::RectD const & limitRect)
int GetMinDrawableScale(TypesHolder const & types, m2::RectD const & limitRect, bool isClosedLine)
{
int const upBound = scales::GetUpperStyleScale();
for (int level = 0; level <= upBound; ++level)
{
if (IsDrawableForIndex(types, limitRect, level))
if (IsDrawableForIndex(types, limitRect, isClosedLine, level))
return level;
}

View file

@ -16,7 +16,7 @@ namespace feature
bool IsCategoryNondrawableType(uint32_t type);
bool IsUsefulType(uint32_t type);
bool IsDrawableForIndex(FeatureType & ft, int level);
bool IsDrawableForIndex(TypesHolder const & types, m2::RectD const & limitRect, int level);
bool IsDrawableForIndex(TypesHolder const & types, m2::RectD const & limitRect, bool isClosedLine, int level);
// The separation into ClassifOnly and GeometryOnly versions is needed to speed up
// the geometrical index (see indexer/scale_index_builder.hpp).
@ -38,7 +38,7 @@ namespace feature
/// @}
int GetMinDrawableScale(FeatureType & ft);
int GetMinDrawableScale(TypesHolder const & types, m2::RectD const & limitRect);
int GetMinDrawableScale(TypesHolder const & types, m2::RectD const & limitRect, bool isClosedLine);
//int GetMinDrawableScaleGeometryOnly(TypesHolder const & types, m2::RectD limitRect);
int GetMinDrawableScaleClassifOnly(TypesHolder const & types);

View file

@ -102,7 +102,7 @@ private:
if (ft.IsEmptyGeometry(scale))
return false;
// This function assumes that geometry rect for the needed scale is already initialized.
// Checks feature's geometry is suitable for the needed scale; assumes the geometry rect is already initialized.
return feature::IsDrawableForIndexGeometryOnly(ft, scale);
}

View file

@ -1,6 +1,7 @@
#include "indexer/scales.hpp"
#include "geometry/mercator.hpp"
#include "indexer/feature_algo.hpp"
#include "geometry/mercator.hpp"
#include "base/math.hpp"
#include <algorithm>
@ -55,10 +56,12 @@ namespace scales
double GetEpsilonForSimplify(int level)
{
// Keep better geometries on highest zoom to allow scaling them deeper
// Keep better geometries on highest zoom to allow scaling them deeper.
// Effectively it leads to x26 precision difference from geom scale 2 to 3.
// Keep crude geometries for all other zooms,
// x4 precision difference from geom scale 0 to 1 and 1 to 2.
if (level == GetUpperScale())
return GetEpsilonImpl(level, 0.4);
// Keep crude geometries for all other zooms
else
return GetEpsilonImpl(level, 1.3);
}
@ -69,4 +72,17 @@ namespace scales
// assume that feature is always visible in upper scale
return (level == GetUpperScale() || std::max(r.SizeX(), r.SizeY()) > GetEpsilonForLevel(level));
}
bool IsGoodOutlineForLevel(int level, Points const & poly)
{
// Areas and closed lines have the same first and last points.
// Hence the minimum number of outline points is 4.
if (poly.size() < 4)
return false;
m2::RectD r;
feature::CalcRect(poly, r);
return IsGoodForLevel(level, r);
}
} // namespace scales

View file

@ -39,4 +39,7 @@ namespace scales
double GetEpsilonForLevel(int level);
double GetEpsilonForSimplify(int level);
bool IsGoodForLevel(int level, m2::RectD const & r);
using Points = std::vector<m2::PointD>;
bool IsGoodOutlineForLevel(int level, Points const & poly);
}

View file

@ -132,7 +132,7 @@ public:
/// @return always the same writable dir for current user with slash at the end
std::string const & WritableDir() const
{
ASSERT(!m_writableDir.empty(), ());
// The generator_tool could be run without -data_path (writableDir) option.
return m_writableDir;
}
/// Set writable dir — use for testing and linux stuff only

View file

@ -593,6 +593,10 @@ void Generator::PackIsolinesForCountry(storage::CountryId const & countryId,
return;
}
// TODO : prepare simplified and filtered isolones for all geom levels here
// (ATM its the most detailed geom3 only) instead of in the generator
// to skip re-doing it for every maps gen. And it'll be needed anyway
// for the longer term vision to supply isolines in separately downloadable files.
LOG(LINFO, ("Begin packing isolines for country", countryId));
m2::RectD countryRect;
@ -622,6 +626,8 @@ void Generator::PackIsolinesForCountry(storage::CountryId const & countryId,
CropContours(countryRect, countryRegions, params.m_maxIsolineLength,
params.m_alitudesStepFactor, isolines);
// Simplification is done already while processing tiles in ProcessTile().
// But now a different country-specific simpificationZoom could be applied.
if (params.m_simplificationZoom > 0)
SimplifyContours(params.m_simplificationZoom, isolines);

View file

@ -5,6 +5,8 @@
#include "geometry/point2d.hpp"
#include "geometry/region2d.hpp"
#include "indexer/scales.hpp"
#include <vector>
#include <unordered_map>
@ -90,7 +92,14 @@ void SimplifyContours(int simplificationZoom, Contours<ValueType> & contours)
std::vector<m2::PointD> contourSimple;
feature::SimplifyPoints(m2::SquaredDistanceFromSegmentToPoint(),
simplificationZoom, contour, contourSimple);
contour = std::move(contourSimple);
// Discard closed lines which are degenerate (<=3 points) or too small for the requested zoom level.
/// @todo it doesn't fix all cases as the simplification algo
/// can produce e.g. a 5 points closed but degenerate line ("flat", not a loop anymore).
if (contourSimple.size() > 1 && contourSimple.front() == contourSimple.back() &&
!scales::IsGoodOutlineForLevel(simplificationZoom, contourSimple))
contour.clear();
else
contour = std::move(contourSimple);
}
}
}