632 lines
28 KiB
C++
632 lines
28 KiB
C++
// Lot STM1 (STM1-SPEC-13 ; 1.3.0 ; STM1-SPEC-16 : 1.3.5). Voir LPEnrichCityBuildingsCommandlet.h.
|
|
|
|
#include "LPEnrichCityBuildingsCommandlet.h"
|
|
|
|
#include "HAL/FileManager.h"
|
|
#include "LPCityOsm.h"
|
|
#include "Misc/FileHelper.h"
|
|
#include "Misc/PackageName.h"
|
|
#include "StreetMap.h"
|
|
#include "UObject/Package.h"
|
|
#include "UObject/SavePackage.h"
|
|
|
|
DEFINE_LOG_CATEGORY_STATIC(LogLPEnrichCityBuildings, Log, All);
|
|
|
|
namespace LPEnrichCityBuildings
|
|
{
|
|
/** Cles OSM retenues, dans l'ordre de priorite (la restauration est classee a part, juste apres shop). */
|
|
const TCHAR* const Keys[] = { TEXT("shop"), TEXT("amenity"), TEXT("craft"), TEXT("healthcare"), TEXT("tourism"), TEXT("office"), TEXT("leisure") };
|
|
|
|
bool IsFood(const FString& Key, const FString& Value)
|
|
{
|
|
static const TSet<FString> Food = { TEXT("restaurant"), TEXT("cafe"), TEXT("bar"), TEXT("pub"), TEXT("fast_food"), TEXT("ice_cream"), TEXT("biergarten") };
|
|
return Key == TEXT("amenity") && Food.Contains(Value);
|
|
}
|
|
|
|
/** Rang de classe : shop 0, restauration 1, autres amenites 2, craft 3, healthcare 4, tourism 5, office 6, leisure 7. */
|
|
int32 ClassRank(const FString& Key, const FString& Value)
|
|
{
|
|
if (Key == TEXT("shop")) return 0;
|
|
if (IsFood(Key, Value)) return 1;
|
|
for (int32 I = 1; I < UE_ARRAY_COUNT(Keys); ++I)
|
|
{
|
|
if (Key == Keys[I]) return I + 1;
|
|
}
|
|
return 99;
|
|
}
|
|
|
|
/** Premiere cle retenue presente dans les balises (building=hotel compte comme tourism=hotel). */
|
|
bool MainKey(const TMap<FString, FString>& Tags, FString& OutKey, FString& OutValue)
|
|
{
|
|
for (const TCHAR* K : Keys)
|
|
{
|
|
if (const FString* V = Tags.Find(K)) { OutKey = K; OutValue = *V; return true; }
|
|
}
|
|
if (Tags.FindRef(TEXT("building")) == TEXT("hotel")) { OutKey = TEXT("tourism"); OutValue = TEXT("hotel"); return true; }
|
|
return false;
|
|
}
|
|
|
|
/** Amenite ecrite : cle d'AmenityImageDict quand une correspondance existe (STM1-SPEC-13 § 9 et § 10), sinon valeur brute. */
|
|
FString MapAmenity(const FString& Key, const FString& Value)
|
|
{
|
|
static const TMap<FString, FString> Shop = {
|
|
{ TEXT("clothes"), TEXT("clothes:business") }, { TEXT("boutique"), TEXT("clothes:business") }, { TEXT("fashion"), TEXT("clothes:business") },
|
|
{ TEXT("fashion_accessories"), TEXT("clothes:business") }, { TEXT("jewelry"), TEXT("jewellery") }, { TEXT("cosmetics"), TEXT("cosmestics") },
|
|
{ TEXT("perfumery"), TEXT("cosmestics") }, { TEXT("chocolate"), TEXT("confectionary") }, { TEXT("confectionery"), TEXT("confectionary") },
|
|
{ TEXT("pastry"), TEXT("bakery") }, { TEXT("wine"), TEXT("alcohol") }, { TEXT("antiques"), TEXT("art") }, { TEXT("art"), TEXT("art") },
|
|
{ TEXT("interior_decoration"), TEXT("furniture") } };
|
|
static const TMap<FString, FString> Amenity = {
|
|
{ TEXT("school"), TEXT("university:school") }, { TEXT("college"), TEXT("university:school") }, { TEXT("arts_centre"), TEXT("art") } };
|
|
static const TMap<FString, FString> Tourism = {
|
|
{ TEXT("hostel"), TEXT("hotel") }, { TEXT("guest_house"), TEXT("hotel") }, { TEXT("apartment"), TEXT("hotel") },
|
|
{ TEXT("museum"), TEXT("art") }, { TEXT("gallery"), TEXT("art") }, { TEXT("artwork"), TEXT("art") } };
|
|
static const TMap<FString, FString> Leisure = { { TEXT("fitness_centre"), TEXT("sports_centre") } };
|
|
const TMap<FString, FString>* Table = Key == TEXT("shop") ? &Shop : Key == TEXT("amenity") ? &Amenity : Key == TEXT("tourism") ? &Tourism
|
|
: Key == TEXT("leisure") ? &Leisure : nullptr;
|
|
if (Key == TEXT("office") || (Key == TEXT("amenity") && (Value == TEXT("diplomatic") || Value == TEXT("government"))))
|
|
{
|
|
return TEXT("work");
|
|
}
|
|
if (Table)
|
|
{
|
|
if (const FString* Found = Table->Find(Value)) return *Found;
|
|
}
|
|
return Value;
|
|
}
|
|
|
|
/** Minuscules, sans accents, ponctuation en espaces, espaces simples (« McDonald's » -> « mcdonald s »). */
|
|
FString Normalize(const FString& In)
|
|
{
|
|
FString Out;
|
|
Out.Reserve(In.Len());
|
|
auto Put = [&Out](const TCHAR* Text)
|
|
{
|
|
if (Text[0] == TEXT(' ') && (Out.IsEmpty() || Out[Out.Len() - 1] == TEXT(' '))) return;
|
|
Out += Text;
|
|
};
|
|
for (const TCHAR Raw : In)
|
|
{
|
|
const TCHAR C = FChar::ToLower(Raw);
|
|
switch (C)
|
|
{
|
|
case 0x00E0: case 0x00E1: case 0x00E2: case 0x00E3: case 0x00E4: Put(TEXT("a")); break;
|
|
case 0x00E7: Put(TEXT("c")); break;
|
|
case 0x00E8: case 0x00E9: case 0x00EA: case 0x00EB: Put(TEXT("e")); break;
|
|
case 0x00EC: case 0x00ED: case 0x00EE: case 0x00EF: Put(TEXT("i")); break;
|
|
case 0x00F1: Put(TEXT("n")); break;
|
|
case 0x00F2: case 0x00F3: case 0x00F4: case 0x00F5: case 0x00F6: Put(TEXT("o")); break;
|
|
case 0x00F9: case 0x00FA: case 0x00FB: case 0x00FC: Put(TEXT("u")); break;
|
|
case 0x00FF: Put(TEXT("y")); break;
|
|
case 0x0153: Put(TEXT("oe")); break;
|
|
case 0x00E6: Put(TEXT("ae")); break;
|
|
default:
|
|
if (C < 128 && FChar::IsAlnum(C)) { const TCHAR Text[2] = { C, 0 }; Put(Text); }
|
|
else Put(TEXT(" "));
|
|
}
|
|
}
|
|
return Out.TrimEnd();
|
|
}
|
|
|
|
/** Liste des marques (-Generic) : categorie et marque normalisee. */
|
|
struct FBrand
|
|
{
|
|
FString Category;
|
|
FString Brand;
|
|
};
|
|
|
|
/** Categories admises selon le type du point (une marque de vetements ne renomme pas une banque). */
|
|
bool CategoryApplies(const FString& Category, const FString& Key, const FString& Value)
|
|
{
|
|
if (Category == TEXT("banque")) return Key == TEXT("amenity") && (Value == TEXT("bank") || Value == TEXT("atm") || Value == TEXT("bureau_de_change"));
|
|
if (Category == TEXT("velo")) return Key == TEXT("amenity") && Value == TEXT("bicycle_rental");
|
|
if (Category == TEXT("hotel") || Category == TEXT("palace"))
|
|
{
|
|
return Key == TEXT("tourism") && (Value == TEXT("hotel") || Value == TEXT("apartment") || Value == TEXT("hostel") || Value == TEXT("guest_house") || Value == TEXT("motel"));
|
|
}
|
|
if (Category == TEXT("operateur")) return Key == TEXT("shop");
|
|
// annexe : restaurants, bars et boutiques des palaces, nommes d'apres le palace.
|
|
return Key == TEXT("shop") || IsFood(Key, Value);
|
|
}
|
|
|
|
/** Categorie de marque d'un point ou d'un batiment, vide si aucune (regles en tete du fichier de listes). */
|
|
FString MatchBrand(const TArray<FBrand>& Brands, const TMap<FString, FString>& Tags, const FString& Name, const FString& Key, const FString& Value)
|
|
{
|
|
const FString BrandTag = Normalize(Tags.FindRef(TEXT("brand")));
|
|
const FString NameN = Normalize(Name);
|
|
const FString Network = Normalize(Tags.FindRef(TEXT("network"))) + TEXT(" | ") + Normalize(Tags.FindRef(TEXT("operator")));
|
|
const FString Padded = TEXT(" ") + NameN + TEXT(" ");
|
|
for (const FBrand& B : Brands)
|
|
{
|
|
if (!CategoryApplies(B.Category, Key, Value)) continue;
|
|
const FString Prefix = B.Brand + TEXT(" ");
|
|
if (!BrandTag.IsEmpty() && (BrandTag == B.Brand || BrandTag.StartsWith(Prefix))) return B.Category;
|
|
if (NameN == B.Brand || (B.Brand.Len() > 5 && NameN.StartsWith(Prefix))) return B.Category;
|
|
if ((B.Category == TEXT("palace") || B.Category == TEXT("annexe")) && Padded.Contains(TEXT(" ") + B.Brand + TEXT(" "))) return B.Category;
|
|
if (B.Category == TEXT("velo") && Network.Contains(B.Brand)) return B.Category;
|
|
}
|
|
return FString();
|
|
}
|
|
|
|
/** Nom generique en francais (STM1-SPEC-16 § 6.2) ; « nom du type » quand le type n'a pas d'entree. */
|
|
FString GenericName(const FString& Category, const FString& Key, const FString& Value)
|
|
{
|
|
if (Category == TEXT("velo")) return TEXT("Station V\u00E9lib'");
|
|
if (Category == TEXT("banque")) return TEXT("Banque");
|
|
if (Category == TEXT("operateur")) return TEXT("T\u00E9l\u00E9phonie");
|
|
if (Category == TEXT("hotel") || Category == TEXT("palace")) return Value == TEXT("apartment") ? TEXT("R\u00E9sidence h\u00F4teli\u00E8re") : TEXT("H\u00F4tel");
|
|
if (Category == TEXT("patisserie"))
|
|
{
|
|
if (Value == TEXT("pastry") || Value == TEXT("confectionery") || Value == TEXT("chocolate") || Value == TEXT("bakery")) return TEXT("P\u00E2tisserie");
|
|
if (Value == TEXT("tea") || Value == TEXT("coffee") || Value == TEXT("cafe")) return TEXT("Salon de th\u00E9");
|
|
if (Value == TEXT("restaurant")) return TEXT("Restaurant");
|
|
return TEXT("\u00C9picerie fine");
|
|
}
|
|
static const TMap<FString, FString> Amenity = {
|
|
{ TEXT("restaurant"), TEXT("Restaurant") }, { TEXT("fast_food"), TEXT("Restauration rapide") }, { TEXT("cafe"), TEXT("Caf\u00E9") },
|
|
{ TEXT("ice_cream"), TEXT("Glacier") }, { TEXT("bar"), TEXT("Bar") }, { TEXT("pub"), TEXT("Bar") }, { TEXT("biergarten"), TEXT("Bar") },
|
|
{ TEXT("bank"), TEXT("Banque") }, { TEXT("atm"), TEXT("Banque") }, { TEXT("pharmacy"), TEXT("Pharmacie") } };
|
|
static const TMap<FString, FString> Shop = {
|
|
{ TEXT("bakery"), TEXT("Boulangerie") }, { TEXT("pastry"), TEXT("P\u00E2tisserie") }, { TEXT("confectionery"), TEXT("P\u00E2tisserie") },
|
|
{ TEXT("chocolate"), TEXT("P\u00E2tisserie") }, { TEXT("tea"), TEXT("Salon de th\u00E9") }, { TEXT("coffee"), TEXT("Salon de th\u00E9") },
|
|
{ TEXT("supermarket"), TEXT("Supermarch\u00E9") }, { TEXT("convenience"), TEXT("\u00C9picerie") }, { TEXT("frozen_food"), TEXT("Surgel\u00E9s") },
|
|
{ TEXT("organic"), TEXT("\u00C9picerie bio") }, { TEXT("health_food"), TEXT("\u00C9picerie bio") }, { TEXT("deli"), TEXT("\u00C9picerie fine") },
|
|
{ TEXT("alcohol"), TEXT("Caviste") }, { TEXT("wine"), TEXT("Caviste") }, { TEXT("department_store"), TEXT("Grand magasin") },
|
|
{ TEXT("cosmetics"), TEXT("Parfumerie") }, { TEXT("perfumery"), TEXT("Parfumerie") }, { TEXT("beauty"), TEXT("Parfumerie") },
|
|
{ TEXT("optician"), TEXT("Opticien") }, { TEXT("mobile_phone"), TEXT("T\u00E9l\u00E9phonie") }, { TEXT("telecommunication"), TEXT("T\u00E9l\u00E9phonie") },
|
|
{ TEXT("electronics"), TEXT("\u00C9lectronique") }, { TEXT("computer"), TEXT("\u00C9lectronique") }, { TEXT("books"), TEXT("Librairie") },
|
|
{ TEXT("clothes"), TEXT("Boutique de v\u00EAtements") }, { TEXT("boutique"), TEXT("Boutique de v\u00EAtements") },
|
|
{ TEXT("fashion"), TEXT("Boutique de v\u00EAtements") }, { TEXT("fashion_accessories"), TEXT("Boutique de v\u00EAtements") },
|
|
{ TEXT("shoes"), TEXT("Chaussures") }, { TEXT("jewelry"), TEXT("Bijouterie") }, { TEXT("watches"), TEXT("Bijouterie") },
|
|
{ TEXT("bag"), TEXT("Maroquinerie") }, { TEXT("leather"), TEXT("Maroquinerie") }, { TEXT("sports"), TEXT("Articles de sport") },
|
|
{ TEXT("outdoor"), TEXT("Articles de sport") }, { TEXT("variety_store"), TEXT("Bazar") }, { TEXT("gift"), TEXT("Bazar") },
|
|
{ TEXT("ice_cream"), TEXT("Glacier") } };
|
|
if (Key == TEXT("shop"))
|
|
{
|
|
const FString* Found = Shop.Find(Value);
|
|
return Found ? *Found : FString(TEXT("Boutique"));
|
|
}
|
|
if (Key == TEXT("amenity"))
|
|
{
|
|
if (const FString* Found = Amenity.Find(Value)) return *Found;
|
|
}
|
|
FString Type = Value.Replace(TEXT("_"), TEXT(" "));
|
|
if (!Type.IsEmpty()) Type[0] = FChar::ToUpper(Type[0]);
|
|
return Type;
|
|
}
|
|
|
|
/** Ecole ou creche d'apres les balises (STM1-SPEC-16 ; universites et grandes ecoles gardees). */
|
|
bool IsSchoolTags(const TMap<FString, FString>& Tags)
|
|
{
|
|
const FString Amenity = Tags.FindRef(TEXT("amenity"));
|
|
if (Amenity == TEXT("school") || Amenity == TEXT("kindergarten") || Amenity == TEXT("childcare")) return true;
|
|
if (Amenity == TEXT("university") || Amenity == TEXT("college") || Amenity == TEXT("research_institute")) return false;
|
|
if (Tags.Contains(TEXT("school:FR")) || Tags.Contains(TEXT("ref:UAI"))) return true;
|
|
if (const FString* Isced = Tags.Find(TEXT("isced:level")))
|
|
{
|
|
for (const TCHAR C : *Isced)
|
|
{
|
|
if (C >= TEXT('0') && C <= TEXT('3')) return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/** Ecole ou creche d'apres le nom (« ecole maternelle », « lycee », « college » sauf grands etablissements). */
|
|
bool IsSchoolName(const FString& Name)
|
|
{
|
|
static const TCHAR* const Prefixes[] = { TEXT("ecole maternelle"), TEXT("ecole elementaire"), TEXT("ecole primaire"), TEXT("ecole polyvalente"),
|
|
TEXT("groupe scolaire"), TEXT("cite scolaire"), TEXT("lycee"), TEXT("college"), TEXT("creche"), TEXT("halte garderie"), TEXT("jardin d enfants"),
|
|
TEXT("multi accueil") };
|
|
static const TCHAR* const Kept[] = { TEXT("college de france"), TEXT("college des bernardins"), TEXT("college des ecossais"), TEXT("college des ingenieurs") };
|
|
const FString N = Normalize(Name);
|
|
for (const TCHAR* K : Kept)
|
|
{
|
|
if (N.StartsWith(K)) return false;
|
|
}
|
|
for (const TCHAR* P : Prefixes)
|
|
{
|
|
if (N == P || N.StartsWith(FString(P) + TEXT(" "))) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool IsSchoolAmenity(const FString& Amenity)
|
|
{
|
|
return Amenity == TEXT("school") || Amenity == TEXT("kindergarten") || Amenity == TEXT("childcare") || Amenity == TEXT("university:school");
|
|
}
|
|
|
|
double SegmentDistance(const FVector2D& P, const FVector2D& A, const FVector2D& B)
|
|
{
|
|
const FVector2D AB = B - A;
|
|
const double T = AB.SizeSquared() > 0.0 ? FMath::Clamp(FVector2D::DotProduct(P - A, AB) / AB.SizeSquared(), 0.0, 1.0) : 0.0;
|
|
return FVector2D::Distance(P, A + AB * T);
|
|
}
|
|
|
|
/** Grille de 50 m : indices d'objets par case (boite englobante). */
|
|
struct FGrid
|
|
{
|
|
static constexpr double CellCm = 5000.0;
|
|
TMultiMap<FIntPoint, int32> Cells;
|
|
static FIntPoint Key(const FVector2D& P) { return FIntPoint(FMath::FloorToInt32(P.X / CellCm), FMath::FloorToInt32(P.Y / CellCm)); }
|
|
void Add(const FBox2D& Box, int32 Index)
|
|
{
|
|
const FIntPoint A = Key(Box.Min), B = Key(Box.Max);
|
|
for (int32 X = A.X; X <= B.X; ++X)
|
|
for (int32 Y = A.Y; Y <= B.Y; ++Y)
|
|
Cells.Add(FIntPoint(X, Y), Index);
|
|
}
|
|
void Near(const FVector2D& P, int32 Radius, TSet<int32>& Out) const
|
|
{
|
|
const FIntPoint K = Key(P);
|
|
for (int32 DX = -Radius; DX <= Radius; ++DX)
|
|
for (int32 DY = -Radius; DY <= Radius; ++DY)
|
|
for (auto It = Cells.CreateConstKeyIterator(K + FIntPoint(DX, DY)); It; ++It)
|
|
Out.Add(It.Value());
|
|
}
|
|
};
|
|
|
|
struct FCandidate
|
|
{
|
|
int32 Rank = 99;
|
|
double StreetCm = 0.0;
|
|
FString Name;
|
|
FString Key;
|
|
FString Value;
|
|
FString Category; // marque de la liste -Generic : passe apres les noms propres, ecrite sous un nom generique
|
|
};
|
|
}
|
|
|
|
ULPEnrichCityBuildingsCommandlet::ULPEnrichCityBuildingsCommandlet()
|
|
{
|
|
IsClient = false;
|
|
IsServer = false;
|
|
IsEditor = true;
|
|
LogToConsole = true;
|
|
}
|
|
|
|
int32 ULPEnrichCityBuildingsCommandlet::Main(const FString& Params)
|
|
{
|
|
using namespace LPEnrichCityBuildings;
|
|
|
|
TArray<FString> Tokens, Switches;
|
|
TMap<FString, FString> ParamsMap;
|
|
ParseCommandLine(*Params, Tokens, Switches, ParamsMap);
|
|
const FString City = ParamsMap.FindRef(TEXT("City"));
|
|
const FString MapRoot = ParamsMap.FindRef(TEXT("MapRoot"));
|
|
const FString OriginText = ParamsMap.FindRef(TEXT("Origin"));
|
|
const FString OsmPath = ParamsMap.FindRef(TEXT("Osm"));
|
|
const FString KeysPath = ParamsMap.FindRef(TEXT("Keys"));
|
|
const FString GenericPath = ParamsMap.FindRef(TEXT("Generic"));
|
|
const FString ReportPath = ParamsMap.FindRef(TEXT("Report"));
|
|
const double MaxDistCm = (ParamsMap.Contains(TEXT("MaxDistM")) ? FCString::Atod(*ParamsMap[TEXT("MaxDistM")]) : 8.0) * 100.0;
|
|
const bool bDryRun = Switches.Contains(TEXT("DryRun"));
|
|
const bool bOverwrite = Switches.Contains(TEXT("Overwrite"));
|
|
const bool bRemoveSchools = Switches.Contains(TEXT("RemoveSchools"));
|
|
TArray<FString> PoiPaths;
|
|
ParamsMap.FindRef(TEXT("Pois")).ParseIntoArray(PoiPaths, TEXT(";"));
|
|
|
|
TArray<FString> Report;
|
|
auto Line = [&Report](const FString& Text)
|
|
{
|
|
UE_LOG(LogLPEnrichCityBuildings, Display, TEXT("%s"), *Text);
|
|
Report.Add(Text);
|
|
};
|
|
auto Finish = [&](int32 Code)
|
|
{
|
|
Line(FString::Printf(TEXT("CODE %d"), Code));
|
|
if (!ReportPath.IsEmpty())
|
|
{
|
|
FFileHelper::SaveStringArrayToFile(Report, *ReportPath, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM);
|
|
}
|
|
return Code;
|
|
};
|
|
|
|
TArray<FString> OriginParts;
|
|
OriginText.ParseIntoArray(OriginParts, TEXT(","));
|
|
TArray<FString> RootParts;
|
|
MapRoot.ParseIntoArray(RootParts, TEXT("/"));
|
|
if (City.IsEmpty() || PoiPaths.IsEmpty() || OriginParts.Num() != 2 || bDryRun == bOverwrite)
|
|
{
|
|
Line(TEXT("usage : -City=<Ville> -MapRoot=</lpm_city_<v>/StreetMap> -Origin=<lat>,<lon> -Pois=<f1>;<f2> [-Osm=<f.osm.xml>] [-Keys=<cles.txt>] [-Generic=<marques.txt>] [-RemoveSchools] [-MaxDistM=8] (-DryRun | -Overwrite) [-Report=<f.md>]"));
|
|
return Finish(2);
|
|
}
|
|
Line(FString::Printf(TEXT("# LPEnrichCityBuildings %s (%s) ; racine %s ; rattachement a %.0f m"), *City, bDryRun ? TEXT("DryRun") : TEXT("ecriture en place"), *MapRoot, MaxDistCm / 100.0));
|
|
if (!(RootParts.Num() == 2 && RootParts[0].StartsWith(TEXT("lpm_city_")) && RootParts[1] == TEXT("StreetMap")))
|
|
{
|
|
Line(TEXT("ECHEC : -MapRoot doit etre /lpm_city_<ville>/StreetMap (jamais la carte du jeu de base) ; rien ecrit"));
|
|
return Finish(1);
|
|
}
|
|
const double OriginLat = FCString::Atod(*OriginParts[0]), OriginLon = FCString::Atod(*OriginParts[1]);
|
|
|
|
// Liste des marques a nom generique.
|
|
TArray<FBrand> Brands;
|
|
if (!GenericPath.IsEmpty())
|
|
{
|
|
TArray<FString> BrandLines;
|
|
if (!FFileHelper::LoadFileToStringArray(BrandLines, *GenericPath))
|
|
{
|
|
Line(FString::Printf(TEXT("ECHEC : lecture de %s ; rien ecrit"), *GenericPath));
|
|
return Finish(1);
|
|
}
|
|
TMap<FString, int32> PerCategory;
|
|
for (const FString& Raw : BrandLines)
|
|
{
|
|
const FString Text = Raw.TrimStartAndEnd();
|
|
FString Category, Brand;
|
|
if (Text.IsEmpty() || Text.StartsWith(TEXT("#")) || !Text.Split(TEXT(":"), &Category, &Brand)) continue;
|
|
Brand = Normalize(Brand);
|
|
Category = Category.TrimStartAndEnd();
|
|
if (Brand.IsEmpty()) continue;
|
|
Brands.Add({ Category, Brand });
|
|
++PerCategory.FindOrAdd(Category);
|
|
}
|
|
TArray<FString> Counts;
|
|
for (const TPair<FString, int32>& P : PerCategory) Counts.Add(FString::Printf(TEXT("%s %d"), *P.Key, P.Value));
|
|
Line(FString::Printf(TEXT("liste des marques %s : %d entrees (%s)"), *GenericPath, Brands.Num(), *FString::Join(Counts, TEXT(", "))));
|
|
}
|
|
|
|
UStreetMap* StreetMap = LoadObject<UStreetMap>(nullptr, *FString::Printf(TEXT("%s/%s.%s"), *MapRoot, *City, *City));
|
|
if (!StreetMap)
|
|
{
|
|
Line(TEXT("ECHEC : UStreetMap introuvable"));
|
|
return Finish(1);
|
|
}
|
|
TArray<FStreetMapBuilding>& Buildings = StreetMap->GetBuildings();
|
|
int32 NamedBefore = 0;
|
|
for (const FStreetMapBuilding& Building : Buildings) NamedBefore += Building.BuildingName.IsEmpty() ? 0 : 1;
|
|
|
|
// Batiments filtres (balises OSM de leur voie) ; ecoles (-RemoveSchools) ; noms de marque deja presents (-Generic).
|
|
TBitArray<> Excluded(false, Buildings.Num());
|
|
TBitArray<> School(false, Buildings.Num());
|
|
TBitArray<> IsGeneric(false, Buildings.Num());
|
|
int32 ExcludedCount = 0, SchoolBuildings = 0, SchoolNamesCleared = 0, ExistingGeneric = 0;
|
|
TMap<FString, int32> GenericPerCategory;
|
|
LPCityOsm::FReader Osm;
|
|
TArray<int32> WayOf;
|
|
WayOf.Init(INDEX_NONE, Buildings.Num());
|
|
if (!OsmPath.IsEmpty())
|
|
{
|
|
Osm.OriginLat = OriginLat;
|
|
Osm.OriginLon = OriginLon;
|
|
FString Error;
|
|
if (!LPCityOsm::Read(Osm, OsmPath, Error))
|
|
{
|
|
Line(FString::Printf(TEXT("ECHEC : %s ; rien ecrit"), *Error));
|
|
return Finish(1);
|
|
}
|
|
Osm.Nodes.Empty();
|
|
LPCityOsm::FWayIndex Index;
|
|
Index.Build(Osm.WayCenters);
|
|
for (int32 I = 0; I < Buildings.Num(); ++I)
|
|
{
|
|
if (Buildings[I].BuildingPoints.Num() < 3) continue;
|
|
const int32 Way = Index.Find(LPCityOsm::BoxCenter(Buildings[I].BuildingPoints));
|
|
WayOf[I] = Way;
|
|
if (Way != INDEX_NONE && LPCityOsm::IsExcludedBuilding(&Osm.WayTags[Way]))
|
|
{
|
|
Excluded[I] = true;
|
|
++ExcludedCount;
|
|
}
|
|
}
|
|
}
|
|
for (int32 I = 0; I < Buildings.Num(); ++I)
|
|
{
|
|
FStreetMapBuilding& Building = Buildings[I];
|
|
const TMap<FString, FString>* Tags = WayOf[I] != INDEX_NONE ? &Osm.WayTags[WayOf[I]] : nullptr;
|
|
if (bRemoveSchools && ((Tags && IsSchoolTags(*Tags)) || IsSchoolName(Building.BuildingName) || IsSchoolAmenity(Building.Amenity)))
|
|
{
|
|
School[I] = true;
|
|
++SchoolBuildings;
|
|
SchoolNamesCleared += Building.BuildingName.IsEmpty() ? 0 : 1;
|
|
Building.BuildingName.Empty();
|
|
if (IsSchoolAmenity(Building.Amenity) || (Tags && IsSchoolTags(*Tags))) Building.Amenity.Empty();
|
|
continue;
|
|
}
|
|
if (Brands.IsEmpty() || Building.BuildingName.IsEmpty() || !Tags) continue;
|
|
FString Key, Value;
|
|
if (!MainKey(*Tags, Key, Value)) continue;
|
|
const FString Category = MatchBrand(Brands, *Tags, Building.BuildingName, Key, Value);
|
|
if (Category.IsEmpty()) continue;
|
|
Building.BuildingName = GenericName(Category, Key, Value);
|
|
IsGeneric[I] = true;
|
|
++ExistingGeneric;
|
|
++GenericPerCategory.FindOrAdd(Category);
|
|
}
|
|
|
|
// Points d'interet.
|
|
TArray<LPCityOsm::FPoint> Points;
|
|
TSet<FString> SeenIds;
|
|
int32 SchoolPoints = 0;
|
|
for (const FString& Path : PoiPaths)
|
|
{
|
|
LPCityOsm::FReader Reader;
|
|
Reader.OriginLat = OriginLat;
|
|
Reader.OriginLon = OriginLon;
|
|
Reader.bWays = false;
|
|
Reader.bPoints = true;
|
|
FString Error;
|
|
if (!LPCityOsm::Read(Reader, Path, Error))
|
|
{
|
|
Line(FString::Printf(TEXT("ECHEC : %s ; rien ecrit"), *Error));
|
|
return Finish(1);
|
|
}
|
|
int32 Kept = 0;
|
|
for (LPCityOsm::FPoint& Point : Reader.Points)
|
|
{
|
|
bool bSeen = false;
|
|
SeenIds.Add(Point.Id, &bSeen);
|
|
if (bSeen || Point.Tags.FindRef(TEXT("name")).IsEmpty()) continue;
|
|
if (bRemoveSchools && (IsSchoolTags(Point.Tags) || IsSchoolName(Point.Tags.FindRef(TEXT("name"))))) { ++SchoolPoints; continue; }
|
|
Points.Add(MoveTemp(Point));
|
|
++Kept;
|
|
}
|
|
Line(FString::Printf(TEXT("points %s : %d nommes (doublons entre fichiers et ecoles ecartes)"), *Path, Kept));
|
|
}
|
|
|
|
// Grilles : batiments (boite) et routes (segments). Les ecoles ne recoivent aucun point.
|
|
FGrid BuildingGrid, RoadGrid;
|
|
for (int32 I = 0; I < Buildings.Num(); ++I)
|
|
{
|
|
if (Buildings[I].BuildingPoints.Num() >= 3 && !Excluded[I] && !School[I]) BuildingGrid.Add(FBox2D(Buildings[I].BuildingPoints), I);
|
|
}
|
|
TArray<TPair<FVector2D, FVector2D>> Segments;
|
|
for (const FStreetMapRoad& Road : StreetMap->GetRoads())
|
|
{
|
|
for (int32 I = 0; I + 1 < Road.RoadPoints.Num(); ++I)
|
|
{
|
|
const int32 Index = Segments.Add(TPair<FVector2D, FVector2D>(Road.RoadPoints[I], Road.RoadPoints[I + 1]));
|
|
FBox2D Box(ForceInit);
|
|
Box += Road.RoadPoints[I];
|
|
Box += Road.RoadPoints[I + 1];
|
|
RoadGrid.Add(Box, Index);
|
|
}
|
|
}
|
|
auto StreetDistance = [&](const FVector2D& P)
|
|
{
|
|
TSet<int32> Near;
|
|
RoadGrid.Near(P, 1, Near);
|
|
double Best = 1e12;
|
|
for (const int32 S : Near) Best = FMath::Min(Best, SegmentDistance(P, Segments[S].Key, Segments[S].Value));
|
|
return Best;
|
|
};
|
|
|
|
// Rattachement.
|
|
TMap<FString, int32> NameCount;
|
|
for (const LPCityOsm::FPoint& Point : Points) ++NameCount.FindOrAdd(Point.Tags.FindRef(TEXT("name")));
|
|
TMap<int32, TArray<FCandidate>> PerBuilding;
|
|
int32 Inside = 0, Close = 0, Lost = 0, Vacant = 0, NoKey = 0;
|
|
for (const LPCityOsm::FPoint& Point : Points)
|
|
{
|
|
FString Key, Value;
|
|
for (const TCHAR* K : Keys)
|
|
{
|
|
if (const FString* V = Point.Tags.Find(K)) { Key = K; Value = *V; break; }
|
|
}
|
|
if (Key.IsEmpty()) { ++NoKey; continue; }
|
|
if (Key == TEXT("shop") && Value == TEXT("vacant")) { ++Vacant; continue; }
|
|
TSet<int32> Near;
|
|
BuildingGrid.Near(Point.Position, 1, Near);
|
|
int32 Hit = INDEX_NONE;
|
|
double BestDist = MaxDistCm;
|
|
for (const int32 B : Near)
|
|
{
|
|
const TArray<FVector2D>& Pts = Buildings[B].BuildingPoints;
|
|
if (LPCityOsm::InsidePolygon(Pts, Point.Position)) { Hit = B; BestDist = -1.0; break; }
|
|
for (int32 E = 0; E < Pts.Num(); ++E)
|
|
{
|
|
const double D = SegmentDistance(Point.Position, Pts[E], Pts[(E + 1) % Pts.Num()]);
|
|
if (D < BestDist) { BestDist = D; Hit = B; }
|
|
}
|
|
}
|
|
if (Hit == INDEX_NONE) { ++Lost; continue; }
|
|
(BestDist < 0.0 ? Inside : Close)++;
|
|
const FString Name = Point.Tags.FindRef(TEXT("name"));
|
|
PerBuilding.FindOrAdd(Hit).Add({ ClassRank(Key, Value), StreetDistance(Point.Position), Name, Key, Value,
|
|
Brands.IsEmpty() ? FString() : MatchBrand(Brands, Point.Tags, Name, Key, Value) });
|
|
}
|
|
|
|
// Choix et ecriture.
|
|
TSet<FString> DictKeys;
|
|
if (!KeysPath.IsEmpty())
|
|
{
|
|
TArray<FString> KeyLines;
|
|
FFileHelper::LoadFileToStringArray(KeyLines, *KeysPath);
|
|
for (const FString& K : KeyLines) if (!K.TrimStartAndEnd().IsEmpty()) DictKeys.Add(K.TrimStartAndEnd());
|
|
}
|
|
int32 NamedAfter = 0, NewNames = 0, NewGeneric = 0, ChainsBehind = 0, AmenityFilled = 0, InDict = 0, Shops = 0, Food = 0, GenericBuildings = 0;
|
|
TMap<FString, int32> RawOutside;
|
|
for (TPair<int32, TArray<FCandidate>>& Pair : PerBuilding)
|
|
{
|
|
TArray<FCandidate>& List = Pair.Value;
|
|
List.Sort([&NameCount](const FCandidate& A, const FCandidate& B)
|
|
{
|
|
if (A.Category.IsEmpty() != B.Category.IsEmpty()) return A.Category.IsEmpty();
|
|
if (A.Rank != B.Rank) return A.Rank < B.Rank;
|
|
if (FMath::Abs(A.StreetCm - B.StreetCm) > 50.0) return A.StreetCm < B.StreetCm;
|
|
const int32 CA = NameCount.FindRef(A.Name), CB = NameCount.FindRef(B.Name);
|
|
if (CA != CB) return CA > CB;
|
|
return A.Name.Len() > B.Name.Len();
|
|
});
|
|
const FCandidate& Best = List[0];
|
|
FStreetMapBuilding& Building = Buildings[Pair.Key];
|
|
const FString Mapped = MapAmenity(Best.Key, Best.Value);
|
|
if (Building.BuildingName.IsEmpty())
|
|
{
|
|
if (Best.Category.IsEmpty())
|
|
{
|
|
Building.BuildingName = Best.Name;
|
|
++NewNames;
|
|
ChainsBehind += List.ContainsByPredicate([](const FCandidate& C) { return !C.Category.IsEmpty(); }) ? 1 : 0;
|
|
}
|
|
else
|
|
{
|
|
Building.BuildingName = GenericName(Best.Category, Best.Key, Best.Value);
|
|
IsGeneric[Pair.Key] = true;
|
|
++NewGeneric;
|
|
++GenericPerCategory.FindOrAdd(Best.Category);
|
|
}
|
|
}
|
|
if (Building.Amenity.IsEmpty())
|
|
{
|
|
Building.Amenity = Mapped;
|
|
++AmenityFilled;
|
|
if (!DictKeys.IsEmpty())
|
|
{
|
|
if (DictKeys.Contains(Mapped)) ++InDict;
|
|
else ++RawOutside.FindOrAdd(Mapped);
|
|
}
|
|
}
|
|
Shops += Best.Rank == 0 ? 1 : 0;
|
|
Food += Best.Rank == 1 ? 1 : 0;
|
|
}
|
|
for (int32 I = 0; I < Buildings.Num(); ++I)
|
|
{
|
|
NamedAfter += Buildings[I].BuildingName.IsEmpty() ? 0 : 1;
|
|
GenericBuildings += IsGeneric[I] ? 1 : 0;
|
|
}
|
|
const double Total = FMath::Max(1, Buildings.Num());
|
|
|
|
Line(FString::Printf(TEXT("batiments %d ; filtres (sans nom ajoute) %d ; points rattaches : dans le polygone %d, a moins de %.0f m %d ; sans batiment %d ; shop=vacant ecartes %d ; sans cle %d"),
|
|
Buildings.Num(), ExcludedCount, Inside, MaxDistCm / 100.0, Close, Lost, Vacant, NoKey));
|
|
if (bRemoveSchools)
|
|
{
|
|
Line(FString::Printf(TEXT("ecoles et creches : points ecartes %d ; batiments d'ecole %d (noms effaces %d), sans nom ni point ajoute"), SchoolPoints, SchoolBuildings, SchoolNamesCleared));
|
|
}
|
|
if (!Brands.IsEmpty())
|
|
{
|
|
TArray<FString> Counts;
|
|
for (const TPair<FString, int32>& P : GenericPerCategory) Counts.Add(FString::Printf(TEXT("%s %d"), *P.Key, P.Value));
|
|
Line(FString::Printf(TEXT("noms generiques : %d batiments (%d noms OSM deja presents remplaces, %d choix ecrits) ; par categorie : %s ; chaines passees apres un nom propre : %d batiments"),
|
|
GenericBuildings, ExistingGeneric, NewGeneric, *FString::Join(Counts, TEXT(", ")), ChainsBehind));
|
|
}
|
|
Line(FString::Printf(TEXT("batiments avec point d'interet %d : choix commerce %d, restauration %d (%.1f %%) ; noms propres ajoutes %d ; amenites ecrites %d"),
|
|
PerBuilding.Num(), Shops, Food, PerBuilding.Num() ? 100.0 * (Shops + Food) / PerBuilding.Num() : 0.0, NewNames, AmenityFilled));
|
|
Line(FString::Printf(TEXT("batiments nommes : %d avant, %d apres, sur %d (%.1f %% en comptant les noms generiques ; %.1f %% en noms propres seuls)"),
|
|
NamedBefore, NamedAfter, Buildings.Num(), 100.0 * NamedAfter / Total, 100.0 * (NamedAfter - GenericBuildings) / Total));
|
|
if (!DictKeys.IsEmpty())
|
|
{
|
|
RawOutside.ValueSort([](int32 A, int32 B) { return A > B; });
|
|
TArray<FString> Top;
|
|
for (const TPair<FString, int32>& P : RawOutside) { if (Top.Num() >= 20) break; Top.Add(FString::Printf(TEXT("%s %d"), *P.Key, P.Value)); }
|
|
Line(FString::Printf(TEXT("amenites ecrites dans AmenityImageDict (%d cles) : %d sur %d (%.1f %%) ; hors dictionnaire, les plus frequentes : %s"),
|
|
DictKeys.Num(), InDict, AmenityFilled, 100.0 * InDict / FMath::Max(1, AmenityFilled), *FString::Join(Top, TEXT(", "))));
|
|
}
|
|
|
|
if (bDryRun)
|
|
{
|
|
Line(TEXT("DryRun : rien ecrit"));
|
|
return Finish(0);
|
|
}
|
|
UPackage* Package = StreetMap->GetPackage();
|
|
Package->MarkPackageDirty();
|
|
FSavePackageArgs SaveArgs;
|
|
SaveArgs.TopLevelFlags = RF_Public | RF_Standalone;
|
|
const FString FileName = FPackageName::LongPackageNameToFilename(Package->GetName(), FPackageName::GetAssetPackageExtension());
|
|
if (!UPackage::SavePackage(Package, StreetMap, *FileName, SaveArgs))
|
|
{
|
|
Line(FString::Printf(TEXT("ECHEC : sauvegarde de %s"), *FileName));
|
|
return Finish(1);
|
|
}
|
|
Line(FString::Printf(TEXT("asset ecrit en place : %s (%s), %lld octets"), *Package->GetName(), *FileName, IFileManager::Get().FileSize(*FileName)));
|
|
return Finish(0);
|
|
}
|