diff --git a/.gitignore b/.gitignore index 82c594460..ca7bea1ce 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,7 @@ data/symbols/**/symbols.sdf data/bookmarks data/edits.xml +data/traffic.xml data/World.mwm data/WorldCoasts.mwm data/world_mwm/* diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro index 19d264243..473e1d5a9 100644 --- a/android/app/proguard-rules.pro +++ b/android/app/proguard-rules.pro @@ -28,3 +28,8 @@ # R8 crypts the source line numbers in all log messages. # https://github.com/organicmaps/organicmaps/issues/6559#issuecomment-1812039926 -dontoptimize + +# Keep classes for Android TraFF support +-keep class app.organicmaps.sdk.traffxml.SourceImplV0_7 { *; } +-keep class app.organicmaps.sdk.traffxml.SourceImplV0_8 { *; } + diff --git a/android/app/src/fdroid/play/version.yaml b/android/app/src/fdroid/play/version.yaml index ef749110e..5d9cfbd0f 100644 --- a/android/app/src/fdroid/play/version.yaml +++ b/android/app/src/fdroid/play/version.yaml @@ -1 +1 @@ -version: 2025.03.02-7-FDroid+25030207 +version: 2025.07.23-4-FDroid+25072304 diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index cdf7fb136..e9e59442a 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -62,6 +62,21 @@ + + + + + + + + + + + + + + + diff --git a/android/app/src/main/java/app/organicmaps/maplayer/LayersUtils.java b/android/app/src/main/java/app/organicmaps/maplayer/LayersUtils.java index b4b883872..73378e07a 100644 --- a/android/app/src/main/java/app/organicmaps/maplayer/LayersUtils.java +++ b/android/app/src/main/java/app/organicmaps/maplayer/LayersUtils.java @@ -12,6 +12,7 @@ public static List getAvailableLayers() availableLayers.add(Mode.OUTDOORS); availableLayers.add(Mode.ISOLINES); availableLayers.add(Mode.SUBWAY); + availableLayers.add(Mode.TRAFFIC); return availableLayers; } } diff --git a/android/app/src/main/java/app/organicmaps/settings/SettingsPrefsFragment.java b/android/app/src/main/java/app/organicmaps/settings/SettingsPrefsFragment.java index f9ea06078..e348815bb 100644 --- a/android/app/src/main/java/app/organicmaps/settings/SettingsPrefsFragment.java +++ b/android/app/src/main/java/app/organicmaps/settings/SettingsPrefsFragment.java @@ -5,7 +5,11 @@ import static app.organicmaps.sdk.editor.data.Language.AUTO_LANG_CODE; import android.annotation.SuppressLint; +import android.content.ComponentName; +import android.content.Context; import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; import android.content.SharedPreferences; import android.net.Uri; import android.os.Build; @@ -14,9 +18,12 @@ import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.StringRes; import androidx.appcompat.app.AppCompatDelegate; import androidx.core.os.LocaleListCompat; +import androidx.preference.EditTextPreference; import androidx.preference.ListPreference; +import androidx.preference.MultiSelectListPreference; import androidx.preference.Preference; import androidx.preference.PreferenceCategory; import androidx.preference.PreferenceManager; @@ -38,6 +45,7 @@ import app.organicmaps.sdk.search.SearchRecents; import app.organicmaps.sdk.settings.MapLanguageCode; import app.organicmaps.sdk.settings.UnitLocale; +import app.organicmaps.sdk.traffxml.AndroidTransport; import app.organicmaps.sdk.util.Config; import app.organicmaps.sdk.util.NetworkPolicy; import app.organicmaps.sdk.util.PowerManagment; @@ -50,6 +58,7 @@ import java.util.Collection; import java.util.List; import java.util.Locale; +import java.util.Set; public class SettingsPrefsFragment extends BaseXmlSettingsFragment implements MapLanguagesFragment.Listener, AppLanguagesFragment.Listener @@ -72,6 +81,10 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat initAutoDownloadPrefsCallbacks(); initLargeFontSizePrefsCallbacks(); initTransliterationPrefsCallbacks(); + initTrafficHttpEnabledPrefsCallbacks(); + initTrafficHttpUrlPrefsCallbacks(); + initTrafficAppsPrefs(); + initTrafficLegacyEnabledPrefsCallbacks(); initAlternativeMapLanguageHandlingCallbacks(); init3dModePrefsCallbacks(); initPerspectivePrefsCallbacks(); @@ -177,6 +190,46 @@ private void updateAppLanguageCodeSummary() } } + private void updateTrafficHttpUrlSummary() + { + final Preference pref = getPreference(getString(R.string.pref_traffic_http_url)); + String summary = Config.getTrafficHttpUrl(); + if (summary.length() == 0) + pref.setSummary(R.string.traffic_http_url_not_set); + else + pref.setSummary(summary); + } + + private void updateTrafficAppsSummary() + { + final MultiSelectListPreference pref = getPreference(getString(R.string.pref_traffic_apps)); + /* + * If the preference is disabled, it has not been initialized. This is the case if no TraFF + * apps were found. The code below would crash when trying to access the entries, and there + * is no need to update the summary if the setting cannot be changed. + */ + if (!pref.isEnabled()) + return; + String[] apps = Config.getTrafficApps(); + if (apps.length == 0) + pref.setSummary(R.string.traffic_apps_none_selected); + else + { + String summary = ""; + for (int i = 0; i < apps.length; i++) + { + if (i > 0) + summary = summary + ", "; + int index = pref.findIndexOfValue(apps[i]); + if (i >= 0) + summary = summary + pref.getEntries()[index]; + else + summary = summary + apps[i]; + } + pref.setSummary(summary); + } + } + private void updateRoutingSettingsPrefsSummary() { final Preference pref = getPreference(getString(R.string.prefs_routing)); @@ -205,6 +258,8 @@ public void onResume() updateRoutingSettingsPrefsSummary(); updateMapLanguageCodeSummary(); updateAppLanguageCodeSummary(); + updateTrafficHttpUrlSummary(); + updateTrafficAppsSummary(); } @Override @@ -301,6 +356,91 @@ private void initTransliterationPrefsCallbacks() }); } + private void initTrafficHttpEnabledPrefsCallbacks() + { + final Preference pref = getPreference(getString(R.string.pref_traffic_http_enabled)); + + ((TwoStatePreference)pref).setChecked(Config.getTrafficHttpEnabled()); + pref.setOnPreferenceChangeListener((preference, newValue) -> { + final boolean oldVal = Config.getTrafficHttpEnabled(); + final boolean newVal = (Boolean) newValue; + if (oldVal != newVal) + Config.setTrafficHttpEnabled(newVal); + + return true; + }); + } + + private void initTrafficHttpUrlPrefsCallbacks() + { + final Preference pref = getPreference(getString(R.string.pref_traffic_http_url)); + + ((EditTextPreference)pref).setText(Config.getTrafficHttpUrl()); + pref.setOnPreferenceChangeListener((preference, newValue) -> { + final String oldVal = Config.getTrafficHttpUrl(); + final String newVal = (String) newValue; + if (!oldVal.equals(newVal)) + Config.setTrafficHttpUrl(newVal); + + return true; + }); + } + + private void initTrafficAppsPrefs() + { + final MultiSelectListPreference pref = getPreference(getString(R.string.pref_traffic_apps)); + + PackageManager pm = getContext().getPackageManager(); + List receivers = pm.queryBroadcastReceivers(new Intent(AndroidTransport.ACTION_TRAFF_GET_CAPABILITIES), 0); + + if (receivers == null || receivers.isEmpty()) + { + pref.setSummary(R.string.traffic_apps_not_available); + pref.setEnabled(false); + return; + } + + pref.setEnabled(true); + + List entryList = new ArrayList<>(receivers.size()); + List valueList = new ArrayList<>(receivers.size()); + + for (ResolveInfo receiver : receivers) + { + // friendly name + entryList.add(receiver.loadLabel(pm).toString()); + // actual value (we just need the package name, broadcasts are sent to any receiver in the package) + valueList.add(receiver.activityInfo.applicationInfo.packageName); + } + + pref.setEntries(entryList.toArray(new CharSequence[0])); + pref.setEntryValues(valueList.toArray(new CharSequence[0])); + + pref.setOnPreferenceChangeListener((preference, newValue) -> { + // newValue is a Set, each item is a package ID + String[] apps = ((Set)newValue).toArray(new String[0]); + Config.setTrafficApps(apps); + updateTrafficAppsSummary(); + + return true; + }); + } + + private void initTrafficLegacyEnabledPrefsCallbacks() + { + final Preference pref = getPreference(getString(R.string.pref_traffic_legacy_enabled)); + + ((TwoStatePreference)pref).setChecked(Config.getTrafficLegacyEnabled()); + pref.setOnPreferenceChangeListener((preference, newValue) -> { + final boolean oldVal = Config.getTrafficLegacyEnabled(); + final boolean newVal = (Boolean) newValue; + if (oldVal != newVal) + Config.setTrafficLegacyEnabled(newVal); + + return true; + }); + } + private void initUseMobileDataPrefsCallbacks() { final ListPreference mobilePref = getPreference(getString(R.string.pref_use_mobile_data)); diff --git a/android/app/src/main/res/values/donottranslate.xml b/android/app/src/main/res/values/donottranslate.xml index 61bb59dca..b58e94f57 100644 --- a/android/app/src/main/res/values/donottranslate.xml +++ b/android/app/src/main/res/values/donottranslate.xml @@ -35,6 +35,11 @@ GeneralSettings Navigation Information + Traffic + TrafficHttpEnabled + TrafficHttpUrl + TrafficApps + TrafficLegacyEnabled Transliteration PowerManagment KeepScreenOn diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 37cf02a66..70496f15f 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -211,6 +211,7 @@ Information Navigation + Traffic information Zoom buttons Display on the map @@ -795,6 +796,24 @@ Map language App language + + Enable live traffic data + + When enabled, the app will periodically retrieve traffic information from the configured URL. + + Traffic service URL + + Not set + + Use data from TraFF applications + + No apps installed + + No apps selected + + Use data from legacy TraFF applications + + When enabled, the app will receive and process traffic data from legacy TraFF applications. Local Language diff --git a/android/app/src/main/res/xml/prefs_main.xml b/android/app/src/main/res/xml/prefs_main.xml index 77056d443..a4094ecd4 100644 --- a/android/app/src/main/res/xml/prefs_main.xml +++ b/android/app/src/main/res/xml/prefs_main.xml @@ -223,6 +223,38 @@ app:singleLineTitle="false" /> + + + + + + + CallVoidMethod(*onComplete, methodId); + + ASSERT(g_framework, ("g_framework must be non-null")); + + /* + * Add traffic sources for Android. + */ + jclass configClass = env->FindClass("app/organicmaps/sdk/util/Config"); + jmethodID const getTrafficLegacyEnabledId = jni::GetStaticMethodID(env, configClass, + "getTrafficLegacyEnabled", "()Z"); + jmethodID const applyTrafficLegacyEnabledId = jni::GetStaticMethodID(env, configClass, + "applyTrafficLegacyEnabled", "(Z)V"); + jmethodID const getTrafficAppsId = jni::GetStaticMethodID(env, configClass, + "getTrafficApps", "()[Ljava/lang/String;"); + jmethodID const applyTrafficAppsId = jni::GetStaticMethodID(env, configClass, + "applyTrafficApps", "([Ljava/lang/String;)V"); + + env->CallStaticVoidMethod(configClass, applyTrafficLegacyEnabledId, + env->CallStaticBooleanMethod(configClass, getTrafficLegacyEnabledId)); + env->CallStaticVoidMethod(configClass, applyTrafficAppsId, + (jobjectArray)env->CallStaticObjectMethod(configClass, getTrafficAppsId)); }); } } diff --git a/android/sdk/src/main/cpp/app/organicmaps/sdk/traffxml/AndroidTraffSource.cpp b/android/sdk/src/main/cpp/app/organicmaps/sdk/traffxml/AndroidTraffSource.cpp new file mode 100644 index 000000000..a9410c45c --- /dev/null +++ b/android/sdk/src/main/cpp/app/organicmaps/sdk/traffxml/AndroidTraffSource.cpp @@ -0,0 +1,115 @@ +#include "AndroidTraffSource.hpp" + +#include "app/organicmaps/sdk/core/jni_helper.hpp" + +namespace traffxml { +void AndroidTraffSourceV0_7::Create(TraffSourceManager & manager) +{ + std::unique_ptr source = std::unique_ptr(new AndroidTraffSourceV0_7(manager)); + manager.RegisterSource(std::move(source)); +} + +AndroidTraffSourceV0_7::AndroidTraffSourceV0_7(TraffSourceManager & manager) + : TraffSource(manager) +{ + JNIEnv * env = jni::GetEnv(); + + static jclass const implClass = jni::GetGlobalClassRef(env, "app/organicmaps/sdk/traffxml/SourceImplV0_7"); + + static jmethodID const implConstructor = jni::GetConstructorID(env, implClass, "(Landroid/content/Context;J)V"); + + jlong nativeManager = reinterpret_cast(&manager); + + jobject implObject = env->NewObject( + implClass, implConstructor, android::Platform::Instance().GetContext(), nativeManager); + + m_implObject = env->NewGlobalRef(implObject); + + m_subscribeImpl = jni::GetMethodID(env, m_implObject, "subscribe", "(Ljava/lang/String;)V"); + m_unsubscribeImpl = jni::GetMethodID(env, m_implObject, "unsubscribe", "()V"); +} + +AndroidTraffSourceV0_7::~AndroidTraffSourceV0_7() +{ + jni::GetEnv()->DeleteGlobalRef(m_implObject); +} + +void AndroidTraffSourceV0_7::Close() +{ + Unsubscribe(); +} + +void AndroidTraffSourceV0_7::Subscribe(std::set & mwms) +{ + jni::GetEnv()->CallVoidMethod(m_implObject, m_subscribeImpl, nullptr); +} + +void AndroidTraffSourceV0_7::Unsubscribe() +{ + jni::GetEnv()->CallVoidMethod(m_implObject, m_unsubscribeImpl); +} + +void AndroidTraffSourceV0_8::Create(TraffSourceManager & manager, std::string const & packageId) +{ + std::unique_ptr source = std::unique_ptr(new AndroidTraffSourceV0_8(manager, packageId)); + manager.RegisterSource(std::move(source)); +} + +AndroidTraffSourceV0_8::AndroidTraffSourceV0_8(TraffSourceManager & manager, std::string const & packageId) + : TraffSource(manager) +{ + JNIEnv * env = jni::GetEnv(); + + static jclass const implClass = jni::GetGlobalClassRef(env, "app/organicmaps/sdk/traffxml/SourceImplV0_8"); + + static jmethodID const implConstructor = jni::GetConstructorID(env, implClass, "(Landroid/content/Context;JLjava/lang/String;)V"); + + jlong nativeManager = reinterpret_cast(&manager); + + jobject implObject = env->NewObject( + implClass, implConstructor, android::Platform::Instance().GetContext(), nativeManager, jni::ToJavaString(env, packageId)); + + m_implObject = env->NewGlobalRef(implObject); + + m_subscribeImpl = jni::GetMethodID(env, m_implObject, "subscribe", "(Ljava/lang/String;)V"); + m_changeSubscriptionImpl = jni::GetMethodID(env, m_implObject, "changeSubscription", "(Ljava/lang/String;)V"); + m_unsubscribeImpl = jni::GetMethodID(env, m_implObject, "unsubscribe", "()V"); + + // TODO packageId (if we need that at all here) +} + +AndroidTraffSourceV0_8::~AndroidTraffSourceV0_8() +{ + jni::GetEnv()->DeleteGlobalRef(m_implObject); +} + +void AndroidTraffSourceV0_8::Close() +{ + Unsubscribe(); +} + +void AndroidTraffSourceV0_8::Subscribe(std::set & mwms) +{ + JNIEnv * env = jni::GetEnv(); + std::string data = "\n" + + GetMwmFilters(mwms) + + ""; + + env->CallVoidMethod(m_implObject, m_subscribeImpl, jni::ToJavaString(env, data)); +} + +void AndroidTraffSourceV0_8::ChangeSubscription(std::set & mwms) +{ + JNIEnv * env = jni::GetEnv(); + std::string data = "\n" + + GetMwmFilters(mwms) + + ""; + + env->CallVoidMethod(m_implObject, m_changeSubscriptionImpl, jni::ToJavaString(env, data)); +} + +void AndroidTraffSourceV0_8::Unsubscribe() +{ + jni::GetEnv()->CallVoidMethod(m_implObject, m_unsubscribeImpl); +} +} // namespace traffxml diff --git a/android/sdk/src/main/cpp/app/organicmaps/sdk/traffxml/AndroidTraffSource.hpp b/android/sdk/src/main/cpp/app/organicmaps/sdk/traffxml/AndroidTraffSource.hpp new file mode 100644 index 000000000..05e04c659 --- /dev/null +++ b/android/sdk/src/main/cpp/app/organicmaps/sdk/traffxml/AndroidTraffSource.hpp @@ -0,0 +1,199 @@ +#pragma once + +#include "traffxml/traff_source.hpp" + +namespace traffxml +{ +/** + * @brief A TraFF source which relies on Android Binder for message delivery, using version 0.7 of the TraFF protocol. + * + * TraFF 0.7 does not support subscriptions. Messages are broadcast as the payload to a `FEED` intent. + */ +class AndroidTraffSourceV0_7 : public TraffSource +{ +public: + /** + * @brief Creates a new `AndroidTraffSourceV0_7` instance and registers it with the traffic manager. + * + * @param manager The traffic manager to register the new instance with + */ + static void Create(TraffSourceManager & manager); + + virtual ~AndroidTraffSourceV0_7() override; + + /** + * @brief Prepares the traffic source for unloading. + */ + // TODO do we need a close operation here? + // TODO move this to the parent class and override it here? + void Close(); + + /** + * @brief Subscribes to a traffic service. + * + * TraFF 0.7 does not support subscriptions. This implementation registers a broadcast receiver. + * + * @param mwms The MWMs for which data is needed (not used by this implementation). + */ + virtual void Subscribe(std::set & mwms) override; + + /** + * @brief Changes an existing traffic subscription. + * + * This implementation does nothing, as TraFF 0.7 does not support subscriptions. + * + * @param mwms The new set of MWMs for which data is needed. + */ + virtual void ChangeSubscription(std::set & mwms) override {}; + + /** + * @brief Unsubscribes from a traffic service we are subscribed to. + * + * TraFF 0.7 does not support subscriptions. This implementation unregisters the broadcast + * receiver which was registered by `Subscribe()`. + */ + virtual void Unsubscribe() override; + + /** + * @brief Whether this source should be polled. + * + * Prior to calling `Poll()` on a source, the caller should always first call `IsPollNeeded()` and + * poll the source only if the result is true. + * + * This implementation always returns false, as message delivery on Android uses `FEED` (push). + * + * @return true if the source should be polled, false if not. + */ + virtual bool IsPollNeeded() override { return false; }; + + /** + * @brief Polls the traffic service for updates. + * + * This implementation does nothing, as message delivery on Android uses `FEED` (push). + */ + virtual void Poll() override {}; + +protected: + /** + * @brief Constructs a new `AndroidTraffSourceV0_7`. + * @param manager The `TrafficSourceManager` instance to register the source with. + */ + AndroidTraffSourceV0_7(TraffSourceManager & manager); + +private: + // TODO “subscription” (i.e. broadcast receiver) state + + /** + * @brief The Java implementation class instance. + */ + jobject m_implObject; + + /** + * @brief The Java subscribe method. + */ + jmethodID m_subscribeImpl; + + /** + * @brief The Java unsubscribe method. + */ + jmethodID m_unsubscribeImpl; +}; + +/** + * @brief A TraFF source which relies on Android Binder for message delivery, using version 0.8 of the TraFF protocol. + * + * TraFF 0.8 supports subscriptions. Messages are announced through a `FEED` intent, whereupon the + * consumer can retrieve them from a content provider. + */ +class AndroidTraffSourceV0_8 : public TraffSource +{ +public: + /** + * @brief Creates a new `AndroidTraffSourceV0_8` instance and registers it with the traffic manager. + * + * @param manager The traffic manager to register the new instance with + * @param packageId The package ID of the app providing the TraFF source. + */ + static void Create(TraffSourceManager & manager, std::string const & packageId); + + virtual ~AndroidTraffSourceV0_8() override; + + /** + * @brief Prepares the traffic source for unloading. + * + * If there is still an active subscription, it unsubscribes, but without processing the result + * received from the service. Otherwise, teardown is a no-op. + */ + // TODO move this to the parent class and override it here? + void Close(); + + /** + * @brief Subscribes to a traffic service. + * + * @param mwms The MWMs for which data is needed. + */ + virtual void Subscribe(std::set & mwms) override; + + /** + * @brief Changes an existing traffic subscription. + * + * @param mwms The new set of MWMs for which data is needed. + */ + virtual void ChangeSubscription(std::set & mwms) override; + + /** + * @brief Unsubscribes from a traffic service we are subscribed to. + */ + virtual void Unsubscribe() override; + + /** + * @brief Whether this source should be polled. + * + * Prior to calling `Poll()` on a source, the caller should always first call `IsPollNeeded()` and + * poll the source only if the result is true. + * + * This implementation always returns false, as message delivery on Android uses `FEED` (push). + * + * @return true if the source should be polled, false if not. + */ + virtual bool IsPollNeeded() override { return false; }; + + /** + * @brief Polls the traffic service for updates. + * + * This implementation does nothing, as message delivery on Android uses `FEED` (push). + */ + virtual void Poll() override {}; + +protected: + /** + * @brief Constructs a new `AndroidTraffSourceV0_8`. + * @param manager The `TrafficSourceManager` instance to register the source with. + * @param packageId The package ID of the app providing the TraFF source. + */ + AndroidTraffSourceV0_8(TraffSourceManager & manager, std::string const & packageId); + +private: + // TODO subscription state + + /** + * @brief The Java implementation class instance. + */ + jobject m_implObject; + + /** + * @brief The Java subscribe method. + */ + jmethodID m_subscribeImpl; + + /** + * @brief The Java changeSubscription method. + */ + jmethodID m_changeSubscriptionImpl; + + /** + * @brief The Java unsubscribe method. + */ + jmethodID m_unsubscribeImpl; +}; +} // namespace traffxml diff --git a/android/sdk/src/main/cpp/app/organicmaps/sdk/traffxml/SourceImpl.cpp b/android/sdk/src/main/cpp/app/organicmaps/sdk/traffxml/SourceImpl.cpp new file mode 100644 index 000000000..885c3b5fc --- /dev/null +++ b/android/sdk/src/main/cpp/app/organicmaps/sdk/traffxml/SourceImpl.cpp @@ -0,0 +1,34 @@ +// TODO which of the two do we need? (jni_helper includes jni) +//#include +#include "app/organicmaps/sdk/core/jni_helper.hpp" + +#include "traffxml/traff_source.hpp" +#include "traffxml/traff_model_xml.hpp" + +#include + +extern "C" +{ + JNIEXPORT void JNICALL + Java_app_organicmaps_sdk_traffxml_SourceImpl_onFeedReceivedImpl(JNIEnv * env, jclass thiz, jlong nativeManager, jstring feed) + { + std::string feedStd = jni::ToNativeString(env, feed); + pugi::xml_document document; + traffxml::TraffFeed parsedFeed; + + if (!document.load_string(feedStd.c_str())) + { + LOG(LWARNING, ("Feed is not a well-formed XML document")); + return; + } + + if (!traffxml::ParseTraff(document, std::nullopt, parsedFeed)) + { + LOG(LWARNING, ("Feed is not a valid TraFF feed")); + return; + } + + traffxml::TraffSourceManager & manager = *reinterpret_cast(nativeManager); + manager.ReceiveFeed(parsedFeed); + } +} diff --git a/android/sdk/src/main/cpp/app/organicmaps/sdk/util/Config.cpp b/android/sdk/src/main/cpp/app/organicmaps/sdk/util/Config.cpp index 64d8ee5bd..639a42c29 100644 --- a/android/sdk/src/main/cpp/app/organicmaps/sdk/util/Config.cpp +++ b/android/sdk/src/main/cpp/app/organicmaps/sdk/util/Config.cpp @@ -129,4 +129,74 @@ JNIEXPORT void JNICALL Java_app_organicmaps_sdk_util_Config_nativeSetTranslitera frm()->SaveTransliteration(value); frm()->AllowTransliteration(value); } + +JNIEXPORT jboolean JNICALL +Java_app_organicmaps_sdk_util_Config_nativeGetTrafficHttpEnabled(JNIEnv * env, jclass thiz) +{ + return frm()->LoadTrafficHttpEnabled(); +} + +JNIEXPORT void JNICALL +Java_app_organicmaps_sdk_util_Config_nativeSetTrafficHttpEnabled(JNIEnv * env, jclass thiz, + jboolean value) +{ + frm()->SaveTrafficHttpEnabled(value); + frm()->SetTrafficHttpEnabled(value); +} + +JNIEXPORT jstring JNICALL +Java_app_organicmaps_sdk_util_Config_nativeGetTrafficHttpUrl(JNIEnv * env, jclass thiz) +{ + std::string value = frm()->LoadTrafficHttpUrl(); + return jni::ToJavaString(env, value); +} + +JNIEXPORT void JNICALL +Java_app_organicmaps_sdk_util_Config_nativeSetTrafficHttpUrl(JNIEnv * env, jclass thiz, + jstring value) +{ + frm()->SaveTrafficHttpUrl(jni::ToNativeString(env, value)); + frm()->SetTrafficHttpUrl(jni::ToNativeString(env, value)); +} + +JNIEXPORT void JNICALL +Java_app_organicmaps_sdk_util_Config_applyTrafficLegacyEnabled(JNIEnv * env, jclass thiz, + jboolean value) +{ + TrafficManager & tm = g_framework->GetTrafficManager(); + tm.RemoveTraffSourceIf([](traffxml::TraffSource* source) { + if (traffxml::AndroidTraffSourceV0_7* traffSource = dynamic_cast(source)) + { + traffSource->Close(); + return true; + } + else + return false; + }); + if (value) + traffxml::AndroidTraffSourceV0_7::Create(tm); +} + +JNIEXPORT void JNICALL +Java_app_organicmaps_sdk_util_Config_applyTrafficApps(JNIEnv * env, jclass thiz, jobjectArray value) +{ + jsize valueLen = env->GetArrayLength(value); + TrafficManager & tm = g_framework->GetTrafficManager(); + tm.RemoveTraffSourceIf([](traffxml::TraffSource* source) { + if (traffxml::AndroidTraffSourceV0_8* traffSource = dynamic_cast(source)) + { + traffSource->Close(); + return true; + } + else + return false; + }); + for (jsize i = 0; i < valueLen; i++) + { + jstring jAppId = (jstring)env->GetObjectArrayElement(value, i); + std::string appId = jni::ToNativeString(env, jAppId); + traffxml::AndroidTraffSourceV0_8::Create(tm, appId); + env->DeleteLocalRef(jAppId); + } +} } // extern "C" diff --git a/android/sdk/src/main/java/app/organicmaps/sdk/traffxml/AndroidConsumer.java b/android/sdk/src/main/java/app/organicmaps/sdk/traffxml/AndroidConsumer.java new file mode 100644 index 000000000..b83e845b3 --- /dev/null +++ b/android/sdk/src/main/java/app/organicmaps/sdk/traffxml/AndroidConsumer.java @@ -0,0 +1,111 @@ +/* + * Copyright © 2017–2020 traffxml.org. + * + * Relicensed to CoMaps by the original author. + */ + +package app.organicmaps.sdk.traffxml; + +import java.util.List; + +import app.organicmaps.sdk.traffxml.Version; +import app.organicmaps.sdk.traffxml.AndroidTransport; + +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.IntentFilter.MalformedMimeTypeException; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.net.Uri; +import android.os.Bundle; + +public class AndroidConsumer { + /** + * Creates an Intent filter which matches the Intents a TraFF consumer needs to receive. + * + *

Different filters are available for consumers implementing different versions of the TraFF + * specification. + * + * @param version The version of the TraFF specification (one of the constants in {@link org.traffxml.traff.Version}) + * + * @return An intent filter matching the necessary Intents + */ + public static IntentFilter createIntentFilter(int version) { + IntentFilter res = new IntentFilter(); + switch (version) { + case Version.V0_7: + res.addAction(AndroidTransport.ACTION_TRAFF_PUSH); + break; + case Version.V0_8: + res.addAction(AndroidTransport.ACTION_TRAFF_PUSH); + res.addDataScheme(AndroidTransport.CONTENT_SCHEMA); + try { + res.addDataType(AndroidTransport.MIME_TYPE_TRAFF); + } catch (MalformedMimeTypeException e) { + // as long as the constant is a well-formed MIME type, this exception never gets thrown + e.printStackTrace(); + } + break; + default: + throw new IllegalArgumentException("Invalid version code: " + version); + } + return res; + } + + /** + * Sends a TraFF intent to a source. + * + *

This encapsulates most of the low-level Android handling. + * + *

If the recipient specified in {@code packageName} declares multiple receivers for the intent in its + * manifest, a separate intent will be delivered to each of them. The intent will not be delivered to + * receivers registered at runtime. + * + *

All intents are sent as explicit ordered broadcasts. This means two things: + * + *

Any app which declares a matching receiver in its manifest will be woken up to process the intent. + * This works even with certain Android 7 builds which restrict intent delivery to apps which are not + * currently running. + * + *

It is safe for the recipient to unconditionally set result data. If the recipient does not set + * result data, the result will have a result code of + * {@link org.traffxml.transport.android.AndroidTransport#RESULT_INTERNAL_ERROR}, no data and no extras. + * + * @param context The context + * @param action The intent action. + * @param data The intent data (for TraFF, this is the content provider URI), or null + * @param extras The extras for the intent + * @param packageName The package name for the intent recipient, or null to deliver the intent to all matching receivers + * @param receiverPermission A permission which the recipient must hold, or null if not required + * @param resultReceiver A BroadcastReceiver which will receive the result for the intent + */ + public static void sendTraffIntent(Context context, String action, Uri data, Bundle extras, String packageName, + String receiverPermission, BroadcastReceiver resultReceiver) { + Intent outIntent = new Intent(action); + PackageManager pm = context.getPackageManager(); + List receivers = pm.queryBroadcastReceivers(outIntent, 0); + if (receivers != null) + for (ResolveInfo receiver : receivers) { + if ((packageName != null) && !packageName.equals(receiver.activityInfo.applicationInfo.packageName)) + continue; + ComponentName cn = new ComponentName(receiver.activityInfo.applicationInfo.packageName, + receiver.activityInfo.name); + outIntent = new Intent(action); + if (data != null) + outIntent.setData(data); + if (extras != null) + outIntent.putExtras(extras); + outIntent.setComponent(cn); + context.sendOrderedBroadcast (outIntent, + receiverPermission, + resultReceiver, + null, // scheduler, + AndroidTransport.RESULT_INTERNAL_ERROR, // initialCode, + null, // initialData, + null); + } + } +} diff --git a/android/sdk/src/main/java/app/organicmaps/sdk/traffxml/AndroidTransport.java b/android/sdk/src/main/java/app/organicmaps/sdk/traffxml/AndroidTransport.java new file mode 100644 index 000000000..93c1f4b0c --- /dev/null +++ b/android/sdk/src/main/java/app/organicmaps/sdk/traffxml/AndroidTransport.java @@ -0,0 +1,222 @@ +/* + * Copyright © 2019–2020 traffxml.org. + * + * Relicensed to CoMaps by the original author. + */ + +package app.organicmaps.sdk.traffxml; + +public class AndroidTransport { + /** + * Intent to poll a peer for its capabilities. + * + *

This is a broadcast intent and must be sent as an explicit broadcast. + */ + public static final String ACTION_TRAFF_GET_CAPABILITIES = "org.traffxml.traff.GET_CAPABILITIES"; + + /** + * Intent to send a heartbeat to a peer. + * + *

This is a broadcast intent and must be sent as an explicit broadcast. + */ + public static final String ACTION_TRAFF_HEARTBEAT = "org.traffxml.traff.GET_HEARTBEAT"; + + /** + * Intent to poll a source for information. + * + *

This is a broadcast intent and must be sent as an explicit broadcast. + * + *

Polling is a legacy feature on Android and deprecated in TraFF 0.8 (rather than polling, TraFF 0.8 + * applications query the content provider). Therefore, poll operations are subscriptionless, and the + * source should either reply with all messages it currently holds, or ignore the request. + */ + @Deprecated + public static final String ACTION_TRAFF_POLL = "org.traffxml.traff.POLL"; + + /** + * Intent for a push feed. + * + *

This is a broadcast intent. It can be used in different forms: + * + *

As of TraFF 0.8, it must be sent as an explicit broadcast and include the + * {@link #EXTRA_SUBSCRIPTION_ID} extra. The intent data must be a URI to the content provider from which + * the messages can be retrieved. The {@link #EXTRA_FEED} extra is not supported. The feed is part of a + * subscription and will contain only changes over feeds sent previously as part of the same + * subscription. + * + *

Legacy applications omit the {@link #EXTRA_SUBSCRIPTION_ID} extra and may send it as an implicit + * broadcast. If an application supports both legacy transport and TraFF 0.8 or later, it must include + * the {@link #EXTRA_PACKAGE} extra. The feed is sent in the {@link #EXTRA_FEED} extra, as legacy + * applications may not support content providers. If sent as a response to a subscriptionless poll, the + * source should include all messages it holds, else the set of messages included is at the discretion of + * the source. + * + *

Future applications may reintroduce unsolicited push operations for certain scenarios. + */ + public static final String ACTION_TRAFF_PUSH = "org.traffxml.traff.FEED"; + + /** + * Intent for a subscription request. + * + *

This is a broadcast intent and must be sent as an explicit broadcast. + * + *

The filter list must be specified in the {@link #EXTRA_FILTER_LIST} extra. + * + *

The sender must indicate its package name in the {@link #EXTRA_PACKAGE} extra. + */ + public static final String ACTION_TRAFF_SUBSCRIBE = "org.traffxml.traff.SUBSCRIBE"; + + /** + * Intent for a subscription change request, + * + *

This is a broadcast intent and must be sent as an explicit broadcast. + * + *

This intent must have {@link #EXTRA_SUBSCRIPTION_ID} set to the ID of an existing subscription between + * the calling consumer and the source which receives the broadcast. + * + *

The new filter list must be specified in the {@link #EXTRA_FILTER_LIST} extra. + */ + public static final String ACTION_TRAFF_SUBSCRIPTION_CHANGE = "org.traffxml.traff.SUBSCRIPTION_CHANGE"; + + /** + * Intent for an unsubscribe request, + * + *

This is a broadcast intent and must be sent as an explicit broadcast. + * + *

This intent must have {@link #EXTRA_SUBSCRIPTION_ID} set to the ID of an existing subscription between + * the calling consumer and the source which receives the broadcast. It signals that the consumer is no + * longer interested in receiving messages related to that subscription, and that the source should stop + * sending updates. Unsubscribing from a nonexistent subscription is a no-op. + */ + public static final String ACTION_TRAFF_UNSUBSCRIBE = "org.traffxml.traff.UNSUBSCRIBE"; + + /** + * Name for the column which holds the message data. + */ + public static final String COLUMN_DATA = "data"; + + /** + * Schema for TraFF content URIs. + */ + public static final String CONTENT_SCHEMA = "content"; + + /** + * String representations of TraFF result codes + */ + public static final String[] ERROR_STRINGS = { + "unknown (0)", + "invalid request (1)", + "subscription rejected by the source (2)", + "requested area not covered (3)", + "requested area partially covered (4)", + "subscription ID not recognized by the source (5)", + "unknown (6)", + "source reported an internal error (7)" + }; + + /** + * Extra which contains the capabilities of the peer. + * + *

This is a String extra. It contains a {@code capabilities} XML element. + */ + public static final String EXTRA_CAPABILITIES = "capabilities"; + + /** + * Extra which contains a TraFF feed. + * + *

This is a String extra. It contains a {@code feed} XML element. + * + *

The sender should be careful to keep the size of this extra low, as Android has a 1 MByte limit on all + * pending Binder transactions. However, there is no feedback to the sender about the capacity still + * available, or whether a request exceeds that limit. Therefore, senders should keep the size if each + * feed significantly below that limit. If necessary, they should split up a feed into multiple smaller + * ones and send them with a delay in between. + * + *

This mechanism is deprecated since TraFF 0.8 and peers are no longer required to support it. Peers + * which support TraFF 0.8 must rely on content providers for message transport. + */ + @Deprecated + public static final String EXTRA_FEED = "feed"; + + /** + * Extra which contains a filter list. + * + *

This is a String extra. It contains a {@code filter_list} XML element. + */ + public static final String EXTRA_FILTER_LIST = "filter_list"; + + /** + * Extra which contains the package name of the app sending it. + * + *

This is a String extra. + */ + public static final String EXTRA_PACKAGE = "package"; + + /** + * Extra which contains a subscription ID. + * + *

This is a String extra. + */ + public static final String EXTRA_SUBSCRIPTION_ID = "subscription_id"; + + /** + * Extra which contains the timeout duration for a subscription. + * + *

This is an integer extra. + */ + public static final String EXTRA_TIMEOUT = "timeout"; + + /** + * The MIME type for TraFF content providers. + */ + public static final String MIME_TYPE_TRAFF = "vnd.android.cursor.dir/org.traffxml.message"; + + /** + * The operation completed successfully. + */ + public static final int RESULT_OK = -1; + + /** + * An internal error prevented the recipient from fulfilling the request. + */ + public static final int RESULT_INTERNAL_ERROR = 7; + + /** + * A nonexistent operation was attempted, or an operation was attempted with incomplete or otherwise + * invalid data. + */ + public static final int RESULT_INVALID = 1; + + /** + * The subscription was rejected, and no messages will be sent. + */ + public static final int RESULT_SUBSCRIPTION_REJECTED = 2; + + /** + * The subscription was rejected because the source will never provide messages matching the selection. + */ + public static final int RESULT_NOT_COVERED = 3; + + /** + * The subscription was accepted but the source can only provide messages for parts of the selection. + */ + public static final int RESULT_PARTIALLY_COVERED = 4; + + /** + * The request failed because it refers to a subscription which does not exist between the source and + * consumer involved. + */ + public static final int RESULT_SUBSCRIPTION_UNKNOWN = 5; + + /** + * The request failed because the aggregator does not accept unsolicited push requests from the sensor. + */ + public static final int RESULT_PUSH_REJECTED = 6; + + public static String formatTraffError(int code) { + if ((code < 0) || (code >= ERROR_STRINGS.length)) + return String.format("unknown (%d)", code); + else + return ERROR_STRINGS[code]; + } +} diff --git a/android/sdk/src/main/java/app/organicmaps/sdk/traffxml/SourceImpl.java b/android/sdk/src/main/java/app/organicmaps/sdk/traffxml/SourceImpl.java new file mode 100644 index 000000000..6fa6a18c4 --- /dev/null +++ b/android/sdk/src/main/java/app/organicmaps/sdk/traffxml/SourceImpl.java @@ -0,0 +1,70 @@ +package app.organicmaps.sdk.traffxml; + +import android.content.BroadcastReceiver; +import android.content.Context; + +/** + * Abstract superclass for TraFF source implementations. + */ +public abstract class SourceImpl extends BroadcastReceiver +{ + /** + * Creates a new instance. + * + * @param context The application context + */ + public SourceImpl(Context context, long nativeManager) + { + super(); + this.context = context; + this.nativeManager = nativeManager; + } + + protected Context context; + + /** + * The native `TraffSourceManager` instance. + */ + protected long nativeManager; + + /** + * Subscribes to a traffic source. + * + * @param filterList The filter list in XML format + */ + public abstract void subscribe(String filterList); + + /** + * Changes an existing traffic subscription. + * + * @param filterList The filter list in XML format + */ + public abstract void changeSubscription(String filterList); + + /** + * Unsubscribes from a traffic source we are subscribed to. + */ + public abstract void unsubscribe(); + + /** + * Forwards a newly received TraFF feed to the traffic module for processing. + * + * Called when a TraFF feed is received. This is a wrapper around {@link #onFeedReceivedImpl(long, String)}. + * + * @param feed The TraFF feed + */ + protected void onFeedReceived(String feed) + { + onFeedReceivedImpl(nativeManager, feed); + } + + /** + * Forwards a newly received TraFF feed to the traffic module for processing. + * + * Called when a TraFF feed is received. + * + * @param nativeManager The native `TraffSourceManager` instance + * @param feed The TraFF feed + */ + protected static native void onFeedReceivedImpl(long nativeManager, String feed); +} diff --git a/android/sdk/src/main/java/app/organicmaps/sdk/traffxml/SourceImplV0_7.java b/android/sdk/src/main/java/app/organicmaps/sdk/traffxml/SourceImplV0_7.java new file mode 100644 index 000000000..cff6d0d75 --- /dev/null +++ b/android/sdk/src/main/java/app/organicmaps/sdk/traffxml/SourceImplV0_7.java @@ -0,0 +1,127 @@ +package app.organicmaps.sdk.traffxml; + +import java.util.ArrayList; +import java.util.List; + +import android.Manifest; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import app.organicmaps.sdk.util.log.Logger; + +/** + * Implementation for a TraFF 0.7 source. + */ +public class SourceImplV0_7 extends SourceImpl +{ + private PackageManager pm; + + /** + * Creates a new instance. + * + * @param context The application context + */ + public SourceImplV0_7(Context context, long nativeManager) + { + super(context, nativeManager); + // TODO Auto-generated constructor stub + } + + /** + * Subscribes to a traffic source. + * + * @param filterList The filter list in XML format + */ + @Override + public void subscribe(String filterList) + { + IntentFilter traffFilter07 = new IntentFilter(); + traffFilter07.addAction(AndroidTransport.ACTION_TRAFF_PUSH); + + this.context.registerReceiver(this, traffFilter07); + + // Broadcast a poll intent to all TraFF 0.7-only receivers + Intent outIntent = new Intent(AndroidTransport.ACTION_TRAFF_POLL); + pm = this.context.getPackageManager(); + List receivers07 = pm.queryBroadcastReceivers(outIntent, 0); + List receivers08 = pm.queryBroadcastReceivers(new Intent(AndroidTransport.ACTION_TRAFF_GET_CAPABILITIES), 0); + if (receivers07 != null) + { + /* + * Get receivers which support only TraFF 0.7 and poll them. + * If there are no TraFF 0.7 sources at the moment, we register the receiver nonetheless. + * That way, if any new sources are added during the session, we get any messages they send. + */ + if (receivers08 != null) + receivers07.removeAll(receivers08); + for (ResolveInfo receiver : receivers07) + { + ComponentName cn = new ComponentName(receiver.activityInfo.applicationInfo.packageName, + receiver.activityInfo.name); + outIntent = new Intent(AndroidTransport.ACTION_TRAFF_POLL); + outIntent.setComponent(cn); + this.context.sendBroadcast(outIntent, Manifest.permission.ACCESS_COARSE_LOCATION); + } + } + } + + /** + * Changes an existing traffic subscription. + * + * This implementation does nothing, as TraFF 0.7 does not support subscriptions. + * + * @param filterList The filter list in XML format + */ + @Override + public void changeSubscription(String filterList) + { + // NOP + } + + /** + * Unsubscribes from a traffic source we are subscribed to. + */ + @Override + public void unsubscribe() + { + this.context.unregisterReceiver(this); + } + + @Override + public void onReceive(Context context, Intent intent) + { + if (intent == null) + return; + + if (intent.getAction().equals(AndroidTransport.ACTION_TRAFF_PUSH)) + { + /* 0.7 feed */ + String packageName = intent.getStringExtra(AndroidTransport.EXTRA_PACKAGE); + /* + * If the feed comes from a TraFF 0.8+ source, skip it (this may happen with “bilingual” + * TraFF 0.7/0.8 sources). That ensures the only way to get information from such sources is + * through a TraFF 0.8 subscription. Fetching the list from scratch each time ensures that + * apps installed during runtime get considered.) + */ + if (packageName != null) + { + for (ResolveInfo info : pm.queryBroadcastReceivers(new Intent(AndroidTransport.ACTION_TRAFF_GET_CAPABILITIES), 0)) + if (packageName.equals(info.resolvePackageName)) + return; + } + String feed = intent.getStringExtra(AndroidTransport.EXTRA_FEED); + if (feed == null) + { + Logger.w(this.getClass().getSimpleName(), "empty feed, ignoring"); + } + else + { + onFeedReceived(feed); + } + } + } + +} diff --git a/android/sdk/src/main/java/app/organicmaps/sdk/traffxml/SourceImplV0_8.java b/android/sdk/src/main/java/app/organicmaps/sdk/traffxml/SourceImplV0_8.java new file mode 100644 index 000000000..2518525ea --- /dev/null +++ b/android/sdk/src/main/java/app/organicmaps/sdk/traffxml/SourceImplV0_8.java @@ -0,0 +1,240 @@ +package app.organicmaps.sdk.traffxml; + +import android.Manifest; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.IntentFilter.MalformedMimeTypeException; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import app.organicmaps.sdk.util.log.Logger; + +/** + * Implementation for a TraFF 0.8 source. + */ +public class SourceImplV0_8 extends SourceImpl +{ + + private String packageName; + private String subscriptionId = null; + + /** + * Creates a new instance. + * + * @param context The application context + * @param packageName The package name for the source + */ + public SourceImplV0_8(Context context, long nativeManager, String packageName) + { + super(context, nativeManager); + this.packageName = packageName; + } + + /** + * Subscribes to a traffic source. + * + * @param filterList The filter list in XML format + */ + @Override + public void subscribe(String filterList) + { + IntentFilter filter = new IntentFilter(); + filter.addAction(AndroidTransport.ACTION_TRAFF_PUSH); + filter.addDataScheme(AndroidTransport.CONTENT_SCHEMA); + try + { + filter.addDataType(AndroidTransport.MIME_TYPE_TRAFF); + } + catch (MalformedMimeTypeException e) + { + // as long as the constant is a well-formed MIME type, this exception never gets thrown + // TODO revisit logging + e.printStackTrace(); + } + + context.registerReceiver(this, filter); + + Bundle extras = new Bundle(); + extras.putString(AndroidTransport.EXTRA_PACKAGE, context.getPackageName()); + extras.putString(AndroidTransport.EXTRA_FILTER_LIST, filterList); + AndroidConsumer.sendTraffIntent(context, AndroidTransport.ACTION_TRAFF_SUBSCRIBE, null, + extras, packageName, Manifest.permission.ACCESS_COARSE_LOCATION, this); + } + + /** + * Changes an existing traffic subscription. + * + * @param filterList The filter list in XML format + */ + @Override + public void changeSubscription(String filterList) + { + Bundle extras = new Bundle(); + extras.putString(AndroidTransport.EXTRA_SUBSCRIPTION_ID, subscriptionId); + extras.putString(AndroidTransport.EXTRA_FILTER_LIST, filterList); + AndroidConsumer.sendTraffIntent(context, AndroidTransport.ACTION_TRAFF_SUBSCRIPTION_CHANGE, null, + extras, packageName, Manifest.permission.ACCESS_COARSE_LOCATION, this); + } + + /** + * Unsubscribes from a traffic source we are subscribed to. + */ + @Override + public void unsubscribe() + { + Bundle extras = new Bundle(); + extras.putString(AndroidTransport.EXTRA_SUBSCRIPTION_ID, subscriptionId); + AndroidConsumer.sendTraffIntent(this.context, AndroidTransport.ACTION_TRAFF_UNSUBSCRIBE, null, + extras, packageName, Manifest.permission.ACCESS_COARSE_LOCATION, this); + + this.context.unregisterReceiver(this); + } + + @Override + public void onReceive(Context context, Intent intent) + { + if (intent == null) + return; + + if (intent.getAction().equals(AndroidTransport.ACTION_TRAFF_PUSH)) + { + Uri uri = intent.getData(); + if (uri != null) + { + /* 0.8 feed */ + String subscriptionId = intent.getStringExtra(AndroidTransport.EXTRA_SUBSCRIPTION_ID); + if (subscriptionId.equals(this.subscriptionId)) + fetchMessages(context, uri); + } + else + { + Logger.w(this.getClass().getSimpleName(), "no URI in feed, ignoring"); + } // uri != null + } else if (intent.getAction().equals(AndroidTransport.ACTION_TRAFF_SUBSCRIBE)) { + if (this.getResultCode() != AndroidTransport.RESULT_OK) { + Bundle extras = this.getResultExtras(true); + if (extras != null) + Logger.e(this.getClass().getSimpleName(), String.format("subscription to %s failed, %s", + extras.getString(AndroidTransport.EXTRA_PACKAGE), AndroidTransport.formatTraffError(this.getResultCode()))); + else + Logger.e(this.getClass().getSimpleName(), String.format("subscription failed, %s", + AndroidTransport.formatTraffError(this.getResultCode()))); + if (this.getResultCode() == AndroidTransport.RESULT_INTERNAL_ERROR) + Logger.e(this.getClass().getSimpleName(), "Make sure the TraFF source app has at least coarse location permission, even when running in background"); + return; + } + Bundle extras = this.getResultExtras(true); + String data = this.getResultData(); + String packageName = extras.getString(AndroidTransport.EXTRA_PACKAGE); + if (!this.packageName.equals(packageName)) + return; + String subscriptionId = extras.getString(AndroidTransport.EXTRA_SUBSCRIPTION_ID); + if (subscriptionId == null) { + Logger.e(this.getClass().getSimpleName(), + String.format("subscription to %s failed: no subscription ID returned", packageName)); + return; + } else if (packageName == null) { + Logger.e(this.getClass().getSimpleName(), "subscription failed: no package name"); + return; + } else if (data == null) { + Logger.w(this.getClass().getSimpleName(), + String.format("subscription to %s successful (ID: %s) but no content URI was supplied. " + + "This is an issue with the source and may result in delayed message retrieval.", + packageName, subscriptionId)); + this.subscriptionId = subscriptionId; + return; + } + Logger.d(this.getClass().getSimpleName(), + "subscription to " + packageName + " successful, ID: " + subscriptionId); + this.subscriptionId = subscriptionId; + fetchMessages(context, Uri.parse(data)); + } else if (intent.getAction().equals(AndroidTransport.ACTION_TRAFF_SUBSCRIPTION_CHANGE)) { + if (this.getResultCode() != AndroidTransport.RESULT_OK) { + Bundle extras = this.getResultExtras(true); + if (extras != null) + Logger.e(this.getClass().getSimpleName(), + String.format("subscription change for %s failed: %s", + extras.getString(AndroidTransport.EXTRA_SUBSCRIPTION_ID), + AndroidTransport.formatTraffError(this.getResultCode()))); + else + Logger.e(this.getClass().getSimpleName(), + String.format("subscription change failed: %s", + AndroidTransport.formatTraffError(this.getResultCode()))); + return; + } + Bundle extras = intent.getExtras(); + String data = this.getResultData(); + String subscriptionId = extras.getString(AndroidTransport.EXTRA_SUBSCRIPTION_ID); + if (subscriptionId == null) { + Logger.w(this.getClass().getSimpleName(), + "subscription change successful but the source did not specify the subscription ID. " + + "This is an issue with the source and may result in delayed message retrieval. " + + "URI: " + data); + return; + } else if (!subscriptionId.equals(this.subscriptionId)) { + return; + } else if (data == null) { + Logger.w(this.getClass().getSimpleName(), + String.format("subscription change for %s successful but no content URI was supplied. " + + "This is an issue with the source and may result in delayed message retrieval.", + subscriptionId)); + return; + } + Logger.d(this.getClass().getSimpleName(), + "subscription change for " + subscriptionId + " successful"); + fetchMessages(context, Uri.parse(data)); + } else if (intent.getAction().equals(AndroidTransport.ACTION_TRAFF_UNSUBSCRIBE)) { + String subscriptionId = intent.getStringExtra(AndroidTransport.EXTRA_SUBSCRIPTION_ID); + if (subscriptionId.equals(this.subscriptionId)) + this.subscriptionId = null; + // TODO is there anything to do here? (Comment below is from Navit) + /* + * If we ever unsubscribe for reasons other than that we are shutting down or got a feed for + * a subscription we don’t recognize, or if we start keeping a persistent list of + * subscriptions, we need to delete the subscription from our list. Until then, there is + * nothing to do here: either the subscription isn’t in the list, or we are about to shut + * down and the whole list is about to get discarded. + */ + } else if (intent.getAction().equals(AndroidTransport.ACTION_TRAFF_HEARTBEAT)) { + String subscriptionId = intent.getStringExtra(AndroidTransport.EXTRA_SUBSCRIPTION_ID); + if (subscriptionId.equals(this.subscriptionId)) { + Logger.d(this.getClass().getSimpleName(), + String.format("got a heartbeat from %s for subscription %s; sending result", + intent.getStringExtra(AndroidTransport.EXTRA_PACKAGE), subscriptionId)); + this.setResult(AndroidTransport.RESULT_OK, null, null); + } + } // intent.getAction() + // TODO Auto-generated method stub + + } + + /** + * Fetches TraFF messages from a content provider. + * + * @param context The context to use for the content resolver + * @param uri The content provider URI + */ + private void fetchMessages(Context context, Uri uri) { + try { + Cursor cursor = context.getContentResolver().query(uri, new String[] {AndroidTransport.COLUMN_DATA}, null, null, null); + if (cursor == null) + return; + if (cursor.getCount() < 1) { + cursor.close(); + return; + } + StringBuilder builder = new StringBuilder("\n"); + while (cursor.moveToNext()) + builder.append(cursor.getString(cursor.getColumnIndex(AndroidTransport.COLUMN_DATA))).append("\n"); + builder.append(""); + cursor.close(); + onFeedReceived(builder.toString()); + } catch (Exception e) { + Logger.w(this.getClass().getSimpleName(), + String.format("Unable to fetch messages from %s", uri.toString()), e); + e.printStackTrace(); + } + } + +} diff --git a/android/sdk/src/main/java/app/organicmaps/sdk/traffxml/Version.java b/android/sdk/src/main/java/app/organicmaps/sdk/traffxml/Version.java new file mode 100644 index 000000000..c2603b2ec --- /dev/null +++ b/android/sdk/src/main/java/app/organicmaps/sdk/traffxml/Version.java @@ -0,0 +1,18 @@ +/* + * Copyright © 2019–2020 traffxml.org. + * + * Relicensed to CoMaps by the original author. + */ + +package app.organicmaps.sdk.traffxml; + +/** + * Constants for versions. + */ +public class Version { + /** Version 0.7: introduced transport on Android. */ + public static final int V0_7 = 7; + + /** Version 0.8: introduced subscriptions and HTTP transport. */ + public static final int V0_8 = 8; +} diff --git a/android/sdk/src/main/java/app/organicmaps/sdk/util/Config.java b/android/sdk/src/main/java/app/organicmaps/sdk/util/Config.java index bc06fce5c..b6f7f0b4e 100644 --- a/android/sdk/src/main/java/app/organicmaps/sdk/util/Config.java +++ b/android/sdk/src/main/java/app/organicmaps/sdk/util/Config.java @@ -70,6 +70,16 @@ public final class Config * True if the first start animation has been seen. */ private static final String KEY_MISC_FIRST_START_DIALOG_SEEN = "FirstStartDialogSeen"; + + /** + * Whether feeds from legacy TraFF applications (TraFF 0.7, Android transport) are enabled. + */ + private static final String KEY_TRAFFIC_LEGACY_ENABLED = "TrafficLegacyEnabled"; + + /** + * TraFF (0.8+) applications from which to request traffic data. + */ + private static final String KEY_TRAFFIC_APPS = "TrafficApps"; private Config() {} @@ -403,6 +413,63 @@ public static void setTransliteration(boolean value) nativeSetTransliteration(value); } + public static boolean getTrafficHttpEnabled() + { + return nativeGetTrafficHttpEnabled(); + } + + public static void setTrafficHttpEnabled(boolean value) + { + nativeSetTrafficHttpEnabled(value); + } + + public static String getTrafficHttpUrl() + { + return nativeGetTrafficHttpUrl(); + } + + public static void setTrafficHttpUrl(String value) + { + nativeSetTrafficHttpUrl(value); + } + + public static String[] getTrafficApps() + { + String appString = getString(KEY_TRAFFIC_APPS, ""); + if (appString.length() == 0) + return new String[0]; + return appString.split(","); + } + + public static void setTrafficApps(String[] value) + { + String valueString = ""; + for (int i = 0; i < value.length; i++) + { + valueString = valueString + value[i]; + if ((i + 1) < value.length) + valueString = valueString + ","; + } + setString(KEY_TRAFFIC_APPS, valueString); + applyTrafficApps(value); + } + + public static boolean getTrafficLegacyEnabled() + { + return getBool(KEY_TRAFFIC_LEGACY_ENABLED, false); + } + + public static void setTrafficLegacyEnabled(boolean value) + { + setBool(KEY_TRAFFIC_LEGACY_ENABLED, value); + applyTrafficLegacyEnabled(value); + } + + public static boolean isNY() + { + return getBool("NY"); + } + @NonNull public static String getDonateUrl() { @@ -548,4 +615,10 @@ public static void setAnnounceStreets(boolean enabled) private static native void nativeSetAlternativeMapLanguageHandling(int value); private static native boolean nativeGetTransliteration(); private static native void nativeSetTransliteration(boolean value); + private static native boolean nativeGetTrafficHttpEnabled(); + private static native void nativeSetTrafficHttpEnabled(boolean value); + private static native String nativeGetTrafficHttpUrl(); + private static native void nativeSetTrafficHttpUrl(String value); + private static native void applyTrafficApps(String[] value); + private static native void applyTrafficLegacyEnabled(boolean value); } diff --git a/android/sdk/src/main/res/values-af/types_strings.xml b/android/sdk/src/main/res/values-af/types_strings.xml index 7a5cf0ee5..4cc8bfa49 100644 --- a/android/sdk/src/main/res/values-af/types_strings.xml +++ b/android/sdk/src/main/res/values-af/types_strings.xml @@ -359,7 +359,24 @@ Tonnel Bushalte + Pad in aanbou + Pad in aanbou + Pad in aanbou + Pad in aanbou + Pad in aanbou + Pad in aanbou + Pad in aanbou + Pad in aanbou + Pad in aanbou + Pad in aanbou + Pad in aanbou + Pad in aanbou + Pad in aanbou + Pad in aanbou + Pad in aanbou + Pad in aanbou + Pad in aanbou Fietspad Brug diff --git a/android/sdk/src/main/res/values-ar/types_strings.xml b/android/sdk/src/main/res/values-ar/types_strings.xml index 099e41382..9696b951d 100644 --- a/android/sdk/src/main/res/values-ar/types_strings.xml +++ b/android/sdk/src/main/res/values-ar/types_strings.xml @@ -380,7 +380,24 @@ نفق موقف حافلات + طريق تحت الإنشاء + طريق تحت الإنشاء + طريق تحت الإنشاء + طريق تحت الإنشاء + طريق تحت الإنشاء + طريق تحت الإنشاء + طريق تحت الإنشاء + طريق تحت الإنشاء + طريق تحت الإنشاء + طريق تحت الإنشاء + طريق تحت الإنشاء + طريق تحت الإنشاء + طريق تحت الإنشاء + طريق تحت الإنشاء + طريق تحت الإنشاء + طريق تحت الإنشاء + طريق تحت الإنشاء مسار للدراجات جسر diff --git a/android/sdk/src/main/res/values-az/types_strings.xml b/android/sdk/src/main/res/values-az/types_strings.xml index fd4f9fed8..775844b63 100644 --- a/android/sdk/src/main/res/values-az/types_strings.xml +++ b/android/sdk/src/main/res/values-az/types_strings.xml @@ -370,7 +370,24 @@ Tunel Avtobus dayanacağı + Tikili Yol + Tikili Yol + Tikili Yol + Tikili Yol + Tikili Yol + Tikili Yol + Tikili Yol + Tikili Yol + Tikili Yol + Tikili Yol + Tikili Yol + Tikili Yol + Tikili Yol + Tikili Yol + Tikili Yol + Tikili Yol + Tikili Yol Velosiped yolu Körpü diff --git a/android/sdk/src/main/res/values-bg/types_strings.xml b/android/sdk/src/main/res/values-bg/types_strings.xml index 06fc1e3ed..da11056df 100644 --- a/android/sdk/src/main/res/values-bg/types_strings.xml +++ b/android/sdk/src/main/res/values-bg/types_strings.xml @@ -956,7 +956,24 @@ Виетнамска Пътека за юздечки Пътека за юздечки + Път в процес на изграждане + Път в процес на изграждане + Път в процес на изграждане + Път в процес на изграждане + Път в процес на изграждане + Път в процес на изграждане + Път в процес на изграждане + Път в процес на изграждане + Път в процес на изграждане + Път в процес на изграждане + Път в процес на изграждане + Път в процес на изграждане + Път в процес на изграждане + Път в процес на изграждане + Път в процес на изграждане + Път в процес на изграждане + Път в процес на изграждане Жилищна улица Пътека Пътека diff --git a/android/sdk/src/main/res/values-ca/types_strings.xml b/android/sdk/src/main/res/values-ca/types_strings.xml index 8d4f131a0..9c5b751df 100644 --- a/android/sdk/src/main/res/values-ca/types_strings.xml +++ b/android/sdk/src/main/res/values-ca/types_strings.xml @@ -935,7 +935,24 @@ Centre comercial Desfibril·lador Carretera + Via en construcció + Via en construcció + Via en construcció + Via en construcció + Via en construcció + Via en construcció + Via en construcció + Via en construcció + Via en construcció + Via en construcció + Via en construcció + Via en construcció + Via en construcció + Via en construcció + Via en construcció + Via en construcció + Via en construcció Continent Quiosc diff --git a/android/sdk/src/main/res/values-cs/types_strings.xml b/android/sdk/src/main/res/values-cs/types_strings.xml index 11a843f26..1127305f3 100644 --- a/android/sdk/src/main/res/values-cs/types_strings.xml +++ b/android/sdk/src/main/res/values-cs/types_strings.xml @@ -327,7 +327,24 @@ Tunel Autobusová zastávka + Silnice v rekonstrukci + Silnice v rekonstrukci + Silnice v rekonstrukci + Silnice v rekonstrukci + Silnice v rekonstrukci + Silnice v rekonstrukci + Silnice v rekonstrukci + Silnice v rekonstrukci + Silnice v rekonstrukci + Silnice v rekonstrukci + Silnice v rekonstrukci + Silnice v rekonstrukci + Silnice v rekonstrukci + Silnice v rekonstrukci + Silnice v rekonstrukci + Silnice v rekonstrukci + Silnice v rekonstrukci Most diff --git a/android/sdk/src/main/res/values-da/types_strings.xml b/android/sdk/src/main/res/values-da/types_strings.xml index 5176aca32..3bd536cd2 100644 --- a/android/sdk/src/main/res/values-da/types_strings.xml +++ b/android/sdk/src/main/res/values-da/types_strings.xml @@ -317,7 +317,24 @@ Tunnel Busstoppested + Vej under anlæggelse + Vej under anlæggelse + Vej under anlæggelse + Vej under anlæggelse + Vej under anlæggelse + Vej under anlæggelse + Vej under anlæggelse + Vej under anlæggelse + Vej under anlæggelse + Vej under anlæggelse + Vej under anlæggelse + Vej under anlæggelse + Vej under anlæggelse + Vej under anlæggelse + Vej under anlæggelse + Vej under anlæggelse + Vej under anlæggelse Bro diff --git a/android/sdk/src/main/res/values-de/types_strings.xml b/android/sdk/src/main/res/values-de/types_strings.xml index f8238a939..aef37da73 100644 --- a/android/sdk/src/main/res/values-de/types_strings.xml +++ b/android/sdk/src/main/res/values-de/types_strings.xml @@ -417,7 +417,24 @@ Tunnel Bushaltestelle + Straße im Bau + Straße im Bau + Straße im Bau + Straße im Bau + Straße im Bau + Straße im Bau + Straße im Bau + Straße im Bau + Straße im Bau + Straße im Bau + Straße im Bau + Straße im Bau + Straße im Bau + Straße im Bau + Straße im Bau + Straße im Bau + Straße im Bau Radweg Brücke diff --git a/android/sdk/src/main/res/values-el/types_strings.xml b/android/sdk/src/main/res/values-el/types_strings.xml index 9ec8dcba5..3657c3d9e 100644 --- a/android/sdk/src/main/res/values-el/types_strings.xml +++ b/android/sdk/src/main/res/values-el/types_strings.xml @@ -335,7 +335,24 @@ Σήραγγα Στάση λεωφορείου + Οδός υπό κατασκευή + Οδός υπό κατασκευή + Οδός υπό κατασκευή + Οδός υπό κατασκευή + Οδός υπό κατασκευή + Οδός υπό κατασκευή + Οδός υπό κατασκευή + Οδός υπό κατασκευή + Οδός υπό κατασκευή + Οδός υπό κατασκευή + Οδός υπό κατασκευή + Οδός υπό κατασκευή + Οδός υπό κατασκευή + Οδός υπό κατασκευή + Οδός υπό κατασκευή + Οδός υπό κατασκευή + Οδός υπό κατασκευή Γέφυρα diff --git a/android/sdk/src/main/res/values-es-rMX/types_strings.xml b/android/sdk/src/main/res/values-es-rMX/types_strings.xml index fb4916e55..ef0f8710d 100644 --- a/android/sdk/src/main/res/values-es-rMX/types_strings.xml +++ b/android/sdk/src/main/res/values-es-rMX/types_strings.xml @@ -57,7 +57,24 @@ Puente Túnel + Vía en construcción + Vía en construcción + Vía en construcción + Vía en construcción + Vía en construcción + Vía en construcción + Vía en construcción + Vía en construcción + Vía en construcción + Vía en construcción + Vía en construcción + Vía en construcción + Vía en construcción + Vía en construcción + Vía en construcción + Vía en construcción + Vía en construcción Puente diff --git a/android/sdk/src/main/res/values-es/types_strings.xml b/android/sdk/src/main/res/values-es/types_strings.xml index 5bf8a01c6..eed7b763d 100644 --- a/android/sdk/src/main/res/values-es/types_strings.xml +++ b/android/sdk/src/main/res/values-es/types_strings.xml @@ -385,7 +385,24 @@ Túnel Parada de bus + Vía en construcción + Vía en construcción + Vía en construcción + Vía en construcción + Vía en construcción + Vía en construcción + Vía en construcción + Vía en construcción + Vía en construcción + Vía en construcción + Vía en construcción + Vía en construcción + Vía en construcción + Vía en construcción + Vía en construcción + Vía en construcción + Vía en construcción Ciclovía Puente diff --git a/android/sdk/src/main/res/values-et/types_strings.xml b/android/sdk/src/main/res/values-et/types_strings.xml index 07deaa4b5..c5803de42 100644 --- a/android/sdk/src/main/res/values-et/types_strings.xml +++ b/android/sdk/src/main/res/values-et/types_strings.xml @@ -382,7 +382,24 @@ Tunnel Bussipeatus + Ehitusjärgus tee + Ehitusjärgus tee + Ehitusjärgus tee + Ehitusjärgus tee + Ehitusjärgus tee + Ehitusjärgus tee + Ehitusjärgus tee + Ehitusjärgus tee + Ehitusjärgus tee + Ehitusjärgus tee + Ehitusjärgus tee + Ehitusjärgus tee + Ehitusjärgus tee + Ehitusjärgus tee + Ehitusjärgus tee + Ehitusjärgus tee + Ehitusjärgus tee Jalgrattatee Jalgrattasild diff --git a/android/sdk/src/main/res/values-eu/types_strings.xml b/android/sdk/src/main/res/values-eu/types_strings.xml index ceb4e4263..e112c8a39 100644 --- a/android/sdk/src/main/res/values-eu/types_strings.xml +++ b/android/sdk/src/main/res/values-eu/types_strings.xml @@ -373,7 +373,24 @@ Tunela Autobus geltokia + Eraikitzen ari diren errepidea + Eraikitzen ari diren errepidea + Eraikitzen ari diren errepidea + Eraikitzen ari diren errepidea + Eraikitzen ari diren errepidea + Eraikitzen ari diren errepidea + Eraikitzen ari diren errepidea + Eraikitzen ari diren errepidea + Eraikitzen ari diren errepidea + Eraikitzen ari diren errepidea + Eraikitzen ari diren errepidea + Eraikitzen ari diren errepidea + Eraikitzen ari diren errepidea + Eraikitzen ari diren errepidea + Eraikitzen ari diren errepidea + Eraikitzen ari diren errepidea + Eraikitzen ari diren errepidea Bidegorri edo bizikleta-bidea Zubia diff --git a/android/sdk/src/main/res/values-fa/types_strings.xml b/android/sdk/src/main/res/values-fa/types_strings.xml index 5ff2035ee..656cf590e 100644 --- a/android/sdk/src/main/res/values-fa/types_strings.xml +++ b/android/sdk/src/main/res/values-fa/types_strings.xml @@ -244,7 +244,24 @@ تونل حمل و نقل + جاده در دست ساخت است + جاده در دست ساخت است + جاده در دست ساخت است + جاده در دست ساخت است + جاده در دست ساخت است + جاده در دست ساخت است + جاده در دست ساخت است + جاده در دست ساخت است + جاده در دست ساخت است + جاده در دست ساخت است + جاده در دست ساخت است + جاده در دست ساخت است + جاده در دست ساخت است + جاده در دست ساخت است + جاده در دست ساخت است + جاده در دست ساخت است + جاده در دست ساخت است پل diff --git a/android/sdk/src/main/res/values-fi/types_strings.xml b/android/sdk/src/main/res/values-fi/types_strings.xml index e0fa7245b..eb775e21d 100644 --- a/android/sdk/src/main/res/values-fi/types_strings.xml +++ b/android/sdk/src/main/res/values-fi/types_strings.xml @@ -375,7 +375,24 @@ Tunneli Bussipysäkki + Rakenteilla oleva tie + Rakenteilla oleva tie + Rakenteilla oleva tie + Rakenteilla oleva tie + Rakenteilla oleva tie + Rakenteilla oleva tie + Rakenteilla oleva tie + Rakenteilla oleva tie + Rakenteilla oleva tie + Rakenteilla oleva tie + Rakenteilla oleva tie + Rakenteilla oleva tie + Rakenteilla oleva tie + Rakenteilla oleva tie + Rakenteilla oleva tie + Rakenteilla oleva tie + Rakenteilla oleva tie Pyörätie Silta diff --git a/android/sdk/src/main/res/values-fr/types_strings.xml b/android/sdk/src/main/res/values-fr/types_strings.xml index 94f7d1c03..d1d93e878 100644 --- a/android/sdk/src/main/res/values-fr/types_strings.xml +++ b/android/sdk/src/main/res/values-fr/types_strings.xml @@ -370,7 +370,24 @@ Tunnel Arrêt de bus + Route en construction + Route en construction + Route en construction + Route en construction + Route en construction + Route en construction + Route en construction + Route en construction + Route en construction + Route en construction + Route en construction + Route en construction + Route en construction + Route en construction + Route en construction + Route en construction + Route en construction Piste cyclable Pont diff --git a/android/sdk/src/main/res/values-gl/types_strings.xml b/android/sdk/src/main/res/values-gl/types_strings.xml index a07fca348..7bed81337 100644 --- a/android/sdk/src/main/res/values-gl/types_strings.xml +++ b/android/sdk/src/main/res/values-gl/types_strings.xml @@ -367,7 +367,24 @@ Puente Túnel Parada de autobús + Vía en construción + Vía en construción + Vía en construción + Vía en construción + Vía en construción + Vía en construción + Vía en construción + Vía en construción + Vía en construción + Vía en construción + Vía en construción + Vía en construción + Vía en construción + Vía en construción + Vía en construción + Vía en construción + Vía en construción Ciclovía Puente Ciclovía diff --git a/android/sdk/src/main/res/values-gsw/types_strings.xml b/android/sdk/src/main/res/values-gsw/types_strings.xml index 3ea62239b..04c5a508d 100644 --- a/android/sdk/src/main/res/values-gsw/types_strings.xml +++ b/android/sdk/src/main/res/values-gsw/types_strings.xml @@ -333,7 +333,24 @@ Brugg Tunnel Bushaltistell + Strass im Bau + Strass im Bau + Strass im Bau + Strass im Bau + Strass im Bau + Strass im Bau + Strass im Bau + Strass im Bau + Strass im Bau + Strass im Bau + Strass im Bau + Strass im Bau + Strass im Bau + Strass im Bau + Strass im Bau + Strass im Bau + Strass im Bau Veloweg Brugg Veloweg diff --git a/android/sdk/src/main/res/values-hi/types_strings.xml b/android/sdk/src/main/res/values-hi/types_strings.xml index 9cc4513fd..055d40fe6 100644 --- a/android/sdk/src/main/res/values-hi/types_strings.xml +++ b/android/sdk/src/main/res/values-hi/types_strings.xml @@ -263,7 +263,24 @@ राजमार्ग बस स्टॉप + निर्माणाधीन सड़क + निर्माणाधीन सड़क + निर्माणाधीन सड़क + निर्माणाधीन सड़क + निर्माणाधीन सड़क + निर्माणाधीन सड़क + निर्माणाधीन सड़क + निर्माणाधीन सड़क + निर्माणाधीन सड़क + निर्माणाधीन सड़क + निर्माणाधीन सड़क + निर्माणाधीन सड़क + निर्माणाधीन सड़क + निर्माणाधीन सड़क + निर्माणाधीन सड़क + निर्माणाधीन सड़क + निर्माणाधीन सड़क साइकिल मार्ग साइकिल मार्ग उत्थापक diff --git a/android/sdk/src/main/res/values-hu/types_strings.xml b/android/sdk/src/main/res/values-hu/types_strings.xml index ecdcc5f3f..b194a1251 100644 --- a/android/sdk/src/main/res/values-hu/types_strings.xml +++ b/android/sdk/src/main/res/values-hu/types_strings.xml @@ -329,7 +329,24 @@ Alagút Buszmegálló + Útépítés + Útépítés + Útépítés + Útépítés + Útépítés + Útépítés + Útépítés + Útépítés + Útépítés + Útépítés + Útépítés + Útépítés + Útépítés + Útépítés + Útépítés + Útépítés + Útépítés Híd diff --git a/android/sdk/src/main/res/values-in/types_strings.xml b/android/sdk/src/main/res/values-in/types_strings.xml index ca7c5bc74..7acde82a3 100644 --- a/android/sdk/src/main/res/values-in/types_strings.xml +++ b/android/sdk/src/main/res/values-in/types_strings.xml @@ -323,7 +323,24 @@ Terowongan Halte bus + Jalan sedang dibangun + Jalan sedang dibangun + Jalan sedang dibangun + Jalan sedang dibangun + Jalan sedang dibangun + Jalan sedang dibangun + Jalan sedang dibangun + Jalan sedang dibangun + Jalan sedang dibangun + Jalan sedang dibangun + Jalan sedang dibangun + Jalan sedang dibangun + Jalan sedang dibangun + Jalan sedang dibangun + Jalan sedang dibangun + Jalan sedang dibangun + Jalan sedang dibangun Menjembatani diff --git a/android/sdk/src/main/res/values-is/types_strings.xml b/android/sdk/src/main/res/values-is/types_strings.xml index 0ce70b142..40860f438 100644 --- a/android/sdk/src/main/res/values-is/types_strings.xml +++ b/android/sdk/src/main/res/values-is/types_strings.xml @@ -1046,7 +1046,24 @@ Snjóleikjagarður Þráðlaust net Setskýli + Vegur í byggingu + Vegur í byggingu + Vegur í byggingu + Vegur í byggingu + Vegur í byggingu + Vegur í byggingu + Vegur í byggingu + Vegur í byggingu + Vegur í byggingu + Vegur í byggingu + Vegur í byggingu + Vegur í byggingu + Vegur í byggingu + Vegur í byggingu + Vegur í byggingu + Vegur í byggingu + Vegur í byggingu Svæði fyrir fótgangandi Svæði fyrir fótgangandi Sögulegur skriðdreki diff --git a/android/sdk/src/main/res/values-it/types_strings.xml b/android/sdk/src/main/res/values-it/types_strings.xml index 25a08eeca..1cb54d3cf 100644 --- a/android/sdk/src/main/res/values-it/types_strings.xml +++ b/android/sdk/src/main/res/values-it/types_strings.xml @@ -382,7 +382,24 @@ Galleria Fermata dell\'autobus + Strada in costruzione + Strada in costruzione + Strada in costruzione + Strada in costruzione + Strada in costruzione + Strada in costruzione + Strada in costruzione + Strada in costruzione + Strada in costruzione + Strada in costruzione + Strada in costruzione + Strada in costruzione + Strada in costruzione + Strada in costruzione + Strada in costruzione + Strada in costruzione + Strada in costruzione Pista ciclabile Ponte diff --git a/android/sdk/src/main/res/values-iw/types_strings.xml b/android/sdk/src/main/res/values-iw/types_strings.xml index d46c06469..d9f68278f 100644 --- a/android/sdk/src/main/res/values-iw/types_strings.xml +++ b/android/sdk/src/main/res/values-iw/types_strings.xml @@ -382,7 +382,24 @@ מִנהָרָה תחנת אוטובוס + כביש בבניה + כביש בבניה + כביש בבניה + כביש בבניה + כביש בבניה + כביש בבניה + כביש בבניה + כביש בבניה + כביש בבניה + כביש בבניה + כביש בבניה + כביש בבניה + כביש בבניה + כביש בבניה + כביש בבניה + כביש בבניה + כביש בבניה שביל אופניים גשר לאופניים diff --git a/android/sdk/src/main/res/values-ja/types_strings.xml b/android/sdk/src/main/res/values-ja/types_strings.xml index 50557c2c2..7f5990df1 100644 --- a/android/sdk/src/main/res/values-ja/types_strings.xml +++ b/android/sdk/src/main/res/values-ja/types_strings.xml @@ -356,7 +356,24 @@ トンネル バス停 + 工事中の道路 + 工事中の道路 + 工事中の道路 + 工事中の道路 + 工事中の道路 + 工事中の道路 + 工事中の道路 + 工事中の道路 + 工事中の道路 + 工事中の道路 + 工事中の道路 + 工事中の道路 + 工事中の道路 + 工事中の道路 + 工事中の道路 + 工事中の道路 + 工事中の道路 自転車道 自転車道(橋) diff --git a/android/sdk/src/main/res/values-ko/types_strings.xml b/android/sdk/src/main/res/values-ko/types_strings.xml index e62739e1a..9d1c1912d 100644 --- a/android/sdk/src/main/res/values-ko/types_strings.xml +++ b/android/sdk/src/main/res/values-ko/types_strings.xml @@ -330,7 +330,24 @@ 터널 버스 정류장 + 공사 중 도로 + 공사 중 도로 + 공사 중 도로 + 공사 중 도로 + 공사 중 도로 + 공사 중 도로 + 공사 중 도로 + 공사 중 도로 + 공사 중 도로 + 공사 중 도로 + 공사 중 도로 + 공사 중 도로 + 공사 중 도로 + 공사 중 도로 + 공사 중 도로 + 공사 중 도로 + 공사 중 도로 다리 diff --git a/android/sdk/src/main/res/values-kw/types_strings.xml b/android/sdk/src/main/res/values-kw/types_strings.xml index fe985615b..206dc602d 100644 --- a/android/sdk/src/main/res/values-kw/types_strings.xml +++ b/android/sdk/src/main/res/values-kw/types_strings.xml @@ -286,7 +286,24 @@ Pons Kowfordh Kyttrinva + Fordh y\'n Drehevyans + Fordh y\'n Drehevyans + Fordh y\'n Drehevyans + Fordh y\'n Drehevyans + Fordh y\'n Drehevyans + Fordh y\'n Drehevyans + Fordh y\'n Drehevyans + Fordh y\'n Drehevyans + Fordh y\'n Drehevyans + Fordh y\'n Drehevyans + Fordh y\'n Drehevyans + Fordh y\'n Drehevyans + Fordh y\'n Drehevyans + Fordh y\'n Drehevyans + Fordh y\'n Drehevyans + Fordh y\'n Drehevyans + Fordh y\'n Drehevyans Hyns rag Diwrosow Pons Hyns rag Diwrosow diff --git a/android/sdk/src/main/res/values-lt/types_strings.xml b/android/sdk/src/main/res/values-lt/types_strings.xml index 1b44e94f8..5f73fdd1b 100644 --- a/android/sdk/src/main/res/values-lt/types_strings.xml +++ b/android/sdk/src/main/res/values-lt/types_strings.xml @@ -466,7 +466,24 @@ Tiltas Tunelis Stotelė + Tiesiamas kelias + Tiesiamas kelias + Tiesiamas kelias + Tiesiamas kelias + Tiesiamas kelias + Tiesiamas kelias + Tiesiamas kelias + Tiesiamas kelias + Tiesiamas kelias + Tiesiamas kelias + Tiesiamas kelias + Tiesiamas kelias + Tiesiamas kelias + Tiesiamas kelias + Tiesiamas kelias + Tiesiamas kelias + Tiesiamas kelias Dviračių takas Tiltas Dviračių takas diff --git a/android/sdk/src/main/res/values-mr/types_strings.xml b/android/sdk/src/main/res/values-mr/types_strings.xml index b8628957d..967171b06 100644 --- a/android/sdk/src/main/res/values-mr/types_strings.xml +++ b/android/sdk/src/main/res/values-mr/types_strings.xml @@ -290,7 +290,24 @@ बोगदा बस थांबा + बांधकामाधीन रस्ता + बांधकामाधीन रस्ता + बांधकामाधीन रस्ता + बांधकामाधीन रस्ता + बांधकामाधीन रस्ता + बांधकामाधीन रस्ता + बांधकामाधीन रस्ता + बांधकामाधीन रस्ता + बांधकामाधीन रस्ता + बांधकामाधीन रस्ता + बांधकामाधीन रस्ता + बांधकामाधीन रस्ता + बांधकामाधीन रस्ता + बांधकामाधीन रस्ता + बांधकामाधीन रस्ता + बांधकामाधीन रस्ता + बांधकामाधीन रस्ता सायकल वाट पूल diff --git a/android/sdk/src/main/res/values-nb/types_strings.xml b/android/sdk/src/main/res/values-nb/types_strings.xml index c3b4a5905..36fdb7d5b 100644 --- a/android/sdk/src/main/res/values-nb/types_strings.xml +++ b/android/sdk/src/main/res/values-nb/types_strings.xml @@ -337,7 +337,24 @@ Tunnel Busstopp + Veikonstruksjon + Veikonstruksjon + Veikonstruksjon + Veikonstruksjon + Veikonstruksjon + Veikonstruksjon + Veikonstruksjon + Veikonstruksjon + Veikonstruksjon + Veikonstruksjon + Veikonstruksjon + Veikonstruksjon + Veikonstruksjon + Veikonstruksjon + Veikonstruksjon + Veikonstruksjon + Veikonstruksjon Sykkelvei Bru diff --git a/android/sdk/src/main/res/values-nl/types_strings.xml b/android/sdk/src/main/res/values-nl/types_strings.xml index 1659fc12c..9b69c0f26 100644 --- a/android/sdk/src/main/res/values-nl/types_strings.xml +++ b/android/sdk/src/main/res/values-nl/types_strings.xml @@ -145,7 +145,7 @@ GFTE afval Drankverpakkingen Restaurant - Caravantoilet cassette-leegpunt + Camperverzorgingsplaats School Schuilplaats @@ -167,7 +167,7 @@ Sigarettenautomaat Koffieautomaat Condoomautomaat - Drankautomaat + Drankenautomaat Etenswarenautomaat Krantenautomaat Parkeerautomaat @@ -180,7 +180,7 @@ Dierenarts Vuilnisbak Afvalcontainer - Overslagstation voor afval + Afvaloverslagstation Waterpunt Waterpunt Barrière @@ -238,7 +238,7 @@ Brouwerij Cateraar Timmerman - Banketbakkerij + Snoepmakerij Elektricien Elektronica-reparatie Tuinarchitect @@ -279,7 +279,7 @@ Koffie Pannenkoek Kroatisch - Curry\'s + Curry Delicatessen Diner Donuts @@ -292,7 +292,7 @@ Georgisch Duits Grieks - Gegrilde gerechten + Grill Heuriger Hotdogs Hongaars @@ -353,7 +353,7 @@ Ingang Hoofdingang - Uitgang (enig) + Uitgang (alleen uitgang) Gratis Medisch laboratorium Fysiotherapeut @@ -373,13 +373,30 @@ Ruiterpad Tunnel - Speciale busweg + Busbaan Brug Tunnel Bushalte - Baan in aanbouw + + Weg in aanbouw + Weg in aanbouw + Weg in aanbouw + Weg in aanbouw + Weg in aanbouw + Weg in aanbouw + Weg in aanbouw + Weg in aanbouw + Weg in aanbouw + Weg in aanbouw + Weg in aanbouw + Weg in aanbouw + Weg in aanbouw + Weg in aanbouw + Weg in aanbouw + Weg in aanbouw + Weg in aanbouw Fietspad Fietsbrug @@ -396,7 +413,7 @@ Voetgangerstunnel Voorde - Straat + Woonerf Brug @@ -407,7 +424,7 @@ Snelwegtunnel Afrit - Snelwegoprit + Snelwegoprit/afrit Brug @@ -416,7 +433,7 @@ Moeilijk of slecht zichtbaar pad - Zeer moeilijk of niet te onderscheiden spoor + Zeer moeilijk of niet te onderscheiden pad Fiets- & voetpad Fiets- & voetpad @@ -491,27 +508,27 @@ Brug Tunnel - Straat - Straat + Bos-/veldweg + Bos-/veldweg Brug - Straat - Straat + Bos-/veldweg + Bos-/veldweg Tunnel Verkeerslichten - Straat + Autoweg Brug Tunnel - Straat + Autowegoprit/afrit Brug Tunnel - Straat - Straat + Weg + Weg Brug @@ -525,12 +542,12 @@ Hoofdweg Straat Secundaire weg - Straat + Serviceweg Tertiaire weg - Trappen - Straat - Straat - Straat + Trap + Bos-/veldweg + Autoweg + Weg Historisch object Historische vliegtuigen @@ -546,7 +563,7 @@ Vesting Heuvelfort Kremlin - Manoir + Landhuis Paleis Japans kasteel Landhuis @@ -566,7 +583,7 @@ Historische mijn Monument Schandpaal - Ruïne + Historische ruïne Historisch schip Historische tank Graftombe @@ -575,7 +592,7 @@ Wegkruis Kruisbeeld - Schipbreuk + Schipwrak Internet Draadloos internet Kruispunt @@ -590,35 +607,35 @@ Christelijke begraafplaats Commercieel gebied Bouwplaats - Educatieve voorzieningen + Onderwijsgebied Landbouwgrond Boerenerf - Bloemenbed + Bloembed Bos Naaldbos Loofbos Gemengd bos Garages Gazon - Kassen + Kas Industrieterrein Stortplaats Weiland Militair terrein Boomgaard Steengroeve - Spoorwegfaciliteiten + Spoorwegterrein Recreatiezone Woonwijk - Detailhandel - Zoutpoel + Detailhandelsgebied + Zoutpan Dorpspark Wijngaard - Ontspanning + Recreatie Openbare grond Hondenuitlaatplek Fitnesscentrum - Fitness-Station + Fitness-station Danszaal Tuin Tuin @@ -629,9 +646,9 @@ Natuurreservaat Zitplaatsen buiten Park - Park + Privépark Park - Park + Privépark Picknicktafel Sportveld Speeltuin @@ -643,18 +660,18 @@ Yoga Stadion Zwembad - Privé zwembad + Privézwembad Parcours Parcours Waterpark Strandresort - Kunstmatige constructies + Kunstmatige constructie Golfbreker - Steengroeve + Steenmannetje Fabrieksschoorsteen - Bospad + Cutline Meetpunt - Vlaggenpaal + Vlaggenmast Vuurtoren Mast Pier @@ -680,7 +697,7 @@ Waterput Windmolen Fabriek - Militair + Militair object Bunker Bergpas Natuur @@ -695,9 +712,9 @@ Zandstrand Kiezelstrand Kaap - Grot + Grotingang Klif - Klif + Aardhelling Baanlichaam Kustlijn Woestijn @@ -707,8 +724,8 @@ Heide Hete bron Meer - Slotkamer - Plas + Schutkolk + Vijver Reservoir Waterbassin Rivier @@ -739,7 +756,7 @@ NGO-kantoor Mobiele provider Alleen biologisch - o.a. biologisch + Biologische opties Stad Hoofdstad Stad @@ -770,72 +787,72 @@ Zee Plein Deelstaat - Deelstaat + Staat Buitenwijk Stad Dorp - Electriciteit + Elektriciteit Zonne-generator - Wind generator - Gasturbine elektriciteitscentrale + Windgenerator + Gascentrale Waterkrachtcentrale Hoogspanningsleiding Ondergrondse hoogspanningsleiding Laag-/middelspanningsleiding Energiecentrale - Kolen elektriciteitscentrale - Gasturbine elektriciteitscentrale + Kolencentrale + Gascentrale Waterkrachtcentrale Zonne-energiecentrale Windkrachtcentrale Transformatorstation - Elektriciteitsmast + Hoogspanningsmast Openbaar vervoer - Platform + Perron Spoorweg Voormalig treinspoor - Treinspoor in aanbouw + Spoor in aanbouw Spoorwegovergang Ongebruikt spoor - Tunnel - Kabelspoorbuur + Kabelspoor + Kabelspoorbrug Kabelspoortunnel Station Spoorwegovergang Lightrail - Lightrail-brug - Lightrail-tunnel + Lightrailbrug + Lightrailtunnel Monorail - Monorail-brug - Monorail-tunnel - Smalspoorbaan + Monorailbrug + Monorailtunnel + Smalspoor Smalspoorbrug Smalspoortunnel Perron - Museumspoorbaan - Museumspoorbaanbrug - Museumspoorbaantunnel - Spoorlijn - Hogesnelheidstrein + Museumspoorweg + Museumspoorbrug + Museumspoortunnel + Spoor + Hogesnelheidsspoor Toeristische spoorlijn Spoor - Secundaire spoorwegen + Secundair spoor - Spoorwegen + Dienstspoor Zijspoor Hulpspoor - Treinspoorbrug - Treinspoorbrug - Treinspoorbrug - Treinspoorbrug - Treinspoorbrug - Treinspoorbrug - Treinspoorbrug - Treinspoorbrug + Spoorbrug + Spoorbrug + Spoorbrug + Spoorbrug + Spoorbrug + Spoorbrug + Spoorbrug + Spoorbrug Spoortunnel Spoortunnel Spoortunnel @@ -845,12 +862,12 @@ Spoortunnel Spoortunnel Station - Tunnel - Station + Kabelspoorstation + Lightrailstation S-bahn station Station Station - Station + Monorailstation Metrostation Metrostation Metrostation @@ -965,124 +982,124 @@ Metrostation Metrostation Metrostation - Metro + Metrolijn Metrobrug Metrotunnel - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Metro ingang - Tram + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Metro-ingang + Tramspoor Trambrug Tramtunnel Tramhalte @@ -1093,27 +1110,27 @@ Bakkerij Badkamerinrichting Schoonheidssalon - Drank + Drankenwinkel Fietsenwinkel - Boekbinder + Wedkantoor Boekwinkel Slager Coffeeshop - Autohandelaar - Auto-onderdelen - Auto reparatie + Autodealer + Auto-onderdelenwinkel + Autogarage Bandenreparatie - Caravan en camper verkoper + Caravan- en camperdealer Tapijtenwinkel Drogisterij Chocolaterie Kledingwinkel - Koffieverkoop + Koffiewinkel Computerwinkel Snoepwinkel - Buurtwinkel + Gemakswinkel Kopieerwinkel - Schoonheidsmiddelen + Cosmeticawinkel Gordijnenwinkel Delicatessenwinkel Warenhuis @@ -1123,21 +1140,21 @@ Erotiekwinkel Stoffenwinkel Boerderijwinkel - Modeaccessoires + Modeaccessoirewinkel Bloemist - Begrafenisondernemer + Uitvaartondernemer Meubelwinkel Tuincentrum Gaswinkel Cadeauwinkel Groentenwinkel - Boodschappen + Kruidenierswinkel Kapper - IJzerwaren + IJzerwarenwinkel Natuurvoedingswinkel - Winkel met hoortoestellen + Hoortoestellenwinkel Kruidenwinkel - HiFi-audio + Hifiwinkel Huishoudwinkel Juwelier Kiosk @@ -1145,18 +1162,18 @@ Wasserette Winkelcentrum Massagesalon - Mobiele telefoonwinkel + Mobiele-telefoonwinkel Geldschieter Motorzaak Motorfietsreparatie Muziekwinkel Muziekinstrumentenwinkel - Kiosk + Krantenkiosk Opticien - Outdooruitrusting + Buitensportwinkel Afhaalpunt Pastawinkel - Banketbakker + Banketbakkerij Pandjeshuis Dierenwinkel Huisdierverzorging @@ -1166,10 +1183,10 @@ Visboer Tweedehandswinkel Schoenenwinkel - Sportartikelen + Sportwinkel Kantoorboekhandel Supermarkt - Tattoosalon + Tatoeagestudio Theewinkel Kaartjesverkoop Speelgoedwinkel @@ -1184,27 +1201,27 @@ Witgoedwinkel Kunstwinkel - Babyspullenwinkel + Babywinkel Tassenwinkel Beddenwinkel Boetiek Kringloopwinkel Kaaswinkel - Kunst en ambacht - Zuivelproducten + Kunst- en hobbywinkel + Zuivelwinkel Elektronische benodigdhedenwinkel Viswinkel Interieurdecoratiewinkel - Loten - Medische benodigdheden + Loterijwinkel + Medische benodigdhedenwinkel Voedingssupplementenwinkel Verfwinkel Parfumerie - Naaibenodigdheden + Naai- en fourniturenwinkel Opslagverhuur - Tabakszaak - Handelsbenodigdheden - Horlogezaak + Tabakswinkel + Technische groothandel + Horlogewinkel Groothandel Sport American football @@ -1214,18 +1231,18 @@ Honkbal Basketbal Beachvolleybal - Bowling + Bowls Schaken Cricket Curling - Paardesport + Paardensport Golf Gymnastiek Handbal Diverse sporten Duiken - schieten + Schietsport Skateboarden Skiën Voetbal @@ -1236,57 +1253,57 @@ Bowlen Bowlen Padel - Futsal + Zaalvoetbal IJshockey Veldhockey Badminton - Baskische peloton + Pelota Tourisme Aquarium Berghut - Appartement + Vakantieappartement Kunstwerk - Kunstwerk + Architectonisch kunstwerk Schilderij - Kunstwerk + Sculptuur Standbeeld - Toeristische attractie - Prettocht + Bezienswaardigheid + Attractie Dierenverblijf Botsauto\'s - Reuzenrad + Draaimolen Historische attractie Doolhof Achtbaan Waterglijbaan Attractie - Kampeerterrein - Caravan site + Camping + Caravan- en camperplaats Vakantiehuis - Galerij - Gasthuis + Galerie + Pension Jeugdherberg Hotel Toeristische informatie Informatiebord Wegwijzer Toeristische kaart - Toeristische informatie + Toeristeninformatiepunt Bezoekerscentrum Museum - Picnicplaats + Picknickplaats Complex Attractiepark Uitkijkpunt - Wildernishut + Zelfverzorgingshut Dierentuin Kinderboerderij - Snelheidsmatigende maatregel + Snelheidsremmende maatregel Verkeersdrempel - Verkeersheuvel + Verkeersdrempel Waterweg Kanaal Kanaal @@ -1309,37 +1326,37 @@ Waterval Stuw Rolstoel - Gedeeltelijk uitgerust voor gehandicapten - Niet uitgerust voor gehandicapten - Uitgerust voor gehandicapten + Gedeeltelijk rolstoeltoegankelijk + Niet rolstoeltoegankelijk + Volledig rolstoeltoegankelijk Sleeplift (J) - Loopband - Sleeplift + Lopende band (skilift) + Pannenkoeklift Touwlift Sleeplift (T-beugel) - Ski-afdaling - Ski-afdaling - Moeilijke afdaling - Moeilijke afdaling - Makkelijke afdaling - Makkelijke afdaling - Afdaling voor experts - Afdaling voor experts - Ski-afdaling - Gemiddelde afdaling - Gemiddelde afdaling - Beginnersafdaling - Beginnersafdaling + Skipiste + Skipiste + Moeilijke skipiste + Moeilijke skipiste + Makkelijke skipiste + Makkelijke skipiste + Zeer moeilijke skipiste + Zeer moeilijke skipiste + Freeride ski-afdaling + Gemiddelde skipiste + Gemiddelde skipiste + Beginnersskipiste + Beginnersskipiste Langlaufroute Sleebaan Sleebaan Sneeuwpark Sneeuwwandelpad Pisteverbinding - Skitourspoor + Toerskiroute Evenementenlocatie Veiling - Verzamelobjecten + Verzamelaarswinkel Zelfbediening beschikbaar Alleen zelfbediening Gedeeltelijke zelfbediening @@ -1354,7 +1371,7 @@ Sporthal Bubbelthee - Stroommast + Elektriciteitsmast Paal Fish and Chips Reservoir @@ -1362,7 +1379,7 @@ Reuzenrad Afschotbeschutting Braakliggend terrein - Greenfield + Bouwgrond Generator Hackerspace Foodcourt @@ -1387,7 +1404,7 @@ Overdekte fietsenstalling Post- en pakketpunt Escaperoom - Boswachterkantoor + Boswachterskantoor Dierenasiel Klinket Beveiligingskantoor @@ -1403,7 +1420,7 @@ Observatorium Carpool Reddingsboei - Muziekpodium + Muziekkoepel Sportcentrum Sportcentrum Sportcentrum @@ -1437,14 +1454,14 @@ Sportcentrum Zwemcentrum Telecommunicatiewinkel - Leegstand bedrijfspand + Leegstaand bedrijfspand Gaarkeuken Voedselbank Verzamelpunt Yogastudio Ligbed Betaalkantoor - Speelhal + Indoor speeltuin Zeilschool Vliegschool Bijscholing @@ -1483,7 +1500,7 @@ Huis entree Garage entree Dienstingang - Ingang (enig) + Ingang (alleen ingang) Nooduitgang Kerstboom Schoonheidssalon @@ -1493,13 +1510,13 @@ Geïsoleerde gevaarsboei Sferische geïsoleerde gevaarsboei Nagelsalon - Voedsel delen - Giftenkist + Voedseldeelpunt + Weggeefkast Watertappunt|waterkraan - Miniatuur spoorweg - Miniatuur spoorbrug - Miniatuur spoortunnel - Spoorweg-keerpunt + Miniatuurspoorweg + Miniatuurspoorbrug + Miniatuurspoortunnel + Spoordraaischijf Tactiele kaart Ongebruikte spoorbrug Ongebruikte spoortunnel @@ -1514,8 +1531,8 @@ Four Square Sportcentrum Sportcentrum - jeu de boules + Jeu de boules Sportcentrum Pickleball - Niet biologisch + Geen biologische opties diff --git a/android/sdk/src/main/res/values-pl/types_strings.xml b/android/sdk/src/main/res/values-pl/types_strings.xml index 18294752c..9a0806436 100644 --- a/android/sdk/src/main/res/values-pl/types_strings.xml +++ b/android/sdk/src/main/res/values-pl/types_strings.xml @@ -380,7 +380,24 @@ Tunel Przystanek autobusowy + Droga w trakcie budowy + Droga w trakcie budowy + Droga w trakcie budowy + Droga w trakcie budowy + Droga w trakcie budowy + Droga w trakcie budowy + Droga w trakcie budowy + Droga w trakcie budowy + Droga w trakcie budowy + Droga w trakcie budowy + Droga w trakcie budowy + Droga w trakcie budowy + Droga w trakcie budowy + Droga w trakcie budowy + Droga w trakcie budowy + Droga w trakcie budowy + Droga w trakcie budowy Droga rowerowa Most drogowy dla rowerów diff --git a/android/sdk/src/main/res/values-pt-rBR/types_strings.xml b/android/sdk/src/main/res/values-pt-rBR/types_strings.xml index 8bcfffa2a..b28cadd25 100644 --- a/android/sdk/src/main/res/values-pt-rBR/types_strings.xml +++ b/android/sdk/src/main/res/values-pt-rBR/types_strings.xml @@ -333,7 +333,24 @@ Túnel Ponto de ônibus + Via em construção + Via em construção + Via em construção + Via em construção + Via em construção + Via em construção + Via em construção + Via em construção + Via em construção + Via em construção + Via em construção + Via em construção + Via em construção + Via em construção + Via em construção + Via em construção + Via em construção Ciclovia Ponte para ciclistas diff --git a/android/sdk/src/main/res/values-pt/types_strings.xml b/android/sdk/src/main/res/values-pt/types_strings.xml index 95d457e82..dd7b42fd1 100644 --- a/android/sdk/src/main/res/values-pt/types_strings.xml +++ b/android/sdk/src/main/res/values-pt/types_strings.xml @@ -368,7 +368,24 @@ Túnel Paragem de autocarros + Estrada em construção + Estrada em construção + Estrada em construção + Estrada em construção + Estrada em construção + Estrada em construção + Estrada em construção + Estrada em construção + Estrada em construção + Estrada em construção + Estrada em construção + Estrada em construção + Estrada em construção + Estrada em construção + Estrada em construção + Estrada em construção + Estrada em construção Ciclovia Ponte diff --git a/android/sdk/src/main/res/values-ro/types_strings.xml b/android/sdk/src/main/res/values-ro/types_strings.xml index 8cce5160e..a566ed311 100644 --- a/android/sdk/src/main/res/values-ro/types_strings.xml +++ b/android/sdk/src/main/res/values-ro/types_strings.xml @@ -322,7 +322,24 @@ Tunel Stație de autobuz + Drum în construcție + Drum în construcție + Drum în construcție + Drum în construcție + Drum în construcție + Drum în construcție + Drum în construcție + Drum în construcție + Drum în construcție + Drum în construcție + Drum în construcție + Drum în construcție + Drum în construcție + Drum în construcție + Drum în construcție + Drum în construcție + Drum în construcție Pod diff --git a/android/sdk/src/main/res/values-ru/types_strings.xml b/android/sdk/src/main/res/values-ru/types_strings.xml index a498d2f31..b9f104442 100644 --- a/android/sdk/src/main/res/values-ru/types_strings.xml +++ b/android/sdk/src/main/res/values-ru/types_strings.xml @@ -386,7 +386,24 @@ Тоннель Остановка + Строящаяся дорога + Строящаяся дорога + Строящаяся дорога + Строящаяся дорога + Строящаяся дорога + Строящаяся дорога + Строящаяся дорога + Строящаяся дорога + Строящаяся дорога + Строящаяся дорога + Строящаяся дорога + Строящаяся дорога + Строящаяся дорога + Строящаяся дорога + Строящаяся дорога + Строящаяся дорога + Строящаяся дорога Велодорожка Мост diff --git a/android/sdk/src/main/res/values-sk/types_strings.xml b/android/sdk/src/main/res/values-sk/types_strings.xml index c20f3af01..3fe46e90f 100644 --- a/android/sdk/src/main/res/values-sk/types_strings.xml +++ b/android/sdk/src/main/res/values-sk/types_strings.xml @@ -374,7 +374,24 @@ Tunel Autobusová zastávka + Cesta vo výstavbe + Cesta vo výstavbe + Cesta vo výstavbe + Cesta vo výstavbe + Cesta vo výstavbe + Cesta vo výstavbe + Cesta vo výstavbe + Cesta vo výstavbe + Cesta vo výstavbe + Cesta vo výstavbe + Cesta vo výstavbe + Cesta vo výstavbe + Cesta vo výstavbe + Cesta vo výstavbe + Cesta vo výstavbe + Cesta vo výstavbe + Cesta vo výstavbe Cyklocesta Most diff --git a/android/sdk/src/main/res/values-sl/types_strings.xml b/android/sdk/src/main/res/values-sl/types_strings.xml index 1ccc079f5..6fda6fedb 100644 --- a/android/sdk/src/main/res/values-sl/types_strings.xml +++ b/android/sdk/src/main/res/values-sl/types_strings.xml @@ -498,7 +498,24 @@ Most Predor Avtobusno postajališče + Cesta v gradnji + Cesta v gradnji + Cesta v gradnji + Cesta v gradnji + Cesta v gradnji + Cesta v gradnji + Cesta v gradnji + Cesta v gradnji + Cesta v gradnji + Cesta v gradnji + Cesta v gradnji + Cesta v gradnji + Cesta v gradnji + Cesta v gradnji + Cesta v gradnji + Cesta v gradnji + Cesta v gradnji Kolesarska steza Most Kolesarska steza diff --git a/android/sdk/src/main/res/values-sr/types_strings.xml b/android/sdk/src/main/res/values-sr/types_strings.xml index 85f564cfb..48577eb07 100644 --- a/android/sdk/src/main/res/values-sr/types_strings.xml +++ b/android/sdk/src/main/res/values-sr/types_strings.xml @@ -383,7 +383,24 @@ Тунел Аутобуско стајалиште + Пут у изградњи + Пут у изградњи + Пут у изградњи + Пут у изградњи + Пут у изградњи + Пут у изградњи + Пут у изградњи + Пут у изградњи + Пут у изградњи + Пут у изградњи + Пут у изградњи + Пут у изградњи + Пут у изградњи + Пут у изградњи + Пут у изградњи + Пут у изградњи + Пут у изградњи Бициклистичка стаза Мост diff --git a/android/sdk/src/main/res/values-sv/types_strings.xml b/android/sdk/src/main/res/values-sv/types_strings.xml index 3e1aa59d5..7ba2636d8 100644 --- a/android/sdk/src/main/res/values-sv/types_strings.xml +++ b/android/sdk/src/main/res/values-sv/types_strings.xml @@ -325,7 +325,24 @@ Tunnel Busshållplats + Väg under uppförande + Väg under uppförande + Väg under uppförande + Väg under uppförande + Väg under uppförande + Väg under uppförande + Väg under uppförande + Väg under uppförande + Väg under uppförande + Väg under uppförande + Väg under uppförande + Väg under uppförande + Väg under uppförande + Väg under uppförande + Väg under uppförande + Väg under uppförande + Väg under uppförande Cykelbro diff --git a/android/sdk/src/main/res/values-sw/types_strings.xml b/android/sdk/src/main/res/values-sw/types_strings.xml index d8e1e523d..95b44a2ce 100644 --- a/android/sdk/src/main/res/values-sw/types_strings.xml +++ b/android/sdk/src/main/res/values-sw/types_strings.xml @@ -131,7 +131,24 @@ Daraja Mtaro + Barabara inatengenezwa + Barabara inatengenezwa + Barabara inatengenezwa + Barabara inatengenezwa + Barabara inatengenezwa + Barabara inatengenezwa + Barabara inatengenezwa + Barabara inatengenezwa + Barabara inatengenezwa + Barabara inatengenezwa + Barabara inatengenezwa + Barabara inatengenezwa + Barabara inatengenezwa + Barabara inatengenezwa + Barabara inatengenezwa + Barabara inatengenezwa + Barabara inatengenezwa Daraja diff --git a/android/sdk/src/main/res/values-ta/types_strings.xml b/android/sdk/src/main/res/values-ta/types_strings.xml index 05dcd1825..9bb5e3f0a 100644 --- a/android/sdk/src/main/res/values-ta/types_strings.xml +++ b/android/sdk/src/main/res/values-ta/types_strings.xml @@ -386,7 +386,24 @@ பாலம் சுரங்கப்பாதை பேருந்து நிறுத்தம் + சாலை கட்டுமானத்தில் உள்ளது + சாலை கட்டுமானத்தில் உள்ளது + சாலை கட்டுமானத்தில் உள்ளது + சாலை கட்டுமானத்தில் உள்ளது + சாலை கட்டுமானத்தில் உள்ளது + சாலை கட்டுமானத்தில் உள்ளது + சாலை கட்டுமானத்தில் உள்ளது + சாலை கட்டுமானத்தில் உள்ளது + சாலை கட்டுமானத்தில் உள்ளது + சாலை கட்டுமானத்தில் உள்ளது + சாலை கட்டுமானத்தில் உள்ளது + சாலை கட்டுமானத்தில் உள்ளது + சாலை கட்டுமானத்தில் உள்ளது + சாலை கட்டுமானத்தில் உள்ளது + சாலை கட்டுமானத்தில் உள்ளது + சாலை கட்டுமானத்தில் உள்ளது + சாலை கட்டுமானத்தில் உள்ளது சைக்கிள் பாதை பாலம் சைக்கிள் பாதை diff --git a/android/sdk/src/main/res/values-th/types_strings.xml b/android/sdk/src/main/res/values-th/types_strings.xml index 202ca4b7a..5291511e8 100644 --- a/android/sdk/src/main/res/values-th/types_strings.xml +++ b/android/sdk/src/main/res/values-th/types_strings.xml @@ -331,7 +331,24 @@ อุโมงค์ ป้ายรถเมล์ + ทางกำลังอยู่ในการก่อสร้าง + ทางกำลังอยู่ในการก่อสร้าง + ทางกำลังอยู่ในการก่อสร้าง + ทางกำลังอยู่ในการก่อสร้าง + ทางกำลังอยู่ในการก่อสร้าง + ทางกำลังอยู่ในการก่อสร้าง + ทางกำลังอยู่ในการก่อสร้าง + ทางกำลังอยู่ในการก่อสร้าง + ทางกำลังอยู่ในการก่อสร้าง + ทางกำลังอยู่ในการก่อสร้าง + ทางกำลังอยู่ในการก่อสร้าง + ทางกำลังอยู่ในการก่อสร้าง + ทางกำลังอยู่ในการก่อสร้าง + ทางกำลังอยู่ในการก่อสร้าง + ทางกำลังอยู่ในการก่อสร้าง + ทางกำลังอยู่ในการก่อสร้าง + ทางกำลังอยู่ในการก่อสร้าง สะพาน diff --git a/android/sdk/src/main/res/values-tr/types_strings.xml b/android/sdk/src/main/res/values-tr/types_strings.xml index 3fb63a9d9..d0f9d0c1b 100644 --- a/android/sdk/src/main/res/values-tr/types_strings.xml +++ b/android/sdk/src/main/res/values-tr/types_strings.xml @@ -376,7 +376,24 @@ Tünel Otobüs Durağı + Yapım Aşamasında Yol + Yapım Aşamasında Yol + Yapım Aşamasında Yol + Yapım Aşamasında Yol + Yapım Aşamasında Yol + Yapım Aşamasında Yol + Yapım Aşamasında Yol + Yapım Aşamasında Yol + Yapım Aşamasında Yol + Yapım Aşamasında Yol + Yapım Aşamasında Yol + Yapım Aşamasında Yol + Yapım Aşamasında Yol + Yapım Aşamasında Yol + Yapım Aşamasında Yol + Yapım Aşamasında Yol + Yapım Aşamasında Yol Bisiklet Yolu Köprü diff --git a/android/sdk/src/main/res/values-uk/types_strings.xml b/android/sdk/src/main/res/values-uk/types_strings.xml index b2297ed76..ad54caf50 100644 --- a/android/sdk/src/main/res/values-uk/types_strings.xml +++ b/android/sdk/src/main/res/values-uk/types_strings.xml @@ -381,7 +381,24 @@ Тунель Зупинка + Дорога, що будується + Дорога, що будується + Дорога, що будується + Дорога, що будується + Дорога, що будується + Дорога, що будується + Дорога, що будується + Дорога, що будується + Дорога, що будується + Дорога, що будується + Дорога, що будується + Дорога, що будується + Дорога, що будується + Дорога, що будується + Дорога, що будується + Дорога, що будується + Дорога, що будується Велодоріжка Міст diff --git a/android/sdk/src/main/res/values-vi/types_strings.xml b/android/sdk/src/main/res/values-vi/types_strings.xml index 128f8f60f..d35888d66 100644 --- a/android/sdk/src/main/res/values-vi/types_strings.xml +++ b/android/sdk/src/main/res/values-vi/types_strings.xml @@ -329,7 +329,24 @@ Đường hầm Bến xe buýt + Đường đang thi công + Đường đang thi công + Đường đang thi công + Đường đang thi công + Đường đang thi công + Đường đang thi công + Đường đang thi công + Đường đang thi công + Đường đang thi công + Đường đang thi công + Đường đang thi công + Đường đang thi công + Đường đang thi công + Đường đang thi công + Đường đang thi công + Đường đang thi công + Đường đang thi công Cầu diff --git a/android/sdk/src/main/res/values-zh-rHK/types_strings.xml b/android/sdk/src/main/res/values-zh-rHK/types_strings.xml index ece4baf03..2f0dc49e2 100644 --- a/android/sdk/src/main/res/values-zh-rHK/types_strings.xml +++ b/android/sdk/src/main/res/values-zh-rHK/types_strings.xml @@ -379,7 +379,24 @@ 隧道 巴士站 + 在建道路 + 在建道路 + 在建道路 + 在建道路 + 在建道路 + 在建道路 + 在建道路 + 在建道路 + 在建道路 + 在建道路 + 在建道路 + 在建道路 + 在建道路 + 在建道路 + 在建道路 + 在建道路 + 在建道路 自行車道 diff --git a/android/sdk/src/main/res/values-zh-rTW/types_strings.xml b/android/sdk/src/main/res/values-zh-rTW/types_strings.xml index 73032d099..1d996d812 100644 --- a/android/sdk/src/main/res/values-zh-rTW/types_strings.xml +++ b/android/sdk/src/main/res/values-zh-rTW/types_strings.xml @@ -626,7 +626,24 @@ 橋樑 隧道 公車站 + 施工中道路 + 施工中道路 + 施工中道路 + 施工中道路 + 施工中道路 + 施工中道路 + 施工中道路 + 施工中道路 + 施工中道路 + 施工中道路 + 施工中道路 + 施工中道路 + 施工中道路 + 施工中道路 + 施工中道路 + 施工中道路 + 施工中道路 單車道 橋樑 單車道 diff --git a/android/sdk/src/main/res/values-zh/types_strings.xml b/android/sdk/src/main/res/values-zh/types_strings.xml index da409ba11..b5afc1da0 100644 --- a/android/sdk/src/main/res/values-zh/types_strings.xml +++ b/android/sdk/src/main/res/values-zh/types_strings.xml @@ -380,7 +380,24 @@ 隧道 公交站 + 在建道路 + 在建道路 + 在建道路 + 在建道路 + 在建道路 + 在建道路 + 在建道路 + 在建道路 + 在建道路 + 在建道路 + 在建道路 + 在建道路 + 在建道路 + 在建道路 + 在建道路 + 在建道路 + 在建道路 自行车道 diff --git a/android/sdk/src/main/res/values/types_strings.xml b/android/sdk/src/main/res/values/types_strings.xml index feb7aedc5..5dfd855c3 100644 --- a/android/sdk/src/main/res/values/types_strings.xml +++ b/android/sdk/src/main/res/values/types_strings.xml @@ -456,7 +456,24 @@ Tunnel Bus Stop + Road Under Construction + Road Under Construction + Road Under Construction + Road Under Construction + Road Under Construction + Road Under Construction + Road Under Construction + Road Under Construction + Road Under Construction + Road Under Construction + Road Under Construction + Road Under Construction + Road Under Construction + Road Under Construction + Road Under Construction + Road Under Construction + Road Under Construction Cycle Path Bridge diff --git a/data/mapcss-mapping.csv b/data/mapcss-mapping.csv index 2b5a43e86..7f0470359 100644 --- a/data/mapcss-mapping.csv +++ b/data/mapcss-mapping.csv @@ -916,22 +916,22 @@ leisure|sports_centre|sport|soccer;[leisure=sports_centre][sport=soccer];;name;i leisure|sports_centre|sport|four_square;[leisure=sports_centre][sport=four_square];;name;int_name;787; leisure|sports_centre|sport|boules;[leisure=sports_centre][sport=boules];;name;int_name;788; leisure|sports_centre|sport|pickleball;[leisure=sports_centre][sport=pickleball];;name;int_name;789; -deprecated|deprecated;790;x -deprecated|deprecated;791;x -deprecated|deprecated;792;x -deprecated|deprecated;793;x -deprecated|deprecated;794;x -deprecated|deprecated;795;x -deprecated|deprecated;796;x -deprecated|deprecated;797;x -deprecated|deprecated;798;x -deprecated|deprecated;799;x -deprecated|deprecated;800;x -deprecated|deprecated;801;x -deprecated|deprecated;802;x -deprecated|deprecated;803;x -deprecated|deprecated;804;x -deprecated|deprecated;805;x +highway|construction|motorway;[highway=construction][construction=motorway];;name;int_name;790; +highway|construction|motorway_link;[highway=construction][construction=motorway_link];;name;int_name;791; +highway|construction|trunk;[highway=construction][construction=trunk];;name;int_name;792; +highway|construction|trunk_link;[highway=construction][construction=trunk_link];;name;int_name;793; +highway|construction|primary;[highway=construction][construction=primary];;name;int_name;794; +highway|construction|primary_link;[highway=construction][construction=primary_link];;name;int_name;795; +highway|construction|secondary;[highway=construction][construction=secondary];;name;int_name;796; +highway|construction|secondary_link;[highway=construction][construction=secondary_link];;name;int_name;797; +highway|construction|tertiary;[highway=construction][construction=tertiary];;name;int_name;798; +highway|construction|tertiary_link;[highway=construction][construction=tertiary_link];;name;int_name;799; +highway|construction|residential;[highway=construction][construction=residential];;name;int_name;800; +highway|construction|unclassified;[highway=construction][construction=unclassified];;name;int_name;801; +highway|construction|service;[highway=construction][construction=service];;name;int_name;802; +highway|construction|living_street;[highway=construction][construction=living_street];;name;int_name;803; +highway|construction|road;[highway=construction][construction=road];;name;int_name;804; +highway|construction|track;[highway=construction][construction=track];;name;int_name;805; deprecated|deprecated;806;x deprecated|deprecated;807;x deprecated|deprecated;808;x diff --git a/data/styles/default/include/priorities_3_FG.prio.txt b/data/styles/default/include/priorities_3_FG.prio.txt index 7fe56ebeb..9befbf7a1 100644 --- a/data/styles/default/include/priorities_3_FG.prio.txt +++ b/data/styles/default/include/priorities_3_FG.prio.txt @@ -293,6 +293,22 @@ highway-track-tunnel # line z15- (also has line:: === 190 highway-construction # line z13- (also has pathtext z15-) +highway-construction-living_street # line z13- (also has pathtext z15-) +highway-construction-motorway # line z13- (also has pathtext z15-) +highway-construction-motorway_link # line z13- (also has pathtext z15-) +highway-construction-primary # line z13- (also has pathtext z15-) +highway-construction-primary_link # line z13- (also has pathtext z15-) +highway-construction-residential # line z13- (also has pathtext z15-) +highway-construction-road # line z13- (also has pathtext z15-) +highway-construction-secondary # line z13- (also has pathtext z15-) +highway-construction-secondary_link # line z13- (also has pathtext z15-) +highway-construction-service # line z13- (also has pathtext z15-) +highway-construction-tertiary # line z13- (also has pathtext z15-) +highway-construction-tertiary_link # line z13- (also has pathtext z15-) +highway-construction-track # line z13- (also has pathtext z15-) +highway-construction-trunk # line z13- (also has pathtext z15-) +highway-construction-trunk_link # line z13- (also has pathtext z15-) +highway-construction-unclassified # line z13- (also has pathtext z15-) leisure-track # line z15- (also has caption z16-) railway-abandoned # line z16- railway-construction # line z15- diff --git a/data/styles/default/include/priorities_4_overlays.prio.txt b/data/styles/default/include/priorities_4_overlays.prio.txt index 1d898111d..337f1166d 100644 --- a/data/styles/default/include/priorities_4_overlays.prio.txt +++ b/data/styles/default/include/priorities_4_overlays.prio.txt @@ -838,6 +838,22 @@ attraction-maze # icon z16- (also has captio attraction-roller_coaster # icon z16- (also has caption(optional) z16-) attraction-water_slide # icon z17- (also has caption(optional) z17-) highway-construction # pathtext z15- (also has line z13-) +highway-construction-living_street # pathtext z15- (also has line z13-) +highway-construction-motorway # pathtext z15- (also has line z13-) +highway-construction-motorway_link # pathtext z15- (also has line z13-) +highway-construction-primary # pathtext z15- (also has line z13-) +highway-construction-primary_link # pathtext z15- (also has line z13-) +highway-construction-residential # pathtext z15- (also has line z13-) +highway-construction-road # pathtext z15- (also has line z13-) +highway-construction-secondary # pathtext z15- (also has line z13-) +highway-construction-secondary_link # pathtext z15- (also has line z13-) +highway-construction-service # pathtext z15- (also has line z13-) +highway-construction-tertiary # pathtext z15- (also has line z13-) +highway-construction-tertiary_link # pathtext z15- (also has line z13-) +highway-construction-track # pathtext z15- (also has line z13-) +highway-construction-trunk # pathtext z15- (also has line z13-) +highway-construction-trunk_link # pathtext z15- (also has line z13-) +highway-construction-unclassified # pathtext z15- (also has line z13-) highway-living_street # pathtext z14- (also has line z12-, line::border z14-) highway-living_street-bridge # pathtext z14- (also has line z12-, line::border z14-) highway-living_street-tunnel # pathtext z14- (also has line z12-, line::border z14-, line(casing) z16-) diff --git a/data/styles/outdoors/include/priorities_3_FG.prio.txt b/data/styles/outdoors/include/priorities_3_FG.prio.txt index c9dd78f72..0c0e76124 100644 --- a/data/styles/outdoors/include/priorities_3_FG.prio.txt +++ b/data/styles/outdoors/include/priorities_3_FG.prio.txt @@ -295,6 +295,22 @@ highway-track-tunnel # line z11- (also has line:: === 190 highway-construction # line z11- (also has pathtext z15-) +highway-construction-living_street # line z11- (also has pathtext z15-) +highway-construction-motorway # line z11- (also has pathtext z15-) +highway-construction-motorway_link # line z11- (also has pathtext z15-) +highway-construction-primary # line z11- (also has pathtext z15-) +highway-construction-primary_link # line z11- (also has pathtext z15-) +highway-construction-residential # line z11- (also has pathtext z15-) +highway-construction-road # line z11- (also has pathtext z15-) +highway-construction-secondary # line z11- (also has pathtext z15-) +highway-construction-secondary_link # line z11- (also has pathtext z15-) +highway-construction-service # line z11- (also has pathtext z15-) +highway-construction-tertiary # line z11- (also has pathtext z15-) +highway-construction-tertiary_link # line z11- (also has pathtext z15-) +highway-construction-track # line z11- (also has pathtext z15-) +highway-construction-trunk # line z11- (also has pathtext z15-) +highway-construction-trunk_link # line z11- (also has pathtext z15-) +highway-construction-unclassified # line z11- (also has pathtext z15-) leisure-track # line z15- (also has caption z16-) railway-abandoned # line z13- railway-construction # line z13- diff --git a/data/styles/outdoors/include/priorities_4_overlays.prio.txt b/data/styles/outdoors/include/priorities_4_overlays.prio.txt index 1785479cc..2320321bc 100644 --- a/data/styles/outdoors/include/priorities_4_overlays.prio.txt +++ b/data/styles/outdoors/include/priorities_4_overlays.prio.txt @@ -838,6 +838,22 @@ attraction-maze # icon z16- (also has captio attraction-roller_coaster # icon z16- (also has caption(optional) z16-) attraction-water_slide # icon z17- (also has caption(optional) z17-) highway-construction # pathtext z15- (also has line z11-) +highway-construction-living_street # pathtext z15- (also has line z11-) +highway-construction-motorway # pathtext z15- (also has line z11-) +highway-construction-motorway_link # pathtext z15- (also has line z11-) +highway-construction-primary # pathtext z15- (also has line z11-) +highway-construction-primary_link # pathtext z15- (also has line z11-) +highway-construction-residential # pathtext z15- (also has line z11-) +highway-construction-road # pathtext z15- (also has line z11-) +highway-construction-secondary # pathtext z15- (also has line z11-) +highway-construction-secondary_link # pathtext z15- (also has line z11-) +highway-construction-service # pathtext z15- (also has line z11-) +highway-construction-tertiary # pathtext z15- (also has line z11-) +highway-construction-tertiary_link # pathtext z15- (also has line z11-) +highway-construction-track # pathtext z15- (also has line z11-) +highway-construction-trunk # pathtext z15- (also has line z11-) +highway-construction-trunk_link # pathtext z15- (also has line z11-) +highway-construction-unclassified # pathtext z15- (also has line z11-) highway-living_street # pathtext z14- (also has line z12-, line::border z12-) highway-living_street-bridge # pathtext z14- (also has line z12-, line::border z12-) highway-living_street-tunnel # pathtext z14- (also has line z12-, line::border z12-, line(casing) z16-) diff --git a/data/styles/vehicle/include/priorities_3_FG.prio.txt b/data/styles/vehicle/include/priorities_3_FG.prio.txt index 2a2904281..9068150fe 100644 --- a/data/styles/vehicle/include/priorities_3_FG.prio.txt +++ b/data/styles/vehicle/include/priorities_3_FG.prio.txt @@ -271,6 +271,22 @@ highway-track-no-access # line z16- === 130 highway-construction # line z13- +highway-construction-living_street # line z13- +highway-construction-motorway # line z13- +highway-construction-motorway_link # line z13- +highway-construction-primary # line z13- +highway-construction-primary_link # line z13- +highway-construction-residential # line z13- +highway-construction-road # line z13- +highway-construction-secondary # line z13- +highway-construction-secondary_link # line z13- +highway-construction-service # line z13- +highway-construction-tertiary # line z13- +highway-construction-tertiary_link # line z13- +highway-construction-track # line z13- +highway-construction-trunk # line z13- +highway-construction-trunk_link # line z13- +highway-construction-unclassified # line z13- railway-abandoned # line z16- railway-construction # line z16- railway-disused # line z16- diff --git a/data/test_data/traff/DE-A10-Werder-GrossKreutz.xml b/data/test_data/traff/DE-A10-Werder-GrossKreutz.xml new file mode 100644 index 000000000..6ea4707b8 --- /dev/null +++ b/data/test_data/traff/DE-A10-Werder-GrossKreutz.xml @@ -0,0 +1,17 @@ + + + + + + +52.334801 +12.814650 + +52.393700 +12.835000 + + + + + + + diff --git a/data/test_data/traff/DE-A115-PotsdamDrewitz-Nuthetal.xml b/data/test_data/traff/DE-A115-PotsdamDrewitz-Nuthetal.xml new file mode 100644 index 000000000..3d1c93472 --- /dev/null +++ b/data/test_data/traff/DE-A115-PotsdamDrewitz-Nuthetal.xml @@ -0,0 +1,17 @@ + + + + + + +52.352650 +13.140700 + +52.300201 +13.083500 + + + + + + + diff --git a/data/test_data/traff/DE-B2R-LerchenauerStrasse-Petueltunnel.xml b/data/test_data/traff/DE-B2R-LerchenauerStrasse-Petueltunnel.xml new file mode 100644 index 000000000..f8ac9beb7 --- /dev/null +++ b/data/test_data/traff/DE-B2R-LerchenauerStrasse-Petueltunnel.xml @@ -0,0 +1,16 @@ + + + + + +48.176102 +11.558100 + +48.178001 +11.572800 + + + + + + + diff --git a/data/test_data/traff/DE-B2R-SendlingSued-Passauerstrasse.xml b/data/test_data/traff/DE-B2R-SendlingSued-Passauerstrasse.xml new file mode 100644 index 000000000..50df83776 --- /dev/null +++ b/data/test_data/traff/DE-B2R-SendlingSued-Passauerstrasse.xml @@ -0,0 +1,19 @@ + + + + + + +48.110901 +11.518500 + +48.110649 +11.534150 + + + + + + + diff --git a/data/test_data/traff/DE-GM4-UrbanDualCarriagewayEndpoint.xml b/data/test_data/traff/DE-GM4-UrbanDualCarriagewayEndpoint.xml new file mode 100644 index 000000000..5340bdd5e --- /dev/null +++ b/data/test_data/traff/DE-GM4-UrbanDualCarriagewayEndpoint.xml @@ -0,0 +1,22 @@ + + + + + +48.167301 +11.586200 + +48.164200 +11.586500 + + + + + + + + + + + diff --git a/data/test_data/traff/DE-RoundaboutEndpoint.xml b/data/test_data/traff/DE-RoundaboutEndpoint.xml new file mode 100755 index 000000000..616834699 --- /dev/null +++ b/data/test_data/traff/DE-RoundaboutEndpoint.xml @@ -0,0 +1,58 @@ + + + + + +48.661098 +7.936800 + +48.683701 +7.916600 + + + + + + + + + + + +49.822948 +7.906900 + +49.803650 +7.901450 + + + + + + + + + + + +49.239750 +8.222300 + +49.253448 +8.268100 + + + + + + + + + + + +50.372398 +8.038500 + +50.370850 +8.004050 + + + + + + + + + + diff --git a/data/test_data/traff/LT-A5-Kaunas-Eastbound.xml b/data/test_data/traff/LT-A5-Kaunas-Eastbound.xml new file mode 100644 index 000000000..d700afd4c --- /dev/null +++ b/data/test_data/traff/LT-A5-Kaunas-Eastbound.xml @@ -0,0 +1,23 @@ + + + + + + 54.939945 23.879789 + 54.940094 23.881950 + + + + + + + + diff --git a/defines.hpp b/defines.hpp index 79b48e27a..eabc7ac63 100644 --- a/defines.hpp +++ b/defines.hpp @@ -109,8 +109,6 @@ auto constexpr TMP_OFFSETS_EXT = OFFSET_EXT EXTENSION_TMP; #define CROSS_MWM_OSM_WAYS_DIR "cross_mwm_osm_ways" #define TEMP_ADDR_EXTENSION ".tempaddr" -#define TRAFFIC_FILE_EXTENSION ".traffic" - #define SKIPPED_ELEMENTS_FILE "skipped_elements.json" #define MAPCSS_MAPPING_FILE "mapcss-mapping.csv" diff --git a/generator/CMakeLists.txt b/generator/CMakeLists.txt index 9818a561d..15e5e643d 100644 --- a/generator/CMakeLists.txt +++ b/generator/CMakeLists.txt @@ -205,8 +205,6 @@ set(SRC tesselator.hpp towns_dumper.cpp towns_dumper.hpp - traffic_generator.cpp - traffic_generator.hpp transit_generator.cpp transit_generator.hpp transit_generator_experimental.cpp diff --git a/generator/generator_tool/generator_tool.cpp b/generator/generator_tool/generator_tool.cpp index c0dcc8cef..75c887f9e 100644 --- a/generator/generator_tool/generator_tool.cpp +++ b/generator/generator_tool/generator_tool.cpp @@ -26,7 +26,6 @@ #include "generator/routing_world_roads_generator.hpp" #include "generator/search_index_builder.hpp" #include "generator/statistics.hpp" -#include "generator/traffic_generator.hpp" #include "generator/transit_generator.hpp" #include "generator/transit_generator_experimental.hpp" #include "generator/unpack_mwm.hpp" @@ -171,7 +170,6 @@ DEFINE_string(unpack_borders, "", "Convert packed_polygons to a directory of pol DEFINE_bool(unpack_mwm, false, "Unpack each section of mwm into a separate file with name filePath.sectionName."); DEFINE_bool(check_mwm, false, "Check map file to be correct."); DEFINE_string(delete_section, "", "Delete specified section (defines.hpp) from container."); -DEFINE_bool(generate_traffic_keys, false, "Generate keys for the traffic map (road segment -> speed group)."); DEFINE_bool(dump_mwm_tmp, false, "Prints feature builder objects from .mwm.tmp"); @@ -546,12 +544,6 @@ MAIN_WITH_ERROR_HANDLING([](int argc, char ** argv) BuildPopularPlacesFromDescriptions(dataFile); } } - - if (FLAGS_generate_traffic_keys) - { - if (!traffic::GenerateTrafficKeysFromDataFile(dataFile)) - LOG(LCRITICAL, ("Error generating traffic keys.")); - } } string const dataFile = base::JoinPath(path, FLAGS_output + DATA_FILE_EXTENSION); diff --git a/generator/road_access_generator.cpp b/generator/road_access_generator.cpp index 9c5707c14..0420e0967 100644 --- a/generator/road_access_generator.cpp +++ b/generator/road_access_generator.cpp @@ -220,6 +220,7 @@ RoadAccessTagProcessor::RoadAccessTagProcessor(VehicleType vehicleType) : m_vehi switch (vehicleType) { case VehicleType::Car: + case VehicleType::Decoder: // Order is important here starting from most specific (motorcar) to generic (access). m_accessMappings.push_back(&kMotorCarTagMapping); m_accessMappings.push_back(&kMotorVehicleTagMapping); diff --git a/generator/routing_index_generator.cpp b/generator/routing_index_generator.cpp index 094ab83b6..dfd411670 100644 --- a/generator/routing_index_generator.cpp +++ b/generator/routing_index_generator.cpp @@ -22,6 +22,7 @@ #include "routing_common/bicycle_model.hpp" #include "routing_common/car_model.hpp" +#include "routing_common/decoder_model.hpp" #include "routing_common/pedestrian_model.hpp" #include "indexer/feature.hpp" @@ -61,19 +62,17 @@ class VehicleMaskBuilder final : m_pedestrianModel(PedestrianModelFactory(countryParentNameGetterFn).GetVehicleModelForCountry(country)) , m_bicycleModel(BicycleModelFactory(countryParentNameGetterFn).GetVehicleModelForCountry(country)) , m_carModel(CarModelFactory(countryParentNameGetterFn).GetVehicleModelForCountry(country)) - , m_constructionType(classif().GetTypeByPath({"highway", "construction"})) + , m_decoderModel(DecoderModelFactory(countryParentNameGetterFn).GetVehicleModelForCountry(country)) { CHECK(m_pedestrianModel, ()); CHECK(m_bicycleModel, ()); CHECK(m_carModel, ()); + CHECK(m_decoderModel, ()); } VehicleMask CalcRoadMask(FeatureType & f) const { feature::TypesHolder const types(f); - if (types.HasWithSubclass(m_constructionType)) - return 0; - return CalcMask([&](VehicleModelInterface const & model) { return model.IsRoad(types); }); } @@ -94,6 +93,8 @@ class VehicleMaskBuilder final mask |= kBicycleMask; if (fn(*m_carModel)) mask |= kCarMask; + if (fn(*m_decoderModel)) + mask |= kDecoderMask; return mask; } @@ -101,8 +102,7 @@ class VehicleMaskBuilder final std::shared_ptr const m_pedestrianModel; std::shared_ptr const m_bicycleModel; std::shared_ptr const m_carModel; - - uint32_t const m_constructionType; + std::shared_ptr const m_decoderModel; }; class Processor final diff --git a/generator/traffic_generator.cpp b/generator/traffic_generator.cpp deleted file mode 100644 index c2dd80217..000000000 --- a/generator/traffic_generator.cpp +++ /dev/null @@ -1,44 +0,0 @@ -#include "generator/traffic_generator.hpp" - -#include "routing/routing_helpers.hpp" - -#include "traffic/traffic_info.hpp" - -#include "routing_common/car_model.hpp" - -#include "platform/mwm_traits.hpp" - -#include "indexer/feature_algo.hpp" -#include "indexer/feature_processor.hpp" -#include "indexer/features_offsets_table.hpp" - -#include "coding/file_writer.hpp" -#include "coding/files_container.hpp" - -#include - -namespace traffic -{ -bool GenerateTrafficKeysFromDataFile(std::string const & mwmPath) -{ - try - { - std::vector keys; - TrafficInfo::ExtractTrafficKeys(mwmPath, keys); - - std::vector buf; - TrafficInfo::SerializeTrafficKeys(keys, buf); - - FilesContainerW writeContainer(mwmPath, FileWriter::OP_WRITE_EXISTING); - auto writer = writeContainer.GetWriter(TRAFFIC_KEYS_FILE_TAG); - writer->Write(buf.data(), buf.size()); - } - catch (RootException const & e) - { - LOG(LERROR, ("Failed to build traffic keys:", e.Msg())); - return false; - } - - return true; -} -} // namespace traffic diff --git a/generator/traffic_generator.hpp b/generator/traffic_generator.hpp deleted file mode 100644 index f09cc24cc..000000000 --- a/generator/traffic_generator.hpp +++ /dev/null @@ -1,8 +0,0 @@ -#pragma once - -#include - -namespace traffic -{ -bool GenerateTrafficKeysFromDataFile(std::string const & mwmPath); -} // namespace traffic diff --git a/iphone/Maps/Core/Settings/MWMSettings.h b/iphone/Maps/Core/Settings/MWMSettings.h index da011bfad..263155dfa 100644 --- a/iphone/Maps/Core/Settings/MWMSettings.h +++ b/iphone/Maps/Core/Settings/MWMSettings.h @@ -1,6 +1,12 @@ NS_SWIFT_NAME(SettingsBridge) @interface MWMSettings : NSObject ++ (BOOL)liveTrafficEnabled; ++ (void)setLiveTrafficEnabled:(BOOL)liveTrafficEnabled; + ++ (NSURL *)liveTrafficUrl; ++ (void)setLiveTrafficUrl:(NSURL *)liveTrafficUrl; + + (BOOL)buildings3dViewEnabled; + (void)setBuildings3dViewEnabled:(BOOL)buildings3dViewEnabled; diff --git a/iphone/Maps/Core/Settings/MWMSettings.mm b/iphone/Maps/Core/Settings/MWMSettings.mm index 40f92d59d..ca2e42267 100644 --- a/iphone/Maps/Core/Settings/MWMSettings.mm +++ b/iphone/Maps/Core/Settings/MWMSettings.mm @@ -25,6 +25,40 @@ @implementation MWMSettings ++ (BOOL)liveTrafficEnabled; +{ + return GetFramework().LoadTrafficHttpEnabled(); +} + ++ (void)setLiveTrafficEnabled:(BOOL)liveTrafficEnabled; +{ + auto &f = GetFramework(); + f.SaveTrafficHttpEnabled(liveTrafficEnabled); + f.SetTrafficHttpEnabled(liveTrafficEnabled); +} + ++ (NSURL *)liveTrafficUrl; +{ + NSString * link = @(GetFramework().LoadTrafficHttpUrl().c_str()); + if ([link length] == 0) { + return nil; + } else { + return [NSURL URLWithString:link]; + } +} + ++ (void)setLiveTrafficUrl:(NSURL *)liveTrafficUrl; +{ + auto &f = GetFramework(); + if (liveTrafficUrl == nil) { + f.SaveTrafficHttpUrl(@"".UTF8String); + f.SetTrafficHttpUrl(@"".UTF8String); + } else { + f.SaveTrafficHttpUrl(liveTrafficUrl.absoluteString.UTF8String); + f.SetTrafficHttpUrl(liveTrafficUrl.absoluteString.UTF8String); + } +} + + (BOOL)buildings3dViewEnabled; { bool _ = true, on = true; diff --git a/iphone/Maps/LocalizedStrings/af.lproj/LocalizableTypes.strings b/iphone/Maps/LocalizedStrings/af.lproj/LocalizableTypes.strings index cd7a56a43..f47faaab2 100644 --- a/iphone/Maps/LocalizedStrings/af.lproj/LocalizableTypes.strings +++ b/iphone/Maps/LocalizedStrings/af.lproj/LocalizableTypes.strings @@ -452,6 +452,22 @@ "type.highway.busway.tunnel" = "Tonnel"; "type.highway.bus_stop" = "Bushalte"; "type.highway.construction" = "Pad in aanbou"; +"type.highway.construction.motorway" = "Pad in aanbou"; +"type.highway.construction.motorway_link" = "Pad in aanbou"; +"type.highway.construction.trunk" = "Pad in aanbou"; +"type.highway.construction.trunk_link" = "Pad in aanbou"; +"type.highway.construction.primary" = "Pad in aanbou"; +"type.highway.construction.primary_link" = "Pad in aanbou"; +"type.highway.construction.secondary" = "Pad in aanbou"; +"type.highway.construction.secondary_link" = "Pad in aanbou"; +"type.highway.construction.tertiary" = "Pad in aanbou"; +"type.highway.construction.tertiary_link" = "Pad in aanbou"; +"type.highway.construction.residential" = "Pad in aanbou"; +"type.highway.construction.unclassified" = "Pad in aanbou"; +"type.highway.construction.service" = "Pad in aanbou"; +"type.highway.construction.living_street" = "Pad in aanbou"; +"type.highway.construction.road" = "Pad in aanbou"; +"type.highway.construction.track" = "Pad in aanbou"; "type.highway.cycleway" = "Fietspad"; /* These translations are used for all type.highway.*.bridge. */ "type.highway.cycleway.bridge" = "Brug"; diff --git a/iphone/Maps/LocalizedStrings/ar.lproj/LocalizableTypes.strings b/iphone/Maps/LocalizedStrings/ar.lproj/LocalizableTypes.strings index 33224871a..331e949ca 100644 --- a/iphone/Maps/LocalizedStrings/ar.lproj/LocalizableTypes.strings +++ b/iphone/Maps/LocalizedStrings/ar.lproj/LocalizableTypes.strings @@ -452,6 +452,22 @@ "type.highway.busway.tunnel" = "نفق"; "type.highway.bus_stop" = "موقف حافلات"; "type.highway.construction" = "طريق تحت الإنشاء"; +"type.highway.construction.motorway" = "طريق تحت الإنشاء"; +"type.highway.construction.motorway_link" = "طريق تحت الإنشاء"; +"type.highway.construction.trunk" = "طريق تحت الإنشاء"; +"type.highway.construction.trunk_link" = "طريق تحت الإنشاء"; +"type.highway.construction.primary" = "طريق تحت الإنشاء"; +"type.highway.construction.primary_link" = "طريق تحت الإنشاء"; +"type.highway.construction.secondary" = "طريق تحت الإنشاء"; +"type.highway.construction.secondary_link" = "طريق تحت الإنشاء"; +"type.highway.construction.tertiary" = "طريق تحت الإنشاء"; +"type.highway.construction.tertiary_link" = "طريق تحت الإنشاء"; +"type.highway.construction.residential" = "طريق تحت الإنشاء"; +"type.highway.construction.unclassified" = "طريق تحت الإنشاء"; +"type.highway.construction.service" = "طريق تحت الإنشاء"; +"type.highway.construction.living_street" = "طريق تحت الإنشاء"; +"type.highway.construction.road" = "طريق تحت الإنشاء"; +"type.highway.construction.track" = "طريق تحت الإنشاء"; "type.highway.cycleway" = "مسار للدراجات"; /* These translations are used for all type.highway.*.bridge. */ "type.highway.cycleway.bridge" = "جسر"; diff --git a/iphone/Maps/LocalizedStrings/az.lproj/LocalizableTypes.strings b/iphone/Maps/LocalizedStrings/az.lproj/LocalizableTypes.strings index 7045faf53..1cc3d133c 100644 --- a/iphone/Maps/LocalizedStrings/az.lproj/LocalizableTypes.strings +++ b/iphone/Maps/LocalizedStrings/az.lproj/LocalizableTypes.strings @@ -452,6 +452,22 @@ "type.highway.busway.tunnel" = "Tunel"; "type.highway.bus_stop" = "Avtobus dayanacağı"; "type.highway.construction" = "Tikili Yol"; +"type.highway.construction.motorway" = "Tikili Yol"; +"type.highway.construction.motorway_link" = "Tikili Yol"; +"type.highway.construction.trunk" = "Tikili Yol"; +"type.highway.construction.trunk_link" = "Tikili Yol"; +"type.highway.construction.primary" = "Tikili Yol"; +"type.highway.construction.primary_link" = "Tikili Yol"; +"type.highway.construction.secondary" = "Tikili Yol"; +"type.highway.construction.secondary_link" = "Tikili Yol"; +"type.highway.construction.tertiary" = "Tikili Yol"; +"type.highway.construction.tertiary_link" = "Tikili Yol"; +"type.highway.construction.residential" = "Tikili Yol"; +"type.highway.construction.unclassified" = "Tikili Yol"; +"type.highway.construction.service" = "Tikili Yol"; +"type.highway.construction.living_street" = "Tikili Yol"; +"type.highway.construction.road" = "Tikili Yol"; +"type.highway.construction.track" = "Tikili Yol"; "type.highway.cycleway" = "Velosiped yolu"; /* These translations are used for all type.highway.*.bridge. */ "type.highway.cycleway.bridge" = "Körpü"; diff --git a/iphone/Maps/LocalizedStrings/bg.lproj/LocalizableTypes.strings b/iphone/Maps/LocalizedStrings/bg.lproj/LocalizableTypes.strings index 43a70662e..3b887aa1b 100644 --- a/iphone/Maps/LocalizedStrings/bg.lproj/LocalizableTypes.strings +++ b/iphone/Maps/LocalizedStrings/bg.lproj/LocalizableTypes.strings @@ -452,6 +452,22 @@ "type.highway.busway.tunnel" = "Тунел"; "type.highway.bus_stop" = "Спирка"; "type.highway.construction" = "Път в процес на изграждане"; +"type.highway.construction.motorway" = "Път в процес на изграждане"; +"type.highway.construction.motorway_link" = "Път в процес на изграждане"; +"type.highway.construction.trunk" = "Път в процес на изграждане"; +"type.highway.construction.trunk_link" = "Път в процес на изграждане"; +"type.highway.construction.primary" = "Път в процес на изграждане"; +"type.highway.construction.primary_link" = "Път в процес на изграждане"; +"type.highway.construction.secondary" = "Път в процес на изграждане"; +"type.highway.construction.secondary_link" = "Път в процес на изграждане"; +"type.highway.construction.tertiary" = "Път в процес на изграждане"; +"type.highway.construction.tertiary_link" = "Път в процес на изграждане"; +"type.highway.construction.residential" = "Път в процес на изграждане"; +"type.highway.construction.unclassified" = "Път в процес на изграждане"; +"type.highway.construction.service" = "Път в процес на изграждане"; +"type.highway.construction.living_street" = "Път в процес на изграждане"; +"type.highway.construction.road" = "Път в процес на изграждане"; +"type.highway.construction.track" = "Път в процес на изграждане"; "type.highway.cycleway" = "Пътека за колела"; /* These translations are used for all type.highway.*.bridge. */ "type.highway.cycleway.bridge" = "Мост"; diff --git a/iphone/Maps/LocalizedStrings/ca.lproj/LocalizableTypes.strings b/iphone/Maps/LocalizedStrings/ca.lproj/LocalizableTypes.strings index a552c8947..389e1eb25 100644 --- a/iphone/Maps/LocalizedStrings/ca.lproj/LocalizableTypes.strings +++ b/iphone/Maps/LocalizedStrings/ca.lproj/LocalizableTypes.strings @@ -452,6 +452,22 @@ "type.highway.busway.tunnel" = "Túnel"; "type.highway.bus_stop" = "Parada d’autobús"; "type.highway.construction" = "Via en construcció"; +"type.highway.construction.motorway" = "Via en construcció"; +"type.highway.construction.motorway_link" = "Via en construcció"; +"type.highway.construction.trunk" = "Via en construcció"; +"type.highway.construction.trunk_link" = "Via en construcció"; +"type.highway.construction.primary" = "Via en construcció"; +"type.highway.construction.primary_link" = "Via en construcció"; +"type.highway.construction.secondary" = "Via en construcció"; +"type.highway.construction.secondary_link" = "Via en construcció"; +"type.highway.construction.tertiary" = "Via en construcció"; +"type.highway.construction.tertiary_link" = "Via en construcció"; +"type.highway.construction.residential" = "Via en construcció"; +"type.highway.construction.unclassified" = "Via en construcció"; +"type.highway.construction.service" = "Via en construcció"; +"type.highway.construction.living_street" = "Via en construcció"; +"type.highway.construction.road" = "Via en construcció"; +"type.highway.construction.track" = "Via en construcció"; "type.highway.cycleway" = "Carril bici"; /* These translations are used for all type.highway.*.bridge. */ "type.highway.cycleway.bridge" = "Pont"; diff --git a/iphone/Maps/LocalizedStrings/cs.lproj/LocalizableTypes.strings b/iphone/Maps/LocalizedStrings/cs.lproj/LocalizableTypes.strings index c6fb365e1..487e5cbe2 100644 --- a/iphone/Maps/LocalizedStrings/cs.lproj/LocalizableTypes.strings +++ b/iphone/Maps/LocalizedStrings/cs.lproj/LocalizableTypes.strings @@ -452,6 +452,22 @@ "type.highway.busway.tunnel" = "Tunel"; "type.highway.bus_stop" = "Autobusová zastávka"; "type.highway.construction" = "Silnice v rekonstrukci"; +"type.highway.construction.motorway" = "Silnice v rekonstrukci"; +"type.highway.construction.motorway_link" = "Silnice v rekonstrukci"; +"type.highway.construction.trunk" = "Silnice v rekonstrukci"; +"type.highway.construction.trunk_link" = "Silnice v rekonstrukci"; +"type.highway.construction.primary" = "Silnice v rekonstrukci"; +"type.highway.construction.primary_link" = "Silnice v rekonstrukci"; +"type.highway.construction.secondary" = "Silnice v rekonstrukci"; +"type.highway.construction.secondary_link" = "Silnice v rekonstrukci"; +"type.highway.construction.tertiary" = "Silnice v rekonstrukci"; +"type.highway.construction.tertiary_link" = "Silnice v rekonstrukci"; +"type.highway.construction.residential" = "Silnice v rekonstrukci"; +"type.highway.construction.unclassified" = "Silnice v rekonstrukci"; +"type.highway.construction.service" = "Silnice v rekonstrukci"; +"type.highway.construction.living_street" = "Silnice v rekonstrukci"; +"type.highway.construction.road" = "Silnice v rekonstrukci"; +"type.highway.construction.track" = "Silnice v rekonstrukci"; "type.highway.cycleway" = "Cyklostezka"; /* These translations are used for all type.highway.*.bridge. */ "type.highway.cycleway.bridge" = "Most"; diff --git a/iphone/Maps/LocalizedStrings/da.lproj/LocalizableTypes.strings b/iphone/Maps/LocalizedStrings/da.lproj/LocalizableTypes.strings index 5ef17b5da..0397a1007 100644 --- a/iphone/Maps/LocalizedStrings/da.lproj/LocalizableTypes.strings +++ b/iphone/Maps/LocalizedStrings/da.lproj/LocalizableTypes.strings @@ -452,6 +452,22 @@ "type.highway.busway.tunnel" = "Tunnel"; "type.highway.bus_stop" = "Busstoppested"; "type.highway.construction" = "Vej under anlæggelse"; +"type.highway.construction.motorway" = "Vej under anlæggelse"; +"type.highway.construction.motorway_link" = "Vej under anlæggelse"; +"type.highway.construction.trunk" = "Vej under anlæggelse"; +"type.highway.construction.trunk_link" = "Vej under anlæggelse"; +"type.highway.construction.primary" = "Vej under anlæggelse"; +"type.highway.construction.primary_link" = "Vej under anlæggelse"; +"type.highway.construction.secondary" = "Vej under anlæggelse"; +"type.highway.construction.secondary_link" = "Vej under anlæggelse"; +"type.highway.construction.tertiary" = "Vej under anlæggelse"; +"type.highway.construction.tertiary_link" = "Vej under anlæggelse"; +"type.highway.construction.residential" = "Vej under anlæggelse"; +"type.highway.construction.unclassified" = "Vej under anlæggelse"; +"type.highway.construction.service" = "Vej under anlæggelse"; +"type.highway.construction.living_street" = "Vej under anlæggelse"; +"type.highway.construction.road" = "Vej under anlæggelse"; +"type.highway.construction.track" = "Vej under anlæggelse"; "type.highway.cycleway" = "Cykelsti"; /* These translations are used for all type.highway.*.bridge. */ "type.highway.cycleway.bridge" = "Bro"; diff --git a/iphone/Maps/LocalizedStrings/de.lproj/LocalizableTypes.strings b/iphone/Maps/LocalizedStrings/de.lproj/LocalizableTypes.strings index 0be87478c..d50f857bb 100644 --- a/iphone/Maps/LocalizedStrings/de.lproj/LocalizableTypes.strings +++ b/iphone/Maps/LocalizedStrings/de.lproj/LocalizableTypes.strings @@ -452,6 +452,22 @@ "type.highway.busway.tunnel" = "Tunnel"; "type.highway.bus_stop" = "Bushaltestelle"; "type.highway.construction" = "Straße im Bau"; +"type.highway.construction.motorway" = "Straße im Bau"; +"type.highway.construction.motorway_link" = "Straße im Bau"; +"type.highway.construction.trunk" = "Straße im Bau"; +"type.highway.construction.trunk_link" = "Straße im Bau"; +"type.highway.construction.primary" = "Straße im Bau"; +"type.highway.construction.primary_link" = "Straße im Bau"; +"type.highway.construction.secondary" = "Straße im Bau"; +"type.highway.construction.secondary_link" = "Straße im Bau"; +"type.highway.construction.tertiary" = "Straße im Bau"; +"type.highway.construction.tertiary_link" = "Straße im Bau"; +"type.highway.construction.residential" = "Straße im Bau"; +"type.highway.construction.unclassified" = "Straße im Bau"; +"type.highway.construction.service" = "Straße im Bau"; +"type.highway.construction.living_street" = "Straße im Bau"; +"type.highway.construction.road" = "Straße im Bau"; +"type.highway.construction.track" = "Straße im Bau"; "type.highway.cycleway" = "Radweg"; /* These translations are used for all type.highway.*.bridge. */ "type.highway.cycleway.bridge" = "Brücke"; diff --git a/iphone/Maps/LocalizedStrings/el.lproj/LocalizableTypes.strings b/iphone/Maps/LocalizedStrings/el.lproj/LocalizableTypes.strings index cbd6fb769..30c13237c 100644 --- a/iphone/Maps/LocalizedStrings/el.lproj/LocalizableTypes.strings +++ b/iphone/Maps/LocalizedStrings/el.lproj/LocalizableTypes.strings @@ -452,6 +452,22 @@ "type.highway.busway.tunnel" = "Σήραγγα"; "type.highway.bus_stop" = "Στάση λεωφορείου"; "type.highway.construction" = "Οδός υπό κατασκευή"; +"type.highway.construction.motorway" = "Οδός υπό κατασκευή"; +"type.highway.construction.motorway_link" = "Οδός υπό κατασκευή"; +"type.highway.construction.trunk" = "Οδός υπό κατασκευή"; +"type.highway.construction.trunk_link" = "Οδός υπό κατασκευή"; +"type.highway.construction.primary" = "Οδός υπό κατασκευή"; +"type.highway.construction.primary_link" = "Οδός υπό κατασκευή"; +"type.highway.construction.secondary" = "Οδός υπό κατασκευή"; +"type.highway.construction.secondary_link" = "Οδός υπό κατασκευή"; +"type.highway.construction.tertiary" = "Οδός υπό κατασκευή"; +"type.highway.construction.tertiary_link" = "Οδός υπό κατασκευή"; +"type.highway.construction.residential" = "Οδός υπό κατασκευή"; +"type.highway.construction.unclassified" = "Οδός υπό κατασκευή"; +"type.highway.construction.service" = "Οδός υπό κατασκευή"; +"type.highway.construction.living_street" = "Οδός υπό κατασκευή"; +"type.highway.construction.road" = "Οδός υπό κατασκευή"; +"type.highway.construction.track" = "Οδός υπό κατασκευή"; "type.highway.cycleway" = "Ποδηλατόδρομος"; /* These translations are used for all type.highway.*.bridge. */ "type.highway.cycleway.bridge" = "Γέφυρα"; diff --git a/iphone/Maps/LocalizedStrings/en-GB.lproj/Localizable.strings b/iphone/Maps/LocalizedStrings/en-GB.lproj/Localizable.strings index cb1ae7f09..c9265653f 100644 --- a/iphone/Maps/LocalizedStrings/en-GB.lproj/Localizable.strings +++ b/iphone/Maps/LocalizedStrings/en-GB.lproj/Localizable.strings @@ -538,6 +538,14 @@ "editor_place_doesnt_exist" = "Place does not exist"; "editor_place_doesnt_exist_description" = "Describe what the place looks like now to send an error note to the OpenStreetMap community"; "text_more_button" = "…more"; + +/* Live traffic data */ +"traffic_http" = "Live Traffic"; +"traffic_http_enabled" = "Enable live traffic data"; +"traffic_http_enabled_description" = "When enabled, the app will periodically retrieve traffic information from the configured URL."; +"traffic_http_url" = "Traffic service URL"; +"traffic_http_url_not_set" = "Not set"; + /* Phone number error message */ "error_enter_correct_phone" = "Enter a valid phone number"; "error_enter_correct_web" = "Enter a valid web address"; @@ -626,6 +634,7 @@ "privacy_policy" = "Privacy policy"; "terms_of_use" = "Terms of use"; "button_layer_subway" = "Metro"; +"button_layer_traffic" = "Traffic"; "layers_title" = "Map Styles and Layers"; "subway_data_unavailable" = "Metro map is unavailable"; "title_error_downloading_bookmarks" = "An error occurred"; diff --git a/iphone/Maps/LocalizedStrings/en.lproj/Localizable.strings b/iphone/Maps/LocalizedStrings/en.lproj/Localizable.strings index cc6260d1d..f08ebb8b0 100644 --- a/iphone/Maps/LocalizedStrings/en.lproj/Localizable.strings +++ b/iphone/Maps/LocalizedStrings/en.lproj/Localizable.strings @@ -538,6 +538,14 @@ "editor_place_doesnt_exist" = "Place does not exist"; "editor_place_doesnt_exist_description" = "Describe what the place looks like now to send an error note to the OpenStreetMap community"; "text_more_button" = "…more"; + +/* Live traffic data */ +"traffic_http" = "Live Traffic"; +"traffic_http_enabled" = "Enable live traffic data"; +"traffic_http_enabled_description" = "When enabled, the app will periodically retrieve traffic information from the configured URL."; +"traffic_http_url" = "Traffic service URL"; +"traffic_http_url_not_set" = "Not set"; + /* Phone number error message */ "error_enter_correct_phone" = "Enter a valid phone number"; "error_enter_correct_web" = "Enter a valid web address"; @@ -626,6 +634,7 @@ "privacy_policy" = "Privacy policy"; "terms_of_use" = "Terms of use"; "button_layer_subway" = "Subway"; +"button_layer_traffic" = "Traffic"; "layers_title" = "Map Styles and Layers"; "subway_data_unavailable" = "Subway map is unavailable"; "title_error_downloading_bookmarks" = "An error occurred"; diff --git a/iphone/Maps/LocalizedStrings/es-MX.lproj/LocalizableTypes.strings b/iphone/Maps/LocalizedStrings/es-MX.lproj/LocalizableTypes.strings index cd4a82b12..34cb13dac 100644 --- a/iphone/Maps/LocalizedStrings/es-MX.lproj/LocalizableTypes.strings +++ b/iphone/Maps/LocalizedStrings/es-MX.lproj/LocalizableTypes.strings @@ -452,6 +452,22 @@ "type.highway.busway.tunnel" = "Túnel"; "type.highway.bus_stop" = "Parada de autobús"; "type.highway.construction" = "Vía en construcción"; +"type.highway.construction.motorway" = "Vía en construcción"; +"type.highway.construction.motorway_link" = "Vía en construcción"; +"type.highway.construction.trunk" = "Vía en construcción"; +"type.highway.construction.trunk_link" = "Vía en construcción"; +"type.highway.construction.primary" = "Vía en construcción"; +"type.highway.construction.primary_link" = "Vía en construcción"; +"type.highway.construction.secondary" = "Vía en construcción"; +"type.highway.construction.secondary_link" = "Vía en construcción"; +"type.highway.construction.tertiary" = "Vía en construcción"; +"type.highway.construction.tertiary_link" = "Vía en construcción"; +"type.highway.construction.residential" = "Vía en construcción"; +"type.highway.construction.unclassified" = "Vía en construcción"; +"type.highway.construction.service" = "Vía en construcción"; +"type.highway.construction.living_street" = "Vía en construcción"; +"type.highway.construction.road" = "Vía en construcción"; +"type.highway.construction.track" = "Vía en construcción"; "type.highway.cycleway" = "Ciclovía"; /* These translations are used for all type.highway.*.bridge. */ "type.highway.cycleway.bridge" = "Puente"; diff --git a/iphone/Maps/LocalizedStrings/es.lproj/LocalizableTypes.strings b/iphone/Maps/LocalizedStrings/es.lproj/LocalizableTypes.strings index d1b2f4e87..ef978a6a7 100644 --- a/iphone/Maps/LocalizedStrings/es.lproj/LocalizableTypes.strings +++ b/iphone/Maps/LocalizedStrings/es.lproj/LocalizableTypes.strings @@ -452,6 +452,22 @@ "type.highway.busway.tunnel" = "Túnel"; "type.highway.bus_stop" = "Parada de bus"; "type.highway.construction" = "Vía en construcción"; +"type.highway.construction.motorway" = "Vía en construcción"; +"type.highway.construction.motorway_link" = "Vía en construcción"; +"type.highway.construction.trunk" = "Vía en construcción"; +"type.highway.construction.trunk_link" = "Vía en construcción"; +"type.highway.construction.primary" = "Vía en construcción"; +"type.highway.construction.primary_link" = "Vía en construcción"; +"type.highway.construction.secondary" = "Vía en construcción"; +"type.highway.construction.secondary_link" = "Vía en construcción"; +"type.highway.construction.tertiary" = "Vía en construcción"; +"type.highway.construction.tertiary_link" = "Vía en construcción"; +"type.highway.construction.residential" = "Vía en construcción"; +"type.highway.construction.unclassified" = "Vía en construcción"; +"type.highway.construction.service" = "Vía en construcción"; +"type.highway.construction.living_street" = "Vía en construcción"; +"type.highway.construction.road" = "Vía en construcción"; +"type.highway.construction.track" = "Vía en construcción"; "type.highway.cycleway" = "Ciclovía"; /* These translations are used for all type.highway.*.bridge. */ "type.highway.cycleway.bridge" = "Puente"; diff --git a/iphone/Maps/LocalizedStrings/et.lproj/LocalizableTypes.strings b/iphone/Maps/LocalizedStrings/et.lproj/LocalizableTypes.strings index 5351c7076..3ebbfc57a 100644 --- a/iphone/Maps/LocalizedStrings/et.lproj/LocalizableTypes.strings +++ b/iphone/Maps/LocalizedStrings/et.lproj/LocalizableTypes.strings @@ -452,6 +452,22 @@ "type.highway.busway.tunnel" = "Tunnel"; "type.highway.bus_stop" = "Bussipeatus"; "type.highway.construction" = "Ehitusjärgus tee"; +"type.highway.construction.motorway" = "Ehitusjärgus tee"; +"type.highway.construction.motorway_link" = "Ehitusjärgus tee"; +"type.highway.construction.trunk" = "Ehitusjärgus tee"; +"type.highway.construction.trunk_link" = "Ehitusjärgus tee"; +"type.highway.construction.primary" = "Ehitusjärgus tee"; +"type.highway.construction.primary_link" = "Ehitusjärgus tee"; +"type.highway.construction.secondary" = "Ehitusjärgus tee"; +"type.highway.construction.secondary_link" = "Ehitusjärgus tee"; +"type.highway.construction.tertiary" = "Ehitusjärgus tee"; +"type.highway.construction.tertiary_link" = "Ehitusjärgus tee"; +"type.highway.construction.residential" = "Ehitusjärgus tee"; +"type.highway.construction.unclassified" = "Ehitusjärgus tee"; +"type.highway.construction.service" = "Ehitusjärgus tee"; +"type.highway.construction.living_street" = "Ehitusjärgus tee"; +"type.highway.construction.road" = "Ehitusjärgus tee"; +"type.highway.construction.track" = "Ehitusjärgus tee"; "type.highway.cycleway" = "Jalgrattatee"; /* These translations are used for all type.highway.*.bridge. */ "type.highway.cycleway.bridge" = "Jalgrattasild"; diff --git a/iphone/Maps/LocalizedStrings/eu.lproj/LocalizableTypes.strings b/iphone/Maps/LocalizedStrings/eu.lproj/LocalizableTypes.strings index 48cceea08..ae9d56c0d 100644 --- a/iphone/Maps/LocalizedStrings/eu.lproj/LocalizableTypes.strings +++ b/iphone/Maps/LocalizedStrings/eu.lproj/LocalizableTypes.strings @@ -452,6 +452,22 @@ "type.highway.busway.tunnel" = "Tunela"; "type.highway.bus_stop" = "Autobus geltokia"; "type.highway.construction" = "Eraikitzen ari diren errepidea"; +"type.highway.construction.motorway" = "Eraikitzen ari diren errepidea"; +"type.highway.construction.motorway_link" = "Eraikitzen ari diren errepidea"; +"type.highway.construction.trunk" = "Eraikitzen ari diren errepidea"; +"type.highway.construction.trunk_link" = "Eraikitzen ari diren errepidea"; +"type.highway.construction.primary" = "Eraikitzen ari diren errepidea"; +"type.highway.construction.primary_link" = "Eraikitzen ari diren errepidea"; +"type.highway.construction.secondary" = "Eraikitzen ari diren errepidea"; +"type.highway.construction.secondary_link" = "Eraikitzen ari diren errepidea"; +"type.highway.construction.tertiary" = "Eraikitzen ari diren errepidea"; +"type.highway.construction.tertiary_link" = "Eraikitzen ari diren errepidea"; +"type.highway.construction.residential" = "Eraikitzen ari diren errepidea"; +"type.highway.construction.unclassified" = "Eraikitzen ari diren errepidea"; +"type.highway.construction.service" = "Eraikitzen ari diren errepidea"; +"type.highway.construction.living_street" = "Eraikitzen ari diren errepidea"; +"type.highway.construction.road" = "Eraikitzen ari diren errepidea"; +"type.highway.construction.track" = "Eraikitzen ari diren errepidea"; "type.highway.cycleway" = "Bidegorri edo bizikleta-bidea"; /* These translations are used for all type.highway.*.bridge. */ "type.highway.cycleway.bridge" = "Zubia"; diff --git a/iphone/Maps/LocalizedStrings/fa.lproj/LocalizableTypes.strings b/iphone/Maps/LocalizedStrings/fa.lproj/LocalizableTypes.strings index b4874226f..b22b14412 100644 --- a/iphone/Maps/LocalizedStrings/fa.lproj/LocalizableTypes.strings +++ b/iphone/Maps/LocalizedStrings/fa.lproj/LocalizableTypes.strings @@ -452,6 +452,22 @@ "type.highway.busway.tunnel" = "تونل"; "type.highway.bus_stop" = "حمل و نقل"; "type.highway.construction" = "جاده در دست ساخت است"; +"type.highway.construction.motorway" = "جاده در دست ساخت است"; +"type.highway.construction.motorway_link" = "جاده در دست ساخت است"; +"type.highway.construction.trunk" = "جاده در دست ساخت است"; +"type.highway.construction.trunk_link" = "جاده در دست ساخت است"; +"type.highway.construction.primary" = "جاده در دست ساخت است"; +"type.highway.construction.primary_link" = "جاده در دست ساخت است"; +"type.highway.construction.secondary" = "جاده در دست ساخت است"; +"type.highway.construction.secondary_link" = "جاده در دست ساخت است"; +"type.highway.construction.tertiary" = "جاده در دست ساخت است"; +"type.highway.construction.tertiary_link" = "جاده در دست ساخت است"; +"type.highway.construction.residential" = "جاده در دست ساخت است"; +"type.highway.construction.unclassified" = "جاده در دست ساخت است"; +"type.highway.construction.service" = "جاده در دست ساخت است"; +"type.highway.construction.living_street" = "جاده در دست ساخت است"; +"type.highway.construction.road" = "جاده در دست ساخت است"; +"type.highway.construction.track" = "جاده در دست ساخت است"; "type.highway.cycleway" = "Cycle Path"; /* These translations are used for all type.highway.*.bridge. */ "type.highway.cycleway.bridge" = "پل"; diff --git a/iphone/Maps/LocalizedStrings/fi.lproj/LocalizableTypes.strings b/iphone/Maps/LocalizedStrings/fi.lproj/LocalizableTypes.strings index b2a48f0c8..ec141413b 100644 --- a/iphone/Maps/LocalizedStrings/fi.lproj/LocalizableTypes.strings +++ b/iphone/Maps/LocalizedStrings/fi.lproj/LocalizableTypes.strings @@ -452,6 +452,22 @@ "type.highway.busway.tunnel" = "Tunneli"; "type.highway.bus_stop" = "Bussipysäkki"; "type.highway.construction" = "Rakenteilla oleva tie"; +"type.highway.construction.motorway" = "Rakenteilla oleva tie"; +"type.highway.construction.motorway_link" = "Rakenteilla oleva tie"; +"type.highway.construction.trunk" = "Rakenteilla oleva tie"; +"type.highway.construction.trunk_link" = "Rakenteilla oleva tie"; +"type.highway.construction.primary" = "Rakenteilla oleva tie"; +"type.highway.construction.primary_link" = "Rakenteilla oleva tie"; +"type.highway.construction.secondary" = "Rakenteilla oleva tie"; +"type.highway.construction.secondary_link" = "Rakenteilla oleva tie"; +"type.highway.construction.tertiary" = "Rakenteilla oleva tie"; +"type.highway.construction.tertiary_link" = "Rakenteilla oleva tie"; +"type.highway.construction.residential" = "Rakenteilla oleva tie"; +"type.highway.construction.unclassified" = "Rakenteilla oleva tie"; +"type.highway.construction.service" = "Rakenteilla oleva tie"; +"type.highway.construction.living_street" = "Rakenteilla oleva tie"; +"type.highway.construction.road" = "Rakenteilla oleva tie"; +"type.highway.construction.track" = "Rakenteilla oleva tie"; "type.highway.cycleway" = "Pyörätie"; /* These translations are used for all type.highway.*.bridge. */ "type.highway.cycleway.bridge" = "Silta"; diff --git a/iphone/Maps/LocalizedStrings/fr.lproj/LocalizableTypes.strings b/iphone/Maps/LocalizedStrings/fr.lproj/LocalizableTypes.strings index a59cef009..5e4f0431b 100644 --- a/iphone/Maps/LocalizedStrings/fr.lproj/LocalizableTypes.strings +++ b/iphone/Maps/LocalizedStrings/fr.lproj/LocalizableTypes.strings @@ -452,6 +452,22 @@ "type.highway.busway.tunnel" = "Tunnel"; "type.highway.bus_stop" = "Arrêt de bus"; "type.highway.construction" = "Route en construction"; +"type.highway.construction.motorway" = "Route en construction"; +"type.highway.construction.motorway_link" = "Route en construction"; +"type.highway.construction.trunk" = "Route en construction"; +"type.highway.construction.trunk_link" = "Route en construction"; +"type.highway.construction.primary" = "Route en construction"; +"type.highway.construction.primary_link" = "Route en construction"; +"type.highway.construction.secondary" = "Route en construction"; +"type.highway.construction.secondary_link" = "Route en construction"; +"type.highway.construction.tertiary" = "Route en construction"; +"type.highway.construction.tertiary_link" = "Route en construction"; +"type.highway.construction.residential" = "Route en construction"; +"type.highway.construction.unclassified" = "Route en construction"; +"type.highway.construction.service" = "Route en construction"; +"type.highway.construction.living_street" = "Route en construction"; +"type.highway.construction.road" = "Route en construction"; +"type.highway.construction.track" = "Route en construction"; "type.highway.cycleway" = "Piste cyclable"; /* These translations are used for all type.highway.*.bridge. */ "type.highway.cycleway.bridge" = "Pont"; diff --git a/iphone/Maps/LocalizedStrings/gl.lproj/LocalizableTypes.strings b/iphone/Maps/LocalizedStrings/gl.lproj/LocalizableTypes.strings index 01a65cd56..1570bad09 100644 --- a/iphone/Maps/LocalizedStrings/gl.lproj/LocalizableTypes.strings +++ b/iphone/Maps/LocalizedStrings/gl.lproj/LocalizableTypes.strings @@ -452,6 +452,22 @@ "type.highway.busway.tunnel" = "Túnel"; "type.highway.bus_stop" = "Parada de autobús"; "type.highway.construction" = "Vía en construción"; +"type.highway.construction.motorway" = "Vía en construción"; +"type.highway.construction.motorway_link" = "Vía en construción"; +"type.highway.construction.trunk" = "Vía en construción"; +"type.highway.construction.trunk_link" = "Vía en construción"; +"type.highway.construction.primary" = "Vía en construción"; +"type.highway.construction.primary_link" = "Vía en construción"; +"type.highway.construction.secondary" = "Vía en construción"; +"type.highway.construction.secondary_link" = "Vía en construción"; +"type.highway.construction.tertiary" = "Vía en construción"; +"type.highway.construction.tertiary_link" = "Vía en construción"; +"type.highway.construction.residential" = "Vía en construción"; +"type.highway.construction.unclassified" = "Vía en construción"; +"type.highway.construction.service" = "Vía en construción"; +"type.highway.construction.living_street" = "Vía en construción"; +"type.highway.construction.road" = "Vía en construción"; +"type.highway.construction.track" = "Vía en construción"; "type.highway.cycleway" = "Ciclovía"; /* These translations are used for all type.highway.*.bridge. */ "type.highway.cycleway.bridge" = "Puente"; diff --git a/iphone/Maps/LocalizedStrings/gsw.lproj/LocalizableTypes.strings b/iphone/Maps/LocalizedStrings/gsw.lproj/LocalizableTypes.strings index 0d7a0159f..0748e8c45 100644 --- a/iphone/Maps/LocalizedStrings/gsw.lproj/LocalizableTypes.strings +++ b/iphone/Maps/LocalizedStrings/gsw.lproj/LocalizableTypes.strings @@ -452,6 +452,22 @@ "type.highway.busway.tunnel" = "Tunnel"; "type.highway.bus_stop" = "Bushaltistell"; "type.highway.construction" = "Strass im Bau"; +"type.highway.construction.motorway" = "Strass im Bau"; +"type.highway.construction.motorway_link" = "Strass im Bau"; +"type.highway.construction.trunk" = "Strass im Bau"; +"type.highway.construction.trunk_link" = "Strass im Bau"; +"type.highway.construction.primary" = "Strass im Bau"; +"type.highway.construction.primary_link" = "Strass im Bau"; +"type.highway.construction.secondary" = "Strass im Bau"; +"type.highway.construction.secondary_link" = "Strass im Bau"; +"type.highway.construction.tertiary" = "Strass im Bau"; +"type.highway.construction.tertiary_link" = "Strass im Bau"; +"type.highway.construction.residential" = "Strass im Bau"; +"type.highway.construction.unclassified" = "Strass im Bau"; +"type.highway.construction.service" = "Strass im Bau"; +"type.highway.construction.living_street" = "Strass im Bau"; +"type.highway.construction.road" = "Strass im Bau"; +"type.highway.construction.track" = "Strass im Bau"; "type.highway.cycleway" = "Veloweg"; /* These translations are used for all type.highway.*.bridge. */ "type.highway.cycleway.bridge" = "Brugg"; diff --git a/iphone/Maps/LocalizedStrings/he.lproj/LocalizableTypes.strings b/iphone/Maps/LocalizedStrings/he.lproj/LocalizableTypes.strings index eaae92700..144fc0041 100644 --- a/iphone/Maps/LocalizedStrings/he.lproj/LocalizableTypes.strings +++ b/iphone/Maps/LocalizedStrings/he.lproj/LocalizableTypes.strings @@ -452,6 +452,22 @@ "type.highway.busway.tunnel" = "מִנהָרָה"; "type.highway.bus_stop" = "תחנת אוטובוס"; "type.highway.construction" = "כביש בבניה"; +"type.highway.construction.motorway" = "כביש בבניה"; +"type.highway.construction.motorway_link" = "כביש בבניה"; +"type.highway.construction.trunk" = "כביש בבניה"; +"type.highway.construction.trunk_link" = "כביש בבניה"; +"type.highway.construction.primary" = "כביש בבניה"; +"type.highway.construction.primary_link" = "כביש בבניה"; +"type.highway.construction.secondary" = "כביש בבניה"; +"type.highway.construction.secondary_link" = "כביש בבניה"; +"type.highway.construction.tertiary" = "כביש בבניה"; +"type.highway.construction.tertiary_link" = "כביש בבניה"; +"type.highway.construction.residential" = "כביש בבניה"; +"type.highway.construction.unclassified" = "כביש בבניה"; +"type.highway.construction.service" = "כביש בבניה"; +"type.highway.construction.living_street" = "כביש בבניה"; +"type.highway.construction.road" = "כביש בבניה"; +"type.highway.construction.track" = "כביש בבניה"; "type.highway.cycleway" = "שביל אופניים"; /* These translations are used for all type.highway.*.bridge. */ "type.highway.cycleway.bridge" = "גשר לאופניים"; diff --git a/iphone/Maps/LocalizedStrings/hi.lproj/LocalizableTypes.strings b/iphone/Maps/LocalizedStrings/hi.lproj/LocalizableTypes.strings index d00b5f6bf..979a024f0 100644 --- a/iphone/Maps/LocalizedStrings/hi.lproj/LocalizableTypes.strings +++ b/iphone/Maps/LocalizedStrings/hi.lproj/LocalizableTypes.strings @@ -452,6 +452,22 @@ "type.highway.busway.tunnel" = "Tunnel"; "type.highway.bus_stop" = "बस स्टॉप"; "type.highway.construction" = "निर्माणाधीन सड़क"; +"type.highway.construction.motorway" = "निर्माणाधीन सड़क"; +"type.highway.construction.motorway_link" = "निर्माणाधीन सड़क"; +"type.highway.construction.trunk" = "निर्माणाधीन सड़क"; +"type.highway.construction.trunk_link" = "निर्माणाधीन सड़क"; +"type.highway.construction.primary" = "निर्माणाधीन सड़क"; +"type.highway.construction.primary_link" = "निर्माणाधीन सड़क"; +"type.highway.construction.secondary" = "निर्माणाधीन सड़क"; +"type.highway.construction.secondary_link" = "निर्माणाधीन सड़क"; +"type.highway.construction.tertiary" = "निर्माणाधीन सड़क"; +"type.highway.construction.tertiary_link" = "निर्माणाधीन सड़क"; +"type.highway.construction.residential" = "निर्माणाधीन सड़क"; +"type.highway.construction.unclassified" = "निर्माणाधीन सड़क"; +"type.highway.construction.service" = "निर्माणाधीन सड़क"; +"type.highway.construction.living_street" = "निर्माणाधीन सड़क"; +"type.highway.construction.road" = "निर्माणाधीन सड़क"; +"type.highway.construction.track" = "निर्माणाधीन सड़क"; "type.highway.cycleway" = "साइकिल मार्ग"; /* These translations are used for all type.highway.*.bridge. */ "type.highway.cycleway.bridge" = "Bridge"; diff --git a/iphone/Maps/LocalizedStrings/hu.lproj/LocalizableTypes.strings b/iphone/Maps/LocalizedStrings/hu.lproj/LocalizableTypes.strings index 7312d8abe..292bb067c 100644 --- a/iphone/Maps/LocalizedStrings/hu.lproj/LocalizableTypes.strings +++ b/iphone/Maps/LocalizedStrings/hu.lproj/LocalizableTypes.strings @@ -452,6 +452,22 @@ "type.highway.busway.tunnel" = "Alagút"; "type.highway.bus_stop" = "Buszmegálló"; "type.highway.construction" = "Útépítés"; +"type.highway.construction.motorway" = "Útépítés"; +"type.highway.construction.motorway_link" = "Útépítés"; +"type.highway.construction.trunk" = "Útépítés"; +"type.highway.construction.trunk_link" = "Útépítés"; +"type.highway.construction.primary" = "Útépítés"; +"type.highway.construction.primary_link" = "Útépítés"; +"type.highway.construction.secondary" = "Útépítés"; +"type.highway.construction.secondary_link" = "Útépítés"; +"type.highway.construction.tertiary" = "Útépítés"; +"type.highway.construction.tertiary_link" = "Útépítés"; +"type.highway.construction.residential" = "Útépítés"; +"type.highway.construction.unclassified" = "Útépítés"; +"type.highway.construction.service" = "Útépítés"; +"type.highway.construction.living_street" = "Útépítés"; +"type.highway.construction.road" = "Útépítés"; +"type.highway.construction.track" = "Útépítés"; "type.highway.cycleway" = "Kerékpárút"; /* These translations are used for all type.highway.*.bridge. */ "type.highway.cycleway.bridge" = "Híd"; diff --git a/iphone/Maps/LocalizedStrings/id.lproj/LocalizableTypes.strings b/iphone/Maps/LocalizedStrings/id.lproj/LocalizableTypes.strings index 65e0abc17..f31e4c265 100644 --- a/iphone/Maps/LocalizedStrings/id.lproj/LocalizableTypes.strings +++ b/iphone/Maps/LocalizedStrings/id.lproj/LocalizableTypes.strings @@ -430,6 +430,22 @@ "type.highway.busway.tunnel" = "Terowongan"; "type.highway.bus_stop" = "Halte bus"; "type.highway.construction" = "Jalan sedang dibangun"; +"type.highway.construction.motorway" = "Jalan sedang dibangun"; +"type.highway.construction.motorway_link" = "Jalan sedang dibangun"; +"type.highway.construction.trunk" = "Jalan sedang dibangun"; +"type.highway.construction.trunk_link" = "Jalan sedang dibangun"; +"type.highway.construction.primary" = "Jalan sedang dibangun"; +"type.highway.construction.primary_link" = "Jalan sedang dibangun"; +"type.highway.construction.secondary" = "Jalan sedang dibangun"; +"type.highway.construction.secondary_link" = "Jalan sedang dibangun"; +"type.highway.construction.tertiary" = "Jalan sedang dibangun"; +"type.highway.construction.tertiary_link" = "Jalan sedang dibangun"; +"type.highway.construction.residential" = "Jalan sedang dibangun"; +"type.highway.construction.unclassified" = "Jalan sedang dibangun"; +"type.highway.construction.service" = "Jalan sedang dibangun"; +"type.highway.construction.living_street" = "Jalan sedang dibangun"; +"type.highway.construction.road" = "Jalan sedang dibangun"; +"type.highway.construction.track" = "Jalan sedang dibangun"; "type.highway.cycleway" = "Jalur Sepeda"; /* These translations are used for all type.highway.*.bridge. */ "type.highway.cycleway.bridge" = "Menjembatani"; diff --git a/iphone/Maps/LocalizedStrings/is.lproj/LocalizableTypes.strings b/iphone/Maps/LocalizedStrings/is.lproj/LocalizableTypes.strings index 6df51d2f9..e3d4aded1 100644 --- a/iphone/Maps/LocalizedStrings/is.lproj/LocalizableTypes.strings +++ b/iphone/Maps/LocalizedStrings/is.lproj/LocalizableTypes.strings @@ -452,6 +452,22 @@ "type.highway.busway.tunnel" = "Göng"; "type.highway.bus_stop" = "Strætisvagnabiðstöð"; "type.highway.construction" = "Vegur í byggingu"; +"type.highway.construction.motorway" = "Vegur í byggingu"; +"type.highway.construction.motorway_link" = "Vegur í byggingu"; +"type.highway.construction.trunk" = "Vegur í byggingu"; +"type.highway.construction.trunk_link" = "Vegur í byggingu"; +"type.highway.construction.primary" = "Vegur í byggingu"; +"type.highway.construction.primary_link" = "Vegur í byggingu"; +"type.highway.construction.secondary" = "Vegur í byggingu"; +"type.highway.construction.secondary_link" = "Vegur í byggingu"; +"type.highway.construction.tertiary" = "Vegur í byggingu"; +"type.highway.construction.tertiary_link" = "Vegur í byggingu"; +"type.highway.construction.residential" = "Vegur í byggingu"; +"type.highway.construction.unclassified" = "Vegur í byggingu"; +"type.highway.construction.service" = "Vegur í byggingu"; +"type.highway.construction.living_street" = "Vegur í byggingu"; +"type.highway.construction.road" = "Vegur í byggingu"; +"type.highway.construction.track" = "Vegur í byggingu"; "type.highway.cycleway" = "Hjólastígur"; /* These translations are used for all type.highway.*.bridge. */ "type.highway.cycleway.bridge" = "Brú"; diff --git a/iphone/Maps/LocalizedStrings/it.lproj/LocalizableTypes.strings b/iphone/Maps/LocalizedStrings/it.lproj/LocalizableTypes.strings index a53066363..7ff467b0e 100644 --- a/iphone/Maps/LocalizedStrings/it.lproj/LocalizableTypes.strings +++ b/iphone/Maps/LocalizedStrings/it.lproj/LocalizableTypes.strings @@ -452,6 +452,22 @@ "type.highway.busway.tunnel" = "Galleria"; "type.highway.bus_stop" = "Fermata dell'autobus"; "type.highway.construction" = "Strada in costruzione"; +"type.highway.construction.motorway" = "Strada in costruzione"; +"type.highway.construction.motorway_link" = "Strada in costruzione"; +"type.highway.construction.trunk" = "Strada in costruzione"; +"type.highway.construction.trunk_link" = "Strada in costruzione"; +"type.highway.construction.primary" = "Strada in costruzione"; +"type.highway.construction.primary_link" = "Strada in costruzione"; +"type.highway.construction.secondary" = "Strada in costruzione"; +"type.highway.construction.secondary_link" = "Strada in costruzione"; +"type.highway.construction.tertiary" = "Strada in costruzione"; +"type.highway.construction.tertiary_link" = "Strada in costruzione"; +"type.highway.construction.residential" = "Strada in costruzione"; +"type.highway.construction.unclassified" = "Strada in costruzione"; +"type.highway.construction.service" = "Strada in costruzione"; +"type.highway.construction.living_street" = "Strada in costruzione"; +"type.highway.construction.road" = "Strada in costruzione"; +"type.highway.construction.track" = "Strada in costruzione"; "type.highway.cycleway" = "Pista ciclabile"; /* These translations are used for all type.highway.*.bridge. */ "type.highway.cycleway.bridge" = "Ponte"; diff --git a/iphone/Maps/LocalizedStrings/ja.lproj/LocalizableTypes.strings b/iphone/Maps/LocalizedStrings/ja.lproj/LocalizableTypes.strings index d738e2909..b5245d9ef 100644 --- a/iphone/Maps/LocalizedStrings/ja.lproj/LocalizableTypes.strings +++ b/iphone/Maps/LocalizedStrings/ja.lproj/LocalizableTypes.strings @@ -452,6 +452,22 @@ "type.highway.busway.tunnel" = "トンネル"; "type.highway.bus_stop" = "バス停"; "type.highway.construction" = "工事中の道路"; +"type.highway.construction.motorway" = "工事中の道路"; +"type.highway.construction.motorway_link" = "工事中の道路"; +"type.highway.construction.trunk" = "工事中の道路"; +"type.highway.construction.trunk_link" = "工事中の道路"; +"type.highway.construction.primary" = "工事中の道路"; +"type.highway.construction.primary_link" = "工事中の道路"; +"type.highway.construction.secondary" = "工事中の道路"; +"type.highway.construction.secondary_link" = "工事中の道路"; +"type.highway.construction.tertiary" = "工事中の道路"; +"type.highway.construction.tertiary_link" = "工事中の道路"; +"type.highway.construction.residential" = "工事中の道路"; +"type.highway.construction.unclassified" = "工事中の道路"; +"type.highway.construction.service" = "工事中の道路"; +"type.highway.construction.living_street" = "工事中の道路"; +"type.highway.construction.road" = "工事中の道路"; +"type.highway.construction.track" = "工事中の道路"; "type.highway.cycleway" = "自転車道"; /* These translations are used for all type.highway.*.bridge. */ "type.highway.cycleway.bridge" = "自転車道(橋)"; diff --git a/iphone/Maps/LocalizedStrings/ko.lproj/LocalizableTypes.strings b/iphone/Maps/LocalizedStrings/ko.lproj/LocalizableTypes.strings index 0e670e6fa..b8d6b720b 100644 --- a/iphone/Maps/LocalizedStrings/ko.lproj/LocalizableTypes.strings +++ b/iphone/Maps/LocalizedStrings/ko.lproj/LocalizableTypes.strings @@ -452,6 +452,22 @@ "type.highway.busway.tunnel" = "터널"; "type.highway.bus_stop" = "버스 정류장"; "type.highway.construction" = "공사 중 도로"; +"type.highway.construction.motorway" = "공사 중 도로"; +"type.highway.construction.motorway_link" = "공사 중 도로"; +"type.highway.construction.trunk" = "공사 중 도로"; +"type.highway.construction.trunk_link" = "공사 중 도로"; +"type.highway.construction.primary" = "공사 중 도로"; +"type.highway.construction.primary_link" = "공사 중 도로"; +"type.highway.construction.secondary" = "공사 중 도로"; +"type.highway.construction.secondary_link" = "공사 중 도로"; +"type.highway.construction.tertiary" = "공사 중 도로"; +"type.highway.construction.tertiary_link" = "공사 중 도로"; +"type.highway.construction.residential" = "공사 중 도로"; +"type.highway.construction.unclassified" = "공사 중 도로"; +"type.highway.construction.service" = "공사 중 도로"; +"type.highway.construction.living_street" = "공사 중 도로"; +"type.highway.construction.road" = "공사 중 도로"; +"type.highway.construction.track" = "공사 중 도로"; "type.highway.cycleway" = "Cycle Path"; /* These translations are used for all type.highway.*.bridge. */ "type.highway.cycleway.bridge" = "다리"; diff --git a/iphone/Maps/LocalizedStrings/lt.lproj/LocalizableTypes.strings b/iphone/Maps/LocalizedStrings/lt.lproj/LocalizableTypes.strings index 5f826c63c..afc6749b1 100644 --- a/iphone/Maps/LocalizedStrings/lt.lproj/LocalizableTypes.strings +++ b/iphone/Maps/LocalizedStrings/lt.lproj/LocalizableTypes.strings @@ -452,6 +452,22 @@ "type.highway.busway.tunnel" = "Tunelis"; "type.highway.bus_stop" = "Stotelė"; "type.highway.construction" = "Tiesiamas kelias"; +"type.highway.construction.motorway" = "Tiesiamas kelias"; +"type.highway.construction.motorway_link" = "Tiesiamas kelias"; +"type.highway.construction.trunk" = "Tiesiamas kelias"; +"type.highway.construction.trunk_link" = "Tiesiamas kelias"; +"type.highway.construction.primary" = "Tiesiamas kelias"; +"type.highway.construction.primary_link" = "Tiesiamas kelias"; +"type.highway.construction.secondary" = "Tiesiamas kelias"; +"type.highway.construction.secondary_link" = "Tiesiamas kelias"; +"type.highway.construction.tertiary" = "Tiesiamas kelias"; +"type.highway.construction.tertiary_link" = "Tiesiamas kelias"; +"type.highway.construction.residential" = "Tiesiamas kelias"; +"type.highway.construction.unclassified" = "Tiesiamas kelias"; +"type.highway.construction.service" = "Tiesiamas kelias"; +"type.highway.construction.living_street" = "Tiesiamas kelias"; +"type.highway.construction.road" = "Tiesiamas kelias"; +"type.highway.construction.track" = "Tiesiamas kelias"; "type.highway.cycleway" = "Dviračių takas"; /* These translations are used for all type.highway.*.bridge. */ "type.highway.cycleway.bridge" = "Tiltas"; diff --git a/iphone/Maps/LocalizedStrings/mr.lproj/LocalizableTypes.strings b/iphone/Maps/LocalizedStrings/mr.lproj/LocalizableTypes.strings index 14918935b..d73ca0f31 100644 --- a/iphone/Maps/LocalizedStrings/mr.lproj/LocalizableTypes.strings +++ b/iphone/Maps/LocalizedStrings/mr.lproj/LocalizableTypes.strings @@ -452,6 +452,22 @@ "type.highway.busway.tunnel" = "बोगदा"; "type.highway.bus_stop" = "बस थांबा"; "type.highway.construction" = "बांधकामाधीन रस्ता"; +"type.highway.construction.motorway" = "बांधकामाधीन रस्ता"; +"type.highway.construction.motorway_link" = "बांधकामाधीन रस्ता"; +"type.highway.construction.trunk" = "बांधकामाधीन रस्ता"; +"type.highway.construction.trunk_link" = "बांधकामाधीन रस्ता"; +"type.highway.construction.primary" = "बांधकामाधीन रस्ता"; +"type.highway.construction.primary_link" = "बांधकामाधीन रस्ता"; +"type.highway.construction.secondary" = "बांधकामाधीन रस्ता"; +"type.highway.construction.secondary_link" = "बांधकामाधीन रस्ता"; +"type.highway.construction.tertiary" = "बांधकामाधीन रस्ता"; +"type.highway.construction.tertiary_link" = "बांधकामाधीन रस्ता"; +"type.highway.construction.residential" = "बांधकामाधीन रस्ता"; +"type.highway.construction.unclassified" = "बांधकामाधीन रस्ता"; +"type.highway.construction.service" = "बांधकामाधीन रस्ता"; +"type.highway.construction.living_street" = "बांधकामाधीन रस्ता"; +"type.highway.construction.road" = "बांधकामाधीन रस्ता"; +"type.highway.construction.track" = "बांधकामाधीन रस्ता"; "type.highway.cycleway" = "सायकल वाट"; /* These translations are used for all type.highway.*.bridge. */ "type.highway.cycleway.bridge" = "पूल"; diff --git a/iphone/Maps/LocalizedStrings/nb.lproj/LocalizableTypes.strings b/iphone/Maps/LocalizedStrings/nb.lproj/LocalizableTypes.strings index e97471363..811985abb 100644 --- a/iphone/Maps/LocalizedStrings/nb.lproj/LocalizableTypes.strings +++ b/iphone/Maps/LocalizedStrings/nb.lproj/LocalizableTypes.strings @@ -452,6 +452,22 @@ "type.highway.busway.tunnel" = "Tunnel"; "type.highway.bus_stop" = "Busstopp"; "type.highway.construction" = "Veikonstruksjon"; +"type.highway.construction.motorway" = "Veikonstruksjon"; +"type.highway.construction.motorway_link" = "Veikonstruksjon"; +"type.highway.construction.trunk" = "Veikonstruksjon"; +"type.highway.construction.trunk_link" = "Veikonstruksjon"; +"type.highway.construction.primary" = "Veikonstruksjon"; +"type.highway.construction.primary_link" = "Veikonstruksjon"; +"type.highway.construction.secondary" = "Veikonstruksjon"; +"type.highway.construction.secondary_link" = "Veikonstruksjon"; +"type.highway.construction.tertiary" = "Veikonstruksjon"; +"type.highway.construction.tertiary_link" = "Veikonstruksjon"; +"type.highway.construction.residential" = "Veikonstruksjon"; +"type.highway.construction.unclassified" = "Veikonstruksjon"; +"type.highway.construction.service" = "Veikonstruksjon"; +"type.highway.construction.living_street" = "Veikonstruksjon"; +"type.highway.construction.road" = "Veikonstruksjon"; +"type.highway.construction.track" = "Veikonstruksjon"; "type.highway.cycleway" = "Sykkelvei"; /* These translations are used for all type.highway.*.bridge. */ "type.highway.cycleway.bridge" = "Bru"; diff --git a/iphone/Maps/LocalizedStrings/nl.lproj/LocalizableTypes.strings b/iphone/Maps/LocalizedStrings/nl.lproj/LocalizableTypes.strings index 1119cabb8..7a0990d1c 100644 --- a/iphone/Maps/LocalizedStrings/nl.lproj/LocalizableTypes.strings +++ b/iphone/Maps/LocalizedStrings/nl.lproj/LocalizableTypes.strings @@ -52,7 +52,7 @@ "type.leisure.amusement_arcade" = "Speelhal"; "type.amenity.charging_station" = "Laadstation"; "type.amenity.charging_station.motorcar" = "Auto's"; -"type.amenity.charging_station.motorcycle" = "Motors"; +"type.amenity.charging_station.motorcycle" = "Motorfietsen"; "type.amenity.charging_station.bicycle" = "Fietsen"; "type.amenity.charging_station.small" = "Beperkte capaciteit"; "type.amenity.childcare" = "Crèche"; @@ -167,7 +167,7 @@ "type.recycling.green_waste" = "GFTE afval"; "type.recycling.cartons" = "Drankverpakkingen"; "type.amenity.restaurant" = "Restaurant"; -"type.amenity.sanitary_dump_station" = "Caravantoilet cassette-leegpunt"; +"type.amenity.sanitary_dump_station" = "Camperverzorgingsplaats"; "type.amenity.school" = "School"; /* Weather shelter (including sun shelters and natural rock shelters). */ "type.amenity.shelter" = "Schuilplaats"; @@ -191,7 +191,7 @@ "type.amenity.vending_machine.cigarettes" = "Sigarettenautomaat"; "type.amenity.vending_machine.coffee" = "Koffieautomaat"; "type.amenity.vending_machine.condoms" = "Condoomautomaat"; -"type.amenity.vending_machine.drinks" = "Drankautomaat"; +"type.amenity.vending_machine.drinks" = "Drankenautomaat"; "type.amenity.vending_machine.food" = "Etenswarenautomaat"; "type.amenity.vending_machine.newspapers" = "Krantenautomaat"; "type.amenity.vending_machine.parking_tickets" = "Parkeerautomaat"; @@ -205,7 +205,7 @@ "type.amenity.animal_shelter" = "Dierenasiel"; "type.amenity.waste_basket" = "Vuilnisbak"; "type.amenity.waste_disposal" = "Afvalcontainer"; -"type.amenity.waste_transfer_station" = "Overslagstation voor afval"; +"type.amenity.waste_transfer_station" = "Afvaloverslagstation"; "type.amenity.water_point" = "Waterpunt"; "type.amenity.water_point.drinking_water_no" = "Waterpunt"; "type.barrier" = "Barrière"; @@ -265,7 +265,7 @@ "type.craft.brewery" = "Brouwerij"; "type.craft.caterer" = "Cateraar"; "type.craft.carpenter" = "Timmerman"; -"type.craft.confectionery" = "Banketbakkerij"; +"type.craft.confectionery" = "Snoepmakerij"; "type.craft.electrician" = "Elektricien"; "type.craft.electronics_repair" = "Elektronica-reparatie"; "type.craft.gardener" = "Tuinarchitect"; @@ -307,7 +307,7 @@ "type.cuisine.coffee_shop" = "Koffie"; "type.cuisine.crepe" = "Pannenkoek"; "type.cuisine.croatian" = "Kroatisch"; -"type.cuisine.curry" = "Curry's"; +"type.cuisine.curry" = "Curry"; "type.cuisine.deli" = "Delicatessen"; "type.cuisine.diner" = "Diner"; "type.cuisine.donut" = "Donuts"; @@ -321,7 +321,7 @@ "type.cuisine.georgian" = "Georgisch"; "type.cuisine.german" = "Duits"; "type.cuisine.greek" = "Grieks"; -"type.cuisine.grill" = "Gegrilde gerechten"; +"type.cuisine.grill" = "Grill"; "type.cuisine.heuriger" = "Heuriger"; "type.cuisine.hotdog" = "Hotdogs"; "type.cuisine.hungarian" = "Hongaars"; @@ -423,8 +423,8 @@ "type.entrance.house" = "Huis entree"; "type.entrance.garage" = "Garage entree"; "type.entrance.service" = "Dienstingang"; -"type.entrance.entry" = "Ingang (enig)"; -"type.entrance.exit" = "Uitgang (enig)"; +"type.entrance.entry" = "Ingang (alleen ingang)"; +"type.entrance.exit" = "Uitgang (alleen uitgang)"; "type.entrance.emergency" = "Nooduitgang"; "type.fee.yes" = "€"; "type.fee.no" = "Gratis"; @@ -445,13 +445,29 @@ "type.highway.bridleway.permissive" = "Ruiterpad"; /* These translations are used for all type.highway.*.tunnel. */ "type.highway.bridleway.tunnel" = "Tunnel"; -"type.highway.busway" = "Speciale busweg"; +"type.highway.busway" = "Busbaan"; /* These translations are used for all type.highway.*.bridge. */ "type.highway.busway.bridge" = "Brug"; /* These translations are used for all type.highway.*.tunnel. */ "type.highway.busway.tunnel" = "Tunnel"; "type.highway.bus_stop" = "Bushalte"; -"type.highway.construction" = "Baan in aanbouw"; +"type.highway.construction" = "Weg in aanbouw"; +"type.highway.construction.motorway" = "Weg in aanbouw"; +"type.highway.construction.motorway_link" = "Weg in aanbouw"; +"type.highway.construction.trunk" = "Weg in aanbouw"; +"type.highway.construction.trunk_link" = "Weg in aanbouw"; +"type.highway.construction.primary" = "Weg in aanbouw"; +"type.highway.construction.primary_link" = "Weg in aanbouw"; +"type.highway.construction.secondary" = "Weg in aanbouw"; +"type.highway.construction.secondary_link" = "Weg in aanbouw"; +"type.highway.construction.tertiary" = "Weg in aanbouw"; +"type.highway.construction.tertiary_link" = "Weg in aanbouw"; +"type.highway.construction.residential" = "Weg in aanbouw"; +"type.highway.construction.unclassified" = "Weg in aanbouw"; +"type.highway.construction.service" = "Weg in aanbouw"; +"type.highway.construction.living_street" = "Weg in aanbouw"; +"type.highway.construction.road" = "Weg in aanbouw"; +"type.highway.construction.track" = "Weg in aanbouw"; "type.highway.cycleway" = "Fietspad"; /* These translations are used for all type.highway.*.bridge. */ "type.highway.cycleway.bridge" = "Fietsbrug"; @@ -468,7 +484,7 @@ /* These translations are used for all type.highway.*.tunnel. */ "type.highway.footway.tunnel" = "Voetgangerstunnel"; "type.highway.ford" = "Voorde"; -"type.highway.living_street" = "Straat"; +"type.highway.living_street" = "Woonerf"; /* These translations are used for all type.highway.*.bridge. */ "type.highway.living_street.bridge" = "Brug"; /* These translations are used for all type.highway.*.tunnel. */ @@ -479,7 +495,7 @@ /* These translations are used for all type.highway.*.tunnel. */ "type.highway.motorway.tunnel" = "Snelwegtunnel"; "type.highway.motorway_junction" = "Afrit"; -"type.highway.motorway_link" = "Snelwegoprit"; +"type.highway.motorway_link" = "Snelwegoprit/afrit"; /* These translations are used for all type.highway.*.bridge. */ "type.highway.motorway_link.bridge" = "Brug"; /* These translations are used for all type.highway.*.tunnel. */ @@ -488,7 +504,7 @@ /* Hiking trail tagged as sac_scale=demanding_mountain_hiking (3 of 6) or trail_visibility=bad. */ "type.highway.path.difficult" = "Moeilijk of slecht zichtbaar pad"; /* Hiking trail tagged as sac_scale=alpine_hiking (4+ of 6) or trail_visibility=horrible or more extreme. */ -"type.highway.path.expert" = "Zeer moeilijk of niet te onderscheiden spoor"; +"type.highway.path.expert" = "Zeer moeilijk of niet te onderscheiden pad"; "type.highway.path.bicycle" = "Fiets- & voetpad"; "type.highway.footway.bicycle" = "Fiets- & voetpad"; /* These translations are used for all type.highway.*.bridge. */ @@ -564,27 +580,27 @@ "type.highway.tertiary_link.bridge" = "Brug"; /* These translations are used for all type.highway.*.tunnel. */ "type.highway.tertiary_link.tunnel" = "Tunnel"; -"type.highway.track" = "Straat"; -"type.highway.track.area" = "Straat"; +"type.highway.track" = "Bos-/veldweg"; +"type.highway.track.area" = "Bos-/veldweg"; /* These translations are used for all type.highway.*.bridge. */ "type.highway.track.bridge" = "Brug"; -"type.highway.track.grade1" = "Straat"; -"type.highway.track.no.access" = "Straat"; +"type.highway.track.grade1" = "Bos-/veldweg"; +"type.highway.track.no.access" = "Bos-/veldweg"; /* These translations are used for all type.highway.*.tunnel. */ "type.highway.track.tunnel" = "Tunnel"; "type.highway.traffic_signals" = "Verkeerslichten"; -"type.highway.trunk" = "Straat"; +"type.highway.trunk" = "Autoweg"; /* These translations are used for all type.highway.*.bridge. */ "type.highway.trunk.bridge" = "Brug"; /* These translations are used for all type.highway.*.tunnel. */ "type.highway.trunk.tunnel" = "Tunnel"; -"type.highway.trunk_link" = "Straat"; +"type.highway.trunk_link" = "Autowegoprit/afrit"; /* These translations are used for all type.highway.*.bridge. */ "type.highway.trunk_link.bridge" = "Brug"; /* These translations are used for all type.highway.*.tunnel. */ "type.highway.trunk_link.tunnel" = "Tunnel"; -"type.highway.unclassified" = "Straat"; -"type.highway.unclassified.area" = "Straat"; +"type.highway.unclassified" = "Weg"; +"type.highway.unclassified.area" = "Weg"; /* These translations are used for all type.highway.*.bridge. */ "type.highway.unclassified.bridge" = "Brug"; /* These translations are used for all type.highway.*.tunnel. */ @@ -598,12 +614,12 @@ "type.area_highway.primary" = "Hoofdweg"; "type.area_highway.residential" = "Straat"; "type.area_highway.secondary" = "Secundaire weg"; -"type.area_highway.service" = "Straat"; +"type.area_highway.service" = "Serviceweg"; "type.area_highway.tertiary" = "Tertiaire weg"; -"type.area_highway.steps" = "Trappen"; -"type.area_highway.track" = "Straat"; -"type.area_highway.trunk" = "Straat"; -"type.area_highway.unclassified" = "Straat"; +"type.area_highway.steps" = "Trap"; +"type.area_highway.track" = "Bos-/veldweg"; +"type.area_highway.trunk" = "Autoweg"; +"type.area_highway.unclassified" = "Weg"; "type.historic" = "Historisch object"; "type.historic.aircraft" = "Historische vliegtuigen"; "type.historic.anchor" = "Historisch anker"; @@ -618,7 +634,7 @@ "type.historic.castle.fortress" = "Vesting"; "type.historic.castle.hillfort" = "Heuvelfort"; "type.historic.castle.kremlin" = "Kremlin"; -"type.historic.castle.manor" = "Manoir"; +"type.historic.castle.manor" = "Landhuis"; "type.historic.castle.palace" = "Paleis"; "type.historic.castle.shiro" = "Japans kasteel"; "type.historic.castle.stately" = "Landhuis"; @@ -638,7 +654,7 @@ "type.historic.mine" = "Historische mijn"; "type.historic.monument" = "Monument"; "type.historic.pillory" = "Schandpaal"; -"type.historic.ruins" = "Ruïne"; +"type.historic.ruins" = "Historische ruïne"; "type.historic.ship" = "Historisch schip"; "type.historic.tank" = "Historische tank"; "type.historic.tomb" = "Graftombe"; @@ -647,7 +663,7 @@ /* Usually a Christian historical cross placed along a road. */ "type.historic.wayside_cross" = "Wegkruis"; "type.historic.wayside_shrine" = "Kruisbeeld"; -"type.historic.wreck" = "Schipbreuk"; +"type.historic.wreck" = "Schipwrak"; "type.internet_access" = "Internet"; "type.internet_access.wlan" = "Draadloos internet"; "type.junction" = "Kruispunt"; @@ -664,41 +680,41 @@ "type.landuse.religious" = "Religieus Land"; "type.landuse.commercial" = "Commercieel gebied"; "type.landuse.construction" = "Bouwplaats"; -"type.landuse.education" = "Educatieve voorzieningen"; +"type.landuse.education" = "Onderwijsgebied"; "type.landuse.farmland" = "Landbouwgrond"; "type.landuse.farmyard" = "Boerenerf"; "type.landuse.field" = "Veld"; -"type.landuse.flowerbed" = "Bloemenbed"; +"type.landuse.flowerbed" = "Bloembed"; "type.landuse.forest" = "Bos"; "type.landuse.forest.coniferous" = "Naaldbos"; "type.landuse.forest.deciduous" = "Loofbos"; "type.landuse.forest.mixed" = "Gemengd bos"; "type.landuse.garages" = "Garages"; "type.landuse.grass" = "Gazon"; -"type.landuse.greenfield" = "Greenfield"; -"type.landuse.greenhouse_horticulture" = "Kassen"; +"type.landuse.greenfield" = "Bouwgrond"; +"type.landuse.greenhouse_horticulture" = "Kas"; "type.landuse.industrial" = "Industrieterrein"; "type.landuse.landfill" = "Stortplaats"; "type.landuse.meadow" = "Weiland"; "type.landuse.military" = "Militair terrein"; "type.landuse.orchard" = "Boomgaard"; "type.landuse.quarry" = "Steengroeve"; -"type.landuse.railway" = "Spoorwegfaciliteiten"; +"type.landuse.railway" = "Spoorwegterrein"; "type.landuse.recreation_ground" = "Recreatiezone"; "type.landuse.reservoir" = "Reservoir"; "type.landuse.residential" = "Woonwijk"; -"type.landuse.retail" = "Detailhandel"; -"type.landuse.salt_pond" = "Zoutpoel"; +"type.landuse.retail" = "Detailhandelsgebied"; +"type.landuse.salt_pond" = "Zoutpan"; "type.landuse.village_green" = "Dorpspark"; "type.landuse.vineyard" = "Wijngaard"; "type.landuse.plant_nursery" = "Plantenkwekerij"; -"type.leisure" = "Ontspanning"; -"type.leisure.bandstand" = "Muziekpodium"; +"type.leisure" = "Recreatie"; +"type.leisure.bandstand" = "Muziekkoepel"; "type.leisure.common" = "Openbare grond"; "type.leisure.dog_park" = "Hondenuitlaatplek"; "type.leisure.escape_game" = "Escaperoom"; "type.leisure.fitness_centre" = "Fitnesscentrum"; -"type.leisure.fitness_station" = "Fitness-Station"; +"type.leisure.fitness_station" = "Fitness-station"; "type.leisure.dance" = "Danszaal"; "type.leisure.garden" = "Tuin"; "type.leisure.garden.residential" = "Tuin"; @@ -706,15 +722,15 @@ "type.leisure.miniature_golf" = "Minigolf"; "type.leisure.hackerspace" = "Hackerspace"; "type.leisure.ice_rink" = "Schaatsbaan"; -"type.leisure.indoor_play" = "Speelhal"; +"type.leisure.indoor_play" = "Indoor speeltuin"; "type.leisure.marina" = "Jachthaven"; "type.leisure.nature_reserve" = "Natuurreservaat"; "type.leisure.outdoor_seating" = "Zitplaatsen buiten"; "type.leisure.firepit" = "Vuurplaats"; "type.leisure.park" = "Park"; -"type.leisure.park.no.access" = "Park"; +"type.leisure.park.no.access" = "Privépark"; "type.leisure.park.permissive" = "Park"; -"type.leisure.park.private" = "Park"; +"type.leisure.park.private" = "Privépark"; "type.leisure.picnic_table" = "Picknicktafel"; "type.leisure.pitch" = "Sportveld"; "type.leisure.playground" = "Speeltuin"; @@ -757,18 +773,18 @@ "type.leisure.fitness_centre.sport.yoga" = "Yogastudio"; "type.leisure.stadium" = "Stadion"; "type.leisure.swimming_pool" = "Zwembad"; -"type.leisure.swimming_pool.private" = "Privé zwembad"; +"type.leisure.swimming_pool.private" = "Privézwembad"; "type.leisure.track" = "Parcours"; "type.leisure.track.area" = "Parcours"; "type.leisure.water_park" = "Waterpark"; "type.leisure.beach_resort" = "Strandresort"; -"type.man_made" = "Kunstmatige constructies"; +"type.man_made" = "Kunstmatige constructie"; "type.man_made.breakwater" = "Golfbreker"; -"type.man_made.cairn" = "Steengroeve"; +"type.man_made.cairn" = "Steenmannetje"; "type.man_made.chimney" = "Fabrieksschoorsteen"; -"type.man_made.cutline" = "Bospad"; +"type.man_made.cutline" = "Cutline"; "type.man_made.survey_point" = "Meetpunt"; -"type.man_made.flagpole" = "Vlaggenpaal"; +"type.man_made.flagpole" = "Vlaggenmast"; "type.man_made.lighthouse" = "Vuurtoren"; "type.man_made.mast" = "Mast"; "type.man_made.pier" = "Pier"; @@ -797,7 +813,7 @@ "type.man_made.water_well.drinking_water_no" = "Waterput"; "type.man_made.windmill" = "Windmolen"; "type.man_made.works" = "Fabriek"; -"type.military" = "Militair"; +"type.military" = "Militair object"; "type.military.bunker" = "Bunker"; "type.mountain_pass" = "Bergpas"; "type.natural" = "Natuur"; @@ -812,9 +828,9 @@ "type.natural.beach.sand" = "Zandstrand"; "type.natural.beach.gravel" = "Kiezelstrand"; "type.natural.cape" = "Kaap"; -"type.natural.cave_entrance" = "Grot"; +"type.natural.cave_entrance" = "Grotingang"; "type.natural.cliff" = "Klif"; -"type.natural.earth_bank" = "Klif"; +"type.natural.earth_bank" = "Aardhelling"; "type.man_made.embankment" = "Baanlichaam"; "type.natural.coastline" = "Kustlijn"; "type.natural.desert" = "Woestijn"; @@ -825,8 +841,8 @@ "type.natural.heath" = "Heide"; "type.natural.hot_spring" = "Hete bron"; "type.natural.water.lake" = "Meer"; -"type.natural.water.lock" = "Slotkamer"; -"type.natural.water.pond" = "Plas"; +"type.natural.water.lock" = "Schutkolk"; +"type.natural.water.pond" = "Vijver"; "type.natural.water.reservoir" = "Reservoir"; "type.natural.water.basin" = "Waterbassin"; "type.natural.water.river" = "Rivier"; @@ -896,83 +912,83 @@ "type.place.sea" = "Zee"; "type.place.square" = "Plein"; "type.place.state" = "Deelstaat"; -"type.place.state.USA" = "Deelstaat"; +"type.place.state.USA" = "Staat"; /* Named part of a city or town, bigger than place=quarter (Wiki: https://wiki.openstreetmap.org/wiki/Tag:place%3Dsuburb) */ "type.place.suburb" = "Buitenwijk"; "type.place.town" = "Stad"; "type.place.village" = "Dorp"; -"type.power" = "Electriciteit"; +"type.power" = "Elektriciteit"; "type.power.generator" = "Generator"; "type.power.generator.solar" = "Zonne-generator"; -"type.power.generator.wind" = "Wind generator"; -"type.power.generator.gas" = "Gasturbine elektriciteitscentrale"; +"type.power.generator.wind" = "Windgenerator"; +"type.power.generator.gas" = "Gascentrale"; "type.power.generator.hydro" = "Waterkrachtcentrale"; "type.power.line" = "Hoogspanningsleiding"; "type.power.line.underground" = "Ondergrondse hoogspanningsleiding"; "type.power.minor_line" = "Laag-/middelspanningsleiding"; "type.power.plant" = "Energiecentrale"; -"type.power.plant.coal" = "Kolen elektriciteitscentrale"; -"type.power.plant.gas" = "Gasturbine elektriciteitscentrale"; +"type.power.plant.coal" = "Kolencentrale"; +"type.power.plant.gas" = "Gascentrale"; "type.power.plant.hydro" = "Waterkrachtcentrale"; "type.power.plant.solar" = "Zonne-energiecentrale"; "type.power.plant.wind" = "Windkrachtcentrale"; "type.power.substation" = "Transformatorstation"; /* A tower or pylon carrying high voltage electricity cables. */ -"type.power.tower" = "Elektriciteitsmast"; +"type.power.tower" = "Hoogspanningsmast"; /* A single pole supporting minor power lines. */ -"type.power.pole" = "Stroommast"; +"type.power.pole" = "Elektriciteitsmast"; /* A single pole supporting various public utilities, such as lighting or telephony. */ "type.man_made.utility_pole" = "Paal"; "type.public_transport" = "Openbaar vervoer"; -"type.public_transport.platform" = "Platform"; +"type.public_transport.platform" = "Perron"; "type.railway" = "Spoorweg"; "type.railway.abandoned" = "Voormalig treinspoor"; -"type.railway.construction" = "Treinspoor in aanbouw"; +"type.railway.construction" = "Spoor in aanbouw"; "type.railway.crossing" = "Spoorwegovergang"; "type.railway.disused" = "Ongebruikt spoor"; "type.railway.disused.bridge" = "Ongebruikte spoorbrug"; "type.railway.disused.tunnel" = "Ongebruikte spoortunnel"; -"type.railway.funicular" = "Tunnel"; -"type.railway.funicular.bridge" = "Kabelspoorbuur"; +"type.railway.funicular" = "Kabelspoor"; +"type.railway.funicular.bridge" = "Kabelspoorbrug"; "type.railway.funicular.tunnel" = "Kabelspoortunnel"; "type.railway.halt" = "Station"; "type.railway.level_crossing" = "Spoorwegovergang"; "type.railway.light_rail" = "Lightrail"; -"type.railway.light_rail.bridge" = "Lightrail-brug"; -"type.railway.light_rail.tunnel" = "Lightrail-tunnel"; +"type.railway.light_rail.bridge" = "Lightrailbrug"; +"type.railway.light_rail.tunnel" = "Lightrailtunnel"; "type.railway.monorail" = "Monorail"; -"type.railway.monorail.bridge" = "Monorail-brug"; -"type.railway.monorail.tunnel" = "Monorail-tunnel"; -"type.railway.miniature" = "Miniatuur spoorweg"; -"type.railway.miniature.bridge" = "Miniatuur spoorbrug"; -"type.railway.miniature.tunnel" = "Miniatuur spoortunnel"; -"type.railway.narrow_gauge" = "Smalspoorbaan"; +"type.railway.monorail.bridge" = "Monorailbrug"; +"type.railway.monorail.tunnel" = "Monorailtunnel"; +"type.railway.miniature" = "Miniatuurspoorweg"; +"type.railway.miniature.bridge" = "Miniatuurspoorbrug"; +"type.railway.miniature.tunnel" = "Miniatuurspoortunnel"; +"type.railway.narrow_gauge" = "Smalspoor"; "type.railway.narrow_gauge.bridge" = "Smalspoorbrug"; "type.railway.narrow_gauge.tunnel" = "Smalspoortunnel"; "type.railway.platform" = "Perron"; -"type.railway.preserved" = "Museumspoorbaan"; -"type.railway.preserved.bridge" = "Museumspoorbaanbrug"; -"type.railway.preserved.tunnel" = "Museumspoorbaantunnel"; -"type.railway.rail" = "Spoorlijn"; -"type.railway.rail.highspeed" = "Hogesnelheidstrein"; +"type.railway.preserved" = "Museumspoorweg"; +"type.railway.preserved.bridge" = "Museumspoorbrug"; +"type.railway.preserved.tunnel" = "Museumspoortunnel"; +"type.railway.rail" = "Spoor"; +"type.railway.rail.highspeed" = "Hogesnelheidsspoor"; "type.railway.rail.tourism" = "Toeristische spoorlijn"; "type.railway.rail.main" = "Spoor"; -"type.railway.turntable" = "Spoorweg-keerpunt"; +"type.railway.turntable" = "Spoordraaischijf"; /* Includes ordinary railway=rail w/o more specific usage= and service= tags. */ -"type.railway.rail.branch" = "Secundaire spoorwegen"; +"type.railway.rail.branch" = "Secundair spoor"; /* Non-passenger utility tracks: industrial, military, test. */ -"type.railway.rail.utility" = "Spoorwegen"; +"type.railway.rail.utility" = "Dienstspoor"; "type.railway.rail.spur" = "Zijspoor"; /* Short service tracks: siding, yard, crossover. */ "type.railway.rail.service" = "Hulpspoor"; -"type.railway.rail.bridge" = "Treinspoorbrug"; -"type.railway.rail.highspeed.bridge" = "Treinspoorbrug"; -"type.railway.rail.tourism.bridge" = "Treinspoorbrug"; -"type.railway.rail.main.bridge" = "Treinspoorbrug"; -"type.railway.rail.branch.bridge" = "Treinspoorbrug"; -"type.railway.rail.utility.bridge" = "Treinspoorbrug"; -"type.railway.rail.spur.bridge" = "Treinspoorbrug"; -"type.railway.rail.service.bridge" = "Treinspoorbrug"; +"type.railway.rail.bridge" = "Spoorbrug"; +"type.railway.rail.highspeed.bridge" = "Spoorbrug"; +"type.railway.rail.tourism.bridge" = "Spoorbrug"; +"type.railway.rail.main.bridge" = "Spoorbrug"; +"type.railway.rail.branch.bridge" = "Spoorbrug"; +"type.railway.rail.utility.bridge" = "Spoorbrug"; +"type.railway.rail.spur.bridge" = "Spoorbrug"; +"type.railway.rail.service.bridge" = "Spoorbrug"; "type.railway.rail.tunnel" = "Spoortunnel"; "type.railway.rail.highspeed.tunnel" = "Spoortunnel"; "type.railway.rail.tourism.tunnel" = "Spoortunnel"; @@ -982,12 +998,12 @@ "type.railway.rail.spur.tunnel" = "Spoortunnel"; "type.railway.rail.service.tunnel" = "Spoortunnel"; "type.railway.station" = "Station"; -"type.railway.station.funicular" = "Tunnel"; -"type.railway.station.light_rail" = "Station"; +"type.railway.station.funicular" = "Kabelspoorstation"; +"type.railway.station.light_rail" = "Lightrailstation"; "type.railway.station.light_rail.berlin" = "S-bahn station"; "type.railway.station.light_rail.london" = "Station"; "type.railway.station.light_rail.porto" = "Station"; -"type.railway.station.monorail" = "Station"; +"type.railway.station.monorail" = "Monorailstation"; "type.railway.station.subway" = "Metrostation"; "type.railway.station.subway.adana" = "Metrostation"; "type.railway.station.subway.algiers" = "Metrostation"; @@ -1103,124 +1119,124 @@ "type.railway.station.subway.wuhan" = "Metrostation"; "type.railway.station.subway.yerevan" = "Metrostation"; "type.railway.station.subway.yokohama" = "Metrostation"; -"type.railway.subway" = "Metro"; +"type.railway.subway" = "Metrolijn"; "type.railway.subway.bridge" = "Metrobrug"; "type.railway.subway.tunnel" = "Metrotunnel"; -"type.railway.subway_entrance" = "Metro ingang"; -"type.railway.subway_entrance.adana" = "Metro ingang"; -"type.railway.subway_entrance.algiers" = "Metro ingang"; -"type.railway.subway_entrance.almaty" = "Metro ingang"; -"type.railway.subway_entrance.amsterdam" = "Metro ingang"; -"type.railway.subway_entrance.ankara" = "Metro ingang"; -"type.railway.subway_entrance.athens" = "Metro ingang"; -"type.railway.subway_entrance.baku" = "Metro ingang"; -"type.railway.subway_entrance.bangkok" = "Metro ingang"; -"type.railway.subway_entrance.barcelona" = "Metro ingang"; -"type.railway.subway_entrance.beijing" = "Metro ingang"; -"type.railway.subway_entrance.bengalore" = "Metro ingang"; -"type.railway.subway_entrance.berlin" = "Metro ingang"; -"type.railway.subway_entrance.bilbao" = "Metro ingang"; -"type.railway.subway_entrance.brasilia" = "Metro ingang"; -"type.railway.subway_entrance.brescia" = "Metro ingang"; -"type.railway.subway_entrance.brussels" = "Metro ingang"; -"type.railway.subway_entrance.bucharest" = "Metro ingang"; -"type.railway.subway_entrance.budapest" = "Metro ingang"; -"type.railway.subway_entrance.buenos_aires" = "Metro ingang"; -"type.railway.subway_entrance.bursa" = "Metro ingang"; -"type.railway.subway_entrance.cairo" = "Metro ingang"; -"type.railway.subway_entrance.caracas" = "Metro ingang"; -"type.railway.subway_entrance.catania" = "Metro ingang"; -"type.railway.subway_entrance.changchun" = "Metro ingang"; -"type.railway.subway_entrance.chengdu" = "Metro ingang"; -"type.railway.subway_entrance.chicago" = "Metro ingang"; -"type.railway.subway_entrance.chongqing" = "Metro ingang"; -"type.railway.subway_entrance.dalian" = "Metro ingang"; -"type.railway.subway_entrance.delhi" = "Metro ingang"; -"type.railway.subway_entrance.dnepro" = "Metro ingang"; -"type.railway.subway_entrance.dubai" = "Metro ingang"; -"type.railway.subway_entrance.ekb" = "Metro ingang"; -"type.railway.subway_entrance.fukuoka" = "Metro ingang"; -"type.railway.subway_entrance.glasgow" = "Metro ingang"; -"type.railway.subway_entrance.guangzhou" = "Metro ingang"; -"type.railway.subway_entrance.hamburg" = "Metro ingang"; -"type.railway.subway_entrance.helsinki" = "Metro ingang"; -"type.railway.subway_entrance.hiroshima" = "Metro ingang"; -"type.railway.subway_entrance.hongkong" = "Metro ingang"; -"type.railway.subway_entrance.isfahan" = "Metro ingang"; -"type.railway.subway_entrance.istanbul" = "Metro ingang"; -"type.railway.subway_entrance.izmir" = "Metro ingang"; -"type.railway.subway_entrance.kazan" = "Metro ingang"; -"type.railway.subway_entrance.kharkiv" = "Metro ingang"; -"type.railway.subway_entrance.kiev" = "Metro ingang"; -"type.railway.subway_entrance.kobe" = "Metro ingang"; -"type.railway.subway_entrance.kolkata" = "Metro ingang"; -"type.railway.subway_entrance.kunming" = "Metro ingang"; -"type.railway.subway_entrance.kyoto" = "Metro ingang"; -"type.railway.subway_entrance.la" = "Metro ingang"; -"type.railway.subway_entrance.lausanne" = "Metro ingang"; -"type.railway.subway_entrance.lille" = "Metro ingang"; -"type.railway.subway_entrance.lima" = "Metro ingang"; -"type.railway.subway_entrance.lisboa" = "Metro ingang"; -"type.railway.subway_entrance.london" = "Metro ingang"; -"type.railway.subway_entrance.lyon" = "Metro ingang"; -"type.railway.subway_entrance.madrid" = "Metro ingang"; -"type.railway.subway_entrance.malaga" = "Metro ingang"; -"type.railway.subway_entrance.manila" = "Metro ingang"; -"type.railway.subway_entrance.maracaibo" = "Metro ingang"; -"type.railway.subway_entrance.mashhad" = "Metro ingang"; -"type.railway.subway_entrance.mecca" = "Metro ingang"; -"type.railway.subway_entrance.medellin" = "Metro ingang"; -"type.railway.subway_entrance.mexico" = "Metro ingang"; -"type.railway.subway_entrance.milan" = "Metro ingang"; -"type.railway.subway_entrance.minsk" = "Metro ingang"; -"type.railway.subway_entrance.montreal" = "Metro ingang"; -"type.railway.subway_entrance.moscow" = "Metro ingang"; -"type.railway.subway_entrance.munchen" = "Metro ingang"; -"type.railway.subway_entrance.nagoya" = "Metro ingang"; -"type.railway.subway_entrance.newyork" = "Metro ingang"; -"type.railway.subway_entrance.nnov" = "Metro ingang"; -"type.railway.subway_entrance.novosibirsk" = "Metro ingang"; -"type.railway.subway_entrance.osaka" = "Metro ingang"; -"type.railway.subway_entrance.oslo" = "Metro ingang"; -"type.railway.subway_entrance.palma" = "Metro ingang"; -"type.railway.subway_entrance.panama" = "Metro ingang"; -"type.railway.subway_entrance.paris" = "Metro ingang"; -"type.railway.subway_entrance.philadelphia" = "Metro ingang"; -"type.railway.subway_entrance.pyongyang" = "Metro ingang"; -"type.railway.subway_entrance.rennes" = "Metro ingang"; -"type.railway.subway_entrance.rio" = "Metro ingang"; -"type.railway.subway_entrance.roma" = "Metro ingang"; -"type.railway.subway_entrance.rotterdam" = "Metro ingang"; -"type.railway.subway_entrance.samara" = "Metro ingang"; -"type.railway.subway_entrance.santiago" = "Metro ingang"; -"type.railway.subway_entrance.santo_domingo" = "Metro ingang"; -"type.railway.subway_entrance.saopaulo" = "Metro ingang"; -"type.railway.subway_entrance.sapporo" = "Metro ingang"; -"type.railway.subway_entrance.sendai" = "Metro ingang"; -"type.railway.subway_entrance.sf" = "Metro ingang"; -"type.railway.subway_entrance.shanghai" = "Metro ingang"; -"type.railway.subway_entrance.shenzhen" = "Metro ingang"; -"type.railway.subway_entrance.shiraz" = "Metro ingang"; -"type.railway.subway_entrance.singapore" = "Metro ingang"; -"type.railway.subway_entrance.sofia" = "Metro ingang"; -"type.railway.subway_entrance.spb" = "Metro ingang"; -"type.railway.subway_entrance.stockholm" = "Metro ingang"; -"type.railway.subway_entrance.tabriz" = "Metro ingang"; -"type.railway.subway_entrance.taipei" = "Metro ingang"; -"type.railway.subway_entrance.taoyuan" = "Metro ingang"; -"type.railway.subway_entrance.tashkent" = "Metro ingang"; -"type.railway.subway_entrance.tbilisi" = "Metro ingang"; -"type.railway.subway_entrance.tehran" = "Metro ingang"; -"type.railway.subway_entrance.tianjin" = "Metro ingang"; -"type.railway.subway_entrance.tokyo" = "Metro ingang"; -"type.railway.subway_entrance.valencia" = "Metro ingang"; -"type.railway.subway_entrance.vienna" = "Metro ingang"; -"type.railway.subway_entrance.warszawa" = "Metro ingang"; -"type.railway.subway_entrance.washington" = "Metro ingang"; -"type.railway.subway_entrance.wuhan" = "Metro ingang"; -"type.railway.subway_entrance.yerevan" = "Metro ingang"; -"type.railway.subway_entrance.yokohama" = "Metro ingang"; -"type.railway.tram" = "Tram"; +"type.railway.subway_entrance" = "Metro-ingang"; +"type.railway.subway_entrance.adana" = "Metro-ingang"; +"type.railway.subway_entrance.algiers" = "Metro-ingang"; +"type.railway.subway_entrance.almaty" = "Metro-ingang"; +"type.railway.subway_entrance.amsterdam" = "Metro-ingang"; +"type.railway.subway_entrance.ankara" = "Metro-ingang"; +"type.railway.subway_entrance.athens" = "Metro-ingang"; +"type.railway.subway_entrance.baku" = "Metro-ingang"; +"type.railway.subway_entrance.bangkok" = "Metro-ingang"; +"type.railway.subway_entrance.barcelona" = "Metro-ingang"; +"type.railway.subway_entrance.beijing" = "Metro-ingang"; +"type.railway.subway_entrance.bengalore" = "Metro-ingang"; +"type.railway.subway_entrance.berlin" = "Metro-ingang"; +"type.railway.subway_entrance.bilbao" = "Metro-ingang"; +"type.railway.subway_entrance.brasilia" = "Metro-ingang"; +"type.railway.subway_entrance.brescia" = "Metro-ingang"; +"type.railway.subway_entrance.brussels" = "Metro-ingang"; +"type.railway.subway_entrance.bucharest" = "Metro-ingang"; +"type.railway.subway_entrance.budapest" = "Metro-ingang"; +"type.railway.subway_entrance.buenos_aires" = "Metro-ingang"; +"type.railway.subway_entrance.bursa" = "Metro-ingang"; +"type.railway.subway_entrance.cairo" = "Metro-ingang"; +"type.railway.subway_entrance.caracas" = "Metro-ingang"; +"type.railway.subway_entrance.catania" = "Metro-ingang"; +"type.railway.subway_entrance.changchun" = "Metro-ingang"; +"type.railway.subway_entrance.chengdu" = "Metro-ingang"; +"type.railway.subway_entrance.chicago" = "Metro-ingang"; +"type.railway.subway_entrance.chongqing" = "Metro-ingang"; +"type.railway.subway_entrance.dalian" = "Metro-ingang"; +"type.railway.subway_entrance.delhi" = "Metro-ingang"; +"type.railway.subway_entrance.dnepro" = "Metro-ingang"; +"type.railway.subway_entrance.dubai" = "Metro-ingang"; +"type.railway.subway_entrance.ekb" = "Metro-ingang"; +"type.railway.subway_entrance.fukuoka" = "Metro-ingang"; +"type.railway.subway_entrance.glasgow" = "Metro-ingang"; +"type.railway.subway_entrance.guangzhou" = "Metro-ingang"; +"type.railway.subway_entrance.hamburg" = "Metro-ingang"; +"type.railway.subway_entrance.helsinki" = "Metro-ingang"; +"type.railway.subway_entrance.hiroshima" = "Metro-ingang"; +"type.railway.subway_entrance.hongkong" = "Metro-ingang"; +"type.railway.subway_entrance.isfahan" = "Metro-ingang"; +"type.railway.subway_entrance.istanbul" = "Metro-ingang"; +"type.railway.subway_entrance.izmir" = "Metro-ingang"; +"type.railway.subway_entrance.kazan" = "Metro-ingang"; +"type.railway.subway_entrance.kharkiv" = "Metro-ingang"; +"type.railway.subway_entrance.kiev" = "Metro-ingang"; +"type.railway.subway_entrance.kobe" = "Metro-ingang"; +"type.railway.subway_entrance.kolkata" = "Metro-ingang"; +"type.railway.subway_entrance.kunming" = "Metro-ingang"; +"type.railway.subway_entrance.kyoto" = "Metro-ingang"; +"type.railway.subway_entrance.la" = "Metro-ingang"; +"type.railway.subway_entrance.lausanne" = "Metro-ingang"; +"type.railway.subway_entrance.lille" = "Metro-ingang"; +"type.railway.subway_entrance.lima" = "Metro-ingang"; +"type.railway.subway_entrance.lisboa" = "Metro-ingang"; +"type.railway.subway_entrance.london" = "Metro-ingang"; +"type.railway.subway_entrance.lyon" = "Metro-ingang"; +"type.railway.subway_entrance.madrid" = "Metro-ingang"; +"type.railway.subway_entrance.malaga" = "Metro-ingang"; +"type.railway.subway_entrance.manila" = "Metro-ingang"; +"type.railway.subway_entrance.maracaibo" = "Metro-ingang"; +"type.railway.subway_entrance.mashhad" = "Metro-ingang"; +"type.railway.subway_entrance.mecca" = "Metro-ingang"; +"type.railway.subway_entrance.medellin" = "Metro-ingang"; +"type.railway.subway_entrance.mexico" = "Metro-ingang"; +"type.railway.subway_entrance.milan" = "Metro-ingang"; +"type.railway.subway_entrance.minsk" = "Metro-ingang"; +"type.railway.subway_entrance.montreal" = "Metro-ingang"; +"type.railway.subway_entrance.moscow" = "Metro-ingang"; +"type.railway.subway_entrance.munchen" = "Metro-ingang"; +"type.railway.subway_entrance.nagoya" = "Metro-ingang"; +"type.railway.subway_entrance.newyork" = "Metro-ingang"; +"type.railway.subway_entrance.nnov" = "Metro-ingang"; +"type.railway.subway_entrance.novosibirsk" = "Metro-ingang"; +"type.railway.subway_entrance.osaka" = "Metro-ingang"; +"type.railway.subway_entrance.oslo" = "Metro-ingang"; +"type.railway.subway_entrance.palma" = "Metro-ingang"; +"type.railway.subway_entrance.panama" = "Metro-ingang"; +"type.railway.subway_entrance.paris" = "Metro-ingang"; +"type.railway.subway_entrance.philadelphia" = "Metro-ingang"; +"type.railway.subway_entrance.pyongyang" = "Metro-ingang"; +"type.railway.subway_entrance.rennes" = "Metro-ingang"; +"type.railway.subway_entrance.rio" = "Metro-ingang"; +"type.railway.subway_entrance.roma" = "Metro-ingang"; +"type.railway.subway_entrance.rotterdam" = "Metro-ingang"; +"type.railway.subway_entrance.samara" = "Metro-ingang"; +"type.railway.subway_entrance.santiago" = "Metro-ingang"; +"type.railway.subway_entrance.santo_domingo" = "Metro-ingang"; +"type.railway.subway_entrance.saopaulo" = "Metro-ingang"; +"type.railway.subway_entrance.sapporo" = "Metro-ingang"; +"type.railway.subway_entrance.sendai" = "Metro-ingang"; +"type.railway.subway_entrance.sf" = "Metro-ingang"; +"type.railway.subway_entrance.shanghai" = "Metro-ingang"; +"type.railway.subway_entrance.shenzhen" = "Metro-ingang"; +"type.railway.subway_entrance.shiraz" = "Metro-ingang"; +"type.railway.subway_entrance.singapore" = "Metro-ingang"; +"type.railway.subway_entrance.sofia" = "Metro-ingang"; +"type.railway.subway_entrance.spb" = "Metro-ingang"; +"type.railway.subway_entrance.stockholm" = "Metro-ingang"; +"type.railway.subway_entrance.tabriz" = "Metro-ingang"; +"type.railway.subway_entrance.taipei" = "Metro-ingang"; +"type.railway.subway_entrance.taoyuan" = "Metro-ingang"; +"type.railway.subway_entrance.tashkent" = "Metro-ingang"; +"type.railway.subway_entrance.tbilisi" = "Metro-ingang"; +"type.railway.subway_entrance.tehran" = "Metro-ingang"; +"type.railway.subway_entrance.tianjin" = "Metro-ingang"; +"type.railway.subway_entrance.tokyo" = "Metro-ingang"; +"type.railway.subway_entrance.valencia" = "Metro-ingang"; +"type.railway.subway_entrance.vienna" = "Metro-ingang"; +"type.railway.subway_entrance.warszawa" = "Metro-ingang"; +"type.railway.subway_entrance.washington" = "Metro-ingang"; +"type.railway.subway_entrance.wuhan" = "Metro-ingang"; +"type.railway.subway_entrance.yerevan" = "Metro-ingang"; +"type.railway.subway_entrance.yokohama" = "Metro-ingang"; +"type.railway.tram" = "Tramspoor"; "type.railway.tram.bridge" = "Trambrug"; "type.railway.tram.tunnel" = "Tramtunnel"; "type.railway.tram_stop" = "Tramhalte"; @@ -1232,27 +1248,27 @@ "type.shop.bathroom_furnishing" = "Badkamerinrichting"; "type.shop.beauty" = "Schoonheidssalon"; "type.shop.beauty.nails" = "Nagelsalon"; -"type.shop.beverages" = "Drank"; +"type.shop.beverages" = "Drankenwinkel"; "type.shop.bicycle" = "Fietsenwinkel"; -"type.shop.bookmaker" = "Boekbinder"; +"type.shop.bookmaker" = "Wedkantoor"; "type.shop.books" = "Boekwinkel"; "type.shop.butcher" = "Slager"; "type.shop.cannabis" = "Coffeeshop"; -"type.shop.car" = "Autohandelaar"; -"type.shop.car_parts" = "Auto-onderdelen"; -"type.shop.car_repair" = "Auto reparatie"; +"type.shop.car" = "Autodealer"; +"type.shop.car_parts" = "Auto-onderdelenwinkel"; +"type.shop.car_repair" = "Autogarage"; "type.shop.car_repair.tyres" = "Bandenreparatie"; -"type.shop.caravan" = "Caravan en camper verkoper"; +"type.shop.caravan" = "Caravan- en camperdealer"; "type.shop.carpet" = "Tapijtenwinkel"; "type.shop.chemist" = "Drogisterij"; "type.shop.chocolate" = "Chocolaterie"; "type.shop.clothes" = "Kledingwinkel"; -"type.shop.coffee" = "Koffieverkoop"; +"type.shop.coffee" = "Koffiewinkel"; "type.shop.computer" = "Computerwinkel"; "type.shop.confectionery" = "Snoepwinkel"; -"type.shop.convenience" = "Buurtwinkel"; +"type.shop.convenience" = "Gemakswinkel"; "type.shop.copyshop" = "Kopieerwinkel"; -"type.shop.cosmetics" = "Schoonheidsmiddelen"; +"type.shop.cosmetics" = "Cosmeticawinkel"; "type.shop.curtain" = "Gordijnenwinkel"; "type.shop.deli" = "Delicatessenwinkel"; "type.shop.department_store" = "Warenhuis"; @@ -1262,21 +1278,21 @@ "type.shop.erotic" = "Erotiekwinkel"; "type.shop.fabric" = "Stoffenwinkel"; "type.shop.farm" = "Boerderijwinkel"; -"type.shop.fashion_accessories" = "Modeaccessoires"; +"type.shop.fashion_accessories" = "Modeaccessoirewinkel"; "type.shop.florist" = "Bloemist"; -"type.shop.funeral_directors" = "Begrafenisondernemer"; +"type.shop.funeral_directors" = "Uitvaartondernemer"; "type.shop.furniture" = "Meubelwinkel"; "type.shop.garden_centre" = "Tuincentrum"; "type.shop.gas" = "Gaswinkel"; "type.shop.gift" = "Cadeauwinkel"; "type.shop.greengrocer" = "Groentenwinkel"; -"type.shop.grocery" = "Boodschappen"; +"type.shop.grocery" = "Kruidenierswinkel"; "type.shop.hairdresser" = "Kapper"; -"type.shop.hardware" = "IJzerwaren"; +"type.shop.hardware" = "IJzerwarenwinkel"; "type.shop.health_food" = "Natuurvoedingswinkel"; -"type.shop.hearing_aids" = "Winkel met hoortoestellen"; +"type.shop.hearing_aids" = "Hoortoestellenwinkel"; "type.shop.herbalist" = "Kruidenwinkel"; -"type.shop.hifi" = "HiFi-audio"; +"type.shop.hifi" = "Hifiwinkel"; "type.shop.houseware" = "Huishoudwinkel"; "type.shop.jewelry" = "Juwelier"; "type.shop.kiosk" = "Kiosk"; @@ -1285,18 +1301,18 @@ "type.shop.mall" = "Winkelcentrum"; "type.shop.beauty.day_spa" = "Schoonheidssalon"; "type.shop.massage" = "Massagesalon"; -"type.shop.mobile_phone" = "Mobiele telefoonwinkel"; +"type.shop.mobile_phone" = "Mobiele-telefoonwinkel"; "type.shop.money_lender" = "Geldschieter"; "type.shop.motorcycle" = "Motorzaak"; "type.shop.motorcycle_repair" = "Motorfietsreparatie"; "type.shop.music" = "Muziekwinkel"; "type.shop.musical_instrument" = "Muziekinstrumentenwinkel"; -"type.shop.newsagent" = "Kiosk"; +"type.shop.newsagent" = "Krantenkiosk"; "type.shop.optician" = "Opticien"; -"type.shop.outdoor" = "Outdooruitrusting"; +"type.shop.outdoor" = "Buitensportwinkel"; "type.shop.outpost" = "Afhaalpunt"; "type.shop.pasta" = "Pastawinkel"; -"type.shop.pastry" = "Banketbakker"; +"type.shop.pastry" = "Banketbakkerij"; "type.shop.pawnbroker" = "Pandjeshuis"; "type.shop.pet" = "Dierenwinkel"; "type.shop.pet_grooming" = "Huisdierverzorging"; @@ -1306,10 +1322,10 @@ "type.shop.seafood" = "Visboer"; "type.shop.second_hand" = "Tweedehandswinkel"; "type.shop.shoes" = "Schoenenwinkel"; -"type.shop.sports" = "Sportartikelen"; +"type.shop.sports" = "Sportwinkel"; "type.shop.stationery" = "Kantoorboekhandel"; "type.shop.supermarket" = "Supermarkt"; -"type.shop.tattoo" = "Tattoosalon"; +"type.shop.tattoo" = "Tatoeagestudio"; "type.shop.tea" = "Theewinkel"; "type.shop.telecommunication" = "Telecommunicatiewinkel"; "type.shop.ticket" = "Kaartjesverkoop"; @@ -1326,30 +1342,30 @@ "type.shop.water" = "Waterwinkel"; /* maybe change to Art Gallery for en-US when supported */ "type.shop.art" = "Kunstwinkel"; -"type.shop.baby_goods" = "Babyspullenwinkel"; +"type.shop.baby_goods" = "Babywinkel"; "type.shop.bag" = "Tassenwinkel"; "type.shop.bed" = "Beddenwinkel"; "type.shop.boutique" = "Boetiek"; "type.shop.charity" = "Kringloopwinkel"; "type.shop.cheese" = "Kaaswinkel"; -"type.shop.craft" = "Kunst en ambacht"; -"type.shop.dairy" = "Zuivelproducten"; +"type.shop.craft" = "Kunst- en hobbywinkel"; +"type.shop.dairy" = "Zuivelwinkel"; "type.shop.electrical" = "Elektronische benodigdhedenwinkel"; "type.shop.fishing" = "Viswinkel"; "type.shop.interior_decoration" = "Interieurdecoratiewinkel"; -"type.shop.lottery" = "Loten"; -"type.shop.medical_supply" = "Medische benodigdheden"; +"type.shop.lottery" = "Loterijwinkel"; +"type.shop.medical_supply" = "Medische benodigdhedenwinkel"; "type.shop.nutrition_supplements" = "Voedingssupplementenwinkel"; "type.shop.paint" = "Verfwinkel"; "type.shop.perfumery" = "Parfumerie"; -"type.shop.sewing" = "Naaibenodigdheden"; +"type.shop.sewing" = "Naai- en fourniturenwinkel"; "type.shop.storage_rental" = "Opslagverhuur"; -"type.shop.tobacco" = "Tabakszaak"; -"type.shop.trade" = "Handelsbenodigdheden"; -"type.shop.watches" = "Horlogezaak"; +"type.shop.tobacco" = "Tabakswinkel"; +"type.shop.trade" = "Technische groothandel"; +"type.shop.watches" = "Horlogewinkel"; "type.shop.wholesale" = "Groothandel"; "type.shop.lighting" = "Verlichtingswinkel"; -"type.disusedbusiness" = "Leegstand bedrijfspand"; +"type.disusedbusiness" = "Leegstaand bedrijfspand"; "type.sport" = "Sport"; "type.sport.multi" = "Diverse sporten"; "type.sport.9pin" = "Bowlen"; @@ -1362,23 +1378,23 @@ "type.sport.baseball" = "Honkbal"; "type.sport.basketball" = "Basketbal"; "type.sport.beachvolleyball" = "Beachvolleybal"; -"type.sport.bowls" = "Bowling"; +"type.sport.bowls" = "Bowls"; "type.sport.chess" = "Schaken"; "type.sport.climbing" = "Klimmen"; "type.sport.cricket" = "Cricket"; "type.sport.curling" = "Curling"; "type.sport.diving" = "Klifduiken"; -"type.sport.equestrian" = "Paardesport"; +"type.sport.equestrian" = "Paardensport"; "type.sport.field_hockey" = "Veldhockey"; -"type.sport.futsal" = "Futsal"; +"type.sport.futsal" = "Zaalvoetbal"; "type.sport.golf" = "Golf"; "type.sport.gymnastics" = "Gymnastiek"; "type.sport.handball" = "Handbal"; "type.sport.ice_hockey" = "IJshockey"; "type.sport.padel" = "Padel"; -"type.sport.pelota" = "Baskische peloton"; +"type.sport.pelota" = "Pelota"; "type.sport.scuba_diving" = "Duiken"; -"type.sport.shooting" = "schieten"; +"type.sport.shooting" = "Schietsport"; "type.sport.skateboard" = "Skateboarden"; "type.sport.skiing" = "Skiën"; "type.sport.soccer" = "Voetbal"; @@ -1391,30 +1407,30 @@ "type.tourism.aquarium" = "Aquarium"; /* Typically serviced, staff is present and food is available (compared to wilderness_hut). */ "type.tourism.alpine_hut" = "Berghut"; -"type.tourism.apartment" = "Appartement"; +"type.tourism.apartment" = "Vakantieappartement"; "type.tourism.artwork" = "Kunstwerk"; -"type.tourism.artwork.architecture" = "Kunstwerk"; +"type.tourism.artwork.architecture" = "Architectonisch kunstwerk"; "type.tourism.artwork.painting" = "Schilderij"; -"type.tourism.artwork.sculpture" = "Kunstwerk"; +"type.tourism.artwork.sculpture" = "Sculptuur"; "type.tourism.artwork.statue" = "Standbeeld"; -"type.tourism.attraction" = "Toeristische attractie"; +"type.tourism.attraction" = "Bezienswaardigheid"; "type.attraction" = "Attractie"; -"type.attraction.amusement_ride" = "Prettocht"; +"type.attraction.amusement_ride" = "Attractie"; "type.attraction.animal" = "Dierenverblijf"; "type.attraction.bumper_car" = "Botsauto's"; "type.attraction.big_wheel" = "Reuzenrad"; -"type.attraction.carousel" = "Reuzenrad"; +"type.attraction.carousel" = "Draaimolen"; "type.attraction.historic" = "Historische attractie"; "type.attraction.maze" = "Doolhof"; "type.attraction.roller_coaster" = "Achtbaan"; "type.attraction.water_slide" = "Waterglijbaan"; "type.tourism.attraction.specified" = "Attractie"; -"type.tourism.camp_site" = "Kampeerterrein"; -"type.tourism.caravan_site" = "Caravan site"; +"type.tourism.camp_site" = "Camping"; +"type.tourism.caravan_site" = "Caravan- en camperplaats"; /* A rentable countryside vacation house. */ "type.tourism.chalet" = "Vakantiehuis"; -"type.tourism.gallery" = "Galerij"; -"type.tourism.guest_house" = "Gasthuis"; +"type.tourism.gallery" = "Galerie"; +"type.tourism.guest_house" = "Pension"; "type.tourism.hostel" = "Jeugdherberg"; "type.tourism.hotel" = "Hotel"; "type.tourism.information" = "Toeristische informatie"; @@ -1422,22 +1438,22 @@ "type.tourism.information.guidepost" = "Wegwijzer"; "type.tourism.information.map" = "Toeristische kaart"; "type.tourism.information.tactile_map" = "Tactiele kaart"; -"type.tourism.information.office" = "Toeristische informatie"; +"type.tourism.information.office" = "Toeristeninformatiepunt"; "type.tourism.information.visitor_centre" = "Bezoekerscentrum"; -"type.amenity.ranger_station" = "Boswachterkantoor"; +"type.amenity.ranger_station" = "Boswachterskantoor"; "type.tourism.motel" = "Motel"; "type.tourism.museum" = "Museum"; -"type.tourism.picnic_site" = "Picnicplaats"; +"type.tourism.picnic_site" = "Picknickplaats"; "type.leisure.resort" = "Complex"; "type.tourism.theme_park" = "Attractiepark"; "type.tourism.viewpoint" = "Uitkijkpunt"; /* Typically more basic, not staffed and free (compared to alpine_hut). */ -"type.tourism.wilderness_hut" = "Wildernishut"; +"type.tourism.wilderness_hut" = "Zelfverzorgingshut"; "type.tourism.zoo" = "Dierentuin"; "type.tourism.zoo.petting" = "Kinderboerderij"; -"type.traffic_calming" = "Snelheidsmatigende maatregel"; +"type.traffic_calming" = "Snelheidsremmende maatregel"; "type.traffic_calming.bump" = "Verkeersdrempel"; -"type.traffic_calming.hump" = "Verkeersheuvel"; +"type.traffic_calming.hump" = "Verkeersdrempel"; "type.waterway" = "Waterweg"; "type.waterway.canal" = "Kanaal"; "type.waterway.canal.tunnel" = "Kanaal"; @@ -1461,37 +1477,37 @@ "type.waterway.waterfall" = "Waterval"; "type.waterway.weir" = "Stuw"; "type.wheelchair" = "Rolstoel"; -"type.wheelchair.limited" = "Gedeeltelijk uitgerust voor gehandicapten"; -"type.wheelchair.no" = "Niet uitgerust voor gehandicapten"; -"type.wheelchair.yes" = "Uitgerust voor gehandicapten"; +"type.wheelchair.limited" = "Gedeeltelijk rolstoeltoegankelijk"; +"type.wheelchair.no" = "Niet rolstoeltoegankelijk"; +"type.wheelchair.yes" = "Volledig rolstoeltoegankelijk"; "type.aerialway.j.bar" = "Sleeplift (J)"; -"type.aerialway.magic_carpet" = "Loopband"; -"type.aerialway.platter" = "Sleeplift"; +"type.aerialway.magic_carpet" = "Lopende band (skilift)"; +"type.aerialway.platter" = "Pannenkoeklift"; "type.aerialway.rope_tow" = "Touwlift"; "type.aerialway.t.bar" = "Sleeplift (T-beugel)"; -"type.piste_type.downhill" = "Ski-afdaling"; -"type.piste_type.downhill.area" = "Ski-afdaling"; -"type.piste_type.downhill.advanced" = "Moeilijke afdaling"; -"type.piste_type.downhill.advanced.area" = "Moeilijke afdaling"; -"type.piste_type.downhill.easy" = "Makkelijke afdaling"; -"type.piste_type.downhill.easy.area" = "Makkelijke afdaling"; -"type.piste_type.downhill.expert" = "Afdaling voor experts"; -"type.piste_type.downhill.expert.area" = "Afdaling voor experts"; -"type.piste_type.downhill.freeride" = "Ski-afdaling"; -"type.piste_type.downhill.intermediate" = "Gemiddelde afdaling"; -"type.piste_type.downhill.intermediate.area" = "Gemiddelde afdaling"; -"type.piste_type.downhill.novice" = "Beginnersafdaling"; -"type.piste_type.downhill.novice.area" = "Beginnersafdaling"; +"type.piste_type.downhill" = "Skipiste"; +"type.piste_type.downhill.area" = "Skipiste"; +"type.piste_type.downhill.advanced" = "Moeilijke skipiste"; +"type.piste_type.downhill.advanced.area" = "Moeilijke skipiste"; +"type.piste_type.downhill.easy" = "Makkelijke skipiste"; +"type.piste_type.downhill.easy.area" = "Makkelijke skipiste"; +"type.piste_type.downhill.expert" = "Zeer moeilijke skipiste"; +"type.piste_type.downhill.expert.area" = "Zeer moeilijke skipiste"; +"type.piste_type.downhill.freeride" = "Freeride ski-afdaling"; +"type.piste_type.downhill.intermediate" = "Gemiddelde skipiste"; +"type.piste_type.downhill.intermediate.area" = "Gemiddelde skipiste"; +"type.piste_type.downhill.novice" = "Beginnersskipiste"; +"type.piste_type.downhill.novice.area" = "Beginnersskipiste"; "type.piste_type.nordic" = "Langlaufroute"; "type.piste_type.sled" = "Sleebaan"; "type.piste_type.sled.area" = "Sleebaan"; "type.piste_type.snow_park" = "Sneeuwpark"; "type.piste_type.hike" = "Sneeuwwandelpad"; "type.piste_type.connection" = "Pisteverbinding"; -"type.piste_type.skitour" = "Skitourspoor"; +"type.piste_type.skitour" = "Toerskiroute"; "type.amenity.events_venue" = "Evenementenlocatie"; "type.shop.auction" = "Veiling"; -"type.shop.collector" = "Verzamelobjecten"; +"type.shop.collector" = "Verzamelaarswinkel"; "type.self_service.yes" = "Zelfbediening beschikbaar"; "type.self_service.only" = "Alleen zelfbediening"; "type.self_service.partially" = "Gedeeltelijke zelfbediening"; @@ -1500,8 +1516,8 @@ "type.amenity.social_facility" = "Sociale voorziening"; "type.social_facility.soup_kitchen" = "Gaarkeuken"; "type.social_facility.food_bank" = "Voedselbank"; -"type.amenity.food_sharing" = "Voedsel delen"; -"type.amenity.give_box" = "Giftenkist"; +"type.amenity.food_sharing" = "Voedseldeelpunt"; +"type.amenity.give_box" = "Weggeefkast"; /* https://wiki.openstreetmap.org/wiki/Tag:emergency=emergency_ward_entrance */ "type.emergency.emergency_ward_entrance" = "Ingang noodafdeling"; /* https://wiki.openstreetmap.org/wiki/Tag:amenity=dojo */ @@ -1511,7 +1527,7 @@ "type.xmas.tree" = "Kerstboom"; "type.leisure.sports_centre.sport.four_square" = "Sportcentrum"; "type.sport.four_square" = "Four Square"; -"type.sport.boules" = "jeu de boules"; +"type.sport.boules" = "Jeu de boules"; "type.leisure.sports_centre.sport.boules" = "Sportcentrum"; "type.sport.pickleball" = "Pickleball"; "type.leisure.sports_centre.sport.pickleball" = "Sportcentrum"; diff --git a/iphone/Maps/LocalizedStrings/pl.lproj/LocalizableTypes.strings b/iphone/Maps/LocalizedStrings/pl.lproj/LocalizableTypes.strings index d1ebcf6e2..47f40d336 100644 --- a/iphone/Maps/LocalizedStrings/pl.lproj/LocalizableTypes.strings +++ b/iphone/Maps/LocalizedStrings/pl.lproj/LocalizableTypes.strings @@ -452,6 +452,22 @@ "type.highway.busway.tunnel" = "Tunel"; "type.highway.bus_stop" = "Przystanek autobusowy"; "type.highway.construction" = "Droga w trakcie budowy"; +"type.highway.construction.motorway" = "Droga w trakcie budowy"; +"type.highway.construction.motorway_link" = "Droga w trakcie budowy"; +"type.highway.construction.trunk" = "Droga w trakcie budowy"; +"type.highway.construction.trunk_link" = "Droga w trakcie budowy"; +"type.highway.construction.primary" = "Droga w trakcie budowy"; +"type.highway.construction.primary_link" = "Droga w trakcie budowy"; +"type.highway.construction.secondary" = "Droga w trakcie budowy"; +"type.highway.construction.secondary_link" = "Droga w trakcie budowy"; +"type.highway.construction.tertiary" = "Droga w trakcie budowy"; +"type.highway.construction.tertiary_link" = "Droga w trakcie budowy"; +"type.highway.construction.residential" = "Droga w trakcie budowy"; +"type.highway.construction.unclassified" = "Droga w trakcie budowy"; +"type.highway.construction.service" = "Droga w trakcie budowy"; +"type.highway.construction.living_street" = "Droga w trakcie budowy"; +"type.highway.construction.road" = "Droga w trakcie budowy"; +"type.highway.construction.track" = "Droga w trakcie budowy"; "type.highway.cycleway" = "Droga rowerowa"; /* These translations are used for all type.highway.*.bridge. */ "type.highway.cycleway.bridge" = "Most drogowy dla rowerów"; diff --git a/iphone/Maps/LocalizedStrings/pt-BR.lproj/LocalizableTypes.strings b/iphone/Maps/LocalizedStrings/pt-BR.lproj/LocalizableTypes.strings index c72164138..f014c7469 100644 --- a/iphone/Maps/LocalizedStrings/pt-BR.lproj/LocalizableTypes.strings +++ b/iphone/Maps/LocalizedStrings/pt-BR.lproj/LocalizableTypes.strings @@ -452,6 +452,22 @@ "type.highway.busway.tunnel" = "Túnel"; "type.highway.bus_stop" = "Ponto de ônibus"; "type.highway.construction" = "Via em construção"; +"type.highway.construction.motorway" = "Via em construção"; +"type.highway.construction.motorway_link" = "Via em construção"; +"type.highway.construction.trunk" = "Via em construção"; +"type.highway.construction.trunk_link" = "Via em construção"; +"type.highway.construction.primary" = "Via em construção"; +"type.highway.construction.primary_link" = "Via em construção"; +"type.highway.construction.secondary" = "Via em construção"; +"type.highway.construction.secondary_link" = "Via em construção"; +"type.highway.construction.tertiary" = "Via em construção"; +"type.highway.construction.tertiary_link" = "Via em construção"; +"type.highway.construction.residential" = "Via em construção"; +"type.highway.construction.unclassified" = "Via em construção"; +"type.highway.construction.service" = "Via em construção"; +"type.highway.construction.living_street" = "Via em construção"; +"type.highway.construction.road" = "Via em construção"; +"type.highway.construction.track" = "Via em construção"; "type.highway.cycleway" = "Ciclovia"; /* These translations are used for all type.highway.*.bridge. */ "type.highway.cycleway.bridge" = "Ponte para ciclistas"; diff --git a/iphone/Maps/LocalizedStrings/pt.lproj/LocalizableTypes.strings b/iphone/Maps/LocalizedStrings/pt.lproj/LocalizableTypes.strings index 66f873814..779b15f49 100644 --- a/iphone/Maps/LocalizedStrings/pt.lproj/LocalizableTypes.strings +++ b/iphone/Maps/LocalizedStrings/pt.lproj/LocalizableTypes.strings @@ -452,6 +452,22 @@ "type.highway.busway.tunnel" = "Túnel"; "type.highway.bus_stop" = "Paragem de autocarros"; "type.highway.construction" = "Estrada em construção"; +"type.highway.construction.motorway" = "Estrada em construção"; +"type.highway.construction.motorway_link" = "Estrada em construção"; +"type.highway.construction.trunk" = "Estrada em construção"; +"type.highway.construction.trunk_link" = "Estrada em construção"; +"type.highway.construction.primary" = "Estrada em construção"; +"type.highway.construction.primary_link" = "Estrada em construção"; +"type.highway.construction.secondary" = "Estrada em construção"; +"type.highway.construction.secondary_link" = "Estrada em construção"; +"type.highway.construction.tertiary" = "Estrada em construção"; +"type.highway.construction.tertiary_link" = "Estrada em construção"; +"type.highway.construction.residential" = "Estrada em construção"; +"type.highway.construction.unclassified" = "Estrada em construção"; +"type.highway.construction.service" = "Estrada em construção"; +"type.highway.construction.living_street" = "Estrada em construção"; +"type.highway.construction.road" = "Estrada em construção"; +"type.highway.construction.track" = "Estrada em construção"; "type.highway.cycleway" = "Ciclovia"; /* These translations are used for all type.highway.*.bridge. */ "type.highway.cycleway.bridge" = "Ponte"; diff --git a/iphone/Maps/LocalizedStrings/ro.lproj/LocalizableTypes.strings b/iphone/Maps/LocalizedStrings/ro.lproj/LocalizableTypes.strings index c6c7fd80c..761979379 100644 --- a/iphone/Maps/LocalizedStrings/ro.lproj/LocalizableTypes.strings +++ b/iphone/Maps/LocalizedStrings/ro.lproj/LocalizableTypes.strings @@ -452,6 +452,22 @@ "type.highway.busway.tunnel" = "Tunel"; "type.highway.bus_stop" = "Stație de autobuz"; "type.highway.construction" = "Drum în construcție"; +"type.highway.construction.motorway" = "Drum în construcție"; +"type.highway.construction.motorway_link" = "Drum în construcție"; +"type.highway.construction.trunk" = "Drum în construcție"; +"type.highway.construction.trunk_link" = "Drum în construcție"; +"type.highway.construction.primary" = "Drum în construcție"; +"type.highway.construction.primary_link" = "Drum în construcție"; +"type.highway.construction.secondary" = "Drum în construcție"; +"type.highway.construction.secondary_link" = "Drum în construcție"; +"type.highway.construction.tertiary" = "Drum în construcție"; +"type.highway.construction.tertiary_link" = "Drum în construcție"; +"type.highway.construction.residential" = "Drum în construcție"; +"type.highway.construction.unclassified" = "Drum în construcție"; +"type.highway.construction.service" = "Drum în construcție"; +"type.highway.construction.living_street" = "Drum în construcție"; +"type.highway.construction.road" = "Drum în construcție"; +"type.highway.construction.track" = "Drum în construcție"; "type.highway.cycleway" = "Pistă de Biciclete"; /* These translations are used for all type.highway.*.bridge. */ "type.highway.cycleway.bridge" = "Pod"; diff --git a/iphone/Maps/LocalizedStrings/ru.lproj/LocalizableTypes.strings b/iphone/Maps/LocalizedStrings/ru.lproj/LocalizableTypes.strings index 3ad63b87d..e1eeb2d67 100644 --- a/iphone/Maps/LocalizedStrings/ru.lproj/LocalizableTypes.strings +++ b/iphone/Maps/LocalizedStrings/ru.lproj/LocalizableTypes.strings @@ -452,6 +452,22 @@ "type.highway.busway.tunnel" = "Тоннель"; "type.highway.bus_stop" = "Остановка"; "type.highway.construction" = "Строящаяся дорога"; +"type.highway.construction.motorway" = "Строящаяся дорога"; +"type.highway.construction.motorway_link" = "Строящаяся дорога"; +"type.highway.construction.trunk" = "Строящаяся дорога"; +"type.highway.construction.trunk_link" = "Строящаяся дорога"; +"type.highway.construction.primary" = "Строящаяся дорога"; +"type.highway.construction.primary_link" = "Строящаяся дорога"; +"type.highway.construction.secondary" = "Строящаяся дорога"; +"type.highway.construction.secondary_link" = "Строящаяся дорога"; +"type.highway.construction.tertiary" = "Строящаяся дорога"; +"type.highway.construction.tertiary_link" = "Строящаяся дорога"; +"type.highway.construction.residential" = "Строящаяся дорога"; +"type.highway.construction.unclassified" = "Строящаяся дорога"; +"type.highway.construction.service" = "Строящаяся дорога"; +"type.highway.construction.living_street" = "Строящаяся дорога"; +"type.highway.construction.road" = "Строящаяся дорога"; +"type.highway.construction.track" = "Строящаяся дорога"; "type.highway.cycleway" = "Велодорожка"; /* These translations are used for all type.highway.*.bridge. */ "type.highway.cycleway.bridge" = "Мост"; diff --git a/iphone/Maps/LocalizedStrings/sk.lproj/LocalizableTypes.strings b/iphone/Maps/LocalizedStrings/sk.lproj/LocalizableTypes.strings index 3a58f841c..c9527912b 100644 --- a/iphone/Maps/LocalizedStrings/sk.lproj/LocalizableTypes.strings +++ b/iphone/Maps/LocalizedStrings/sk.lproj/LocalizableTypes.strings @@ -452,6 +452,22 @@ "type.highway.busway.tunnel" = "Tunel"; "type.highway.bus_stop" = "Autobusová zastávka"; "type.highway.construction" = "Cesta vo výstavbe"; +"type.highway.construction.motorway" = "Cesta vo výstavbe"; +"type.highway.construction.motorway_link" = "Cesta vo výstavbe"; +"type.highway.construction.trunk" = "Cesta vo výstavbe"; +"type.highway.construction.trunk_link" = "Cesta vo výstavbe"; +"type.highway.construction.primary" = "Cesta vo výstavbe"; +"type.highway.construction.primary_link" = "Cesta vo výstavbe"; +"type.highway.construction.secondary" = "Cesta vo výstavbe"; +"type.highway.construction.secondary_link" = "Cesta vo výstavbe"; +"type.highway.construction.tertiary" = "Cesta vo výstavbe"; +"type.highway.construction.tertiary_link" = "Cesta vo výstavbe"; +"type.highway.construction.residential" = "Cesta vo výstavbe"; +"type.highway.construction.unclassified" = "Cesta vo výstavbe"; +"type.highway.construction.service" = "Cesta vo výstavbe"; +"type.highway.construction.living_street" = "Cesta vo výstavbe"; +"type.highway.construction.road" = "Cesta vo výstavbe"; +"type.highway.construction.track" = "Cesta vo výstavbe"; "type.highway.cycleway" = "Cyklocesta"; /* These translations are used for all type.highway.*.bridge. */ "type.highway.cycleway.bridge" = "Most"; diff --git a/iphone/Maps/LocalizedStrings/sl.lproj/LocalizableTypes.strings b/iphone/Maps/LocalizedStrings/sl.lproj/LocalizableTypes.strings index b8b71c21c..d103a4b4d 100644 --- a/iphone/Maps/LocalizedStrings/sl.lproj/LocalizableTypes.strings +++ b/iphone/Maps/LocalizedStrings/sl.lproj/LocalizableTypes.strings @@ -452,6 +452,22 @@ "type.highway.busway.tunnel" = "Predor"; "type.highway.bus_stop" = "Avtobusno postajališče"; "type.highway.construction" = "Cesta v gradnji"; +"type.highway.construction.motorway" = "Cesta v gradnji"; +"type.highway.construction.motorway_link" = "Cesta v gradnji"; +"type.highway.construction.trunk" = "Cesta v gradnji"; +"type.highway.construction.trunk_link" = "Cesta v gradnji"; +"type.highway.construction.primary" = "Cesta v gradnji"; +"type.highway.construction.primary_link" = "Cesta v gradnji"; +"type.highway.construction.secondary" = "Cesta v gradnji"; +"type.highway.construction.secondary_link" = "Cesta v gradnji"; +"type.highway.construction.tertiary" = "Cesta v gradnji"; +"type.highway.construction.tertiary_link" = "Cesta v gradnji"; +"type.highway.construction.residential" = "Cesta v gradnji"; +"type.highway.construction.unclassified" = "Cesta v gradnji"; +"type.highway.construction.service" = "Cesta v gradnji"; +"type.highway.construction.living_street" = "Cesta v gradnji"; +"type.highway.construction.road" = "Cesta v gradnji"; +"type.highway.construction.track" = "Cesta v gradnji"; "type.highway.cycleway" = "Kolesarska steza"; /* These translations are used for all type.highway.*.bridge. */ "type.highway.cycleway.bridge" = "Most"; diff --git a/iphone/Maps/LocalizedStrings/sr.lproj/LocalizableTypes.strings b/iphone/Maps/LocalizedStrings/sr.lproj/LocalizableTypes.strings index 04faa5d22..0e4b62f0b 100644 --- a/iphone/Maps/LocalizedStrings/sr.lproj/LocalizableTypes.strings +++ b/iphone/Maps/LocalizedStrings/sr.lproj/LocalizableTypes.strings @@ -452,6 +452,22 @@ "type.highway.busway.tunnel" = "Тунел"; "type.highway.bus_stop" = "Аутобуско стајалиште"; "type.highway.construction" = "Пут у изградњи"; +"type.highway.construction.motorway" = "Пут у изградњи"; +"type.highway.construction.motorway_link" = "Пут у изградњи"; +"type.highway.construction.trunk" = "Пут у изградњи"; +"type.highway.construction.trunk_link" = "Пут у изградњи"; +"type.highway.construction.primary" = "Пут у изградњи"; +"type.highway.construction.primary_link" = "Пут у изградњи"; +"type.highway.construction.secondary" = "Пут у изградњи"; +"type.highway.construction.secondary_link" = "Пут у изградњи"; +"type.highway.construction.tertiary" = "Пут у изградњи"; +"type.highway.construction.tertiary_link" = "Пут у изградњи"; +"type.highway.construction.residential" = "Пут у изградњи"; +"type.highway.construction.unclassified" = "Пут у изградњи"; +"type.highway.construction.service" = "Пут у изградњи"; +"type.highway.construction.living_street" = "Пут у изградњи"; +"type.highway.construction.road" = "Пут у изградњи"; +"type.highway.construction.track" = "Пут у изградњи"; "type.highway.cycleway" = "Бициклистичка стаза"; /* These translations are used for all type.highway.*.bridge. */ "type.highway.cycleway.bridge" = "Мост"; diff --git a/iphone/Maps/LocalizedStrings/sv.lproj/LocalizableTypes.strings b/iphone/Maps/LocalizedStrings/sv.lproj/LocalizableTypes.strings index fb7710792..9940f4cad 100644 --- a/iphone/Maps/LocalizedStrings/sv.lproj/LocalizableTypes.strings +++ b/iphone/Maps/LocalizedStrings/sv.lproj/LocalizableTypes.strings @@ -452,6 +452,22 @@ "type.highway.busway.tunnel" = "Tunnel"; "type.highway.bus_stop" = "Busshållplats"; "type.highway.construction" = "Väg under uppförande"; +"type.highway.construction.motorway" = "Väg under uppförande"; +"type.highway.construction.motorway_link" = "Väg under uppförande"; +"type.highway.construction.trunk" = "Väg under uppförande"; +"type.highway.construction.trunk_link" = "Väg under uppförande"; +"type.highway.construction.primary" = "Väg under uppförande"; +"type.highway.construction.primary_link" = "Väg under uppförande"; +"type.highway.construction.secondary" = "Väg under uppförande"; +"type.highway.construction.secondary_link" = "Väg under uppförande"; +"type.highway.construction.tertiary" = "Väg under uppförande"; +"type.highway.construction.tertiary_link" = "Väg under uppförande"; +"type.highway.construction.residential" = "Väg under uppförande"; +"type.highway.construction.unclassified" = "Väg under uppförande"; +"type.highway.construction.service" = "Väg under uppförande"; +"type.highway.construction.living_street" = "Väg under uppförande"; +"type.highway.construction.road" = "Väg under uppförande"; +"type.highway.construction.track" = "Väg under uppförande"; "type.highway.cycleway" = "Cykelbana"; /* These translations are used for all type.highway.*.bridge. */ "type.highway.cycleway.bridge" = "Cykelbro"; diff --git a/iphone/Maps/LocalizedStrings/sw.lproj/LocalizableTypes.strings b/iphone/Maps/LocalizedStrings/sw.lproj/LocalizableTypes.strings index 2e55cf16e..370cab5b4 100644 --- a/iphone/Maps/LocalizedStrings/sw.lproj/LocalizableTypes.strings +++ b/iphone/Maps/LocalizedStrings/sw.lproj/LocalizableTypes.strings @@ -452,6 +452,22 @@ "type.highway.busway.tunnel" = "Mtaro"; "type.highway.bus_stop" = "Bus Stop"; "type.highway.construction" = "Barabara inatengenezwa"; +"type.highway.construction.motorway" = "Barabara inatengenezwa"; +"type.highway.construction.motorway_link" = "Barabara inatengenezwa"; +"type.highway.construction.trunk" = "Barabara inatengenezwa"; +"type.highway.construction.trunk_link" = "Barabara inatengenezwa"; +"type.highway.construction.primary" = "Barabara inatengenezwa"; +"type.highway.construction.primary_link" = "Barabara inatengenezwa"; +"type.highway.construction.secondary" = "Barabara inatengenezwa"; +"type.highway.construction.secondary_link" = "Barabara inatengenezwa"; +"type.highway.construction.tertiary" = "Barabara inatengenezwa"; +"type.highway.construction.tertiary_link" = "Barabara inatengenezwa"; +"type.highway.construction.residential" = "Barabara inatengenezwa"; +"type.highway.construction.unclassified" = "Barabara inatengenezwa"; +"type.highway.construction.service" = "Barabara inatengenezwa"; +"type.highway.construction.living_street" = "Barabara inatengenezwa"; +"type.highway.construction.road" = "Barabara inatengenezwa"; +"type.highway.construction.track" = "Barabara inatengenezwa"; "type.highway.cycleway" = "Cycle Path"; /* These translations are used for all type.highway.*.bridge. */ "type.highway.cycleway.bridge" = "Daraja"; diff --git a/iphone/Maps/LocalizedStrings/ta.lproj/LocalizableTypes.strings b/iphone/Maps/LocalizedStrings/ta.lproj/LocalizableTypes.strings index afabc5b62..b79562586 100644 --- a/iphone/Maps/LocalizedStrings/ta.lproj/LocalizableTypes.strings +++ b/iphone/Maps/LocalizedStrings/ta.lproj/LocalizableTypes.strings @@ -452,6 +452,22 @@ "type.highway.busway.tunnel" = "சுரங்கப்பாதை"; "type.highway.bus_stop" = "பேருந்து நிறுத்தம்"; "type.highway.construction" = "சாலை கட்டுமானத்தில் உள்ளது"; +"type.highway.construction.motorway" = "சாலை கட்டுமானத்தில் உள்ளது"; +"type.highway.construction.motorway_link" = "சாலை கட்டுமானத்தில் உள்ளது"; +"type.highway.construction.trunk" = "சாலை கட்டுமானத்தில் உள்ளது"; +"type.highway.construction.trunk_link" = "சாலை கட்டுமானத்தில் உள்ளது"; +"type.highway.construction.primary" = "சாலை கட்டுமானத்தில் உள்ளது"; +"type.highway.construction.primary_link" = "சாலை கட்டுமானத்தில் உள்ளது"; +"type.highway.construction.secondary" = "சாலை கட்டுமானத்தில் உள்ளது"; +"type.highway.construction.secondary_link" = "சாலை கட்டுமானத்தில் உள்ளது"; +"type.highway.construction.tertiary" = "சாலை கட்டுமானத்தில் உள்ளது"; +"type.highway.construction.tertiary_link" = "சாலை கட்டுமானத்தில் உள்ளது"; +"type.highway.construction.residential" = "சாலை கட்டுமானத்தில் உள்ளது"; +"type.highway.construction.unclassified" = "சாலை கட்டுமானத்தில் உள்ளது"; +"type.highway.construction.service" = "சாலை கட்டுமானத்தில் உள்ளது"; +"type.highway.construction.living_street" = "சாலை கட்டுமானத்தில் உள்ளது"; +"type.highway.construction.road" = "சாலை கட்டுமானத்தில் உள்ளது"; +"type.highway.construction.track" = "சாலை கட்டுமானத்தில் உள்ளது"; "type.highway.cycleway" = "சைக்கிள் பாதை"; /* These translations are used for all type.highway.*.bridge. */ "type.highway.cycleway.bridge" = "பாலம்"; diff --git a/iphone/Maps/LocalizedStrings/th.lproj/LocalizableTypes.strings b/iphone/Maps/LocalizedStrings/th.lproj/LocalizableTypes.strings index 539dee496..44a861866 100644 --- a/iphone/Maps/LocalizedStrings/th.lproj/LocalizableTypes.strings +++ b/iphone/Maps/LocalizedStrings/th.lproj/LocalizableTypes.strings @@ -452,6 +452,22 @@ "type.highway.busway.tunnel" = "อุโมงค์"; "type.highway.bus_stop" = "ป้ายรถเมล์"; "type.highway.construction" = "ทางกำลังอยู่ในการก่อสร้าง"; +"type.highway.construction.motorway" = "ทางกำลังอยู่ในการก่อสร้าง"; +"type.highway.construction.motorway_link" = "ทางกำลังอยู่ในการก่อสร้าง"; +"type.highway.construction.trunk" = "ทางกำลังอยู่ในการก่อสร้าง"; +"type.highway.construction.trunk_link" = "ทางกำลังอยู่ในการก่อสร้าง"; +"type.highway.construction.primary" = "ทางกำลังอยู่ในการก่อสร้าง"; +"type.highway.construction.primary_link" = "ทางกำลังอยู่ในการก่อสร้าง"; +"type.highway.construction.secondary" = "ทางกำลังอยู่ในการก่อสร้าง"; +"type.highway.construction.secondary_link" = "ทางกำลังอยู่ในการก่อสร้าง"; +"type.highway.construction.tertiary" = "ทางกำลังอยู่ในการก่อสร้าง"; +"type.highway.construction.tertiary_link" = "ทางกำลังอยู่ในการก่อสร้าง"; +"type.highway.construction.residential" = "ทางกำลังอยู่ในการก่อสร้าง"; +"type.highway.construction.unclassified" = "ทางกำลังอยู่ในการก่อสร้าง"; +"type.highway.construction.service" = "ทางกำลังอยู่ในการก่อสร้าง"; +"type.highway.construction.living_street" = "ทางกำลังอยู่ในการก่อสร้าง"; +"type.highway.construction.road" = "ทางกำลังอยู่ในการก่อสร้าง"; +"type.highway.construction.track" = "ทางกำลังอยู่ในการก่อสร้าง"; "type.highway.cycleway" = "Cycle Path"; /* These translations are used for all type.highway.*.bridge. */ "type.highway.cycleway.bridge" = "สะพาน"; diff --git a/iphone/Maps/LocalizedStrings/tr.lproj/LocalizableTypes.strings b/iphone/Maps/LocalizedStrings/tr.lproj/LocalizableTypes.strings index 64c184d8c..aa4118850 100644 --- a/iphone/Maps/LocalizedStrings/tr.lproj/LocalizableTypes.strings +++ b/iphone/Maps/LocalizedStrings/tr.lproj/LocalizableTypes.strings @@ -452,6 +452,22 @@ "type.highway.busway.tunnel" = "Tünel"; "type.highway.bus_stop" = "Otobüs Durağı"; "type.highway.construction" = "Yapım Aşamasında Yol"; +"type.highway.construction.motorway" = "Yapım Aşamasında Yol"; +"type.highway.construction.motorway_link" = "Yapım Aşamasında Yol"; +"type.highway.construction.trunk" = "Yapım Aşamasında Yol"; +"type.highway.construction.trunk_link" = "Yapım Aşamasında Yol"; +"type.highway.construction.primary" = "Yapım Aşamasında Yol"; +"type.highway.construction.primary_link" = "Yapım Aşamasında Yol"; +"type.highway.construction.secondary" = "Yapım Aşamasında Yol"; +"type.highway.construction.secondary_link" = "Yapım Aşamasında Yol"; +"type.highway.construction.tertiary" = "Yapım Aşamasında Yol"; +"type.highway.construction.tertiary_link" = "Yapım Aşamasında Yol"; +"type.highway.construction.residential" = "Yapım Aşamasında Yol"; +"type.highway.construction.unclassified" = "Yapım Aşamasında Yol"; +"type.highway.construction.service" = "Yapım Aşamasında Yol"; +"type.highway.construction.living_street" = "Yapım Aşamasında Yol"; +"type.highway.construction.road" = "Yapım Aşamasında Yol"; +"type.highway.construction.track" = "Yapım Aşamasında Yol"; "type.highway.cycleway" = "Bisiklet Yolu"; /* These translations are used for all type.highway.*.bridge. */ "type.highway.cycleway.bridge" = "Köprü"; diff --git a/iphone/Maps/LocalizedStrings/uk.lproj/LocalizableTypes.strings b/iphone/Maps/LocalizedStrings/uk.lproj/LocalizableTypes.strings index f77c8e226..cf619f2d2 100644 --- a/iphone/Maps/LocalizedStrings/uk.lproj/LocalizableTypes.strings +++ b/iphone/Maps/LocalizedStrings/uk.lproj/LocalizableTypes.strings @@ -452,6 +452,22 @@ "type.highway.busway.tunnel" = "Тунель"; "type.highway.bus_stop" = "Зупинка"; "type.highway.construction" = "Дорога, що будується"; +"type.highway.construction.motorway" = "Дорога, що будується"; +"type.highway.construction.motorway_link" = "Дорога, що будується"; +"type.highway.construction.trunk" = "Дорога, що будується"; +"type.highway.construction.trunk_link" = "Дорога, що будується"; +"type.highway.construction.primary" = "Дорога, що будується"; +"type.highway.construction.primary_link" = "Дорога, що будується"; +"type.highway.construction.secondary" = "Дорога, що будується"; +"type.highway.construction.secondary_link" = "Дорога, що будується"; +"type.highway.construction.tertiary" = "Дорога, що будується"; +"type.highway.construction.tertiary_link" = "Дорога, що будується"; +"type.highway.construction.residential" = "Дорога, що будується"; +"type.highway.construction.unclassified" = "Дорога, що будується"; +"type.highway.construction.service" = "Дорога, що будується"; +"type.highway.construction.living_street" = "Дорога, що будується"; +"type.highway.construction.road" = "Дорога, що будується"; +"type.highway.construction.track" = "Дорога, що будується"; "type.highway.cycleway" = "Велодоріжка"; /* These translations are used for all type.highway.*.bridge. */ "type.highway.cycleway.bridge" = "Міст"; diff --git a/iphone/Maps/LocalizedStrings/vi.lproj/LocalizableTypes.strings b/iphone/Maps/LocalizedStrings/vi.lproj/LocalizableTypes.strings index d0812b31a..e893f6a66 100644 --- a/iphone/Maps/LocalizedStrings/vi.lproj/LocalizableTypes.strings +++ b/iphone/Maps/LocalizedStrings/vi.lproj/LocalizableTypes.strings @@ -452,6 +452,22 @@ "type.highway.busway.tunnel" = "Đường hầm"; "type.highway.bus_stop" = "Bến xe buýt"; "type.highway.construction" = "Đường đang thi công"; +"type.highway.construction.motorway" = "Đường đang thi công"; +"type.highway.construction.motorway_link" = "Đường đang thi công"; +"type.highway.construction.trunk" = "Đường đang thi công"; +"type.highway.construction.trunk_link" = "Đường đang thi công"; +"type.highway.construction.primary" = "Đường đang thi công"; +"type.highway.construction.primary_link" = "Đường đang thi công"; +"type.highway.construction.secondary" = "Đường đang thi công"; +"type.highway.construction.secondary_link" = "Đường đang thi công"; +"type.highway.construction.tertiary" = "Đường đang thi công"; +"type.highway.construction.tertiary_link" = "Đường đang thi công"; +"type.highway.construction.residential" = "Đường đang thi công"; +"type.highway.construction.unclassified" = "Đường đang thi công"; +"type.highway.construction.service" = "Đường đang thi công"; +"type.highway.construction.living_street" = "Đường đang thi công"; +"type.highway.construction.road" = "Đường đang thi công"; +"type.highway.construction.track" = "Đường đang thi công"; "type.highway.cycleway" = "Cycle Path"; /* These translations are used for all type.highway.*.bridge. */ "type.highway.cycleway.bridge" = "Cầu"; diff --git a/iphone/Maps/LocalizedStrings/zh-Hans.lproj/LocalizableTypes.strings b/iphone/Maps/LocalizedStrings/zh-Hans.lproj/LocalizableTypes.strings index 04034ad3b..6c048a662 100644 --- a/iphone/Maps/LocalizedStrings/zh-Hans.lproj/LocalizableTypes.strings +++ b/iphone/Maps/LocalizedStrings/zh-Hans.lproj/LocalizableTypes.strings @@ -452,6 +452,22 @@ "type.highway.busway.tunnel" = "隧道"; "type.highway.bus_stop" = "公交站"; "type.highway.construction" = "在建道路"; +"type.highway.construction.motorway" = "在建道路"; +"type.highway.construction.motorway_link" = "在建道路"; +"type.highway.construction.trunk" = "在建道路"; +"type.highway.construction.trunk_link" = "在建道路"; +"type.highway.construction.primary" = "在建道路"; +"type.highway.construction.primary_link" = "在建道路"; +"type.highway.construction.secondary" = "在建道路"; +"type.highway.construction.secondary_link" = "在建道路"; +"type.highway.construction.tertiary" = "在建道路"; +"type.highway.construction.tertiary_link" = "在建道路"; +"type.highway.construction.residential" = "在建道路"; +"type.highway.construction.unclassified" = "在建道路"; +"type.highway.construction.service" = "在建道路"; +"type.highway.construction.living_street" = "在建道路"; +"type.highway.construction.road" = "在建道路"; +"type.highway.construction.track" = "在建道路"; "type.highway.cycleway" = "自行车道"; /* These translations are used for all type.highway.*.bridge. */ "type.highway.cycleway.bridge" = "桥"; diff --git a/iphone/Maps/LocalizedStrings/zh-Hant.lproj/LocalizableTypes.strings b/iphone/Maps/LocalizedStrings/zh-Hant.lproj/LocalizableTypes.strings index 17f755638..a43aec4ba 100644 --- a/iphone/Maps/LocalizedStrings/zh-Hant.lproj/LocalizableTypes.strings +++ b/iphone/Maps/LocalizedStrings/zh-Hant.lproj/LocalizableTypes.strings @@ -452,6 +452,22 @@ "type.highway.busway.tunnel" = "隧道"; "type.highway.bus_stop" = "公車站"; "type.highway.construction" = "施工中道路"; +"type.highway.construction.motorway" = "施工中道路"; +"type.highway.construction.motorway_link" = "施工中道路"; +"type.highway.construction.trunk" = "施工中道路"; +"type.highway.construction.trunk_link" = "施工中道路"; +"type.highway.construction.primary" = "施工中道路"; +"type.highway.construction.primary_link" = "施工中道路"; +"type.highway.construction.secondary" = "施工中道路"; +"type.highway.construction.secondary_link" = "施工中道路"; +"type.highway.construction.tertiary" = "施工中道路"; +"type.highway.construction.tertiary_link" = "施工中道路"; +"type.highway.construction.residential" = "施工中道路"; +"type.highway.construction.unclassified" = "施工中道路"; +"type.highway.construction.service" = "施工中道路"; +"type.highway.construction.living_street" = "施工中道路"; +"type.highway.construction.road" = "施工中道路"; +"type.highway.construction.track" = "施工中道路"; "type.highway.cycleway" = "單車道"; /* These translations are used for all type.highway.*.bridge. */ "type.highway.cycleway.bridge" = "橋樑"; diff --git a/iphone/Maps/Maps.xcodeproj/project.pbxproj b/iphone/Maps/Maps.xcodeproj/project.pbxproj index ebd4c6072..b0c2bd429 100644 --- a/iphone/Maps/Maps.xcodeproj/project.pbxproj +++ b/iphone/Maps/Maps.xcodeproj/project.pbxproj @@ -22,6 +22,7 @@ 272F1F3D2E0EE0C800FA52EF /* ProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 272F1F3C2E0EE0C400FA52EF /* ProfileView.swift */; }; 272F1F462E0EEF9400FA52EF /* SafariView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 272F1F452E0EEF8B00FA52EF /* SafariView.swift */; }; 2744A9912F23D86D00E7D02C /* AlternativeMapLanguageHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2744A98E2F23D86B00E7D02C /* AlternativeMapLanguageHandling.swift */; }; + 2747205A2E439FBA00C516DF /* libtraffxml.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 274720592E439FBA00C516DF /* libtraffxml.a */; }; 2752B6CA2E31197500887CC4 /* MapLanguage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2752B6C92E31197000887CC4 /* MapLanguage.swift */; }; 2752B6CE2E3121D900887CC4 /* Language.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2752B6CD2E3121D800887CC4 /* Language.swift */; }; 2765D1D02E13F9C20005CA2B /* BridgeControllers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2765D1CD2E13F9BC0005CA2B /* BridgeControllers.swift */; }; @@ -776,6 +777,7 @@ 272F1F3C2E0EE0C400FA52EF /* ProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileView.swift; sourceTree = ""; }; 272F1F452E0EEF8B00FA52EF /* SafariView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariView.swift; sourceTree = ""; }; 2744A98E2F23D86B00E7D02C /* AlternativeMapLanguageHandling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlternativeMapLanguageHandling.swift; sourceTree = ""; }; + 274720592E439FBA00C516DF /* libtraffxml.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = libtraffxml.a; sourceTree = BUILT_PRODUCTS_DIR; }; 2752B6C92E31197000887CC4 /* MapLanguage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapLanguage.swift; sourceTree = ""; }; 2752B6CD2E3121D800887CC4 /* Language.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Language.swift; sourceTree = ""; }; 2765D1CD2E13F9BC0005CA2B /* BridgeControllers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BridgeControllers.swift; sourceTree = ""; }; @@ -1802,6 +1804,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 2747205A2E439FBA00C516DF /* libtraffxml.a in Frameworks */, FAF9DDA32A86DC54000D7037 /* libharfbuzz.a in Frameworks */, FA456C3C26BDC6AD00B83C20 /* Chart.framework in Frameworks */, FA853BF326BC5DE50026D455 /* libshaders.a in Frameworks */, @@ -2041,6 +2044,7 @@ 29B97323FDCFA39411CA2CEA /* Frameworks */ = { isa = PBXGroup; children = ( + 274720592E439FBA00C516DF /* libtraffxml.a */, FAF9DDA22A86DC54000D7037 /* libharfbuzz.a */, FA456C3B26BDC6AD00B83C20 /* Chart.framework */, FA853BF226BC5DE50026D455 /* libshaders.a */, diff --git a/iphone/Maps/Model/Settings.swift b/iphone/Maps/Model/Settings.swift index b5207571a..c6b4a52d0 100644 --- a/iphone/Maps/Model/Settings.swift +++ b/iphone/Maps/Model/Settings.swift @@ -448,6 +448,28 @@ import Combine } + /// If live traffic data should be used + @objc static var hasLiveTraffic: Bool { + get { + return SettingsBridge.liveTrafficEnabled() + } + set { + SettingsBridge.setLiveTrafficEnabled(newValue) + } + } + + + /// The url of the live traffic data server + @objc static var liveTrafficServerUrl: URL? { + get { + return SettingsBridge.liveTrafficUrl() + } + set { + SettingsBridge.setLiveTrafficUrl(newValue) + } + } + + // MARK: Methods diff --git a/iphone/Maps/UI/BottomMenu/Menu/Cells/BottomMenuLayersCell.swift b/iphone/Maps/UI/BottomMenu/Menu/Cells/BottomMenuLayersCell.swift index 704445eda..460b228a6 100644 --- a/iphone/Maps/UI/BottomMenu/Menu/Cells/BottomMenuLayersCell.swift +++ b/iphone/Maps/UI/BottomMenu/Menu/Cells/BottomMenuLayersCell.swift @@ -18,6 +18,11 @@ class BottomMenuLayersCell: UITableViewCell { updateOutdoorButton() } } + @IBOutlet private var trafficButton: BottomMenuLayerButton! { + didSet { + updateTrafficButton() + } + } var onClose: (()->())? @@ -32,6 +37,7 @@ class BottomMenuLayersCell: UITableViewCell { outdoorButton.setupWith(image: UIImage(resource: .btnMenuOutdoors), text: L("button_layer_outdoor")) isoLinesButton.setupWith(image: UIImage(resource: .btnMenuIsomaps), text: L("button_layer_isolines")) subwayButton.setupWith(image: UIImage(resource: .btnMenuSubway), text: L("button_layer_subway")) + trafficButton.setupWith(image: UIImage(resource: .btnMenuTraffic), text: L("button_layer_traffic")) } deinit { @@ -56,6 +62,11 @@ class BottomMenuLayersCell: UITableViewCell { let enabled = MapOverlayManager.outdoorEnabled() outdoorButton.setStyleAndApply(styleFor(enabled)) } + + private func updateTrafficButton() { + let enabled = MapOverlayManager.trafficEnabled() + trafficButton.setStyleAndApply(styleFor(enabled)) + } @IBAction func onCloseButtonPressed(_ sender: Any) { onClose?() @@ -75,6 +86,11 @@ class BottomMenuLayersCell: UITableViewCell { let enable = !MapOverlayManager.outdoorEnabled() MapOverlayManager.setOutdoorEnabled(enable) } + + @IBAction func onTrafficButton(_ sender: Any) { + let enable = !MapOverlayManager.trafficEnabled() + MapOverlayManager.setTrafficEnabled(enable) + } } extension BottomMenuLayersCell: MapOverlayManagerObserver { @@ -89,6 +105,10 @@ extension BottomMenuLayersCell: MapOverlayManagerObserver { func onOutdoorStateUpdated() { updateOutdoorButton() } + + func onTrafficStateUpdated() { + updateTrafficButton() + } } private extension BottomMenuLayersCell { diff --git a/iphone/Maps/UI/BottomMenu/Menu/Cells/BottomMenuLayersCell.xib b/iphone/Maps/UI/BottomMenu/Menu/Cells/BottomMenuLayersCell.xib index 2261721f3..cc71a35e5 100644 --- a/iphone/Maps/UI/BottomMenu/Menu/Cells/BottomMenuLayersCell.xib +++ b/iphone/Maps/UI/BottomMenu/Menu/Cells/BottomMenuLayersCell.xib @@ -1,9 +1,9 @@ - + - + @@ -81,26 +81,35 @@ - + - + - + + + + + + + + + + @@ -144,6 +153,7 @@ + diff --git a/iphone/Maps/UI/Settings/SettingsNavigationView.swift b/iphone/Maps/UI/Settings/SettingsNavigationView.swift index 8debbd132..8e7d96291 100644 --- a/iphone/Maps/UI/Settings/SettingsNavigationView.swift +++ b/iphone/Maps/UI/Settings/SettingsNavigationView.swift @@ -60,6 +60,14 @@ struct SettingsNavigationView: View { @State var forceRefreshDate: Date = Date.now + /// If live traffic data should be used + @State var hasLiveTraffic: Bool = false + + + /// The url of the live traffic data server + @State var liveTrafficServerUrlString: String = "" + + /// The actual view var body: some View { List { @@ -222,6 +230,24 @@ struct SettingsNavigationView: View { Text("driving_options_title") } } + + Section { + Toggle(isOn: $hasLiveTraffic) { + VStack(alignment: .leading) { + Text("traffic_http_enabled") + + Text("traffic_http_enabled_description") + .font(.footnote) + .foregroundStyle(.secondary) + } + } + .tint(.accent) + + TextField("traffic_http_url", text: $liveTrafficServerUrlString, prompt: Text("traffic_http_url_not_set")) + .tint(.accent) + } header: { + Text("traffic_http") + } } .accentColor(.accent) .navigationViewStyle(StackNavigationViewStyle()) @@ -239,6 +265,8 @@ struct SettingsNavigationView: View { shouldAvoidFerriesWhileRouting = Settings.shouldAvoidFerriesWhileRouting shouldAvoidMotorwaysWhileRouting = Settings.shouldAvoidMotorwaysWhileRouting shouldAvoidStepsWhileRouting = Settings.shouldAvoidStepsWhileRouting + hasLiveTraffic = Settings.hasLiveTraffic + liveTrafficServerUrlString = Settings.liveTrafficServerUrl?.absoluteString ?? "" } .onChange(of: scenePhase) { _ in forceRefreshDate = Date.now @@ -286,6 +314,15 @@ struct SettingsNavigationView: View { } .onChange(of: shouldAvoidStepsWhileRouting) { changedShouldAvoidStepsWhileRouting in Settings.shouldAvoidStepsWhileRouting = changedShouldAvoidStepsWhileRouting + .onChange(of: hasLiveTraffic) { changedHasLiveTraffic in + Settings.hasLiveTraffic = changedHasLiveTraffic + } + .onChange(of: liveTrafficServerUrlString) { changedLiveTrafficServerUrlString in + if !changedLiveTrafficServerUrlString.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty, let changedLiveTrafficServerUrl = URL(string: changedLiveTrafficServerUrlString.trimmingCharacters(in: .whitespacesAndNewlines)) { + Settings.liveTrafficServerUrl = changedLiveTrafficServerUrl + } else { + Settings.liveTrafficServerUrl = nil + } } } } diff --git a/libs/CMakeLists.txt b/libs/CMakeLists.txt index 00a05fa9d..fd648a805 100644 --- a/libs/CMakeLists.txt +++ b/libs/CMakeLists.txt @@ -19,4 +19,5 @@ add_subdirectory(shaders) add_subdirectory(storage) add_subdirectory(tracking) add_subdirectory(traffic) +add_subdirectory(traffxml) add_subdirectory(transit) diff --git a/libs/drape_frontend/drape_api.hpp b/libs/drape_frontend/drape_api.hpp index 2d7497012..d9cf95bfa 100644 --- a/libs/drape_frontend/drape_api.hpp +++ b/libs/drape_frontend/drape_api.hpp @@ -15,6 +15,9 @@ namespace df { +/** + * @brief Geometry and style for a line rendered with Drape. + */ struct DrapeApiLineData { DrapeApiLineData() = default; @@ -22,6 +25,14 @@ struct DrapeApiLineData DrapeApiLineData(std::vector const & points, dp::Color const & color) : m_points(points), m_color(color) {} + /** + * @brief Enables showing the points of the line. + * + * Calling this method will set the internal showPoints attribute to true (causing the points of + * the line to be shown as dots) and the markPoints attribite to the value supplied. + * + * @param markPoints Whether to mark the points. + */ DrapeApiLineData & ShowPoints(bool markPoints) { m_showPoints = true; @@ -29,6 +40,9 @@ struct DrapeApiLineData return *this; } + /** + * @brief Sets the width for the line in pixels. + */ DrapeApiLineData & Width(float width) { m_width = width; @@ -59,8 +73,25 @@ class DrapeApi void SetDrapeEngine(ref_ptr engine); + /** + * @brief Adds a new line. + * + * @param id A unique identifier for the line. If a line with the same identifier exists, it will + * be replaced by the new line. + * @param data The data (geometry and style) for the new line. + */ void AddLine(std::string const & id, DrapeApiLineData const & data); + + /** + * @brief Removes a line. + * + * @param id The id of the line to remove. + */ void RemoveLine(std::string const & id); + + /** + * @brief Removes all lines added with AddLine. + */ void Clear(); void Invalidate(); diff --git a/libs/drape_frontend/rule_drawer.cpp b/libs/drape_frontend/rule_drawer.cpp index 03ab5b6ed..fa0174c00 100644 --- a/libs/drape_frontend/rule_drawer.cpp +++ b/libs/drape_frontend/rule_drawer.cpp @@ -38,9 +38,9 @@ namespace { // The first zoom level in kAverageSegmentsCount. int constexpr kFirstZoomInAverageSegments = 10; -std::array constexpr kAverageSegmentsCount = { - // 10 11 12 13 14 15 16 17 18 19 - 10000, 5000, 10000, 5000, 2500, 5000, 2000, 1000, 500, 500}; +std::array constexpr kAverageSegmentsCount = { + // 10 11 12 13 14 15 16 17 18 19 20 + 10000, 5000, 10000, 5000, 2500, 5000, 2000, 1000, 500, 500, 500}; double constexpr kMetersPerLevel = 3.0; diff --git a/libs/geometry/parametrized_segment.hpp b/libs/geometry/parametrized_segment.hpp index e71af2e28..8d5a79669 100644 --- a/libs/geometry/parametrized_segment.hpp +++ b/libs/geometry/parametrized_segment.hpp @@ -1,5 +1,6 @@ #pragma once +#include "geometry/mercator.hpp" #include "geometry/point2d.hpp" #include "base/math.hpp" @@ -9,17 +10,20 @@ namespace m2 { -// This class holds a parametrization of the -// line segment between two points p0 and p1. -// The parametrization is of the form -// p(t) = p0 + t * dir. -// Other conditions: -// dir is the normalized (p1 - p0) vector. -// length(dir) = 1. -// p(0) = p0. -// p(T) = p1 with T = length(p1 - p0). -// -// The points with t in [0, T] are the points of the segment. +/** + * @brief This class holds a parametrization of the line segment between two points `p0` and `p1`. + * + * The parametrization is of the form + * `p(t) = p0 + t * dir`. + * + * Other conditions: + * * `dir` is the normalized `(p1 - p0)` vector. + * * `length(dir) = 1`. + * * `p(0) = p0`. + * * `p(T) = p1` with `T = length(p1 - p0)`. + * + * The points with `t` in `[0, T]` are the points of the segment. + */ template class ParametrizedSegment { @@ -36,7 +40,9 @@ class ParametrizedSegment m_d = m_d / m_length; } - // Returns the squared (euclidean) distance from the segment to |p|. + /** + * @brief Returns the squared (euclidean) distance from the segment to `p`. + */ double SquaredDistanceToPoint(Point const & p) const { m2::PointD const diff(p - m_p0); @@ -52,9 +58,22 @@ class ParametrizedSegment return math::Pow2(CrossProduct(diff, m_d)); } - // Returns the point of the segment that is closest to |p|. - m2::PointD ClosestPointTo(Point const & p) const + /** + * @brief Returns the point of the segment that is closest to `p`. + * + * @param p The checkpoint + * @param snapToEnds If true, the result is the endpoint of the segment which is closest to `p` + */ + m2::PointD ClosestPointTo(Point const & p, bool snapToEnds = false) const { + if (snapToEnds) + { + if (mercator::DistanceOnEarth(p, m_p0) < mercator::DistanceOnEarth(p, m_p1)) + return m_p0; + else + return m_p1; + } + m2::PointD const diff(p - m_p0); double const t = DotProduct(m_d, diff); diff --git a/libs/indexer/classificator.hpp b/libs/indexer/classificator.hpp index 152b87e2d..4a0420d6a 100644 --- a/libs/indexer/classificator.hpp +++ b/libs/indexer/classificator.hpp @@ -26,12 +26,37 @@ void PushValue(uint32_t & type, uint8_t value); /// @pre level < GetLevel(type). uint8_t GetValue(uint32_t type, uint8_t level); void PopValue(uint32_t & type); + +/** + * @brief Truncates a type to the number of levels specified. + * + * For example, truncating `natural-beach-sand` to 2 levels would result in `natural-beach`. + * + * Truncation is done in place, i.e. `type` is altered. + * + * @param type The type to truncate + * @param level The number of levels to truncate to + */ void TruncValue(uint32_t & type, uint8_t level); + +/** + * @brief Truncates a type to the number of levels specified, and returns the result. + * + * For example, truncating `natural-beach-sand` to 2 levels would result in `natural-beach`. + * + * This is a convenience wrapper around `TruncValue()`, which will not alter `type`. + * + * @param type The type to truncate + * @param level The number of levels to truncate to + * + * @return The truncated type + */ inline uint32_t Trunc(uint32_t type, uint8_t level) { TruncValue(type, level); return type; } + uint8_t GetLevel(uint32_t type); } // namespace ftype diff --git a/libs/indexer/data_source.hpp b/libs/indexer/data_source.hpp index fb9f4e680..efcc51ec0 100644 --- a/libs/indexer/data_source.hpp +++ b/libs/indexer/data_source.hpp @@ -19,20 +19,31 @@ class DataSource : public MwmSet /// Registers a new map. std::pair RegisterMap(platform::LocalCountryFile const & localFile); - /// Deregisters a map from internal records. - /// - /// \param countryFile A countryFile denoting a map to be deregistered. - /// \return True if the map was successfully deregistered. If map is locked - /// now, returns false. + /** + * @brief Deregisters a map from internal records. + * @param countryFile A `CountryFile` denoting a map to be deregistered. + * @return True if the map was successfully deregistered, false if the map is locked now. + */ bool DeregisterMap(platform::CountryFile const & countryFile); void ForEachFeatureIDInRect(FeatureIdCallback const & f, m2::RectD const & rect, int scale, covering::CoveringMode mode = covering::ViewportWithLowLevels) const; void ForEachInRect(FeatureCallback const & f, m2::RectD const & rect, int scale) const; - // Calls |f| for features closest to |center| until |stopCallback| returns true or distance - // |sizeM| from has been reached. Then for EditableDataSource calls |f| for each edited feature - // inside square with center |center| and side |2 * sizeM|. Edited features are not in the same - // hierarchy and there is no fast way to merge frozen and edited features. + + /** + * @brief Iterates over features within a given distance of a center point. + * + * Calls `f` for features closest to `center` until `stopCallback` returns true or distance + * `sizeM` from has been reached. Then for EditableDataSource calls `f` for each edited feature + * inside square with center `center` and side `2 * sizeM`. Edited features are not in the same + * hierarchy and there is no fast way to merge frozen and edited features. + * + * @brief f Callback function that is called on each feature. + * @brief stopCallback Callback function which decides whether to continue searching or stop. + * @brief center The center of the search area. + * @brief sizeM The size of the search area, as a distance from the center point. + * @brief scale + */ void ForClosestToPoint(FeatureCallback const & f, StopSearchCallback const & stopCallback, m2::PointD const & center, double sizeM, int scale) const; void ForEachInScale(FeatureCallback const & f, int scale) const; @@ -66,18 +77,24 @@ class DataSource : public MwmSet std::unique_ptr m_factory; }; -// DataSource which operates with features from mwm file and does not support features creation -// deletion or modification. +/** + * @brief A `DataSource` which operates with features from an MWM file and does not support + * creation, deletion or modification of features. + */ class FrozenDataSource : public DataSource { public: FrozenDataSource() : DataSource(std::make_unique()) {} }; -/// Guard for loading features from particular MWM by demand. -/// @note If you need to work with FeatureType from different threads you need to use -/// a unique FeaturesLoaderGuard instance for every thread. -/// For an example of concurrent extracting feature details please see ConcurrentFeatureParsingTest. +/** + * @brief Guard for loading features from particular MWM by demand. + * + * @note If you need to work with `FeatureType` from different threads, you need to use + * a unique `FeaturesLoaderGuard` instance for every thread. + * For an example of concurrent extracting feature details please see `ConcurrentFeatureParsingTest` + * in `routing/routing_integration_tests`. + */ class FeaturesLoaderGuard { public: diff --git a/libs/indexer/feature.hpp b/libs/indexer/feature.hpp index 2f6de8486..3668cf7f3 100644 --- a/libs/indexer/feature.hpp +++ b/libs/indexer/feature.hpp @@ -77,6 +77,18 @@ class FeatureType // (number of points in inner triangle-strips). using PointsBufferT = buffer_vector; + /** + * @brief Retrieves the points of the feature. + * + * Depending on `scale`, the geometry may be simplified by reducing groups of nearby points to + * one point. If `scale` equals `FeatureType::BEST_GEOMETRY`, no such simplification takes place. + * + * Points are cached between calls and `scale` may not be honored if cached points are returned. + * To reliably enforce `scale`, call `ResetGemoetry()` immediately prior to `GetPoints()`. + * + * @param scale The map scale + * @return The points of the feature, simplified according to `scale`. + */ PointsBufferT const & GetPoints(int scale); PointsBufferT const & GetTrianglesAsPoints(int scale); @@ -86,6 +98,13 @@ class FeatureType void ParseHeader2(); void ParseRelations(); void ParseAllBeforeGeometry() { ParseRelations(); } + + /** + * @brief Resets the geometry. + * + * This discards any cached points, resulting in points being re-fetched the next time + * `GetPoints()` or `GetTrianglesAsPoints()` is called. + */ void ResetGeometry(); void ParseGeometry(int scale); void ParseTriangles(int scale); diff --git a/libs/indexer/ftypes_matcher.cpp b/libs/indexer/ftypes_matcher.cpp index 1082d294f..af640ea64 100644 --- a/libs/indexer/ftypes_matcher.cpp +++ b/libs/indexer/ftypes_matcher.cpp @@ -66,7 +66,32 @@ class HighwayClasses m_map[c.GetTypeByPath({"highway", "steps"})] = HighwayClass::Pedestrian; m_map[c.GetTypeByPath({"highway", "cycleway"})] = HighwayClass::Pedestrian; m_map[c.GetTypeByPath({"highway", "path"})] = HighwayClass::Pedestrian; + + // mvglasow: Appears to never have been used m_map[c.GetTypeByPath({"highway", "construction"})] = HighwayClass::Pedestrian; + + // Construction types + m_map[c.GetTypeByPath({"highway", "construction", "motorway"})] = HighwayClass::Motorway; + m_map[c.GetTypeByPath({"highway", "construction", "motorway_link"})] = HighwayClass::Motorway; + m_map[c.GetTypeByPath({"highway", "construction", "trunk"})] = HighwayClass::Trunk; + m_map[c.GetTypeByPath({"highway", "construction", "trunk_link"})] = HighwayClass::Trunk; + + m_map[c.GetTypeByPath({"highway", "construction", "primary"})] = HighwayClass::Primary; + m_map[c.GetTypeByPath({"highway", "primary_link"})] = HighwayClass::Primary; + + m_map[c.GetTypeByPath({"highway", "construction", "secondary"})] = HighwayClass::Secondary; + m_map[c.GetTypeByPath({"highway", "construction", "secondary_link"})] = HighwayClass::Secondary; + + m_map[c.GetTypeByPath({"highway", "construction", "tertiary"})] = HighwayClass::Tertiary; + m_map[c.GetTypeByPath({"highway", "construction", "tertiary_link"})] = HighwayClass::Tertiary; + + m_map[c.GetTypeByPath({"highway", "construction", "unclassified"})] = HighwayClass::LivingStreet; + m_map[c.GetTypeByPath({"highway", "construction", "residential"})] = HighwayClass::LivingStreet; + m_map[c.GetTypeByPath({"highway", "construction", "living_street"})] = HighwayClass::LivingStreet; + m_map[c.GetTypeByPath({"highway", "construction", "road"})] = HighwayClass::LivingStreet; + + m_map[c.GetTypeByPath({"highway", "construction", "service"})] = HighwayClass::Service; + m_map[c.GetTypeByPath({"highway", "construction", "track"})] = HighwayClass::Service; } HighwayClass Get(uint32_t t) const diff --git a/libs/indexer/ftypes_matcher.hpp b/libs/indexer/ftypes_matcher.hpp index c44dea423..281a391be 100644 --- a/libs/indexer/ftypes_matcher.hpp +++ b/libs/indexer/ftypes_matcher.hpp @@ -708,24 +708,46 @@ double GetRadiusByPopulationForRouting(uint64_t p, LocalityType localityType); uint64_t GetPopulationByRadius(double r); //@} -// Highway class. The order is important. -// The enum values follow from the biggest roads (Trunk) to the smallest ones (Service). +/** + * @brief Highway class. + * + * The order is important. The enum values follow from the biggest roads (Trunk) to the smallest ones (Service). + */ enum class HighwayClass { - Undefined = 0, // There has not been any attempt of calculating HighwayClass. + /** + * Used when there has not been any attempt of calculating HighwayClass. + */ + Undefined = 0, Motorway, Trunk, Primary, Secondary, Tertiary, + /** + * Unclassified, residential, living street and `highway=road`. + */ LivingStreet, + /** + * Service, track, busway and `man_made=pier`. + */ Service, // OSM highway=service type is widely used even for _significant_ roads. // Adding a new type to distinguish mapped driveway or parking_aisle. ServiceMinor, + /** + * Anything not intended for motorized traffic: pedestrian, footway, bridleway, steps, cycleway, + * path and also `highway=construction`. + */ Pedestrian, - Transported, // Vehicles are transported by train or ferry. - Count // This value is used for internals only. + /** + * Vehicles are transported by train or ferry. + */ + Transported, + /** + * This value is used for internals only. + */ + Count }; std::string DebugPrint(HighwayClass const cls); diff --git a/libs/indexer/mwm_set.hpp b/libs/indexer/mwm_set.hpp index 6236fe6dc..7e064cc6e 100644 --- a/libs/indexer/mwm_set.hpp +++ b/libs/indexer/mwm_set.hpp @@ -151,8 +151,10 @@ class MwmSet explicit MwmSet(size_t cacheSize = 64) : m_cacheSize(cacheSize) {} virtual ~MwmSet() = default; - // Mwm handle, which is used to refer to mwm and prevent it from - // deletion when its FileContainer is used. + /** + * @brief Mwm handle, which is used to refer to mwm and prevent it from deletion when its + * FileContainer is used. + */ class MwmHandle { public: @@ -237,19 +239,26 @@ class MwmSet BadFile }; - // An Observer interface to MwmSet. Note that these functions can - // be called from *ANY* thread because most signals are sent when - // some thread releases its MwmHandle, so overrides must be as fast - // as possible and non-blocking when it's possible. + /** + * @brief An Observer interface to `MwmSet`. + * + * Note that these functions can be called from *ANY* thread because most signals are sent when + * some thread releases its MwmHandle, so overrides must be as fast as possible and non-blocking + * when it's possible. + */ class Observer { public: virtual ~Observer() = default; - // Called when a map is registered for the first time and can be used. + /** + * @brief Called when a map is registered for the first time and can be used. + */ virtual void OnMapRegistered(platform::LocalCountryFile const & /* localFile */) {} - // Called when a map is deregistered and can no longer be used. + /** + * @brief Called when a map is deregistered and can no longer be used. + */ virtual void OnMapDeregistered(platform::LocalCountryFile const & /* localFile */) {} }; @@ -298,7 +307,14 @@ class MwmSet /// @todo In fact, std::shared_ptr is a MwmId. Seems like better to make vector interface. void GetMwmsInfo(std::vector> & info) const; - // Clears caches and mwm's registry. All known mwms won't be marked as DEREGISTERED. + /** + * @brief Clears caches and mwm's registry. + * + * All known mwms won't be marked as DEREGISTERED. + * + * @todo what does “all won’t be marked” mean? Not all will be marked/some might not be marked? + * Or all will be unmarked? + */ void Clear(); void ClearCache(); @@ -339,10 +355,18 @@ class MwmSet ProcessEventList(events); } - // Sets |status| in |info|, adds corresponding event to |event|. + /** + * @brief Sets `status` in `info`, adds corresponding event to `event`. + * @param info + * @param status + * @param events + */ void SetStatus(MwmInfo & info, MwmInfo::Status status, EventList & events); - // Triggers observers on each event in |events|. + /** + * @brief Triggers observers on each event in `events`. + * @param events + */ void ProcessEventList(EventList & events); /// @precondition This function is always called under mutex m_lock. diff --git a/libs/map/framework.cpp b/libs/map/framework.cpp index 605253b57..e9345a433 100644 --- a/libs/map/framework.cpp +++ b/libs/map/framework.cpp @@ -7,7 +7,9 @@ #include "ge0/url_generator.hpp" +#include "routing/index_router.hpp" #include "routing/route.hpp" +#include "routing/routing_helpers.hpp" #include "routing/speed_camera_prohibition.hpp" #include "routing_common/num_mwm_id.hpp" @@ -17,13 +19,18 @@ #include "search/locality_finder.hpp" #include "storage/country_info_getter.hpp" +#include "storage/routing_helpers.hpp" #include "storage/storage.hpp" #include "storage/storage_helpers.hpp" +#include "traffxml/traff_source.hpp" + #include "drape_frontend/color_constants.hpp" #include "drape_frontend/gps_track_point.hpp" #include "drape_frontend/visual_params.hpp" +#include "editor/editable_data_source.hpp" + #include "descriptions/loader.hpp" #include "indexer/categories_holder.hpp" @@ -39,6 +46,7 @@ #include "indexer/scales.hpp" #include "indexer/transliteration_loader.hpp" +#include "platform/local_country_file_utils.hpp" #include "platform/localization.hpp" #include "platform/measurement_utils.hpp" #include "platform/mwm_version.hpp" @@ -61,11 +69,13 @@ #include "base/logging.hpp" #include "base/math.hpp" +#include "base/stl_helpers.hpp" #include "base/string_utils.hpp" #include "std/target_os.hpp" #include "defines.hpp" +#include "private.h" #include @@ -93,6 +103,8 @@ std::string_view constexpr kAllow3dKey = "Allow3d"; std::string_view constexpr kAllow3dBuildingsKey = "Buildings3d"; std::string_view constexpr kAllowAutoZoom = "AutoZoom"; std::string_view constexpr kTrafficEnabledKey = "TrafficEnabled"; +std::string_view constexpr kTrafficHttpEnabledKey = "TrafficHttpEnabled"; +std::string_view constexpr kTrafficHttpUrlKey = "TrafficHttpUrl"; std::string_view constexpr kTransitSchemeEnabledKey = "TransitSchemeEnabled"; std::string_view constexpr kIsolinesEnabledKey = "IsolinesEnabled"; std::string_view constexpr kOutdoorsEnabledKey = "OutdoorsEnabled"; @@ -288,8 +300,11 @@ Framework::Framework(FrameworkParams const & params, bool loadMaps) [this]() -> StringsBundle const & { return m_stringsBundle; }, [this]() -> power_management::PowerManager const & { return m_powerManager; }), static_cast(*this)) - , m_trafficManager(bind(&Framework::GetMwmsByRect, this, _1, false /* rough */), kMaxTrafficCacheSizeBytes, - m_routingManager.RoutingSession()) + , m_trafficManager(m_featuresFetcher.GetDataSource(), + [this]() -> storage::CountryInfoGetter const & { return GetCountryInfoGetter(); }, + [this](string const & id) -> string { return m_storage.GetParentIdFor(id); }, + bind(&Framework::GetMwmsByRect, this, _1, false /* rough */), + kMaxTrafficCacheSizeBytes, m_routingManager.RoutingSession()) , m_lastReportedCountry(kInvalidCountryId) , m_popularityLoader(m_featuresFetcher.GetDataSource(), POPULARITY_RANKS_FILE_TAG) , m_descriptionsLoader(std::make_unique(m_featuresFetcher.GetDataSource())) @@ -363,10 +378,10 @@ Framework::Framework(FrameworkParams const & params, bool loadMaps) editor.SetDelegate(make_unique(m_featuresFetcher.GetDataSource())); editor.SetInvalidateFn([this]() { InvalidateRect(GetCurrentViewport()); }); - /// @todo Uncomment when we will integrate a traffic provider. - // m_trafficManager.SetCurrentDataVersion(m_storage.GetCurrentDataVersion()); - // m_trafficManager.SetSimplifiedColorScheme(LoadTrafficSimplifiedColors()); - // m_trafficManager.SetEnabled(LoadTrafficEnabled()); + if (params.m_trafficTestMode) + m_trafficManager.SetTestMode(); + m_trafficManager.SetCurrentDataVersion(m_storage.GetCurrentDataVersion()); + m_trafficManager.SetSimplifiedColorScheme(LoadTrafficSimplifiedColors()); m_isolinesManager.SetEnabled(LoadIsolinesEnabled()); @@ -398,6 +413,19 @@ Framework::~Framework() m_featuresFetcher.SetOnMapDeregisteredCallback(nullptr); } +void Framework::InitializeTraffic() +{ + m_trafficManager.SetEnabled(LoadTrafficEnabled()); + if (!m_trafficManager.IsTestMode() && LoadTrafficHttpEnabled()) + // TODO handle invalid URLs + traffxml::HttpTraffSource::Create(m_trafficManager, LoadTrafficHttpUrl()); + + /* + * MockTraffSource for debugging purposes. + */ + //traffxml::MockTraffSource::Create(m_trafficManager); +} + void Framework::ShowNode(storage::CountryId const & countryId) { StopLocationFollow(); @@ -413,14 +441,14 @@ void Framework::OnCountryFileDownloaded(storage::CountryId const &, storage::Loc m2::RectD rect = mercator::Bounds::FullRect(); if (localFile && localFile->OnDisk(MapFileType::Map)) - { - auto const res = RegisterMap(*localFile); - MwmSet::MwmId const & id = res.first; - if (id.IsAlive()) - rect = id.GetInfo()->m_bordersRect; - } + m_trafficManager.RunSynchronized([this, localFile, &rect](){ + auto const res = RegisterMap(*localFile); + MwmSet::MwmId const & id = res.first; + if (id.IsAlive()) + rect = id.GetInfo()->m_bordersRect; + m_trafficManager.Invalidate(id); + }); - m_trafficManager.Invalidate(); m_transitManager.Invalidate(); m_isolinesManager.Invalidate(); @@ -502,6 +530,8 @@ void Framework::LoadMapsSync() LOG(LDEBUG, ("Editor initialized")); GetStorage().RestoreDownloadQueue(); + + InitializeTraffic(); } // Small copy-paste with LoadMapsSync, but I don't have a better solution. @@ -524,6 +554,8 @@ void Framework::LoadMapsAsync(std::function && callback) GetStorage().RestoreDownloadQueue(); + InitializeTraffic(); + callback(); }); }).detach(); @@ -2490,6 +2522,42 @@ void Framework::SaveTrafficEnabled(bool trafficEnabled) settings::Set(kTrafficEnabledKey, trafficEnabled); } +void Framework::SetTrafficHttpEnabled(bool enabled) +{ + m_trafficManager.SetHttpTraffSource(enabled, LoadTrafficHttpUrl()); +} + +bool Framework::LoadTrafficHttpEnabled() +{ + bool enabled; + if (!settings::Get(kTrafficHttpEnabledKey, enabled)) + enabled = false; + return enabled; +} + +void Framework::SaveTrafficHttpEnabled(bool trafficHttpEnabled) +{ + settings::Set(kTrafficHttpEnabledKey, trafficHttpEnabled); +} + +void Framework::SetTrafficHttpUrl(std::string url) +{ + m_trafficManager.SetHttpTraffSource(LoadTrafficHttpEnabled(), url); +} + +std::string Framework::LoadTrafficHttpUrl() +{ + std::string url; + if (!settings::Get(kTrafficHttpUrlKey, url)) + url = TRAFFIC_HTTP_URL_DEFAULT; + return url; +} + +void Framework::SaveTrafficHttpUrl(std::string trafficHttpUrl) +{ + settings::Set(kTrafficHttpUrlKey, trafficHttpUrl); +} + bool Framework::LoadTrafficSimplifiedColors() { bool simplified; diff --git a/libs/map/framework.hpp b/libs/map/framework.hpp index 6ef95bab4..b2febdb08 100644 --- a/libs/map/framework.hpp +++ b/libs/map/framework.hpp @@ -104,11 +104,22 @@ class Loader; /// build version for screenshots. // #define FIXED_LOCATION +/** + * @brief Initialization parameters for the framework. + * + * `FrameworkParams` is intended for parameters which are hardcoded rather than read from a + * configuration. It allows test cases to run on a tailored configuration. + */ struct FrameworkParams { bool m_enableDiffs = true; size_t m_numSearchAPIThreads = 1; + /** + * @brief Whether the traffic manager should start in test mode. + */ + bool m_trafficTestMode = false; + FrameworkParams() = default; FrameworkParams(bool enableDiffs) : m_enableDiffs(enableDiffs) {} }; @@ -234,7 +245,34 @@ class Framework /// \note It works for group and leaf node. bool HasUnsavedEdits(storage::CountryId const & countryId); + /** + * @brief Loads maps synchronously. + * + * Maps are loaded on the calling thread. + * + * This function also performs certain initialization operations which depend on map data being + * available, such as search, traffic and the download queue. + * + * @note This function is not suitable for use on platforms which enforce restrictions on + * time-consuming or potentially blocking operations on the UI thread (as Android does). On such + * platforms, `LoadMapsAsync()` should be used instead. + */ void LoadMapsSync(); + + /** + * @brief Loads maps asynchronously. + * + * Maps are loaded on a new thread. Some operations are executed as part of a separate task which + * is posted to the GUI thread. + * + * This function also performs certain initialization operations which depend on map data being + * available, such as search, traffic and the download queue. + * + * After finishing initialization, the caller-supplied callback function is called. This function + * also runs on the GUI thread and should therefore not perform any time-consuming operations. + * + * @param callback A callback function to run at the end of initialization. + */ void LoadMapsAsync(std::function && callback); /// Registers all local map files in internal indexes. @@ -385,6 +423,16 @@ class Framework private: std::vector GetSelectedFeatureTriangles() const; + /** + * @brief Initializes the traffic manager. + * + * This enables the traffic manager if defined in settings. If the traffic manager is not in test + * mode, all cunfigured sources are also added here. + * + * Maps must be loaded prior to calling this method. + */ + void InitializeTraffic(); + public: /// @name GPS location updates routine. void OnLocationError(location::TLocationError error); @@ -491,11 +539,18 @@ class Framework std::unique_ptr m_descriptionsLoader; public: - // Moves viewport to the search result and taps on it. + /** + * @brief Moves viewport to the search result and taps on it. + * @param res + * @param animation + */ void SelectSearchResult(search::Result const & res, bool animation); - // Cancels all searches, stops location follow and then selects - // search result. + /** + * @brief Cancels all searches, stops location follow and then selects search result. + * @param res + * @param animation + */ void ShowSearchResult(search::Result const & res, bool animation = true); void UpdateViewport(search::Results const & results); @@ -729,6 +784,14 @@ class Framework bool LoadTrafficEnabled(); void SaveTrafficEnabled(bool trafficEnabled); + void SetTrafficHttpEnabled(bool enabled); + bool LoadTrafficHttpEnabled(); + void SaveTrafficHttpEnabled(bool trafficHttpEnabled); + + void SetTrafficHttpUrl(std::string url); + std::string LoadTrafficHttpUrl(); + void SaveTrafficHttpUrl(std::string trafficHttpUrl); + bool LoadTrafficSimplifiedColors(); void SaveTrafficSimplifiedColors(bool simplified); diff --git a/libs/map/routing_manager.cpp b/libs/map/routing_manager.cpp index 0f881f5d6..3876a5f19 100644 --- a/libs/map/routing_manager.cpp +++ b/libs/map/routing_manager.cpp @@ -498,7 +498,7 @@ void RoutingManager::SetRouterImpl(RouterType type) VehicleType const vehicleType = GetVehicleType(type); - m_loadAltitudes = vehicleType != VehicleType::Car; + m_loadAltitudes = (vehicleType != VehicleType::Car) && (vehicleType != VehicleType::Decoder); auto const countryFileGetter = [this](m2::PointD const & p) -> string { diff --git a/libs/map/traffic_manager.cpp b/libs/map/traffic_manager.cpp index 66114e50b..be2a2976e 100644 --- a/libs/map/traffic_manager.cpp +++ b/libs/map/traffic_manager.cpp @@ -8,51 +8,80 @@ #include "indexer/ftypes_matcher.hpp" #include "indexer/scales.hpp" +#include "geometry/distance_on_sphere.hpp" #include "geometry/mercator.hpp" #include "platform/platform.hpp" +#include "traffxml/traff_model_xml.hpp" + using namespace std::chrono; namespace { +/** + * Poll interval for traffic data + */ auto constexpr kUpdateInterval = minutes(1); + +/** + * Purge interval for expired traffic messages + */ +auto constexpr kPurgeInterval = minutes(1); + auto constexpr kOutdatedDataTimeout = minutes(5) + kUpdateInterval; auto constexpr kNetworkErrorTimeout = minutes(20); auto constexpr kMaxRetriesCount = 5; + +/** + * Interval at which the Drape engine gets traffic updates while messages are being processed. + */ +auto constexpr kDrapeUpdateInterval = seconds(10); + +/** + * Interval at which the traffic observer gets traffic updates while messages are being processed. + */ +auto constexpr kObserverUpdateInterval = minutes(1); + +/** + * Interval at which the message cache file is updated while messages are being processed. + */ +auto constexpr kStorageUpdateInterval = minutes(1); + +/** + * File name at which traffic data is persisted. + */ +auto constexpr kTrafficXMLFileName = "traffic.xml"; + +/** + * Threshold by which viewport or user position can change without requiring the nessage queue to + * be resorted. + */ +auto constexpr kPositionThreshold = 1000.0; } // namespace -TrafficManager::CacheEntry::CacheEntry() - : m_isLoaded(false) - , m_dataSize(0) - , m_retriesCount(0) - , m_isWaitingForResponse(false) - , m_lastAvailability(traffic::TrafficInfo::Availability::Unknown) -{} - -TrafficManager::CacheEntry::CacheEntry(time_point const & requestTime) - : m_isLoaded(false) - , m_dataSize(0) - , m_lastActiveTime(requestTime) - , m_lastRequestTime(requestTime) - , m_retriesCount(0) - , m_isWaitingForResponse(true) - , m_lastAvailability(traffic::TrafficInfo::Availability::Unknown) -{} - -TrafficManager::TrafficManager(GetMwmsByRectFn const & getMwmsByRectFn, size_t maxCacheSizeBytes, - traffic::TrafficObserver & observer) - : m_getMwmsByRectFn(getMwmsByRectFn) - , m_observer(observer) +TrafficManager::TrafficManager(DataSource & dataSource, CountryInfoGetterFn countryInfoGetter, + const CountryParentNameGetterFn &countryParentNameGetter, + GetMwmsByRectFn const & getMwmsByRectFn, size_t maxCacheSizeBytes, + routing::RoutingSession & routingSession) + : m_dataSource(dataSource) + , m_countryInfoGetterFn(countryInfoGetter) + , m_countryParentNameGetterFn(countryParentNameGetter) + , m_getMwmsByRectFn(getMwmsByRectFn) + , m_routingSession(routingSession) , m_currentDataVersion(0) , m_state(TrafficState::Disabled) - , m_maxCacheSizeBytes(maxCacheSizeBytes) , m_isRunning(true) , m_isPaused(false) , m_thread(&TrafficManager::ThreadRoutine, this) { CHECK(m_getMwmsByRectFn != nullptr, ()); + GetPlatform().RunTask(Platform::Thread::Gui, [this]() { + m_routingSession.SetChangeSessionStateCallback([this](routing::SessionState previous, routing::SessionState current) { + OnChangeRoutingSessionState(previous, current); + }); + }); } TrafficManager::~TrafficManager() @@ -77,6 +106,12 @@ void TrafficManager::Teardown() m_thread.join(); } +std::map TrafficManager::GetMessageCache() +{ + std::lock_guard lock(m_mutex); + return m_messageCache; +} + TrafficManager::TrafficState TrafficManager::GetState() const { return m_state; @@ -89,32 +124,62 @@ void TrafficManager::SetStateListener(TrafficStateChangedFn const & onStateChang void TrafficManager::SetEnabled(bool enabled) { + /* + * Whether to notify interested parties that traffic data has been updated. + * This is based on the return value of RestoreCache(). + */ + bool notifyUpdate = false; { std::lock_guard lock(m_mutex); if (enabled == IsEnabled()) - return; - Clear(); + return; + if (enabled) + { + if (!m_traffDecoder) + // deferred decoder initialization (requires maps to be loaded) + m_traffDecoder = make_unique(m_dataSource, m_countryInfoGetterFn, + m_countryParentNameGetterFn, m_messageCache); + if (!m_storage && !IsTestMode()) + { + m_storage = std::make_unique(kTrafficXMLFileName); + notifyUpdate = RestoreCache(); + m_lastStorageUpdate = steady_clock::now(); + } + } ChangeState(enabled ? TrafficState::Enabled : TrafficState::Disabled); } m_drapeEngine.SafeCall(&df::DrapeEngine::EnableTraffic, enabled); if (enabled) - Invalidate(); + { + if (notifyUpdate) + OnTrafficDataUpdate(); + else + RecalculateSubscription(true); + m_canSetMode = false; + } else - m_observer.OnTrafficInfoClear(); + { + Unsubscribe(); + m_routingSession.OnTrafficInfoClear(); + } } void TrafficManager::Clear() { - m_currentCacheSizeBytes = 0; - m_mwmCache.clear(); - m_lastDrapeMwmsByRect.clear(); - m_lastRoutingMwmsByRect.clear(); - m_activeDrapeMwms.clear(); - m_activeRoutingMwms.clear(); - m_requestedMwms.clear(); - m_trafficETags.clear(); + { + std::lock_guard lock(m_mutex); + + m_messageCache.clear(); + m_feedQueue.clear(); + } + OnTrafficDataUpdate(); +} + +void TrafficManager::SetTrafficUpdateCallbackFn(TrafficUpdateCallbackFn && fn) +{ + m_trafficUpdateCallbackFn = std::move(fn); } void TrafficManager::SetDrapeEngine(ref_ptr engine) @@ -129,24 +194,7 @@ void TrafficManager::SetCurrentDataVersion(int64_t dataVersion) void TrafficManager::OnMwmDeregistered(platform::LocalCountryFile const & countryFile) { - if (!IsEnabled()) - return; - - { - std::lock_guard lock(m_mutex); - - MwmSet::MwmId mwmId; - for (auto const & cacheEntry : m_mwmCache) - { - if (cacheEntry.first.IsDeregistered(countryFile)) - { - mwmId = cacheEntry.first; - break; - } - } - - ClearCache(mwmId); - } + // TODO we don’t need this any more (called by Framework::OnMapDeregistered()) } void TrafficManager::OnDestroySurface() @@ -159,18 +207,154 @@ void TrafficManager::OnRecoverSurface() Resume(); } -void TrafficManager::Invalidate() +void TrafficManager::OnChangeRoutingSessionState(routing::SessionState previous, routing::SessionState current) { - if (!IsEnabled()) + // TODO assert we’re running on the UI thread + LOG(LINFO, ("Routing session state changed from", previous, "to", current)); + LOG(LINFO, ("Running on thread", std::this_thread::get_id())); + + m_routingSessionState = current; + + /* + * Filter based on session state (see routing_callbacks.hpp for states and transitions). + * + * GetAllRegions() seems to get fresh data during build (presumably also rebuild), which will be + * available on the next state transition (to RouteNotStarted or OnRoute) and remain unchanged + * until the next route (re)build. Therefore, calling GetAllRegions() when new state is one of + * RouteNotStarted, OnRoute or RouteNoFollowing, and clearing MWMs when the new state is + * NoValidRoute, and ignoring all other transitions, should work for our purposes. + * There may be cases where we first calculate the route, then subscribe to the regions and only + * then get notified about a traffic problem on the route, requiring us to recalculate. + */ + std::set mwmNames; + if (current == routing::SessionState::RouteNotStarted + || current == routing::SessionState::OnRoute + || current == routing::SessionState::RouteNoFollowing) + /* + * GetAllRegions() may block when run for the first time. This should happen during routing, so + * the call here would always return cached results without blocking, causing no problems on the + * UI thread. + * + * Note that this method is called before the state is updated internally, thus `GetAllRegions()` + * and any other functions would still have `previous` as their state. + */ + m_routingSession.GetAllRegions(mwmNames); + else if (current == routing::SessionState::NoValidRoute) + mwmNames.clear(); + else + // for all other states, keep current set return; - m_lastDrapeMwmsByRect.clear(); - m_lastRoutingMwmsByRect.clear(); + LOG(LINFO, ("Router MWMs:", mwmNames)); + + std::set mwms; + for (auto const & mwmName : mwmNames) + { + MwmSet::MwmId mwmId = m_dataSource.GetMwmIdByCountryFile(platform::CountryFile(mwmName)); + if (mwmId.IsAlive()) + mwms.insert(mwmId); + } + + LOG(LINFO, ("MWM set:", mwms)); + + { + std::lock_guard lock(m_mutex); + + if (mwms != m_activeRoutingMwms) + { + m_activeMwmsChanged = true; + std::swap(mwms, m_activeRoutingMwms); + + if ((m_activeDrapeMwms.empty() && m_activePositionMwms.empty() && m_activeRoutingMwms.empty()) + || !IsEnabled() || IsInvalidState() || IsPausedAndNotRouting()) + return; + + m_condition.notify_one(); + } + } +} + +void TrafficManager::RecalculateSubscription(bool forceRenewal) +{ + if (!IsEnabled() || IsPausedAndNotRouting()) + return; if (m_currentModelView.second) UpdateViewport(m_currentModelView.first); if (m_currentPosition.second) UpdateMyPosition(m_currentPosition.first); + + { + std::lock_guard lock(m_mutex); + + /* + * If UpdateViewport() or UpdateMyPosition() had changes, they would also have updated the + * routing MWMs and reset m_activeMwmsChanged. If neither of them had changes and + * m_activeMwmsChanged is true, it indicates changes in route MWMs which we need to process. + * If `forceRenewal` is true, we set `m_activeMwmsChanged` to true in order to force renewal of + * all subscriptions. + */ + m_activeMwmsChanged |= forceRenewal; + if (m_activeMwmsChanged) + { + if ((m_activeDrapeMwms.empty() && m_activePositionMwms.empty() && m_activeRoutingMwms.empty()) + || IsInvalidState()) + return; + + m_condition.notify_one(); + } + } +} + +void TrafficManager::Invalidate(MwmSet::MwmId const & mwmId) +{ + auto const mwmRect = mwmId.GetInfo()->m_bordersRect; // m2::RectD + traffxml::TraffFeed invalidated; + + for (auto it = m_messageCache.begin(); it != m_messageCache.end(); ) + { + if (!it->second.m_location) + { + it++; + continue; + } + + bool isInvalid = false; + + // invalidate if decoded location uses a previous version of the MWM + for (auto const & [decodedMwmId, coloring] : it->second.m_decoded) + if (decodedMwmId.GetInfo()->GetCountryName() == mwmId.GetInfo()->GetCountryName()) + isInvalid = true; + + // invalidate if bounding rect of reference points intersects with bounding rect of MWM + if (!isInvalid) + { + m2::RectD locationRect; + for (auto const & point : {it->second.m_location.value().m_from, + it->second.m_location.value().m_via, + it->second.m_location.value().m_at, + it->second.m_location.value().m_to}) + if (point) + locationRect.Add(mercator::FromLatLon(point.value().m_coordinates)); + isInvalid = locationRect.IsIntersect(mwmRect); + } + + if (isInvalid) + { + traffxml::TraffMessage message(it->second); + message.m_decoded.clear(); + invalidated.push_back(message); + it = m_messageCache.erase(it); + } + else + ++it; + } + + if (!invalidated.empty()) + { + m_feedQueue.insert(m_feedQueue.begin(), invalidated); + m_condition.notify_one(); + } } void TrafficManager::UpdateActiveMwms(m2::RectD const & rect, std::vector & lastMwmsByRect, @@ -183,11 +367,17 @@ void TrafficManager::UpdateActiveMwms(m2::RectD const & rect, std::vector lock(m_mutex); + m_activeMwmsChanged = true; activeMwms.clear(); for (auto const & mwm : mwms) if (mwm.IsAlive()) activeMwms.insert(mwm); - RequestTrafficData(); + + if ((m_activeDrapeMwms.empty() && m_activePositionMwms.empty() && m_activeRoutingMwms.empty()) + || !IsEnabled() || IsInvalidState() || IsPausedAndNotRouting()) + return; + + m_condition.notify_one(); } } @@ -198,21 +388,33 @@ void TrafficManager::UpdateMyPosition(MyPosition const & myPosition) double constexpr kSquareSideM = 5000.0; m_currentPosition = {myPosition, true /* initialized */}; - if (!IsEnabled() || IsInvalidState() || m_isPaused) + if (!m_currentPositionLazy.second + || (mercator::DistanceOnEarth(m_currentPositionLazy.first.m_position, myPosition.m_position) > kPositionThreshold)) + { + m_isFeedQueueSortInvalid = true; + m_currentPositionLazy = m_currentPosition; + } + + if (!IsEnabled() || IsInvalidState() || IsPausedAndNotRouting()) return; m2::RectD const rect = mercator::RectByCenterXYAndSizeInMeters(myPosition.m_position, kSquareSideM / 2.0); // Request traffic. - UpdateActiveMwms(rect, m_lastRoutingMwmsByRect, m_activeRoutingMwms); - - // @TODO Do all routing stuff. + UpdateActiveMwms(rect, m_lastPositionMwmsByRect, m_activePositionMwms); } void TrafficManager::UpdateViewport(ScreenBase const & screen) { m_currentModelView = {screen, true /* initialized */}; - if (!IsEnabled() || IsInvalidState() || m_isPaused) + if (!m_currentModelViewLazy.second + || (mercator::DistanceOnEarth(m_currentModelViewLazy.first.ClipRect().Center(), screen.ClipRect().Center()) > kPositionThreshold)) + { + m_isFeedQueueSortInvalid = true; + m_currentModelViewLazy = m_currentModelView; + } + + if (!IsEnabled() || IsInvalidState() || IsPausedAndNotRouting()) return; if (df::GetZoomLevel(screen.GetScale()) < df::kRoadClass0ZoomLevel) @@ -222,287 +424,658 @@ void TrafficManager::UpdateViewport(ScreenBase const & screen) UpdateActiveMwms(screen.ClipRect(), m_lastDrapeMwmsByRect, m_activeDrapeMwms); } -void TrafficManager::ThreadRoutine() +void TrafficManager::SubscribeOrChangeSubscription() { - std::vector mwms; - while (WaitForRequest(mwms)) + std::set activeMwms; + + if (m_activeMwmsChanged) { - for (auto const & mwm : mwms) { - if (!mwm.IsAlive()) - continue; - - traffic::TrafficInfo info(mwm, m_currentDataVersion); - - std::string tag; - { - std::lock_guard lock(m_mutex); - tag = m_trafficETags[mwm]; - } - - if (info.ReceiveTrafficData(tag)) - { - OnTrafficDataResponse(std::move(info)); - } - else - { - LOG(LWARNING, ("Traffic request failed. Mwm =", mwm)); - OnTrafficRequestFailed(std::move(info)); - } + std::lock_guard lock(m_mutex); + m_activeMwmsChanged = false; + UniteActiveMwms(activeMwms); + } - { - std::lock_guard lock(m_mutex); - m_trafficETags[mwm] = tag; - } + { + std::lock_guard lock(m_trafficSourceMutex); + for (auto & source : m_trafficSources) + source->SubscribeOrChangeSubscription(activeMwms); } - mwms.clear(); } } -bool TrafficManager::WaitForRequest(std::vector & mwms) +void TrafficManager::Unsubscribe() { - std::unique_lock lock(m_mutex); - - bool const timeout = - !m_condition.wait_for(lock, kUpdateInterval, [this] { return !m_isRunning || !m_requestedMwms.empty(); }); + std::lock_guard lock(m_trafficSourceMutex); + for (auto & source : m_trafficSources) + source->Unsubscribe(); +} - if (!m_isRunning) +bool TrafficManager::RestoreCache() +{ + ASSERT(m_storage, ("m_storage cannot be null")); + pugi::xml_document document; + if (!m_storage->Load(document)) + { + LOG(LWARNING, ("Failed to reload cache from storage")); return false; + } - if (timeout) - RequestTrafficData(); + traffxml::TraffFeed feedIn; + traffxml::TraffFeed feedOut; + bool hasDecoded = false; + bool hasUndecoded = false; + if (traffxml::ParseTraff(document, m_dataSource, feedIn)) + { + while (!feedIn.empty()) + { + traffxml::TraffMessage message; + std::swap(message, feedIn.front()); + feedIn.erase(feedIn.begin()); - if (!m_requestedMwms.empty()) - mwms.swap(m_requestedMwms); + if (!message.IsExpired(traffxml::IsoTime::Now())) + { + if (!message.m_decoded.empty()) + { + hasDecoded = true; + // store message in cache + m_messageCache.insert_or_assign(message.m_id, message); + } + else + { + hasUndecoded = true; + // message needs decoding, prepare to enqueue + feedOut.push_back(message); + } + } + } + if (!feedOut.empty()) + m_feedQueue.insert(m_feedQueue.begin(), feedOut); + // update notification is caller’s responsibility + return hasDecoded && !hasUndecoded; + } + else + { + LOG(LWARNING, ("An error occurred parsing the cache file")); + } + return false; +} - return true; +void TrafficManager::Poll() +{ + std::lock_guard lock(m_trafficSourceMutex); + for (auto & source : m_trafficSources) + if (source->IsPollNeeded()) + source->Poll(); } -void TrafficManager::RequestTrafficData(MwmSet::MwmId const & mwmId, bool force) +void TrafficManager::ReceiveFeed(traffxml::TraffFeed feed) { - bool needRequesting = false; - auto const currentTime = steady_clock::now(); - auto const it = m_mwmCache.find(mwmId); - if (it == m_mwmCache.end()) { - needRequesting = true; - m_mwmCache.insert(std::make_pair(mwmId, CacheEntry(currentTime))); + std::lock_guard lock(m_mutex); + m_feedQueue.push_back(feed); + m_isFeedQueueSortInvalid = true; } - else + m_condition.notify_one(); +} + +void TrafficManager::RegisterSource(std::unique_ptr source) +{ + if (IsEnabled()) { - auto const passedSeconds = currentTime - it->second.m_lastRequestTime; - if (passedSeconds >= kUpdateInterval || force) + std::set activeMwms; + { - needRequesting = true; - it->second.m_isWaitingForResponse = true; - it->second.m_lastRequestTime = currentTime; + std::lock_guard lock(m_mutex); + UniteActiveMwms(activeMwms); } - if (!force) - it->second.m_lastActiveTime = currentTime; + + if (!activeMwms.empty()) + source->SubscribeOrChangeSubscription(activeMwms); } - if (needRequesting) { - m_requestedMwms.push_back(mwmId); - m_condition.notify_one(); + std::lock_guard lock(m_trafficSourceMutex); + m_trafficSources.push_back(std::move(source)); } + + m_isPollNeeded = IsEnabled(); } -void TrafficManager::RequestTrafficData() +void TrafficManager::PurgeExpiredMessages() { - if ((m_activeDrapeMwms.empty() && m_activeRoutingMwms.empty()) || !IsEnabled() || IsInvalidState() || m_isPaused) - return; - - ForEachActiveMwm([this](MwmSet::MwmId const & mwmId) - { - ASSERT(mwmId.IsAlive(), ()); - RequestTrafficData(mwmId, false /* force */); - }); - UpdateState(); + PurgeExpiredMessagesImpl(); + OnTrafficDataUpdate(); } -void TrafficManager::OnTrafficRequestFailed(traffic::TrafficInfo && info) +bool TrafficManager::PurgeExpiredMessagesImpl() { std::lock_guard lock(m_mutex); - - auto it = m_mwmCache.find(info.GetMwmId()); - if (it == m_mwmCache.end()) - return; - - it->second.m_isWaitingForResponse = false; - it->second.m_lastAvailability = info.GetAvailability(); - - if (info.GetAvailability() == traffic::TrafficInfo::Availability::Unknown && !it->second.m_isLoaded) + bool result = false; + LOG(LINFO, ("before:", m_messageCache.size(), "message(s)")); + traffxml::IsoTime now = traffxml::IsoTime::Now(); + for (auto it = m_messageCache.begin(); it != m_messageCache.end(); ) { - if (m_activeDrapeMwms.find(info.GetMwmId()) != m_activeDrapeMwms.cend() || - m_activeRoutingMwms.find(info.GetMwmId()) != m_activeRoutingMwms.cend()) + if (it->second.IsExpired(now)) { - if (it->second.m_retriesCount < kMaxRetriesCount) - RequestTrafficData(info.GetMwmId(), true /* force */); - ++it->second.m_retriesCount; + it = m_messageCache.erase(it); + result = true; } else + ++it; + } + LOG(LINFO, ("after:", m_messageCache.size(), "message(s)")); + return result; +} + +void TrafficManager::ConsolidateFeedQueue() +{ + std::lock_guard lock(m_mutex); + if (m_feedQueue.empty()) + return; + for (size_t i = m_feedQueue.size() - 1; i <= 0; i--) + for (size_t j = m_feedQueue.size() - 1; j <= 0; j--) { - it->second.m_retriesCount = 0; + if (i == j) + continue; + for (auto it_i = m_feedQueue[i].begin(); it_i != m_feedQueue[i].end(); ) + for (auto it_j = m_feedQueue[j].end(); it_j != m_feedQueue[j].end(); ) + if (it_i->m_id == it_j->m_id) + { + // dupe, remove older + if (it_i->m_updateTime < it_j->m_updateTime) + { + // standard case: i has the newer one + ++it_i; + it_j = m_feedQueue[j].erase(it_j); + } + else if (it_i->m_updateTime < it_j->m_updateTime) + { + // j has the newer one + it_i = m_feedQueue[i].erase(it_i); + ++it_j; + } + else if (i > j) + { + // same time, but feed i was received after j, keep i + ++it_i; + it_j = m_feedQueue[j].erase(it_j); + } + else + { + // same time, but feed j was received after i, keep j + ASSERT(i != j, ()); + it_i = m_feedQueue[i].erase(it_i); + ++it_j; + } + } } + // remove empty feeds from the beginning of the queue + while (!m_feedQueue.empty() && m_feedQueue.front().empty()) + m_feedQueue.erase(m_feedQueue.begin()); + // merge everything into the first vector + for (size_t i = 1; i < m_feedQueue.size(); i++) + { + if (m_feedQueue[i].empty()) + continue; + m_feedQueue[0].insert(m_feedQueue[0].end(), + std::make_move_iterator(m_feedQueue[i].begin()), + std::make_move_iterator(m_feedQueue[i].end())); + m_feedQueue[i].clear(); + } + // remove empty feeds (any feeds after the first one are empty at this point) + if (!m_feedQueue.empty()) + { + if (!m_feedQueue.front().empty()) + m_feedQueue.resize(1); + else + m_feedQueue.clear(); } - - UpdateState(); } -void TrafficManager::OnTrafficDataResponse(traffic::TrafficInfo && info) +void TrafficManager::DecodeFirstMessage() { + traffxml::TraffMessage message; { + // Lock the mutex while iterating over the feed queue std::lock_guard lock(m_mutex); - - auto it = m_mwmCache.find(info.GetMwmId()); - if (it == m_mwmCache.end()) + // remove empty feeds from the beginning of the queue + while (!m_feedQueue.empty() && m_feedQueue.front().empty()) + m_feedQueue.erase(m_feedQueue.begin()); + // if we have no more feeds, return (nothing to do) + if (m_feedQueue.empty()) return; + if (m_isFeedQueueSortInvalid + && (m_currentPositionLazy.second || m_currentModelViewLazy.second)) + { + std::sort(m_feedQueue.front().begin(), m_feedQueue.front().end(), + [this](const traffxml::TraffMessage & a, const traffxml::TraffMessage & b){ + // return a < b + // put messages first which decode quickly: cancellations, and updates with same location + if (a.m_cancellation) + return !b.m_cancellation; + else if (b.m_cancellation) + return false; + auto aIt = m_messageCache.find(a.m_id); + auto bIt = m_messageCache.find(b.m_id); + if ((aIt != m_messageCache.end()) && (aIt->second.m_location == a.m_location)) + return !((bIt != m_messageCache.end()) && (bIt->second.m_location == b.m_location)); + else if ((bIt != m_messageCache.end()) && (bIt->second.m_location == b.m_location)) + return false; + // sort by shortest distance between reference point and position/viewport + std::vector locations; + if (m_currentPositionLazy.second) + locations.push_back(mercator::ToLatLon(m_currentPositionLazy.first.m_position)); + if (m_currentModelViewLazy.second) + locations.push_back(mercator::ToLatLon(m_currentModelViewLazy.first.ClipRect().Center())); + double aDist = 4.5e+7; + double bDist = 4.5e+7; + for (auto const & [message, dist] : { std::pair{a, std::ref(aDist)}, {b, std::ref(bDist)} }) + { + // messages without location first + if (!message.m_location) + { + dist.get() = 0; + continue; + } + // for simplification, we are skipping the via point + for (auto const & point : { message.m_location.value().m_from, + message.m_location.value().m_at, + message.m_location.value().m_to }) + { + if (!point) + continue; + for (auto const & location : locations) + { + auto newdist = ms::DistanceOnEarth(point.value().m_coordinates, location); + if (newdist < dist.get()) + dist.get() = newdist; + } + } + } + return aDist < bDist; + }); + m_isFeedQueueSortInvalid = false; + } + // retrieve the first message from the first feed, remove it from the feed + std::swap(message, m_feedQueue.front().front()); + m_feedQueue.front().erase(m_feedQueue.front().begin()); + // if the feed has no more messages, erase it (eager erase, as an empty queue is used as a condition later) + if (m_feedQueue.front().empty()) + m_feedQueue.erase(m_feedQueue.begin()); + } - it->second.m_isLoaded = true; - it->second.m_lastResponseTime = steady_clock::now(); - it->second.m_isWaitingForResponse = false; - it->second.m_lastAvailability = info.GetAvailability(); + { + std::lock_guard lock(m_mutex); - if (!info.GetColoring().empty()) + // check if message is actually newer + auto it = m_messageCache.find(message.m_id); + bool process = (it == m_messageCache.end()); + if (!process) + process = (it->second.m_updateTime < message.m_updateTime); + if (!process) { - // Update cache. - size_t constexpr kElementSize = sizeof(traffic::TrafficInfo::RoadSegmentId) + sizeof(traffic::SpeedGroup); - size_t const dataSize = info.GetColoring().size() * kElementSize; - m_currentCacheSizeBytes += (dataSize - it->second.m_dataSize); - it->second.m_dataSize = dataSize; - ShrinkCacheToAllowableSize(); + LOG(LINFO, ("message", message.m_id, "is already in cache, skipping")); + return; } - - UpdateState(); } - if (!info.GetColoring().empty()) + LOG(LINFO, (" ", message.m_id, ":", message)); + m_traffDecoder->DecodeMessage(message); { - m_drapeEngine.SafeCall(&df::DrapeEngine::UpdateTraffic, static_cast(info)); + std::lock_guard lock(m_mutex); + + // store message in cache + m_messageCache.insert_or_assign(message.m_id, message); - // Update traffic colors for routing. - m_observer.OnTrafficInfoAdded(std::move(info)); + for (auto & replaced : message.m_replaces) + { + auto it = m_messageCache.find(replaced); + if (it != m_messageCache.cend()) + m_messageCache.erase(it); + } } + /* + * TODO detect if we can do a quick update: + * - new message which does not replace any existing message + * - coloring “wins” over replaced message: + * - contains all the segments of the previous message (always true when location is the same) + * - speed groups are the same or lower as in previous message (always true when all members of + * traffic impact are unchanged or have worsened) – for this purpose, closure is considered + * lower than any other speed group + * In this case, run: + * traffxml::MergeMultiMwmColoring(message.m_decoded, m_allMwmColoring); + * Otherwise, set a flag indicating we need to process coloring in full. + */ } -void TrafficManager::UniteActiveMwms(std::set & activeMwms) const +void TrafficManager::ThreadRoutine() { - activeMwms.insert(m_activeDrapeMwms.cbegin(), m_activeDrapeMwms.cend()); - activeMwms.insert(m_activeRoutingMwms.cbegin(), m_activeRoutingMwms.cend()); + // initially, treat last purge and drape/observer update as having just happened + auto lastPurged = steady_clock::now(); + m_lastDrapeUpdate = steady_clock::now(); + m_lastObserverUpdate = steady_clock::now(); + + while (WaitForRequest()) + { + if (!IsEnabled() || IsPausedAndNotRouting()) + continue; + + /* + * Whether to call OnTrafficDataUpdate() at the end of the current round. + * The logic may fail to catch cases in which the first message in queue replaces another + * message without changing coloring. This would usually occur in a larger feed, where other + * messages would likely require an announcement, making this a minor issue. A single round + * (after a timeout) with no messages expired and an empty queue would not trigger an update. + */ + bool hasUpdates = false; + + if (!IsTestMode()) + { + if (steady_clock::now() - lastPurged >= kPurgeInterval) + { + lastPurged = steady_clock::now(); + hasUpdates |= PurgeExpiredMessagesImpl(); + } + + LOG(LINFO, ("active MWMs changed:", m_activeMwmsChanged, ", poll needed:", m_isPollNeeded)); + + // this is a no-op if active MWMs have not changed + SubscribeOrChangeSubscription(); + + /* + * Poll sources if needed. + * m_isPollNeeded may be set by WaitForRequest() and set/unset by SubscribeOrChangeSubscription(). + */ + if (m_isPollNeeded) + { + m_lastResponseTime = steady_clock::now(); + m_isPollNeeded = false; + Poll(); + } + } + LOG(LINFO, (m_feedQueue.size(), "feed(s) in queue")); + + // consolidate feed queue (remove older messages in favor of newer ones) + ConsolidateFeedQueue(); + hasUpdates |= !m_feedQueue.empty(); + + // decode one message and add it to the cache + DecodeFirstMessage(); + + // set new coloring for MWMs + // `m_mutex` is obtained inside the method, no need to do it here + if (hasUpdates) + OnTrafficDataUpdate(); + } + Unsubscribe(); } -void TrafficManager::ShrinkCacheToAllowableSize() +bool TrafficManager::WaitForRequest() { - // Calculating number of different active mwms. - std::set activeMwms; - UniteActiveMwms(activeMwms); - size_t const numActiveMwms = activeMwms.size(); + std::unique_lock lock(m_mutex); - if (m_currentCacheSizeBytes > m_maxCacheSizeBytes && m_mwmCache.size() > numActiveMwms) + /* + * if we got terminated, return false immediately + * (don’t wait until sleep, we might not get much sleep if we’re busy processing a long feed) + */ + if (!m_isRunning) + return false; + + if (IsEnabled() && !IsPausedAndNotRouting()) { - std::multimap, MwmSet::MwmId> seenTimings; - for (auto const & mwmInfo : m_mwmCache) - seenTimings.insert(std::make_pair(mwmInfo.second.m_lastActiveTime, mwmInfo.first)); + // if we have feeds in the queue, return immediately + if (!m_feedQueue.empty()) + { + LOG(LINFO, ("feed queue not empty, returning immediately")); + return true; + } - auto itSeen = seenTimings.begin(); - while (m_currentCacheSizeBytes > m_maxCacheSizeBytes && m_mwmCache.size() > numActiveMwms) + if (!IsTestMode()) { - ClearCache(itSeen->second); - ++itSeen; + // if update interval has elapsed, return immediately + auto const currentTime = steady_clock::now(); + auto const passedSeconds = currentTime - m_lastResponseTime; + if (passedSeconds >= kUpdateInterval) + { + LOG(LINFO, ("last response was", passedSeconds, "ago, returning immediately")); + m_isPollNeeded = true; + return true; + } } } + + LOG(LINFO, ("nothing to do for now, waiting for timeout or notification")); + bool const timeout = !m_condition.wait_for(lock, kUpdateInterval, [this] + { + // return false to continue waiting, true for any condition we want to process immediately + // return immediately if we got terminated + if (!m_isRunning) + return true; + // otherwise continue waiting if we are paused or disabled + if (!IsEnabled() || m_isPaused) + return false; + return (m_activeMwmsChanged && !IsTestMode()) || !m_feedQueue.empty(); + }); + + // check again if we got terminated while waiting (or woken up because we got terminated) + if (!m_isRunning) + return false; + + // this works as long as wait timeout is at least equal to the poll interval + if (IsEnabled() && !m_isPaused) + m_isPollNeeded |= timeout; + + LOG(LINFO, ("timeout:", timeout, "active MWMs changed:", m_activeMwmsChanged, "test mode:", IsTestMode())); + return true; } -void TrafficManager::ClearCache(MwmSet::MwmId const & mwmId) +void TrafficManager::OnTrafficDataUpdate() { - auto const it = m_mwmCache.find(mwmId); - if (it == m_mwmCache.end()) - return; + bool feedQueueEmpty = false; - if (it->second.m_isLoaded) { - ASSERT_GREATER_OR_EQUAL(m_currentCacheSizeBytes, it->second.m_dataSize, ()); - m_currentCacheSizeBytes -= it->second.m_dataSize; + std::lock_guard lock(m_mutex); + feedQueueEmpty = m_feedQueue.empty(); + } + // Whether to notify the Drape engine of the update. + bool notifyDrape = (feedQueueEmpty); + + // Whether to notify the observer of the update. + bool notifyObserver = (feedQueueEmpty); - m_drapeEngine.SafeCall(&df::DrapeEngine::ClearTrafficCache, mwmId); + // Whether to update the cache file. + bool updateStorage = (feedQueueEmpty); - GetPlatform().RunTask(Platform::Thread::Gui, [this, mwmId]() { m_observer.OnTrafficInfoRemoved(mwmId); }); + if (!feedQueueEmpty) + { + auto const currentTime = steady_clock::now(); + auto const drapeAge = currentTime - m_lastDrapeUpdate; + auto const storageAge = currentTime - m_lastStorageUpdate; + notifyDrape = (drapeAge >= kDrapeUpdateInterval); + updateStorage = (storageAge >= kStorageUpdateInterval); + if (!IsObserverInhibited()) + { + /* + * To avoid resetting the route over and over again while building, inhibit periodic updates + * to the router while a route is being built. Periodic updates will resume once the route is + * fully built. During route calculation, traffic updates are sent only if the queue is empty. + * TODO test this with TMC, where messages arrive one by one, or in short bursts, and the + * queue may run empty multiple times while a route is being calculated. + */ + auto const observerAge = currentTime - m_lastObserverUpdate; + notifyObserver = (observerAge >= kObserverUpdateInterval); + } } - m_mwmCache.erase(it); - m_trafficETags.erase(mwmId); - m_activeDrapeMwms.erase(mwmId); - m_activeRoutingMwms.erase(mwmId); - m_lastDrapeMwmsByRect.clear(); - m_lastRoutingMwmsByRect.clear(); -} -bool TrafficManager::IsEnabled() const -{ - return m_state != TrafficState::Disabled; -} + if (!m_storage || IsTestMode()) + updateStorage = false; -bool TrafficManager::IsInvalidState() const -{ - return m_state == TrafficState::NetworkError; -} + if (updateStorage) + { + std::lock_guard lock(m_mutex); -void TrafficManager::UpdateState() -{ - if (!IsEnabled() || IsInvalidState()) - return; + pugi::xml_document document; - auto const currentTime = steady_clock::now(); - auto maxPassedTime = steady_clock::duration::zero(); + traffxml::GenerateTraff(m_messageCache, document); + if (!m_storage->Save(document)) + LOG(LWARNING, ("Storing message cache to file failed.")); - bool waiting = false; - bool networkError = false; - bool expiredApp = false; - bool expiredData = false; - bool noData = false; + m_lastStorageUpdate = steady_clock::now(); + } - for (MwmSet::MwmId const & mwmId : m_activeDrapeMwms) + if (m_trafficUpdateCallbackFn) + m_trafficUpdateCallbackFn.value()(feedQueueEmpty); + + if (!notifyDrape && !notifyObserver) + return; + + LOG(LINFO, ("Announcing traffic update, notifyDrape:", notifyDrape, "notifyObserver:", notifyObserver)); + + /* + * TODO introduce a flag to indicate we need to fully reprocess coloring, skip if it is false. + * The flag would get set when messages get deleted (including any clear/purge operations), + * or when a new message is added without indicating a simplified update in `DecodeFirstMessage()`. + * When we reprocess coloring in full (the block below), reset this flag. + */ { - auto it = m_mwmCache.find(mwmId); - ASSERT(it != m_mwmCache.end(), ()); + std::lock_guard lock(m_mutex); + + m_allMwmColoring.clear(); + for (const auto & [id, message] : m_messageCache) + traffxml::MergeMultiMwmColoring(message.m_decoded, m_allMwmColoring); + } + + /* + * Much of this code is copied and pasted together from old MWM code, with some minor adaptations: + * + * ForEachMwm (not the body) is from RequestTrafficData() (now RequestTrafficSubscription()), + * modification: cycle over all MWMs (active or not). + * Handling dead MWMs and traffic cache lookup is original code. + * TrafficInfo construction is taken fron ThreadRoutine(), with modifications (different constructor). + * The remainder of the loop is from OnTrafficDataResponse(traffic::TrafficInfo &&), with some modifications + * (removed CacheEntry logic; deciding whether to notify a component and managing timestamps is original code). + * Existing coloring deletion (if there is no new coloring) is original code. + */ + ForEachMwm([this, notifyDrape, notifyObserver](std::shared_ptr info) { + std::lock_guard lock(m_mutex); + + if (info->GetCountryName().starts_with(WORLD_FILE_NAME)) + return; - if (it->second.m_isWaitingForResponse) + MwmSet::MwmId const mwmId(info); + + auto tcit = m_allMwmColoring.find(mwmId); + if (!mwmId.IsAlive()) { - waiting = true; + // MWM was deleted or replaced during decoding + if (tcit != m_allMwmColoring.end()) + m_allMwmColoring.erase(tcit); + return; } - else + if (tcit != m_allMwmColoring.end()) { - expiredApp |= it->second.m_lastAvailability == traffic::TrafficInfo::Availability::ExpiredApp; - expiredData |= it->second.m_lastAvailability == traffic::TrafficInfo::Availability::ExpiredData; - noData |= it->second.m_lastAvailability == traffic::TrafficInfo::Availability::NoData; + traffic::TrafficInfo::Coloring coloring = tcit->second; + LOG(LINFO, ("Setting new coloring for", mwmId, "with", coloring.size(), "entries")); + traffic::TrafficInfo info(mwmId, std::move(coloring)); - if (it->second.m_isLoaded) + if (notifyDrape) { - auto const timeSinceLastResponse = currentTime - it->second.m_lastResponseTime; - if (timeSinceLastResponse > maxPassedTime) - maxPassedTime = timeSinceLastResponse; + /* + * TODO calling ClearTrafficCache before UpdateTraffic is a workaround for a bug in the + * Drape engine: some segments found in the old coloring but not in the new one may get + * left behind. This was not a problem for MapsWithMe as the set of segments never + * changed, but is an issue wherever the segment set is dynamic. Workaround is to clear + * before sending an update. Ultimately, the processing logic for UpdateTraffic needs to + * be fixed, but the code is hard to read (involves multiple messages getting thrown back + * and forth between threads). + */ + m_drapeEngine.SafeCall(&df::DrapeEngine::ClearTrafficCache, + static_cast(mwmId)); + m_drapeEngine.SafeCall(&df::DrapeEngine::UpdateTraffic, + static_cast(info)); + m_lastDrapeUpdate = steady_clock::now(); } - else if (it->second.m_retriesCount >= kMaxRetriesCount) + + if (notifyObserver) { - networkError = true; + // Update traffic colors for routing. + m_routingSession.OnTrafficInfoAdded(std::move(info)); + m_lastObserverUpdate = steady_clock::now(); } } + else + { + if (notifyDrape) + { + m_drapeEngine.SafeCall(&df::DrapeEngine::ClearTrafficCache, + static_cast(mwmId)); + m_lastDrapeUpdate = steady_clock::now(); + } + + if (notifyObserver) + { + // Update traffic colors for routing. + m_routingSession.OnTrafficInfoRemoved(mwmId); + m_lastObserverUpdate = steady_clock::now(); + } + } + }); +} + +void TrafficManager::GetActiveMwms(std::set & activeMwms) +{ + std::lock_guard lock(m_mutex); + UniteActiveMwms(activeMwms); +} + +void TrafficManager::UniteActiveMwms(std::set & activeMwms) const +{ + activeMwms.insert(m_activeDrapeMwms.cbegin(), m_activeDrapeMwms.cend()); + activeMwms.insert(m_activePositionMwms.cbegin(), m_activePositionMwms.cend()); + activeMwms.insert(m_activeRoutingMwms.cbegin(), m_activeRoutingMwms.cend()); +} + +bool TrafficManager::IsEnabled() const +{ + return m_state != TrafficState::Disabled; +} + +void TrafficManager::SetHttpTraffSource(bool enabled, std::string url) +{ + if (IsTestMode()) + return; + + { + std::lock_guard lock(m_trafficSourceMutex); + + for (auto it = m_trafficSources.begin(); it != m_trafficSources.end(); ) + if (traffxml::HttpTraffSource* httpSource = dynamic_cast(it->get())) + { + httpSource->Close(); + m_trafficSources.erase(it); + } + else + ++it; } + if (enabled) + traffxml::HttpTraffSource::Create(*this, url); +} - if (networkError || maxPassedTime >= kNetworkErrorTimeout) - ChangeState(TrafficState::NetworkError); - else if (waiting) - ChangeState(TrafficState::WaitingData); - else if (expiredApp) - ChangeState(TrafficState::ExpiredApp); - else if (expiredData) - ChangeState(TrafficState::ExpiredData); - else if (noData) - ChangeState(TrafficState::NoData); - else if (maxPassedTime >= kOutdatedDataTimeout) - ChangeState(TrafficState::Outdated); - else - ChangeState(TrafficState::Enabled); +void TrafficManager::RemoveTraffSourceIf(const std::function &pred) +{ + std::lock_guard lock(m_trafficSourceMutex); + + for (auto it = m_trafficSources.begin(); it != m_trafficSources.end(); ) + if (pred(it->get())) + m_trafficSources.erase(it); + else + ++it; +} + +bool TrafficManager::IsInvalidState() const +{ + return m_state == TrafficState::NetworkError; } void TrafficManager::ChangeState(TrafficState newState) @@ -540,7 +1113,7 @@ void TrafficManager::Resume() return; m_isPaused = false; - Invalidate(); + RecalculateSubscription(false); } void TrafficManager::SetSimplifiedColorScheme(bool simplified) @@ -549,6 +1122,16 @@ void TrafficManager::SetSimplifiedColorScheme(bool simplified) m_drapeEngine.SafeCall(&df::DrapeEngine::SetSimplifiedTrafficColors, simplified); } +void TrafficManager::SetTestMode() +{ + if (!m_canSetMode) + { + LOG(LWARNING, ("Mode cannot be set once the traffic manager has been enabled")); + return; + } + m_mode = Mode::Test; +} + std::string DebugPrint(TrafficManager::TrafficState state) { switch (state) diff --git a/libs/map/traffic_manager.hpp b/libs/map/traffic_manager.hpp index e015b3dd1..31ce2a4a2 100644 --- a/libs/map/traffic_manager.hpp +++ b/libs/map/traffic_manager.hpp @@ -9,6 +9,15 @@ #include "indexer/mwm_set.hpp" +#include "routing/routing_session.hpp" + +#include "storage/country_info_getter.hpp" + +#include "traffxml/traff_decoder.hpp" +#include "traffxml/traff_model.hpp" +#include "traffxml/traff_source.hpp" +#include "traffxml/traff_storage.hpp" + #include "geometry/point2d.hpp" #include "geometry/polyline2d.hpp" #include "geometry/screenbase.hpp" @@ -28,21 +37,68 @@ #include #include -class TrafficManager final +class TrafficManager final : public traffxml::TraffSourceManager { public: + using CountryInfoGetterFn = std::function; + using CountryParentNameGetterFn = std::function; + using TrafficUpdateCallbackFn = std::function; + + /** + * @brief Global state of traffic information. + */ + /* + * TODO clean out obsolete states. + * Only `Disabled` and `Enabled` are currently used, but some might be reactivated in the future + * and platforms (android/iphone) still evaluate all states. + * `ExpiredData` is definitely obsolete, as traffic data is no longer dependent on a particular + * map version, but still evaluated by android/iphone code. + */ enum class TrafficState { + /** Traffic is disabled, no traffic data will be retrieved or considered for routing. */ Disabled, + /** Traffic is enabled and working normally (the first request may not have been scheduled yet). */ Enabled, + /** At least one request is currently pending. */ WaitingData, + /** At least one MWM has stale traffic data. */ Outdated, + /** Traffic data for at least one MWM was invalid or not found on the server. */ NoData, + /** At least one request failed or timed out. */ NetworkError, + /** Traffic data could not be retrieved because the map data is outdated. */ ExpiredData, + /** Traffic data could not be retrieved because the app version is outdated. */ ExpiredApp }; + /** + * @brief The mode for the traffic manager. + * + * Future versions may introduce further test modes. Therefore, always use `TrafficManager::IsTestMode()` + * to verify if the traffic manager is running in test mode. + */ + enum class Mode + { + /** + * Traffic manager mode for normal operation. + * + * This is the default mode unless something else is explicitly set. + */ + Normal, + /** + * Test mode. + * + * This mode will prevent the traffic manager from automatically subscribing to sources and + * polling them. It will still receive and process push feeds. + * + * Future versions may introduce further behavior changes, and/or introduce more test modes. + */ + Test + }; + struct MyPosition { m2::PointD m_position = m2::PointD(0.0, 0.0); @@ -55,25 +111,122 @@ class TrafficManager final using TrafficStateChangedFn = std::function; using GetMwmsByRectFn = std::function(m2::RectD const &)>; - TrafficManager(GetMwmsByRectFn const & getMwmsByRectFn, size_t maxCacheSizeBytes, - traffic::TrafficObserver & observer); + TrafficManager(DataSource & dataSource, + CountryInfoGetterFn countryInfoGetter, + CountryParentNameGetterFn const & countryParentNameGetter, + GetMwmsByRectFn const & getMwmsByRectFn, size_t maxCacheSizeBytes, + routing::RoutingSession & routingSession); ~TrafficManager(); void Teardown(); + /** + * @brief Returns a copy of the cache of all currently active TraFF messages. + * + * For testing purposes. + * + * Keys are message IDs, values are messages. + * + * This method is safe to call from any thread. + */ + std::map GetMessageCache(); + TrafficState GetState() const; void SetStateListener(TrafficStateChangedFn const & onStateChangedFn); void SetDrapeEngine(ref_ptr engine); + /** + * @brief Sets the version of the MWM used locally. + */ void SetCurrentDataVersion(int64_t dataVersion); + /** + * @brief Enables or disables the traffic manager. + * + * This sets the internal state and notifies the drape engine. + * + * Upon creation, the traffic manager is disabled. MWMs must be loaded before first enabling the + * traffic manager. + * + * While disabled, the traffic manager will not update its subscription area (upon being enabled + * again, it will do so if necessary). It will not poll any sources or process any messages. Feeds + * added via `ReceiveFeed()` will be added to the queue but will not be processed until the + * traffic manager is re-enabled. + * + * Calling this function with `enabled` identical to the current state is a no-op. + * + * @todo Currently, all MWMs must be loaded before calling `SetEnabled()`, as MWMs loaded after + * that will not get picked up. We need to extend `TrafficManager` to react to MWMs being added + * (and removed) – note that this affects the `DataSource`, not the set of active MWMs. + * See `Framework::OnMapDeregistered()` implementation for the opposite case (MWM deregistered). + * + * @param enabled True to enable, false to disable + */ void SetEnabled(bool enabled); + + /** + * @brief Whether the traffic manager is enabled. + * + * @return True if enabled, false if not + */ bool IsEnabled() const; + /** + * @brief Sets the enabled state and URL for the `HttpTraffSource`. + * + * If the traffic manager is in test mode, this function is a no-op. + * + * Otherwise this function is expected to be called only if the enabled state and/or URL have + * actually changed. Setting both to the current state will remove the current source and create + * a new one with identical settings. + * + * This function currently assumes that there is never more than one `HttpTraffSource` configured + * at the same time. + * + * @param enabled Whether the HTTP TraFF source is enabled. + * @param url The URL for the TraFF API. + */ + void SetHttpTraffSource(bool enabled, std::string url); + + /** + * @brief Removes all `TraffSource` instances which satisfy a predicate. + * + * This method iterates over all currently configured `TraffSource` instances and calls the + * caller-suppplied predicate function `pred` on each of them. If `pred` returns true, the source + * is removed, else it is kept. + * + * @todo For now, `pred` deliberately takes a non-const argument so we can do cleanup inside + * `pred`. If we manage to move any such cleanup into the destructor of the `TraffSource` subclass + * and get rid of any `Close()` methods in subclasses (which is preferable for other reasons as + * well), the argument can be made const. + * + * @param pred The predicate function, see description. + */ + void RemoveTraffSourceIf(const std::function& pred); + + /** + * @brief Starts the traffic manager. + * + */ + void Start(); + void UpdateViewport(ScreenBase const & screen); void UpdateMyPosition(MyPosition const & myPosition); - void Invalidate(); + /** + * @brief Invalidates traffic information for the specified MWM. + * + * Invalidation of traffic data is always per MWM and affects locations which refer to any version + * of this MWM, or whose enclosing rectangle overlaps with that of the MWM. The decoded segments + * for these locations are discarded and decoded again, ensuring they are based on the new MWM. + * The TraFF messages themselves remain unchanged. + * + * This method must either be called from a lambda function passed to `RunSynchronized()`, + * or the caller must explicitly lock the private `m_mutex` prior to calling this method. + * + * @param mwmId The newly addded MWM. + */ + void Invalidate(MwmSet::MwmId const & mwmId); void OnDestroySurface(); void OnRecoverSurface(); @@ -85,57 +238,295 @@ class TrafficManager final void SetSimplifiedColorScheme(bool simplified); bool HasSimplifiedColorScheme() const { return m_hasSimplifiedColorScheme; } -private: - struct CacheEntry - { - CacheEntry(); - explicit CacheEntry(std::chrono::time_point const & requestTime); - - bool m_isLoaded; - size_t m_dataSize; - - std::chrono::time_point m_lastActiveTime; - std::chrono::time_point m_lastRequestTime; - std::chrono::time_point m_lastResponseTime; + /** + * @brief Whether the traffic manager is operating in test mode. + */ + bool IsTestMode() { return m_mode != Mode::Normal; } + + /** + * @brief Switches the traffic manager into test mode. + * + * The mode can only be set before the traffic manager is first enabled. After that, this method + * will log a warning but otherwise do nothing. + * + * In test mode, the traffic manager will not subscribe to sources or poll them automatically. + * Expired messages will not get purged automatically, but `PurgeExpiredMessages()` can be called + * to purge expired messages once. The traffic manager will still receive and process push feeds. + * + * Future versions may introduce further behavior changes. + */ + void SetTestMode(); + + /** + * @brief Processes a traffic feed. + * + * The feed may be a result of a pull operation, or received through a push operation. + * (Push operations are not supported by all sources.) + * + * This method is safe to call from any thread. + * + * @param feed The traffic feed. + */ + virtual void ReceiveFeed(traffxml::TraffFeed feed) override; + + /** + * @brief Registers a `TraffSource`. + * @param source The source. + */ + virtual void RegisterSource(std::unique_ptr source) override; + + /** + * @brief Retrieves all currently active MWMs. + * + * This method retrieves all MWMs in the viewport, within a certain distance of the current + * position (if there is a valid position) or part of the route (if any), and stores them in + * `activeMwms`. + * + * This method locks `m_mutex` and is therefore safe to call from any thread. Callers which + * already hold `m_mutex` can use the private `UniteActiveMwms()` method instead. + * + * @param activeMwms Retrieves the list of active MWMs. + */ + virtual void GetActiveMwms(std::set & activeMwms) override; + + /** + * @brief Purges expired messages from the cache. + * + * This method is safe to call from any thread, except for the traffic worker thread. + */ + void PurgeExpiredMessages(); + + /** + * @brief Clears the traffic message cache and feed queue. + * + * This is intended for testing purposes and clears the message cache, as well as the feed queue. + * Subscriptions are not changed. + */ + void Clear(); - int m_retriesCount; - bool m_isWaitingForResponse; + /** + * @brief Registers a callback function which gets called on traffic updates. + * + * Intended for testing. + * + * @param fn The callback function. + */ + void SetTrafficUpdateCallbackFn(TrafficUpdateCallbackFn && fn); + + /** + * @brief Runs a function guarded by the traffic manager mutex. + * + * This locks `m_mutex`, then runs `f` and releases the mutex. + * + * @param f + */ + void RunSynchronized(std::function f) + { + std::lock_guard lock(m_mutex); + f(); + } - traffic::TrafficInfo::Availability m_lastAvailability; - }; +private: + /** + * @brief Recalculates the TraFF subscription area. + * + * The subscription area needs to be recalculated when the traffic manager goes from disabled to + * enabled, or when it is resumed after being paused, as the subscription area is not updated + * while the traffic manager is disabled or paused. + * + * If the subscription area has changed, or if `forceRenewal` is true, TraFF subscriptions are + * renewed by calling `SubscribeOrChangeSubscription()`. + * + * No traffic data is discarded, but sources will be polled for an update, which may turn out + * larger than usual if the traffic manager was in disabled/paused state for an extended period of + * time or the subscription area has changed. + * + * @param forceRenewal If true, renew subscriptions even if the subscription area has not changed. + */ + void RecalculateSubscription(bool forceRenewal); + + /** + * @brief Ensures every TraFF source has a subscription covering all currently active MWMs. + * + * This method cycles through all TraFF sources in `m_trafficSources` and calls + * `SubscribeOrChangeSubscription()` on each of them. + */ + void SubscribeOrChangeSubscription(); + + /** + * @brief Unsubscribes from all traffic services we are subscribed to. + * + * This method cycles through all TraFF sources in `m_trafficSources` and calls `Unsubscribe()` + * on each of them. + */ + void Unsubscribe(); + + /** + * @brief Restores the message cache from file storage. + * + * @note The caller must lock `m_mutex` prior to calling this function, as it makes unprotected + * changes to shared data structures. + * + * @note The return value indicates whether actions related to a traffic update should be taken, + * such as notifying the routing and drape engine. It is true if at least one message with a + * decoded location was read, and no messages without decoded locations. If messages without a + * decoded location were read, the return value is false, as the location decoding will trigger + * updates by itself. If errors occurred and no messages are read, the return value is also false. + * + * @return True if a traffic update needs to be sent, false if not + */ + bool RestoreCache(); + + /** + * @brief Polls all traffic services for updates. + * + * This method cycles through all TraFF sources in `m_trafficSources` and calls `IsPollNeeded()` + * on each of them. If this method returns true, it then calls `Poll()` on the source. + */ + void Poll(); + + /** + * @brief Purges expired messages from the cache. + * + * This is the internal conterpart of `PurgeExpiredMessages()`. It is safe to call from any + * thread. Unlike `PurgeExpiredMessages()`, it does not wake the worker thread, making it suitable + * for use on the worker thread. + * + * @return true if messages were purged, false if not + */ + bool PurgeExpiredMessagesImpl(); + + /** + * @brief Consolidates the feed queue. + * + * If multiple feeds in the queue have the same message ID, only the message with the newest + * update time is kept (if two messages have the same ID and update time, the one in the feed + * with the higher index is kept); other messages with the same ID are discarded. Empty feeds + * are discarded. + */ + void ConsolidateFeedQueue(); + + /** + * @brief Removes the first message from the first feed and decodes it. + */ + void DecodeFirstMessage(); + + /** + * @brief Event loop for the traffic worker thread. + * + * This method runs an event loop, which blocks until woken up or a timeout equivalent to the + * update interval elapses. It cycles through the list of MWMs for which updates have been + * scheduled, triggering a network request for each and processing the result. + */ void ThreadRoutine(); - bool WaitForRequest(std::vector & mwms); - - void OnTrafficDataResponse(traffic::TrafficInfo && info); - void OnTrafficRequestFailed(traffic::TrafficInfo && info); - /// \brief Updates |activeMwms| and request traffic data. - /// \param rect is a rectangle covering a new active mwm set. - /// \note |lastMwmsByRect|/|activeMwms| may be either |m_lastDrapeMwmsByRect/|m_activeDrapeMwms| - /// or |m_lastRoutingMwmsByRect|/|m_activeRoutingMwms|. - /// \note |m_mutex| is locked inside the method. So the method should be called without |m_mutex|. + /** + * @brief Blocks until a request for traffic data is received or a timeout expires. + * + * This method acts as the loop condition for `ThreadRoutine()`. It blocks until woken up or the + * update interval expires. In the latter case, it calls `RequestTrafficData()` to insert all + * currently active MWMs into the list of MWMs to update; otherwise, it leaves the list as it is. + * In either case, it populates `mwms` with the list and returns. + * + * @return `true` during normal operation, `false` during teardown (signaling the event loop to exit). + */ + bool WaitForRequest(); + + /** + * @brief Processes new traffic data. + * + * The new per-MWM colorings (preprocessed traffic information) are taken from `m_allMmColoring`. + * `m_allMwmColoring` is rebuilt from per-message colorings in `m_messageCache` as needed. + * + * This method is normally called from the traffic worker thread. Test tools may also call it from + * other threads. + */ + void OnTrafficDataUpdate(); + + /** + * @brief Updates `activeMwms` and requests traffic data. + * + * The old and new list of active MWMs may refer either to those used by the rendering engine + * (`m_lastDrapeMwmsByRect`/`m_activeDrapeMwms`) or to those around the current position. + * (`m_lastPositionMwmsByRect`/`m_activePositionMwms`). + * + * The method first determines the list of MWMs overlapping with `rect`. If it is identical to + * `lastMwmsByRect`, the method returns immediately. Otherwise, it stores the new set in + * `lastMwmsByRect` and populates `activeMwms` with the elements. + * + * This method locks `m_mutex` while populating `activeMwms`. There is no need for the caller to + * do that. + * + * @param rect Rectangle covering the new active MWM set. + * @param lastMwmsByRect Set of active MWMs, see description. + * @param activeMwms Vector of active MWMs, see description. + */ void UpdateActiveMwms(m2::RectD const & rect, std::vector & lastMwmsByRect, std::set & activeMwms); // This is a group of methods that haven't their own synchronization inside. - void RequestTrafficData(); - void RequestTrafficData(MwmSet::MwmId const & mwmId, bool force); - - void Clear(); - void ClearCache(MwmSet::MwmId const & mwmId); - void ShrinkCacheToAllowableSize(); - void UpdateState(); void ChangeState(TrafficState newState); bool IsInvalidState() const; + void OnChangeRoutingSessionState(routing::SessionState previous, routing::SessionState current); + + /** + * @brief Retrieves all currently active MWMs. + * + * This method retrieves all MWMs in the viewport, within a certain distance of the current + * position (if there is a valid position) or part of the route (if any), and stores them in + * `activeMwms`. + * + * The caller must hold `m_mutex` prior to calling this method. `GetActiveMwms()` is available + * as a convenience wrapper which locks `m_mutex`, calls this method and releases it. + * + * @param activeMwms Retrieves the list of active MWMs. + */ void UniteActiveMwms(std::set & activeMwms) const; + /** + * @brief Pauses the traffic manager. + * + * Upon creation, the traffic manager is not paused. + * + * While the traffic manager is paused and no route is active, the traffic manager will not update + * its subscription area (upon resuming, it will do so if necessary). It will not poll any sources + * or process any messages. Feeds added via `ReceiveFeed()` will be added to the queue but will + * not be processed until the traffic manager is resumed. + * + * Pausing and resuming is similar in effect to disabling and enabling the traffic manager, except + * it does not change the external state, and an active route effectively overrides the paused + * state. It is intended for internal use by the framework. + */ void Pause(); + + /** + * @brief Resumes the traffic manager. + * + * Upon creation, the traffic manager is not paused. Resuming a traffic manager that is not paused + * is a no-op. + * + * Upon resume, the traffic manager will recalculate its subscription area and change its + * subscription if necessary. It will continue processing feeds in the queue, including those + * received before or while the traffic manager was paused. + * + * Pausing and resuming is similar in effect to disabling and enabling the traffic manager, except + * it does not change the external state, and an active route effectively overrides the paused + * state. It is intended for internal use by the framework. + */ void Resume(); + template + void ForEachMwm(F && f) const + { + std::vector> allMwmInfo; + m_dataSource.GetMwmsInfo(allMwmInfo); + std::for_each(allMwmInfo.begin(), allMwmInfo.end(), std::forward(f)); + } + template void ForEachActiveMwm(F && f) const { @@ -144,44 +535,233 @@ class TrafficManager final std::for_each(activeMwms.begin(), activeMwms.end(), std::forward(f)); } + /** + * @brief Whether updates to the observer are currently inhibited. + * + * Updates are inhibited while a route calculation is in progress. In this state, the observer + * receives traffic updates only if the queue has run empty, not if nore locations are waiting + * to be decoded. + * + * Inhibtiting the observer is necessary as traffic updates during route calculation will cause + * it to restart from scratch. Once the route has been calculated, updates will trigger a + * recalculation, which is much faster (seconds or less). + */ + bool IsObserverInhibited() const { return (m_routingSessionState == routing::SessionState::RouteBuilding) + || (m_routingSessionState == routing::SessionState::RouteRebuilding); } + + /** + * @brief Whether we are currently routing. + */ + bool IsRouting() const { return m_routingSessionState != routing::SessionState::NoValidRoute; } + + /** + * @brief Whether the traffic manager is paused and not routing. + * + * This is used to inhibit polling and message decoding. + */ + bool IsPausedAndNotRouting() const { return m_isPaused && !IsRouting(); } + + DataSource & m_dataSource; + CountryInfoGetterFn m_countryInfoGetterFn; + CountryParentNameGetterFn m_countryParentNameGetterFn; GetMwmsByRectFn m_getMwmsByRectFn; - traffic::TrafficObserver & m_observer; + + /* + * Originally this was m_observer, of type traffic::TrafficObserver. Since routing::RoutingSession + * inherits from that class, and an interface to the routing session is needed in order to + * determine the MWMs for which we need traffic information, the type was changed and the member + * renamed to reflect that. + */ + routing::RoutingSession & m_routingSession; + + /** + * @brief Cached state of the routing session. + * + * `m_routingSession` methods which query the state may only be called from the GUI thread, + * therefore we are caching this value when we get notified of a change. + */ + routing::SessionState m_routingSessionState = routing::SessionState::NoValidRoute; df::DrapeEngineSafePtr m_drapeEngine; std::atomic m_currentDataVersion; // These fields have a flag of their initialization. + /* + * The lazy ones get updated only if they are not initialized, or if their new position is more + * than a certain distance from the previously stored one. + */ std::pair m_currentPosition = {MyPosition(), false}; + std::pair m_currentPositionLazy = m_currentPosition; std::pair m_currentModelView = {ScreenBase(), false}; + std::pair m_currentModelViewLazy = m_currentModelView; + + /** + * The mode in which the traffic manager is running. + */ + Mode m_mode = Mode::Normal; + + /** + * Whether the traffic manager accepts mode changes. + * + * Mode cannot be set after the traffic manager has been enabled for the first time. + */ + bool m_canSetMode = true; std::atomic m_state; TrafficStateChangedFn m_onStateChangedFn; bool m_hasSimplifiedColorScheme = true; - size_t m_maxCacheSizeBytes; - size_t m_currentCacheSizeBytes = 0; - - std::map m_mwmCache; + /** + * @brief The TraFF sources from which we get traffic information. + * + * Threads must lock `m_trafficSourceMutex` prior to accessing this member. + */ + std::vector> m_trafficSources; bool m_isRunning; std::condition_variable m_condition; + /* + * To determine for which MWMs we need traffic data, we need to keep track of 3 groups of MWMs: + * those used by the renderer (i.e. in or just around the viewport), those within a certain area + * around the current position, and those used by the routing engine (only if currently routing). + * + * Routing MWMs are stored as a set. + * + * The other groups are stored twice: as a set and as a vector. The set always holds the MWMs which + * were last seen in use. Both get updated together when active MWMs are added or removed. + * However, the vector is used as a reference to detect changes. Clear() clears the vector but not + * the set, invalidating the set without destroying its contents. + * + * Methods which use only the set: + * + * * RequestTrafficSubscription(), exits if empty, otherwise cycles through the set. + * * UniteActiveMwms(), build the list of active MWMs (used by RequestTrafficSubscription()). + * + * Methods which use both, but in a different way: + * + * * UpdateActiveMwms(), uses the vector to detect changes (not for routing MWMs). If so, it + * updates both vector and set, but adds MWMs to the set only if they are alive. + */ std::vector m_lastDrapeMwmsByRect; std::set m_activeDrapeMwms; - std::vector m_lastRoutingMwmsByRect; + std::vector m_lastPositionMwmsByRect; + std::set m_activePositionMwms; std::set m_activeRoutingMwms; - // The ETag or entity tag is part of HTTP, the protocol for the World Wide Web. - // It is one of several mechanisms that HTTP provides for web cache validation, - // which allows a client to make conditional requests. - std::map m_trafficETags; + /** + * @brief Whether active MWMs have changed since the last request. + */ + bool m_activeMwmsChanged = false; std::atomic m_isPaused; - std::vector m_requestedMwms; + /** + * @brief Mutex for access to shared members. + * + * Threads which access shared members (see documentation) must lock this mutex while doing so. + * + * @note To access `m_trafficSource`, lock `m_trafficSourceMutex`, not this mutex. + */ std::mutex m_mutex; + + /** + * @brief Mutex for access to `m_trafficSources`. + * + * Threads which access `m_trafficSources` must lock this mutex while doing so. + */ + std::mutex m_trafficSourceMutex; + + /** + * @brief Worker thread which fetches traffic updates. + */ threads::SimpleThread m_thread; + + /** + * @brief When the last response was received. + */ + std::chrono::time_point m_lastResponseTime; + + /** + * @brief When the last update notification to the Drape engine was posted. + */ + std::chrono::time_point m_lastDrapeUpdate; + + /** + * @brief When the last update notification to the traffic observer was posted. + */ + std::chrono::time_point m_lastObserverUpdate; + + /** + * @brief When the cache file was last updated. + */ + std::chrono::time_point m_lastStorageUpdate; + + /** + * @brief Whether a poll operation is needed. + * + * Used in the worker thread to indicate we need to poll all sources. The poll operation may still + * be inhibited for individual sources. + */ + bool m_isPollNeeded; + + /** + * @brief Queue of feeds waiting to be processed. + * + * Threads must lock `m_mutex` before accessing `m_feedQueue`, as some platforms may receive feeds + * on multiple threads. + */ + std::vector m_feedQueue; + + /** + * @brief Whether the feed queue needs to be resorted. + * + * Resorting is needed when a new feed is added, or the current position or the viewport center + * has changed by more than a certain threshold. + */ + std::atomic m_isFeedQueueSortInvalid = false; + + /** + * @brief Cache of all currently active TraFF messages. + * + * Keys are message IDs, values are messages. + * + * Threads must lock `m_mutex` before accessing `m_messageCache`, as access can happen from + * multiple threads (messages are added by the worker thread, `Clear()` can be called from the UI + * thread). + */ + std::map m_messageCache; + + /** + * @brief The storage instance. + * + * Used to persist the TraFF message cache between sessions. + */ + std::unique_ptr m_storage; + + /** + * @brief The TraFF decoder instance. + * + * Used to decode TraFF locations into road segments on the map. + */ + std::unique_ptr m_traffDecoder; + + /** + * @brief Map between MWM IDs and their colorings. + * + * Threads must lock `m_mutex` before accessing `m_allMwmColoring`, as access can happen from + * multiple threads (messages are added by the worker thread, `Clear()` can be called from the UI + * thread). + */ + std::map m_allMwmColoring; + + /** + * @brief Callback function which gets called on traffic updates. + * + * Intended for testing. + */ + std::optional m_trafficUpdateCallbackFn; }; extern std::string DebugPrint(TrafficManager::TrafficState state); diff --git a/libs/map/user_mark.hpp b/libs/map/user_mark.hpp index a9ff23a9b..71fbced5d 100644 --- a/libs/map/user_mark.hpp +++ b/libs/map/user_mark.hpp @@ -36,20 +36,61 @@ class UserMark : public df::UserPointMark RoadWarningFirstFerry, }; + /** + * @brief User mark types. + * + * `UserMark` subclasses are assigned a value from this enum. + */ enum Type : uint32_t { + /** + * `Bookmark` + */ BOOKMARK, // Should always be the first one + /** + * `ApiMarkPoint` + */ API, + /** + * `SearchMarkPoint` + */ SEARCH, + /** + * `StaticMarkPoint` + */ STATIC, + /** + * `RouteMarkPoint` + */ ROUTING, + /** + * `SpeedCameraMark` + */ SPEED_CAM, + /** + * `RoadWarningMark` + */ ROAD_WARNING, + /** + * `TransitMark` + */ TRANSIT, LOCAL_ADS, + /** + * `TrackInfoMark` + */ TRACK_INFO, + /** + * `TrackSelectionMark` + */ TRACK_SELECTION, + /** + * `DebugMarkPoint` + */ DEBUG_MARK, // Plain "DEBUG" results in a name collision. + /** + * `ColoredMarkPoint` + */ COLORED, USER_MARK_TYPES_COUNT, USER_MARK_TYPES_COUNT_MAX = 1000, @@ -133,6 +174,9 @@ class MyPositionMarkPoint : public StaticMarkPoint bool m_hasPosition = false; }; +/** + * @brief A mark in the shape of a dot. + */ class DebugMarkPoint : public UserMark { public: @@ -141,6 +185,9 @@ class DebugMarkPoint : public UserMark drape_ptr GetSymbolNames() const override; }; +/** + * @brief A mark in the shape of a dot, of caller-defined color and radius. + */ class ColoredMarkPoint : public UserMark { public: diff --git a/libs/platform/local_country_file.hpp b/libs/platform/local_country_file.hpp index 20a8495d3..3f95349c6 100644 --- a/libs/platform/local_country_file.hpp +++ b/libs/platform/local_country_file.hpp @@ -13,53 +13,88 @@ namespace platform { -// This class represents a path to disk files corresponding to some -// country region. -// -// This class also wraps World.mwm and WorldCoasts.mwm -// files from resource bundle, when they can't be found in a data -// directory. In this exceptional case, directory will be empty and -// SyncWithDisk()/DeleteFromDisk()/GetPath()/GetSize() will return -// incorrect results. -// -// In any case, when you're going to read a file LocalCountryFile points to, -// use platform::GetCountryReader(). +/** + * @brief Represents a path to disk files corresponding to some country region. + * + * This class also wraps World.mwm and WorldCoasts.mwm files from resource bundle, when they can't + * be found in a data directory. In this exceptional case, directory will be empty and + * `SyncWithDisk()`/`DeleteFromDisk()`/`GetPath()`/`GetSize()` will return incorrect results. + * + * In any case, when you're going to read a file LocalCountryFile points to, use + * `platform::GetCountryReader()`. + */ class LocalCountryFile { public: LocalCountryFile(); - // Creates an instance holding a path to countryFile's in a - // directory. Note that no disk operations are not performed until - // SyncWithDisk() is called. - // The directory must contain a full path to the country file. + /** + * @brief Creates an instance holding a path to countryFile's in a directory. + * + * Note that no disk operations are performed until `SyncWithDisk()` is called. + * + * @param directory full path to the country file + * @param countryFile + * @param version + */ LocalCountryFile(std::string directory, CountryFile countryFile, int64_t version); - // Syncs internal state like availability of files, their sizes etc. with disk. - // Generality speaking it's not always true. To know it for sure it's necessary to read a mwm in - // this method but it's not implemented by performance reasons. This check is done on - // building routes stage. + /** + * @brief Syncs internal state like availability of files, their sizes etc. with disk. + * + * Generality speaking it's not always true. To know it for sure it's necessary to read a mwm in + * this method but it's not implemented by performance reasons. This check is done on + * building routes stage. + */ void SyncWithDisk(); - // Removes specified file from disk if it is known for LocalCountryFile, i.e. - // it was found by a previous SyncWithDisk() call. + /** + * @brief Deletes a file from disk. + * + * Removes the specified file from disk for `LocalCountryFile`, if it is known, i.e. it was found + * by a previous SyncWithDisk() call. + * @param type + */ void DeleteFromDisk(MapFileType type) const; - // Returns path to a file. - // Return value may be empty until SyncWithDisk() is called. + /** + * @brief Returns the path to a file. + * + * Return value may be empty until SyncWithDisk() is called. + * + * @param type + * @return + */ std::string GetPath(MapFileType type) const; std::string GetFileName(MapFileType type) const; - // Returns size of a file. - // Return value may be zero until SyncWithDisk() is called. + /** + * @brief Returns the size of a file. + * + * Return value may be zero until SyncWithDisk() is called. + * + * @param type + * @return + */ uint64_t GetSize(MapFileType type) const; - // Returns true when some files are found during SyncWithDisk. - // Return value is false until SyncWithDisk() is called. + /** + * @brief Returns true when files are found during `SyncWithDisk()`. + * + * Return value is false until `SyncWithDisk()` is called. + * + * @return + */ bool HasFiles() const; - // Checks whether files specified in filesMask are on disk. - // Return value will be false until SyncWithDisk() is called. + /** + * @brief Checks whether files specified in filesMask are on disk. + * + * Return value will be false until SyncWithDisk() is called. + * + * @param type + * @return + */ bool OnDisk(MapFileType type) const; bool IsInBundle() const { return m_directory.empty(); } @@ -74,8 +109,17 @@ class LocalCountryFile bool ValidateIntegrity() const; - // Creates LocalCountryFile for test purposes, for a country region - // with countryFileName (without any extensions). Automatically performs sync with disk. + // + /** + * @brief Creates a `LocalCountryFile` for test purposes. + * + * Creates a `LocalCountryFile` for test purposes, for a country region with `countryFileName`. + * Automatically performs sync with disk. + * + * @param countryFileName The filename, without any extension. + * @param version The data version. + * @return + */ static LocalCountryFile MakeForTesting(std::string countryFileName, int64_t version = 0); // Used in generator only to simplify getting instance from path. diff --git a/libs/platform/mwm_version.hpp b/libs/platform/mwm_version.hpp index f58944f24..dc69ce80c 100644 --- a/libs/platform/mwm_version.hpp +++ b/libs/platform/mwm_version.hpp @@ -13,6 +13,20 @@ DECLARE_EXCEPTION(CorruptedMwmFile, RootException); namespace version { +/** + * @brief The MWM format version. + * + * This is the global versioning for the MWM format. Some structures in the MWM file may have their + * own versioning in addition to the MWM format version. + * + * When a new version is introduced, add a new member to this `enum` and bump `lastFormat` to the + * new version. + * + * @todo Document where checks for the format version happen, and criteria for adding or not adding + * a new version when changes to the format are made. See #4414. + * + * Versions up to v11 were introduced by Maps.me; v10 and earlier were deprecated by OrganicMaps. + */ enum class Format { unknownFormat = -1, @@ -30,7 +44,8 @@ enum class Format // header, sdx section with header, dat section renamed to features, features section with // header). v11, // September 2020 (compressed string storage for metadata). - lastFormat = v11 + v12, // June 2026 (additional vehicle model DecoderModel, construction types allowed in routing section). + lastFormat = v12 }; std::string DebugPrint(Format f); diff --git a/libs/routing/absent_regions_finder.cpp b/libs/routing/absent_regions_finder.cpp index d3267d6df..25d335cd8 100644 --- a/libs/routing/absent_regions_finder.cpp +++ b/libs/routing/absent_regions_finder.cpp @@ -17,6 +17,9 @@ AbsentRegionsFinder::AbsentRegionsFinder(CountryFileGetterFn const & countryFile void AbsentRegionsFinder::GenerateAbsentRegions(Checkpoints const & checkpoints, RouterDelegate const & delegate) { + std::lock_guard lock(m_mutex); + m_regions.clear(); + if (m_routerThread) { m_routerThread->Cancel(); @@ -48,18 +51,22 @@ void AbsentRegionsFinder::GetAbsentRegions(std::set & regions) void AbsentRegionsFinder::GetAllRegions(std::set & countries) { - countries.clear(); - - if (!m_routerThread) - return; + std::lock_guard lock(m_mutex); + // Note: if called from `RoutingSession` callback, m_state will still have its pre-update value. + if (m_routerThread) + { + m_routerThread->Join(); - m_routerThread->Join(); + for (auto const & mwmName : m_routerThread->GetRoutineAs()->GetMwmNames()) + { + if (!mwmName.empty()) + m_regions.emplace(mwmName); + } - for (auto const & mwmName : m_routerThread->GetRoutineAs()->GetMwmNames()) - if (!mwmName.empty()) - countries.emplace(mwmName); + m_routerThread.reset(); + } - m_routerThread.reset(); + countries = m_regions; } bool AbsentRegionsFinder::AreCheckpointsInSameMwm(Checkpoints const & checkpoints) const diff --git a/libs/routing/absent_regions_finder.hpp b/libs/routing/absent_regions_finder.hpp index 4a2d8734f..6f5d42611 100644 --- a/libs/routing/absent_regions_finder.hpp +++ b/libs/routing/absent_regions_finder.hpp @@ -14,19 +14,45 @@ namespace routing { using LocalFileCheckerFn = std::function; -// Encapsulates generation of mwm names of absent regions needed for building the route between -// |checkpoints|. For this purpose the new thread is used. +/** + * @brief Generates a list of MWMs needed to build a route. + * + * The `AbsentRegionsFinder` class encapsulates generation of MWM names of absent regions needed + * for building the route between `checkpoints`. For this purpose a separate worker thread is used. + */ class AbsentRegionsFinder { public: AbsentRegionsFinder(CountryFileGetterFn const & countryFileGetter, LocalFileCheckerFn const & localFileChecker, std::shared_ptr numMwmIds, DataSource & dataSource); - // Creates new thread |m_routerThread| and starts routing in it. + /** + * @brief Creates new thread `m_routerThread` and starts routing in it. + * @param checkpoints The checkpoints of the route (start, optional intermediate points, destination) + * @param delegate + */ void GenerateAbsentRegions(Checkpoints const & checkpoints, RouterDelegate const & delegate); - // Waits for the routing thread |m_routerThread| to finish and returns results from it. + + /** + * @brief Retrieves the MWMs needed to build the route. + * + * When called for the first time after `GenerateAbsentRegions()`, this method waits for the + * routing thread `m_routerThread` to finish and returns results from it. Results are cached and + * subsequent calls are served from the cache. + * + * @param countries Receives the list of MWM names. + */ void GetAllRegions(std::set & countries); - // Waits for the results from GetAllRegions() and returns only regions absent on the device. + + /** + * @brief Retrieves the missing MWMs needed to build the route. + * + * This calls `GetAllRegions()` and strips from the result all regions already present on the + * device, leaving only the missing ones. If the call to `GetAllRegions()` is the first one after + * calling `GenerateAbsentRegions()`, this involves waiting for the router thread to finish. + * + * @param absentCountries Receives the list of missing MWM names. + */ void GetAbsentRegions(std::set & absentCountries); private: @@ -39,5 +65,20 @@ class AbsentRegionsFinder DataSource & m_dataSource; std::unique_ptr m_routerThread; + + /** + * @brief Mutex for access to member variables. + * + * Methods which access any of the non-`const` class member variables must lock this mutex while + * doing so. + */ + std::mutex m_mutex; + + /** + * @brief Regions required for building the last route. + * + * This member is cleared by `GenerateAbsentRegions()` and populated by `GetAllRegions()`. + */ + std::set m_regions; }; } // namespace routing diff --git a/libs/routing/async_router.cpp b/libs/routing/async_router.cpp index cbd9036ea..b129a3d72 100644 --- a/libs/routing/async_router.cpp +++ b/libs/routing/async_router.cpp @@ -80,6 +80,13 @@ bool AsyncRouter::FindClosestProjectionToRoad(m2::PointD const & point, m2::Poin return m_router->FindClosestProjectionToRoad(point, direction, radius, proj); } +void AsyncRouter::GetAllRegions(std::set & countries) +{ + if (!m_absentRegionsFinder) + return; + m_absentRegionsFinder->GetAllRegions(countries); +} + void AsyncRouter::RouterDelegateProxy::OnProgress(float progress) { ProgressCallback onProgress = nullptr; diff --git a/libs/routing/async_router.hpp b/libs/routing/async_router.hpp index 20209914d..81217bbcd 100644 --- a/libs/routing/async_router.hpp +++ b/libs/routing/async_router.hpp @@ -23,11 +23,15 @@ namespace routing { -/// Dispatches a route calculation on a worker thread +/** + * @brief The AsyncRouter class is a wrapper class to run routing routines in a different thread. + * + * It encapsulates an `IRouter` (or subclass) instance, set with `SetRouter()`, and runs it in a + * separate worker thread to calculate the route. + */ class AsyncRouter final { public: - /// AsyncRouter is a wrapper class to run routing routines in the different thread AsyncRouter(PointCheckCallback const & pointCheckCallback); ~AsyncRouter(); @@ -59,6 +63,15 @@ class AsyncRouter final bool FindClosestProjectionToRoad(m2::PointD const & point, m2::PointD const & direction, double radius, EdgeProj & proj); + /** + * @brief Retrieves the MWMs needed to build the route. + * + * Waits for the routing thread to finish and returns the list of MWM names from it. + * + * @param countries Receives the list of MWM names. + */ + void GetAllRegions(std::set & countries); + private: /// Worker thread function void ThreadFunc(); diff --git a/libs/routing/directions_engine.cpp b/libs/routing/directions_engine.cpp index 321654452..ac924524f 100644 --- a/libs/routing/directions_engine.cpp +++ b/libs/routing/directions_engine.cpp @@ -85,6 +85,7 @@ void DirectionsEngine::LoadPathAttributes(FeatureID const & featureId, LoadedPat pathSegment.m_isOneWay = m_onewayChecker(types); pathSegment.m_roadNameInfo.m_isLink = pathSegment.m_isLink; + pathSegment.m_roadNameInfo.m_onRoundabout = pathSegment.m_onRoundabout; pathSegment.m_roadNameInfo.m_junction_ref = ft->GetMetadata(feature::Metadata::FMD_JUNCTION_REF); pathSegment.m_roadNameInfo.m_destination_ref = ft->GetMetadata(feature::Metadata::FMD_DESTINATION_REF); pathSegment.m_roadNameInfo.m_destination = ft->GetMetadata(feature::Metadata::FMD_DESTINATION); diff --git a/libs/routing/directions_engine.hpp b/libs/routing/directions_engine.hpp index c7dc61840..c5babdb30 100644 --- a/libs/routing/directions_engine.hpp +++ b/libs/routing/directions_engine.hpp @@ -33,9 +33,18 @@ class DirectionsEngine // @TODO(bykoianko) Method Generate() should fill // vector instead of corresponding arguments. - /// \brief Generates all args which are passed by reference. - /// \param path is points of the route. It should not be empty. - /// \returns true if fields passed by reference are filled correctly and false otherwise. + /** + * @brief Calculates segments from a path on a route graph. + * + * Segments are calculated from `graph` (the route graph) and `path` (points on the route); each + * pair of consecutive points becomes a segment. + * + * @param graph The route graph + * @param path The route path, an ordered list of points on the route + * @param cancellable + * @param routeSegments Receives the list of segments + * @return true on successful completion, false if cancelled or an error occurred + */ bool Generate(IndexRoadGraph const & graph, std::vector const & path, base::Cancellable const & cancellable, std::vector & routeSegments); void Clear(); diff --git a/libs/routing/edge_estimator.cpp b/libs/routing/edge_estimator.cpp index ed9df5336..881a94a53 100644 --- a/libs/routing/edge_estimator.cpp +++ b/libs/routing/edge_estimator.cpp @@ -826,6 +826,11 @@ shared_ptr EdgeEstimator::Create(VehicleType vehicleType, double case VehicleType::Bicycle: return make_shared(maxWeighSpeedKMpH, offroadSpeedKMpH); case VehicleType::Car: return make_shared(dataSourcePtr, numMwmIds, trafficStash, maxWeighSpeedKMpH, offroadSpeedKMpH); + /* + * VehicleType::Decoder is for use with the TraFF decoder, which creates its EdgeEstimator by + * explicitly calling the constructor for the appropriate subclass. + */ + case VehicleType::Decoder: CHECK(false, ("Creating EdgeEstimator for Decoder is not supported")); return nullptr; case VehicleType::Count: CHECK(false, ("Can't create EdgeEstimator for", vehicleType)); return nullptr; } UNREACHABLE(); diff --git a/libs/routing/edge_estimator.hpp b/libs/routing/edge_estimator.hpp index 4729f9d3f..f224c44c8 100644 --- a/libs/routing/edge_estimator.hpp +++ b/libs/routing/edge_estimator.hpp @@ -23,41 +23,173 @@ class TrafficStash; class EdgeEstimator { public: + /** + * @brief The purpose for which cost calculations are to be used. + * + * A number of cost estimation functions take `Purpose` as an argument and may return different + * values depending on the value of that argument. + */ enum class Purpose { + /** + * @brief Indicates that cost calculations are for the purpose of choosing the best route. + */ Weight, + /** + * @brief Indicates that cost calculations are for the purpose of calculating the estimated time + * of arrival. + */ ETA }; + /** + * @brief Constructs a new `EdgeEstimator`. + * + * @param vehicleType The vehicle type. + * @param maxWeightSpeedKMpH The maximum speed for the vehicle on a road. + * @param offroadSpeedKMpH The maximum speed for the vehicle on an off-road link. + * @param dataSourcePtr + * @param numMwmIds + */ EdgeEstimator(VehicleType vehicleType, double maxWeightSpeedKMpH, SpeedKMpH const & offroadSpeedKMpH, DataSource * dataSourcePtr = nullptr, std::shared_ptr numMwmIds = nullptr); virtual ~EdgeEstimator() = default; + /** + * @brief Calculates the heuristic for two points. + * + * The heuristic is used by the A* routing algorithm when choosing the next point to examine. It + * must be less than, or equal to, the lowest possible cost of traveling from one point to the + * other. Zero is an admissible heuristic, but effectively downgrades the A* algorithm to behave + * exactly like the Dijkstra algorithm, of which A* is an improved version. A good heuristic is as + * close as possible to the actual cost, without violating the aforementioned requirement. + * + * @param from The start point for the part of the route for which the heuristic is to be calculated. + * @param to The destination point for the part of the route for which the heuristic is to be calculated. + * @return The heuristic, expressed as travel time in seconds. + */ double CalcHeuristic(ms::LatLon const & from, ms::LatLon const & to) const; - // Estimates time in seconds it takes to go from point |from| to point |to| along a leap (fake) - // edge |from|-|to| using real features. - // Note 1. The result of the method should be used if it's necessary to add a leap (fake) edge - // (|from|, |to|) in road graph. - // Note 2. The result of the method should be less or equal to CalcHeuristic(|from|, |to|). - // Note 3. It's assumed here that CalcLeapWeight(p1, p2) == CalcLeapWeight(p2, p1). + + /** + * @brief Estimates travel time between two points along a leap (fake) edge using real features. + * + * Estimates time in seconds it takes to go from point `from` to point `to` along a leap (fake) + * edge `from`-`to` using real features. + * + * Note 1. The result of the method should be used if it is necessary to add a leap (fake) edge + * (`from`, `to`) in road graph. + * + * Note 2. The result of the method should be less or equal to `CalcHeuristic(from, to)`. + * + * Note 3. It is assumed here that `CalcLeapWeight(p1, p2) == CalcLeapWeight(p2, p1)`. + * + * @todo Note 2 looks like a typo, presumably the result of this method should be no less than the + * heuristic (otherwise the heuristic might not satisfy the requirements of A*). + * + * @param from The start point. + * @param to The destination point. + * @param mwmId + * @return Travel time in seconds. + */ double CalcLeapWeight(ms::LatLon const & from, ms::LatLon const & to, NumMwmId mwmId = kFakeNumMwmId); + /** + * @brief Returns the maximum speed this `EdgeEstimator` instance assumes for any road. + * @return The speed in m/s. + */ double GetMaxWeightSpeedMpS() const; - // Estimates time in seconds it takes to go from point |from| to point |to| along direct fake edge. - double CalcOffroad(ms::LatLon const & from, ms::LatLon const & to, Purpose purpose) const; + /** + * @brief Estimates travel time between two points along a direct fake edge. + * + * Estimates time in seconds it takes to go from point `from` to point `to` along direct fake edge. + * + * @param from The start point. + * @param to The destination point. + * @param purpose The purpose for which the result is to be used. + * @return Travel time in seconds. + */ + virtual double CalcOffroad(ms::LatLon const & from, ms::LatLon const & to, Purpose purpose) const; + /** + * @brief Returns the travel time along a segment. + * + * @param segment The segment. + * @param road The road geometry (speed, restrictions, points) for the road which the segment is a part of. + * @param purpose The purpose for which the result is to be used. + * @return Travel time in seconds. + */ virtual double CalcSegmentWeight(Segment const & segment, RoadGeometry const & road, Purpose purpose) const = 0; + + /** + * @brief Returns the penalty for making a U turn. + * + * The penalty is a fixed amount of time, determined by the implementation. + * + * U turns are determined and the penalty is applied in `IndexGraph::GetPenalties()`, + * in (`index_graph.cpp`). Actual U turn detection is deferred to `IsUTurn()` in the same file. + * + * @param purpose The purpose for which the result is to be used. + * @return The penalty in seconds. + */ virtual double GetUTurnPenalty(Purpose purpose) const = 0; + virtual double GetTurnPenalty(Purpose purpose, double angle, RoadGeometry const & from_road, RoadGeometry const & to_road, bool is_left_hand_traffic = false) const = 0; + + /** + * @brief Returns the penalty for using a ferry or rail transit link. + * + * The penalty is a fixed amount of time, determined by the implementation. It applies once per + * link, hence it needs to cover the sum of the time for boarding and unboarding. + * + * @param purpose The purpose for which the result is to be used. + * @return The penalty in seconds. + */ virtual double GetFerryLandingPenalty(Purpose purpose) const = 0; + /** + * @brief Whether access restrictions are ignored. + * + * A return value of false indicates that access restrictions should be observed, which is the + * default behavior for a routing use case. If true, it indicates that routing should ignore + * access restrictions. This is needed to resolve traffic message locations; it could also be + * used e.g. for emergency vehicle use cases. + * + * This implementation always returns false. + */ + virtual bool IsAccessIgnored() const { return false; } + + /** + * @brief Creates an `EdgeEstimator` based on maximum speeds. + * + * @param vehicleType The vehicle type. + * @param maxWeighSpeedKMpH The maximum speed for the vehicle on a road. + * @param offroadSpeedKMpH The maximum speed for the vehicle on an off-road link. + * @param trafficStash The traffic stash (used only for some vehicle types). + * @param dataSourcePtr + * @param numMwmIds + * @return The `EdgeEstimator` instance. + */ static std::shared_ptr Create(VehicleType vehicleType, double maxWeighSpeedKMpH, SpeedKMpH const & offroadSpeedKMpH, std::shared_ptr trafficStash, DataSource * dataSourcePtr, std::shared_ptr numMwmIds); + /** + * @brief Creates an `EdgeEstimator` based on a vehicle model. + * + * This is a convenience wrapper around `Create(VehicleType, double, SpeedKMpH const &, + * std::shared_ptr, DataSource *, std::shared_ptr)`, which takes a + * `VehicleModel` and derives the maximum speeds for the vehicle from that. + * + * @param vehicleType The vehicle type. + * @param vehicleModel + * @param trafficStash The traffic stash (used only for some vehicle types). + * @param dataSourcePtr + * @param numMwmIds + * @return The `EdgeEstimator` instance. + */ static std::shared_ptr Create(VehicleType vehicleType, VehicleModelInterface const & vehicleModel, std::shared_ptr trafficStash, DataSource * dataSourcePtr, std::shared_ptr numMwmIds); @@ -75,12 +207,54 @@ class EdgeEstimator // std::shared_ptr m_numMwmIds; // ankerl::unordered_dense::map m_leapWeightSpeedMpS; + /** + * @brief Computes the default speed for leap (fake) segments. + * + * The result is used by `GetLeapWeightSpeed()`. + * + * @return Speed in m/s. + */ double ComputeDefaultLeapWeightSpeed() const; + + /** + * @brief Returns the deafult speed for leap (fake) segments for a given MWM. + * @param mwmId + * @return Speed in m/s. + */ double GetLeapWeightSpeed(NumMwmId mwmId); // double LoadLeapWeightSpeed(NumMwmId mwmId); }; +/** + * @brief Calculates the climb penalty for pedestrians. + * + * The climb penalty is a factor which can be multiplied with the cost of an edge which goes uphill + * or downhill. The factor for no penalty is 1, i.e. the cost of the edge is not changed. + * + * The climb penalty may depend on the mode of transportation, the ascent or descent, as well as the + * altitude (allowing for different penalties at greater altitudes). + * + * @param purpose The purpose for which the result is to be used. + * @param tangent The tangent of the ascent or descent (10% would be 0.1 for ascent, -0.1 for descent). + * @param altitudeM The altitude in meters. + * @return The climb penalty, as a factor. + */ double GetPedestrianClimbPenalty(EdgeEstimator::Purpose purpose, double tangent, geometry::Altitude altitudeM); + +/** + * @brief Calculates the climb penalty for cyclists. + * + * The climb penalty is a factor which can be multiplied with the cost of an edge which goes uphill + * or downhill. The factor for no penalty is 1, i.e. the cost of the edge is not changed. + * + * The climb penalty may depend on the mode of transportation, the ascent or descent, as well as the + * altitude (allowing for different penalties at greater altitudes). + * + * @param purpose The purpose for which the result is to be used. + * @param tangent The tangent of the ascent or descent (10% would be 0.1 for ascent, -0.1 for descent). + * @param altitudeM The altitude in meters. + * @return The climb penalty, as a factor. + */ double GetBicycleClimbPenalty(EdgeEstimator::Purpose purpose, double tangent, geometry::Altitude altitudeM); } // namespace routing diff --git a/libs/routing/fake_ending.cpp b/libs/routing/fake_ending.cpp index 683bad7e3..4a32f439f 100644 --- a/libs/routing/fake_ending.cpp +++ b/libs/routing/fake_ending.cpp @@ -18,12 +18,12 @@ using namespace routing; using namespace std; LatLonWithAltitude CalcProjectionToSegment(LatLonWithAltitude const & begin, LatLonWithAltitude const & end, - m2::PointD const & point) + m2::PointD const & point, bool snapToEnds) { m2::ParametrizedSegment segment(mercator::FromLatLon(begin.GetLatLon()), mercator::FromLatLon(end.GetLatLon())); - auto const projectedPoint = segment.ClosestPointTo(point); + auto const projectedPoint = segment.ClosestPointTo(point, snapToEnds); auto const distBeginToEnd = ms::DistanceOnEarth(begin.GetLatLon(), end.GetLatLon()); auto const projectedLatLon = mercator::ToLatLon(projectedPoint); @@ -45,7 +45,8 @@ bool Projection::operator==(Projection const & other) const tie(other.m_segment, other.m_isOneWay, other.m_segmentFront, other.m_segmentBack, other.m_junction); } -FakeEnding MakeFakeEnding(vector const & segments, m2::PointD const & point, WorldGraph & graph) +FakeEnding MakeFakeEnding(vector const & segments, m2::PointD const & point, + WorldGraph & graph, bool snapToEnds) { FakeEnding ending; double averageAltitude = 0.0; @@ -57,7 +58,7 @@ FakeEnding MakeFakeEnding(vector const & segments, m2::PointD const & p bool const oneWay = graph.IsOneWay(segment.GetMwmId(), segment.GetFeatureId()); auto const & frontJunction = graph.GetJunction(segment, true /* front */); auto const & backJunction = graph.GetJunction(segment, false /* front */); - auto const & projectedJunction = CalcProjectionToSegment(backJunction, frontJunction, point); + auto const & projectedJunction = CalcProjectionToSegment(backJunction, frontJunction, point, snapToEnds); ending.m_projections.emplace_back(segment, oneWay, frontJunction, backJunction, projectedJunction); @@ -69,13 +70,14 @@ FakeEnding MakeFakeEnding(vector const & segments, m2::PointD const & p return ending; } -FakeEnding MakeFakeEnding(Segment const & segment, m2::PointD const & point, IndexGraph & graph) +FakeEnding MakeFakeEnding(Segment const & segment, m2::PointD const & point, IndexGraph & graph, + bool snapToEnds) { auto const & road = graph.GetRoadGeometry(segment.GetFeatureId()); bool const oneWay = road.IsOneWay(); auto const & frontJunction = road.GetJunction(segment.GetPointId(true /* front */)); auto const & backJunction = road.GetJunction(segment.GetPointId(false /* front */)); - auto const & projectedJunction = CalcProjectionToSegment(backJunction, frontJunction, point); + auto const & projectedJunction = CalcProjectionToSegment(backJunction, frontJunction, point, snapToEnds); FakeEnding ending; ending.m_originJunction = LatLonWithAltitude(mercator::ToLatLon(point), projectedJunction.GetAltitude()); diff --git a/libs/routing/fake_ending.hpp b/libs/routing/fake_ending.hpp index f12215603..5cc59a7d0 100644 --- a/libs/routing/fake_ending.hpp +++ b/libs/routing/fake_ending.hpp @@ -40,9 +40,11 @@ struct FakeEnding final std::vector m_projections; }; -FakeEnding MakeFakeEnding(std::vector const & segments, m2::PointD const & point, WorldGraph & graph); -FakeEnding MakeFakeEnding(Segment const & segment, m2::PointD const & point, IndexGraph & graph); +FakeEnding MakeFakeEnding(std::vector const & segments, m2::PointD const & point, + WorldGraph & graph, bool snapToEnds = false); +FakeEnding MakeFakeEnding(Segment const & segment, m2::PointD const & point, IndexGraph & graph, + bool snapToEnds = false); LatLonWithAltitude CalcProjectionToSegment(LatLonWithAltitude const & begin, LatLonWithAltitude const & end, - m2::PointD const & point); + m2::PointD const & point, bool snapToEnds = false); } // namespace routing diff --git a/libs/routing/features_road_graph.cpp b/libs/routing/features_road_graph.cpp index 13c894983..f0bd2ec08 100644 --- a/libs/routing/features_road_graph.cpp +++ b/libs/routing/features_road_graph.cpp @@ -143,9 +143,10 @@ void FeaturesRoadGraphBase::ForEachFeatureClosestToCross(m2::PointD const & cros } void FeaturesRoadGraphBase::FindClosestEdges(m2::RectD const & rect, uint32_t count, - vector> & vicinities) const + vector> & vicinities, + bool snapToEnds) const { - NearestEdgeFinder finder(rect.Center(), nullptr /* IsEdgeProjGood */); + NearestEdgeFinder finder(rect.Center(), nullptr /* IsEdgeProjGood */, snapToEnds); m_dataSource.ForEachStreet([&](FeatureType & ft) { diff --git a/libs/routing/features_road_graph.hpp b/libs/routing/features_road_graph.hpp index 712236bad..9a323ab3b 100644 --- a/libs/routing/features_road_graph.hpp +++ b/libs/routing/features_road_graph.hpp @@ -84,8 +84,21 @@ class FeaturesRoadGraphBase : public IRoadGraph /// @name IRoadGraph overrides /// @{ void ForEachFeatureClosestToCross(m2::PointD const & cross, ICrossEdgesLoader & edgesLoader) const override; + + /** + * @brief Finds the closest edges to a reference point within a given distance. + * + * @param rect A rectangle. Its center is the reference point; the search distance is expressed + * through the height and width. + * @param count The number of results to return. + * @param vicinities Receives the results. + * @param snapToEnds If true, the projection point (the point on the edge closest to the reference + * point) is constrained to one of the edge endpoints; if false, it can be anywhere on the edge. + */ void FindClosestEdges(m2::RectD const & rect, uint32_t count, - std::vector> & vicinities) const override; + std::vector> & vicinities, + bool snapToEnds = false) const override; + std::vector FindRoads(m2::RectD const & rect, IsGoodFeatureFn const & isGoodFeature) const override; void GetFeatureTypes(FeatureID const & featureId, feature::TypesHolder & types) const override; diff --git a/libs/routing/index_graph.hpp b/libs/routing/index_graph.hpp index 5b7db8980..56c0cec12 100644 --- a/libs/routing/index_graph.hpp +++ b/libs/routing/index_graph.hpp @@ -28,6 +28,20 @@ namespace routing { +/** + * @brief Whether a maneuver between two segments is a U turn. + * + * A maneuver between two segments `u` and `v` is a U turn if, and only if, both segments differ in + * direction but are otherwise identical. + * + * For U turns on multiple-carriageway roads, i.e. from one carriageway onto a crossing road (or + * connecting cariageway) and further to the opposite cariageway, this function returns false. + * + * @param u The first segment + * @param v The second segment + * + * @return True if the maneuver is a U turn on the same segment, false otherwise. + */ bool IsUTurn(Segment const & u, Segment const & v); enum class WorldGraphMode; @@ -218,6 +232,9 @@ template bool IndexGraph::IsAccessNoForSure(AccessPositionType const & accessPositionType, RouteWeight const & weight, bool useAccessConditional) const { + if (m_estimator->IsAccessIgnored()) + return false; + auto const [accessType, confidence] = useAccessConditional ? m_roadAccess.GetAccess(accessPositionType, weight) : m_roadAccess.GetAccessWithoutConditional(accessPositionType); diff --git a/libs/routing/index_graph_loader.cpp b/libs/routing/index_graph_loader.cpp index 56c7a47b2..959e7b811 100644 --- a/libs/routing/index_graph_loader.cpp +++ b/libs/routing/index_graph_loader.cpp @@ -227,7 +227,12 @@ bool ReadRoadPenaltyFromMwm(MwmValue const & mwmValue, VehicleType vehicleType, // Read number of vehicle types uint32_t numVehicleTypes = ReadPrimitiveFromSource(src); - CHECK_EQUAL(numVehicleTypes, static_cast(VehicleType::Count), ()); + if (numVehicleTypes <= static_cast(vehicleType) + && vehicleType == VehicleType::Decoder + && numVehicleTypes > static_cast(VehicleType::Car)) + // This is expected for older mwm files (up to v11) - not an error + vehicleType = VehicleType::Car; + CHECK(numVehicleTypes > static_cast(vehicleType), ()); // Skip to the correct vehicle type for (uint32_t i = 0; i < static_cast(vehicleType); ++i) diff --git a/libs/routing/index_router.cpp b/libs/routing/index_router.cpp index b524a99d5..37879ba2a 100644 --- a/libs/routing/index_router.cpp +++ b/libs/routing/index_router.cpp @@ -27,6 +27,7 @@ #include "routing_common/bicycle_model.hpp" #include "routing_common/car_model.hpp" +#include "routing_common/decoder_model.hpp" #include "routing_common/pedestrian_model.hpp" #include "indexer/data_source.hpp" @@ -104,6 +105,7 @@ shared_ptr CreateVehicleModelFactory( case VehicleType::Transit: return make_shared(countryParentNameGetterFn); case VehicleType::Bicycle: return make_shared(countryParentNameGetterFn); case VehicleType::Car: return make_shared(countryParentNameGetterFn); + case VehicleType::Decoder: return make_shared(countryParentNameGetterFn); case VehicleType::Count: CHECK(false, ("Can't create VehicleModelFactoryInterface for", vehicleType)); return nullptr; } UNREACHABLE(); @@ -117,16 +119,23 @@ unique_ptr CreateDirectionsEngine(VehicleType vehicleType, sha case VehicleType::Pedestrian: case VehicleType::Transit: return make_unique(dataSource, numMwmIds); case VehicleType::Bicycle: - case VehicleType::Car: return make_unique(dataSource, numMwmIds); + case VehicleType::Car: + case VehicleType::Decoder: + /* + * For `VehicleType::Decoder` the directions engine serves no useful purpose. + * We could return `nullptr` here, but we would have to go through every usage of + * `m_directionsEngine` and ensure it can handle a null pointer. + */ + return make_unique(dataSource, numMwmIds); case VehicleType::Count: CHECK(false, ("Can't create DirectionsEngine for", vehicleType)); return nullptr; } UNREACHABLE(); } -shared_ptr CreateTrafficStash(VehicleType, shared_ptr, traffic::TrafficCache const &) +shared_ptr CreateTrafficStash(VehicleType vehicleType, shared_ptr numMwmIds, + traffic::TrafficCache const & trafficCache) { - return nullptr; - // return (vehicleType == VehicleType::Car ? make_shared(trafficCache, numMwmIds) : nullptr); + return (vehicleType == VehicleType::Car ? make_shared(trafficCache, numMwmIds) : nullptr); } void PushPassedSubroutes(Checkpoints const & checkpoints, vector & subroutes) @@ -262,6 +271,37 @@ IndexRouter::IndexRouter(VehicleType vehicleType, bool loadAltitudes, CHECK(m_directionsEngine, ()); } +IndexRouter::IndexRouter(VehicleType vehicleType, bool loadAltitudes, + CountryParentNameGetterFn const & countryParentNameGetterFn, + TCountryFileFn const & countryFileFn, CountryRectFn const & countryRectFn, + shared_ptr numMwmIds, unique_ptr> numMwmTree, + std::shared_ptr estimator, DataSource & dataSource) + : m_vehicleType(vehicleType) + , m_loadAltitudes(loadAltitudes) + , m_name("astar-bidirectional-" + ToString(m_vehicleType)) + , m_dataSource(dataSource, numMwmIds) + , m_vehicleModelFactory(CreateVehicleModelFactory(m_vehicleType, countryParentNameGetterFn)) + , m_countryFileFn(countryFileFn) + , m_countryRectFn(countryRectFn) + , m_numMwmIds(std::move(numMwmIds)) + , m_numMwmTree(std::move(numMwmTree)) + , m_trafficStash(nullptr) + , m_roadGraph(m_dataSource, + vehicleType == VehicleType::Pedestrian || vehicleType == VehicleType::Transit + ? IRoadGraph::Mode::IgnoreOnewayTag + : IRoadGraph::Mode::ObeyOnewayTag, + m_vehicleModelFactory) + , m_estimator(estimator) + , m_directionsEngine(CreateDirectionsEngine(m_vehicleType, m_numMwmIds, m_dataSource)) + , m_countryParentNameGetterFn(countryParentNameGetterFn) +{ + CHECK(!m_name.empty(), ()); + CHECK(m_numMwmIds, ()); + CHECK(m_numMwmTree, ()); + CHECK(m_estimator, ()); + CHECK(m_directionsEngine, ()); +} + unique_ptr IndexRouter::MakeSingleMwmWorldGraph() { auto worldGraph = MakeWorldGraph(); @@ -282,8 +322,9 @@ bool IndexRouter::FindClosestProjectionToRoad(m2::PointD const & point, m2::Poin auto const rect = mercator::RectByCenterXYAndSizeInMeters(point, radius); std::vector candidates; + // TODO should we increase the count in decoding mode? uint32_t const count = direction.IsAlmostZero() ? 1 : 4; - m_roadGraph.FindClosestEdges(rect, count, candidates); + m_roadGraph.FindClosestEdges(rect, count, candidates, (GetMode() == Mode::Decoding)); if (candidates.empty()) return false; @@ -516,7 +557,7 @@ RouterResultCode IndexRouter::DoCalculateRoute(Checkpoints const & checkpoints, guidesMwmId = m_numMwmIds->GetId(country); } - if (!route.GetAbsentCountries().empty()) + if ((GetMode() == Mode::Navigation) && !route.GetAbsentCountries().empty()) return RouterResultCode::NeedMoreMaps; TrafficStash::Guard guard(m_trafficStash); @@ -549,7 +590,7 @@ RouterResultCode IndexRouter::DoCalculateRoute(Checkpoints const & checkpoints, FakeEnding startFakeEnding = m_guides.GetFakeEnding(i); FakeEnding finishFakeEnding = m_guides.GetFakeEnding(i + 1); - bool isStartSegmentStrictForward = (m_vehicleType == VehicleType::Car); + bool isStartSegmentStrictForward = ((m_vehicleType == VehicleType::Car) || (m_vehicleType == VehicleType::Decoder)); if (startFakeEnding.m_projections.empty() || finishFakeEnding.m_projections.empty()) { bool const isFirstSubroute = (i == checkpoints.GetPassedIdx()); @@ -1060,10 +1101,15 @@ RouterResultCode IndexRouter::AdjustRoute(Checkpoints const & checkpoints, m2::P return RouterResultCode::NoError; } +RoutingOptions IndexRouter::GetRoutingOptions() +{ + return RoutingOptions::LoadCarOptionsFromSettings(); +} + unique_ptr IndexRouter::MakeWorldGraph() { // Use saved routing options for all types (car, bicycle, pedestrian). - RoutingOptions const routingOptions = RoutingOptions::LoadCarOptionsFromSettings(); + RoutingOptions const routingOptions = GetRoutingOptions(); /// @DebugNote // Add avoid roads here for debug purpose. // routingOptions.Add(RoutingOptions::Road::Motorway); @@ -1110,10 +1156,10 @@ int IndexRouter::PointsOnEdgesSnapping::Snap(m2::PointD const & start, m2::Point // One of startEnding or finishEnding will be empty here. if (startEnding.m_projections.empty()) - startEnding = MakeFakeEnding(m_startSegments, start, m_graph); + startEnding = MakeFakeEnding(m_startSegments, start, m_graph, (m_router.GetMode() == Mode::Decoding)); if (finishEnding.m_projections.empty()) - finishEnding = MakeFakeEnding(finishSegments, finish, m_graph); + finishEnding = MakeFakeEnding(finishSegments, finish, m_graph, (m_router.GetMode() == Mode::Decoding)); return 0; } @@ -1195,12 +1241,11 @@ bool IndexRouter::PointsOnEdgesSnapping::IsFencedOff(m2::PointD const & point, E return false; } -// static void IndexRouter::PointsOnEdgesSnapping::RoadsToNearestEdges(m2::PointD const & point, vector const & roads, IsEdgeProjGood const & isGood, vector & edgeProj) { - NearestEdgeFinder finder(point, isGood); + NearestEdgeFinder finder(point, isGood, (m_router.GetMode() == Mode::Decoding)); for (auto const & road : roads) finder.AddInformationSource(road); @@ -1324,7 +1369,7 @@ bool IndexRouter::PointsOnEdgesSnapping::FindBestEdges(m2::PointD const & checkp } // Removing all candidates which are fenced off by the road graph (|closestRoads|) from |checkpoint|. - return !IsFencedOff(checkpoint, edgeProj, closestRoads); + return (m_router.GetMode() == Mode::Decoding) || !IsFencedOff(checkpoint, edgeProj, closestRoads); }; // Getting closest edges from |closestRoads| if they are correct according to isGood() function. @@ -1800,6 +1845,7 @@ void IndexRouter::SetupAlgorithmMode(IndexGraphStarter & starter, bool guidesAct case VehicleType::Bicycle: starter.GetGraph().SetMode(WorldGraphMode::Joints); break; case VehicleType::Transit: starter.GetGraph().SetMode(WorldGraphMode::NoLeaps); break; case VehicleType::Car: + case VehicleType::Decoder: starter.GetGraph().SetMode(AreMwmsNear(starter) ? WorldGraphMode::Joints : WorldGraphMode::LeapsOnly); break; case VehicleType::Count: CHECK(false, ("Unknown vehicle type:", m_vehicleType)); break; diff --git a/libs/routing/index_router.hpp b/libs/routing/index_router.hpp index eccc81bc7..3647f1690 100644 --- a/libs/routing/index_router.hpp +++ b/libs/routing/index_router.hpp @@ -43,6 +43,24 @@ class IndexGraphStarter; class IndexRouter : public IRouter { public: + /** + * @brief Indicates the mode in which the router is operating. + * + * The mode controls some aspects of router behavior, such as asking for additional maps or how + * checkpoints are matched to nearby segments. + */ + enum Mode + { + /** + * Router mode for navigation, i.e. user-initiated route guidance. + */ + Navigation, + /** + * Router mode for location decoding. + */ + Decoding + }; + class BestEdgeComparator final { public: @@ -67,6 +85,24 @@ class IndexRouter : public IRouter m2::PointD const m_direction; }; + /** + * @brief Creates a new `IndexRouter` instance. + * + * This is the constructor intended for normal routing. It requires a `TrafficCache` argument, + * from which it may create a traffic stash so the traffic situation can be considered for the + * route, depending on the vehicle type. + * + * @param vehicleType The vehichle type + * @param loadAltitudes Whether to load altitudes + * @param countryParentNameGetterFn Function which converts a country name into the name of its parent country) + * @param countryFileFn Function which converts a pointer to its country name + * @param countryRectFn Function which returns the rect for a country + * @param numMwmIds MWMs to use for route calculation (this should include all MWMs, whether or + * not we have the file locally, but not World or WorldCoasts) + * @param numMwmTree + * @param trafficCache The traffic cache (used only if `vehicleType` is `VehicleType::Car`) + * @param dataSource The MWM data source + */ IndexRouter(VehicleType vehicleType, bool loadAltitudes, CountryParentNameGetterFn const & countryParentNameGetterFn, TCountryFileFn const & countryFileFn, CountryRectFn const & countryRectFn, std::shared_ptr numMwmIds, std::unique_ptr> numMwmTree, @@ -95,6 +131,63 @@ class IndexRouter : public IRouter m_currentTimeGetter = std::forward(getter); } +protected: + /** + * @brief Creates a new `IndexRouter` instance. + * + * This constructor is intended for use by the TraFF decoder, not for normal routing. It differs + * from the general-purpose constructor in two ways. + * + * It takes an explicit `EdgeEstimator` argument, instance, which gives the caller fine-grained + * control over the cost calculations used for routing by supplying an `EdgeEstimator` of their + * choice. + * + * It also lacks the `TrafficCache` argument and never creates a traffic stash. This creates a + * router instance which ignores the traffic situation, regardless of the vehicle type. + * + * @param vehicleType The vehichle type + * @param loadAltitudes Whether to load altitudes + * @param countryParentNameGetterFn Function which converts a country name into the name of its parent country) + * @param countryFileFn Function which converts a pointer to its country name + * @param countryRectFn Function which returns the rect for a country + * @param numMwmIds + * @param numMwmTree + * @param estimator An edge estimator + * @param dataSource The MWM data source + */ + IndexRouter(VehicleType vehicleType, bool loadAltitudes, + CountryParentNameGetterFn const & countryParentNameGetterFn, + TCountryFileFn const & countryFileFn, CountryRectFn const & countryRectFn, + std::shared_ptr numMwmIds, std::unique_ptr> numMwmTree, + std::shared_ptr estimator, DataSource & dataSource); + + + /** + * @brief Returns the mode in which the router is operating. + * + * The `IndexRouter` always returns `Mode::Navigation`; subclasses may override this method and + * return different values. + * + * In navigation mode, the router may exit with `RouterResultCode::NeedMoreMaps` if it determines + * that a better route can be calculated with additional maps. When snapping endpoints to edges, + * it will consider only edges which are not “fenced off” by other edges, i.e. which can be + * reached from the endpoint without crossing other edges. This decreases the number of fake + * endings and thus speeds up routing, without any undesirable side effects for that use case. + * + * In decoding mode, the router will never exit with `RouterResultCode::NeedMoreMaps`: it will try + * to find a route with the existing maps, or exit without finding a route. When snapping + * endpoints to edges, it considers all edges within the given radius, fenced off or not. + */ + virtual Mode GetMode() { return Mode::Navigation; } + + /** + * @brief Returns current routing options. + * + * In this class, the routing options are the one set in the GUI. Subclasses may override this + * method to provide different routing options. + */ + virtual RoutingOptions GetRoutingOptions(); + private: RouterResultCode CalculateSubrouteJointsMode(IndexGraphStarter & starter, RouterDelegate const & delegate, std::shared_ptr const & progress, @@ -159,8 +252,8 @@ class IndexRouter : public IRouter static bool IsFencedOff(m2::PointD const & point, EdgeProjectionT const & edgeProjection, std::vector const & fences); - static void RoadsToNearestEdges(m2::PointD const & point, std::vector const & roads, - IsEdgeProjGood const & isGood, std::vector & edgeProj); + void RoadsToNearestEdges(m2::PointD const & point, std::vector const & roads, + IsEdgeProjGood const & isGood, std::vector & edgeProj); Segment GetSegmentByEdge(Edge const & edge) const; diff --git a/libs/routing/nearest_edge_finder.cpp b/libs/routing/nearest_edge_finder.cpp index f7c0a41fa..964127823 100644 --- a/libs/routing/nearest_edge_finder.cpp +++ b/libs/routing/nearest_edge_finder.cpp @@ -9,9 +9,11 @@ namespace routing { using namespace std; -NearestEdgeFinder::NearestEdgeFinder(m2::PointD const & point, IsEdgeProjGood const & isEdgeProjGood) +NearestEdgeFinder::NearestEdgeFinder(m2::PointD const & point, IsEdgeProjGood const & isEdgeProjGood, + bool snapToEnds) : m_point(point) , m_isEdgeProjGood(isEdgeProjGood) + , m_snapToEnds(snapToEnds) {} void NearestEdgeFinder::AddInformationSource(IRoadGraph::FullRoadInfo const & roadInfo) @@ -28,7 +30,7 @@ void NearestEdgeFinder::AddInformationSource(IRoadGraph::FullRoadInfo const & ro { m2::ParametrizedSegment segment(junctions[i - 1].GetPoint(), junctions[i].GetPoint()); - m2::PointD const closestPoint = segment.ClosestPointTo(m_point); + m2::PointD const closestPoint = segment.ClosestPointTo(m_point, m_snapToEnds); double const squaredDist = m_point.SquaredLength(closestPoint); if (squaredDist < res.m_squaredDist) @@ -48,7 +50,7 @@ void NearestEdgeFinder::AddInformationSource(IRoadGraph::FullRoadInfo const & ro geometry::Altitude const startAlt = segStart.GetAltitude(); geometry::Altitude const endAlt = segEnd.GetAltitude(); m2::ParametrizedSegment segment(junctions[idx - 1].GetPoint(), junctions[idx].GetPoint()); - m2::PointD const closestPoint = segment.ClosestPointTo(m_point); + m2::PointD const closestPoint = segment.ClosestPointTo(m_point, m_snapToEnds); double const segLenM = mercator::DistanceOnEarth(segStart.GetPoint(), segEnd.GetPoint()); geometry::Altitude projPointAlt = geometry::kDefaultAltitudeMeters; diff --git a/libs/routing/nearest_edge_finder.hpp b/libs/routing/nearest_edge_finder.hpp index bcdf428f2..c7546d713 100644 --- a/libs/routing/nearest_edge_finder.hpp +++ b/libs/routing/nearest_edge_finder.hpp @@ -27,7 +27,12 @@ using IsEdgeProjGood = std::function & res) const; m2::PointD const m_point; + bool m_snapToEnds; std::vector m_candidates; IsEdgeProjGood m_isEdgeProjGood; }; diff --git a/libs/routing/road_access_serialization.cpp b/libs/routing/road_access_serialization.cpp index 7e7191587..1dde0a865 100644 --- a/libs/routing/road_access_serialization.cpp +++ b/libs/routing/road_access_serialization.cpp @@ -11,6 +11,7 @@ std::string DebugPrint(RoadAccessSerializer::Header const & header) case RoadAccessSerializer::Header::TheFirstVersionRoadAccess: return "TheFirstVersionRoadAccess"; case RoadAccessSerializer::Header::WithoutAccessConditional: return "WithoutAccessConditional"; case RoadAccessSerializer::Header::WithAccessConditional: return "WithAccessConditional"; + case RoadAccessSerializer::Header::DecoderModelAccess: return "DecoderModelAccess"; } UNREACHABLE(); } diff --git a/libs/routing/road_access_serialization.hpp b/libs/routing/road_access_serialization.hpp index d0913f66e..a2b007c9b 100644 --- a/libs/routing/road_access_serialization.hpp +++ b/libs/routing/road_access_serialization.hpp @@ -33,11 +33,24 @@ class RoadAccessSerializer final using PointToAccessConditional = RoadAccess::PointToAccessConditional; using RoadAccessByVehicleType = std::array(VehicleType::Count)>; + /** + * @brief Values for the access version header. + * + * Each version introduced a breaking change to the way road access is stored in the MWM, + * requiring a different approach to deserialization. + * + * For any future format changes, append a new value with a descriptive name to this enum, + * and update `kLatestVersion` as well as `kVehicleTypeCount`. + * + * For any new values added here, the corresponding `DebugPrint()` function in the cpp file also + * needs to be updated. + */ enum class Header : uint32_t { - TheFirstVersionRoadAccess = 0, // Version of section roadaccess in 2017. - WithoutAccessConditional = 1, // Section roadaccess before conditional was implemented. - WithAccessConditional = 2 // Section roadaccess with conditional. + TheFirstVersionRoadAccess = 0, //< Version of section roadaccess in 2017. + WithoutAccessConditional = 1, //< Section roadaccess before conditional was implemented. + WithAccessConditional = 2, //< Section roadaccess with conditional. + DecoderModelAccess = 3 //< New vehicle model `DecoderModel` added. }; RoadAccessSerializer() = delete; @@ -59,16 +72,47 @@ class RoadAccessSerializer final switch (header) { case Header::TheFirstVersionRoadAccess: break; // Version of 2017. Unsupported. - case Header::WithoutAccessConditional: DeserializeAccess(src, vehicleType, roadAccess); break; + case Header::WithoutAccessConditional: DeserializeAccess(src, vehicleType, roadAccess, header); break; case Header::WithAccessConditional: - DeserializeAccess(src, vehicleType, roadAccess); - DeserializeAccessConditional(src, vehicleType, roadAccess); + case Header::DecoderModelAccess: + DeserializeAccess(src, vehicleType, roadAccess, header); + DeserializeAccessConditional(src, vehicleType, roadAccess, header); break; } } private: - inline static Header const kLatestVersion = Header::WithAccessConditional; + /** + * @brief Current version of the road access format. + * + * This should always be equal to the last value of `Header` and must be updated whenever format + * changes are introduced that require different processing. + * + * When changing this value, you must ensure backward compatibility with older versions. + */ + inline static Header const kLatestVersion = Header::DecoderModelAccess; + + /** + * @brief Number of vehicle types by version. + * + * Use `Header` as a numeric index into this array to determine the number of vehicle types for + * which access should be read upon encountering that version. + * + * When adding or removing vehicle types, append a new value to `Header` (see there), set + * `kLatestVersion` to that new value, and append an entry here, indicating the new number of + * vehicle types. You also need to review all functions in which `kVehicleTypeCount` is used and + * adapt them as needed to be compatible with both the old and new format. + */ + inline static std::array const kVehicleTypeCount = { + // TheFirstVersionRoadAccess (version of section roadaccess in 2017, no longer supported) + 0, + // WithoutAccessConditional (section roadaccess before conditional was implemented, 4 vehicle types) + 4, + // WithAccessConditional (section roadaccess with conditional, 4 vehicle types) + 4, + // DecoderModelAccess (new vehicle model `DecoderModel` and vehicle type added) + 5 + }; class AccessPosition { @@ -130,13 +174,18 @@ class RoadAccessSerializer final } template - static void DeserializeAccess(Source & src, VehicleType vehicleType, RoadAccess & roadAccess) + static void DeserializeAccess(Source & src, VehicleType vehicleType, RoadAccess & roadAccess, Header header) { - std::array(VehicleType::Count)> sectionSizes{}; - static_assert(static_cast(VehicleType::Count) == 4, - "It is assumed below that there are only 4 vehicle types and we store 4 numbers " - "of sections size. If you add or remove vehicle type you should up " - "|kLatestVersion| and save back compatibility here."); + std::vector sectionSizes(kVehicleTypeCount[static_cast(header)]); + static_assert(kVehicleTypeCount.size() == (static_cast(kLatestVersion) + 1), + "The number of entries in `kVehicleTypeCount` does not match `kLatestVersion`. " + "Ensure `kVehicleTypeCount` has an entry for each possible value of `Header`, " + "and `kLatestVersion` matches the highest possible value of `Header`."); + static_assert(static_cast(VehicleType::Count) == 5, + "The current version of the access header assumes there are exactly 5 vehicle " + "types. Adding or removing vehicle types is a breaking change and requires " + "increasing `kLatestVersion` and updating `kVehicleTypeCount`. See documentation " + "of `kVehicleTypeCount` for details."); for (auto & sectionSize : sectionSizes) sectionSize = ReadPrimitiveFromSource(src); @@ -186,13 +235,18 @@ class RoadAccessSerializer final } template - static void DeserializeAccessConditional(Source & src, VehicleType vehicleType, RoadAccess & roadAccess) + static void DeserializeAccessConditional(Source & src, VehicleType vehicleType, RoadAccess & roadAccess, Header header) { - std::array(VehicleType::Count)> sectionSizes{}; - static_assert(static_cast(VehicleType::Count) == 4, - "It is assumed below that there are only 4 vehicle types and we store 4 numbers " - "of sections size. If you add or remove vehicle type you should up " - "|kLatestVersion| and save back compatibility here."); + std::vector sectionSizes(kVehicleTypeCount[static_cast(header)]); + static_assert(kVehicleTypeCount.size() == (static_cast(kLatestVersion) + 1), + "The number of entries in `kVehicleTypeCount` does not match `kLatestVersion`. " + "Ensure `kVehicleTypeCount` has an entry for each possible value of `Header`, " + "and `kLatestVersion` matches the highest possible value of `Header`."); + static_assert(static_cast(VehicleType::Count) == 5, + "The current version of the access header assumes there are exactly 5 vehicle " + "types. Adding or removing vehicle types is a breaking change and requires " + "increasing `kLatestVersion` and updating `kVehicleTypeCount`. See documentation " + "of `kVehicleTypeCount` for details."); for (auto & sectionSize : sectionSizes) sectionSize = ReadPrimitiveFromSource(src); @@ -341,6 +395,8 @@ class RoadAccessSerializer final } // todo(@m) This code borrows heavily from traffic/traffic_info.hpp:SerializeTrafficKeys. + // SerializeTrafficKeys is from the defunct MapsWithMe traffic module and will be obsoleted by + // the new traffic feature currently under development. template static void SerializeSegments(Sink & sink, std::vector const & segments) { diff --git a/libs/routing/road_graph.hpp b/libs/routing/road_graph.hpp index 4258d8ba8..5bbdfa588 100644 --- a/libs/routing/road_graph.hpp +++ b/libs/routing/road_graph.hpp @@ -263,7 +263,7 @@ class IRoadGraph : public RoadGraphBase /// then returns empty array. using EdgeProjectionT = std::pair; virtual void FindClosestEdges(m2::RectD const & /*rect*/, uint32_t /*count*/, - std::vector & /*vicinities*/) const + std::vector & /*vicinities*/, bool snapToEnds) const {} /// \returns Vector of pairs FeatureID and corresponding RoadInfo for road features diff --git a/libs/routing/road_point.hpp b/libs/routing/road_point.hpp index 40c3c13e8..09f8f28b4 100644 --- a/libs/routing/road_point.hpp +++ b/libs/routing/road_point.hpp @@ -9,10 +9,11 @@ namespace routing { -// RoadPoint is a unique identifier for any road point in mwm file. -// -// Contains feature id and point id. -// Point id is the ordinal number of the point in the road. +/** + * @brief A unique identifier for any point on a road in an mwm file. + * + * It contains a feature id and point id. The point id is the ordinal number of the point in the road. + */ class RoadPoint final { public: diff --git a/libs/routing/route.hpp b/libs/routing/route.hpp index 90d91ac3f..23cb718b2 100644 --- a/libs/routing/route.hpp +++ b/libs/routing/route.hpp @@ -39,8 +39,21 @@ namespace routing using SubrouteUid = uint64_t; SubrouteUid constexpr kInvalidSubrouteId = std::numeric_limits::max(); -/// \brief The route is composed of one or several subroutes. Every subroute is composed of segments. -/// For every Segment is kept some attributes in the structure SegmentInfo. +/** + * @brief A segment of the route. + * + * The route is composed of one or several subroutes. Every subroute is composed of segments. + * + * For every Segment, some attributes are kept in the `SegmentInfo` structure. + * + * @todo the statement regarding `SegmentInfo` seems to be outdated, is `SegmentInfo` a prececessor + * of `RouteSegment`? + * + * Segment data which is actually related to a point, such as junction, distance and time, refer to + * the end which is closer to the end of the route. For the first segment, distance and time are the + * length and travel time of the segment itself. For the last segment, distance and time are the + * length and travel time of the entore route, and the junction is the finish point. + */ class RouteSegment final { public: @@ -69,17 +82,50 @@ class RouteSegment final uint8_t m_maxSpeedKmPH = 0; }; + /** + * @brief Holds structured information about a road. + * + * `m_ref` and `m_name` refer to the road itself. + * + * This structure is only populated for the first segment of a feature (segment index is either 0, + * or one less than the segment count of the feature, or the segment is the first segment of the + * route which is not a fake segment). For subsequent segments of the same feature, it is empty. + */ struct RoadNameInfo { - // This is for street/road. |m_ref| |m_name|. - std::string m_name; // E.g "Johnson Ave.". - std::string m_destination_ref; // Number of next road, e.g. "CA 85", Sometimes "CA 85 South". Usually match |m_ref| - // of next main road. - // This is for 1st segment of link after junction. Exit |junction_ref| to |m_destination_ref| for |m_destination|. - std::string m_junction_ref; // Number of junction e.g. "398B". - std::string m_destination; // E.g. "Cupertino". - std::string m_ref; // Number of street/road e.g. "CA 85". + /** + * @brief The name of the road, e.g. “Johnson Ave”. + */ + std::string m_name; + /** + * @brief The number of the next road. + * + * This usually matches `m_ref` of the naxt main road, e.g. “CA 85”, sometimes “CA 85 South”. + */ + std::string m_destination_ref; + /** + * @brief The junction number, e.g. “398B”. + * + * This is used for the first link segment after a junction (exit `junction_ref` to + * `m_destination_ref` for `m_destination`). + */ + std::string m_junction_ref; + /** + * @brief The destination of the road, e.g. “Cupertino”. + */ + std::string m_destination; + /** + * @brief The number of the road, e.g. “CA85”. + */ + std::string m_ref; + /** + * @brief Whether the road is of a link type. + */ bool m_isLink = false; + /** + * @brief Whether the road is part of a roundabout. + */ + bool m_onRoundabout = false; RoadNameInfo() = default; RoadNameInfo(std::string name) : m_name(std::move(name)) {} @@ -158,8 +204,30 @@ class RouteSegment final turns::TurnItem const & GetTurn() const { return m_turn; } void ClearTurnLanes() { m_turn.m_lanes.clear(); } + /** + * @brief Returns distance from the beginning of the route in meters. + * + * Distance is measured up to the end of the current segment, i.e. including the segment. For the + * first segment, this is identical to the length of the segment; for the last segment, it is + * identical to the length of the entire route. + * + * Note that the first and last real (non-fake) segment on the route may not report the actual + * length between their endpoints, but only the part which is also part of the route. + */ double GetDistFromBeginningMeters() const { return m_distFromBeginningMeters; } double GetDistFromBeginningMerc() const { return m_distFromBeginningMerc; } + + /** + * @brief Returns travel time from the beginning of the route. + * + * Travel time is the ETA from the beginning of the route to the end of the current segment, i.e. + * including the segment. For the first segment, this is identical to the travel time along the + * segment; for the last segment, it is identical to the travel time along the entire route. + * + * Note that the first and last real (non-fake) segment on the route may report time based not + * upon the actual length between their endpoints, but upon the part which is also part of the + * route. + */ double GetTimeFromBeginningSec() const { return m_timeFromBeginningS; } bool HasTransitInfo() const { return m_transitInfo.HasTransitInfo(); } diff --git a/libs/routing/routes_builder/routes_builder.cpp b/libs/routing/routes_builder/routes_builder.cpp index 9e1eaa0d8..a37108d5e 100644 --- a/libs/routing/routes_builder/routes_builder.cpp +++ b/libs/routing/routes_builder/routes_builder.cpp @@ -269,7 +269,7 @@ void RoutesBuilder::Processor::InitRouter(VehicleType type) return cigSharedPtr->GetLimitRectForLeaf(countryId); }; - bool const loadAltitudes = type != VehicleType::Car; + bool const loadAltitudes = (type != VehicleType::Car) && (type != VehicleType::Decoder); if (!m_dataSource) m_dataSource = m_dataSourceStorage.GetDataSource(); diff --git a/libs/routing/routes_builder/routes_builder_tool/utils.cpp b/libs/routing/routes_builder/routes_builder_tool/utils.cpp index cdb24d5e3..8662f95bc 100644 --- a/libs/routing/routes_builder/routes_builder_tool/utils.cpp +++ b/libs/routing/routes_builder/routes_builder_tool/utils.cpp @@ -56,6 +56,8 @@ routing::VehicleType ConvertVehicleTypeFromString(std::string const & str) return routing::VehicleType::Bicycle; if (str == "transit") return routing::VehicleType::Transit; + if (str == "decoder") + return routing::VehicleType::Decoder; CHECK(false, ("Unknown vehicle type:", str)); UNREACHABLE(); diff --git a/libs/routing/routing_callbacks.hpp b/libs/routing/routing_callbacks.hpp index 4a5bcb831..e05510fac 100644 --- a/libs/routing/routing_callbacks.hpp +++ b/libs/routing/routing_callbacks.hpp @@ -45,16 +45,49 @@ enum class RouterResultCode enum class SessionState { - NoValidRoute, // No valid route: no route after application launching or the route was removed. - RouteBuilding, // We requested a route and wait when it will be built. User may be following - // the previous route. - RouteNotStarted, // Route is built but the user isn't on it. - OnRoute, // User follows the route. - RouteNeedsRebuild, // User left the route. - RouteFinished, // Destination point is reached but the session isn't closed. - RouteNoFollowing, // Route is built but following mode has been disabled. - RouteRebuilding, // We requested a route rebuild and wait when it will be rebuilt. - // User may following the previous route. + /** + * No valid route: no route after application launching, or the route was removed. + * + * This is the initial state at launch; in order to get out of it, a destination must be set AND + * the current location must be obtained. + */ + NoValidRoute, + + /** + * We have requested a route and are waiting for it to be built. User may be following the previous route. + */ + RouteBuilding, + + /** + * Route is built but the user isn't on it. + */ + RouteNotStarted, + + /** + * User is following the route. + */ + OnRoute, + + /** + * User has left the route. + */ + RouteNeedsRebuild, + + /** + * Destination point has been reached but the session isn’t closed yet. + */ + RouteFinished, + + /** + * Route has been built but following mode has been disabled. + */ + RouteNoFollowing, + + /** + * We have requested a route rebuild and are waiting for it to be rebuilt. User may be following + * the previous route. + */ + RouteRebuilding, }; /* diff --git a/libs/routing/routing_helpers.hpp b/libs/routing/routing_helpers.hpp index 154a1c7c7..ab825759c 100644 --- a/libs/routing/routing_helpers.hpp +++ b/libs/routing/routing_helpers.hpp @@ -61,6 +61,26 @@ bool IsRoad(Types const & types) void FillSegmentInfo(std::vector const & times, std::vector & routeSegments); +/** + * @brief Constructs or reconstructs a route. + * + * This function populates `route` with segments and geometry. Segments are calculated from `graph` + * (the route graph) and `path` (points on the route); each pair of consecutive points becomes a + * segment. The actual calculation is delegated to `engine` and can be influenced by passing a + * different directions engine. Segment information is then enriched with the length of each segment + * (calculated directly) and the estimated travel time specified in `times`. Geometry is calculated + * from `path` by extracting latitude and longitude from each item. + * + * The number of items in `times` must be equal to the number of segments, or the number of items in + * `points` minus 1. The items of `times` are travel times from start, therefore no value can be + * less than the previous one. + * + * @param engine The directions engine + * @param graph The route graph + * @param path The route path, an ordered list of points on the route + * @param times Travel times (from start) for each segment + * @param route The route + */ void ReconstructRoute(DirectionsEngine & engine, IndexRoadGraph const & graph, base::Cancellable const & cancellable, std::vector const & path, std::vector const & times, Route & route); diff --git a/libs/routing/routing_session.cpp b/libs/routing/routing_session.cpp index 5670e8009..8f2ebed4d 100644 --- a/libs/routing/routing_session.cpp +++ b/libs/routing/routing_session.cpp @@ -503,6 +503,13 @@ double RoutingSession::GetCompletionPercent() const return percent; } +void RoutingSession::GetAllRegions(std::set & countries) +{ + if (!m_router) + return; + m_router->GetAllRegions(countries); +} + void RoutingSession::PassCheckpoints() { CHECK_THREAD_CHECKER(m_threadChecker, ()); diff --git a/libs/routing/routing_session.hpp b/libs/routing/routing_session.hpp index 990b3492b..4e6dfcdbc 100644 --- a/libs/routing/routing_session.hpp +++ b/libs/routing/routing_session.hpp @@ -40,9 +40,12 @@ class RouteMatchingInfo; namespace routing { -/// \breaf This class is responsible for the route built in the program. -/// \note All method of this class should be called from ui thread if there's no -/// a special note near a method. +/** + * @brief This class is responsible for the route built in the program. + * + * @note Methods of this class may only be called from the UI thread, unless the method + * documentation states otherwise. + */ class RoutingSession : public traffic::TrafficObserver , public traffic::TrafficCache @@ -66,24 +69,105 @@ class RoutingSession m2::PointD GetStartPoint() const; m2::PointD GetEndPoint() const; + /** + * @brief Whether routing is currently active. + * + * A route is considered active if a destination has been entered and the route has ben built or + * is currently being built or rebuilt, even if the user is not currently following it. + * + * @return True if active, false if not + */ bool IsActive() const; + + /** + * @brief Whether the route is currently navigable. + * + * The route is considered navigable if it has been built and following mode is enabled, and the + * user either has not yet started following the route, is currently following it or has reached + * their destination but not yet closed the session. The route is no longer navigable if the user + * leaves it. + * + * @return True if navigable, false if not. + */ bool IsNavigable() const; + + /** + * @brief Whether a built route exists. + * + * A route is considered built if it is navigable (see `IsNavigable()`), but retains that state + * even if the user leaves the route. + * + * @return True if built, false if not. + */ bool IsBuilt() const; - /// \returns true if a new route is in process of building rebuilding or - /// if a route is being rebuilt in case the user left the route, and false otherwise. + + /** + * @brief Whether a route is currently being built or rebuilt. + * + * This is the case if a new route is in process of building, rebuilding, or if a route is being + * rebuilt after the user has left the route. + * + * @return True if building or rebuilding, false otherwise. + */ bool IsBuilding() const; + + /** + * @brief Whether the route is currently being built from scratch. + * + * This is the case if a new route is currently being built, but not if an already existing route + * is being rebuilt. + * + * @return True if building, false otherwise (also if rebuilding). + */ bool IsBuildingOnly() const; + + /** + * @brief Whether the route is currently being rebuilt. + * + * This is the case if an already existing route is being rebuilt, but not if a new route is + * currently being built. + * + * @return True if rebuilding, false otherwise (also if building from scratch). + */ bool IsRebuildingOnly() const; + + /** + * @brief Whether the route is finished. + * + * The route is considered finished if the destination point has been reached but the session has + * not been closed. + * + * @return True if finished, false otherwise. + */ bool IsFinished() const; + + /** + * @brief Whether a route has been built while following mode is disabled. + * + * @return True if a route has been built and following mode disabled, false otherwise. + */ bool IsNoFollowing() const; + + /** + * @brief Whether the user is currently following a previously built route. + * + * This is the case if the route has been built, route following is enabled, the user has started + * following the route and not left it. + * + * @return True if the user is following the route, false otherwise. + */ bool IsOnRoute() const; + bool IsFollowing() const; void Reset(); void SetState(SessionState state); - /// \returns true if altitude information along |m_route| is available and - /// false otherwise. + /** + * @brief Whether altitude information along the route is available. + * + * @return True if altitude information along `m_route` is available, false otherwise. + */ bool HasRouteAltitude() const; bool IsRouteId(uint64_t routeId) const; bool IsRouteValid() const; @@ -174,6 +258,15 @@ class RoutingSession double GetCompletionPercent() const; + /** + * @brief Retrieves the MWMs needed to build the route. + * + * Waits for the `RegionsRouter` thread to finish and returns the list of MWM names from it. + * + * @param countries Receives the list of MWM names. + */ + void GetAllRegions(std::set & countries); + private: struct DoReadyCallback { diff --git a/libs/routing/routing_settings.cpp b/libs/routing/routing_settings.cpp index c2ffae340..ca95b0eba 100644 --- a/libs/routing/routing_settings.cpp +++ b/libs/routing/routing_settings.cpp @@ -75,6 +75,7 @@ RoutingSettings GetRoutingSettings(VehicleType vehicleType) 3 /* m_notSoCloseMaxPointsCount */, 25.0 /* m_notSoCloseMaxDistMeters */}; case VehicleType::Car: + case VehicleType::Decoder: return {true /* useDirectionForRouteBuilding */, true /* m_matchRoute */, true /* m_soundDirection */, diff --git a/libs/routing/segment.hpp b/libs/routing/segment.hpp index c7db67989..e2ba1316a 100644 --- a/libs/routing/segment.hpp +++ b/libs/routing/segment.hpp @@ -9,14 +9,16 @@ namespace routing { -// This is directed road segment used as vertex in index graph. -// -// You can imagine the segment as a material arrow. -// Head of each arrow is connected to fletchings of next arrows with invisible links: -// these are the edges of the graph. -// -// Position of the segment is a position of the arrowhead: GetPointId(true). -// This position is used in heuristic and edges weight calculations. +/** + * @brief A directed road segment used as a vertex in the index graph. + * + * You can imagine the segment as a material arrow. + * Head of each arrow is connected to fletchings of next arrows with invisible links: + * these are the edges of the graph. + * + * Position of the segment is a position of the arrowhead: GetPointId(true). + * This position is used in heuristic and edges weight calculations. + */ class Segment final { public: @@ -78,7 +80,9 @@ class SegmentEdge final bool operator<(SegmentEdge const & edge) const; private: - // Target is vertex going to for outgoing edges, vertex going from for ingoing edges. + /** + * @brief Vertex going to for outgoing edges, vertex going from for ingoing edges. + */ Segment m_target; RouteWeight m_weight; }; diff --git a/libs/routing/vehicle_mask.cpp b/libs/routing/vehicle_mask.cpp index 11c4395ec..22528d4ec 100644 --- a/libs/routing/vehicle_mask.cpp +++ b/libs/routing/vehicle_mask.cpp @@ -15,6 +15,7 @@ std::string DebugPrint(VehicleType vehicleType) case VehicleType::Bicycle: return "Bicycle"; case VehicleType::Car: return "Car"; case VehicleType::Transit: return "Transit"; + case VehicleType::Decoder: return "Decoder"; case VehicleType::Count: return "Count"; } UNREACHABLE(); @@ -35,6 +36,8 @@ void FromString(std::string_view s, VehicleType & vehicleType) vehicleType = VehicleType::Car; else if (s == "Transit") vehicleType = VehicleType::Transit; + else if (s == "Decoder") + vehicleType = VehicleType::Decoder; else { ASSERT(false, ("Could not read VehicleType from string", s)); diff --git a/libs/routing/vehicle_mask.hpp b/libs/routing/vehicle_mask.hpp index 61b31c3fb..6a7d2d146 100644 --- a/libs/routing/vehicle_mask.hpp +++ b/libs/routing/vehicle_mask.hpp @@ -13,7 +13,8 @@ enum class VehicleType Bicycle = 1, Car = 2, Transit = 3, - Count = 4 + Decoder = 4, + Count = 5 }; using VehicleMask = uint32_t; @@ -30,6 +31,7 @@ VehicleMask constexpr kPedestrianMask = GetVehicleMask(VehicleType::Pedestrian); VehicleMask constexpr kBicycleMask = GetVehicleMask(VehicleType::Bicycle); VehicleMask constexpr kCarMask = GetVehicleMask(VehicleType::Car); VehicleMask constexpr kTransitMask = GetVehicleMask(VehicleType::Transit); +VehicleMask constexpr kDecoderMask = GetVehicleMask(VehicleType::Decoder); std::string DebugPrint(VehicleType vehicleType); std::string ToString(VehicleType vehicleType); diff --git a/libs/routing_common/CMakeLists.txt b/libs/routing_common/CMakeLists.txt index f03636b2d..e031491ec 100644 --- a/libs/routing_common/CMakeLists.txt +++ b/libs/routing_common/CMakeLists.txt @@ -6,6 +6,8 @@ set(SRC car_model.cpp car_model.hpp car_model_coefs.hpp # This file is generated by running ../configure.sh script. + decoder_model.cpp + decoder_model.hpp maxspeed_conversion.cpp maxspeed_conversion.hpp num_mwm_id.hpp diff --git a/libs/routing_common/car_model.cpp b/libs/routing_common/car_model.cpp index aaa405cec..9fd87c2ce 100644 --- a/libs/routing_common/car_model.cpp +++ b/libs/routing_common/car_model.cpp @@ -21,6 +21,10 @@ using namespace routing; // of the route except for some edge cases. SpeedKMpH constexpr kSpeedOffroadKMpH = {0.01 /* weight */, kNotUsed /* eta */}; +/* + * The number of entries must match `kHighwayBasedFactors` and `kHighwayBasedSpeeds` + * in `car_model_coefs.hpp`. + */ VehicleModel::LimitsInitList const kDefaultOptions = { // {HighwayType, passThroughAllowed} {HighwayType::HighwayMotorway, true}, {HighwayType::HighwayMotorwayLink, true}, @@ -31,6 +35,7 @@ VehicleModel::LimitsInitList const kDefaultOptions = { {HighwayType::HighwayResidential, true}, {HighwayType::HighwayUnclassified, true}, {HighwayType::HighwayService, true}, {HighwayType::HighwayLivingStreet, true}, {HighwayType::HighwayRoad, true}, {HighwayType::HighwayTrack, true}, + // Non-highway types {HighwayType::RouteShuttleTrain, true}, {HighwayType::RouteFerry, true}, {HighwayType::ManMadePier, true}}; diff --git a/libs/routing_common/car_model.hpp b/libs/routing_common/car_model.hpp index 9201d9f25..4147655fc 100644 --- a/libs/routing_common/car_model.hpp +++ b/libs/routing_common/car_model.hpp @@ -5,6 +5,15 @@ namespace routing { +/** + * @brief A `VehicleModel` suitable for car routing. + * + * Each instance can have its own set of feature type-specific access limitations. These specify + * which roads can be used by this vehicle, and which roads can only be used for destination + * traffic. + * + * Instances are typically retrieved from `CarModelFactory` or the `AllLimitsInstance()` method. + */ class CarModel : public VehicleModel { public: @@ -15,8 +24,16 @@ class CarModel : public VehicleModel SpeedKMpH GetSpeed(FeatureTypes const & types, SpeedParams const & speedParams) const override; SpeedKMpH const & GetOffroadSpeed() const override; + /** + * @brief Returns an instance with the default access limitations. + */ static CarModel const & AllLimitsInstance(); + + /** + * @brief Returns the default access limitations. + */ static LimitsInitList const & GetOptions(); + static SurfaceInitList const & GetSurfaces(); }; diff --git a/libs/routing_common/car_model_coefs.hpp b/libs/routing_common/car_model_coefs.hpp index 6a466d946..447f6bbb5 100644 --- a/libs/routing_common/car_model_coefs.hpp +++ b/libs/routing_common/car_model_coefs.hpp @@ -3,6 +3,7 @@ #include "routing_common/vehicle_model.hpp" // These are default car model coefficients for open source developers. +// Both maps, as well as `car_model::kDefaultOptions` (in `car_model.cpp`), must have the same number of entries. namespace routing { diff --git a/libs/routing_common/decoder_model.cpp b/libs/routing_common/decoder_model.cpp new file mode 100644 index 000000000..7a116287b --- /dev/null +++ b/libs/routing_common/decoder_model.cpp @@ -0,0 +1,311 @@ +#include "routing_common/decoder_model.hpp" + +#include "indexer/classificator.hpp" + +namespace decoder_model +{ +using namespace routing; + +// See model specifics in different countries here: +// https://wiki.openstreetmap.org/wiki/OSM_tags_for_routing/Access-Restrictions + +// See road types here: +// https://wiki.openstreetmap.org/wiki/Key:highway + +/* + * One meter per second. The TraffEstimator works on distance in meters, not travel time. For code + * which works with speeds and assumes cost to be time-based, a speed of 1 m/s means such + * calculations will effectively return distances in meters. + */ +auto constexpr kOneMpSInKmpH = 3.6; + +/* + * Penalty factor for using a fake segment to get to a nearby road. + * Offroad penalty applies to direct distance whereas road penalty applies to roads, which can be up + * to around 3 times the direct distance (theoretically unlimited). Therefore, a factor of 3–4 times + * the penalty of a well-matched road may be needed to avoid competing with the correct route. + * On the other hand, a very high offroad penalty would give preference to a poorly matched route + * over a well-matched one if it is closer to the reference points. + * Maximum penalty for roads is currently 64 (4 for ramps * 4 for road type * 4 for ref). + * A well-matched road may still have a penalty of around 4 (twice the reduced attribute penalty, or + * once the full attribute penalty). + * A “wrong” road may also just have a penalty of 4 (e.g. road name mismatch, but road class and + * ramp type match). + * A value of 16 has worked well for the DE-B2R-SendlingSued-Passauerstrasse test case. (The + * DE-A10-Werder-GrossKreutz or DE-A115-PotsdamDrewitz-Nuthetal test cases gave incorrect results + * due to lack of fake segments, which was fixed through truncation and now works correctly even + * with an offroad penalty of 128.) + */ +auto constexpr kOffroadPenalty = 16; + +// |kSpeedOffroadKMpH| is a speed which is used for edges that don't lie on road features. +// For example for pure fake edges. In car routing, off road speed for calculation ETA is not used. +// The weight of such edges is considered as 0 seconds. It's especially actual when an airport is +// a start or finish. On the other hand, while route calculation the fake edges are considered +// as quite heavy. The idea behind that is to use the closest edge for the start and the finish +// of the route except for some edge cases. +/* + * TODO move this constant to the header + * `traff_decoder.cpp` uses `kOneMpSInKmpH / kOffroadPenalty` to initialize its `EdgeEstimator`, + * and includes the header for its vehicle model. + */ +SpeedKMpH constexpr kSpeedOffroadKMpH = kOneMpSInKmpH / kOffroadPenalty; + +HighwayBasedFactors const kDefaultFactors = { + // {highway class : InOutCityFactor(in city, out city)} + + {HighwayType::HighwayMotorway, InOutCityFactor(1.0)}, + {HighwayType::HighwayMotorwayLink, InOutCityFactor(1.0)}, + {HighwayType::HighwayTrunk, InOutCityFactor(1.0)}, + {HighwayType::HighwayTrunkLink, InOutCityFactor(1.0)}, + + {HighwayType::HighwayPrimary, InOutCityFactor(1.0)}, + {HighwayType::HighwayPrimaryLink, InOutCityFactor(1.0)}, + {HighwayType::HighwaySecondary, InOutCityFactor(1.0)}, + {HighwayType::HighwaySecondaryLink, InOutCityFactor(1.0)}, + + {HighwayType::HighwayTertiary, InOutCityFactor(1.0)}, + {HighwayType::HighwayTertiaryLink, InOutCityFactor(1.0)}, + {HighwayType::HighwayUnclassified, InOutCityFactor(1.0)}, + + {HighwayType::HighwayResidential, InOutCityFactor(1.0)}, + {HighwayType::HighwayLivingStreet, InOutCityFactor(1.0)}, + + {HighwayType::HighwayService, InOutCityFactor(1.0)}, + {HighwayType::HighwayRoad, InOutCityFactor(1.0)}, + {HighwayType::HighwayTrack, InOutCityFactor(1.0)}, + {HighwayType::ManMadePier, InOutCityFactor(1.0)}, + + {HighwayType::RouteFerry, InOutCityFactor(1.0)}, + {HighwayType::RouteShuttleTrain, InOutCityFactor(1.0)}, + + // Generic construction type + {HighwayType::HighwayConstruction, InOutCityFactor(1.0)}, + + // Construction types for each highway type: + {HighwayType::HighwayConstructionMotorway, InOutCityFactor(1.0)}, + {HighwayType::HighwayConstructionMotorwayLink, InOutCityFactor(1.0)}, + {HighwayType::HighwayConstructionTrunk, InOutCityFactor(1.0)}, + {HighwayType::HighwayConstructionTrunkLink, InOutCityFactor(1.0)}, + {HighwayType::HighwayConstructionPrimary, InOutCityFactor(1.0)}, + {HighwayType::HighwayConstructionPrimaryLink, InOutCityFactor(1.0)}, + {HighwayType::HighwayConstructionSecondary, InOutCityFactor(1.0)}, + {HighwayType::HighwayConstructionSecondaryLink, InOutCityFactor(1.0)}, + {HighwayType::HighwayConstructionTertiary, InOutCityFactor(1.0)}, + {HighwayType::HighwayConstructionTertiaryLink, InOutCityFactor(1.0)}, + {HighwayType::HighwayConstructionResidential, InOutCityFactor(1.0)}, + {HighwayType::HighwayConstructionUnclassified, InOutCityFactor(1.0)}, + {HighwayType::HighwayConstructionService, InOutCityFactor(1.0)}, + {HighwayType::HighwayConstructionLivingStreet, InOutCityFactor(1.0)}, + {HighwayType::HighwayConstructionRoad, InOutCityFactor(1.0)}, + {HighwayType::HighwayConstructionTrack, InOutCityFactor(1.0)}, +}; + +HighwayBasedSpeeds const kDefaultSpeeds = { + // {highway class : InOutCitySpeedKMpH(in city(weight, eta), out city(weight eta))} + {HighwayType::HighwayMotorway, InOutCitySpeedKMpH(kOneMpSInKmpH)}, + {HighwayType::HighwayMotorwayLink, InOutCitySpeedKMpH(kOneMpSInKmpH)}, + {HighwayType::HighwayTrunk, InOutCitySpeedKMpH(kOneMpSInKmpH)}, + {HighwayType::HighwayTrunkLink, InOutCitySpeedKMpH(kOneMpSInKmpH)}, + {HighwayType::HighwayPrimary, InOutCitySpeedKMpH(kOneMpSInKmpH)}, + {HighwayType::HighwayPrimaryLink, InOutCitySpeedKMpH(kOneMpSInKmpH)}, + {HighwayType::HighwaySecondary, InOutCitySpeedKMpH(kOneMpSInKmpH)}, + {HighwayType::HighwaySecondaryLink, InOutCitySpeedKMpH(kOneMpSInKmpH)}, + {HighwayType::HighwayTertiary, InOutCitySpeedKMpH(kOneMpSInKmpH)}, + {HighwayType::HighwayTertiaryLink, InOutCitySpeedKMpH(kOneMpSInKmpH)}, + {HighwayType::HighwayResidential, InOutCitySpeedKMpH(kOneMpSInKmpH)}, + {HighwayType::HighwayUnclassified, InOutCitySpeedKMpH(kOneMpSInKmpH)}, + {HighwayType::HighwayService, InOutCitySpeedKMpH(kOneMpSInKmpH)}, + {HighwayType::HighwayLivingStreet, InOutCitySpeedKMpH(kOneMpSInKmpH)}, + {HighwayType::HighwayRoad, InOutCitySpeedKMpH(kOneMpSInKmpH)}, + {HighwayType::HighwayTrack, InOutCitySpeedKMpH(kOneMpSInKmpH)}, + // The router truncates types to two levels, so we need this in addition to the long construction types + {HighwayType::HighwayConstruction, InOutCitySpeedKMpH(kOneMpSInKmpH)}, + // Construction conterparts to each of the types above + // (needed for the map generator to include them in the routing section) + {HighwayType::HighwayConstructionMotorway, InOutCitySpeedKMpH(kOneMpSInKmpH)}, + {HighwayType::HighwayConstructionMotorwayLink, InOutCitySpeedKMpH(kOneMpSInKmpH)}, + {HighwayType::HighwayConstructionTrunk, InOutCitySpeedKMpH(kOneMpSInKmpH)}, + {HighwayType::HighwayConstructionTrunkLink, InOutCitySpeedKMpH(kOneMpSInKmpH)}, + {HighwayType::HighwayConstructionPrimary, InOutCitySpeedKMpH(kOneMpSInKmpH)}, + {HighwayType::HighwayConstructionPrimaryLink, InOutCitySpeedKMpH(kOneMpSInKmpH)}, + {HighwayType::HighwayConstructionSecondary, InOutCitySpeedKMpH(kOneMpSInKmpH)}, + {HighwayType::HighwayConstructionSecondaryLink, InOutCitySpeedKMpH(kOneMpSInKmpH)}, + {HighwayType::HighwayConstructionTertiary, InOutCitySpeedKMpH(kOneMpSInKmpH)}, + {HighwayType::HighwayConstructionTertiaryLink, InOutCitySpeedKMpH(kOneMpSInKmpH)}, + {HighwayType::HighwayConstructionResidential, InOutCitySpeedKMpH(kOneMpSInKmpH)}, + {HighwayType::HighwayConstructionUnclassified, InOutCitySpeedKMpH(kOneMpSInKmpH)}, + {HighwayType::HighwayConstructionService, InOutCitySpeedKMpH(kOneMpSInKmpH)}, + {HighwayType::HighwayConstructionLivingStreet, InOutCitySpeedKMpH(kOneMpSInKmpH)}, + {HighwayType::HighwayConstructionRoad, InOutCitySpeedKMpH(kOneMpSInKmpH)}, + {HighwayType::HighwayConstructionTrack, InOutCitySpeedKMpH(kOneMpSInKmpH)}, + // Non-highway types + {HighwayType::RouteShuttleTrain, InOutCitySpeedKMpH(kOneMpSInKmpH)}, + {HighwayType::RouteFerry, InOutCitySpeedKMpH(kOneMpSInKmpH)}, + {HighwayType::ManMadePier, InOutCitySpeedKMpH(kOneMpSInKmpH)}}; + +/* + * The number of entries must match `kHighwayBasedFactors` and `kHighwayBasedSpeeds` + * in `decoder_model_coefs.hpp`. + */ +VehicleModel::LimitsInitList const kDefaultOptions = { + // {HighwayType, passThroughAllowed} + {HighwayType::HighwayMotorway, true}, {HighwayType::HighwayMotorwayLink, true}, + {HighwayType::HighwayTrunk, true}, {HighwayType::HighwayTrunkLink, true}, + {HighwayType::HighwayPrimary, true}, {HighwayType::HighwayPrimaryLink, true}, + {HighwayType::HighwaySecondary, true}, {HighwayType::HighwaySecondaryLink, true}, + {HighwayType::HighwayTertiary, true}, {HighwayType::HighwayTertiaryLink, true}, + {HighwayType::HighwayResidential, true}, {HighwayType::HighwayUnclassified, true}, + {HighwayType::HighwayService, true}, {HighwayType::HighwayLivingStreet, true}, + {HighwayType::HighwayRoad, true}, {HighwayType::HighwayTrack, true}, + // The router truncates types to two levels, so we need this in addition to the long construction types + {HighwayType::HighwayConstruction, true}, + // Construction conterparts to each of the types above + // (needed for the map generator to include them in the routing section) + {HighwayType::HighwayConstructionMotorway, true}, {HighwayType::HighwayConstructionMotorwayLink, true}, + {HighwayType::HighwayConstructionTrunk, true}, {HighwayType::HighwayConstructionTrunkLink, true}, + {HighwayType::HighwayConstructionPrimary, true}, {HighwayType::HighwayConstructionPrimaryLink, true}, + {HighwayType::HighwayConstructionSecondary, true}, {HighwayType::HighwayConstructionSecondaryLink, true}, + {HighwayType::HighwayConstructionTertiary, true}, {HighwayType::HighwayConstructionTertiaryLink, true}, + {HighwayType::HighwayConstructionResidential, true}, {HighwayType::HighwayConstructionUnclassified, true}, + {HighwayType::HighwayConstructionService, true}, {HighwayType::HighwayConstructionLivingStreet, true}, + {HighwayType::HighwayConstructionRoad, true}, {HighwayType::HighwayConstructionTrack, true}, + // Non-highway types + {HighwayType::RouteShuttleTrain, true}, {HighwayType::RouteFerry, true}, + {HighwayType::ManMadePier, true}}; + +VehicleModel::LimitsInitList NoPassThroughLivingStreet() +{ + auto res = kDefaultOptions; + for (auto & e : res) + if (e.m_type == HighwayType::HighwayLivingStreet) + e.m_isPassThroughAllowed = false; + return res; +} + +VehicleModel::LimitsInitList NoPassThroughService(VehicleModel::LimitsInitList res = kDefaultOptions) +{ + for (auto & e : res) + if (e.m_type == HighwayType::HighwayService) + e.m_isPassThroughAllowed = false; + return res; +} + +VehicleModel::LimitsInitList NoTrack() +{ + VehicleModel::LimitsInitList res; + res.reserve(kDefaultOptions.size() - 1); + for (auto const & e : kDefaultOptions) + if (e.m_type != HighwayType::HighwayTrack) + res.push_back(e); + return res; +} + +VehicleModel::LimitsInitList NoPassThroughTrack() +{ + auto res = kDefaultOptions; + for (auto & e : res) + if (e.m_type == HighwayType::HighwayTrack) + e.m_isPassThroughAllowed = false; + return res; +} + +/* + * TODO are these entries meaningful in TraFF decoder context? + * They are stored in the vehicle model and retrieved with VehicleModel::GetSurfaceFactor(), which + * is called by VehicleModel::GetTypeSpeedImpl(). The latter is also called by + * `DecoderModel::GetSpeed()`, which seems to have the same callers as its `CarModel` counterpart, + * excluding tests. + */ +/// @todo Should make some compare constrains (like in CarModel_TrackVsGravelTertiary test) +/// to better fit these factors with reality. I have no idea, how they were set. +VehicleModel::SurfaceInitList const kDecoderSurface = { + // {{surfaceType, surfaceType}, {weightFactor, etaFactor}} + {{"psurface", "paved_good"}, {1.0, 1.0}}, + {{"psurface", "paved_bad"}, {0.6, 0.7}}, + {{"psurface", "unpaved_good"}, {0.4, 0.7}}, + {{"psurface", "unpaved_bad"}, {0.2, 0.3}}}; +} // namespace decoder_model + +namespace routing +{ +DecoderModel::DecoderModel() : DecoderModel(decoder_model::kDefaultOptions) {} + +DecoderModel::DecoderModel(VehicleModel::LimitsInitList const & roadLimits) + : VehicleModel(classif(), roadLimits, decoder_model::kDecoderSurface, + {decoder_model::kDefaultSpeeds, decoder_model::kDefaultFactors}) +{ + ASSERT_EQUAL(decoder_model::kDefaultFactors.size(), decoder_model::kDefaultSpeeds.size(), ()); + ASSERT_EQUAL(decoder_model::kDefaultOptions.size(), decoder_model::kDefaultSpeeds.size(), ()); + + std::vector hwtagYesCar = {"hwtag", "yescar"}; + auto const & cl = classif(); + + m_noType = cl.GetTypeByPath({"hwtag", "nocar"}); + m_yesType = cl.GetTypeByPath(hwtagYesCar); + + // Set small track speed if highway is not in kDefaultSpeeds (path, pedestrian), but marked as yescar. + AddAdditionalRoadTypes(cl, {{std::move(hwtagYesCar), decoder_model::kDefaultSpeeds.at(HighwayType::HighwayTrack)}}); + + /* + * Set max possible (reasonable) car speed. See EdgeEstimator::CalcHeuristic. + * Segment weight is distance-based, optionally with a penalty factor (>= 1). Thus maximum speed + * is 1 m/s, or 3.6 km/h. Rounding up as the assertion requires `kMaxCarSpeedKMpH` to be truly + * greater than the speed assumed for any road (cannot be equal). + * However, tests do not show a significant performance improvement over 200 km/h. + */ + SpeedKMpH constexpr kMaxCarSpeedKMpH(4.0); + CHECK_LESS(m_maxModelSpeed, kMaxCarSpeedKMpH, ()); + m_maxModelSpeed = kMaxCarSpeedKMpH; +} + +SpeedKMpH DecoderModel::GetSpeed(FeatureTypes const & types, SpeedParams const & speedParams) const +{ + return GetTypeSpeedImpl(types, speedParams, true /* isCar */); +} + +SpeedKMpH const & DecoderModel::GetOffroadSpeed() const +{ + return decoder_model::kSpeedOffroadKMpH; +} + +// static +DecoderModel const & DecoderModel::AllLimitsInstance() +{ + static DecoderModel const instance; + return instance; +} + +// static +VehicleModel::LimitsInitList const & DecoderModel::GetOptions() +{ + return decoder_model::kDefaultOptions; +} + +DecoderModelFactory::DecoderModelFactory(CountryParentNameGetterFn const & countryParentNameGetterFn) + : VehicleModelFactory(countryParentNameGetterFn) +{ + using namespace decoder_model; + using std::make_shared; + + // Names must be the same with country names from countries.txt + m_models[""] = make_shared(); + + /* + * This section should mirror the definitions in `CarModel`. Any deviations should be considered + * a bug, unless there is a comment stating that this is intentional (and ideally also the reason + * it is needed). + */ + m_models["Austria"] = make_shared(NoPassThroughLivingStreet()); + m_models["Belarus"] = make_shared(NoPassThroughLivingStreet()); + m_models["Brazil"] = make_shared(NoPassThroughService(NoPassThroughTrack())); + m_models["Denmark"] = make_shared(NoTrack()); + m_models["Germany"] = make_shared(NoPassThroughTrack()); + m_models["Hungary"] = make_shared(NoPassThroughLivingStreet()); + m_models["Poland"] = make_shared(NoPassThroughService()); + m_models["Romania"] = make_shared(NoPassThroughLivingStreet()); + m_models["Russian Federation"] = make_shared(NoPassThroughService(NoPassThroughLivingStreet())); + m_models["Slovakia"] = make_shared(NoPassThroughLivingStreet()); + m_models["Ukraine"] = make_shared(NoPassThroughService(NoPassThroughLivingStreet())); +} +} // namespace routing diff --git a/libs/routing_common/decoder_model.hpp b/libs/routing_common/decoder_model.hpp new file mode 100644 index 000000000..005756c26 --- /dev/null +++ b/libs/routing_common/decoder_model.hpp @@ -0,0 +1,51 @@ +#pragma once + +#include "routing_common/vehicle_model.hpp" + +namespace routing +{ + +/** + * @brief A `VehicleModel` suitable for TraFF location decoding. + * + * Each instance can have its own set of feature type-specific access limitations. These specify + * which roads can be used by this vehicle, and which roads can only be used for destination + * traffic. + * + * `DecoderModel` is similar to `CarModel`, with the following differences: + * + * Construction types are routable and treated like regular roads (the decoder may still + * exclude them on a case-by-case basis). + * + * Speed is assumed to be 1 m/s for any routable road, and 1 m/s divided by offline penalty for + * offroad. + * + * Instances are typically retrieved from `DecoderModelFactory` or the `AllLimitsInstance()` method. + */ +class DecoderModel : public VehicleModel +{ +public: + DecoderModel(); + explicit DecoderModel(LimitsInitList const & roadLimits); + + // VehicleModelInterface overrides: + SpeedKMpH GetSpeed(FeatureTypes const & types, SpeedParams const & speedParams) const override; + SpeedKMpH const & GetOffroadSpeed() const override; + + /** + * @brief Returns an instance with the default access limitations. + */ + static DecoderModel const & AllLimitsInstance(); + + /** + * @brief Returns the default access limitations. + */ + static LimitsInitList const & GetOptions(); +}; + +class DecoderModelFactory : public VehicleModelFactory +{ +public: + DecoderModelFactory(CountryParentNameGetterFn const & countryParentNameGetterF); +}; +} // namespace routing diff --git a/libs/routing_common/maxspeed_conversion.hpp b/libs/routing_common/maxspeed_conversion.hpp index 939e966c9..fc326553a 100644 --- a/libs/routing_common/maxspeed_conversion.hpp +++ b/libs/routing_common/maxspeed_conversion.hpp @@ -218,19 +218,53 @@ class Maxspeed MaxspeedType GetForward() const { return m_forward; } MaxspeedType GetBackward() const { return m_backward; } + /** + * @brief Whether the maxspeed is valid for the forward direction. + * + * Valid maxspeeds include numeric values as well as `kNoneMaxSpeed` and `kWalkMaxSpeed`. + * + * Only the forward direction is evaluated, maxspeed for the backward direction may still be invalid. + * To check for valid maxspeed in both directions, use `IsBidirectional()`. + * + * @return true if valid, false if not. + */ bool IsValid() const { return m_forward != kInvalidSpeed; } - /// \returns true if Maxspeed is considered as Bidirectional(). It means different - /// speed is set for forward and backward direction. Otherwise returns false. It means - /// |m_forward| speed should be used for the both directions. + + /** + * @brief Whether the maxspeed is valid for both directions. + * + * @return true if valid for both directions, false if not. + * + * @todo The documentation previously stated: + * “[Returns] true if Maxspeed is considered as Bidirectional(). It means different speed is set + * for forward and backward direction. Otherwise returns false. It means `m_forward` speed should + * be used for the both directions.” However, this is at odds with the actual code, which just + * checks for validity, not identity. + */ bool IsBidirectional() const { return IsValid() && m_backward != kInvalidSpeed; } - /// \brief returns speed according to |m_units|. |kInvalidSpeed|, |kNoneMaxSpeed| or - /// |kWalkMaxSpeed| may be returned. + /** + * @brief Returns maxspeed in native units. + * + * Native units are the units returned by `GetUnits()`. + * + * @param forward Whether to return maxspeed for the forward or backward direction. + * + * @return Speed in native units, or `kInvalidSpeed`, `kNoneMaxSpeed` or `kWalkMaxSpeed`. + */ MaxspeedType GetSpeedInUnits(bool forward) const; - /// \brief returns speed in km per hour. If it's not valid |kInvalidSpeed| is - /// returned. Otherwise forward or backward speed in km per hour is returned. |kNoneMaxSpeed| and - /// |kWalkMaxSpeed| are converted to some numbers. + /** + * @brief Returns maxspeed in km per hour. + * + * If the maxspeed is not valid `kInvalidSpeed` is returned. Otherwise forward or backward speed + * in km per hour is returned. `kNoneMaxSpeed` and `kWalkMaxSpeed` are converted to actual speeds + * which can be used directly. + * + * @param forward Whether to return maxspeed for the forward or backward direction. + * + * @return Speed in km/h, or `kInvalidSpeed`. + */ MaxspeedType GetSpeedKmPH(bool forward) const; private: diff --git a/libs/routing_common/num_mwm_id.hpp b/libs/routing_common/num_mwm_id.hpp index c5293930b..e20736b30 100644 --- a/libs/routing_common/num_mwm_id.hpp +++ b/libs/routing_common/num_mwm_id.hpp @@ -17,11 +17,21 @@ using NumMwmId = std::uint16_t; NumMwmId constexpr kFakeNumMwmId = std::numeric_limits::max(); NumMwmId constexpr kGeneratorMwmId = 0; +/** + * @brief A numbered list of country files. + */ class NumMwmIds final { public: bool IsEmpty() const { return m_idToFile.empty(); } + /** + * @brief Registers a file, i.e. adds it to the instance. + * + * If the instance already contains the file, this is a no-op. + * + * @param file + */ void RegisterFile(platform::CountryFile const & file) { if (ContainsFile(file)) @@ -34,16 +44,36 @@ class NumMwmIds final // LOG(LDEBUG, ("MWM:", file.GetName(), "=", id)); } + /** + * @brief Whether this instance contains a given file. + * @param file + * @return + */ bool ContainsFile(platform::CountryFile const & file) const { return m_fileToId.find(file) != m_fileToId.cend(); } + /** + * @brief Whether this instance contains a file at a given index. + * @param mwmId The index. + * @return + */ bool ContainsFileForMwm(NumMwmId mwmId) const { return mwmId < m_idToFile.size(); } + /** + * @brief Returns a file by index. + * @param mwmId The index. + * @return + */ platform::CountryFile const & GetFile(NumMwmId mwmId) const { ASSERT_LESS(mwmId, m_idToFile.size(), ()); return m_idToFile[mwmId]; } + /** + * @brief Returns the index for a given file. + * @param file + * @return + */ NumMwmId GetId(platform::CountryFile const & file) const { auto const it = m_fileToId.find(file); diff --git a/libs/routing_common/vehicle_model.cpp b/libs/routing_common/vehicle_model.cpp index 28d3308d0..d64085793 100644 --- a/libs/routing_common/vehicle_model.cpp +++ b/libs/routing_common/vehicle_model.cpp @@ -71,13 +71,29 @@ void VehicleModel::AddAdditionalRoadTypes(Classificator const & classif, Additio std::optional VehicleModel::GetHighwayType(FeatureTypes const & types) const { + /* + * Iterate through types. + * For each type, truncate to three levels and see if it has a match. If not, truncate to two + * levels and retry. + * First type in `types` which has a match wins over all subsequent ones. + * Within a type, three-level matches win over two-level ones. + */ for (uint32_t t : types) { - ftype::TruncValue(t, 2); + ftype::TruncValue(t, 3); auto const ret = GetHighwayType(t); if (ret) return *ret; + + if (ftype::GetLevel(t) > 2) + { + ftype::TruncValue(t, 2); + + auto const ret = GetHighwayType(t); + if (ret) + return *ret; + } } // For example Denmark has "No track" profile (see kCarOptionsDenmark), but tracks exist in MWM. @@ -361,6 +377,23 @@ string DebugPrint(HighwayType type) case HighwayType::HighwayPedestrian: return "highway-pedestrian"; case HighwayType::HighwayTrunkLink: return "highway-trunk_link"; case HighwayType::HighwayPrimaryLink: return "highway-primary_link"; + case HighwayType::HighwayConstruction: return "highway-construction"; + case HighwayType::HighwayConstructionMotorway: return "highway-construction-motorway"; + case HighwayType::HighwayConstructionMotorwayLink: return "highway-construction-motorway_link"; + case HighwayType::HighwayConstructionTrunk: return "highway-construction-trunk"; + case HighwayType::HighwayConstructionTrunkLink: return "highway-construction-trunk-link"; + case HighwayType::HighwayConstructionPrimary: return "highway-construction-primary"; + case HighwayType::HighwayConstructionPrimaryLink: return "highway-construction-primary_link"; + case HighwayType::HighwayConstructionSecondary: return "highway-construction-secondary"; + case HighwayType::HighwayConstructionSecondaryLink: return "highway-construction-secondary_link"; + case HighwayType::HighwayConstructionTertiary: return "highway-construction-tertiary"; + case HighwayType::HighwayConstructionTertiaryLink: return "highway-construction-tertiary_link"; + case HighwayType::HighwayConstructionResidential: return "highway-construction-residential"; + case HighwayType::HighwayConstructionUnclassified: return "highway-construction-unclassified"; + case HighwayType::HighwayConstructionService: return "highway-construction-service"; + case HighwayType::HighwayConstructionLivingStreet: return "highway-construction-living_street"; + case HighwayType::HighwayConstructionRoad: return "highway-construction-road"; + case HighwayType::HighwayConstructionTrack: return "highway-construction-track"; case HighwayType::ManMadePier: return "man_made-pier"; case HighwayType::HighwayBridleway: return "highway-bridleway"; case HighwayType::HighwaySecondaryLink: return "highway-secondary_link"; @@ -390,7 +423,16 @@ void FromString(std::string_view s, HighwayType & highwayType) HighwayType::HighwayTrunk, HighwayType::HighwayPedestrian, HighwayType::HighwayTrunkLink, HighwayType::HighwayPrimaryLink, HighwayType::ManMadePier, HighwayType::HighwayBridleway, HighwayType::HighwaySecondaryLink, HighwayType::RouteFerry, HighwayType::HighwayTertiaryLink, - HighwayType::HighwayBusway, HighwayType::RouteShuttleTrain}; + HighwayType::HighwayBusway, HighwayType::RouteShuttleTrain, HighwayType::HighwayConstruction, + HighwayType::HighwayConstructionMotorway, HighwayType::HighwayConstructionMotorwayLink, + HighwayType::HighwayConstructionTrunk, HighwayType::HighwayConstructionTrunkLink, + HighwayType::HighwayConstructionPrimary, HighwayType::HighwayConstructionPrimaryLink, + HighwayType::HighwayConstructionSecondary, HighwayType::HighwayConstructionSecondaryLink, + HighwayType::HighwayConstructionTertiary, HighwayType::HighwayConstructionTertiaryLink, + HighwayType::HighwayConstructionResidential, HighwayType::HighwayConstructionUnclassified, + HighwayType::HighwayConstructionService, HighwayType::HighwayConstructionLivingStreet, + HighwayType::HighwayConstructionRoad, HighwayType::HighwayConstructionTrack, + }; for (auto type : allTypes) map[DebugPrint(type)] = type; diff --git a/libs/routing_common/vehicle_model.hpp b/libs/routing_common/vehicle_model.hpp index 2eeb6b41e..490b951f9 100644 --- a/libs/routing_common/vehicle_model.hpp +++ b/libs/routing_common/vehicle_model.hpp @@ -52,6 +52,24 @@ enum class HighwayType : uint16_t HighwayTrunkLink = 90, HighwayPrimaryLink = 95, ManMadePier = 119, + // HighwayConstruction and its subtypes are needed for traffic message decoding + HighwayConstruction = 162, + HighwayConstructionMotorway = 789, + HighwayConstructionMotorwayLink = 790, + HighwayConstructionTrunk = 791, + HighwayConstructionTrunkLink = 792, + HighwayConstructionPrimary = 793, + HighwayConstructionPrimaryLink = 794, + HighwayConstructionSecondary = 795, + HighwayConstructionSecondaryLink = 796, + HighwayConstructionTertiary = 797, + HighwayConstructionTertiaryLink = 798, + HighwayConstructionResidential = 799, + HighwayConstructionUnclassified = 800, + HighwayConstructionService = 801, + HighwayConstructionLivingStreet = 802, + HighwayConstructionRoad = 803, + HighwayConstructionTrack = 804, HighwayBridleway = 167, HighwaySecondaryLink = 176, RouteFerry = 259, diff --git a/libs/storage/storage.hpp b/libs/storage/storage.hpp index 6b1bfa4e0..847efd1b7 100644 --- a/libs/storage/storage.hpp +++ b/libs/storage/storage.hpp @@ -150,14 +150,15 @@ struct NodeStatuses bool m_groupNode; }; -// This class is used for downloading, updating and deleting maps. -// Storage manages a queue of mwms to be downloaded. -// Every operation with this queue must be executed -// on the storage thread. In the current implementation, the storage -// thread coincides with the main (UI) thread. -// Downloading of only one mwm at a time is supported, so while the -// mwm at the top of the queue is being downloaded (or updated by -// applying a diff file) all other mwms have to wait. +/** + * @brief The Storage class is used for downloading, updating and deleting maps. + * + * Storage manages a queue of mwms to be downloaded. Every operation with this queue must be + * executed on the storage thread. In the current implementation, the storage thread coincides with + * the main (UI) thread. Downloading of only one mwm at a time is supported, so while the mwm at the + * top of the queue is being downloaded (or updated by applying a diff file) all other mwms have to + * wait. + */ class Storage final : public QueuedCountry::Subscriber { public: diff --git a/libs/traffic/pytraffic/bindings.cpp b/libs/traffic/pytraffic/bindings.cpp index b2278e500..e6c197f8b 100644 --- a/libs/traffic/pytraffic/bindings.cpp +++ b/libs/traffic/pytraffic/bindings.cpp @@ -103,6 +103,11 @@ boost::python::list GenerateTrafficKeys(std::string const & mwmPath) return pyhelpers::StdVectorToPythonList(result); } +/* + * TODO Inherited from MWM/OM, no longer works with TraFF logic (API logic changed). + * We no longer separate keys (segments IDs) from values (their speed groups). + * See if we can refactor this into something meaningful and useful, else remove. + */ std::vector GenerateTrafficValues(std::vector const & keys, boost::python::dict const & segmentMappingDict, uint8_t useTempBlock) { @@ -139,6 +144,13 @@ std::vector GenerateTrafficValues(std::vector GenerateTrafficValuesFromList(boost::python::list const & keys, boost::python::dict const & segmentMappingDict) { @@ -148,6 +160,13 @@ std::vector GenerateTrafficValuesFromList(boost::python::list const & k return GenerateTrafficValues(keysVec, segmentMappingDict, 1 /* useTempBlock */); } +/* + * TODO Inherited from MWM/OM, no longer works with TraFF logic (API logic changed). + * We no longer separate keys (segments IDs) from values (their speed groups), nor do we store + * either in binary files: Segment/speed group pairs are generated from TraFF data and cached in + * XML format (using a custom extension to TraFF). + * See if we can refactor this into something meaningful and useful, else remove. + */ std::vector GenerateTrafficValuesFromBinary(std::vector const & keysBlob, boost::python::dict const & segmentMappingDict, uint8_t useTempBlock = 1) @@ -201,7 +220,9 @@ BOOST_PYTHON_MODULE(pytraffic) def("load_classificator", LoadClassificator); def("generate_traffic_keys", GenerateTrafficKeys); + // TODO obsolete, see function definition def("generate_traffic_values_from_list", GenerateTrafficValuesFromList); + // TODO obsolete, see function definition def("generate_traffic_values_from_binary", GenerateTrafficValuesFromBinary, (arg("keysBlob"), arg("segmentMappingDict"), arg("useTempBlock") = 1)); } diff --git a/libs/traffic/speed_groups.hpp b/libs/traffic/speed_groups.hpp index 385e68707..23c0da9ec 100644 --- a/libs/traffic/speed_groups.hpp +++ b/libs/traffic/speed_groups.hpp @@ -5,6 +5,15 @@ namespace traffic { +/** + * A bucket for the ratio of the speed of moving traffic to the posted speed limit. + * + * Let Vmax be the posted speed limit and Vreal the speed at which traffic is currently flowing + * or expected to flow. The possible ratios (Vreal/Vmax) are grouped into buckets and, from then + * on, only the bucket number is used. + * + * The threshold ratios for the individual values are defined in `kSpeedGroupThresholdPercentage`. + */ enum class SpeedGroup : uint8_t { G0 = 0, @@ -20,24 +29,33 @@ enum class SpeedGroup : uint8_t static_assert(static_cast(SpeedGroup::Count) <= 8, ""); -// Let M be the maximal speed that is possible on a free road -// and let V be the maximal speed that is possible on this road when -// taking the traffic data into account. -// We group all possible ratios (V/M) into a small number of -// buckets and only use the number of a bucket everywhere. -// That is, we forget the specific values of V when transmitting and -// displaying traffic information. The value M of a road is known at the -// stage of building the mwm containing this road. -// -// kSpeedGroupThresholdPercentage[g] denotes the maximal value of (V/M) -// that is possible for group |g|. Values falling on a border of two groups -// may belong to either group. -// -// The threshold percentage is defined to be 100 for the -// special groups where V is unknown or not defined. +/** + * Threshold ratios for the individual values of `SpeedGroup`. + * + * Let Vmax be the posted speed limit and Vreal the speed at which traffic is currently flowing + * or expected to flow. The possible ratios (Vreal/Vmax) are grouped into buckets and, from then + * on, only the bucket number is used. + * + * `kSpeedGroupThresholdPercentage[g]` is the maximum percentage of Vreal/Vmax for group g. Values + * falling on the border of two groups may belong to either group. + * + * For special groups, where Vreal/Vmax is unknown or undefined, the threshold is 100%. + */ extern uint32_t const kSpeedGroupThresholdPercentage[static_cast(SpeedGroup::Count)]; -/// \note This method is used in traffic jam generation. +/** + * Converts the ratio between speed of flowing traffic and the posted limit to a `SpeedGroup`. + * + * This method is used in traffic jam generation: Let Vmax be the posted speed limit and Vreal the + * speed at which traffic is currently flowing or expected to flow. The possible ratios + * (Vreal/Vmax) are grouped into buckets and, from then on, only the bucket number is used. + * + * This method performs the conversion from the ratio to a `SpeedGroup` bucket. + * + * @param p Vreal / Vmax * 100% (ratio expressed in percent) + * + * @return the `SpeedGroup` value which corresponds to `p` + */ SpeedGroup GetSpeedGroupByPercentage(double p); std::string DebugPrint(SpeedGroup const & group); diff --git a/libs/traffic/traffic_info.cpp b/libs/traffic/traffic_info.cpp index ce218c896..e6bb11b7a 100644 --- a/libs/traffic/traffic_info.cpp +++ b/libs/traffic/traffic_info.cpp @@ -33,49 +33,6 @@ namespace traffic { using namespace std; -namespace -{ -bool ReadRemoteFile(string const & url, vector & contents, int & errorCode) -{ - platform::HttpClient request(url); - if (!request.RunHttpRequest()) - { - errorCode = request.ErrorCode(); - LOG(LINFO, ("Couldn't run traffic request", url, ". Error:", errorCode)); - return false; - } - - errorCode = request.ErrorCode(); - - string const & result = request.ServerResponse(); - contents.resize(result.size()); - memcpy(contents.data(), result.data(), result.size()); - - if (errorCode != 200) - { - LOG(LINFO, ("Traffic request", url, "failed. HTTP Error:", errorCode)); - return false; - } - - return true; -} - -string MakeRemoteURL(string const & name, uint64_t version) -{ - if (string(TRAFFIC_DATA_BASE_URL).empty()) - return {}; - - stringstream ss; - ss << TRAFFIC_DATA_BASE_URL; - if (version != 0) - ss << version << "/"; - ss << url::UrlEncode(name) << TRAFFIC_FILE_EXTENSION; - return ss.str(); -} - -char constexpr kETag[] = "etag"; -} // namespace - // TrafficInfo::RoadSegmentId ----------------------------------------------------------------- TrafficInfo::RoadSegmentId::RoadSegmentId() : m_fid(0), m_idx(0), m_dir(0) {} @@ -88,46 +45,11 @@ TrafficInfo::RoadSegmentId::RoadSegmentId(uint32_t fid, uint16_t idx, uint8_t di uint8_t const TrafficInfo::kLatestKeysVersion = 0; uint8_t const TrafficInfo::kLatestValuesVersion = 0; -TrafficInfo::TrafficInfo(MwmSet::MwmId const & mwmId, int64_t currentDataVersion) +TrafficInfo::TrafficInfo(MwmSet::MwmId const & mwmId, Coloring && coloring) : m_mwmId(mwmId) - , m_currentDataVersion(currentDataVersion) + , m_coloring(std::move(coloring)) { - if (!mwmId.IsAlive()) - { - LOG(LWARNING, ("Attempt to create a traffic info for dead mwm.")); - return; - } - string const mwmPath = mwmId.GetInfo()->GetLocalFile().GetPath(MapFileType::Map); - try - { - FilesContainerR rcont(mwmPath); - if (rcont.IsExist(TRAFFIC_KEYS_FILE_TAG)) - { - auto reader = rcont.GetReader(TRAFFIC_KEYS_FILE_TAG); - vector buf(static_cast(reader.Size())); - reader.Read(0, buf.data(), buf.size()); - LOG(LINFO, ("Reading keys for", mwmId, "from section")); - try - { - DeserializeTrafficKeys(buf, m_keys); - } - catch (Reader::Exception const & e) - { - auto const info = mwmId.GetInfo(); - LOG(LINFO, - ("Could not read traffic keys from section. MWM:", info->GetCountryName(), "Version:", info->GetVersion())); - } - } - else - { - LOG(LINFO, ("Reading traffic keys for", mwmId, "from the web")); - ReceiveTrafficKeys(); - } - } - catch (RootException const & e) - { - LOG(LWARNING, ("Could not initialize traffic keys")); - } + m_availability = Availability::IsAvailable; } // static @@ -144,19 +66,6 @@ void TrafficInfo::SetTrafficKeysForTesting(vector const & keys) m_availability = Availability::IsAvailable; } -bool TrafficInfo::ReceiveTrafficData(string & etag) -{ - vector values; - switch (ReceiveTrafficValues(etag, values)) - { - case ServerDataStatus::New: return UpdateTrafficData(values); - case ServerDataStatus::NotChanged: return true; - case ServerDataStatus::NotFound: - case ServerDataStatus::Error: return false; - } - return false; -} - SpeedGroup TrafficInfo::GetSpeedGroup(RoadSegmentId const & id) const { auto const it = m_coloring.find(id); @@ -216,70 +125,6 @@ void TrafficInfo::CombineColorings(vector const & ke ASSERT_EQUAL(numUnexpectedKeys, 0, ()); } -// static -void TrafficInfo::SerializeTrafficKeys(vector const & keys, vector & result) -{ - vector fids; - vector numSegs; - vector oneWay; - for (size_t i = 0; i < keys.size();) - { - size_t j = i; - while (j < keys.size() && keys[i].m_fid == keys[j].m_fid) - ++j; - - bool ow = true; - for (size_t k = i; k < j; ++k) - { - if (keys[k].m_dir == RoadSegmentId::kReverseDirection) - { - ow = false; - break; - } - } - - auto const numDirs = ow ? 1 : 2; - size_t numSegsForThisFid = j - i; - CHECK_GREATER(numDirs, 0, ()); - CHECK_EQUAL(numSegsForThisFid % numDirs, 0, ()); - numSegsForThisFid /= numDirs; - - fids.push_back(keys[i].m_fid); - numSegs.push_back(numSegsForThisFid); - oneWay.push_back(ow); - - i = j; - } - - MemWriter> memWriter(result); - WriteToSink(memWriter, kLatestKeysVersion); - WriteVarUint(memWriter, fids.size()); - - { - BitWriter bitWriter(memWriter); - - uint32_t prevFid = 0; - for (auto const & fid : fids) - { - uint64_t const fidDiff = static_cast(fid - prevFid); - bool ok = coding::GammaCoder::Encode(bitWriter, fidDiff + 1); - ASSERT(ok, ()); - UNUSED_VALUE(ok); - prevFid = fid; - } - - for (auto const & s : numSegs) - { - bool ok = coding::GammaCoder::Encode(bitWriter, s + 1); - ASSERT(ok, ()); - UNUSED_VALUE(ok); - } - - for (auto const val : oneWay) - bitWriter.Write(val ? 1 : 0, 1 /* numBits */); - } -} - // static void TrafficInfo::DeserializeTrafficKeys(vector const & data, vector & result) { @@ -352,166 +197,6 @@ void TrafficInfo::SerializeTrafficValues(vector const & values, vect deflate(buf.data(), buf.size(), back_inserter(result)); } -// static -void TrafficInfo::DeserializeTrafficValues(vector const & data, vector & result) -{ - using Inflate = coding::ZLib::Inflate; - - vector decompressedData; - - Inflate inflate(Inflate::Format::ZLib); - inflate(data.data(), data.size(), back_inserter(decompressedData)); - - MemReaderWithExceptions memReader(decompressedData.data(), decompressedData.size()); - ReaderSource src(memReader); - - auto const version = ReadPrimitiveFromSource(src); - CHECK_EQUAL(version, kLatestValuesVersion, ("Unsupported version of traffic keys.")); - - auto const n = ReadVarUint(src); - result.resize(n); - BitReader bitReader(src); - for (size_t i = 0; i < static_cast(n); ++i) - { - // SpeedGroup's values fit into 3 bits. - result[i] = static_cast(bitReader.Read(3)); - } - - ASSERT_EQUAL(src.Size(), 0, ()); -} - -// todo(@m) This is a temporary method. Do not refactor it. -bool TrafficInfo::ReceiveTrafficKeys() -{ - if (!m_mwmId.IsAlive()) - return false; - auto const & info = m_mwmId.GetInfo(); - if (!info) - return false; - - string const url = MakeRemoteURL(info->GetCountryName(), info->GetVersion()); - - if (url.empty()) - return false; - - vector contents; - int errorCode; - if (!ReadRemoteFile(url + ".keys", contents, errorCode)) - return false; - if (errorCode != 200) - { - LOG(LWARNING, ("Network error when reading keys")); - return false; - } - - vector keys; - try - { - DeserializeTrafficKeys(contents, keys); - } - catch (Reader::Exception const & e) - { - LOG(LINFO, ("Could not read traffic keys received from server. MWM:", info->GetCountryName(), - "Version:", info->GetVersion())); - return false; - } - m_keys.swap(keys); - return true; -} - -TrafficInfo::ServerDataStatus TrafficInfo::ReceiveTrafficValues(string & etag, vector & values) -{ - if (!m_mwmId.IsAlive()) - return ServerDataStatus::Error; - - auto const & info = m_mwmId.GetInfo(); - if (!info) - return ServerDataStatus::Error; - - auto const version = info->GetVersion(); - string const url = MakeRemoteURL(info->GetCountryName(), version); - - if (url.empty()) - return ServerDataStatus::Error; - - platform::HttpClient request(url); - request.LoadHeaders(true); - request.SetRawHeader("If-None-Match", etag); - - if (!request.RunHttpRequest() || request.ErrorCode() != 200) - return ProcessFailure(request, version); - try - { - string const & response = request.ServerResponse(); - vector contents(response.cbegin(), response.cend()); - DeserializeTrafficValues(contents, values); - } - catch (Reader::Exception const & e) - { - m_availability = Availability::NoData; - LOG(LWARNING, ("Could not read traffic values received from server. MWM:", info->GetCountryName(), - "Version:", info->GetVersion())); - return ServerDataStatus::Error; - } - // Update ETag for this MWM. - auto const & headers = request.GetHeaders(); - auto const it = headers.find(kETag); - if (it != headers.end()) - etag = it->second; - - m_availability = Availability::IsAvailable; - return ServerDataStatus::New; -} - -bool TrafficInfo::UpdateTrafficData(vector const & values) -{ - m_coloring.clear(); - - if (m_keys.size() != values.size()) - { - LOG(LWARNING, ("The number of received traffic values does not correspond to the number of keys:", m_keys.size(), - "keys", values.size(), "values.")); - m_availability = Availability::NoData; - return false; - } - - for (size_t i = 0; i < m_keys.size(); ++i) - if (values[i] != SpeedGroup::Unknown) - m_coloring.emplace(m_keys[i], values[i]); - - return true; -} - -TrafficInfo::ServerDataStatus TrafficInfo::ProcessFailure(platform::HttpClient const & request, - int64_t const mwmVersion) -{ - switch (request.ErrorCode()) - { - case 404: /* Not Found */ - { - int64_t version = 0; - VERIFY(strings::to_int64(request.ServerResponse().c_str(), version), ()); - - if (version > mwmVersion && version <= m_currentDataVersion) - m_availability = Availability::ExpiredData; - else if (version > m_currentDataVersion) - m_availability = Availability::ExpiredApp; - else - m_availability = Availability::NoData; - return ServerDataStatus::NotFound; - } - case 304: /* Not Modified */ - { - m_availability = Availability::IsAvailable; - return ServerDataStatus::NotChanged; - } - } - - m_availability = Availability::Unknown; - - return ServerDataStatus::Error; -} - string DebugPrint(TrafficInfo::RoadSegmentId const & id) { string const dir = id.m_dir == TrafficInfo::RoadSegmentId::kForwardDirection ? "Forward" : "Backward"; diff --git a/libs/traffic/traffic_info.hpp b/libs/traffic/traffic_info.hpp index 5981a8aab..8e1776135 100644 --- a/libs/traffic/traffic_info.hpp +++ b/libs/traffic/traffic_info.hpp @@ -15,23 +15,49 @@ class HttpClient; namespace traffic { -// This class is responsible for providing the real-time -// information about road traffic for one mwm file. +/** + * @brief The `TrafficInfo` class is responsible for providing the real-time information about road + * traffic for one MWM. + */ class TrafficInfo { public: static uint8_t const kLatestKeysVersion; static uint8_t const kLatestValuesVersion; + /** + * @brief Whether traffic data is available in this `TrafficInfo` instance. + */ + /* + * TODO A global traffic update would require some 2–3 states: + * * IsAvailable + * * Data available but not yet decoded + * * (possibly) No traffic reports for this MWM + */ enum class Availability { + /** This `TrafficInfo` instance has data available. */ IsAvailable, + /** No traffic data is available (file not found on the server, or server returned invalid data). */ NoData, + /** Traffic data could not be retrieved because the map data is outdated. */ ExpiredData, + /** Traffic data could not be retrieved because the app version is outdated. */ ExpiredApp, + /** No traffic data is available because the server responded with an error (other than “not found”), or no request was made yet. */ Unknown }; + /** + * @brief The RoadSegmentId struct models a segment of a road. + * + * A road segment is the link between two consecutive points of an OSM way. The way must be + * tagged with a valid `highway` tag. A segment refers to a single direction. + * + * Therefore, an OSM way with `n` points has `n - 1` segments if tagged as one-way, `2 (n - 1)` + * otherwise (as each pair of adjacent points is connected by two segments, one in each + * direction.) + */ struct RoadSegmentId { // m_dir can be kForwardDirection or kReverseDirection. @@ -68,84 +94,102 @@ class TrafficInfo uint8_t m_dir : 1; }; + /** + * @brief Mapping from feature segments to speed groups (see `speed_groups.hpp`), for one MWM. + */ // todo(@m) unordered_map? using Coloring = std::map; TrafficInfo() = default; - TrafficInfo(MwmSet::MwmId const & mwmId, int64_t currentDataVersion); + TrafficInfo(MwmSet::MwmId const & mwmId, Coloring && coloring); + /** + * @brief Returns a `TrafficInfo` instance with pre-populated traffic information. + * @param coloring The traffic information (road segments and their speed group) + * @return The new `TrafficInfo` instance + */ static TrafficInfo BuildForTesting(Coloring && coloring); void SetTrafficKeysForTesting(std::vector const & keys); - // Fetches the latest traffic data from the server and updates the coloring and ETag. - // Construct the url by passing an MwmId. - // The ETag or entity tag is part of HTTP, the protocol for the World Wide Web. - // It is one of several mechanisms that HTTP provides for web cache validation, - // which allows a client to make conditional requests. - // *NOTE* This method must not be called on the UI thread. - bool ReceiveTrafficData(std::string & etag); - - // Returns the latest known speed group by a feature segment's id - // or SpeedGroup::Unknown if there is no information about the segment. + /** + * @brief Returns the latest known speed group by a feature segment's ID. + * @param id The road segment ID. + * @return The speed group, or `SpeedGroup::Unknown` if no information is available. + */ SpeedGroup GetSpeedGroup(RoadSegmentId const & id) const; MwmSet::MwmId const & GetMwmId() const { return m_mwmId; } Coloring const & GetColoring() const { return m_coloring; } Availability GetAvailability() const { return m_availability; } - // Extracts RoadSegmentIds from mwm and stores them in a sorted order. + /** + * @brief Extracts RoadSegmentIds from an MWM and stores them in a sorted order. + * @param mwmPath Path to the MWM file + * + * @todo We don’t need this any longer as the API has been reworked: We no longer separate keys + * (segment IDs) from values (their speed groups) and no longer have a use case for retrieving a + * list of all possible segment IDs – rather, we decode TraFF messages into segments, or have a + * cached list of the segments affected by a particular message. However, pytraffic still has some + * references to this function. We need to clean those up first, then we can delete this function. + */ static void ExtractTrafficKeys(std::string const & mwmPath, std::vector & result); - // Adds the unknown values to the partially known coloring map |knownColors| - // so that the keys of the resulting map are exactly |keys|. + /** + * @brief Adds unknown values to a partially known coloring map. + * + * After this method returns, the keys of `result` will be exactly `keys`. The speed group + * associated with each key will be the same as in `knownColors`, or `SpeedGroup::Unknown` for + * keys which are not found in `knownColors`. + * + * Keys in `knownColors` which are not in `keys` will be ignored. + * + * If `result` contains mappings prior to this method being called, they will be deleted. + * + * @param keys The keys for the result map. + * @param knownColors The map containing the updates. + * @param result The map to be updated. + */ static void CombineColorings(std::vector const & keys, TrafficInfo::Coloring const & knownColors, TrafficInfo::Coloring & result); // Serializes the keys of the coloring map to |result|. // The keys are road segments ids which do not change during // an mwm's lifetime so there's no point in downloading them every time. - // todo(@m) Document the format. - static void SerializeTrafficKeys(std::vector const & keys, std::vector & result); - + /* + * TODO We don’t need these any longer as the format is obsolete, but pytraffic still has some + * references to these. We need to clean those up first, then we can delete these functions. + */ static void DeserializeTrafficKeys(std::vector const & data, std::vector & result); static void SerializeTrafficValues(std::vector const & values, std::vector & result); - static void DeserializeTrafficValues(std::vector const & data, std::vector & result); - private: + /** + * @brief Result of the last request to the server. + */ enum class ServerDataStatus { + /** New data was returned. */ New, + /** Data has not changed since the last request. */ NotChanged, + /** The URL was not found on the server. */ NotFound, + /** An error prevented data from being requested, or the server responded with an error. */ Error, }; - friend void UnitTest_TrafficInfo_UpdateTrafficData(); - - // todo(@m) A temporary method. Remove it once the keys are added - // to the generator and the data is regenerated. - bool ReceiveTrafficKeys(); - - // Tries to read the values of the Coloring map from server into |values|. - // Returns result of communicating with server as ServerDataStatus. - // Otherwise, returns false and does not change m_coloring. - ServerDataStatus ReceiveTrafficValues(std::string & etag, std::vector & values); - - // Updates the coloring and changes the availability status if needed. - bool UpdateTrafficData(std::vector const & values); - - ServerDataStatus ProcessFailure(platform::HttpClient const & request, int64_t const mwmVersion); - - // The mapping from feature segments to speed groups (see speed_groups.hpp). + /** + * @brief The mapping from feature segments to speed groups (see speed_groups.hpp). + */ Coloring m_coloring; - // The keys of the coloring map. The values are downloaded periodically - // and combined with the keys to form m_coloring. - // *NOTE* The values must be received in the exact same order that the - // keys are saved in. + /** + * @brief The keys of the coloring map. The values are downloaded periodically + * and combined with the keys to form `m_coloring`. + * *NOTE* The values must be received in the exact same order that the keys are saved in. + */ std::vector m_keys; MwmSet::MwmId m_mwmId; diff --git a/libs/traffic/traffic_tests/traffic_info_test.cpp b/libs/traffic/traffic_tests/traffic_info_test.cpp index 003ef0c97..b638f5dc0 100644 --- a/libs/traffic/traffic_tests/traffic_info_test.cpp +++ b/libs/traffic/traffic_tests/traffic_info_test.cpp @@ -37,7 +37,11 @@ class TestMwmSet : public MwmSet }; } // namespace -/// @todo Need TRAFFIC_DATA_BASE_URL for this test. +/* + * TODO Inherited from MWM/OM, no longer works with TraFF logic (API logic changed). + * Leaving it here for now, maybe we can derive some TraFF tests from it. + * This tests retrieval of traffic information from the server. + */ /* UNIT_TEST(TrafficInfo_RemoteFile) { @@ -69,6 +73,13 @@ UNIT_TEST(TrafficInfo_RemoteFile) } */ +/* + * TODO Inherited from MWM/OM, no longer works with TraFF logic (API logic changed). + * Leaving it here for now, maybe we can derive some TraFF tests from it. + * This tests serialization of traffic data to files and reading it back, results should be + * identical and satisfy whatever constraints there are in the app. + */ +/* UNIT_TEST(TrafficInfo_Serialization) { TrafficInfo::Coloring coloring = { @@ -114,7 +125,14 @@ UNIT_TEST(TrafficInfo_Serialization) TEST_EQUAL(values, deserializedValues, ()); } } +*/ +/* + * TODO Inherited from MWM/OM, no longer works with TraFF logic (API logic changed). + * Leaving it here for now, maybe we can derive some TraFF tests from it. + * This tests processing of updated traffic data. + */ +/* UNIT_TEST(TrafficInfo_UpdateTrafficData) { vector const keys = { @@ -147,4 +165,5 @@ UNIT_TEST(TrafficInfo_UpdateTrafficData) for (size_t i = 0; i < keys.size(); ++i) TEST_EQUAL(info.GetSpeedGroup(keys[i]), values2[i], ()); } +*/ } // namespace traffic diff --git a/libs/traffxml/CMakeLists.txt b/libs/traffxml/CMakeLists.txt new file mode 100644 index 000000000..937805518 --- /dev/null +++ b/libs/traffxml/CMakeLists.txt @@ -0,0 +1,21 @@ +project(traffxml) + +set(SRC + traff_decoder.cpp + traff_decoder.hpp + traff_model.cpp + traff_model.hpp + traff_model_xml.cpp + traff_model_xml.hpp + traff_source.cpp + traff_source.hpp + traff_storage.cpp + traff_storage.hpp +) + +omim_add_library(${PROJECT_NAME} ${SRC}) + +target_link_libraries(${PROJECT_NAME} + pugixml + coding +) diff --git a/libs/traffxml/traff_decoder.cpp b/libs/traffxml/traff_decoder.cpp new file mode 100644 index 000000000..62622482b --- /dev/null +++ b/libs/traffxml/traff_decoder.cpp @@ -0,0 +1,1670 @@ +#include "traffxml/traff_decoder.hpp" + +//#include "traffxml/traff_foo.hpp" + +#include "geometry/distance_on_sphere.hpp" +#include "geometry/mercator.hpp" + +#include "indexer/feature.hpp" +#include "indexer/road_shields_parser.hpp" + +// Only needed for OpenlrTraffDecoder, see below +#if 0 +#include "openlr/decoded_path.hpp" +#include "openlr/openlr_decoder.hpp" +#include "openlr/openlr_model.hpp" +#endif + +#include "routing/checkpoints.hpp" +#include "routing/edge_estimator.hpp" +#include "routing/maxspeeds.hpp" +#include "routing/route.hpp" +#include "routing/router_delegate.hpp" +#include "routing/routing_helpers.hpp" + +#include "routing_common/decoder_model.hpp" +#include "routing_common/maxspeed_conversion.hpp" + +#include "storage/routing_helpers.hpp" + +#include "traffic/traffic_cache.hpp" + +#include + +namespace traffxml +{ +enum class RefParserState +{ + Whitespace, + Alpha, + Numeric +}; + +// Only needed for OpenlrTraffDecoder, see below +#if 0 +// Number of worker threads for the OpenLR decoder +/* + * TODO how to determine the best number of worker threads? + * One per direction? Does not seem to help with bidirectional locations (two reference points). + * One per segment (from–via/from–at, via–to/at–to)? Not yet tested. + * Otherwise there is little to be gained, as we decode messages one at a time. + */ +auto constexpr kNumDecoderThreads = 1; +#endif + +// Timeout for the router in seconds, used by RoutingTraffDecoder +// TODO set to a sensible value +auto constexpr kRouterTimeoutSec = 30; + +/* + * One meter per second. The TraffEstimator works on distance in meters, not travel time. For code + * which works with speeds and assumes cost to be time-based, a speed of 1 m/s means such + * calculations will effectively return distances in meters. + */ +auto constexpr kOneMpSInKmpH = 3.6; + +/* + * Penalty factor for using a fake segment to get to a nearby road. + * Offroad penalty applies to direct distance whereas road penalty applies to roads, which can be up + * to around 3 times the direct distance (theoretically unlimited). Therefore, a factor of 3–4 times + * the penalty of a well-matched road may be needed to avoid competing with the correct route. + * On the other hand, a very high offroad penalty would give preference to a poorly matched route + * over a well-matched one if it is closer to the reference points. + * Maximum penalty for roads is currently 64 (4 for ramps * 4 for road type * 4 for ref). + * A well-matched road may still have a penalty of around 4 (twice the reduced attribute penalty, or + * once the full attribute penalty). + * A “wrong” road may also just have a penalty of 4 (e.g. road name mismatch, but road class and + * ramp type match). + * A value of 16 has worked well for the DE-B2R-SendlingSued-Passauerstrasse test case. (The + * DE-A10-Werder-GrossKreutz or DE-A115-PotsdamDrewitz-Nuthetal test cases gave incorrect results + * due to lack of fake segments, which was fixed through truncation and now works correctly even + * with an offroad penalty of 128.) + */ +auto constexpr kOffroadPenalty = 16; + +/* + * Penalty factor for non-matching attributes + */ +auto constexpr kAttributePenalty = 4; + +/* + * Penalty factor for partially matching attributes + */ +auto constexpr kReducedAttributePenalty = 2; + +/* + * Lower and upper boundary for radius around endpoint in which to search for junctions, depending + * on the road class. Boundaries are in meters. If the lower boundary exceeds half the distance + * between endpoints, the latter is used instead. + */ +const std::unordered_map, std::array> kJunctionRadiusBoundaries{ + { RoadClass::Motorway, { {300.0, 500.0} } }, + { RoadClass::Trunk, { {300.0, 500.0} } }, + { RoadClass::Primary, { {200.0, 300.0} } }, + { RoadClass::Secondary, { {150.0, 200.0} } }, + { RoadClass::Tertiary, { {0.0, 150.0} } }, + { RoadClass::Other, { {0.0, 150.0} } }, + { std::nullopt, { {300.0, 500.0} } } +}; + +/* + * Maximum distance in meters from location endpoint at which a turn penalty is applied + */ +auto constexpr kTurnPenaltyMaxDist = 100.0; + +/* + * Minimum angle in degrees at which turn penalty is applied + */ +auto constexpr kTurnPenaltyMinAngle = 65.0; + +/* + * Minimum angle in degrees at which the full turn penalty is applied + */ +auto constexpr kTurnPenaltyFullAngle = 90.0; + +/* + * Penalty applied to impassable ways + */ +auto constexpr kImpassablePenalty = 1.0E4; + +/* + * Invalid feature ID. + * Borrowed from indexer/feature_decl.hpp. + */ +uint32_t constexpr kInvalidFeatureId = std::numeric_limits::max(); + +TraffDecoder::TraffDecoder(DataSource & dataSource, CountryInfoGetterFn countryInfoGetter, + const CountryParentNameGetterFn & countryParentNameGetter, + std::map & messageCache) + : m_dataSource(dataSource) + , m_countryInfoGetterFn(countryInfoGetter) + , m_countryParentNameGetterFn(countryParentNameGetter) + , m_messageCache(messageCache) +{} + +void TraffDecoder::ApplyTrafficImpact(traffxml::TrafficImpact & impact, traffxml::MultiMwmColoring & decoded) +{ + traffic::SpeedGroup fromDelay = traffic::SpeedGroup::Unknown; + + /* + * Convert delay into speed group (skip if the location is blocked). + * TODO: mapping delay to speed group is not optimal, as a long delay on a short location can + * increase travel time by several orders of magnitude. + * Assume a stretch of road with a speed limit of 60 km/h, i.e. 1 km/min, and a delay of 30 min. + * The lowest speed group, G0, translates to 8% of the posted limit. Since travel time ratio is + * the inverse of speed ratio, in order to represent 30 seconds of delay, the location would have + * to be long enough to take ~8% of that time to traverse under normal conditions, i.e. 2.4 min., + * or 2.4 km at 60 km/h. For shorter locations, the speed group will underestimate the delay. + * Higher speeds and longer delays would increase the required length. This is somewhat offset by + * the fact that the router adds a somewhat arbitrary extra penalty to segments for which traffic + * problems are reported. + */ + if ((impact.m_delayMins > 0) && (impact.m_speedGroup != traffic::SpeedGroup::TempBlock)) + { + double normalDurationS = 0; + for (auto dit = decoded.begin(); dit != decoded.end(); dit++) + { + /* + * We are initializing two structures for map access here: an MWM handle to get maxspeeds, + * and a FeaturesLoaderGuard to retrieve points. This may not be very elegant, but works. + * Since both involve somewhat complex structures, a rewrite might not be simple, although + * improvements are certainly welcome. + */ + auto const handle = m_dataSource.GetMwmHandleById(dit->first); + auto const speeds = routing::LoadMaxspeeds(handle); + + FeaturesLoaderGuard g(m_dataSource, dit->first); + uint32_t lastFid = kInvalidFeatureId; + std::vector points; + routing::MaxspeedType speedKmPH = routing::kInvalidSpeed; + for (auto cit = dit->second.begin(); cit != dit->second.end(); cit++) + { + // read points only once per feature ID, not once per segment + if (lastFid != cit->first.GetFid()) + { + auto f = g.GetOriginalFeatureByIndex(cit->first.GetFid()); + f->ResetGeometry(); + assign_range(points, f->GetPoints(FeatureType::BEST_GEOMETRY)); + lastFid = cit->first.GetFid(); + auto const speed = speeds->GetMaxspeed(cit->first.GetFid()); + speedKmPH = speed.GetSpeedKmPH(cit->first.GetDir() == traffic::TrafficInfo::RoadSegmentId::kForwardDirection); + } + auto const idx = cit->first.GetIdx(); + /* + * TODO sum of all lengths may differ from route length by up to ~6% in either direction. + * This is partly due to the fact that route length also includes fake endings, which we are + * not counting here. However, this only explains routes being longer than the sum of all + * lengths calculated here, yet in some cases the route is shorter. + */ + auto const length = mercator::DistanceOnEarth(points[idx], points[idx + 1]); + if (speedKmPH != routing::kInvalidSpeed) + normalDurationS += length * kOneMpSInKmpH / speedKmPH; + } + } + auto const delayedDurationS = normalDurationS + impact.m_delayMins * 60; + fromDelay = traffic::GetSpeedGroupByPercentage(normalDurationS * 100.0f / delayedDurationS); + + LOG(LINFO, ("Normal duration:", normalDurationS, "delayed duration:", delayedDurationS, "speed group:", DebugPrint(fromDelay))); + } + + for (auto dit = decoded.begin(); dit != decoded.end(); dit++) + { + std::unique_ptr speeds = nullptr; + if ((impact.m_speedGroup != traffic::SpeedGroup::TempBlock) && (impact.m_maxspeed != traffxml::kMaxspeedNone)) + { + // load maxspeeds once per MWM and only if needed + auto const handle = m_dataSource.GetMwmHandleById(dit->first); + speeds = routing::LoadMaxspeeds(handle); + } + for (auto cit = dit->second.begin(); cit != dit->second.end(); cit++) + { + /* + * Consolidate TrafficImpact into a single SpeedGroup per segment. + * Exception: if TrafficImpact already has SpeedGrup::TempBlock, no need to evaluate + * the rest. + */ + traffic::SpeedGroup sg = impact.m_speedGroup; + + if ((sg != traffic::SpeedGroup::TempBlock) && (fromDelay != traffic::SpeedGroup::Unknown)) + // process delay + if ((sg == traffic::SpeedGroup::Unknown) || (fromDelay < sg)) + sg = fromDelay; + + if ((sg != traffic::SpeedGroup::TempBlock) && (impact.m_maxspeed != traffxml::kMaxspeedNone)) + { + // process maxspeed + if (speeds) + { + traffic::SpeedGroup fromMaxspeed = traffic::SpeedGroup::Unknown; + auto const speed = speeds->GetMaxspeed(cit->first.GetFid()); + auto const speedKmPH = speed.GetSpeedKmPH(cit->first.GetDir() == traffic::TrafficInfo::RoadSegmentId::kForwardDirection); + if (speedKmPH != routing::kInvalidSpeed) + { + fromMaxspeed = traffic::GetSpeedGroupByPercentage(impact.m_maxspeed * 100.0f / speedKmPH); + if ((sg == traffic::SpeedGroup::Unknown) || (fromMaxspeed < sg)) + sg = fromMaxspeed; + } + } + } + cit->second = sg; + } + } +} + +void TraffDecoder::DecodeMessage(traffxml::TraffMessage & message) +{ + if (!message.m_location) + return; + // Decode events into consolidated traffic impact + m_trafficImpact = message.GetTrafficImpact(); + + LOG(LINFO, (" Impact: ", m_trafficImpact)); + + // Skip further processing if there is no impact + if (!m_trafficImpact) + return; + + traffxml::MultiMwmColoring decoded; + bool isDecoded = false; + + std::vector ids = message.m_replaces; + ids.insert(ids.begin(), message.m_id); + + for (auto & id : ids) + { + auto it = m_messageCache.find(id); + if ((it != m_messageCache.end()) + && !it->second.m_decoded.empty() + && (it->second.m_location == message.m_location)) + { + // cache already has a message with reusable location + + LOG(LINFO, (" Location for message", message.m_id, "can be reused from cache")); + + std::optional cachedImpact = it->second.GetTrafficImpact(); + if (cachedImpact.has_value() && cachedImpact.value() == m_trafficImpact.value()) + { + LOG(LINFO, (" Impact for message", message.m_id, "unchanged, reusing cached coloring")); + + // same impact, m_decoded can be reused altogether + message.m_decoded = it->second.m_decoded; + return; + } + else if (!isDecoded) + { + /* + * populate only on first occurrence but continue searching, we might find a matching + * location with matching impact + */ + decoded = it->second.m_decoded; + isDecoded = true; + } + } + } + if (!isDecoded) + DecodeLocation(message, decoded); + + if (m_trafficImpact) + { + ApplyTrafficImpact(m_trafficImpact.value(), decoded); + std::swap(message.m_decoded, decoded); + m_trafficImpact = std::nullopt; + } +} + +// Disabled for now, as the OpenLR-based decoder is slow, buggy and not well suited to the task. +#if 0 +OpenLrV3TraffDecoder::OpenLrV3TraffDecoder(DataSource & dataSource, CountryInfoGetterFn countryInfoGetter, + const CountryParentNameGetterFn & countryParentNameGetter, + std::map & messageCache) + : TraffDecoder(dataSource, countryInfoGetter, countryParentNameGetter, messageCache) + , m_openLrDecoder(dataSource, countryParentNameGetter) +{} + +openlr::FunctionalRoadClass OpenLrV3TraffDecoder::GetRoadClassFrc(std::optional & roadClass) +{ + if (!roadClass) + return openlr::FunctionalRoadClass::NotAValue; + switch (roadClass.value()) + { + case RoadClass::Motorway: return openlr::FunctionalRoadClass::FRC0; + case RoadClass::Trunk: return openlr::FunctionalRoadClass::FRC0; + case RoadClass::Primary: return openlr::FunctionalRoadClass::FRC1; + case RoadClass::Secondary: return openlr::FunctionalRoadClass::FRC2; + case RoadClass::Tertiary: return openlr::FunctionalRoadClass::FRC3; + /* + * TODO Revisit FRC for Other. + * Other corresponds to FRC4–7. + * FRC4 matches secondary/tertiary (zero score) and anything below (full score). + * FRC5–7 match anything below tertiary (full score); secondary/tertiary never match. + * Primary and above never matches any of these FRCs. + */ + case RoadClass::Other: return openlr::FunctionalRoadClass::FRC4; + } + UNREACHABLE(); +} + +// TODO tweak formula based on FRC, FOW and direct distance (lower FRC roads may have more and sharper turns) +uint32_t OpenLrV3TraffDecoder::GuessDnp(Point & p1, Point & p2) +{ + // direct distance + double doe = mercator::DistanceOnEarth(mercator::FromLatLon(p1.m_coordinates), + mercator::FromLatLon(p2.m_coordinates)); + + /* + * Acceptance boundaries for candidate paths are currently: + * + * for `openlr::LinearSegmentSource::FromLocationReferenceTag`, 0.6 to ~1.67 (i.e. 1/0.6) times + * the direct distance, + * + * for `openlr::LinearSegmentSource::FromCoordinatesTag`, 0.25 to 4 times the direct distance. + * + * A tolerance factor of 1/0.6 is the maximum for which direct distance would be accepted in all + * cases, with an upper boundary of at least ~2.78 times the direct distance. However, this may + * cause the actual distance to be overestimated and an incorrect route chosen as a result, as + * path candidates are scored based on the match between DNP and their length. + * Also, since we use `openlr::LinearSegmentSource::FromCoordinatesTag`, acceptance limits are + * much wider than that. + * In practice, the shortest route from one valley to the next in a mountain area is seldom more + * than 3 times the direct distance, based on a brief examination. This would be even within the + * limits of direct distance, hence we do not need a large correction factor for this scenario. + * + * Candidate values: + * 1.66 (1/0.6) – upper boundary for direct distance to be just within the most stringent limits + * 1.41 (2^0.5) – ratio between two sides of a square and its diagonal + * 1.3 – close to the square root of 1.66 (halfway between 1 and 1.66) + * 1.19 – close to the square root of 1.41 + * 1 – direct distance unmodified + */ + double dist = doe * 1.19f; + + // if we have kilometric points, calculate nominal distance as the difference between them + if (p1.m_distance && p2.m_distance) + { + LOG(LINFO, ("Both points have distance, calculating nominal difference")); + float nominalDist = (p1.m_distance.value() - p2.m_distance.value()) * 1000.0; + if (nominalDist < 0) + nominalDist *= -1; + /* + * Plausibility check for nominal distance, as kilometric points along the route may not be + * continuous: discard if shorter than direct distance (geometrically impossible) or if longer + * than 4 times direct distance (somewhat arbitrary, based on the OpenLR acceptance limit for + * `openlr::LinearSegmentSource::FromCoordinatesTag`, as well as real-world observations of + * distances between two adjacent mountain valleys, which are up to roughly 3 times the direct + * distance). If nominal distance is outside these boundaries, discard it and use `dist` (direct + * distance with a tolerance factor). + */ + if ((nominalDist >= doe) && (nominalDist <= doe * 4)) + dist = nominalDist; + else + LOG(LINFO, ("Nominal distance:", nominalDist, "direct distance:", doe, "– discarding")); + } + return dist + 0.5f; +} + +openlr::LocationReferencePoint OpenLrV3TraffDecoder::PointToLrp(Point & point) +{ + openlr::LocationReferencePoint result; + result.m_latLon = ms::LatLon(point.m_coordinates.m_lat, point.m_coordinates.m_lon); + return result; +} + +openlr::LinearLocationReference OpenLrV3TraffDecoder::TraffLocationToLinearLocationReference(TraffLocation & location, bool backwards) +{ + openlr::LinearLocationReference locationReference; + locationReference.m_points.clear(); + std::vector points; + if (location.m_from) + points.push_back(location.m_from.value()); + if (location.m_at) + points.push_back(location.m_at.value()); + else if (location.m_via) + points.push_back(location.m_via.value()); + if (location.m_to) + points.push_back(location.m_to.value()); + if (backwards) + std::reverse(points.begin(), points.end()); + // m_notVia is ignored as OpenLR does not support this functionality. + CHECK_GREATER(points.size(), 1, ("At least two reference points must be given")); + for (size_t i = 0; i < points.size(); i++) + { + openlr::LocationReferencePoint lrp = PointToLrp(points[i]); + lrp.m_functionalRoadClass = GetRoadClassFrc(location.m_roadClass); + if (location.m_ramps != traffxml::Ramps::None) + lrp.m_formOfWay = openlr::FormOfWay::Sliproad; + if (i < points.size() - 1) + { + lrp.m_distanceToNextPoint + = GuessDnp(points[i], points[i + 1]); + /* + * Somewhat hackish. LFRCNP is evaluated by the same function as FRC and the candidate is + * used or discarded based on whether a score was returned or not (the score itself is not + * used for LFRCNP). However, this means we can use FRC as LFRCNP. + */ + lrp.m_lfrcnp = GetRoadClassFrc(location.m_roadClass); + } + locationReference.m_points.push_back(lrp); + } + return locationReference; +} + +// TODO make segment ID in OpenLR a string value, and store messageId +std::vector OpenLrV3TraffDecoder::TraffLocationToOpenLrSegments(TraffLocation & location, std::string & messageId) +{ + // Convert the location to a format understood by the OpenLR decoder. + std::vector segments; + int dirs = (location.m_directionality == Directionality::BothDirections) ? 2 : 1; + for (int dir = 0; dir < dirs; dir++) + { + openlr::LinearSegment segment; + /* + * Segment IDs are used internally by the decoder but nowhere else. + * Since we decode TraFF locations one at a time, there are at most two segments in a single + * decoder instance (one segment per direction). Therefore, a segment ID derived from the + * direction is unique within the decoder instance. + */ + segment.m_segmentId = dir; + segment.m_messageId = messageId; + /* + * Segments generated from coordinates can have any number of points. Each point, except for + * the last point, must indicate the distance to the next point. Line properties (functional + * road class (FRC), form of way, bearing) or path properties other than distance to next point + * (lowest FRC to next point, againstDrivingDirection) are ignored. + * Segment length is never evaluated. + * TODO update OpenLR decoder to make all line and path properties optional. + */ + segment.m_source = openlr::LinearSegmentSource::FromCoordinatesTag; + segment.m_locationReference = TraffLocationToLinearLocationReference(location, dir == 0 ? false : true); + + segments.push_back(segment); + } + return segments; +} + +/* + * TODO the OpenLR decoder is designed to handle multiple segments (i.e. locations). + * Decoding message by message kind of defeats the purpose. + * But after decoding the location, we need to examine the map features we got in order to + * determine the speed groups, thus we may need to decode one by one (TBD). + * If we batch-decode segments, we need to fix the [partner] segment IDs in the segment and path + * structures to accept a TraFF message ID (string) rather than an integer, or derive + * [partner] segment IDs from TraFF message IDs. + */ +void OpenLrV3TraffDecoder::DecodeLocation(traffxml::TraffMessage & message, traffxml::MultiMwmColoring & decoded) +{ + ASSERT(message.m_location, ("Message has no location")); + decoded.clear(); + + // Convert the location to a format understood by the OpenLR decoder. + std::vector segments + = TraffLocationToOpenLrSegments(message.m_location.value(), message.m_id); + + for (auto segment : segments) + { + LOG(LINFO, (" Segment:", segment.m_segmentId)); + for (size_t i = 0; i < segment.m_locationReference.m_points.size(); i++) + { + LOG(LINFO, (" ", i, ":", segment.m_locationReference.m_points[i].m_latLon)); + if (i < segment.m_locationReference.m_points.size() - 1) + { + LOG(LINFO, (" FRC:", segment.m_locationReference.m_points[i].m_functionalRoadClass)); + LOG(LINFO, (" DNP:", segment.m_locationReference.m_points[i].m_distanceToNextPoint)); + } + } + } + + // Decode the location into a path on the map. + // One path per segment + std::vector paths(segments.size()); + m_openLrDecoder.DecodeV3(segments, kNumDecoderThreads, paths); + + for (size_t i = 0; i < paths.size(); i++) + for (size_t j = 0; j < paths[i].m_path.size(); j++) + { + auto fid = paths[i].m_path[j].GetFeatureId().m_index; + auto segment = paths[i].m_path[j].GetSegId(); + uint8_t direction = paths[i].m_path[j].IsForward() ? + traffic::TrafficInfo::RoadSegmentId::kForwardDirection : + traffic::TrafficInfo::RoadSegmentId::kReverseDirection; + decoded[paths[i].m_path[j].GetFeatureId().m_mwmId][traffic::TrafficInfo::RoadSegmentId(fid, segment, direction)] = traffic::SpeedGroup::Unknown; + } +} +#endif + +double RoutingTraffDecoder::TraffEstimator::GetUTurnPenalty(Purpose /* purpose */) const +{ + /* + * Copied from `CarEstimator::GetUTurnPenalty()`, see comments there. + * + * For the decoder use case, there should never be a need to call this function: Only going back + * on the exact segment is considered a U turn, going back on the opposite carriageway (i.e. on + * a different segment) is not. + * + * Such maneuvers would only be needed when the vehicle is heading in the direction opposite to + * the one in which it needs to travel, which should only occur at the start of a route (or after + * recalculation) and should not happen at all when decoding locations. + * + * U turns on multiple-carriageway roads are addressed by `GetTurnPenalty()`. + */ + return 2 * 60; // seconds, or meters (before penalty) for the decoder use case +} + +double RoutingTraffDecoder::TraffEstimator::GetTurnPenalty(Purpose /* purpose */, double angle, + routing::RoadGeometry const & from_road, + routing::RoadGeometry const & to_road, + bool is_left_hand_traffic) const +{ + // Flip sign for left-hand traffic, so a positive angle always means a turn across traffic + if (is_left_hand_traffic) + angle *= -1; + + // We only penalize sharp turns (above kTurnPenaltyMinAngle) across traffic + if (angle < kTurnPenaltyMinAngle) + return 0.0; + + /* + * Identify coordinates of location endpoints and of the turn, and establish distance between the + * turn and the nearest endpoint. + */ + ms::LatLon from = m_decoder.m_message.value().m_location.value().m_from + ? m_decoder.m_message.value().m_location.value().m_from.value().m_coordinates + : m_decoder.m_message.value().m_location.value().m_at.value().m_coordinates; + ms::LatLon to = m_decoder.m_message.value().m_location.value().m_to + ? m_decoder.m_message.value().m_location.value().m_to.value().m_coordinates + : m_decoder.m_message.value().m_location.value().m_at.value().m_coordinates; + + // Upper boundary for distance (approximately earth circumference) + double dist = 4.0e+7; + + for (auto & fromPoint : { from_road.GetPoint(0), from_road.GetPoint(from_road.GetPointsCount() - 1) }) + for (auto & toPoint : { to_road.GetPoint(0), to_road.GetPoint(to_road.GetPointsCount() - 1) }) + if (fromPoint == toPoint) + for (auto & endpoint : { from, to }) + { + auto newdist = ms::DistanceOnEarth(fromPoint, endpoint); + if (newdist < dist) + dist = newdist; + } + + // We only penalize turns close to an endpoint + if (dist > kTurnPenaltyMaxDist) + return 0.0; + + /* + * The penalty depends on the distance between the turn point and the nearest endpoint: the + * shorter the distance, the greater the penalty. This is obtained by subtracting the distance + * from `kTurnPenaltyMaxDist`. + * + * Above `kTurnPenaltyFullAngle`, the full turn penalty applies, i.e. the distance-based value is + * multiplied with `kAttributePenalty`. + * + * Between `kTurnPenaltyMinAngle` and `kTurnPenaltyFullAngle`, the penalty proportionally + * increases from 0 to the full value. + */ + double result = (kTurnPenaltyMaxDist - dist) * kAttributePenalty; + if (angle < kTurnPenaltyFullAngle) + result *= (angle - kTurnPenaltyMinAngle) / (kTurnPenaltyFullAngle - kTurnPenaltyMinAngle); + return result; +} + +double RoutingTraffDecoder::TraffEstimator::GetFerryLandingPenalty(Purpose /* purpose */) const +{ + return 20 * 60; // seconds +} + +double RoutingTraffDecoder::TraffEstimator::CalcOffroad(ms::LatLon const & from, ms::LatLon const & to, + Purpose /* purpose */) const +{ + /* + * Usage of this method is not quite clear. For some locations this method never gets called. + * There is also no clear pattern in which of the two arguments is the reference point and which + * is part of a segment. Either reference point can appear as either argument for either direction, + * nothing to infer from a particular reference point appearing in a particular argument. + */ + + double defaultWeight = ms::DistanceOnEarth(from, to) * kOffroadPenalty; + + /* + * Retrieves offroad weight from the junctions map supplied, if found, or default. + * + * Bugs: Due to back-and-forth conversion of `roadPoint` from Mercator to WGS84 and back, it may + * no longer match its counterpart in `junctions` (near-miss). + * + * Tests showed very few actual matches. Extending this logic to return near-matches did return + * some more, but still relatively few. This may be due to the way fake segments are chosen. + * + * refPoint: point from TraFF location + * roadPoint: point on segment + * junctions: known junctions for `refPoint` + * + * Returns: reduced offroad weight from table, or default offroad weight if not found + */ + auto const getOffroadFromJunction = [defaultWeight](ms::LatLon const & refPoint, + ms::LatLon const & roadPoint, + std::map const & junctions) + { + m2::PointD m2RoadPoint = mercator::FromLatLon(roadPoint); + auto it = junctions.find(m2RoadPoint); + if (it != junctions.end()) + return it->second; + // TODO this is likely an inefficient way to return near-matches + for (auto & [point, weight] : junctions) + if (m2RoadPoint.EqualDxDy(point, kMwmPointAccuracy)) + return weight; + return defaultWeight; + }; + + /* + * If one of from/to is a reference point and the other is in the corresponding junction map, + * return the weight from the map + */ + if (m_decoder.m_message.value().m_location.value().m_from) + { + if (m_decoder.m_message.value().m_location.value().m_from.value().m_coordinates == from) + return getOffroadFromJunction(from, to, m_decoder.m_startJunctions); + else if (m_decoder.m_message.value().m_location.value().m_from.value().m_coordinates == to) + return getOffroadFromJunction(to, from, m_decoder.m_startJunctions); + } + if (m_decoder.m_message.value().m_location.value().m_to) + { + if (m_decoder.m_message.value().m_location.value().m_to.value().m_coordinates == from) + return getOffroadFromJunction(from, to, m_decoder.m_endJunctions); + else if (m_decoder.m_message.value().m_location.value().m_to.value().m_coordinates == to) + return getOffroadFromJunction(to, from, m_decoder.m_endJunctions); + } + + return defaultWeight; +} + +/* + * Currently, the attribute penalty (kAttributePenalty or kReducedAttributePenalty) can be applied + * up to 3 times: + * - ramp attribute mismatch + * - road class mismatch + * - road ref mismatch + */ +double RoutingTraffDecoder::TraffEstimator::CalcSegmentWeight(routing::Segment const & segment, routing::RoadGeometry const & road, Purpose /* purpose */) const +{ + double result = road.GetDistance(segment.GetSegmentIdx()); + + if (!m_decoder.m_message || !m_decoder.m_message.value().m_location.value().m_roadClass) + return result; + + result *= GetHighwayTypePenalty(road.GetHighwayType(), + m_decoder.m_message.value().m_location.value().m_roadClass, + m_decoder.m_message.value().m_location.value().m_ramps); + + if (!m_decoder.m_roadRef.empty()) + { + auto const countryFile = m_decoder.m_numMwmIds->GetFile(segment.GetMwmId()); + auto const mwmId = m_decoder.m_dataSource.GetMwmIdByCountryFile(countryFile); + FeaturesLoaderGuard g(m_decoder.m_dataSource, mwmId); + auto f = g.GetOriginalFeatureByIndex(segment.GetFeatureId()); + auto refs = ftypes::GetRoadShieldsNames(*f); + + result *= m_decoder.GetRoadRefPenalty(refs); + } + + if (!IsAccessIgnored() && road.GetHighwayType() && IsConstruction(road.GetHighwayType().value())) + result *= kImpassablePenalty; + + return result; +} + +bool RoutingTraffDecoder::TraffEstimator::IsAccessIgnored() const +{ + ASSERT(m_decoder.m_trafficImpact, ("Traffic impact for current message is not set")); + return (m_decoder.m_trafficImpact.value().m_speedGroup == traffic::SpeedGroup::TempBlock); +} + +RoutingTraffDecoder::DecoderRouter::DecoderRouter(CountryParentNameGetterFn const & countryParentNameGetterFn, + routing::TCountryFileFn const & countryFileFn, + routing::CountryRectFn const & countryRectFn, + std::shared_ptr numMwmIds, + std::unique_ptr> numMwmTree, + DataSource & dataSource, RoutingTraffDecoder & decoder) + : routing::IndexRouter(routing::VehicleType::Decoder /* VehicleType vehicleType */, + false /* bool loadAltitudes */, + countryParentNameGetterFn, + countryFileFn, + countryRectFn, + numMwmIds, + std::move(numMwmTree), + //std::nullopt /* std::optional const & trafficCache */, + std::make_shared(&dataSource, numMwmIds, kOneMpSInKmpH /* maxWeighSpeedKMpH */, + routing::SpeedKMpH(kOneMpSInKmpH / kOffroadPenalty /* weight */, + routing::kNotUsed /* eta */) /* offroadSpeedKMpH */, + decoder), + dataSource) + //, m_directionsEngine(CreateDirectionsEngine(m_vehicleType, m_numMwmIds, m_dataSource)) // TODO we don’t need directions, can we disable that? +{} + +routing::RoutingOptions RoutingTraffDecoder::DecoderRouter::GetRoutingOptions() +{ + return routing::RoutingOptions(); +} + +RoutingTraffDecoder::RoutingTraffDecoder(DataSource & dataSource, CountryInfoGetterFn countryInfoGetter, + const CountryParentNameGetterFn & countryParentNameGetter, + std::map & messageCache) + : TraffDecoder(dataSource, countryInfoGetter, countryParentNameGetter, messageCache) +{ + m_dataSource.AddObserver(*this); + InitRouter(); +} + +double RoutingTraffDecoder::GetHighwayTypePenalty(std::optional highwayType, + std::optional roadClass, + Ramps ramps) +{ + double result = 1.0; + if (highwayType) + { + //LOG(LINFO, ("highway type:", highwayType.value())); + if (IsRamp(highwayType.value()) != (ramps != Ramps::None)) + // if one is a ramp and the other is not, treat it as a mismatch + result *= kAttributePenalty; + if (roadClass) + // if the message specifies a road class, penalize mismatches + result *= GetRoadClassPenalty(roadClass.value(), GetRoadClass(highwayType.value())); + } + else // road has no highway class + { + // we can’t determine if it is a ramp, penalize for mismatch + result *= kAttributePenalty; + if (roadClass) + // we can’t determine if the road matches the required road class, treat it as mismatch + result *= kAttributePenalty; + } + return result; +} + +double RoutingTraffDecoder::GetRoadRefPenalty(std::vector & refs) const +{ + double result = kAttributePenalty; + + for (auto & ref : refs) + { + auto newResult = GetRoadRefPenalty(ref); + if (newResult < result) + result = newResult; + if (result == 1) + break; + } + + return result; +} + +double RoutingTraffDecoder::GetRoadRefPenalty(std::string const & ref) const +{ + // skip parsing if ref is empty + if (ref.empty()) + { + if (m_roadRef.empty()) + return 1; + else if (!m_roadRef.empty()) + return kAttributePenalty; + } + + // TODO does caching results per ref improve performance? + + std::vector r = ParseRef(ref); + + size_t matches = 0; + + if (m_roadRef.empty() && r.empty()) + return 1; + else if (m_roadRef.empty() || r.empty()) + return kAttributePenalty; + + // work on a copy of `m_decoder.m_roadRef` + std::vector l = m_roadRef; + + if ((l.size() > 1) && (r.size() > 1) && (l.front() == r.front())) + { + /* + * Discard generic prefixes, which are often used to denote the road class. + * This will turn `A1` and `A2` into `1` and `2`, causing them to be treated as a mismatch, + * not a partial match. + */ + l.erase(l.begin()); + r.erase(r.begin()); + } + + // for both sides, count items matched by the other side + for (auto & litem : l) + for (auto ritem : r) + if (litem == ritem) + { + matches++; + break; + } + + for (auto ritem : r) + for (auto & litem : l) + if (litem == ritem) + { + matches++; + break; + } + + if (matches == 0) + return kAttributePenalty; + else if (matches == (l.size() + r.size())) + return 1; + else + return kReducedAttributePenalty; +} + +void RoutingTraffDecoder::OnMapRegistered(platform::LocalCountryFile const & localFile) +{ + std::lock_guard lock(m_mutex); + // register with our router instance, unless it is World or WorldCoasts. + if (!localFile.GetCountryName().starts_with(WORLD_FILE_NAME)) + m_numMwmIds->RegisterFile(localFile.GetCountryFile()); +} + +bool RoutingTraffDecoder::InitRouter() +{ + std::lock_guard lock(m_mutex); + if (m_router) + return true; + + /* + * Code based on RoutingManager::SetRouterImpl(RouterType), which calls + * m_delegate.RegisterCountryFilesOnRoute(numMwmIds); m_delegate being the framework instance. + * RegisterCountryFilesOnRoute() is protected and uses a private `Storage` instance. + * We therefore have to resort to populating `m_numMwmIds` from `m_dataSource`. Unlike the + * “original”, this will only include MWMs loaded on startup, not those added later. + * For these, we register ourselves as an MwmSet::Observer and add maps to `m_numMwms` as they + * are registered. + * World and WorldCoasts must be excluded (as in the original routine), as they would cause the + * router to return bogus routes. Just like the original, we use a string comparison for this. + */ + std::vector> mwmsInfo; + m_dataSource.GetMwmsInfo(mwmsInfo); + + for (auto mwmInfo : mwmsInfo) + if (!mwmInfo->GetCountryName().starts_with(WORLD_FILE_NAME)) + m_numMwmIds->RegisterFile(mwmInfo->GetLocalFile().GetCountryFile()); + + if (m_numMwmIds->IsEmpty()) + return false; + + auto const countryFileGetter = [this](m2::PointD const & p) -> std::string { + // TODO (@gorshenin): fix CountryInfoGetter to return CountryFile + // instances instead of plain strings. + return m_countryInfoGetterFn().GetRegionCountryId(p); + }; + + auto const getMwmRectByName = [this](std::string const & countryId) -> m2::RectD { + return m_countryInfoGetterFn().GetLimitRectForLeaf(countryId); + }; + + m_router = + make_unique(m_countryParentNameGetterFn, + countryFileGetter, getMwmRectByName, m_numMwmIds, + routing::MakeNumMwmTree(*m_numMwmIds, m_countryInfoGetterFn()), + m_dataSource, *this); + + return true; +} + +// Copied from AsyncRouter +// static +void RoutingTraffDecoder::LogCode(routing::RouterResultCode code, double const elapsedSec) +{ + switch (code) + { + case routing::RouterResultCode::StartPointNotFound: + LOG(LWARNING, ("Can't find start or end node")); + break; + case routing::RouterResultCode::EndPointNotFound: + LOG(LWARNING, ("Can't find end point node")); + break; + case routing::RouterResultCode::PointsInDifferentMWM: + LOG(LWARNING, ("Points are in different MWMs")); + break; + case routing::RouterResultCode::RouteNotFound: + LOG(LWARNING, ("Route not found")); + break; + case routing::RouterResultCode::RouteFileNotExist: + LOG(LWARNING, ("There is no routing file")); + break; + case routing::RouterResultCode::NeedMoreMaps: + LOG(LINFO, + ("Routing can find a better way with additional maps, elapsed seconds:", elapsedSec)); + break; + case routing::RouterResultCode::Cancelled: + LOG(LINFO, ("Route calculation cancelled, elapsed seconds:", elapsedSec)); + break; + case routing::RouterResultCode::NoError: + LOG(LINFO, ("Route found, elapsed seconds:", elapsedSec)); + break; + case routing::RouterResultCode::NoCurrentPosition: + LOG(LINFO, ("No current position")); + break; + case routing::RouterResultCode::InconsistentMWMandRoute: + LOG(LINFO, ("Inconsistent mwm and route")); + break; + case routing::RouterResultCode::InternalError: + LOG(LINFO, ("Internal error")); + break; + case routing::RouterResultCode::FileTooOld: + LOG(LINFO, ("File too old")); + break; + case routing::RouterResultCode::IntermediatePointNotFound: + LOG(LWARNING, ("Can't find intermediate point node")); + break; + case routing::RouterResultCode::TransitRouteNotFoundNoNetwork: + LOG(LWARNING, ("No transit route is found because there's no transit network in the mwm of " + "the route point")); + break; + case routing::RouterResultCode::TransitRouteNotFoundTooLongPedestrian: + LOG(LWARNING, ("No transit route is found because pedestrian way is too long")); + break; + case routing::RouterResultCode::RouteNotFoundRedressRouteError: + LOG(LWARNING, ("Route not found because of a redress route error")); + break; + case routing::RouterResultCode::HasWarnings: + LOG(LINFO, ("Route has warnings, elapsed seconds:", elapsedSec)); + break; + } +} + +void RoutingTraffDecoder::AddDecodedSegment(traffxml::MultiMwmColoring & decoded, routing::Segment & segment) +{ + auto const countryFile = m_numMwmIds->GetFile(segment.GetMwmId()); + MwmSet::MwmId mwmId = m_dataSource.GetMwmIdByCountryFile(countryFile); + + auto const fid = segment.GetFeatureId(); + auto const sid = segment.GetSegmentIdx(); + uint8_t direction = segment.IsForward() ? + traffic::TrafficInfo::RoadSegmentId::kForwardDirection : + traffic::TrafficInfo::RoadSegmentId::kReverseDirection; + + decoded[mwmId][traffic::TrafficInfo::RoadSegmentId(fid, sid, direction)] = traffic::SpeedGroup::Unknown; +} + +void RoutingTraffDecoder::TruncateRoute(std::vector & rsegments, + routing::Checkpoints const & checkpoints, bool backwards) +{ + double startWeight = 0; + double const endWeight = rsegments.back().GetTimeFromBeginningSec(); + + // erase leading and trailing fake segments + while(!rsegments.empty() && rsegments.front().GetSegment().GetMwmId() == routing::kFakeNumMwmId) + { + startWeight = rsegments.front().GetTimeFromBeginningSec(); + rsegments.erase(rsegments.begin()); + } + while(!rsegments.empty() && rsegments.back().GetSegment().GetMwmId() == routing::kFakeNumMwmId) + rsegments.pop_back(); + + if (rsegments.size() < 2) + return; + + // Index of first segment to keep, or number of segments to truncate at start. + size_t start = 0; + // Cost saved by omitting all segments prior to `start`. + double startSaving = 0; + + // Index of last segment to keep. + size_t end = rsegments.size() - 1; + // Cost saved by omitting the last `end` segments. + double endSaving = 0; + + TruncateStart(rsegments, checkpoints, start, startSaving, startWeight, + backwards ? m_endJunctions : m_startJunctions); + TruncateEnd(rsegments, checkpoints, end, endSaving, endWeight, + backwards ? m_startJunctions : m_endJunctions); + + /* + * If start <= end, we can truncate both ends at the same time. + * Else, the segments to truncate overlap. In this case, first truncate where the saving is bigger, + * then recalculate the other end and truncate it as well. + */ + if (start <= end) + { + rsegments.erase(rsegments.begin() + end + 1, rsegments.end()); + rsegments.erase(rsegments.begin(), rsegments.begin() + start); + } + else if (startSaving > endSaving) + { + // truncate start, then recalculate and truncate end + rsegments.erase(rsegments.begin(), rsegments.begin() + start); + end = rsegments.size() - 1; + endSaving = 0; + TruncateEnd(rsegments, checkpoints, end, endSaving, endWeight, + backwards ? m_startJunctions : m_endJunctions); + rsegments.erase(rsegments.begin() + end + 1, rsegments.end()); + } + else + { + // truncate end, then recalculate and truncate start + rsegments.erase(rsegments.begin() + end + 1, rsegments.end()); + start = 0; + startSaving = 0; + TruncateStart(rsegments, checkpoints, start, startSaving, startWeight, + backwards ? m_endJunctions : m_startJunctions); + rsegments.erase(rsegments.begin(), rsegments.begin() + start); + } +} + +void RoutingTraffDecoder::DecodeLocationDirection(traffxml::TraffMessage & message, + traffxml::MultiMwmColoring & decoded, bool backwards) +{ + bool adjustToPrevRoute = false; // calculate a fresh route, no adjustments to previous one + uint64_t routeId = 0; // used in callbacks to identify the route, we might not need it at all + + std::vector points; + if (message.m_location.value().m_from) + points.push_back(mercator::FromLatLon(message.m_location.value().m_from.value().m_coordinates)); + if (message.m_location.value().m_at) + points.push_back(mercator::FromLatLon(message.m_location.value().m_at.value().m_coordinates)); + else if (message.m_location.value().m_via) + points.push_back(mercator::FromLatLon(message.m_location.value().m_via.value().m_coordinates)); + if (message.m_location.value().m_to) + points.push_back(mercator::FromLatLon(message.m_location.value().m_to.value().m_coordinates)); + if (backwards) + std::reverse(points.begin(), points.end()); + // m_notVia is ignored as OpenLR does not support this functionality. + CHECK_GREATER(points.size(), 1, ("At least two reference points must be given")); + + /* + * startDirection is the direction of travel at start. Can be m2::PointD::Zero() to ignore + * direction, or PositionAccumulator::GetDirection(), which basically returns the difference + * between the last position and an earlier one (offset between two points from which the + * direction of travel can be inferred). + * + * For our purposes, points[1] - points[0] would be as close as we could get to a start direction. + * This would be accurate on very straight roads, less accurate on not-so-straight ones. However, + * even on a near-straight road, the standard router (with the default EdgeEstimator) seemed quite + * unimpressed by the direction and insisted on starting off on the carriageway closest to the + * start point and sending us on a huge detour, instead of taking the direct route on the opposite + * carriageway. + */ + m2::PointD startDirection = m2::PointD::Zero(); + + routing::Checkpoints checkpoints(std::move(points)); + + /* + * This code is mostly lifted from AsyncRouter::CalculateRoute(), both with and without arguments. + */ + + /* + * AsyncRouter::CalculateRoute() has a `DelegateProxy`, which is private. We just need the return + * value of GetDelegate(), which is a `routing::RouterDelegate`, so use that instead. We don’t + * need any of the callbacks, therefore we don’t set them. + */ + routing::RouterDelegate delegate; + delegate.SetTimeout(kRouterTimeoutSec); + routing::RouterResultCode code; + std::shared_ptr route; + + { + std::lock_guard lock(m_mutex); + + if (!m_router && !InitRouter()) + return; + + /* + * TODO is that for following a track? If so, can we use that with just 2–3 reference points? + * – Doesn’t look like it, m_guides only seems to get used in test functions + */ + //router->SetGuides(std::move(m_guides)); + //m_guides.clear(); + + route = std::make_shared(m_router->GetName(), routeId); + + base::Timer timer; + double elapsedSec = 0.0; + + try + { + LOG(LINFO, ("Calculating the route of direct length", checkpoints.GetSummaryLengthBetweenPointsMeters(), + "m. checkpoints:", checkpoints, "startDirection:", startDirection, "router name:", m_router->GetName())); + + // Run basic request. + code = m_router->CalculateRoute(checkpoints, startDirection, adjustToPrevRoute, + delegate, *route); + m_router->SetGuides({}); + elapsedSec = timer.ElapsedSeconds(); // routing time + LogCode(code, elapsedSec); + LOG(LINFO, ("ETA:", route->GetTotalTimeSec(), "sec.")); + } + catch (RootException const & e) + { + code = routing::RouterResultCode::InternalError; + LOG(LERROR, ("Exception happened while calculating route:", e.Msg())); + return; + } + } + + if (code == routing::RouterResultCode::NoError) + { + std::vector rsegments(route->GetRouteSegments()); + + TruncateRoute(rsegments, checkpoints, backwards); + + /* + * `m_onRoundabout` is set only for the first segment after the junction. In order to identify + * all roundabout segments, cache the last segment with `m_onRoundabout` set. Any subsequent + * segment with the same MWM and feature ID is also a roundabout segment. + */ + routing::Segment lastRoundaboutSegment; + + /* + * We usually discard roundabouts, unless the location is a point (`at` point set) or the entire + * decoded location is a roundabout. + */ + bool keepRoundabouts = true; + + if (!message.m_location.value().m_at) + for (auto & rsegment : rsegments) + { + if (rsegment.GetRoadNameInfo().m_onRoundabout) + lastRoundaboutSegment = rsegment.GetSegment(); + else if ((rsegment.GetSegment().GetMwmId() != lastRoundaboutSegment.GetMwmId()) + || (rsegment.GetSegment().GetFeatureId() != lastRoundaboutSegment.GetFeatureId())) + { + keepRoundabouts = false; + break; + } + } + + if (!backwards && message.m_location.value().m_at && !message.m_location.value().m_to) + // from–at in forward direction, add last segment + AddDecodedSegment(decoded, rsegments.back().GetSegment()); + else if (!backwards && message.m_location.value().m_at && !message.m_location.value().m_from) + // at–to in forward direction, add last segment + AddDecodedSegment(decoded, rsegments.front().GetSegment()); + else if (backwards && message.m_location.value().m_at && !message.m_location.value().m_to) + // from–at in backward direction, add first segment + AddDecodedSegment(decoded, rsegments.front().GetSegment()); + else if (backwards && message.m_location.value().m_at && !message.m_location.value().m_from) + // at–to in backward direction, add first segment + AddDecodedSegment(decoded, rsegments.back().GetSegment()); + else if (message.m_location.value().m_at) + { + // from–at–to, find closest segment + ms::LatLon at = message.m_location.value().m_at.value().m_coordinates; + routing::RouteSegment & closestRSegment = rsegments.front(); + double closestDist = ms::DistanceOnEarth(at, mercator::ToLatLon(closestRSegment.GetJunction().GetPoint())); + + for (auto & rsegment : rsegments) + { + // If we have more than two checkpoints, fake segments can occur in the middle, skip them. + if (rsegment.GetSegment().GetMwmId() == routing::kFakeNumMwmId) + continue; + + double dist = ms::DistanceOnEarth(at, mercator::ToLatLon(rsegment.GetJunction().GetPoint())); + if (dist < closestDist) + { + closestRSegment = rsegment; + closestDist = dist; + } + } + AddDecodedSegment(decoded, closestRSegment.GetSegment()); + } + else + // from–[via]–to, add all real segments + for (auto & rsegment : rsegments) + { + routing::Segment & segment = rsegment.GetSegment(); + + // Skip roundabouts to avoid side effects on crossing roads + if (!keepRoundabouts) + { + if (rsegment.GetRoadNameInfo().m_onRoundabout) + { + lastRoundaboutSegment = segment; + continue; + } + else if ((segment.GetMwmId() == lastRoundaboutSegment.GetMwmId()) + && (segment.GetFeatureId() == lastRoundaboutSegment.GetFeatureId())) + continue; + } + + // If we have more than two checkpoints, fake segments can occur in the middle, skip them. + if (segment.GetMwmId() == routing::kFakeNumMwmId) + continue; + + AddDecodedSegment(decoded, segment); + } + } +} + +void RoutingTraffDecoder::DecodeLocation(traffxml::TraffMessage & message, traffxml::MultiMwmColoring & decoded) +{ + ASSERT(message.m_location, ("Message has no location")); + decoded.clear(); + + m_message = message; + + if (m_message.value().m_location.value().m_roadRef) + m_roadRef = ParseRef(m_message.value().m_location.value().m_roadRef.value()); + else + m_roadRef.clear(); + + GetJunctionPointCandidates(); + + int dirs = (message.m_location.value().m_directionality == Directionality::BothDirections) ? 2 : 1; + for (int dir = 0; dir < dirs; dir++) + DecodeLocationDirection(message, decoded, dir == 0 ? false : true /* backwards */); + + m_message = std::nullopt; + m_roadRef.clear(); +} + +void RoutingTraffDecoder::GetJunctionPointCandidates() +{ + m_startJunctions.clear(); + m_endJunctions.clear(); + + if (m_message.value().m_location.value().m_fuzziness + && (m_message.value().m_location.value().m_fuzziness.value() == traffxml::Fuzziness::LowRes)) + { + /* + * Identify coordinates of location endpoints and of the turn, and determine distance. + */ + ms::LatLon from = m_message.value().m_location.value().m_from + ? m_message.value().m_location.value().m_from.value().m_coordinates + : m_message.value().m_location.value().m_at.value().m_coordinates; + ms::LatLon to = m_message.value().m_location.value().m_to + ? m_message.value().m_location.value().m_to.value().m_coordinates + : m_message.value().m_location.value().m_at.value().m_coordinates; + + auto dist = ms::DistanceOnEarth(from, to); + + auto bounds = kJunctionRadiusBoundaries.at(m_message.value().m_location.value().m_roadClass); + + m_junctionRadius = dist / 3.0f; + if (m_junctionRadius > bounds[1]) + m_junctionRadius = bounds[1]; + else if (m_junctionRadius < bounds[0]) + { + m_junctionRadius = dist / 2.0f; + if (m_junctionRadius > bounds[0]) + m_junctionRadius = bounds[0]; + } + + if (m_message.value().m_location.value().m_from) + GetJunctionPointCandidates(m_message.value().m_location.value().m_from.value(), m_startJunctions); + if (m_message.value().m_location.value().m_to) + GetJunctionPointCandidates(m_message.value().m_location.value().m_to.value(), m_endJunctions); + } +} + +void RoutingTraffDecoder::GetJunctionPointCandidates(Point const & point, + std::map & junctions) +{ + m2::PointD const m2Point = mercator::FromLatLon(point.m_coordinates); + std::map pointCandidates; + auto const selectCandidates = [&m2Point, &pointCandidates, this](FeatureType & ft) + { + ft.ParseGeometry(FeatureType::BEST_GEOMETRY); + if (ft.GetGeomType() != feature::GeomType::Line || !routing::IsRoad(feature::TypesHolder(ft))) + return; + + for (auto i : {size_t(0), ft.GetPointsCount() - 1}) + { + double weight = mercator::DistanceOnEarth(m2Point, ft.GetPoint(i)); + + if (weight > m_junctionRadius) + continue; + + weight *= GetHighwayTypePenalty(routing::CarModel::AllLimitsInstance().GetHighwayType(feature::TypesHolder(ft)), + m_message.value().m_location.value().m_roadClass, + m_message.value().m_location.value().m_ramps); + + auto refs = ftypes::GetRoadShieldsNames(ft); + weight *= GetRoadRefPenalty(refs); + + /* + * Store candidate point and weight (unless we already have a lower weight). + * These are points read directly from the map, so we should be able to work with true matches + * (according to tests, near-matches are rare and the one we examined was close to the + * tolerance limit, so it could have been accidental). + */ + auto it = pointCandidates.find(ft.GetPoint(i)); + if (it == pointCandidates.end()) + it = pointCandidates.insert(std::make_pair(ft.GetPoint(i), JunctionCandidateInfo(weight))).first; + else if (weight < it->second.m_weight) + it->second.m_weight = weight; + + // check oneway attribute and increase appropriate segment count + if (!ftypes::IsOneWayChecker::Instance()(ft)) + it->second.m_twoWaySegments++; + else if (i == 0) + it->second.m_segmentsOut++; + else + it->second.m_segmentsIn++; + } + }; + + m_dataSource.ForEachInRect(selectCandidates, mercator::RectByCenterXYAndSizeInMeters(m2Point, m_junctionRadius), + scales::GetUpperScale()); + + /* + * Cycle through point candidates and see if they are really junctions. A point is a junction if + * it can be left through more than one segment, other than the one through which it was reached, + * or reached through more than one segment, other than the one through which it will be left. + * Junctions are added to `junctions`, other points are skipped. + * Bug: may fail to catch duplicate ways at MWM boundaries + */ + for (auto & [candidatePoint, candidateInfo] : pointCandidates) + { + if (candidateInfo.m_segmentsIn > 0) + candidateInfo.m_segmentsIn--; + else if (candidateInfo.m_twoWaySegments > 0) + candidateInfo.m_twoWaySegments--; + + if (candidateInfo.m_segmentsOut > 0) + candidateInfo.m_segmentsOut--; + else if (candidateInfo.m_twoWaySegments > 0) + candidateInfo.m_twoWaySegments--; + + if ((candidateInfo.m_segmentsIn > 0) + || (candidateInfo.m_segmentsOut > 0) + || (candidateInfo.m_twoWaySegments > 0)) + junctions.insert(std::make_pair(candidatePoint, candidateInfo.m_weight)); + } +} + +traffxml::RoadClass GetRoadClass(routing::HighwayType highwayType) +{ + switch(highwayType) + { + /* + * Parallel carriageways may be tagged as link, hence consider them equivalent to the underlying + * highway type. + */ + case routing::HighwayType::HighwayMotorway: + case routing::HighwayType::HighwayMotorwayLink: + case routing::HighwayType::HighwayConstructionMotorway: + case routing::HighwayType::HighwayConstructionMotorwayLink: + return traffxml::RoadClass::Motorway; + case routing::HighwayType::HighwayTrunk: + case routing::HighwayType::HighwayTrunkLink: + case routing::HighwayType::HighwayConstructionTrunk: + case routing::HighwayType::HighwayConstructionTrunkLink: + return traffxml::RoadClass::Trunk; + case routing::HighwayType::HighwayPrimary: + case routing::HighwayType::HighwayPrimaryLink: + case routing::HighwayType::HighwayConstructionPrimary: + case routing::HighwayType::HighwayConstructionPrimaryLink: + return traffxml::RoadClass::Primary; + case routing::HighwayType::HighwaySecondary: + case routing::HighwayType::HighwaySecondaryLink: + case routing::HighwayType::HighwayConstructionSecondary: + case routing::HighwayType::HighwayConstructionSecondaryLink: + return traffxml::RoadClass::Secondary; + case routing::HighwayType::HighwayTertiary: + case routing::HighwayType::HighwayTertiaryLink: + case routing::HighwayType::HighwayConstructionTertiary: + case routing::HighwayType::HighwayConstructionTertiaryLink: + return traffxml::RoadClass::Tertiary; + default: + return traffxml::RoadClass::Other; + } +} + +/** + * @brief Returns the penalty factor for road class match/mismatch. + * + * If `lhs` and `rhs` are identical, the penalty factor is 1 (no penalty). If they are adjacent road + * classes (e.g. trunk and primary), the penalty factor is `kReducedAttributePenalty`, in all other + * cases it is `kAttributePenalty`. + * + * @param lhs + * @param rhs + * @return The penalty factor, see above. + */ +double GetRoadClassPenalty(traffxml::RoadClass lhs, traffxml::RoadClass rhs) +{ + if (lhs == rhs) + return 1; + switch (lhs) + { + case traffxml::RoadClass::Motorway: + if (rhs == traffxml::RoadClass::Trunk) + return kReducedAttributePenalty; + else + return kAttributePenalty; + case traffxml::RoadClass::Trunk: + if (rhs == traffxml::RoadClass::Motorway || rhs == traffxml::RoadClass::Primary) + return kReducedAttributePenalty; + else + return kAttributePenalty; + case traffxml::RoadClass::Primary: + if (rhs == traffxml::RoadClass::Trunk || rhs == traffxml::RoadClass::Secondary) + return kReducedAttributePenalty; + else + return kAttributePenalty; + case traffxml::RoadClass::Secondary: + if (rhs == traffxml::RoadClass::Primary || rhs == traffxml::RoadClass::Tertiary) + return kReducedAttributePenalty; + else + return kAttributePenalty; + case traffxml::RoadClass::Tertiary: + if (rhs == traffxml::RoadClass::Secondary || rhs == traffxml::RoadClass::Other) + return kReducedAttributePenalty; + else + return kAttributePenalty; + case traffxml::RoadClass::Other: + if (rhs == traffxml::RoadClass::Tertiary) + return kReducedAttributePenalty; + else + return kAttributePenalty; + default: + UNREACHABLE(); + } +} + +bool IsConstruction(routing::HighwayType highwayType) +{ + switch(highwayType) + { + case routing::HighwayType::HighwayConstruction: + case routing::HighwayType::HighwayConstructionMotorway: + case routing::HighwayType::HighwayConstructionMotorwayLink: + case routing::HighwayType::HighwayConstructionTrunk: + case routing::HighwayType::HighwayConstructionTrunkLink: + case routing::HighwayType::HighwayConstructionPrimary: + case routing::HighwayType::HighwayConstructionPrimaryLink: + case routing::HighwayType::HighwayConstructionSecondary: + case routing::HighwayType::HighwayConstructionSecondaryLink: + case routing::HighwayType::HighwayConstructionTertiary: + case routing::HighwayType::HighwayConstructionTertiaryLink: + case routing::HighwayType::HighwayConstructionResidential: + case routing::HighwayType::HighwayConstructionUnclassified: + case routing::HighwayType::HighwayConstructionService: + case routing::HighwayType::HighwayConstructionLivingStreet: + case routing::HighwayType::HighwayConstructionRoad: + case routing::HighwayType::HighwayConstructionTrack: + return true; + default: + return false; + } +} + +bool IsRamp(routing::HighwayType highwayType) +{ + switch(highwayType) + { + case routing::HighwayType::HighwayMotorwayLink: + case routing::HighwayType::HighwayConstructionMotorwayLink: + case routing::HighwayType::HighwayTrunkLink: + case routing::HighwayType::HighwayConstructionTrunkLink: + case routing::HighwayType::HighwayPrimaryLink: + case routing::HighwayType::HighwayConstructionPrimaryLink: + case routing::HighwayType::HighwaySecondaryLink: + case routing::HighwayType::HighwayConstructionSecondaryLink: + case routing::HighwayType::HighwayTertiaryLink: + case routing::HighwayType::HighwayConstructionTertiaryLink: + return true; + default: + return false; + } +} + +std::vector ParseRef(std::string const & ref) +{ + std::vector res; + std::string curr = ""; + RefParserState state = RefParserState::Whitespace; + + for (size_t i = 0; i < ref.size(); i++) + { + // TODO this list of delimiters might not be exhaustive + if ((ref[i] <= 0x20) || (ref[i] == ',') || (ref[i] == '-') || (ref[i] == '.') || (ref[i] == '/')) + { + // whitespace + if (state != RefParserState::Whitespace) + { + if (state == RefParserState::Alpha) + boost::to_lower(curr); + res.push_back(curr); + curr = ""; + } + state = RefParserState::Whitespace; + } + /* + * TODO adapt this to other number systems as well. + * Roman numerals (or any use of letters as numbers) are a stupid idea. If they are at least + * properly delimited, they are treated as a letter group, which sort of works for comparison. + * However, `IVbis` will be treated as one group, whereas `IV bis` will be treated as two. + */ + else if ((ref[i] >= '0') && (ref[i] <= '9')) + { + // numeric + if (state == RefParserState::Alpha) + { + boost::to_lower(curr); + res.push_back(curr); + curr = ""; + } + curr += ref[i]; + state = RefParserState::Numeric; + } + // anything that is not a delimiter or a digit (as per the above rules) is considered a letter + else + { + // alpha + if (state == RefParserState::Numeric) + { + res.push_back(curr); + curr = ""; + } + curr += ref[i]; + state = RefParserState::Alpha; + } + } + if (!curr.empty()) + res.push_back(curr); + return res; +} + +void TruncateStart(std::vector & rsegments, + routing::Checkpoints const & checkpoints, + size_t & start, double & startSaving, double const startWeight, + std::map const & junctions) +{ + if (rsegments.empty()) + return; + + for (size_t i = 0; i < rsegments.size(); i++) + { + double newStartSaving = 0; + /* + * Examine end point of segment: for a junction, take weight from table; else calculate it as + * direct distance multiplied with offroad penalty; calculate saving based on that. + */ + auto it = junctions.find(rsegments[i].GetJunction().GetPoint()); + if (it != junctions.end()) + newStartSaving = rsegments[i].GetTimeFromBeginningSec() - it->second; + else + { + bool matched = false; + // TODO this is likely an inefficient way to return near-matches + for (auto & [point, weight] : junctions) + if (rsegments[i].GetJunction().GetPoint().EqualDxDy(point, kMwmPointAccuracy)) + { + newStartSaving = rsegments[i].GetTimeFromBeginningSec() - weight; + matched = true; + break; + } + if (!matched) + newStartSaving = rsegments[i].GetTimeFromBeginningSec() + - (mercator::DistanceOnEarth(checkpoints.GetStart(), rsegments[i].GetJunction().GetPoint()) + * kOffroadPenalty); + } + if (newStartSaving > startSaving) + { + start = i + 1; // add 1 because we are ditching this segment and keeping the next one + startSaving = newStartSaving; + } + } + /* + * The router may return a route that starts and ends with a partial segment. In decoder mode, + * we don’t allow starting or ending a segment at an intermediate point but use the nearest + * segment endpoint instead. However, this may leave us with a zero-length segment, which we + * need to remove so we don’t overshoot the reference point. + */ + // TODO not if we have an `at` point + if ((start == 0) && (rsegments.size() > 1) + && (rsegments[0].GetTimeFromBeginningSec() == startWeight)) + start = 1; +} + +void TruncateEnd(std::vector & rsegments, + routing::Checkpoints const & checkpoints, + size_t & end, double & endSaving, double const endWeight, + std::map const & junctions) +{ + for (size_t i = 0; i < rsegments.size(); i++) + { + double newEndSaving = 0; + /* + * Examine end point of segment: for a junction, take weight from table; else calculate it as + * direct distance multiplied with offroad penalty; calculate saving based on that. + */ + auto it = junctions.find(rsegments[i].GetJunction().GetPoint()); + if (it != junctions.end()) + newEndSaving = endWeight - rsegments[i].GetTimeFromBeginningSec() - it->second; + else + { + bool matched = false; + // TODO this is likely an inefficient way to return near-matches + for (auto & [point, weight] : junctions) + if (rsegments[i].GetJunction().GetPoint().EqualDxDy(point, kMwmPointAccuracy)) + { + newEndSaving = endWeight - rsegments[i].GetTimeFromBeginningSec() - weight; + matched = true; + break; + } + if (!matched) + newEndSaving = endWeight - rsegments[i].GetTimeFromBeginningSec() + - (mercator::DistanceOnEarth(rsegments[i].GetJunction().GetPoint(), checkpoints.GetFinish()) + * kOffroadPenalty); + } + if (newEndSaving > endSaving) + { + end = i; + endSaving = newEndSaving; + } + } + /* + * The router may return a route that starts and ends with a partial segment. In decoder mode, + * we don’t allow starting or ending a segment at an intermediate point but use the nearest + * segment endpoint instead. However, this may leave us with a zero-length segment, which we + * need to remove so we don’t overshoot the reference point. + */ + // TODO not if we have an `at` point + if ((end == (rsegments.size() - 1)) && (rsegments.size() > 1) + && (rsegments[rsegments.size() - 1].GetTimeFromBeginningSec() + == rsegments[rsegments.size() - 2].GetTimeFromBeginningSec())) + end--; +} +} // namespace traffxml diff --git a/libs/traffxml/traff_decoder.hpp b/libs/traffxml/traff_decoder.hpp new file mode 100644 index 000000000..3aa78c130 --- /dev/null +++ b/libs/traffxml/traff_decoder.hpp @@ -0,0 +1,645 @@ +#pragma once + +#include "traffxml/traff_model.hpp" + +#include "indexer/data_source.hpp" +#include "indexer/mwm_set.hpp" + +// Only needed for OpenlrTraffDecoder, see below +#if 0 +#include "openlr/openlr_decoder.hpp" +#include "openlr/openlr_model.hpp" +#endif + +#include "routing/index_router.hpp" +#include "routing/regions_decl.hpp" +#include "routing/router.hpp" +#include "routing/vehicle_mask.hpp" + +#include "routing_common/num_mwm_id.hpp" + +#include "storage/country_info_getter.hpp" + +#include + +namespace traffxml +{ +/** + * @brief Abstract base class for all TraFF decoder implementations. + * + * At this point, `TraffDecoder` is single-threaded and not guaranteed to be thread-safe. This means + * that all `TraffDecoder` operations should be limited to one thread or use appropriate thread + * synchronization mechanisms. In particular, calling `DecodeMessage()` concurrently from multiple + * threads is not supported. + */ +class TraffDecoder +{ +public: + using CountryInfoGetterFn = std::function; + using CountryParentNameGetterFn = std::function; + + TraffDecoder(DataSource & dataSource, CountryInfoGetterFn countryInfoGetter, + const CountryParentNameGetterFn & countryParentNameGetter, + std::map & messageCache); + + virtual ~TraffDecoder() {} + + /** + * @brief Decodes a single message to its segments and their speed groups. + * + * This method is not guaranteed to be thread-safe. All calls to this method should either be + * strictly limited to one designated thread, or be synchronized using an appropriate mechanism. + * + * In addition to the above, this method may access the message cache which was passed to the + * constructor. This is not thread-safe and needs to be synchronized, unless all other operations + * on the message cache are guaranteed to happen on the same thread that called this method. + * + * @param message The message to decode. + */ + void DecodeMessage(traffxml::TraffMessage & message); + +protected: + /** + * @brief Decodes a TraFF location. + * + * @param message The message to decode. + * @param decoded Receives the decoded segments. The speed group will be `Unknown`. + */ + virtual void DecodeLocation(traffxml::TraffMessage & message, traffxml::MultiMwmColoring & decoded) = 0; + + /** + * @brief Applies traffic impact to a decoded TraFF location. + * + * Applying impact sets the corresponding speed groups of the decoded segments. Existing speed groups will be overwritten. + * + * @param impact The traffic impact to apply. + * @param decoded The decoded segments. + */ + void ApplyTrafficImpact(traffxml::TrafficImpact & impact, traffxml::MultiMwmColoring & decoded); + + DataSource & m_dataSource; + CountryInfoGetterFn m_countryInfoGetterFn; + CountryParentNameGetterFn m_countryParentNameGetterFn; + + /** + * @brief Cache of all currently active TraFF messages. + * + * Keys are message IDs, values are messages. + */ + std::map & m_messageCache; + + /** + * @brief Consolidated traffic impact of the message currently being decoded + */ + std::optional m_trafficImpact; + +private: +}; + +// Disabled for now, as the OpenLR-based decoder is slow, buggy and not well suited to the task. +#if 0 +/** + * @brief A `TraffDecoder` implementation which internally uses the version 3 OpenLR decoder. + */ +class OpenLrV3TraffDecoder : public TraffDecoder +{ +public: + OpenLrV3TraffDecoder(DataSource & dataSource, CountryInfoGetterFn countryInfoGetter, + const CountryParentNameGetterFn & countryParentNameGetter, + std::map & messageCache); + +protected: + /** + * @brief Decodes a TraFF location. + * + * @param message The message to decode. + * @param decoded Receives the decoded segments. The speed group will be `Unknown`. + */ + void DecodeLocation(traffxml::TraffMessage & message, traffxml::MultiMwmColoring & decoded) override; + +private: + /** + * @brief Returns the OpenLR functional road class (FRC) matching a TraFF road class. + * + * @param roadClass The TraFF road class. + * @return The FRC. + */ + static openlr::FunctionalRoadClass GetRoadClassFrc(std::optional & roadClass); + + /** + * @brief Guess the distance between two points. + * + * If both `p1` and `p2` have the `distance` attribute set, the difference between these two is + * evaluated. If it is within a certain tolerance margin of the direct distance between the two + * points, this value is returned. Otherwise, the distance is calculated from direct distance, + * multiplied with a tolerance factor to account for the fact that the road is not always a + * straight line. + * + * The result can be used to provide some semi-valid DNP values. + * + * @param p1 The first point. + * @param p2 The second point. + * @return The approximate distance on the ground, in meters. + */ + static uint32_t GuessDnp(Point & p1, Point & p2); + + /** + * @brief Converts a TraFF point to an OpenLR location reference point. + * + * Only coordinates are populated. + * + * @param point The point + * @return An OpenLR LRP with the coordinates of the point. + */ + static openlr::LocationReferencePoint PointToLrp(Point & point); + + /** + * @brief Converts a TraFF location to an OpenLR linear location reference. + * + * @param location The location + * @param backwards If true, gnerates a linear location reference for the backwards direction, + * with the order of points reversed. + * @return An OpenLR linear location reference which corresponds to the location. + */ + static openlr::LinearLocationReference TraffLocationToLinearLocationReference(TraffLocation & location, bool backwards); + + /** + * @brief Converts a TraFF location to a vector of OpenLR segments. + * + * Depending on the directionality, the resulting vector will hold one or two elements: one for + * the forward direction, and for bidirectional locations, a second one for the backward + * direction. + * + * @param location The location + * @param messageId The message ID + * @return A vector holding the resulting OpenLR segments. + */ + static std::vector TraffLocationToOpenLrSegments(TraffLocation & location, std::string & messageId); + + /** + * @brief The OpenLR decoder instance. + * + * Used to decode TraFF locations into road segments on the map. + */ + openlr::OpenLRDecoder m_openLrDecoder; +}; +#endif + +/** + * @brief A `TraffDecoder` implementation which internally uses the routing engine. + */ +class RoutingTraffDecoder : public TraffDecoder, + public MwmSet::Observer +{ +public: + class DecoderRouter : public routing::IndexRouter + { + public: + /** + * @brief Creates a new `DecoderRouter` instance. + * + * @param countryParentNameGetterFn Function which converts a country name into the name of its parent country) + * @param countryFileFn Function which converts a pointer to its country name + * @param countryRectFn Function which returns the rect for a country + * @param numMwmIds + * @param numMwmTree + * @param trafficCache The traffic cache (used only if `vehicleType` is `VehicleType::Car`) + * @param dataSource The MWM data source + * @param decoder The `TraffDecoder` instance to which this router instance is coupled + */ + DecoderRouter(CountryParentNameGetterFn const & countryParentNameGetterFn, + routing::TCountryFileFn const & countryFileFn, + routing::CountryRectFn const & countryRectFn, + std::shared_ptr numMwmIds, + std::unique_ptr> numMwmTree, + DataSource & dataSource, RoutingTraffDecoder & decoder); + protected: + /** + * @brief Whether the set of fake endings generated for the check points is restricted. + * + * The return value is used internally when snapping checkpoints to edges. If this function + * returns true, this instructs the `PointsOnEdgesSnapping` instance to consider only edges which + * are not fenced off, i.e. can be reached from the respective checkpoint without crossing any + * other edges. If it returns false, this restriction does not apply, and all nearby edges are + * considered. + * + * Restricting the set of fake endings in this manner decreases the options considered for routing + * and thus processing time, which is desirable for regular routing and has no side effects. + * For TraFF location matching, simplification has undesirable side effects: if reference points + * are located on one side of the road, the other carriageway may not be considered. This would + * lead to situations like these: + * + * --<--<-+<==<==<==<==<==<==<==<==<==<==<==<==<==<==<==<==<==<==<==<==<==<==<==<==<==<+-<--<-- + * -->-->-+>==>==>==>==>==>==>-->-->-->-->-->-->-->-->-->-->-->==>==>==>==>==>==>==>==>+->-->-- + * *< <* + * + * (-- carriageway, + junction, < > direction, *< end point, <* start point, == route) + * + * To avoid this, the `DecoderRouter` implementation always returns false. + */ + /** + * @brief Returns the mode in which the router is operating. + * + * The `DecoderRouter` always returns `Mode::Decoding`. + * + * In navigation mode, the router may exit with `RouterResultCode::NeedMoreMaps` if it determines + * that a better route can be calculated with additional maps. When snapping endpoints to edges, + * it will consider only edges which are not “fenced off” by other edges, i.e. which can be + * reached from the endpoint without crossing other edges. This decreases the number of fake + * endings and thus speeds up routing, without any undesirable side effects for that use case. + * + * Asking the user to download extra maps is neither practical for a TraFF decoder which runs in + * the background and may decode many locations, one by one, nor is it needed (if maps are + * missing, we do not need to decode traffic reports for them). + * + * Eliminating fenced-off edges from the snapping candidates has an undesirable side effect for + * TraFF location decoding on dual-carriageway roads: if the reference points are outside the + * carriageways, only one direction gets considered for snapping, as the opposite direction is + * fenced off by it. This may lead to situations like these: + * + * ~~~ + * --<--<-+<==<==<==<==<==<==<==<==<==<==<==<==<==<==<==<==<==<==<==<==<==<==<==<==<==<+-<--<-- + * -->-->-+>==>==>==>==>==>==>-->-->-->-->-->-->-->-->-->-->-->==>==>==>==>==>==>==>==>+->-->-- + * |< <| + * + * (-- carriageway, + junction, < > direction, |< end point, <| start point, == route) + * ~~~ + * + * Therefore, in decoding mode, the router will never exit with `RouterResultCode::NeedMoreMaps` + * but tries to find a route with the existing maps, or exits without a route. When snapping + * endpoints to edges, it considers all edges within the given radius, fenced off or not. + */ + IndexRouter::Mode GetMode() override { return IndexRouter::Mode::Decoding; } + + /** + * @brief Returns current routing options. + * + * For traffic decoding purposes, all roads are allowed. + */ + routing::RoutingOptions GetRoutingOptions() override; + + private: + }; + + class TraffEstimator final : public routing::EdgeEstimator + { + public: + TraffEstimator(DataSource * dataSourcePtr, std::shared_ptr numMwmIds, + double maxWeightSpeedKMpH, + routing::SpeedKMpH const & offroadSpeedKMpH, + RoutingTraffDecoder & decoder) + : EdgeEstimator(routing::VehicleType::Decoder, maxWeightSpeedKMpH, offroadSpeedKMpH, dataSourcePtr, numMwmIds) + , m_decoder(decoder) + { + } + + // EdgeEstimator overrides: + + /** + * @brief Estimates travel time between two points along a direct fake edge. + * + * Estimates time in seconds it takes to go from point `from` to point `to` along direct fake edge. + * + * @param from The start point. + * @param to The destination point. + * @param purpose The purpose for which the result is to be used. + * @return Travel time in seconds. + */ + double CalcOffroad(ms::LatLon const & from, ms::LatLon const & to, Purpose /* purpose */) const override; + double CalcSegmentWeight(routing::Segment const & segment, routing::RoadGeometry const & road, Purpose /* purpose */) const override; + + double GetUTurnPenalty(Purpose /* purpose */) const override; + + /** + * @brief Determines the penalty factor for making a turn. + * + * The turn is at the first or last points of `from_road` and `to_road` and can be determined + * by comparing the endpoints of `from_road` and `to_road` for a match. + * + * @param purpose The purpose for which the penalty is calculated, ignored by this implementation + * @param angle The angle in degrees (negative values indicate a right turn) + * @param from_road The road (segment between two junctions) before the turn + * @param to_road The road (segment between two junctions) after the turn + * @param is_left_hand_traffic True for left-hand traffic, false for right-hand traffic + */ + double GetTurnPenalty(Purpose /* purpose */, double angle, routing::RoadGeometry const & from_road, + routing::RoadGeometry const & to_road, bool is_left_hand_traffic = false) const override; + double GetFerryLandingPenalty(Purpose /* purpose */) const override; + + /** + * @brief Whether access restrictions are ignored. + * + * A return value of false indicates that access restrictions should be observed, which is the + * default behavior for a routing use case. If true, it indicates that routing should ignore + * access restrictions. This is needed to resolve traffic message locations; it could also be + * used e.g. for emergency vehicle use cases. + * + * This implementation may return true or false, depending on the location being decoded. + */ + bool IsAccessIgnored() const override; + + private: + RoutingTraffDecoder & m_decoder; + }; + + struct JunctionCandidateInfo + { + JunctionCandidateInfo(double weight) + : m_weight(weight) + {} + + double m_weight; + size_t m_segmentsIn = 0; + size_t m_segmentsOut = 0; + size_t m_twoWaySegments = 0; + }; + + RoutingTraffDecoder(DataSource & dataSource, CountryInfoGetterFn countryInfoGetter, + const CountryParentNameGetterFn & countryParentNameGetter, + std::map & messageCache); + + /** + * @brief Called when a map is registered for the first time and can be used. + */ + void OnMapRegistered(platform::LocalCountryFile const & localFile) override; + + /** + * @brief Called when a map is deregistered and can no longer be used. + * + * This implementation does nothing, as `NumMwmIds` does not support removal. + */ + virtual void OnMapDeregistered(platform::LocalCountryFile const & /* localFile */) override {} + + /** + * @brief Determines the penalty factor bases on how highway attributes match. + * + * This compares the highway type of the candidate feature (as retrieved from OSM) against the + * road class and ramps attributes of the location. + * + * Rules are subject to change but principles are: + * + * Penalties for ramp mismatch and road class mismatch are applied consecutively, thus the maximum + * penalty is `kAttributePenalty ^ 2`. + * + * If ramps mismatch (location specifies a ramp but candidate is not a ramp, or vice versa), the + * penalty is `kAttributePenalty`. + * + * If road classes are similar, the penalty is `kReducedAttributePenalty`. For a complete + * mismatch, the penalty is `kAttributePenalty`. + * + * @param highwayType The OSM highway type of the candidate feature. + * @param roadClass The TraFF road class of the location. + * @param ramps The ramps atribute of the TraFF location. + * + * @return 1 for a perfect match (same road class and ramp type), up to `kAttributePenalty ^ 2` + * for a mismatch. + */ + static double GetHighwayTypePenalty(std::optional highwayType, + std::optional roadClass, + Ramps ramps); + + /** + * @brief Determines the penalty factor based on how two reference numbers match. + * + * Rules are subject to change. + * + * This method takes a vector as an argument, compares each element and returns the penalty for + * the best match. + * + * @param refs A vector of reference numbers of the current segment, compared against `m_roadRef`. + * + * @return 1 for a perfect match (refs are assumed to refer to the same object), `kAttributePenalty` + * for a mismatch (refs are assumed to refer to different objects) or`kReducedAttributePenalty` for + * a partial match (unclear whether both refs refer to the same object). + */ + double GetRoadRefPenalty(std::vector & refs) const; + + /** + * @brief Determines the penalty factor based on how two reference numbers match. + * + * Rules are subject to change. + * + * @param ref The reference number of the current segment, compared against `m_roadRef`. + * + * @return 1 for a perfect match (refs are assumed to refer to the same object), `kAttributePenalty` + * for a mismatch (refs are assumed to refer to different objects) or`kReducedAttributePenalty` for + * a partial match (unclear whether both refs refer to the same object). + */ + double GetRoadRefPenalty(std::string const & ref) const; + +protected: + /** + * @brief Initializes the router. + * + * This is usually done in the constructor but fails if no maps are loaded (attempting to + * construct a router without maps results in a crash, hence we check for maps and exit with an + * error if we have none). It can be repeated any time. + * + * Attempting to initialize a router which has already been succesfully initialized is a no-op. It + * will be reported as success. + * + * @return true if successful, false if not. + */ + bool InitRouter(); + + /** + * @brief Adds a segment to the decoded segments. + * + * @param decoded The decoded segments. + * @param segment The segment to add. + */ + void AddDecodedSegment(traffxml::MultiMwmColoring & decoded, routing::Segment & segment); + + /** + * @brief Decodes one direction of a TraFF location. + * + * @param message The message to decode. + * @param decoded Receives the decoded segments. The speed group will be `Unknown`. + * @param backwards If true, decode the backward direction, else the forward direction. + */ + void DecodeLocationDirection(traffxml::TraffMessage & message, + traffxml::MultiMwmColoring & decoded, bool backwards); + + /** + * @brief Decodes a TraFF location. + * + * @param message The message to decode. + * @param decoded Receives the decoded segments. The speed group will be `Unknown`. + */ + void DecodeLocation(traffxml::TraffMessage & message, traffxml::MultiMwmColoring & decoded) override; + + /** + * @brief Truncates the route so its endpoints best match the reference points. + * + * Leading and trailing fake segments are discarded. + * + * When building the graph, the router creates fake segments to the nearest roads. These are not + * necessarily the best for location decoding, which may result in “heads” or “tails” being added + * to the decoded location. This function attempts to detect and remove them. + * + * To do this, it iterates over the nodes (taken from `rsegments`) and determines if any of them + * is a better start/end candidate. This is done by calculating the cost of leaping between the + * node and the corresponding checkpoint; if this is cheaper than the stretch of route bypassed + * in this way, the node becomes a candidate for the corresponding endpoint. The higher the cost + * saving, the better the candidate. + * + * After identifying the best candidate for each endpoint, segments outside these nodes are + * discarded. + * + * @param rsegments The segments of the route + * @param checkpoints The reference points (at least two) + * @param backwards True when decoding the backward direction, false when decodign the forward direction. + */ + void TruncateRoute(std::vector & rsegments, + routing::Checkpoints const & checkpoints, bool backwards); + +private: + static void LogCode(routing::RouterResultCode code, double const elapsedSec); + + /** + * @brief Populates the list of candidates for junction points. + * + * If the location has a fuzziness of `LowRes`, the map is searched for candidates around the + * `from` and `to` points, which are taken from the `m_location` member of `m_message`. The weight + * for each candidate is calculated based on its distance from the reference point and the match + * between the attributes of the segment and the location. Since junction points are part of + * multiple segments, the best match wins. Candidates and their weight are stored in + * `m_startJunctions` and `m_endJunctions`. + * + * If the location’s fuzziness attribute is empty or does not equal `LowRes`, `m_startJunctions` + * and `m_endJunctions` are cleared. + */ + void GetJunctionPointCandidates(); + + /** + * @brief Populates a list of candidates for junction points. + * + * Implementation for `GetJunctionPointCandidates()`. The map is searched for candidates around + * `point`. The weight for each candidate is calculated based on its distance from `point` and + * the match between the attributes of the segment and the location of `m_message`. Since junction + * points are part of multiple segments, the best match wins. Candidates and their weight are + * stored in `junctions`. + * + * @param point The reference point + * @param junctions Receives a list of junction candidates with their weight + */ + void GetJunctionPointCandidates(Point const & point, + std::map & junctions); + + /** + * @brief Mutex for access to shared members. + * + * This is to prevent adding newly-registered maps while the router is in use. + * + * @todo As per the `MwmSet::Observer` documentation, implementations should be quick and lean, + * as they may be called from any thread. Locking a mutex may be in conflict with this, as it may + * mean locking up the caller while a location is being decoded. + */ + std::mutex m_mutex; + + std::shared_ptr m_numMwmIds = std::make_shared(); + std::unique_ptr m_router; + std::optional m_message = std::nullopt; + + /** + * @brief Junction points near start of location, with their associated offroad weight. + * + * If the list is empty, no junction alignment at the `from` point will be done and decoding + * relies solely on point coordinates. + */ + std::map m_startJunctions; + + /** + * @brief Junction points near end of location, with their associated offroad weight. + * + * If the list is empty, no junction alignment at the `to` point will be done and decoding + * relies solely on point coordinates. + */ + std::map m_endJunctions; + + /** + * @brief Radius around reference points in which to search for junctions. + * + * Determined dynamically, based on distance between reference points. + * Maximum distance is never more than half the distance between endpoints. + * It should be between `kJunctionRadiusMin` and `kJunctionRadiusMax`, and as close as possible to + * 1/3 the distance. + */ + double m_junctionRadius; + + /** + * @brief The road ref of `m_message`, parsed with `ParseRef()` + */ + std::vector m_roadRef; +}; + +/** + * @brief The default TraFF decoder implementation, recommended for production use. + */ +//using DefaultTraffDecoder = OpenLrV3TraffDecoder; +using DefaultTraffDecoder = RoutingTraffDecoder; + +traffxml::RoadClass GetRoadClass(routing::HighwayType highwayType); +double GetRoadClassPenalty(traffxml::RoadClass lhs, traffxml::RoadClass rhs); +bool IsConstruction(routing::HighwayType highwayType); +bool IsRamp(routing::HighwayType highwayType); + +/** + * @brief Breaks down a ref into groups for comparison. + * + * The result of this function can be used to determine if two reference numbers match partially + * (such as `A4`, `A4bis` and `A4.1`). + * + * Implementation details may change; currently the following applies: + * + * A whitespace character (or sequence of whitespace characters), or a switch between letters and + * digits, starts a new group. + * + * Letters are converted to lowercase. + * + * For example, each of `A42`, `A 42` and `-a42` would be broken down into `a, 42`, whereas `A4.2` + * would be broken down into `a, 4, 2`. + */ +std::vector ParseRef(std::string const & ref); + +/** + * @brief Calculates the segments to truncate at the start of the route. + * + * The route is not actually truncated by this function. + * + * `start` and `startSaving` should be 0 when calling this function. After it returns, these values + * will indicate the first segment to keep and the cost saved by truncating everything before. + * + * @param rsegments The segments of the route + * @param checkpoints The reference points (at least two) + * @param start Index of the first segment to keep + * @param startSaving Cost saved by truncating + * @param startWeight Weight of the fake segments up to the first real segment + * @param junctions Junctions with the weight of their leap segment + */ +void TruncateStart(std::vector & rsegments, + routing::Checkpoints const & checkpoints, + size_t & start, double & startSaving, double const startWeight, + std::map const & junctions); + +/** + * @brief Calculates the segments to truncate at the start of the route. + * + * The route is not actually truncated by this function. + * + * `end` should be `rsegments.size() - 1` and `endSaving` should be 0 when calling this function. + * After it returns, these values will indicate the last segment to keep and the cost saved by + * truncating everything after. + * + * @param rsegments The segments of the route + * @param checkpoints The reference points (at least two) + * @param end Index of the last segment to keep + * @param endSaving Cost saved by truncating + * @param endWeight Total weight of the route, including trailing fake segments + * @param junctions Junctions with the weight of their leap segment + */ +void TruncateEnd(std::vector & rsegments, + routing::Checkpoints const & checkpoints, + size_t & end, double & endSaving, double const endWeight, + std::map const & junctions); +} // namespace traffxml diff --git a/libs/traffxml/traff_model.cpp b/libs/traffxml/traff_model.cpp new file mode 100644 index 000000000..1a083a55a --- /dev/null +++ b/libs/traffxml/traff_model.cpp @@ -0,0 +1,635 @@ +#include "traffxml/traff_model.hpp" + +#include "base/logging.hpp" + +#include +#include + +#include + +using namespace std; + +namespace traffxml +{ +const std::unordered_map kEventSpeedGroupMap{ + // TODO Activity*, Authority*, Carpool* (not in enum yet) + {EventType::CongestionHeavyTraffic, traffic::SpeedGroup::G4}, + {EventType::CongestionLongQueue, traffic::SpeedGroup::G0}, + {EventType::CongestionNone, traffic::SpeedGroup::G5}, + {EventType::CongestionNormalTraffic, traffic::SpeedGroup::G5}, + {EventType::CongestionQueue, traffic::SpeedGroup::G2}, + {EventType::CongestionQueueLikely, traffic::SpeedGroup::G3}, + {EventType::CongestionSlowTraffic, traffic::SpeedGroup::G3}, + {EventType::CongestionStationaryTraffic, traffic::SpeedGroup::G1}, + {EventType::CongestionStationaryTrafficLikely, traffic::SpeedGroup::G2}, + {EventType::CongestionTrafficBuildingUp, traffic::SpeedGroup::G4}, + {EventType::CongestionTrafficCongestion, traffic::SpeedGroup::G3}, // TODO or G2? Unquantified, below normal + {EventType::CongestionTrafficFlowingFreely, traffic::SpeedGroup::G5}, + {EventType::CongestionTrafficHeavierThanNormal, traffic::SpeedGroup::G4}, + {EventType::CongestionTrafficLighterThanNormal, traffic::SpeedGroup::G5}, + {EventType::CongestionTrafficMuchHeavierThanNormal, traffic::SpeedGroup::G3}, + {EventType::CongestionTrafficProblem, traffic::SpeedGroup::G3}, // TODO or G2? Unquantified, below normal + // TODO Construction* (not in enum yet) + /* + * Some delay types have a duration which depends on the route. This is better expressed as a + * speed group, although the mapping may be somewhat arbitrary and may need to be corrected. + */ + {EventType::DelayDelay, traffic::SpeedGroup::G2}, + {EventType::DelayDelayPossible, traffic::SpeedGroup::G3}, + {EventType::DelayLongDelay, traffic::SpeedGroup::G1}, + {EventType::DelayVeryLongDelay, traffic::SpeedGroup::G0}, + // TODO Environment*, EquipmentStatus*, Hazard*, Incident* (not in enum yet) + // TODO complete Restriction* (not in enum yet) + {EventType::RestrictionBlocked, traffic::SpeedGroup::TempBlock}, + {EventType::RestrictionBlockedAhead, traffic::SpeedGroup::TempBlock}, + //{EventType::RestrictionCarriagewayBlocked, traffic::SpeedGroup::TempBlock}, // TODO FIXME other carriageways may still be open + //{EventType::RestrictionCarriagewayClosed, traffic::SpeedGroup::TempBlock}, // TODO FIXME other carriageways may still be open + {EventType::RestrictionClosed, traffic::SpeedGroup::TempBlock}, + {EventType::RestrictionClosedAhead, traffic::SpeedGroup::TempBlock}, + {EventType::RestrictionEntryBlocked, traffic::SpeedGroup::TempBlock}, + {EventType::RestrictionExitBlocked, traffic::SpeedGroup::TempBlock}, + {EventType::RestrictionRampBlocked, traffic::SpeedGroup::TempBlock}, + {EventType::RestrictionRampClosed, traffic::SpeedGroup::TempBlock}, + {EventType::RestrictionSpeedLimit, traffic::SpeedGroup::G4}, + // TODO Security*, Transport*, Weather* (not in enum yet) +}; + +// none of the currently define events imply an explicit maxspeed +#if 0 +const std::unordered_map kEventMaxspeedMap{ + // TODO Activity*, Authority*, Carpool* (not in enum yet) + // TODO Construction* (not in enum yet) + // TODO Environment*, EquipmentStatus*, Hazard*, Incident* (not in enum yet) + // TODO complete Restriction* (not in enum yet) + // TODO Security*, Transport*, Weather* (not in enum yet) +}; +#endif + +const std::unordered_map kEventDelayMap{ + // TODO Activity*, Authority*, Carpool* (not in enum yet) + // TODO Construction* (not in enum yet) + //{EventType::DelayDelay, }, // mapped to speed group + //{EventType::DelayDelayPossible, }, // mapped to speed group + //{EventType::DelayLongDelay, }, // mapped to speed group + {EventType::DelaySeveralHours, 150}, // assumption: 2.5 hours + {EventType::DelayUncertainDuration, 60}, // assumption: 1 hour + //{EventType::DelayVeryLongDelay, }, // mapped to speed group + // TODO Environment*, EquipmentStatus*, Hazard*, Incident* (not in enum yet) + // TODO complete Restriction* (not in enum yet) + // TODO Security*, Transport*, Weather* (not in enum yet) +}; + +std::optional IsoTime::ParseIsoTime(std::string timeString) +{ + /* + * TODO this is ugly because we need to work around some compiler deficiencies. + * + * Ideally, we would be using `std::chrono::time_point` and parse the + * string using `std::chrono::from_stream`, using `%FT%T%z` for the format string. + * This works in GCC 14+ and is pleasantly liberal about the time zone format (all of +01, +0100 + * and +01:00 are parsed correctly). Alas, Ubuntu 24.04 (currently the default dev platform) comes + * with GCC 13.2, which lacks this support. Clang, the only supported compiler for Android (and, + * presumably, iOS), as of mid-2025, doesn’t support it at all. + * + * The workaround is therefore to use `std::chrono::time_point`, which + * exposes the same API as its `utc_clock` counterpart, making transition at a later point easy. + * In addition, however, it can be constructed from `std::time_t`, which we can generate from + * `std::tm`. Unlike the other C legacy functions, gmtime is thread-safe. + * Still not the prettiest way (as it relies on legacy C functions which are not + * thread-safe), but the best we can get until we have proper compiler support for `from_stream`. + * + * Should we have support for `std::chrono:clock_cast` but not `std::chrono::from_stream`, we + * could build a `std::chrono::sys_seconds` from the constutuent values and use + * `std::chrono::clock_cast` to convert it to a `std::chrono::time_point`, based on whatever clock + * is supported. This works on Linux (using `utc_clock`) as of mid-2025, but not on the primary + * target platforms (Android and iOS) and has therefore been left out for uniformity (and + * reproducibility of bugs). + */ + /* + * Regex for ISO 8601 time, with some tolerance for time zone offset. If matched, the matcher + * will contain the following items: + * + * 0: 2019-11-01T11:55:42+01:00 (entire expression) + * 1: 2019 (year) + * 2: 11 (month) + * 3: 01 (day) + * 4: 11 (hour, local) + * 5: 55 (minute, local) + * 6: 42.445 (second, local, float) + * 7: .445 (fractional seconds) + * 8: +01:00 (complete UTC offset, or Z; blank if not specified) + * 9: +01:00 (complete UTC offset, blank for Z or of not specified) + * 10: +01 (UTC offset, hours with sign; blank for Z or if not specified) + * 11: :00 (UTC offset, minutes, prefixed with separator) + * 12: 00 (UTC offset, minutes, unsigned; blank for Z or if not specified) + */ + static boost::regex iso8601Regex("([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2}(\\.[0-9]*)?)(Z|(([+-][0-9]{2})(:?([0-9]{2}))?))?"); + + boost::smatch iso8601Matcher; + if (boost::regex_search(timeString, iso8601Matcher, iso8601Regex)) + { + int offset_h = iso8601Matcher[10].matched ? std::stoi(iso8601Matcher[10]) : 0; + int offset_m = iso8601Matcher[12].matched ? std::stoi(iso8601Matcher[12]) : 0; + if (offset_h < 0) + offset_m *= -1; + + std::tm tm = {}; + tm.tm_year = std::stoi(iso8601Matcher[1]) - 1900; + tm.tm_mon = std::stoi(iso8601Matcher[2]) - 1; + tm.tm_mday = std::stoi(iso8601Matcher[3]); + tm.tm_hour = std::stoi(iso8601Matcher[4]) - offset_h; + tm.tm_min = std::stoi(iso8601Matcher[5]) - offset_m; + tm.tm_sec = std::stof(iso8601Matcher[6]) + 0.5f; + + std::time_t tt = timegm(&tm); + + std::chrono::time_point tp = std::chrono::system_clock::from_time_t(tt); + + IsoTime result(tp); + return result; + } + else + { + LOG(LINFO, ("Not a valid ISO 8601 timestamp:", timeString)); + return std::nullopt; + } +} + +IsoTime IsoTime::Now() +{ + return IsoTime(std::chrono::system_clock::now()); +} + +IsoTime::IsoTime(std::chrono::time_point tp) + : m_tp(tp) +{} + +bool IsoTime::IsPast() +{ + return m_tp < std::chrono::system_clock::now(); +}\ + +void IsoTime::Shift(IsoTime nowRef) +{ + auto const offset = std::chrono::system_clock::now() - nowRef.m_tp; + m_tp += offset; +} + +std::string IsoTime::ToString() const +{ + auto const tp_seconds = time_point_cast(m_tp); + auto const time_t = std::chrono::system_clock::to_time_t(tp_seconds); + std::tm tm = *std::gmtime(&time_t); + + std::ostringstream ss; + ss << std::put_time(&tm, "%Y-%m-%dT%H:%M:%S"); + ss << "Z"; + return ss.str(); +} + +bool IsoTime::operator< (IsoTime & rhs) +{ + return m_tp < rhs.m_tp; +} + +bool IsoTime::operator> (IsoTime & rhs) +{ + return m_tp > rhs.m_tp; +} + +bool operator==(TrafficImpact const & lhs, TrafficImpact const & rhs) +{ + if ((lhs.m_speedGroup == traffic::SpeedGroup::TempBlock) + && (rhs.m_speedGroup == traffic::SpeedGroup::TempBlock)) + return true; + return (lhs.m_speedGroup == rhs.m_speedGroup) + && (lhs.m_maxspeed == rhs.m_maxspeed) + && (lhs.m_delayMins == rhs.m_delayMins); +} + +bool operator==(Point const & lhs, Point const & rhs) +{ + return lhs.m_coordinates == rhs.m_coordinates; +} + +bool operator==(TraffLocation const & lhs, TraffLocation const & rhs) +{ + return (lhs.m_from == rhs.m_from) + && (lhs.m_at == rhs.m_at) + && (lhs.m_via == rhs.m_via) + && (lhs.m_notVia == rhs.m_notVia) + && (lhs.m_to == rhs.m_to); +} + +IsoTime TraffMessage::GetEffectiveExpirationTime() +{ + IsoTime result = m_expirationTime; + if (m_startTime && m_startTime.value() > result) + result = m_startTime.value(); + if (m_endTime && m_endTime.value() > result) + result = m_endTime.value(); + return result; +} + +bool TraffMessage::IsExpired(IsoTime now) +{ + return GetEffectiveExpirationTime() < now; +} + +std::optional TraffMessage::GetTrafficImpact() +{ + // no events, no impact + if (m_events.empty()) + return std::nullopt; + + // examine events + std::vector impacts; + for (auto event : m_events) + { + TrafficImpact impact; + + if (auto it = kEventSpeedGroupMap.find(event.m_type); it != kEventSpeedGroupMap.end()) + impact.m_speedGroup = it->second; + + if (event.m_speed) + impact.m_maxspeed = event.m_speed.value(); + // TODO if no explicit speed given, look up in kEventMaxspeedMap (once we have entries) + + if (event.m_class == EventClass::Delay + && event.m_type != EventType::DelayClearance + && event.m_type != EventType::DelayForecastWithdrawn + && event.m_type != EventType::DelaySeveralHours + && event.m_type != EventType::DelayUncertainDuration + && event.m_qDurationMins) + impact.m_delayMins = event.m_qDurationMins.value(); + else if (auto it = kEventDelayMap.find(event.m_type); it != kEventDelayMap.end()) + impact.m_delayMins = it->second; + + // TempBlock overrules everything else, return immediately + if (impact.m_speedGroup == traffic::SpeedGroup::TempBlock) + return impact; + // if there is no actual impact, discard + if ((impact.m_maxspeed < kMaxspeedNone) + || (impact.m_delayMins > 0) + || (impact.m_speedGroup != traffic::SpeedGroup::Unknown)) + impacts.push_back(impact); + } + + if (impacts.empty()) + return std::nullopt; + + TrafficImpact result; + for (auto impact : impacts) + { + ASSERT(impact.m_speedGroup != traffic::SpeedGroup::TempBlock, ("Got SpeedGroup::TempBlock, which should not happen at this stage")); + if (result.m_speedGroup == traffic::SpeedGroup::Unknown) + result.m_speedGroup = impact.m_speedGroup; + // TempBlock cannot occur here, so we can do just a simple comparison + else if ((impact.m_speedGroup != traffic::SpeedGroup::Unknown) && (impact.m_speedGroup < result.m_speedGroup)) + result.m_speedGroup = impact.m_speedGroup; + + if (impact.m_maxspeed < result.m_maxspeed) + result.m_maxspeed = impact.m_maxspeed; + + if (impact.m_delayMins > result.m_delayMins) + result.m_delayMins = impact.m_delayMins; + } + if ((result.m_maxspeed < kMaxspeedNone) + || (result.m_delayMins > 0) + || (result.m_speedGroup != traffic::SpeedGroup::Unknown)) + return result; + else + // should never happen, unless we have a bug somewhere + return std::nullopt; +} + +void TraffMessage::ShiftTimestamps() +{ + IsoTime nowRef = m_updateTime; + m_receiveTime.Shift(nowRef); + m_updateTime.Shift(nowRef); + m_expirationTime.Shift(nowRef); + if (m_startTime) + m_startTime.value().Shift(nowRef); + if (m_endTime) + m_endTime.value().Shift(nowRef); +} + +void MergeMultiMwmColoring(const MultiMwmColoring & delta, MultiMwmColoring & target) +{ + // for each mwm in delta + for (auto [mwmId, coloring] : delta) + // if target contains mwm + if (auto target_it = target.find(mwmId); target_it != target.end()) + // for each segment in delta[mwm] (coloring) + for (auto [rsid, sg] : coloring) + // if target[mwm] contains segment + if (auto c_it = target_it->second.find(rsid) ; c_it != target_it->second.end()) + { + // if delta overrules target (target is Unknown, delta is TempBlock or delta is slower than target) + if ((sg == traffic::SpeedGroup::TempBlock) + || (c_it->second == traffic::SpeedGroup::Unknown) || (sg < c_it->second)) + target_it->second[rsid] = sg; + } + else + // if target[mwm] does not contain segment, add speed group + target_it->second[rsid] = sg; + else + // if target does not contain mwm, add coloring + target[mwmId] = coloring; +} + +/* +string DebugPrint(LinearSegmentSource source) +{ + switch (source) + { + case LinearSegmentSource::NotValid: return "NotValid"; + case LinearSegmentSource::FromLocationReferenceTag: return "FromLocationReferenceTag"; + case LinearSegmentSource::FromCoordinatesTag: return "FromCoordinatesTag"; + } + UNREACHABLE(); +} + */ +std::string DebugPrint(IsoTime time) +{ + std::ostringstream os; + //os << std::put_time(&time.m_tm, "%Y-%m-%d %H:%M:%S %z"); + // %FT%T%z + auto const time_t = std::chrono::system_clock::to_time_t(time.m_tp); + std::tm tm = *std::gmtime(&time_t); + os << std::put_time(&tm, "%Y-%m-%d %H:%M:%S UTC"); + return os.str(); +} + +std::string DebugPrint(Directionality directionality) +{ + switch (directionality) + { + case Directionality::OneDirection: return "OneDirection"; + case Directionality::BothDirections: return "BothDirections"; + } + UNREACHABLE(); +} + +std::string DebugPrint(Fuzziness fuzziness) +{ + switch (fuzziness) + { + case Fuzziness::LowRes: return "LowRes"; + case Fuzziness::MediumRes: return "MediumRes"; + case Fuzziness::EndUnknown: return "EndUnknown"; + case Fuzziness::StartUnknown: return "StartUnknown"; + case Fuzziness::ExtentUnknown: return "ExtentUnknown"; + } + UNREACHABLE(); +} + +std::string DebugPrint(Ramps ramps) +{ + switch (ramps) + { + case Ramps::All: return "All"; + case Ramps::Entry: return "Entry"; + case Ramps::Exit: return "Exit"; + case Ramps::None: return "None"; + } + UNREACHABLE(); +} + +std::string DebugPrint(RoadClass roadClass) +{ + switch (roadClass) + { + case RoadClass::Motorway: return "Motorway"; + case RoadClass::Trunk: return "Trunk"; + case RoadClass::Primary: return "Primary"; + case RoadClass::Secondary: return "Secondary"; + case RoadClass::Tertiary: return "Tertiary"; + case RoadClass::Other: return "Other"; + } + UNREACHABLE(); +} + +std::string DebugPrint(EventClass eventClass) +{ + switch (eventClass) + { + case EventClass::Invalid: return "Invalid"; + case EventClass::Activity: return "Activity"; + case EventClass::Authority: return "Authority"; + case EventClass::Carpool: return "Carpool"; + case EventClass::Congestion: return "Congestion"; + case EventClass::Construction: return "Construction"; + case EventClass::Delay: return "Delay"; + case EventClass::Environment: return "Environment"; + case EventClass::EquipmentStatus: return "EquipmentStatus"; + case EventClass::Hazard: return "Hazard"; + case EventClass::Incident: return "Incident"; + case EventClass::Restriction: return "Restriction"; + case EventClass::Security: return "Security"; + case EventClass::Transport: return "Transport"; + case EventClass::Weather: return "Weather"; + } + UNREACHABLE(); +} + +std::string DebugPrint(EventType eventType) +{ + switch (eventType) + { + case EventType::Invalid: return "Invalid"; + // TODO Activity*, Authority*, Carpool* (not in enum yet) + case EventType::CongestionCleared: return "CongestionCleared"; + case EventType::CongestionForecastWithdrawn: return "CongestionForecastWithdrawn"; + case EventType::CongestionHeavyTraffic: return "CongestionHeavyTraffic"; + case EventType::CongestionLongQueue: return "CongestionLongQueue"; + case EventType::CongestionNone: return "CongestionNone"; + case EventType::CongestionNormalTraffic: return "CongestionNormalTraffic"; + case EventType::CongestionQueue: return "CongestionQueue"; + case EventType::CongestionQueueLikely: return "CongestionQueueLikely"; + case EventType::CongestionSlowTraffic: return "CongestionSlowTraffic"; + case EventType::CongestionStationaryTraffic: return "CongestionStationaryTraffic"; + case EventType::CongestionStationaryTrafficLikely: return "CongestionStationaryTrafficLikely"; + case EventType::CongestionTrafficBuildingUp: return "CongestionTrafficBuildingUp"; + case EventType::CongestionTrafficCongestion: return "CongestionTrafficCongestion"; + case EventType::CongestionTrafficEasing: return "CongestionTrafficEasing"; + case EventType::CongestionTrafficFlowingFreely: return "CongestionTrafficFlowingFreely"; + case EventType::CongestionTrafficHeavierThanNormal: return "CongestionTrafficHeavierThanNormal"; + case EventType::CongestionTrafficLighterThanNormal: return "CongestionTrafficLighterThanNormal"; + case EventType::CongestionTrafficMuchHeavierThanNormal: return "CongestionTrafficMuchHeavierThanNormal"; + case EventType::CongestionTrafficProblem: return "CongestionTrafficProblem"; + // TODO Construction* (not in enum yet) + case EventType::DelayClearance: return "DelayClearance"; + case EventType::DelayDelay: return "DelayDelay"; + case EventType::DelayDelayPossible: return "DelayDelayPossible"; + case EventType::DelayForecastWithdrawn: return "DelayForecastWithdrawn"; + case EventType::DelayLongDelay: return "DelayLongDelay"; + case EventType::DelaySeveralHours: return "DelaySeveralHours"; + case EventType::DelayUncertainDuration: return "DelayUncertainDuration"; + case EventType::DelayVeryLongDelay: return "DelayVeryLongDelay"; + // TODO Environment*, EquipmentStatus*, Hazard*, Incident* (not in enum yet) + // TODO complete Restriction* (not in enum yet) + case EventType::RestrictionBlocked: return "RestrictionBlocked"; + case EventType::RestrictionBlockedAhead: return "RestrictionBlockedAhead"; + case EventType::RestrictionCarriagewayBlocked: return "RestrictionCarriagewayBlocked"; + case EventType::RestrictionCarriagewayClosed: return "RestrictionCarriagewayClosed"; + case EventType::RestrictionClosed: return "RestrictionClosed"; + case EventType::RestrictionClosedAhead: return "RestrictionClosedAhead"; + case EventType::RestrictionEntryBlocked: return "RestrictionEntryBlocked"; + case EventType::RestrictionEntryReopened: return "RestrictionEntryReopened"; + case EventType::RestrictionExitBlocked: return "RestrictionExitBlocked"; + case EventType::RestrictionExitReopened: return "RestrictionExitReopened"; + case EventType::RestrictionOpen: return "RestrictionOpen"; + case EventType::RestrictionRampBlocked: return "RestrictionRampBlocked"; + case EventType::RestrictionRampClosed: return "RestrictionRampClosed"; + case EventType::RestrictionRampReopened: return "RestrictionRampReopened"; + case EventType::RestrictionReopened: return "RestrictionReopened"; + case EventType::RestrictionSpeedLimit: return "RestrictionSpeedLimit"; + case EventType::RestrictionSpeedLimitLifted: return "RestrictionSpeedLimitLifted"; + // TODO Security*, Transport*, Weather* (not in enum yet) + } + UNREACHABLE(); +} + +std::string DebugPrint(ResponseStatus status) +{ + switch (status) + { + case ResponseStatus::Ok: return "Ok"; + case ResponseStatus::InvalidOperation: return "InvalidOperation"; + case ResponseStatus::SubscriptionRejected: return "SubscriptionRejected"; + case ResponseStatus::NotCovered: return "NotCovered"; + case ResponseStatus::PartiallyCovered: return "PartiallyCovered"; + case ResponseStatus::SubscriptionUnknown: return "SubscriptionUnknown"; + case ResponseStatus::PushRejected: return "PushRejected"; + case ResponseStatus::InternalError: return "InternalError"; + case ResponseStatus::Invalid: return "Invalid"; + } + UNREACHABLE(); +} + +std::string DebugPrint(TrafficImpact impact) +{ + std::ostringstream os; + os << "TrafficImpact { "; + os << "speedGroup: " << DebugPrint(impact.m_speedGroup) << ", "; + os << "maxspeed: " << (impact.m_maxspeed == kMaxspeedNone ? "none" : std::to_string(impact.m_maxspeed)) << ", "; + os << "delayMins: " << impact.m_delayMins; + os << " }"; + return os.str(); +} + +std::string DebugPrint(Point point) +{ + std::ostringstream os; + os << "Point { "; + os << "coordinates: " << DebugPrint(point.m_coordinates) << ", "; + os << "distance: " << (point.m_distance ? std::to_string(point.m_distance.value()) : "nullopt") << ", "; + os << "junctionName: " << point.m_junctionName.value_or("nullopt") << ", "; + os << "junctionRef: " << point.m_junctionRef.value_or("nullopt"); + os << " }"; + return os.str(); +} + +std::string DebugPrint(TraffLocation location) +{ + std::ostringstream os; + os << "TraffLocation { "; + os << "from: " << (location.m_from ? DebugPrint(location.m_from.value()) : "nullopt") << ", "; + os << "at: " << (location.m_at ? DebugPrint(location.m_at.value()) : "nullopt") << ", "; + os << "via: " << (location.m_via ? DebugPrint(location.m_via.value()) : "nullopt") << ", "; + os << "to: " << (location.m_to ? DebugPrint(location.m_to.value()) : "nullopt") << ", "; + os << "notVia: " << (location.m_notVia ? DebugPrint(location.m_notVia.value()) : "nullopt") << ", "; + os << "fuzziness: " << (location.m_fuzziness ? DebugPrint(location.m_fuzziness.value()) : "nullopt") << ", "; + os << "country: " << location.m_country.value_or("nullopt") << ", "; + os << "territory: " << location.m_territory.value_or("nullopt") << ", "; + os << "town: " << location.m_town.value_or("nullopt") << ", "; + os << "roadClass: " << (location.m_roadClass ? DebugPrint(location.m_roadClass.value()) : "nullopt") << ", "; + os << "roadRef: " << location.m_roadRef.value_or("nullopt") << ", "; + os << "roadName: " << location.m_roadName.value_or("nullopt") << ", "; + os << "origin: " << location.m_origin.value_or("nullopt") << ", "; + os << "destination: " << location.m_destination.value_or("nullopt") << ", "; + os << "direction: " << location.m_direction.value_or("nullopt") << ", "; + os << "directionality: " << DebugPrint(location.m_directionality) << ", "; + os << "ramps: " << DebugPrint(location.m_ramps); + os << " }"; + return os.str(); +} + +std::string DebugPrint(TraffEvent event) +{ + std::ostringstream os; + os << "TraffEvent { "; + os << "class: " << DebugPrint(event.m_class) << ", "; + os << "type: " << DebugPrint(event.m_type) << ", "; + os << "length: " << (event.m_length ? std::to_string(event.m_length.value()) : "nullopt") << ", "; + os << "probability: " << (event.m_probability ? std::to_string(event.m_probability.value()) : "nullopt") << ", "; + os << "q_duration: " + << (event.m_qDurationMins + ? (std::to_string(event.m_qDurationMins.value() / 60) + ":" + + (event.m_qDurationMins.value() % 60 < 10 ? "0" : "") + + std::to_string(event.m_qDurationMins.value() % 60)) + : "nullopt") + << ", "; + // TODO other quantifiers + os << "speed: " << (event.m_speed ? std::to_string(event.m_speed.value()) : "nullopt"); + // TODO supplementary information + os << " }"; + return os.str(); +} + +std::string DebugPrint(TraffMessage message) +{ + std::string sep; + std::ostringstream os; + os << "TraffMessage { "; + os << "id: " << message.m_id << ", "; + + os << "replaces: ["; + sep = " "; + for (auto const & replacedId : message.m_replaces) + { + os << sep << replacedId; + sep = ", "; + } + os << " ], "; + + os << "receiveTime: " << DebugPrint(message.m_receiveTime) << ", "; + os << "updateTime: " << DebugPrint(message.m_updateTime) << ", "; + os << "expirationTime: " << DebugPrint(message.m_expirationTime) << ", "; + os << "startTime: " << (message.m_startTime ? DebugPrint(message.m_startTime.value()) : "nullopt") << ", "; + os << "endTime: " << (message.m_endTime ? DebugPrint(message.m_endTime.value()) : "nullopt") << ", "; + os << "cancellation: " << message.m_cancellation << ", "; + os << "forecast: " << message.m_forecast << ", "; + // TODO std::optional m_urgency; (not in struct yet) + os << "location: " << (message.m_location ? DebugPrint(message.m_location.value()) : "nullopt") << ", "; + + os << "events: ["; + sep = " "; + for (auto const & event : message.m_events) + { + os << sep << DebugPrint(event); + sep = ", "; + } + os << " ]"; + + os << " }"; + return os.str(); +} + +std::string DebugPrint(TraffFeed feed) +{ + std::string sep; + std::ostringstream os; + os << "[ "; + sep = ""; + for (auto const & message : feed) + { + os << sep << DebugPrint(message); + sep = ", "; + } + os << " ]"; + return os.str(); +} +} // namespace traffxml diff --git a/libs/traffxml/traff_model.hpp b/libs/traffxml/traff_model.hpp new file mode 100644 index 000000000..616f8b31a --- /dev/null +++ b/libs/traffxml/traff_model.hpp @@ -0,0 +1,580 @@ +#pragma once + +//#include "traffxml/traff_foo.hpp" + +#include "geometry/latlon.hpp" + +#include "indexer/mwm_set.hpp" + +#include "traffic/speed_groups.hpp" +#include "traffic/traffic_info.hpp" + +#include +#include +#include +#include + +namespace traffxml +{ +constexpr uint8_t kMaxspeedNone = 255; + +/** + * @brief Date and time decoded from ISO 8601. + * + * `IsoTime` is an opaque type. It is only guaranteed to be capable of holding a timestamp + * converted from ISO 8601 which refers to the same UTC time as its ISO 8601 representation. + * Time zone information is not guaranteed to be preserved: `13:37+01:00` may be returned e.g. as + * `12:37Z` or `06:37-06:00`. + */ +class IsoTime +{ +public: + /** + * @brief Parses time in ISO 8601 format from a string and stores it in an `IsoTime`. + * + * ISO 8601 timestamps have the format `yyyy-mm-ddThh:mm:ss[.sss]`, optionally followed by a UTC + * offset. For example, `2019-11-01T11:45:42+01:00` refers to 11:45:42 in the UTC+1 timezone, which + * is 10:45:42 UTC. + * + * A UTC offset of `Z` denotes UTC and is equivalent to `+00:00` or `-00:00`. UTC is also assumed + * if no UTC offset is specified. + * + * The UTC offset can be specified as `hh:mm`, `hhmm` or `hh`. + * + * Seconds can be specified as integer or float, but will be rounded to the nearest integer. For + * example, 42.645 seconds will be rounded to 43 seconds. + * + * @param timeString Time in ISO8601 format + * @return An `IsoTime` instance corresponding to `timeString`, or `std::nullopt` if `timeString` is not a valid ISO8601 time string. + */ + static std::optional ParseIsoTime(std::string timeString); + + /** + * @brief Returns an `IsoTime` corresponding to current wall clock time. + * + * @return An `IsoTime` corresponding to current wall clock time. + */ + static IsoTime Now(); + + /** + * @brief Whether the instance refers to a point of time in the past. + * + * Comparison is against system time. + * + * @return true if in the past, false of not. + */ + bool IsPast(); + + /** + * @brief Shifts time to the present. + * + * This method is intended for testing. It shifts the timestamp by a fixed amount, so that + * `nowRef` corresponds to current time. After this method returns, the timestamp will have the + * same offset from current time that it had from `nowRef` at the time the call was made. + * + * @param nowRef + */ + void Shift(IsoTime nowRef); + + /** + * @brief Returns a string representation of the instance. + * @return The timestamp in ISO 8601 format. + */ + std::string ToString() const; + + bool operator< (IsoTime & rhs); + bool operator> (IsoTime & rhs); +private: + friend std::string DebugPrint(IsoTime time); + + IsoTime(std::chrono::time_point tp); + + std::chrono::time_point m_tp; +}; + +// TODO enum urgency + +enum class Directionality +{ + OneDirection, + BothDirections +}; + +enum class Fuzziness +{ + LowRes, + MediumRes, + EndUnknown, + StartUnknown, + ExtentUnknown +}; + +enum class Ramps +{ + None, + All, + Entry, + Exit +}; + +enum class RoadClass +{ + Motorway, + Trunk, + Primary, + Secondary, + Tertiary, + Other +}; + +enum class QuantifierType +{ + Dimension, + Duration, + Int, + Ints, + Speed, + Temperature, + Time, + Weight, + Invalid +}; + +/* + * When adding a new event class to this enum, be sure to do the following: + * + * * in `traff_model_xml.cpp`, add the corresponding mapping to `kEventClassMap` + * * in `traff_model.cpp`, extend `DebugPrint(EventClass)` to correctly process the new event classes + * * in this file, add event types for this class to `EventType` + */ +enum class EventClass +{ + Invalid, + Activity, + Authority, + Carpool, + Congestion, + Construction, + Delay, + Environment, + EquipmentStatus, + Hazard, + Incident, + Restriction, + Security, + Transport, + Weather +}; + +/* + * When adding a new event type to this enum, be sure to do the following: + * + * * in `traff_model_xml.cpp`, add the corresponding mapping to `kEventTypeMap` + * * in `traff_model.cpp`: + * * add speed group mappings in `kEventSpeedGroupMap`, if any + * * add maxspeed mappings in `kEventMaxspeedMap`, if any (uncomment if needed) + * * add delay mappings in `kEventDelayMap`, if any + * * extend `DebugPrint(TraffEvent)` to correctly process the new events + */ +enum class EventType +{ + Invalid, + // TODO Activity*, Authority*, Carpool* + CongestionCleared, + CongestionForecastWithdrawn, + CongestionHeavyTraffic, + CongestionLongQueue, + CongestionNone, + CongestionNormalTraffic, + CongestionQueue, + CongestionQueueLikely, + CongestionSlowTraffic, + CongestionStationaryTraffic, + CongestionStationaryTrafficLikely, + CongestionTrafficBuildingUp, + CongestionTrafficCongestion, + CongestionTrafficEasing, + CongestionTrafficFlowingFreely, + CongestionTrafficHeavierThanNormal, + CongestionTrafficLighterThanNormal, + CongestionTrafficMuchHeavierThanNormal, + CongestionTrafficProblem, + // TODO Construction* + DelayClearance, + DelayDelay, + DelayDelayPossible, + DelayForecastWithdrawn, + DelayLongDelay, + DelaySeveralHours, + DelayUncertainDuration, + DelayVeryLongDelay, + // TODO Environment*, EquipmentStatus*, Hazard*, Incident* + // TODO complete Restriction* + RestrictionBlocked, + RestrictionBlockedAhead, + RestrictionCarriagewayBlocked, + RestrictionCarriagewayClosed, + RestrictionClosed, + RestrictionClosedAhead, + RestrictionEntryBlocked, + RestrictionEntryReopened, + RestrictionExitBlocked, + RestrictionExitReopened, + RestrictionOpen, + RestrictionRampBlocked, + RestrictionRampClosed, + RestrictionRampReopened, + RestrictionReopened, + RestrictionSpeedLimit, + RestrictionSpeedLimitLifted, + // TODO Security*, Transport*, Weather* +}; + +enum class ResponseStatus +{ + /** + * The operation was successful. + */ + Ok, + + /** + * The source rejected the operation as invalid + * + * This may happen when a nonexistent operation is attempted, or an operation is attempted with + * incomplete or otherwise invalid data. + * + * @note This corresponds to TraFF status `INVALID` but was renamed here. + * `ResponseStatus::Invalid` refers to a different kind of error. + */ + InvalidOperation, + + /** + * The source rejected the subscription, e.g. because the filtered region is too large. + */ + SubscriptionRejected, + + /** + * The source does not supply data for the requested area; the request has failed. + */ + NotCovered, + + /** + * The source supplies data only for a subset of the requested area; the request was successful + * (i.e. the subscription was created or changed as requested) but the consumer should be prepared + * to receive incomplete data. + */ + PartiallyCovered, + + /** + * An operation (change, push, pull) was attempted on a subscription which the recipient did not + * recognize. On transport channels which support stable identifiers for both communication + * parties, this is also used if a consumer attempts an operation on a subscription created by + * another consumer. + */ + SubscriptionUnknown, + + /** + * The aggregator does not accept unsolicited push requests from the sensor. Reserved for future + * versions and not used as of TraFF 0.8. + */ + PushRejected, + + /** + * An internal error prevented the recipient of the request from fulfilling it. + * + * This is either translated directly from `INTERNAL_ERROR` returned from the source, or may be + * inferred from errors on the transport channel (e.g. HTTP errors). + */ + InternalError, + + /** + * An unrecognized status code. + * + * This is used for all situations where we got a response from the source, with no indication of + * an error, but could not obtain a known status code from it (e.g. XML failed to parse, did not + * contain a status code, or contained an unknown status code). + * + * @note Not to be confused with TraFF status `INVALID`, which maps to + * `ResponseStatus::InvalidOperation`. + */ + Invalid +}; + +/** + * @brief Represents the impact of one or more traffic events. + * + * Impact can be expressed in three ways: + * + * Traffic may flow at a certain percentage of the posted limit, often divided in bins. This is + * used by some traffic services which report e.g. “slow traffic”, “stationary traffic” or + * “queues”, and maps to speed groups in a straightforward way. + * + * Traffic may flow at, or be restricted to, a given speed. This is common with traffic flow + * measurement data, or with temporary speed limits. Converting this to a speed group requires + * knowledge of the regular speed limit. + * + * There may be a fixed delay, expressed as a duration in time. This may happen at checkpoints, + * at sections where traffic flow is limited or where there is single alternate-lane traffic. + * As the routing data model does not provide for explicit delays, they have to be converted into + * speed groups. Again, this requires knowledge of the regular travel time along the route, as well + * as its length. + * + * Closures can be expressed by setting `m_speedGroup` to `traffic::SpeedGroup::TempBlock`. If that + * is the case, the other struct members are to be ignored. + */ +struct TrafficImpact +{ + /** + * @brief Whether two `TrafficImpact` instances are equal. + * + * Instances are considered equal if both have a speed group of `TempBlock`, in which case other + * members are not compared. Otherwise, they are equal if, and only if, all three members hold + * identical values between both instances. + */ + // Non-member friend as member operators do not work with std::optional + friend bool operator==(TrafficImpact const & lhs, TrafficImpact const & rhs); + friend bool operator!=(TrafficImpact const & lhs, TrafficImpact const & rhs) { return !(lhs == rhs); } + + /** + * @brief The speed group for the affected segments, or `traffic::SpeedGroup::Unknown` if unknown. + */ + traffic::SpeedGroup m_speedGroup = traffic::SpeedGroup::Unknown; + + /** + * @brief The speed limit, or speed of flowing traffic; `kMaxspeedNone` if none or unknown. + */ + uint8_t m_maxspeed = kMaxspeedNone; + + /** + * @brief The delay in minutes; 0 if none or unknown. + */ + uint16_t m_delayMins = 0; +}; + +struct Point +{ + /** + * @brief Whether two points are equal. + * + * Two points are equal if, and only if, their coordinates are. Other attributes are not compared. + */ + // Non-member friend as member operators do not work with std::optional + friend bool operator==(Point const & lhs, Point const & rhs); + friend bool operator!=(Point const & lhs, Point const & rhs) { return !(lhs == rhs); } + + // TODO role? + ms::LatLon m_coordinates = ms::LatLon::Zero(); + std::optional m_distance; + std::optional m_junctionName; + std::optional m_junctionRef; +}; + +struct TraffLocation +{ + /** + * @brief Whether two locations are equal. + * + * Two locations are equal if, and only if, they contain the same points in the same roles. + * + * @todo Road class and ramps are not compared, though these values are used by the decoder. Not + * comparing these values could lead to two seemingly equal locations resolving to a different + * path. However, given that comparison only takes place between messages with identical IDs + * (indicating both refer to the same event at the same location), such a situation is highly + * unlikely to occur in practice. + */ + // Non-member friend as member operators do not work with std::optional + friend bool operator==(TraffLocation const & lhs, TraffLocation const & rhs); + friend bool operator!=(TraffLocation const & lhs, TraffLocation const & rhs) { return !(lhs == rhs); } + + std::optional m_country; + std::optional m_destination; + std::optional m_direction; + Directionality m_directionality = Directionality::BothDirections; + std::optional m_fuzziness; + std::optional m_origin; + Ramps m_ramps = Ramps::None; + std::optional m_roadClass; + // disabled for now, optional behaves weird and we don't really need it + //std::optional m_roadIsUrban; + std::optional m_roadRef; + std::optional m_roadName; + std::optional m_territory; + std::optional m_town; + std::optional m_from; + std::optional m_to; + std::optional m_at; + std::optional m_via; + std::optional m_notVia; +}; + +struct TraffEvent +{ + EventClass m_class = EventClass::Invalid; + EventType m_type = EventType::Invalid; + std::optional m_length; + std::optional m_probability; + std::optional m_qDurationMins; + /* + * TODO remaining quantifiers + * q_dimension + * q_int + * q_ints + * q_speed + * q_temperature + * q_time + * q_weight + */ + std::optional m_speed; + // TODO supplementary information +}; + +/** + * @brief Global mapping from feature segments to speed groups, across all MWMs. + */ +using MultiMwmColoring = std::map>; + +struct TraffMessage +{ + /** + * @brief Gets the time after which this message effectively expires. + * + * The effective expiration time is the latest of `m_expirationTime`, `m_startTime` and + * `m_endTime`. `nullopt` values are ignored. + * + * @return The effective expiration time for the message. + */ + IsoTime GetEffectiveExpirationTime(); + + /** + * @brief Whether the message has expired. + * + * A message is considered to have expired if its effective expiration time (as returned by + * `GetEffectiveExpirationTime()` refers to a point in time before `now`. + * + * @param now The reference time to compare to (usually current time) + * @return True if the message has expired, false if not. + */ + bool IsExpired(IsoTime now); + + /** + * @brief Retrieves the traffic impact of all events. + * + * If the message has multiple events, the traffic impact is determined separately for each + * event and then aggregated. Aggregation takes the most restrictive value in each category + * (speed group, maxspeed, delay). + * + * If the aggregated traffic impact includes `SpeedGroup::TempBlock`, its other members are to + * be considered invalid. + * + * @return The aggregated traffic impact, or `std::nullopt` if the message has no events with traffic impact. + */ + std::optional GetTrafficImpact(); + + /** + * @brief Shifts timestamps to the present. + * + * This method is intended for testing. It shifts the timestamps of the message by a fixed amount, + * so that `m_updateTime` corresponds to current time, and all other timestamps maintain their + * offset to `m_updateTime`. If `m_startTime` and/or `m_endTime` are set, they may be adjusted + * further to maintain their offset from midnight or the full hour (currently not implemented). + */ + void ShiftTimestamps(); + + std::string m_id; + IsoTime m_receiveTime = IsoTime::Now(); + IsoTime m_updateTime = IsoTime::Now(); + IsoTime m_expirationTime = IsoTime::Now(); + std::optional m_startTime = {}; + std::optional m_endTime = {}; + bool m_cancellation = false; + bool m_forecast = false; + // TODO std::optional m_urgency; + std::optional m_location; + std::vector m_events; + std::vector m_replaces; + MultiMwmColoring m_decoded; +}; + +using TraffFeed = std::vector; + +// TODO Capabilities + +/* + * Filter: currently not implemented. + * We only use bbox, for which we have a suitable data type. + * min_road_class is not needed as we do not filter by road class. + */ + +/* + * TraffSubscription: currently not implemented. + * We just store the ID as a string. + * Filters are only by bbox, not by min_road_class. The list is auto-generated from the list of + * active MWMs and changes exactly when the active MWM set changes, eliminating the need to store + * the full filter list. + */ + +/** + * @brief Encapsulates the response to a TraFF request. + */ +struct TraffResponse +{ + /** + * @brief The response status for the request which triggered the response. + */ + ResponseStatus m_status = ResponseStatus::Invalid; + + /** + * @brief The subscription ID which the source has assigned to the subscriber. + * + * This attribute is how the source communicates the subscription ID to a subscriber. Required for + * responses to a subscription request; some transport channels may require it for every + * subscription-related operation; forbidden otherwise. + */ + std::string m_subscriptionId; + + /** + * @brief The time in seconds after which the source will consider the subscription invalid if no + * activity occurs. + * + * Required for responses to a subscription request on some transport channels, optional on other + * channels, forbidden for other requests. + * + * If not used, the value is zero. + */ + uint32_t m_timeout = 0; + + /** + * @brief A feed of traffic messages sent as part of the response. + */ + std::optional m_feed; +}; + +/** + * @brief Merges the contents of one `MultiMwmColoring` into another. + * + * After this function returns, `target` will hold the union of the entries it had prior to the + * function call and the entries from `delta`. + * + * In case of conflict, the more restrictive speed group wins. That is, `TempBlock` overrides + * everything else, `Unknown` never overrides anything else, and among `G0` to `G5`, the lowest + * group wins. + * + * @param delta Contains the entries to be added. + * @param target Receives the added entries. + */ +void MergeMultiMwmColoring(const MultiMwmColoring & delta, MultiMwmColoring & target); + +std::string DebugPrint(IsoTime time); +std::string DebugPrint(Directionality directionality); +std::string DebugPrint(Ramps ramps); +std::string DebugPrint(RoadClass roadClass); +std::string DebugPrint(EventClass eventClass); +std::string DebugPrint(EventType eventType); +std::string DebugPrint(ResponseStatus status); +std::string DebugPrint(TrafficImpact impact); +std::string DebugPrint(Point point); +std::string DebugPrint(TraffLocation location); +std::string DebugPrint(TraffEvent event); +std::string DebugPrint(TraffMessage message); +std::string DebugPrint(TraffFeed feed); +} // namespace traffxml diff --git a/libs/traffxml/traff_model_xml.cpp b/libs/traffxml/traff_model_xml.cpp new file mode 100644 index 000000000..0a6643d9c --- /dev/null +++ b/libs/traffxml/traff_model_xml.cpp @@ -0,0 +1,1221 @@ +#include "traffxml/traff_model_xml.hpp" +#include "traffxml/traff_model.hpp" + +#include "base/logging.hpp" + +#include +#include +#include +#include +#include + +#include +#include + +#include + +using namespace std; + +namespace traffxml +{ +/** + * @brief Creates and initializes a `boost::bimap`. + * + * @param list A braced initializer list of left-right tuples. + * @return A new bimap instance of the tuples in `list`. + */ +template +boost::bimap +MakeBimap(std::initializer_list::value_type> list) +{ + return boost::bimap(list.begin(), list.end()); +} + +const boost::bimap kDirectionalityMap = MakeBimap({ + {"ONE_DIRECTION", Directionality::OneDirection}, + {"BOTH_DIRECTIONS", Directionality::BothDirections} +}); + +const boost::bimap kFuzzinessMap = MakeBimap({ + {"LOW_RES", Fuzziness::LowRes}, + {"MEDIUM_RES", Fuzziness::MediumRes}, + {"END_UNKNOWN", Fuzziness::EndUnknown}, + {"START_UNKNOWN", Fuzziness::StartUnknown}, + {"EXTENT_UNKNOWN", Fuzziness::ExtentUnknown} +}); + +const boost::bimap kRampsMap = MakeBimap({ + {"ALL_RAMPS", Ramps::All}, + {"ENTRY_RAMP", Ramps::Entry}, + {"EXIT_RAMP", Ramps::Exit}, + {"NONE", Ramps::None} +}); + +const boost::bimap kRoadClassMap = MakeBimap({ + {"MOTORWAY", RoadClass::Motorway}, + {"TRUNK", RoadClass::Trunk}, + {"PRIMARY", RoadClass::Primary}, + {"SECONDARY", RoadClass::Secondary}, + {"TERTIARY", RoadClass::Tertiary}, + {"OTHER", RoadClass::Other} +}); + +const boost::bimap kEventClassMap = MakeBimap({ + {"INVALID", EventClass::Invalid}, + {"ACTIVITY", EventClass::Activity}, + {"AUTHORITY", EventClass::Authority}, + {"CARPOOL", EventClass::Carpool}, + {"CONGESTION", EventClass::Congestion}, + {"CONSTRUCTION", EventClass::Construction}, + {"DELAY", EventClass::Delay}, + {"ENVIRONMENT", EventClass::Environment}, + {"EQUIPMENT_STATUS", EventClass::EquipmentStatus}, + {"HAZARD", EventClass::Hazard}, + {"INCIDENT", EventClass::Incident}, + {"RESTRICTION", EventClass::Restriction}, + {"SECURITY", EventClass::Security}, + {"TRANSPORT", EventClass::Transport}, + {"WEATHER", EventClass::Weather} +}); + +const boost::bimap kEventTypeMap = MakeBimap({ + {"INVALID", EventType::Invalid}, + // TODO Activity*, Authority*, Carpool* (not in enum yet) + {"CONGESTION_CLEARED", EventType::CongestionCleared}, + {"CONGESTION_FORECAST_WITHDRAWN", EventType::CongestionForecastWithdrawn}, + {"CONGESTION_HEAVY_TRAFFIC", EventType::CongestionHeavyTraffic}, + {"CONGESTION_LONG_QUEUE", EventType::CongestionLongQueue}, + {"CONGESTION_NONE", EventType::CongestionNone}, + {"CONGESTION_NORMAL_TRAFFIC", EventType::CongestionNormalTraffic}, + {"CONGESTION_QUEUE", EventType::CongestionQueue}, + {"CONGESTION_QUEUE_LIKELY", EventType::CongestionQueueLikely}, + {"CONGESTION_SLOW_TRAFFIC", EventType::CongestionSlowTraffic}, + {"CONGESTION_STATIONARY_TRAFFIC", EventType::CongestionStationaryTraffic}, + {"CONGESTION_STATIONARY_TRAFFIC_LIKELY", EventType::CongestionStationaryTrafficLikely}, + {"CONGESTION_TRAFFIC_BUILDING_UP", EventType::CongestionTrafficBuildingUp}, + {"CONGESTION_TRAFFIC_CONGESTION", EventType::CongestionTrafficCongestion}, + {"CONGESTION_TRAFFIC_EASING", EventType::CongestionTrafficEasing}, + {"CONGESTION_TRAFFIC_FLOWING_FREELY", EventType::CongestionTrafficFlowingFreely}, + {"CONGESTION_TRAFFIC_HEAVIER_THAN_NORMAL", EventType::CongestionTrafficHeavierThanNormal}, + {"CONGESTION_TRAFFIC_LIGHTER_THAN_NORMAL", EventType::CongestionTrafficLighterThanNormal}, + {"CONGESTION_TRAFFIC_MUCH_HEAVIER_THAN_NORMAL", EventType::CongestionTrafficMuchHeavierThanNormal}, + {"CONGESTION_TRAFFIC_PROBLEM", EventType::CongestionTrafficProblem}, + // TODO Construction* (not in enum yet) + {"DELAY_CLEARANCE", EventType::DelayClearance}, + {"DELAY_DELAY", EventType::DelayDelay}, + {"DELAY_DELAY_POSSIBLE", EventType::DelayDelayPossible}, + {"DELAY_FORECAST_WITHDRAWN", EventType::DelayForecastWithdrawn}, + {"DELAY_LONG_DELAY", EventType::DelayLongDelay}, + {"DELAY_SEVERAL_HOURS", EventType::DelaySeveralHours}, + {"DELAY_UNCERTAIN_DURATION", EventType::DelayUncertainDuration}, + {"DELAY_VERY_LONG_DELAY", EventType::DelayVeryLongDelay}, + // TODO Environment*, EquipmentStatus*, Hazard*, Incident* (not in enum yet) + // TODO complete Restriction* (not in enum yet) + {"RESTRICTION_BLOCKED", EventType::RestrictionBlocked}, + {"RESTRICTION_BLOCKED_AHEAD", EventType::RestrictionBlockedAhead}, + {"RESTRICTION_CARRIAGEWAY_BLOCKED", EventType::RestrictionCarriagewayBlocked}, + {"RESTRICTION_CARRIAGEWAY_CLOSED", EventType::RestrictionCarriagewayClosed}, + {"RESTRICTION_CLOSED", EventType::RestrictionClosed}, + {"RESTRICTION_CLOSED_AHEAD", EventType::RestrictionClosedAhead}, + {"RESTRICTION_ENTRY_BLOCKED", EventType::RestrictionEntryBlocked}, + {"RESTRICTION_ENTRY_REOPENED", EventType::RestrictionEntryReopened}, + {"RESTRICTION_EXIT_BLOCKED", EventType::RestrictionExitBlocked}, + {"RESTRICTION_EXIT_REOPENED", EventType::RestrictionExitReopened}, + {"RESTRICTION_OPEN", EventType::RestrictionOpen}, + {"RESTRICTION_RAMP_BLOCKED", EventType::RestrictionRampBlocked}, + {"RESTRICTION_RAMP_CLOSED", EventType::RestrictionRampClosed}, + {"RESTRICTION_RAMP_REOPENED", EventType::RestrictionRampReopened}, + {"RESTRICTION_REOPENED", EventType::RestrictionReopened}, + {"RESTRICTION_SPEED_LIMIT", EventType::RestrictionSpeedLimit}, + {"RESTRICTION_SPEED_LIMIT_LIFTED", EventType::RestrictionSpeedLimitLifted}, + // TODO Security*, Transport*, Weather* (not in enum yet) +}); + +const boost::bimap kSpeedGroupMap = MakeBimap({ + {"G0", traffic::SpeedGroup::G0}, + {"G1", traffic::SpeedGroup::G1}, + {"G2", traffic::SpeedGroup::G2}, + {"G3", traffic::SpeedGroup::G3}, + {"G4", traffic::SpeedGroup::G4}, + {"G5", traffic::SpeedGroup::G5}, + {"TEMP_BLOCK", traffic::SpeedGroup::TempBlock}, + {"UNKNOWN", traffic::SpeedGroup::Unknown} +}); + +/** + * @brief Retrieves an integer value from an attribute. + * + * @param attribute The XML attribute to retrieve. + * @param value The variable which will receive the value, must be of an integer type + * @return `true` on success, `false` if the attribute is not set or does not contain an integer value. + */ +template +bool IntegerFromXml(pugi::xml_attribute const & attribute, Value & value) +{ + if (attribute.empty()) + return false; + try + { + value = static_cast(is_signed::value + ? std::stoll(attribute.as_string()) + : std::stoull(attribute.as_string())); + return true; + } + catch (std::invalid_argument const& ex) + { + return false; + } +} + +/** + * @brief Retrieves an integer value from an attribute. + * + * @param attribute The XML attribute to retrieve. + * @return `true` on success, `false` if the attribute is not set or does not contain an integer value. + */ +std::optional OptionalIntegerFromXml(pugi::xml_attribute const & attribute) +{ + if (attribute.empty()) + return std::nullopt; + try + { + int result = std::stoi(attribute.as_string()); + return result; + } + catch (std::invalid_argument const& ex) + { + return std::nullopt; + } +} + +/** + * @brief Retrieves a float value from an attribute. + * + * @param attribute The XML attribute to retrieve. + * @return `true` on success, `false` if the attribute is not set or does not contain a float value. + */ +std::optional OptionalFloatFromXml(pugi::xml_attribute const & attribute) +{ + if (attribute.empty()) + return std::nullopt; + try + { + float result = std::stof(attribute.as_string()); + return result; + } + catch (std::invalid_argument const& ex) + { + return std::nullopt; + } +} + +/** + * @brief Retrieves a string from an attribute. + * + * @param attribute The XML attribute to retrieve. + * @param string Receives the string retrieved. + * @return `true` on success, `false` if the attribute is not set or set to an empty string. + */ +bool StringFromXml(pugi::xml_attribute const & attribute, std::string & string) +{ + if (attribute.empty()) + return false; + string = attribute.as_string(); + return true; +} + +/** + * @brief Retrieves a string from an XML element. + * + * @param node The XML element to retrieve. + * @param string Receives the string retrieved. + * @return `true` on success, `false` if the node does not exist. + */ +bool StringFromXml(pugi::xml_node const & node, std::string & string) +{ + if (!node) + return false; + string = node.text().as_string(); + return true; +} + +/** + * @brief Retrieves a string from an attribute. + * + * @param attribute The XML attribute to retrieve. + * @return The string, or `std::nullopt` if the attribute is not set or set to an empty string. + */ +std::optional OptionalStringFromXml(pugi::xml_attribute const & attribute) +{ + std::string result; + if (!StringFromXml(attribute, result)) + return std::nullopt; + return result; +} + +/** + * @brief Parses time in ISO 8601 format from a time attribute and stores it in an `IsoTime`. + * + * ISO 8601 timestamps have the format `yyyy-mm-ddThh:mm:ss[.sss]`, optionally followed by a UTC + * offset. For example, `2019-11-01T11:45:42+01:00` refers to 11:45:42 in the UTC+1 timezone, which + * is 10:45:42 UTC. + * + * A UTC offset of `Z` denotes UTC and is equivalent to `+00:00` or `-00:00`. UTC is also assumed + * if no UTC offset is specified. + * + * The UTC offset can be specified as `hh:mm`, `hhmm` or `hh`. + * + * Seconds can be specified as integer or float, but will be rounded to the nearest integer. For + * example, 42.645 seconds will be rounded to 43 seconds. + * + * @param attribute The XML attribute from which to receive time. + * @param tm Receives the parsed time. + * @return `true` on success, `false` if the attribute is not set or does not contain a timestamp. + */ +bool TimeFromXml(pugi::xml_attribute const & attribute, IsoTime & tm) +{ + std::string timeString; + if (!StringFromXml(attribute, timeString)) + return false; + + std::optional result = IsoTime::ParseIsoTime(timeString); + if (!result) + return false; + + tm = result.value(); + return true; +} + +/** + * @brief Parses time in ISO 8601 format from a time attribute and stores it in an `IsoTime`. + * + * ISO 8601 timestamps have the format `yyyy-mm-ddThh:mm:ss[.sss]`, optionally followed by a UTC + * offset. For example, `2019-11-01T11:45:42+01:00` refers to 11:45:42 in the UTC+1 timezone, which + * is 10:45:42 UTC. + * + * A UTC offset of `Z` denotes UTC and is equivalent to `+00:00` or `-00:00`. UTC is also assumed + * if no UTC offset is specified. + * + * The UTC offset can be specified as `hh:mm`, `hhmm` or `hh`. + * + * Seconds can be specified as integer or float, but will be rounded to the nearest integer. For + * example, 42.645 seconds will be rounded to 43 seconds. + * + * @param attribute The XML attribute from which to receive time. + * @return The parsed time, or `std::nullopt` if the attribute is not set or does not contain a timestamp. + */ +std::optional OptionalTimeFromXml(pugi::xml_attribute const & attribute) +{ + IsoTime result = IsoTime::Now(); + if (!TimeFromXml(attribute, result)) + return std::nullopt; + return result; +} + +/** + * @brief Retrieves a response status from an attribute. + * + * @param attribute The XML attribute to retrieve. + * @param status Receives the status retrieved. + * @return `true` on success, `false` if the attribute is not set or set to an empty string. + */ +bool ResponseStatusFromXml(pugi::xml_attribute const & attribute, ResponseStatus & status) +{ + std::string statusString; + if (!StringFromXml(attribute, statusString)) + return false; + + if (statusString == "OK") + status = ResponseStatus::Ok; + else if (statusString == "INVALID") + status = ResponseStatus::InvalidOperation; + else if (statusString == "SUBSCRIPTION_REJECTED") + status = ResponseStatus::SubscriptionRejected; + else if (statusString == "NOT_COVERED") + status = ResponseStatus::NotCovered; + else if (statusString == "PARTIALLY_COVERED") + status = ResponseStatus::PartiallyCovered; + else if (statusString == "SUBSCRIPTION_UNKNOWN") + status = ResponseStatus::SubscriptionUnknown; + else if (statusString == "PUSH_REJECTED") + status = ResponseStatus::PushRejected; + else if (statusString == "INTERNAL_ERROR") + status = ResponseStatus::InternalError; + else + status = ResponseStatus::Invalid; + + return true; +} + +/** + * @brief Retrieves a boolean value from an attribute. + * @param attribute The XML attribute to retrieve. + * @param defaultValue The default value to return. + * @return The value of the attribute, or `defaultValue` if the attribute is not set. + */ +bool BoolFromXml(pugi::xml_attribute const & attribute, bool defaultValue) +{ + if (attribute.empty()) + return defaultValue; + return attribute.as_bool(); +} + +/** + * @brief Retrieves a boolean value from an attribute. + * @param attribute The XML attribute to retrieve. + * @return The value of the attribute, or `std::nullopt` if the attribute is not set. + */ +std::optional OptionalBoolFromXml(pugi::xml_attribute const & attribute) +{ + if (attribute.empty()) + return std::nullopt; + return attribute.as_bool(); +} + +/** + * @brief Retrieves an enum value from an attribute. + * + * Enum values are retrieved in two steps: first, a string is retrieved, which is then decoded to + * an enum value. The enum type is determined by the type of `value` and the value type of `map`, + * both of which must match. The mapping between strings and their corresponding enum values is + * determined by the entries in `map`. + * + * @param attribute The XML attribute to retrieve. + * @param Value Receives the enum value to retrieve. + * @param map A map from strings to their respective enum values. + * @return `true` on success, `false` if the attribute is not set or its value is not found in `map`. + */ +template +bool EnumFromXml(pugi::xml_attribute const & attribute, Value & value, + boost::bimap const & map) +{ + std::string string; + if (StringFromXml(attribute, string)) + { + auto it = map.left.find(string); + if (it != map.left.end()) + { + value = it->second; + return true; + } + else + LOG(LWARNING, ("Unknown value for", attribute.name(), ":", string, "(ignoring)")); + } + return false; +} + +/** + * @brief Retrieves an enum value from an attribute. + * + * Enum values are retrieved in two steps: first, a string is retrieved, which is then decoded to + * an enum value. The enum type is determined by the value type of `map`. The mapping between + * strings and their corresponding enum values is determined by the entries in `map`. + * + * @param attribute The XML attribute to retrieve. + * @param map A map from strings to their respective enum values. + * @return The enum value, or `std::nullopt` if the attribute is not set or its value is not found in `map`. + */ +template +std::optional OptionalEnumFromXml(pugi::xml_attribute const & attribute, + boost::bimap const & map) +{ + std::string string; + if (StringFromXml(attribute, string)) + { + auto it = map.left.find(string); + if (it != map.left.end()) + return it->second; + else + LOG(LWARNING, ("Unknown value for", attribute.name(), ":", string, "(ignoring)")); + } + return std::nullopt; +} + +/** + * @brief Stores an enum value in an attribute. + * + * The enum value is translated into a string using `map`. An attribute named `name` is then added + * to `node`, with the translated value. + * + * @param value The enum value. + * @param name The name of the attribute to store the value in. + * @param node The node to which the attribute will be added. + * @param map A map between strings and their respective enum values. + */ +template +void EnumToXml(Value const & value, std::string name, pugi::xml_node & node, boost::bimap const & map) +{ + auto it = map.right.find(value); + if (it != map.right.end()) + node.append_attribute(name).set_value(it->second); + else + { + ASSERT(false, ("Enum value not found in map for", name)); + } +} + +/** + * @brief Retrieves the IDs of replaced messages from an XML element. + * @param node The XML element to retrieve (`merge`). + * @param replacedIds Receives the replaced IDs. + * @return `true` on success (including if the node contains no replaced IDs), `false` if the node does not exist or does not contain valid data. + */ +bool ReplacedMessageIdsFromXml(pugi::xml_node const & node, std::vector & replacedIds) +{ + if (!node) + return false; + + bool result = false; + auto const replacedIdNodes = node.select_nodes("./replaces"); + + if (replacedIdNodes.empty()) + return true; + + for (auto const & xpathNode : replacedIdNodes) + { + auto const replacedIdNode = xpathNode.node(); + std::string replacedId; + if (StringFromXml(replacedIdNode.attribute("id"), replacedId)) + { + replacedIds.push_back(replacedId); + result = true; + } + else + LOG(LWARNING, ("Could not parse merge element, skipping")); + } + return result; +} + +/** + * @brief Retrieves a latitude/longitude pair from an XML element. + * + * Coordinates must be given as latitude, followed by as space, then longitude. Latitude and + * longitude are given as floating-point numbers, optionally with a sign (plus is assumed if no + * sign is given). Coordinates are interpreted as degrees in WGS84 format. + * + * @param node The XML element to retrieve. + * @param latLon Receives the latitude/longitude pair. + * @return `true` on success, `false` if the node does not exist or does not contain valid coordinates. + */ +bool LatLonFromXml(pugi::xml_node const & node, ms::LatLon & latLon) +{ + if (!node) + return false; + std::string string; + if (StringFromXml(node, string)) + { + static boost::regex latLonRegex("([+-]?[0-9]*\\.?[0-9]*)\\s+([+-]?[0-9]*\\.?[0-9]*)"); + boost::smatch latLonMatcher; + if (boost::regex_search(string, latLonMatcher, latLonRegex) && latLonMatcher[1].matched && latLonMatcher[2].matched) + { + try + { + latLon.m_lat = std::stod(latLonMatcher[1]); + latLon.m_lon = std::stod(latLonMatcher[2]); + return true; + } + catch (std::invalid_argument const& ex) + { + LOG(LWARNING, ("Not a valid coordinate pair:", string)); + } + } + else + LOG(LWARNING, ("Not a valid coordinate pair:", string)); + } + return false; +} + +/** + * @brief Retrieves a Traff `Point` from an XML element. + * @param node The XML element to retrieve (any child of `location`). + * @return The point, or `std::nullopt` if the node does not exist or does not contain valid point data, + */ +std::optional OptionalPointFromXml(pugi::xml_node const & node) +{ + if (!node) + return std::nullopt; + Point result; + + if (!LatLonFromXml(node, result.m_coordinates)) + { + LOG(LWARNING, (node.name(), "has no coordinates, ignoring")); + return std::nullopt; + } + + result.m_junctionName = OptionalStringFromXml(node.attribute("junction_name")); + result.m_junctionRef = OptionalStringFromXml(node.attribute("junction_ref")); + result.m_distance = OptionalFloatFromXml(node.attribute("distance")); + + return result; +} + +/** + * @brief Adds a TraFF point to a node. + * + * @param point The TraFF point. + * @param name The name of the node to store the TraFF point in. + * @param parentNode The parent node to which the new node will be added (`location`). + */ +void PointToXml(Point const & point, std::string name, pugi::xml_node & parentNode) +{ + auto node = parentNode.append_child(name); + if (point.m_distance) + node.append_attribute("distance").set_value(point.m_distance.value()); + if (point.m_junctionName) + node.append_attribute("junction_name").set_value(point.m_junctionName.value()); + if (point.m_junctionRef) + node.append_attribute("junction_ref").set_value(point.m_junctionRef.value()); + + std::ostringstream coord_ss; + coord_ss << std::fixed << std::setprecision(5) + << std::showpos << point.m_coordinates.m_lat << " " << point.m_coordinates.m_lon; + node.text() = coord_ss.str().c_str(); +} + +/** + * @brief Retrieves a `TraffLocation` from an XML element. + * @param node The XML element to retrieve (`location`). + * @param location Receives the location. + * @return `true` on success, `false` if the node does not exist or does not contain valid location data. + */ +bool LocationFromXml(pugi::xml_node const & node, TraffLocation & location) +{ + if (!node) + return false; + + location.m_from = OptionalPointFromXml(node.child("from")); + location.m_to = OptionalPointFromXml(node.child("to")); + location.m_at = OptionalPointFromXml(node.child("at")); + location.m_via = OptionalPointFromXml(node.child("via")); + location.m_notVia = OptionalPointFromXml(node.child("not_via")); + + int numPoints = 0; + for (std::optional point : {location.m_from, location.m_to, location.m_at}) + if (point) + numPoints++; + // single-point locations are not supported, locations without points are not valid + if (numPoints < 2) + { + LOG(LWARNING, ("Only", numPoints, "points of from/to/at specified, ignoring location")); + return false; + } + + location.m_country = OptionalStringFromXml(node.attribute("country")); + location.m_destination = OptionalStringFromXml(node.attribute("destination")); + location.m_direction = OptionalStringFromXml(node.attribute("direction")); + + EnumFromXml(node.attribute("directionality"), location.m_directionality, kDirectionalityMap); + location.m_fuzziness = OptionalEnumFromXml(node.attribute("fuzziness"), kFuzzinessMap); + location.m_origin = OptionalStringFromXml(node.attribute("origin")); + EnumFromXml(node.attribute("ramps"), location.m_ramps, kRampsMap); + location.m_roadClass = OptionalEnumFromXml(node.attribute("road_class"), kRoadClassMap); + // disabled for now + //location.m_roadIsUrban = OptionalBoolFromXml(node.attribute(("road_is_urban"))); + location.m_roadRef = OptionalStringFromXml(node.attribute("road_ref")); + location.m_roadName = OptionalStringFromXml(node.attribute("road_name")); + location.m_territory = OptionalStringFromXml(node.attribute("territory")); + location.m_town = OptionalStringFromXml(node.attribute("town")); + + return true; +} + +/** + * @brief Stores a TraFF location in a node. + * + * @param location The TraFF location. + * @param node The `location` node to store the location in. + */ +void LocationToXml(TraffLocation const & location, pugi::xml_node & node) +{ + if (location.m_country) + node.append_attribute("country").set_value(location.m_country.value()); + if (location.m_destination) + node.append_attribute("destination").set_value(location.m_destination.value()); + if (location.m_direction) + node.append_attribute("direction").set_value(location.m_direction.value()); + EnumToXml(location.m_directionality, "directionality", node, kDirectionalityMap); + if (location.m_fuzziness) + EnumToXml(location.m_fuzziness.value(), "fuzziness", node, kFuzzinessMap); + if (location.m_origin) + node.append_attribute("origin").set_value(location.m_origin.value()); + EnumToXml(location.m_ramps, "ramps", node, kRampsMap); + if (location.m_roadClass) + EnumToXml(location.m_roadClass.value(), "road_class", node, kRoadClassMap); + // TODO roadIsUrban (disabled for now) + if (location.m_roadRef) + node.append_attribute("road_ref").set_value(location.m_roadRef.value()); + if (location.m_roadName) + node.append_attribute("roadName").set_value(location.m_roadName.value()); + if (location.m_territory) + node.append_attribute("territory").set_value(location.m_territory.value()); + if (location.m_town) + node.append_attribute("town").set_value(location.m_town.value()); + + if (location.m_from) + PointToXml(location.m_from.value(), "from", node); + if (location.m_at) + PointToXml(location.m_at.value(), "at", node); + if (location.m_via) + PointToXml(location.m_via.value(), "via", node); + if (location.m_notVia) + PointToXml(location.m_notVia.value(), "not_via", node); + if (location.m_to) + PointToXml(location.m_to.value(), "to", node); +} + +/** + * @brief Retrieves a `TraffQuantifier` from an XML element. + * + * The TraFF specification allows only one quantifier per event. The quantifier type depends on the + * event type, and not all events allow quantifiers. + * + * Quantifiers which violate these constraints are not filtered out, i.e. this function may return a + * quantifier for event types that do not allow quantifiers, or of a type illegal for the event type. + * If an event contains multiple quantifiers of different types, any one of these quantifiers may be + * returned, with no preference for legal quantifiers over illegal ones. + * + * @param node The node from which to retrieve the quantifier (`event`). + * @return The quantifier, or `std::nullopt` + */ +std::optional OptionalDurationFromXml(pugi::xml_attribute const & attribute) +{ + std::string durationString; + if (!StringFromXml(attribute, durationString)) + return std::nullopt; + + /* + * Valid time formats: + * 01:30 (hh:mm) + * 1 h + * 30 min + */ + static boost::regex durationRegex("(([0-9]+):([0-9]{2}))|(([0-9]+) *h)|(([0-9]+) *min)"); + boost::smatch durationMatcher; + + if (boost::regex_search(durationString, durationMatcher, durationRegex)) + { + if (!durationMatcher.str(2).empty() && !durationMatcher.str(3).empty()) + return std::stoi(durationMatcher[2]) * 60 + std::stoi(durationMatcher[3]); + else if (!durationMatcher.str(5).empty()) + return std::stoi(durationMatcher[5]) * 60; + else if (!durationMatcher.str(7).empty()) + return std::stoi(durationMatcher[7]); + UNREACHABLE(); + return std::nullopt; + } + else + { + LOG(LINFO, ("Not a valid duration:", durationString)); + return std::nullopt; + } +} + +/** + * @brief Retrieves a `TraffEvent` from an XML element. + * @param node The XML element to retrieve (`event`). + * @param event Receives the event. + * @return `true` on success, `false` if the node does not exist or does not contain valid event data. + */ +bool EventFromXml(pugi::xml_node const & node, TraffEvent & event) +{ + std::string eventClass; + if (!StringFromXml(node.attribute("class"), eventClass)) + { + LOG(LWARNING, ("No event class specified, ignoring")); + return false; + } + if (!EnumFromXml(node.attribute("class"), event.m_class, kEventClassMap)) + return false; + + std::string eventType; + if (!StringFromXml(node.attribute("type"), eventType)) + { + LOG(LWARNING, ("No event type specified, ignoring")); + return false; + } + if (!eventType.starts_with(eventClass + "_")) + { + LOG(LWARNING, ("Event type", eventType, "does not match event class", eventClass, "(ignoring)")); + return false; + } + if (!EnumFromXml(node.attribute("type"), event.m_type, kEventTypeMap)) + return false; + + event.m_length = OptionalIntegerFromXml(node.attribute("length")); + event.m_probability = OptionalIntegerFromXml(node.attribute("probability")); + + event.m_qDurationMins = OptionalDurationFromXml(node.attribute("q_duration")); + + // TODO other quantifiers (not yet implemented in struct) + + event.m_speed = OptionalIntegerFromXml(node.attribute("speed")); + + // TODO supplementary information (not yet implemented in struct) + return true; +} + +/** + * @brief Stores a TraFF event in a node. + * + * @param event The TraFF event. + * @param node The `event` node to store the event in. + */ +void EventToXml(TraffEvent const & event, pugi::xml_node & node) +{ + EnumToXml(event.m_class, "class", node, kEventClassMap); + EnumToXml(event.m_type, "type", node, kEventTypeMap); + if (event.m_length) + node.append_attribute("length").set_value(event.m_length.value()); + if (event.m_probability) + node.append_attribute("probability").set_value(event.m_probability.value()); + + if (event.m_qDurationMins) + { + auto mins = event.m_qDurationMins.value(); + auto hours = mins / 60; + auto remaining_mins = mins % 60; + std::ostringstream duration_ss; + duration_ss << std::setfill('0') << std::setw(2) << hours << ":" + << std::setw(2) << remaining_mins; + node.append_attribute("q_duration").set_value(duration_ss.str().c_str()); + } + + // TODO other quantifiers (not yet implemented in struct) + + if (event.m_speed) + node.append_attribute("speed").set_value(event.m_speed.value()); + + // TODO supplementary information (not yet implemented in struct) +} + +/** + * @brief Retrieves the TraFF events associated with a message from an XML element. + * @param node The XML element to retrieve (`events`). + * @param events Receives the events. + * @return `true` on success, `false` if the node does not exist or does not contain valid event data (including if the node contains no events). + */ +bool EventsFromXml(pugi::xml_node const & node, std::vector & events) +{ + if (!node) + return false; + + bool result = false; + auto const eventNodes = node.select_nodes("./event"); + + if (eventNodes.empty()) + return false; + + for (auto const & xpathNode : eventNodes) + { + auto const eventNode = xpathNode.node(); + TraffEvent event; + if (EventFromXml(eventNode, event)) + { + events.push_back(event); + result = true; + } + else + LOG(LWARNING, ("Could not parse event, skipping")); + } + return result; +} + +/** + * @brief Retrieves a coloring segment (segment with speed group) from XML + * @param node The `segment` node + * @param coloring The coloring to which the segment will be added. + * @return true if each segment was parsed successfully, false if errors occurred (in this case, + * the decoded coloring for this message should be discarded and regenerated from scratch) + */ +bool SegmentFromXml(pugi::xml_node const & node, + std::map & coloring) +{ + uint32_t fid; + // initialize to get rid of compiler warnings (which are false alerts) + uint16_t idx = 0; + uint8_t dir = 0; + if (IntegerFromXml(node.attribute("fid"), fid) + && IntegerFromXml(node.attribute("idx"), idx) + && IntegerFromXml(node.attribute("dir"), dir)) + { + traffic::TrafficInfo::RoadSegmentId segment(fid, idx, dir); + traffic::SpeedGroup sg = traffic::SpeedGroup::Unknown; + if (EnumFromXml(node.attribute("speed_group"), sg, kSpeedGroupMap)) + coloring[segment] = sg; + else + { + LOG(LWARNING, ("missing or invalid speed group for", segment, "(aborting)")); + return false; + } + } + else + { + LOG(LWARNING, ("segment with incomplete information (fid, idx, dir), aborting")); + return false; + } + return true; +} + +/** + * @brief Retrieves coloring for a single MWM from XML. + * + * This function returns false if errors occurred during decoding (due to invalid data), or if the + * data version to which the segments refer does not coincide with the currently used version of the + * corresponding MWM. In this case, the entire coloring for this message should be discarded and the + * message should be decoded from scratch. + * + * @todo Errors in segments are currently not considered, i.e. this function may return true even if + * one or more segments have errors. + * + * @param node The `coloring` node. + * @param dataSource The data source for coloring. + * @param decoded Receives the decoded global coloring. + * @return whether the decoded segments can be used, see description + */ +bool ColoringFromXml(pugi::xml_node const & node, DataSource const & dataSource, + MultiMwmColoring & decoded) +{ + std::string countryName; + if (!StringFromXml(node.attribute("country_name"), countryName)) + { + LOG(LWARNING, ("coloring element without coutry_name attribute, skipping")); + return false; + } + auto const & mwmId = dataSource.GetMwmIdByCountryFile(platform::CountryFile(countryName)); + if (!mwmId.IsAlive()) + { + LOG(LWARNING, ("Can’t get MWM ID for country", countryName, "(skipping)")); + return false; + } + + int64_t version = 0; + if (!IntegerFromXml(node.attribute("version"), version)) + { + LOG(LWARNING, ("Can’t get version for country", countryName, "(skipping)")); + return false; + } + else if (version != mwmId.GetInfo()->GetVersion()) + { + LOG(LINFO, ("XML data for country", countryName, "has version", version, "while MWM has", mwmId.GetInfo()->GetVersion(), "(skipping)")); + return false; + } + + auto const segmentNodes = node.select_nodes("./segment"); + + if (segmentNodes.empty()) + return true; + + std::map coloring; + + for (auto const & segmentXpathNode : segmentNodes) + { + auto const & segmentNode = segmentXpathNode.node(); + if (!SegmentFromXml(segmentNode, coloring)) + return false; + } + + if (!coloring.empty()) + decoded[mwmId] = coloring; + + return true; +} + +/** + * @brief Stores coloring for an indidual MWM in an XML node. + * + * The vaues of `mwmId` will be added to `node` as attributes. The segments and their traffic group + * will be added to `node` as child nodes. + * + * @param mwmId + * @param coloring + * @param node The `coloring` node to store the coloring in. + */ +void ColoringToXml(MwmSet::MwmId const & mwmId, + std::map const & coloring, + pugi::xml_node node) +{ + node.append_attribute("country_name").set_value(mwmId.GetInfo()->GetCountryName()); + node.append_attribute("version").set_value(mwmId.GetInfo()->GetVersion()); + for (auto & [segId, sg] : coloring) + { + auto segNode = node.append_child("segment"); + segNode.append_attribute("fid").set_value(segId.GetFid()); + segNode.append_attribute("idx").set_value(segId.GetIdx()); + segNode.append_attribute("dir").set_value(segId.GetDir()); + EnumToXml(sg, "speed_group", segNode, kSpeedGroupMap); + } +} + +/** + * @brief Retrieves global coloring from XML. + * + * If the MWM version does not match for at least one MWM, no coloring is decoded (`decoded` is + * empty after this function returns) and the message needs to be decoded from scratch. + * + * @param node The `mwm_coloring` node. + * @param dataSource The data source for coloring, see `ParseTraff()`. + * @param decoded Receives the decoded global coloring. + */ +void AllMwmColoringFromXml(pugi::xml_node const & node, + std::optional> dataSource, + MultiMwmColoring & decoded) +{ + if (!node) + return; + + if (!dataSource) + { + LOG(LWARNING, ("Message has mwm_coloring but it cannot be parsed as no data source was specified")); + return; + } + + auto const coloringNodes = node.select_nodes("./coloring"); + + if (coloringNodes.empty()) + return; + + for (auto const & coloringXpathNode : coloringNodes) + { + auto const & coloringNode = coloringXpathNode.node(); + if (!ColoringFromXml(coloringNode, dataSource->get(), decoded)) + { + decoded.clear(); + return; + } + } +} + +/** + * @brief Retrieves a TraFF message from an XML element. + * @param node The XML element to retrieve (`message`). + * @param dataSource The data source for coloring, see `ParseTraff()`. + * @param message Receives the message. + * @return `true` on success, `false` if the node does not exist or does not contain valid message data. + */ +bool MessageFromXml(pugi::xml_node const & node, + std::optional> dataSource, + TraffMessage & message) +{ + if (!StringFromXml(node.attribute("id"), message.m_id)) + { + LOG(LWARNING, ("Message has no id")); + return false; + } + + if (!TimeFromXml(node.attribute("receive_time"), message.m_receiveTime)) + { + LOG(LWARNING, ("Message", message.m_id, "has no receive_time")); + return false; + } + + if (!TimeFromXml(node.attribute("update_time"), message.m_updateTime)) + { + LOG(LWARNING, ("Message", message.m_id, "has no update_time")); + return false; + } + + if (!TimeFromXml(node.attribute("expiration_time"), message.m_expirationTime)) + { + LOG(LWARNING, ("Message", message.m_id, "has no expiration_time")); + return false; + } + + message.m_startTime = OptionalTimeFromXml(node.attribute("start_time")); + message.m_endTime = OptionalTimeFromXml(node.attribute("end_time")); + + message.m_cancellation = BoolFromXml(node.attribute("cancellation"), false); + message.m_forecast = BoolFromXml(node.attribute("forecast"), false); + + // TODO urgency (not yet implemented in struct) + + ReplacedMessageIdsFromXml(node.child("merge"), message.m_replaces); + + if (!message.m_cancellation) + { + message.m_location.emplace(); + if (LocationFromXml(node.child("location"), message.m_location.value())) + AllMwmColoringFromXml(node.child("mwm_coloring"), dataSource, message.m_decoded); + else + { + message.m_location.reset(); + LOG(LWARNING, ("Message", message.m_id, "has no location but is not a cancellation message")); + return false; + } + + if (!EventsFromXml(node.child("events"), message.m_events)) + { + LOG(LWARNING, ("Message", message.m_id, "has no events but is not a cancellation message")); + return false; + } + } + return true; +} + +/** + * @brief Stores a TraFF message in a node. + * + * @param message The TraFF message. + * @param node The `message` node to store the message in. + */ +void MessageToXml(TraffMessage const & message, pugi::xml_node node) +{ + node.append_attribute("id").set_value(message.m_id); + node.append_attribute("receive_time").set_value(message.m_receiveTime.ToString()); + node.append_attribute("update_time").set_value(message.m_updateTime.ToString()); + node.append_attribute("expiration_time").set_value(message.m_expirationTime.ToString()); + if (message.m_startTime) + node.append_attribute("start_time").set_value(message.m_startTime.value().ToString()); + if (message.m_endTime) + node.append_attribute("end_time").set_value(message.m_endTime.value().ToString()); + + node.append_attribute("cancellation").set_value(message.m_cancellation); + node.append_attribute("forecast").set_value(message.m_forecast); + + // TODO urgency (not yet implemented in struct) + + if (!message.m_replaces.empty()) + { + auto mergeNode = node.append_child("merge"); + for (auto const & id : message.m_replaces) + { + auto replacesNode = mergeNode.append_child("replaces"); + replacesNode.append_attribute("id").set_value(id); + } + } + + if (message.m_location) + { + auto locationNode = node.append_child("location"); + LocationToXml(message.m_location.value(), locationNode); + } + + if (!message.m_events.empty()) + { + auto eventsNode = node.append_child("events"); + for (auto const event : message.m_events) + { + auto eventNode = eventsNode.append_child("event"); + EventToXml(event, eventNode); + } + } + + if (!message.m_decoded.empty()) + { + auto allMwmColoringNode = node.append_child("mwm_coloring"); + for (auto & [mwmId, coloring] : message.m_decoded) + { + auto coloringNode = allMwmColoringNode.append_child("coloring"); + ColoringToXml(mwmId, coloring, coloringNode); + } + } +} + +/** + * @brief Retrieves a TraFF feed from an XML element. + * @param node The XML element to retrieve (`feed`). + * @param dataSource The data source for coloring, see `ParseTraff()`. + * @param feed Receives the feed. + * @return `true` on success, `false` if the node does not exist or does not contain valid message data. + */ +bool FeedFromXml(pugi::xml_node const & node, + std::optional> dataSource, + TraffFeed & feed) +{ + bool result = false; + + // Select all messages elements that are direct children of the node. + auto const messages = node.select_nodes("./message"); + + if (messages.empty()) + return true; + + // TODO try block? + for (auto const & xpathNode : messages) + { + auto const messageNode = xpathNode.node(); + TraffMessage message; + if (MessageFromXml(messageNode, dataSource, message)) + { + feed.push_back(message); + result = true; + } + else + LOG(LWARNING, ("Could not parse message, skipping")); + } + return result; +} + +bool ParseTraff(pugi::xml_document const & document, + std::optional> dataSource, + TraffFeed & feed) +{ + return FeedFromXml(document.document_element(), dataSource, feed); +} + +void GenerateTraff(TraffFeed const & feed, pugi::xml_document & document) +{ + auto root = document.append_child("feed"); + for (auto const & message : feed) + { + auto child = root.append_child("message"); + MessageToXml(message, child); + } +} + +void GenerateTraff(std::map const & messages, + pugi::xml_document & document) +{ + auto root = document.append_child("feed"); + for (auto const & [id, message] : messages) + { + auto child = root.append_child("message"); + MessageToXml(message, child); + } +} + +std::string FiltersToXml(std::vector & bboxRects) +{ + std::ostringstream os; + for (auto rect : bboxRects) + os << "\n"; + return os.str(); +} + +TraffResponse ParseResponse(std::string const & responseXml) +{ + TraffResponse result; + pugi::xml_document responseDocument; + if (!responseDocument.load_string(responseXml.c_str())) + return result; + + auto const responseElement = responseDocument.document_element(); + std::string responseElementName(responseElement.name()); + + if (responseElementName != "response") + return result; + + if (!ResponseStatusFromXml(responseElement.attribute("status"), result.m_status)) + return result; + + StringFromXml(responseElement.attribute("subscription_id"), result.m_subscriptionId); + + IntegerFromXml(responseElement.attribute("timeout"), result.m_timeout); + + LOG(LDEBUG, ("Response, status:", result.m_status, "subscription ID:", result.m_subscriptionId, "timeout:", result.m_timeout)); + + if (responseElement.child("feed")) + { + TraffFeed feed; + FeedFromXml(responseElement.child("feed"), std::nullopt /* dataSource */, feed); + LOG(LDEBUG, ("Feed received, number of messages:", feed.size())); + result.m_feed = std::move(feed); + } + else + LOG(LDEBUG, ("No feed in response")); + + return result; +} +} // namespace openlr diff --git a/libs/traffxml/traff_model_xml.hpp b/libs/traffxml/traff_model_xml.hpp new file mode 100644 index 000000000..530210002 --- /dev/null +++ b/libs/traffxml/traff_model_xml.hpp @@ -0,0 +1,123 @@ +#pragma once + +#include "traffxml/traff_model.hpp" + +#include "geometry/rect2d.hpp" + +#include "indexer/data_source.hpp" + +#include +#include +#include + +namespace pugi +{ +class xml_document; +class xml_node; +} // namespace pugi + +namespace traffxml +{ +/** + * @brief Retrieves a TraFF feed from an XML document. + * + * The document must conform loosely to the TraFF specification (currently version 0.8). + * + * The name of the root element is not verified, but the `message` elements must be its immediate + * children. + * + * Values which cannot be parsed correctly are skipped. + * + * Events whose event type does not match their event class are skipped. + * + * Messages, events, locations or points which lack mandatory information are skipped. + * + * If children are skipped but the parent remains valid, parsing it will report success. + * + * Parsing the feed will report failure if all its messages fail to parse, but not if it has no + * messages. + * + * In addition to the TraFF specification, we also use a custom extension, `mwm_coloring`, which is + * a child of `message` and holds decoded traffic coloring. In order to parse it, `dataSource` must + * be specified. If `dataSource` is `nullopt`, coloring will be ignored. It is recommended to pass + * `dataSource` if, and only if, parsing an XML stream that is expected to contain traffic coloring. + * This is only expected to occur in cached data. TraFF from external sources is not expected to + * contain `mwm_coloring` elements and souch information should be ignored in feeds from outside. + * + * @note To pass a reference to the framework data source (assuming the `framework` is the framework + * instance), use `std::cref(framework.GetDataSource())`. + * + * @note Custom elements and attributes which are not part of the TraFF specification, other than + * `mwm_coloring`, are ignored. + * + * @param document The XML document from which to retrieve the messages. + * @param dataSource The data source for coloring, see description. + * @param feed Receives the TraFF feed. + * @return `true` on success, `false` on failure. + */ +bool ParseTraff(pugi::xml_document const & document, + std::optional> dataSource, + TraffFeed & feed); + +/** + * @brief Generates XML from a TraFF feed. + * + * The resulting document largely conforms to the TraFF specification (currently version 0.8), but + * may contain custom elements. + * + * The root element of the generated XML document is `feed`. + * + * @note Currently no custom elements are generated. Future versions may add the location decoded + * into MWM IDs, feature IDs, directions and segments, along with their speed groups. + * + * @param feed The TraFF feed to encode. + * @param document The XML document in which to store the messages. + */ +void GenerateTraff(TraffFeed const & feed, pugi::xml_document & document); + +/** + * @brief Generates XML from a map of TraFF messages. + * + * The resulting document largely conforms to the TraFF specification (currently version 0.8), but + * may contain custom elements. + * + * The root element of the generated XML document is `feed`. + * + * @note Currently no custom elements are generated. Future versions may add the location decoded + * into MWM IDs, feature IDs, directions and segments, along with their speed groups. + * + * @param messages A map whose values contain the TraFF messages to encode. + * @param document The XML document in which to store the messages. + */ +void GenerateTraff(std::map const & messages, + pugi::xml_document & document); + +/** + * @brief Generates a list of XML `filter` elements from a vector of rects representing bboxes. + * + * The resulting string can be placed inside a TraFF XML `filter_list` element or can be passed as + * an extra to an Android intent. + * + * It will have one `filter` element for each element in `bboxRects`, although simplification may + * be applied to reduce the number of rects (currently not implemented). + * + * The `min_road_class` attribute is not used. + * + * @param bboxRects The rectangles, each of which represents a bounding box. + * @return A string of XML `filter` elements. + */ +std::string FiltersToXml(std::vector & bboxRects); + +/** + * @brief Parses the response to a TraFF request. + * + * The response must comply with TraFF 0.8. The root element must be `response`. + * + * If a parsing error occurs, the response returned will have its `m_status` member set to + * `ResponseStatus::Invalid`. + * + * @param responseXml The response, as a string in XML format. + * @return The parsed response. + */ +TraffResponse ParseResponse(std::string const & responseXml); +} // namespace traffxml diff --git a/libs/traffxml/traff_source.cpp b/libs/traffxml/traff_source.cpp new file mode 100644 index 000000000..2724d8136 --- /dev/null +++ b/libs/traffxml/traff_source.cpp @@ -0,0 +1,328 @@ +#include "traffxml/traff_source.hpp" + +#include "traffxml/traff_model_xml.hpp" +#include "traffxml/traff_storage.hpp" + +#include "geometry/rect2d.hpp" + +#include "platform/platform.hpp" + +#include +#include +#include + +namespace traffxml { +TraffSource::TraffSource(TraffSourceManager & manager) + : m_manager(manager) +{} + +void TraffSource::SubscribeOrChangeSubscription(std::set & mwms) +{ + std::lock_guard lock(m_mutex); + if (!IsSubscribed()) + Subscribe(mwms); + else + ChangeSubscription(mwms); +} + +std::string TraffSource::GetMwmFilters(std::set & mwms) +{ + std::vector rects; + for (auto mwmId : mwms) + rects.push_back(mwmId.GetInfo()->m_bordersRect); + return traffxml::FiltersToXml(rects); +} + +void MockTraffSource::Create(TraffSourceManager & manager) +{ + std::unique_ptr source = std::unique_ptr(new MockTraffSource(manager)); + manager.RegisterSource(std::move(source)); +} + +MockTraffSource::MockTraffSource(TraffSourceManager & manager) + : TraffSource(manager) +{} + +void MockTraffSource::Subscribe(std::set & mwms) +{ + std::string filterList = GetMwmFilters(mwms); + LOG(LINFO, ("Would subscribe to:\n", filterList)); + m_subscriptionId = "placeholder_subscription_id"; + m_nextRequestTime = std::chrono::steady_clock::now(); // would be in the future if we got a feed here +} + +void MockTraffSource::ChangeSubscription(std::set & mwms) +{ + if (!IsSubscribed()) + return; + std::string filterList = GetMwmFilters(mwms); + LOG(LINFO, ("Would change subscription", m_subscriptionId, "to:\n", filterList)); + m_nextRequestTime = std::chrono::steady_clock::now(); // would be in the future if we got a feed here +} + +void MockTraffSource::Unsubscribe() +{ + std::lock_guard lock(m_mutex); + if (!IsSubscribed()) + return; + LOG(LINFO, ("Would unsubscribe from", m_subscriptionId)); + m_subscriptionId.clear(); +} + +bool MockTraffSource::IsPollNeeded() +{ + return m_nextRequestTime.load() <= std::chrono::steady_clock::now(); +} + +void MockTraffSource::Poll() +{ + //std::string fileName("test_data/traff/PL-A18-Krzyzowa-Lipiany.xml"); + std::string fileName("test_data/traff/PL-A18-Krzyzowa-Lipiany-bidir.xml"); + //std::string fileName("test_data/traff/LT-A1-Vezaiciai-Endriejavas.xml"); + traffxml::LocalStorage storage(fileName); + pugi::xml_document document; + auto const load_result = storage.Load(document); + if (!load_result) + return; + + m_lastRequestTime = std::chrono::steady_clock::now(); + std::setlocale(LC_ALL, "en_US.UTF-8"); + traffxml::TraffFeed feed; + if (traffxml::ParseTraff(document, std::nullopt /* dataSource */, feed)) + { + m_lastResponseTime = std::chrono::steady_clock::now(); + m_nextRequestTime = std::chrono::steady_clock::now() + m_updateInterval; + m_lastAvailability = Availability::IsAvailable; + m_manager.ReceiveFeed(feed); + } + else + { + LOG(LWARNING, ("An error occurred parsing the TraFF feed")); + m_lastAvailability = Availability::Error; + /* + * TODO how should we deal with future requests? + * Static files usually don’t change. + */ + m_nextRequestTime = std::chrono::steady_clock::now() + m_updateInterval; + } +} + +TraffResponse HttpPost(std::string const & url, std::string data) +{ + platform::HttpClient request(url); + request.SetBodyData(data, "application/xml"); + + if (!request.RunHttpRequest() || request.ErrorCode() != 200) + { + TraffResponse result; + result.m_status = ResponseStatus::InternalError; + return result; + } + + LOG(LDEBUG, ("Got response, status", request.ErrorCode())); + + TraffResponse result = ParseResponse(request.ServerResponse()); + return result; +} + +void HttpTraffSource::Create(TraffSourceManager & manager, std::string const & url) +{ + std::unique_ptr source = std::unique_ptr(new HttpTraffSource(manager, url)); + manager.RegisterSource(std::move(source)); +} + +HttpTraffSource::HttpTraffSource(TraffSourceManager & manager, std::string const & url) + : TraffSource(manager) + , m_url(url) +{} + +void HttpTraffSource::Close() +{ + std::string data; + { + std::lock_guard lock(m_mutex); + + if (m_subscriptionId.empty()) + return; + data = ""; + m_subscriptionId.clear(); + } + + LOG(LDEBUG, ("Sending request:\n", data)); + + threads::SimpleThread thread([this, data]() { + TraffResponse response = HttpPost(m_url, data); + return; + }); + thread.detach(); +} + +void HttpTraffSource::Subscribe(std::set & mwms) +{ + std::string data = "\n\n" + + GetMwmFilters(mwms) + + "\n" + + ""; + LOG(LDEBUG, ("Sending request:\n", data)); + + threads::SimpleThread thread([this, data]() { + // TODO sometimes the request gets sent (and processed) twice + TraffResponse response = HttpPost(m_url, data); + OnSubscribeResponse(response); + return; + }); + thread.detach(); +} + +void HttpTraffSource::OnFeedReceived(TraffFeed & feed) +{ + m_lastResponseTime = std::chrono::steady_clock::now(); + m_nextRequestTime = std::chrono::steady_clock::now() + m_updateInterval; + m_lastAvailability = Availability::IsAvailable; + m_manager.ReceiveFeed(feed); +} + +void HttpTraffSource::OnSubscribeResponse(TraffResponse & response) +{ + if (response.m_status == ResponseStatus::Ok + || response.m_status == ResponseStatus::PartiallyCovered) + { + if (response.m_subscriptionId.empty()) + LOG(LWARNING, ("Server replied with", response.m_status, "but subscription ID is empty; ignoring")); + else + { + { + std::lock_guard lock(m_mutex); + m_subscriptionId = response.m_subscriptionId; + // TODO timeout + } + if (response.m_feed && !response.m_feed.value().empty()) + OnFeedReceived(response.m_feed.value()); + else + Poll(); + } + } + else + LOG(LWARNING, ("Subscribe request failed:", response.m_status)); +} + +void HttpTraffSource::ChangeSubscription(std::set & mwms) +{ + std::string data = "\n" + + "\n" + + GetMwmFilters(mwms) + + "\n" + + ""; + LOG(LDEBUG, ("Sending request:\n", data)); + + threads::SimpleThread thread([this, data]() { + TraffResponse response = HttpPost(m_url, data); + OnChangeSubscriptionResponse(response); + return; + }); + thread.detach(); +} + +void HttpTraffSource::OnChangeSubscriptionResponse(TraffResponse & response) +{ + if (response.m_status == ResponseStatus::Ok + || response.m_status == ResponseStatus::PartiallyCovered) + { + if (response.m_feed && !response.m_feed.value().empty()) + OnFeedReceived(response.m_feed.value()); + else + Poll(); + } + else if (response.m_status == ResponseStatus::SubscriptionUnknown) + { + LOG(LWARNING, ("Change Subscription returned", response.m_status, " – removing subscription", m_subscriptionId)); + { + std::lock_guard lock(m_mutex); + m_subscriptionId.clear(); + } + } + else + LOG(LWARNING, ("Change Subscription request failed:", response.m_status)); +} + +void HttpTraffSource::Unsubscribe() +{ + std::string data; + { + std::lock_guard lock(m_mutex); + + if (m_subscriptionId.empty()) + return; + data = ""; + } + + LOG(LDEBUG, ("Sending request:\n", data)); + + threads::SimpleThread thread([this, data]() { + TraffResponse response = HttpPost(m_url, data); + OnUnsubscribeResponse(response); + return; + }); + thread.detach(); +} + +void HttpTraffSource::OnUnsubscribeResponse(TraffResponse & response) +{ + if (response.m_status != ResponseStatus::Ok + && response.m_status != ResponseStatus::SubscriptionUnknown) + { + LOG(LWARNING, ("Unsubscribe returned", response.m_status, " – removing subscription")); + } + { + std::lock_guard lock(m_mutex); + m_subscriptionId.clear(); + } +} + +bool HttpTraffSource::IsPollNeeded() +{ + // TODO revisit logic + return m_nextRequestTime.load() <= std::chrono::steady_clock::now(); +} + +void HttpTraffSource::Poll() +{ + std::string data; + { + std::lock_guard lock(m_mutex); + + if (m_subscriptionId.empty()) + return; + data = ""; + } + + LOG(LDEBUG, ("Sending request:\n", data)); + + threads::SimpleThread thread([this, data]() { + // TODO sometimes the request gets sent (and processed) twice + TraffResponse response = HttpPost(m_url, data); + OnPollResponse(response); + return; + }); + thread.detach(); +} + +void HttpTraffSource::OnPollResponse(TraffResponse & response) +{ + if (response.m_status == ResponseStatus::Ok) + { + if (response.m_feed && !response.m_feed.value().empty()) + OnFeedReceived(response.m_feed.value()); + } + else if (response.m_status == ResponseStatus::SubscriptionUnknown) + { + LOG(LWARNING, ("Poll returned", response.m_status, " – removing subscription", m_subscriptionId)); + { + std::lock_guard lock(m_mutex); + m_subscriptionId.clear(); + } + } + else + LOG(LWARNING, ("Poll returned", response.m_status)); +} +} // namespace traffxml diff --git a/libs/traffxml/traff_source.hpp b/libs/traffxml/traff_source.hpp new file mode 100644 index 000000000..b93d15a86 --- /dev/null +++ b/libs/traffxml/traff_source.hpp @@ -0,0 +1,546 @@ +#pragma once + +#include "traffxml/traff_model.hpp" + +#include "base/thread.hpp" + +#include "indexer/mwm_set.hpp" + +#include "platform/http_client.hpp" + +#include +#include +#include + +namespace traffxml +{ +class TraffSource; + +/** + * @brief Abstract class which manages TraFF sources. + * + * `TraffSource` and its subclasses register with `TraffSourceManager` upon creation. The + * `TraffSourceManager` calls `TraffSource` methods to manage its subscription and poll for + * messages, and exposes a method to deliver message feeds. + */ +class TraffSourceManager +{ +public: + virtual ~TraffSourceManager() {} + + /** + * @brief Retrieves all currently active MWMs. + * + * This method retrieves all MWMs for which traffic data is needed (viewport, current position + * and route) and stores them in `activeMwms`. + * + * Implementations must ensure thread safety, so that this method can be called from any thread. + * + * @param activeMwms Retrieves the list of active MWMs. + */ + virtual void GetActiveMwms(std::set & activeMwms) = 0; + + /** + * @brief Processes a traffic feed. + * + * The feed may be a result of a pull operation, or received through a push operation. + * (Push operations are not supported by all sources.) + * + * This method is safe to call from any thread. + * + * @param feed The traffic feed. + */ + virtual void ReceiveFeed(traffxml::TraffFeed feed) = 0; + + /** + * @brief Registers a `TraffSource`. + * @param source The source. + */ + virtual void RegisterSource(std::unique_ptr source) = 0; +}; + +/** + * @brief Abstract base class for TraFF sources. + * + * Subclasses encapsulate various forms of TraFF sources. The base class provides methods for + * subscription management, message retrieval and service status. + * + * Any `TraffSource` method may call `TrafficManager` methods exposed through the + * `TraffSourceManager` interface. The traffic manager must therefore ensure there is no conflict + * between thread-synchronization mechanisms held when calling a `TraffSource` method and those + * which may get requested when that method calls a `TraffSourceManager` method. + * + * Each subclass should implement a non-public constructor (private if the subclass is final, + * protected otherwise) and a public factory method. The factory method takes the same arguments + * as the constructor, creates an instance wrapped in a `std::unique_ptr` and registers it with + * the `TraffSourceManager`. It can be implemented as follows: + * ``` + * void SomeTraffSource::Create(TraffSourceManager & manager, SomeOtherArg & otherArg) + * { + * std::unique_ptr source = std::unique_ptr(new SomeTraffSource(manager, otherArg)); + * manager.RegisterSource(std::move(source)); + * } + * ``` + * + * Each subclass must provide implementations for `Subscribe()`, `ChangeSubscription()`, + * `Unsubscribe()`, `IsPollNeeded()` and `Poll()`. + * + * Most of these methods can be called from any thread, including the UI thread (see documentation + * of individual methods for details). This has two implications: + * + * Subclasses must ensure thread safety for methods they implement, in particular regarding access + * to shared members. This can be done by locking `m_mutex`. + * + * Also, methods should not block or perform lengthy operations. Network operations must be + * delegated to a separate thread (attempting a network operation on the UI thread will cause the + * application to be killed on Android). + * + * This class provides various protected members which subclasses can build upon. These include a + * reference to the `TraffSourceManager`, a mutex for thread-safe access, a subscription ID, + * timestamps for the last request and response, as well as for the next request, a retry count + * for failed operations, and an indication of a pending request. + */ +class TraffSource +{ +public: + /** + * @brief Whether traffic data is available. + * + * The default value upon creating a new instance should be `Unknown`. After that, the value + * should be changed based on the result of the last TraFF operation, as detailed below: + * + * `OK` changes the status from `Unknown`, or any error which would be resolved by the last + * operation, to `IsAvailable`. + * + * `INVALID` indicates a condition which should be treated as a bug, either in the source or its + * backend. It should generate a log entry, and changes the status to `Error`. + * + * `SUBSCRIPTION_REJECTED` changes the status to `SubscriptionRejected`. + * + * `NOT_COVERED` changes the status to `NotCovered`. + * + * `PARTIALLY_COVERED` has the same effect as `OK`. + * + * `SUBSCRIPTION_UNKNOWN` should be handled by clearing the subscription ID and resubscribing, + * then setting the status based on the result of the new subscription. + * + * `INTERNAL_ERROR` changes the status to `Error`. + * + * If the source does not seem to be connected to a valid backend (e.g. if a HTTP source responds + * with an HTTP error), the status should be changed to `Error`. + * + * If a TraFF `GET_CAPABILITIES` request returns a minimum version higher than supported by this + * application, the status should be changed to `ExpiredApp` and no further requests to the source + * should be attempted. + * + * @todo Should `PARTIALLY_COVERED`, or `GET_CAPABILITIES` reporting a target version higher than + * supported, be stored in the class instance? + */ + enum class Availability + { + /** + * The source is working normally. + * This status is reached after the first request was made, if it is successful. + */ + IsAvailable, + /** + * The source, or its backend, rejected the subscription. + * This may happen for various reasons, possibly because the requested area was too large. + * An existing subscription ID (if any) remains valid, but no poll operations should be + * attempted until the subscription is changed successfully. + */ + SubscriptionRejected, + /** + * The requested area is not covered by the source. + * An existing subscription ID (if any) remains valid, but poll operations will not return any + * messages until the subscription is changed successfully. + */ + NotCovered, + /** + * The source has reported an internal error, has reported an invalid request or returned + * invalid data. + * The failed operation should be retried at a resonably chosen interval. After the source + * resumes normal operation, previously issued subscription IDs may no longer be valid (in which + * case the caller should attempt to resubscribe) and/or messages may be repeated. + */ + Error, + /** The app does not support the minimum TraFF version required by the source. */ + ExpiredApp, + /** No request was made yet. */ + Unknown + }; + + virtual ~TraffSource() {} + + /** + * @brief Ensures we have a subscription covering the MWMs indicated. + * + * This method subscribes to a traffic service if not already subscribed, or changes the existing + * subscription otherwise. + * + * The default implementation acquires the mutex before running the following code: + * + * ``` + * if (!IsSubscribed()) + * Subscribe(mwms); + * else + * ChangeSubscription(mwms); + * ``` + * + * Therefore, `IsSubscribed()`, `Subscribe()` and `ChangeSubscription()` need not (and should not) + * acquire the mutex on their own. + * + * @param mwms The new set of MWMs for which data is needed. + */ + virtual void SubscribeOrChangeSubscription(std::set & mwms); + + /** + * @brief Whether this source should be polled. + * + * Prior to calling `Poll()` on a source, the caller should always first call `IsPollNeeded()` and + * poll the source only if the result is true. + * + * It is up to the source to decide when to return true or false. Typically a source would return + * false if another request is still pending, a predefined poll interval has not yet elapsed since + * the previous successful response, during the retry interval following an error, or if an error + * is not recoverable (such as `ExpiredApp`). In all other case it would return true. + * + * This method is only called from the `TrafficManager` worker thread. + * + * @return true if the source should be polled, false if not. + */ + virtual bool IsPollNeeded() = 0; + + /** + * @brief Polls the traffic service for updates. + * + * For sources which reliably push data, this implementation may do nothing. + * + * It is up to the caller to call `IsPollNeeded()` prior to calling this function, and use its + * result to decide whether or not to poll, or to force a poll operation. + * + * Sources should handle cases in which the backend responds with `SUBSCRIPTION_UNKNOWN`, usually + * by deleting the subscription ID and resubscribing to the set of active MWMs. The set of active + * MWMs can be retrieved by calling `m_manager.GetActiveMwms()`. + * + * This method is only called from the `TrafficManager` worker thread. + */ + virtual void Poll() = 0; + + /** + * @brief Unsubscribes from a traffic service we are subscribed to. + * + * Unsubscribing without being subscribed is a no-op. + */ + virtual void Unsubscribe() = 0; + +protected: + /** + * @brief Constructs a new `TraffSource`. + * @param manager The `TrafficSourceManager` instance to register the source with. + */ + TraffSource(TraffSourceManager & manager); + + /** + * @brief Returns a TraFF filter list for a set of MWMs. + * + * @param mwms The MWMs for which a filter list is to be created. + * @return A `filter_list` in XML format. + */ + static std::string GetMwmFilters(std::set & mwms); + + /** + * @brief Subscribes to a traffic service. + * + * If the default implementation of `SubscribeOrChangeSubscription()` is used, `m_mutex` is + * acquired before this method is called, and implementations do not need to (and should not) + * acquire it again. Any other calls to this method must be protected by acquiring `m_mutex`. + * + * @param mwms The MWMs for which data is needed. + */ + virtual void Subscribe(std::set & mwms) = 0; + + /** + * @brief Changes an existing traffic subscription. + * + * If the default implementation of `SubscribeOrChangeSubscription()` is used, `m_mutex` is + * acquired before this method is called, and implementations do not need to (and should not) + * acquire it again. Any other calls to this method must be protected by acquiring `m_mutex`. + * + * Sources should handle cases in which the backend responds with `SUBSCRIPTION_UNKNOWN`, usually + * by deleting the subscription ID and resubscribing to `mwms`. Asynchronous implementations, in + * which `mwms` may no longer be available when the operation completes, can retrieve the set of + * active MWMs can be retrieved by calling `m_manager.GetActiveMwms()`. + * + * @param mwms The new set of MWMs for which data is needed. + */ + virtual void ChangeSubscription(std::set & mwms) = 0; + + /** + * @brief Whether we are currently subscribed to a traffic service. + * + * If the default implementation of `SubscribeOrChangeSubscription()` is used, `m_mutex` is + * acquired before this method is called, and implementations do not need to (and should not) + * acquire it again. Any other calls to this method must be protected by acquiring `m_mutex`. + * + * @return true if subscribed, false if not. + */ + virtual bool IsSubscribed() { return !m_subscriptionId.empty(); } + + TraffSourceManager & m_manager; + + /** + * @brief Mutex for access to shared members. + * + * Any access to members shared between threads must be protected by obtaining this mutex first. + */ + std::mutex m_mutex; + + /** + * @brief The subscription ID received from the backend. + * + * An empty subscription ID means no subscription. + */ + std::string m_subscriptionId; + + /** + * @brief When the last update request occurred. + * + * This timestamp is the basis for determining whether an update is needed. + * + * It is initially in the past. Subclasses that use it should update it whenever a request is made. + */ + std::atomic> m_lastRequestTime; + + /** + * @brief When the last response was received. + * + * This timestamp is the basis for determining whether a network request timed out, or if data is + * outdated. + * + * It is initially in the past. Subclasses that use it should update it whenever a response to a + * request is received. + */ + std::atomic> m_lastResponseTime; + + /** + * @brief When the next request should be made. + * + * This timestamp is initiated to current time and updated when a request is made, or a response + * is received. + * + * It is initially in the present. Subclasses that use it should update it on every request or + * response, setting it a defined timespan into the future. + */ + std::atomic> m_nextRequestTime = std::chrono::steady_clock::now(); + + /** + * @brief The number of failed traffic requests for this source. + * + * Reset when a request is successful. + */ + std::atomic m_retriesCount = 0; + + /** + * @brief Whether a request is currently pending for this source. + * + * Set to `true` when a request is scheduled, reverted to `false` when a response is received or + * the request fails. + */ + std::atomic m_isWaitingForResponse = false; + + /** + * @brief The last reported availability of the traffic source. + * + * See the documentation of `Availability` for possible values and their meanings. + * + * Availability is `Unknown` until a result for the first request (positive or negative) has been + * received. Subclasses must update this value, ensuring it always correctly reflects the status + * of the source. + */ + std::atomic m_lastAvailability = Availability::Unknown; + +private: + DISALLOW_COPY(TraffSource); +}; + +/** + * @brief A mock TraFF source. + * + * This source will accept any and all subscription requests and return a static subscription ID. + * Polling will return a static set of messages. + */ +class MockTraffSource : public TraffSource +{ +public: + /** + * @brief Creates a new `MockTraffSource` instance and registers it with the traffic manager. + * + * @param manager The traffic manager to register the new instance with + */ + static void Create(TraffSourceManager & manager); + + /** + * @brief Subscribes to a traffic service. + * + * @param mwms The MWMs for which data is needed. + */ + virtual void Subscribe(std::set & mwms) override; + + /** + * @brief Changes an existing traffic subscription. + * + * @param mwms The new set of MWMs for which data is needed. + */ + virtual void ChangeSubscription(std::set & mwms) override; + + /** + * @brief Unsubscribes from a traffic service we are subscribed to. + */ + virtual void Unsubscribe() override; + + /** + * @brief Whether this source should be polled. + * + * Prior to calling `Poll()` on a source, the caller should always first call `IsPollNeeded()` and + * poll the source only if the result is true. + * + * This implementation uses `m_nextRequestTime` to determine when the next poll is due. When a + * feed is received, `m_nextRequestTime` is set to a point in time 5 minutes in the future. As + * long as `m_nextRequestTime` is in the future, this method returns false. + * + * @return true if the source should be polled, false if not. + */ + virtual bool IsPollNeeded() override; + + /** + * @brief Polls the traffic service for updates. + */ + virtual void Poll() override; + +protected: + /** + * @brief Constructs a new `MockTraffSource`. + * @param manager The `TrafficSourceManager` instance to register the source with. + */ + MockTraffSource(TraffSourceManager & manager); + +private: + /** + * @brief The update interval, 5 minutes. + */ + static auto constexpr m_updateInterval = std::chrono::minutes(5); +}; + +/** + * @brief A TraFF source backed by a HTTP[S] server. + */ +class HttpTraffSource : public TraffSource +{ +public: + /** + * @brief Creates a new `HttpTraffSource` instance and registers it with the traffic manager. + * + * @param manager The traffic manager to register the new instance with + * @param url The URL for the TraFF service API. + */ + static void Create(TraffSourceManager & manager, std::string const & url); + + /** + * @brief Prepares the HTTP traffic source for unloading. + * + * If there is still an active subscription, it unsubscribes, but without processing the result + * received from the service. Otherwise, teardown is a no-op. + */ + // TODO move this to the parent class and override it here? + void Close(); + + /** + * @brief Subscribes to a traffic service. + * + * @param mwms The MWMs for which data is needed. + */ + virtual void Subscribe(std::set & mwms) override; + + /** + * @brief Changes an existing traffic subscription. + * + * @param mwms The new set of MWMs for which data is needed. + */ + virtual void ChangeSubscription(std::set & mwms) override; + + /** + * @brief Unsubscribes from a traffic service we are subscribed to. + */ + virtual void Unsubscribe() override; + + /** + * @brief Whether this source should be polled. + * + * Prior to calling `Poll()` on a source, the caller should always first call `IsPollNeeded()` and + * poll the source only if the result is true. + * + * @todo Document how the result is calculated. For example: + * This implementation uses `m_nextRequestTime` to determine when the next poll is due. When a + * feed is received, `m_nextRequestTime` is set to a point in time 5 minutes in the future. As + * long as `m_nextRequestTime` is in the future, this method returns false. + * + * @return true if the source should be polled, false if not. + */ + virtual bool IsPollNeeded() override; + + /** + * @brief Polls the traffic service for updates. + */ + virtual void Poll() override; + +protected: + /** + * @brief Constructs a new `HttpTraffSource`. + * @param manager The `TrafficSourceManager` instance to register the source with. + * @param url The URL for the TraFF service API. + */ + HttpTraffSource(TraffSourceManager & manager, std::string const & url); + +private: + /** + * @brief Processes a TraFF feed. + * @param feed The feed. + */ + void OnFeedReceived(TraffFeed & feed); + + /** + * @brief Processes the response to a subscribe request. + * @param response The response to the subscribe operation. + */ + void OnSubscribeResponse(TraffResponse & response); + + /** + * @brief Processes the response to a change subscription request. + * @param response The response to the change subscription operation. + */ + void OnChangeSubscriptionResponse(TraffResponse & response); + + /** + * @brief Processes the response to an unsubscribe request. + * @param response The response to the unsubscribe operation. + */ + void OnUnsubscribeResponse(TraffResponse & response); + + /** + * @brief Processes the response to a poll request. + * @param response The response to the poll operation. + */ + void OnPollResponse(TraffResponse & response); + + /** + * @brief The update interval, 5 minutes. + */ + static auto constexpr m_updateInterval = std::chrono::minutes(5); + + /** + * @brief The URL for the TraFF service. + */ + const std::string m_url; +}; +} // namespace traffxml diff --git a/libs/traffxml/traff_storage.cpp b/libs/traffxml/traff_storage.cpp new file mode 100644 index 000000000..50fbb1038 --- /dev/null +++ b/libs/traffxml/traff_storage.cpp @@ -0,0 +1,76 @@ +#include "traffxml/traff_storage.hpp" + +#include "platform/platform.hpp" + +#include "coding/internal/file_data.hpp" + +#include "base/logging.hpp" + +#include + +namespace +{ +std::string GetFilePath(std::string const & fileName) { return GetPlatform().WritablePathForFile(fileName); } +} // namespace + +namespace traffxml +{ +// StorageLocal ------------------------------------------------------------------------------------ +bool LocalStorage::Save(pugi::xml_document const & doc) +{ + auto const filePath = GetFilePath(m_fileName); + + std::lock_guard guard(m_mutex); + + return base::WriteToTempAndRenameToFile(filePath, [&doc](std::string const & fileName) { + return doc.save_file(fileName.data(), " " /* indent */); + }); +} + +bool LocalStorage::Load(pugi::xml_document & doc) +{ + auto const filePath = GetFilePath(m_fileName); + + std::lock_guard guard(m_mutex); + + auto const result = doc.load_file(filePath.c_str()); + /* + * Note: status_file_not_found is ok for our use cases: + * - editor: if a user has never made any edits. + * - traffic: if no traffic information has ever been retrieved (first run) + */ + if (result != pugi::status_ok && result != pugi::status_file_not_found) + { + LOG(LERROR, ("Can't load file from disk:", filePath)); + return false; + } + + return true; +} + +bool LocalStorage::Reset() +{ + std::lock_guard guard(m_mutex); + + return base::DeleteFileX(GetFilePath(m_fileName)); +} + +// StorageMemory ----------------------------------------------------------------------------------- +bool InMemoryStorage::Save(pugi::xml_document const & doc) +{ + m_doc.reset(doc); + return true; +} + +bool InMemoryStorage::Load(pugi::xml_document & doc) +{ + doc.reset(m_doc); + return true; +} + +bool InMemoryStorage::Reset() +{ + m_doc.reset(); + return true; +} +} // namespace traffxml diff --git a/libs/traffxml/traff_storage.hpp b/libs/traffxml/traff_storage.hpp new file mode 100644 index 000000000..f59bfb071 --- /dev/null +++ b/libs/traffxml/traff_storage.hpp @@ -0,0 +1,77 @@ +#pragma once + +#include + +#include + +/* + * TODO combine this header and its CPP file with editor/editor_storage.hpp and its CPP counterpart, + * give it an appropriate namespace and place it where both editor and traffic can use it. + * `traff_storage` is essentially copied from `editor_storage` with just a few modifications: + * - namespace + * - instead of relying on a hardcoded file name, `LocalStorage` is initialized with a file name + * - log output, variable names and comments changed to be use case neutral (no API changes) + * - refactoring: no global `using namespace` (no API changes) + * Apart from the first two points, this file can serve as a drop-in replacement for `editor_storage`. + * Traffic uses only `LocalStorage`, the rest has been kept to ease migration to a unified storage + * component. + */ + +namespace traffxml +{ +/** + * @brief Storage interface for XML data. + */ +class StorageBase +{ +public: + virtual ~StorageBase() = default; + + virtual bool Save(pugi::xml_document const & doc) = 0; + virtual bool Load(pugi::xml_document & doc) = 0; + virtual bool Reset() = 0; +}; + +/** + * @brief Class which saves/loads XML data to/from local file. + * @note this class IS thread-safe. + */ +class LocalStorage : public StorageBase +{ +public: + /** + * @brief Constructs a `LocalStorage` instance. + * @param fileName The file name and path where the file in question will be persisted. It is + * interpreted relative to the platform-specific path; absolute paths are not supported as some + * platforms restrict applications’ access to files outside their designated path. + */ + LocalStorage(std::string const & fileName) + : m_fileName(fileName) + {} + + // StorageBase overrides: + bool Save(pugi::xml_document const & doc) override; + bool Load(pugi::xml_document & doc) override; + bool Reset() override; + +private: + std::string m_fileName; + std::mutex m_mutex; +}; + +/** + * @brief Class which saves/loads data to/from xml_document class instance. + * @note this class is NOT thread-safe. + */ +class InMemoryStorage : public StorageBase +{ +public: + // StorageBase overrides: + bool Save(pugi::xml_document const & doc) override; + bool Load(pugi::xml_document & doc) override; + bool Reset() override; + +private: + pugi::xml_document m_doc; +}; +} // namespace traffxml diff --git a/private.h b/private.h index 123c13e61..40a8ac32a 100644 --- a/private.h +++ b/private.h @@ -8,7 +8,8 @@ #define METASERVER_URL "https://cdn-us-1.comaps.app" #define DEFAULT_URLS_JSON R"([ "https://comaps.firewall-gateway.de/", "https://cdn-us-2.comaps.tech/", "https://cdn-fi-1.comaps.app/", "https://comaps.openstreetmap.fr/", "https://comaps-it1.unfoxo.it/", "https://comaps-cdn.s3-website.cloud.ru/", "https://mapgen-fi-1.comaps.app/" ])" #define DEFAULT_CONNECTION_CHECK_IP "151.101.195.52" // For now the IP of comaps.app (Fastly CDN) -#define TRAFFIC_DATA_BASE_URL "" +// TODO development server, replace with live instance (once we have one) before merging into main +#define TRAFFIC_HTTP_URL_DEFAULT "https://api.dev.traffxml.org" #define USER_BINDING_PKCS12 "" #define USER_BINDING_PKCS12_PASSWORD "" #define COUNTRIES_TXT_SIGNATURE_HEX "91c0a9f6aa182371f047e256ab46489211acc2b51b13197fbe8c94eaa9749c7b" diff --git a/qt/CMakeLists.txt b/qt/CMakeLists.txt index f033d207c..cfadb7d85 100644 --- a/qt/CMakeLists.txt +++ b/qt/CMakeLists.txt @@ -73,6 +73,7 @@ target_link_libraries(${PROJECT_NAME} map gflags::gflags location_service + traffxml ) if (BUILD_DESIGNER) diff --git a/qt/mainwindow.cpp b/qt/mainwindow.cpp index e20e80ac7..45209cde6 100644 --- a/qt/mainwindow.cpp +++ b/qt/mainwindow.cpp @@ -281,10 +281,9 @@ void MainWindow::CreateNavigationBar() m_layers = new PopupMenuHolder(this); - /// @todo Uncomment when we will integrate a traffic provider. - // m_layers->addAction(QIcon(":/navig64/traffic.png"), tr("Traffic"), - // std::bind(&MainWindow::OnLayerEnabled, this, LayerType::TRAFFIC), true); - // m_layers->setChecked(LayerType::TRAFFIC, m_pDrawWidget->GetFramework().LoadTrafficEnabled()); + m_layers->addAction(QIcon(":/navig64/traffic.png"), tr("Traffic"), + std::bind(&MainWindow::OnLayerEnabled, this, LayerType::TRAFFIC), true); + m_layers->setChecked(LayerType::TRAFFIC, m_pDrawWidget->GetFramework().LoadTrafficEnabled()); m_layers->addAction(QIcon(":/navig64/subway.png"), tr("Public transport"), std::bind(&MainWindow::OnLayerEnabled, this, LayerType::TRANSIT), true); @@ -867,11 +866,10 @@ void MainWindow::SetLayerEnabled(LayerType type, bool enable) auto & frm = m_pDrawWidget->GetFramework(); switch (type) { - // @todo Uncomment when we will integrate a traffic provider. - // case LayerType::TRAFFIC: - // frm.GetTrafficManager().SetEnabled(enable); - // frm.SaveTrafficEnabled(enable); - // break; + case LayerType::TRAFFIC: + frm.GetTrafficManager().SetEnabled(enable); + frm.SaveTrafficEnabled(enable); + break; case LayerType::TRANSIT: frm.GetTransitManager().EnableTransitSchemeMode(enable); frm.SaveTransitSchemeEnabled(enable); diff --git a/qt/mainwindow.hpp b/qt/mainwindow.hpp index 0445e2a13..b1e145bfd 100644 --- a/qt/mainwindow.hpp +++ b/qt/mainwindow.hpp @@ -55,9 +55,12 @@ class MainWindow enum LayerType : uint8_t { - /// @todo Uncomment when we will integrate a traffic provider. - // TRAFFIC = 0, - TRANSIT = 0, // Metro scheme + /* + * OM and CM diverge here. OM eliminated TRAFFIC and set TRANSIT to 0. + * If this divergence becomes an issue, we need to do the same and move TRAFFIC further down. + */ + TRAFFIC = 0, + TRANSIT, // Metro scheme ISOLINES, OUTDOORS, }; diff --git a/qt/preferences_dialog.cpp b/qt/preferences_dialog.cpp index 957d3b4ef..e29902583 100644 --- a/qt/preferences_dialog.cpp +++ b/qt/preferences_dialog.cpp @@ -219,6 +219,35 @@ PreferencesDialog::PreferencesDialog(QWidget * parent, Framework & framework) } #endif + QLabel * trafficHttpUrlLabel = new QLabel("Traffic API URL:"); + + QLineEdit * trafficHttpUrlLineEdit = new QLineEdit(); + { + trafficHttpUrlLineEdit->setText(QString::fromStdString(framework.LoadTrafficHttpUrl())); + connect(trafficHttpUrlLineEdit, &QLineEdit::editingFinished, [&framework, trafficHttpUrlLineEdit]() + { + framework.SaveTrafficHttpUrl(trafficHttpUrlLineEdit->text().toStdString()); + framework.SetTrafficHttpUrl(trafficHttpUrlLineEdit->text().toStdString()); + }); + } + + QCheckBox * trafficHttpEnabledCheckBox = new QCheckBox("Enable live traffic data"); + { + trafficHttpEnabledCheckBox->setChecked(framework.LoadTrafficHttpEnabled()); + connect(trafficHttpEnabledCheckBox, &QCheckBox::stateChanged, [&framework, trafficHttpUrlLabel, trafficHttpUrlLineEdit](int i) + { + bool const enable = i > 0; + framework.SaveTrafficHttpEnabled(enable); + framework.SetTrafficHttpEnabled(enable); + /* + * Keep URL editable even when the source is disabled. Since changes are applied immediately + * (no Apply button), we want to be able to set the URL before enabling the source. + */ + //trafficHttpUrlLabel->setEnabled(enable); + //trafficHttpUrlLineEdit->setEnabled(enable); + }); + } + QHBoxLayout * bottomLayout = new QHBoxLayout(); { QPushButton * closeButton = new QPushButton(tr("Close")); @@ -244,6 +273,9 @@ PreferencesDialog::PreferencesDialog(QWidget * parent, Framework & framework) #ifdef BUILD_DESIGNER finalLayout->addWidget(indexRegenCheckBox); #endif + finalLayout->addWidget(trafficHttpEnabledCheckBox); + finalLayout->addWidget(trafficHttpUrlLabel); + finalLayout->addWidget(trafficHttpUrlLineEdit); finalLayout->addLayout(bottomLayout); setLayout(finalLayout); } diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt index 6a8e916f0..a2492d55e 100644 --- a/tools/CMakeLists.txt +++ b/tools/CMakeLists.txt @@ -7,5 +7,6 @@ omim_add_tool_subdirectory(topography_generator) omim_add_tool_subdirectory(track_generator) if (NOT SKIP_QT_GUI) omim_add_tool_subdirectory(skin_generator) + omim_add_tool_subdirectory(traff_assessment_tool) endif() diff --git a/tools/openlr/decoded_path.hpp b/tools/openlr/decoded_path.hpp index 5a94cb017..5687d9acd 100644 --- a/tools/openlr/decoded_path.hpp +++ b/tools/openlr/decoded_path.hpp @@ -28,6 +28,7 @@ DECLARE_EXCEPTION(DecodedPathSaveError, RootException); struct DecodedPath { PartnerSegmentId m_segmentId; + std::string m_messageId; Path m_path; }; diff --git a/tools/openlr/graph.hpp b/tools/openlr/graph.hpp index 452efe9e8..e2a75ac28 100644 --- a/tools/openlr/graph.hpp +++ b/tools/openlr/graph.hpp @@ -30,16 +30,26 @@ class Graph Graph(DataSource & dataSource, std::shared_ptr carModelFactory); - // Appends edges such as that edge.GetStartJunction() == junction to the |edges|. + /** + * Appends edges to `edges` such as that `edge.GetStartJunction() == junction`. + */ void GetOutgoingEdges(geometry::PointWithAltitude const & junction, EdgeListT & edges); - // Appends edges such as that edge.GetEndJunction() == junction to the |edges|. + + /** + * Appends edges to `edges` such as that `edge.GetEndJunction() == junction`. + */ void GetIngoingEdges(geometry::PointWithAltitude const & junction, EdgeListT & edges); - // Appends edges such as that edge.GetStartJunction() == junction and edge.IsFake() == false - // to the |edges|. + /** + * Appends edges to `edges` such as that `edge.GetStartJunction() == junction` and + * `edge.IsFake() == false`. + */ void GetRegularOutgoingEdges(Junction const & junction, EdgeListT & edges); - // Appends edges such as that edge.GetEndJunction() == junction and edge.IsFale() == false - // to the |edges|. + + /** + * Appends edges to `edges` such as that `edge.GetEndJunction() == junction` and + * `edge.IsFake() == false`. + */ void GetRegularIngoingEdges(Junction const & junction, EdgeListT & edges); void FindClosestEdges(m2::PointD const & point, uint32_t const count, diff --git a/tools/openlr/helpers.cpp b/tools/openlr/helpers.cpp index 38a265174..f3cc6d4da 100644 --- a/tools/openlr/helpers.cpp +++ b/tools/openlr/helpers.cpp @@ -40,7 +40,7 @@ optional GetFrcScore(Graph::Edge const & e, FunctionalRoadClass functiona Score constexpr kMaxScoreForFrc = 25; if (functionalRoadClass == FunctionalRoadClass::NotAValue) - return nullopt; + return optional(0); auto const hwClass = infoGetter.Get(e.GetFeatureId()).m_hwClass; @@ -174,7 +174,8 @@ bool PassesRestrictionV3(Graph::Edge const & e, FunctionalRoadClass functionalRo score = *frcScore; Score constexpr kScoreForFormOfWay = 25; - if (formOfWay == FormOfWay::Roundabout && infoGetter.Get(e.GetFeatureId()).m_isRoundabout) + if ((formOfWay == FormOfWay::Roundabout && infoGetter.Get(e.GetFeatureId()).m_isRoundabout) + || (formOfWay == FormOfWay::Sliproad && infoGetter.Get(e.GetFeatureId()).m_link)) score += kScoreForFormOfWay; return true; diff --git a/tools/openlr/helpers.hpp b/tools/openlr/helpers.hpp index 15dd08b24..48f29c232 100644 --- a/tools/openlr/helpers.hpp +++ b/tools/openlr/helpers.hpp @@ -46,14 +46,18 @@ std::common_type_t AbsDifference(T const a, U const b) bool PassesRestriction(Graph::Edge const & e, FunctionalRoadClass restriction, FormOfWay formOfWay, int frcThreshold, RoadInfoGetter & infoGetter); -/// \returns true if |e| conforms |functionalRoadClass| and |formOfWay| and false otherwise. -/// \note If the method returns true |score| should be considered next. +/** + * @return true if `e` conforms `functionalRoadClass` and `formOfWay` and false otherwise. + * @note If the method returns true `score` should be considered next. + */ bool PassesRestrictionV3(Graph::Edge const & e, FunctionalRoadClass functionalRoadClass, FormOfWay formOfWay, RoadInfoGetter & infoGetter, Score & score); -/// \returns true if edge |e| conforms Lowest Functional Road Class to Next Point. -/// \note frc means Functional Road Class. Please see openlr documentation for details: -/// http://www.openlr.org/data/docs/whitepaper/1_0/OpenLR-Whitepaper_v1.0.pdf +/** + * @return true if edge `e` conforms Lowest Functional Road Class to Next Point. + * @note frc means Functional Road Class. Please see openlr documentation for details: + * http://www.openlr.org/data/docs/whitepaper/1_0/OpenLR-Whitepaper_v1.0.pdf + */ bool ConformLfrcnp(Graph::Edge const & e, FunctionalRoadClass lowestFrcToNextPoint, int frcThreshold, RoadInfoGetter & infoGetter); bool ConformLfrcnpV3(Graph::Edge const & e, FunctionalRoadClass lowestFrcToNextPoint, RoadInfoGetter & infoGetter); @@ -62,9 +66,11 @@ size_t IntersectionLen(Graph::EdgeVector a, Graph::EdgeVector b); bool SuffixEqualsPrefix(Graph::EdgeVector const & a, Graph::EdgeVector const & b, size_t len); -// Returns a length of the longest suffix of |a| that matches any prefix of |b|. -// Neither |a| nor |b| can contain several repetitions of any edge. -// Returns -1 if |a| intersection |b| is not equal to some suffix of |a| and some prefix of |b|. +/** + * Returns a length of the longest suffix of `a` that matches any prefix of `b`. + * Neither `a` nor `b` can contain several repetitions of any edge. + * Returns -1 if `a` intersection `b` is not equal to some suffix of `a` and some prefix of `b`. + */ int32_t PathOverlappingLen(Graph::EdgeVector const & a, Graph::EdgeVector const & b); m2::PointD PointAtSegmentM(m2::PointD const & p1, m2::PointD const & p2, double const distanceM); diff --git a/tools/openlr/openlr_decoder.cpp b/tools/openlr/openlr_decoder.cpp index 5b028cb18..8c1f0786b 100644 --- a/tools/openlr/openlr_decoder.cpp +++ b/tools/openlr/openlr_decoder.cpp @@ -234,6 +234,7 @@ class SegmentsDecoderV2 m_graph.ResetFakes(); path.m_segmentId.Set(segment.m_segmentId); + path.m_messageId = segment.m_messageId; auto const & points = segment.GetLRPs(); CHECK_GREATER(points.size(), 1, ("A segment cannot consist of less than two points")); @@ -322,6 +323,7 @@ class SegmentsDecoderV3 uint32_t constexpr kMaxProjectionCandidates = 5; path.m_segmentId.Set(segment.m_segmentId); + path.m_messageId = segment.m_messageId; auto const & points = segment.GetLRPs(); CHECK_GREATER(points.size(), 1, ("A segment cannot consist of less than two points")); @@ -432,9 +434,9 @@ bool OpenLRDecoder::SegmentsFilter::Matches(LinearSegment const & segment) const } // OpenLRDecoder ----------------------------------------------------------------------------- -OpenLRDecoder::OpenLRDecoder(vector & dataSources, +OpenLRDecoder::OpenLRDecoder(DataSource & dataSource, CountryParentNameGetter const & countryParentNameGetter) - : m_dataSources(dataSources) + : m_dataSource(dataSource) , m_countryParentNameGetter(countryParentNameGetter) {} @@ -483,9 +485,9 @@ void OpenLRDecoder::Decode(vector const & segments, uint32_t cons vector stats(numThreads); vector workers; for (size_t i = 1; i < numThreads; ++i) - workers.emplace_back(worker, i, ref(m_dataSources[i]), ref(stats[i])); + workers.emplace_back(worker, i, ref(m_dataSource), ref(stats[i])); - worker(0 /* threadNum */, m_dataSources[0], stats[0]); + worker(0 /* threadNum */, m_dataSource, stats[0]); for (auto & worker : workers) worker.join(); diff --git a/tools/openlr/openlr_decoder.hpp b/tools/openlr/openlr_decoder.hpp index 1c4847ad3..6b6e73db9 100644 --- a/tools/openlr/openlr_decoder.hpp +++ b/tools/openlr/openlr_decoder.hpp @@ -41,19 +41,26 @@ class OpenLRDecoder bool const m_multipointsOnly; }; - OpenLRDecoder(std::vector & dataSources, CountryParentNameGetter const & countryParentNameGetter); + OpenLRDecoder(DataSource & dataSource, CountryParentNameGetter const & countryParentNameGetter); - // Maps partner segments to mwm paths. |segments| should be sorted by partner id. + /** + * Maps partner segments to mwm paths. + * + * `segments` should be sorted by partner id. + */ void DecodeV2(std::vector const & segments, uint32_t const numThreads, std::vector & paths); + /** + * Maps partner segments to mwm paths. + */ void DecodeV3(std::vector const & segments, uint32_t numThreads, std::vector & paths); private: template void Decode(std::vector const & segments, uint32_t const numThreads, std::vector & paths); - std::vector & m_dataSources; + DataSource & m_dataSource; CountryParentNameGetter m_countryParentNameGetter; }; } // namespace openlr diff --git a/tools/openlr/openlr_match_quality/openlr_assessment_tool/CMakeLists.txt b/tools/openlr/openlr_match_quality/openlr_assessment_tool/CMakeLists.txt index 5aa14f8e9..23ad94182 100644 --- a/tools/openlr/openlr_match_quality/openlr_assessment_tool/CMakeLists.txt +++ b/tools/openlr/openlr_match_quality/openlr_assessment_tool/CMakeLists.txt @@ -28,4 +28,5 @@ target_link_libraries(${PROJECT_NAME} qt_common map gflags::gflags + traffxml ) diff --git a/tools/openlr/openlr_model.cpp b/tools/openlr/openlr_model.cpp index 44b47f350..dc9d90dad 100644 --- a/tools/openlr/openlr_model.cpp +++ b/tools/openlr/openlr_model.cpp @@ -38,4 +38,21 @@ string DebugPrint(LinearSegmentSource source) } UNREACHABLE(); } + +string DebugPrint(FunctionalRoadClass frc) +{ + switch (frc) + { + case FunctionalRoadClass::FRC0: return "FRC0"; + case FunctionalRoadClass::FRC1: return "FRC1"; + case FunctionalRoadClass::FRC2: return "FRC2"; + case FunctionalRoadClass::FRC3: return "FRC3"; + case FunctionalRoadClass::FRC4: return "FRC4"; + case FunctionalRoadClass::FRC5: return "FRC5"; + case FunctionalRoadClass::FRC6: return "FRC6"; + case FunctionalRoadClass::FRC7: return "FRC7"; + case FunctionalRoadClass::NotAValue: return "NotAValue"; + } + UNREACHABLE(); +} } // namespace openlr diff --git a/tools/openlr/openlr_model.hpp b/tools/openlr/openlr_model.hpp index a5f4b8435..8acf7167e 100644 --- a/tools/openlr/openlr_model.hpp +++ b/tools/openlr/openlr_model.hpp @@ -10,42 +10,70 @@ namespace openlr { -// The form of way (FOW) describes the physical road type of a line. +/** + * The form of way (FOW) describes the physical road type of a line. + */ enum class FormOfWay { - // The physical road type is unknown. + /** + * The physical road type is unknown. + */ Undefined, - // A road permitted for motorized vehicles only in combination with a prescribed minimum speed. - // It has two or more physically separated carriageways and no single level-crossings. + /** + * A road permitted for motorized vehicles only in combination with a prescribed minimum speed. + * It has two or more physically separated carriageways and no single level-crossings. + */ Motorway, - // A road with physically separated carriageways regardless of the number of lanes. - // If a road is also a motorway, it should be coded as such and not as a multiple carriageway. + /** + * A road with physically separated carriageways regardless of the number of lanes. + * If a road is also a motorway, it should be coded as such and not as a multiple carriageway. + */ MultipleCarriageway, - // All roads without separate carriageways are considered as roads with a single carriageway. + /** + * All roads without separate carriageways are considered as roads with a single carriageway. + */ SingleCarriageway, - // A road which forms a ring on which traffic traveling in only one direction is allowed. + /** + * A road which forms a ring on which traffic traveling in only one direction is allowed. + */ Roundabout, - // An open area (partly) enclosed by roads which is used for non-traffic purposes - // and which is not a Roundabout. + /** + * An open area (partly) enclosed by roads which is used for non-traffic purposes + * and which is not a Roundabout. + */ Trafficsquare, - // A road especially designed to enter or leave a line. + /** + * A road especially designed to enter or leave a line. + */ Sliproad, - // The physical road type is known, but does not fit into one of the other categories. + /** + * The physical road type is known, but does not fit into one of the other categories. + */ Other, - // A path only allowed for bikes. + /** + * A path only allowed for bikes. + */ BikePath, - // A path only allowed for pedestrians. + /** + * A path only allowed for pedestrians. + */ Footpath, NotAValue }; -// The functional road class (FRC) of a line is a road classification based on the importance -// of the road represented by the line. +/** + * The functional road class (FRC) of a line is a road classification based on the importance + * of the road represented by the line. + */ enum class FunctionalRoadClass { - // Main road, highest importance. + /** + * Main road, highest importance. + */ FRC0, - // First class road. + /** + * First class road. + */ FRC1, // Other road classes. @@ -70,27 +98,37 @@ enum class LinearSegmentSource struct LocationReferencePoint { - // Coordinates of the point of interest. + /** + * Coordinates of the point of interest. + */ ms::LatLon m_latLon = ms::LatLon::Zero(); - // The bearing (BEAR) describes the angle between the true North and a line which is defined - // by the coordinate of the LocationReferencePoint and a coordinate which is BEARDIST along - // the line defined by the LocationReference-point attributes. - // For more information see OpenLR-Whitepaper `Bearing' section. + + /** + * The bearing (BEAR) describes the angle between the true North and a line which is defined + * by the coordinate of the LocationReferencePoint and a coordinate which is BEARDIST along + * the line defined by the LocationReference-point attributes. + * For more information see OpenLR-Whitepaper ‘Bearing’ section. + */ uint8_t m_bearing = 0; FunctionalRoadClass m_functionalRoadClass = FunctionalRoadClass::NotAValue; FormOfWay m_formOfWay = FormOfWay::NotAValue; - // The distance to next point field describes the distance to the next LocationReferencePoint - // in the topological connection of the LocationReferencePoints. The distance is measured in meters - // and is calculated along the location reference path between two subsequent LR-points. - // The last LRP has the distance value 0. - // Should not be used in the last point of a segment. + /** + * The distance to next point field describes the distance to the next LocationReferencePoint + * in the topological connection of the LocationReferencePoints. The distance is measured in meters + * and is calculated along the location reference path between two subsequent LR-points. + * The last LRP has the distance value 0. + * Should not be used in the last point of a segment. + */ uint32_t m_distanceToNextPoint = 0; - // The lowest FunctionalRoadClass to the next point (LFRCNP) is the lowest FunctionalRoadClass - // value which appears in the location reference path between two consecutive LR-points. - // This information could be used to limit the number of road classes which need to be - // scanned during the decoding. - // Should not be used in the last point of a segment. + + /** + * The lowest FunctionalRoadClass to the next point (LFRCNP) is the lowest FunctionalRoadClass + * value which appears in the location reference path between two consecutive LR-points. + * This information could be used to limit the number of road classes which need to be + * scanned during the decoding. + * Should not be used in the last point of a segment. + */ FunctionalRoadClass m_lfrcnp = FunctionalRoadClass::NotAValue; bool m_againstDrivingDirection = false; }; @@ -113,6 +151,7 @@ struct LinearSegment LinearSegmentSource m_source = LinearSegmentSource::NotValid; // TODO(mgsergio): Think of using openlr::PartnerSegmentId uint32_t m_segmentId = kInvalidSegmentId; + std::string m_messageId = ""; // TODO(mgsergio): Make sure that one segment cannot contain // more than one location reference. LinearLocationReference m_locationReference; @@ -121,4 +160,5 @@ struct LinearSegment }; std::string DebugPrint(LinearSegmentSource source); +std::string DebugPrint(FunctionalRoadClass frc); } // namespace openlr diff --git a/tools/openlr/openlr_stat/openlr_stat.cpp b/tools/openlr/openlr_stat/openlr_stat.cpp index 7856c6844..0440eb3c1 100644 --- a/tools/openlr/openlr_stat/openlr_stat.cpp +++ b/tools/openlr/openlr_stat/openlr_stat.cpp @@ -58,7 +58,7 @@ int32_t const kMinNumThreads = 1; int32_t const kMaxNumThreads = 128; int32_t const kHandleAllSegments = -1; -void LoadDataSources(std::string const & pathToMWMFolder, std::vector & dataSources) +void LoadDataSource(std::string const & pathToMWMFolder, FrozenDataSource & dataSource) { CHECK(Platform::IsDirectory(pathToMWMFolder), (pathToMWMFolder, "must be a directory.")); @@ -67,8 +67,7 @@ void LoadDataSources(std::string const & pathToMWMFolder, std::vector numCountries(numDataSources); + uint64_t numCountries; for (auto const & fileName : files) { @@ -81,15 +80,12 @@ void LoadDataSources(std::string const & pathToMWMFolder, std::vectorGetType() == MwmInfo::COUNTRY) - ++numCountries[i]; - } + auto const result = dataSource.RegisterMap(localFile); + CHECK_EQUAL(result.second, MwmSet::RegResult::Success, ("Can't register mwm:", localFile)); + + auto const & info = result.first.GetInfo(); + if (info && info->GetType() == MwmInfo::COUNTRY) + ++numCountries; } catch (RootException const & ex) { @@ -97,9 +93,8 @@ void LoadDataSources(std::string const & pathToMWMFolder, std::vector(FLAGS_num_threads); - std::vector dataSources(numThreads); + FrozenDataSource dataSource; - LoadDataSources(FLAGS_mwms_path, dataSources); + LoadDataSource(FLAGS_mwms_path, dataSource); - OpenLRDecoder decoder(dataSources, + OpenLRDecoder decoder(dataSource, storage::CountryParentGetter(FLAGS_countries_filename, GetPlatform().ResourcesDir())); pugi::xml_document document; diff --git a/tools/openlr/router.hpp b/tools/openlr/router.hpp index fca2d79a3..81bfbcbf3 100644 --- a/tools/openlr/router.hpp +++ b/tools/openlr/router.hpp @@ -34,20 +34,30 @@ class Router final class Score final { public: - // A weight for total length of true fake edges. + /** + * A weight for total length of true fake edges. + */ static int const kTrueFakeCoeff = 10; - // A weight for total length of fake edges that are parts of some - // real edges. + /** + * A weight for total length of fake edges that are parts of some + * real edges. + */ static constexpr double kFakeCoeff = 0.001; - // A weight for passing too far from pivot points. + /** + * A weight for passing too far from pivot points. + */ static int const kIntermediateErrorCoeff = 3; - // A weight for excess of distance limit. + /** + * A weight for excess of distance limit. + */ static int const kDistanceErrorCoeff = 3; - // A weight for deviation from bearing. + /** + * A weight for deviation from bearing. + */ static int const kBearingErrorCoeff = 5; void AddDistance(double p) { m_distance += p; } @@ -66,7 +76,9 @@ class Router final bool operator!=(Score const & rhs) const { return !(*this == rhs); } private: - // Reduced length of path in meters. + /** + * Reduced length of path in meters. + */ double m_distance = 0.0; double m_penalty = 0.0; @@ -142,21 +154,31 @@ class Router final bool Init(std::vector const & points, double positiveOffsetM, double negativeOffsetM); bool FindPath(Path & path); - // Returns true if the bearing should be checked for |u|, if the - // real passed distance from the source vertex is |distanceM|. + /** + * Returns true if the bearing should be checked for `u`, if the + * real passed distance from the source vertex is `distanceM`. + */ bool NeedToCheckBearing(Vertex const & u, double distanceM) const; double GetPotential(Vertex const & u) const; - // Returns true if |u| is located near portal to the next stage. - // |pi| is the potential of |u|. + /** + * Returns true if `u` is located near portal to the next stage. + * + * @param pi potential of `u`. + */ bool NearNextStage(Vertex const & u, double pi) const; - // Returns true if it's possible to move to the next stage from |u|. - // |pi| is the potential of |u|. + /** + * Returns true if it's possible to move to the next stage from `u`. + * + * @param pi potential of `u`. + */ bool MayMoveToNextStage(Vertex const & u, double pi) const; - // Returns true if |u| is a final vertex and the router may stop now. + /** + * Returns true if `u` is a final vertex and the router may stop now. + */ bool IsFinalVertex(Vertex const & u) const { return u.m_stage == m_pivots.size(); } double GetWeight(routing::Edge const & e) const @@ -187,20 +209,26 @@ class Router final template size_t FindPrefixLengthToConsume(It b, It const e, double lengthM); - // Finds all edges that are on (u, v) and have the same direction as - // (u, v). Then, computes the fraction of the union of these edges - // to the total length of (u, v). + /** + * Finds all edges that are on (u, v) and have the same direction as + * (u, v). Then, computes the fraction of the union of these edges + * to the total length of (u, v). + */ template double GetCoverage(m2::PointD const & u, m2::PointD const & v, It b, It e); - // Finds the longest prefix of [b, e) that covers edge (u, v). - // Returns the fraction of the coverage to the length of the (u, v). + /** + * Finds the longest prefix of [b, e) that covers edge (u, v). + * Returns the fraction of the coverage to the length of the (u, v). + */ template double GetMatchingScore(m2::PointD const & u, m2::PointD const & v, It b, It e); - // Finds the longest prefix of fake edges of [b, e) that have the - // same stage as |stage|. If the prefix exists, passes its bounding - // iterator to |fn|. + /** + * Finds the longest prefix of fake edges of [b, e) that have the + * same stage as `stage`. If the prefix exists, passes its bounding + * iterator to `fn`. + */ template void ForStagePrefix(It b, It e, size_t stage, Fn && fn); diff --git a/tools/openlr/score_candidate_paths_getter.cpp b/tools/openlr/score_candidate_paths_getter.cpp index 86def7092..5586b5290 100644 --- a/tools/openlr/score_candidate_paths_getter.cpp +++ b/tools/openlr/score_candidate_paths_getter.cpp @@ -139,8 +139,7 @@ void ScoreCandidatePathsGetter::GetAllSuitablePaths(ScoreEdgeVec const & startLi for (auto const & e : startLines) { Score roadScore = 0; // Score based on functional road class and form of way. - if (source == LinearSegmentSource::FromLocationReferenceTag && - !PassesRestrictionV3(e.m_edge, functionalRoadClass, formOfWay, m_infoGetter, roadScore)) + if (!PassesRestrictionV3(e.m_edge, functionalRoadClass, formOfWay, m_infoGetter, roadScore)) { continue; } @@ -194,8 +193,7 @@ void ScoreCandidatePathsGetter::GetAllSuitablePaths(ScoreEdgeVec const & startLi CHECK(currentEdge.HasRealPart(), ()); Score roadScore = 0; - if (source == LinearSegmentSource::FromLocationReferenceTag && - !PassesRestrictionV3(e, functionalRoadClass, formOfWay, m_infoGetter, roadScore)) + if (!PassesRestrictionV3(e, functionalRoadClass, formOfWay, m_infoGetter, roadScore)) { continue; } diff --git a/tools/openlr/score_candidate_paths_getter.hpp b/tools/openlr/score_candidate_paths_getter.hpp index d27fd2046..b4c301366 100644 --- a/tools/openlr/score_candidate_paths_getter.hpp +++ b/tools/openlr/score_candidate_paths_getter.hpp @@ -56,8 +56,10 @@ class ScoreCandidatePathsGetter Graph::Edge const m_edge; double const m_distanceM; Score const m_pointScore; - // Minimum score of segments of the path going along |m_parent| based on functional road class - // and form of way. + /** + * Minimum score of segments of the path going along `m_parent` based on functional road class + * and form of way. + */ Score const m_minRoadScore; }; @@ -94,9 +96,12 @@ class ScoreCandidatePathsGetter // distance-to-next point is taken from point 3. You can learn more in // TomTom OpenLR spec. - /// \brief Fills |allPaths| with paths near start or finish point starting from |startLines|. - /// To extract a path from |allPaths| a item from |allPaths| should be taken, - /// then should be taken the member |m_parent| of the item and so on till the beginning. + /** + * @brief Fills `allPaths` with paths near start or finish point starting from `startLines`. + * + * To extract a path from `allPaths` a item from `allPaths` should be taken, + * then should be taken the member `m_parent` of the item and so on till the beginning. + */ void GetAllSuitablePaths(ScoreEdgeVec const & startLines, LinearSegmentSource source, bool isLastPoint, double bearDistM, FunctionalRoadClass functionalRoadClass, FormOfWay formOfWay, double distanceToNextPointM, std::vector> & allPaths); diff --git a/tools/openlr/score_candidate_points_getter.hpp b/tools/openlr/score_candidate_points_getter.hpp index 960719df7..f04fc7b1f 100644 --- a/tools/openlr/score_candidate_points_getter.hpp +++ b/tools/openlr/score_candidate_points_getter.hpp @@ -35,7 +35,9 @@ class ScoreCandidatePointsGetter void GetJunctionPointCandidates(m2::PointD const & p, bool isLastPoint, ScoreEdgeVec & edgeCandidates); void EnrichWithProjectionPoints(m2::PointD const & p, ScoreEdgeVec & edgeCandidates); - /// \returns true if |p| is a junction and false otherwise. + /** + * @return true if `p` is a junction and false otherwise. + */ bool IsJunction(m2::PointD const & p); Score GetScoreByDistance(m2::PointD const & point, m2::PointD const & candidate); diff --git a/tools/openlr/score_paths_connector.hpp b/tools/openlr/score_paths_connector.hpp index f144eee68..a34548db3 100644 --- a/tools/openlr/score_paths_connector.hpp +++ b/tools/openlr/score_paths_connector.hpp @@ -17,9 +17,12 @@ class ScorePathsConnector public: ScorePathsConnector(Graph & graph, RoadInfoGetter & infoGetter, v2::Stats & stat); - /// \brief Connects |lineCandidates| and fills |resultPath| with the path with maximum score - /// if there's a good enough. - /// \returns true if the best path is found and false otherwise. + /** + * @brief Connects `lineCandidates` and fills `resultPath` with the path with maximum score + * if there's a good enough one. + * + * @return true if the best path is found and false otherwise. + */ bool FindBestPath(std::vector const & points, std::vector> const & lineCandidates, LinearSegmentSource source, std::vector & resultPath); diff --git a/tools/openlr/stats.hpp b/tools/openlr/stats.hpp index bdb121b2b..36393604a 100644 --- a/tools/openlr/stats.hpp +++ b/tools/openlr/stats.hpp @@ -41,7 +41,9 @@ struct alignas(kCacheLineSize) Stats uint32_t m_noShortestPathFound = 0; uint32_t m_notEnoughScore = 0; uint32_t m_wrongOffsets = 0; - // Number of zeroed distance-to-next point values in the input. + /** + * Number of zeroed distance-to-next point values in the input. + */ uint32_t m_zeroDistToNextPointCount = 0; }; } // namespace v2 diff --git a/tools/python/maps_generator/generator/gen_tool.py b/tools/python/maps_generator/generator/gen_tool.py index 9b6ed1310..7abd22a1c 100644 --- a/tools/python/maps_generator/generator/gen_tool.py +++ b/tools/python/maps_generator/generator/gen_tool.py @@ -31,7 +31,6 @@ class GenTool: "generate_regions": bool, "generate_regions_kv": bool, "generate_search_index": bool, - "generate_traffic_keys": bool, "generate_world": bool, "have_borders_for_whole_world": bool, "make_city_roads": bool, diff --git a/tools/python/maps_generator/generator/steps.py b/tools/python/maps_generator/generator/steps.py index 73de465c9..478feb4be 100644 --- a/tools/python/maps_generator/generator/steps.py +++ b/tools/python/maps_generator/generator/steps.py @@ -403,7 +403,6 @@ def step_routing(env: Env, country: AnyStr, **kwargs): make_cross_mwm=True, generate_cameras=True, make_routing_index=True, - generate_traffic_keys=False, output=country, **kwargs, ) diff --git a/tools/traff_assessment_tool/CMakeLists.txt b/tools/traff_assessment_tool/CMakeLists.txt new file mode 100644 index 000000000..a5ee6f868 --- /dev/null +++ b/tools/traff_assessment_tool/CMakeLists.txt @@ -0,0 +1,31 @@ +project(traff_assessment_tool) + +set(SRC + main.cpp + mainwindow.cpp + mainwindow.hpp + map_widget.cpp + map_widget.hpp + points_controller_delegate_base.hpp + resumable_timer.cpp + resumable_timer.hpp + traffic_drawer_delegate_base.hpp + traffic_model.cpp + traffic_model.hpp + traffic_panel.cpp + traffic_panel.hpp + trafficmodeinitdlg.cpp + trafficmodeinitdlg.h + trafficmodeinitdlg.ui +) + +omim_add_executable(${PROJECT_NAME} ${SRC}) + +set_target_properties(${PROJECT_NAME} PROPERTIES AUTOUIC ON AUTOMOC ON) + +target_link_libraries(${PROJECT_NAME} + qt_common + map + gflags::gflags + traffxml +) diff --git a/tools/traff_assessment_tool/Info.plist b/tools/traff_assessment_tool/Info.plist new file mode 100644 index 000000000..384699b4b --- /dev/null +++ b/tools/traff_assessment_tool/Info.plist @@ -0,0 +1,11 @@ + + + + + NSPrincipalClass + NSApplication + + NSHighResolutionCapable + True + + diff --git a/tools/traff_assessment_tool/main.cpp b/tools/traff_assessment_tool/main.cpp new file mode 100644 index 000000000..188c5adb6 --- /dev/null +++ b/tools/traff_assessment_tool/main.cpp @@ -0,0 +1,43 @@ +#include "mainwindow.hpp" + +#include "qt/qt_common/helpers.hpp" + +#include "map/framework.hpp" + +#include + +#include + +namespace +{ +DEFINE_string(resources_path, "", "Path to resources directory"); +DEFINE_string(data_path, "", "Path to data directory"); +} // namespace + +int main(int argc, char * argv[]) +{ + gflags::SetUsageMessage("Visualize and check matched routes."); + gflags::ParseCommandLineFlags(&argc, &argv, true); + + Platform & platform = GetPlatform(); + if (!FLAGS_resources_path.empty()) + platform.SetResourceDir(FLAGS_resources_path); + if (!FLAGS_data_path.empty()) + platform.SetWritableDirForTests(FLAGS_data_path); + + Q_INIT_RESOURCE(resources_common); + QApplication app(argc, argv); + + qt::common::SetDefaultSurfaceFormat(app.platformName()); + + FrameworkParams params; + + params.m_trafficTestMode = true; + + Framework framework(params); + traffxml::MainWindow mainWindow(framework); + + mainWindow.showMaximized(); + + return app.exec(); +} diff --git a/tools/traff_assessment_tool/mainwindow.cpp b/tools/traff_assessment_tool/mainwindow.cpp new file mode 100644 index 000000000..ecd91568d --- /dev/null +++ b/tools/traff_assessment_tool/mainwindow.cpp @@ -0,0 +1,487 @@ +#include "traff_assessment_tool/mainwindow.hpp" + +#include "traff_assessment_tool/map_widget.hpp" +#include "traff_assessment_tool/points_controller_delegate_base.hpp" +#include "traff_assessment_tool/traffic_drawer_delegate_base.hpp" +#include "traff_assessment_tool/traffic_model.hpp" +#include "traff_assessment_tool/traffic_panel.hpp" +#include "traff_assessment_tool/trafficmodeinitdlg.h" + +#include "map/framework.hpp" + +#include "drape_frontend/drape_api.hpp" + +#include "routing/data_source.hpp" +#include "routing/features_road_graph.hpp" +#include "routing/road_graph.hpp" + +#include "routing_common/car_model.hpp" + +#include "storage/country_parent_getter.hpp" + +#include "traffxml/traff_model_xml.hpp" + +#include "geometry/mercator.hpp" +#include "geometry/point2d.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace traffxml +{ +namespace +{ +class TrafficDrawerDelegate : public TrafficDrawerDelegateBase +{ + static constexpr char const * kEncodedLineId = "encodedPath"; + static constexpr char const * kDecodedLineId = "decodedPath"; + static constexpr char const * kGoldenLineId = "goldenPath"; + +public: + explicit TrafficDrawerDelegate(Framework & framework) + : m_framework(framework) + , m_drapeApi(m_framework.GetDrapeApi()) + , m_bm(framework.GetBookmarkManager()) + { + } + + void SetViewportCenter(m2::PointD const & center) override + { + m_framework.SetViewportCenter(center); + } + + void DrawDecodedSegments(std::vector const & points) override + { + CHECK(!points.empty(), ("Points must not be empty.")); + + LOG(LINFO, ("Decoded segment", points)); + m_drapeApi.AddLine(kDecodedLineId, + df::DrapeApiLineData(points, dp::Color(0, 0, 255, 255)) + .Width(3.0f).ShowPoints(true /* markPoints */)); + } + + void DrawEncodedSegment(std::vector const & points) override + { + LOG(LINFO, ("Encoded segment", points)); + m_drapeApi.AddLine(kEncodedLineId, + df::DrapeApiLineData(points, dp::Color(255, 0, 0, 255)) + .Width(3.0f).ShowPoints(true /* markPoints */)); + } + + void DrawGoldenPath(std::vector const & points) override + { + m_drapeApi.AddLine(kGoldenLineId, + df::DrapeApiLineData(points, dp::Color(255, 127, 36, 255)) + .Width(4.0f).ShowPoints(true /* markPoints */)); + } + + void ClearGoldenPath() override + { + m_drapeApi.RemoveLine(kGoldenLineId); + } + + void ClearAllPaths() override + { + m_drapeApi.Clear(); + } + + void VisualizePoints(std::vector const & points) override + { + auto editSession = m_bm.GetEditSession(); + editSession.SetIsVisible(UserMark::Type::DEBUG_MARK, true); + for (auto const & p : points) + editSession.CreateUserMark(p); + } + + void ClearAllVisualizedPoints() override + { + m_bm.GetEditSession().ClearGroup(UserMark::Type::DEBUG_MARK); + } + +private: + Framework & m_framework; + df::DrapeApi & m_drapeApi; + BookmarkManager & m_bm; +}; + +bool PointsMatch(m2::PointD const & a, m2::PointD const & b) +{ + auto constexpr kToleranceDistanceM = 1.0; + return mercator::DistanceOnEarth(a, b) < kToleranceDistanceM; +} + +class PointsControllerDelegate : public PointsControllerDelegateBase +{ +public: + explicit PointsControllerDelegate(Framework & framework) + : m_framework(framework) + , m_dataSource(const_cast(GetDataSource()), nullptr /* numMwmIDs */) + , m_roadGraph(m_dataSource, routing::IRoadGraph::Mode::ObeyOnewayTag, + std::make_unique(storage::CountryParentGetter{})) + { + } + + std::vector GetAllJunctionPointsInViewport() const override + { + std::vector points; + auto const & rect = m_framework.GetCurrentViewport(); + auto const pushPoint = [&points, &rect](m2::PointD const & point) + { + if (!rect.IsPointInside(point)) + return; + for (auto const & p : points) + { + if (PointsMatch(point, p)) + return; + } + points.push_back(point); + }; + + auto const pushFeaturePoints = [&pushPoint](FeatureType & ft) + { + if (ft.GetGeomType() != feature::GeomType::Line) + return; + + /// @todo Transported (railway=rail) are also present here :) + auto const roadClass = ftypes::GetHighwayClass(feature::TypesHolder(ft)); + if (roadClass == ftypes::HighwayClass::Undefined || + roadClass == ftypes::HighwayClass::Pedestrian) + { + return; + } + ft.ForEachPoint(pushPoint, scales::GetUpperScale()); + }; + + GetDataSource().ForEachInRect(pushFeaturePoints, rect, scales::GetUpperScale()); + return points; + } + + std::pair, m2::PointD> GetCandidatePoints( + m2::PointD const & p) const override + { + auto constexpr kInvalidIndex = std::numeric_limits::max(); + + std::vector points; + m2::PointD pointOnFt; + indexer::ForEachFeatureAtPoint(GetDataSource(), [&points, &p, &pointOnFt](FeatureType & ft) + { + if (ft.GetGeomType() != feature::GeomType::Line) + return; + + ft.ParseGeometry(FeatureType::BEST_GEOMETRY); + + auto minDistance = std::numeric_limits::max(); + auto bestPointIndex = kInvalidIndex; + for (size_t i = 0; i < ft.GetPointsCount(); ++i) + { + auto const & fp = ft.GetPoint(i); + auto const distance = mercator::DistanceOnEarth(fp, p); + if (PointsMatch(fp, p) && distance < minDistance) + { + bestPointIndex = i; + minDistance = distance; + } + } + + if (bestPointIndex != kInvalidIndex) + { + points.emplace_back(ft.GetID(), bestPointIndex); + pointOnFt = ft.GetPoint(bestPointIndex); + } + }, p); + + return std::make_pair(points, pointOnFt); + } + + std::vector GetReachablePoints(m2::PointD const & p) const override + { + routing::FeaturesRoadGraph::EdgeListT edges; + m_roadGraph.GetOutgoingEdges(geometry::PointWithAltitude(p, geometry::kDefaultAltitudeMeters), + edges); + + std::vector points; + for (auto const & e : edges) + points.push_back(e.GetEndJunction().GetPoint()); + return points; + } + + ClickType CheckClick(m2::PointD const & clickPoint, + m2::PointD const & lastClickedPoint, + std::vector const & reachablePoints) const override + { + // == Comparison is safe here since |clickPoint| is adjusted by GetFeaturesPointsByPoint + // so to be equal the closest feature's one. + if (clickPoint == lastClickedPoint) + return ClickType::Remove; + for (auto const & p : reachablePoints) + { + if (PointsMatch(clickPoint, p)) + return ClickType::Add; + } + return ClickType::Miss; + } + +private: + DataSource const & GetDataSource() const { return m_framework.GetDataSource(); } + + Framework & m_framework; + routing::MwmDataSource m_dataSource; + routing::FeaturesRoadGraph m_roadGraph; +}; +} // namespace + + +MainWindow::MainWindow(Framework & framework) + : m_framework(framework) +{ + m_mapWidget = new MapWidget(m_framework, this /* parent */); + + m_layout = new QHBoxLayout(); + m_layout->addWidget(m_mapWidget); + + auto * window = new QWidget(); + window->setLayout(m_layout); + window->setGraphicsEffect(nullptr); + + setCentralWidget(window); + + setWindowTitle(tr("TraFF Assessment Tool")); + setWindowIcon(QIcon(":/ui/logo.png")); + + QMenu * fileMenu = new QMenu("File", this); + menuBar()->addMenu(fileMenu); + + fileMenu->addAction("Open sample", QKeySequence("Ctrl+O"), this, &MainWindow::OnOpenTrafficSample); + fileMenu->addAction("Purge expired messages", QKeySequence("Ctrl+P"), this, &MainWindow::OnPurgeExpiredMessages); + fileMenu->addAction("Clear TraFF cache", QKeySequence("Ctrl+D"), this, &MainWindow::OnClearCache); + + m_closeTrafficSampleAction = fileMenu->addAction("Close sample", QKeySequence("Ctrl+W"), this, &MainWindow::OnCloseTrafficSample); + m_saveTrafficSampleAction = fileMenu->addAction("Save sample", QKeySequence("Ctrl+S"), this, &MainWindow::OnSaveTrafficSample); + + fileMenu->addSeparator(); + + m_framework.GetTrafficManager().SetEnabled(true); + +#ifdef openlr_obsolete + m_goldifyMatchedPathAction = fileMenu->addAction("Goldify", QKeySequence("Ctrl+G"), [this] { m_trafficModel->GoldifyMatchedPath(); }); + m_startEditingAction = fileMenu->addAction("Edit", QKeySequence("Ctrl+E"), + [this] { + m_trafficModel->StartBuildingPath(); + m_mapWidget->SetMode(MapWidget::Mode::TrafficMarkup); + m_commitPathAction->setEnabled(true /* enabled */); + m_cancelPathAction->setEnabled(true /* enabled */); + }); + m_commitPathAction = fileMenu->addAction("Accept path", + QKeySequence("Ctrl+A"), + [this] { + m_trafficModel->CommitPath(); + m_mapWidget->SetMode(MapWidget::Mode::Normal); + }); + m_cancelPathAction = fileMenu->addAction("Revert path", + QKeySequence("Ctrl+R"), + [this] { + m_trafficModel->RollBackPath(); + m_mapWidget->SetMode(MapWidget::Mode::Normal); + }); + m_ignorePathAction = fileMenu->addAction("Ignore path", + QKeySequence("Ctrl+I"), + [this] { + m_trafficModel->IgnorePath(); + m_mapWidget->SetMode(MapWidget::Mode::Normal); + }); + + m_goldifyMatchedPathAction->setEnabled(false /* enabled */); + m_closeTrafficSampleAction->setEnabled(false /* enabled */); + m_saveTrafficSampleAction->setEnabled(false /* enabled */); + m_startEditingAction->setEnabled(false /* enabled */); + m_commitPathAction->setEnabled(false /* enabled */); + m_cancelPathAction->setEnabled(false /* enabled */); + m_ignorePathAction->setEnabled(false /* enabled */); +#endif +} + +void MainWindow::CreateTrafficPanel(bool hasMessages) +{ + if (!m_trafficModel) + { + m_trafficModel = new TrafficModel(m_framework); + + connect(m_mapWidget, &MapWidget::TrafficMarkupClick, + m_trafficModel, &TrafficModel::OnClick); + connect(m_trafficModel, &TrafficModel::EditingStopped, + this, &MainWindow::OnPathEditingStop); + connect(m_trafficModel, &TrafficModel::SegmentSelected, + [](int segmentId) { QApplication::clipboard()->setText(QString::number(segmentId)); }); + } + + if (!m_dockWidget) + { + m_dockWidget = new QDockWidget(tr("Messages"), this); + addDockWidget(Qt::DockWidgetArea::RightDockWidgetArea, m_dockWidget); + + m_trafficPanel = new TrafficPanel(m_trafficModel, m_dockWidget); + m_dockWidget->setWidget(m_trafficPanel); + m_trafficModel->SetTrafficPanel(m_trafficPanel); + + m_dockWidget->adjustSize(); + m_dockWidget->setMinimumWidth(400); + } + if (hasMessages) + { + m_trafficPanel->SetStatus(true); + m_trafficPanel->GetTimer().Resume(); + } + m_dockWidget->show(); +} + +void MainWindow::DestroyTrafficPanel() +{ + LOG(LINFO, ("enter")); + removeDockWidget(m_dockWidget); + delete m_dockWidget; + m_dockWidget = nullptr; + + delete m_trafficModel; + m_trafficModel = nullptr; + + m_mapWidget->SetMode(MapWidget::Mode::Normal); +} + +void MainWindow::OnOpenTrafficSample() +{ + TrafficModeInitDlg dlg; + dlg.exec(); + if (dlg.result() != QDialog::DialogCode::Accepted) + return; + + pugi::xml_document document; + LOG(LINFO, ("Attempting to load:", dlg.GetDataFilePath())); + auto const load_result = document.load_file(dlg.GetDataFilePath().data()); + if (!load_result) + { + QMessageBox::critical(this, "Data loading error", QString::asprintf("Can't load file %s: %s", dlg.GetDataFilePath().data(), load_result.description())); + LOG(LERROR, ("Can't load file", dlg.GetDataFilePath(), ":", load_result.description())); + return; + } + + std::setlocale(LC_ALL, "en_US.UTF-8"); + traffxml::TraffFeed feed; + traffxml::TraffFeed shiftedFeed; + bool hasMessages = false; + if (traffxml::ParseTraff(document, std::nullopt /* dataSource */, feed)) + { + hasMessages = !feed.empty(); + for (auto message : feed) + { + // `ShiftTimestamps()` will not change the message in `feed`, therefore construct a new feed + message.ShiftTimestamps(); + shiftedFeed.push_back(message); + } + LOG(LINFO, ("TraFF data parsed successfully, pushing")); + m_framework.GetTrafficManager().ReceiveFeed(shiftedFeed); + LOG(LINFO, ("Push completed")); + } + else + { + QMessageBox::critical(this, "Data loading error", QString("An error occurred parsing the TraFF feed")); + LOG(LWARNING, ("An error occurred parsing the TraFF feed")); + return; + } + + try + { + CreateTrafficPanel(hasMessages); + } + catch (TrafficModelError const & e) + { + QMessageBox::critical(this, "Data loading error", QString("Can't create traffic panel.")); + LOG(LERROR, (e.Msg())); + return; + } + +#ifdef openlr_obsolete + m_goldifyMatchedPathAction->setEnabled(true /* enabled */); + m_closeTrafficSampleAction->setEnabled(true /* enabled */); + m_saveTrafficSampleAction->setEnabled(true /* enabled */); + m_startEditingAction->setEnabled(true /* enabled */); + m_ignorePathAction->setEnabled(true /* enabled */); +#endif +} + +void MainWindow::OnPurgeExpiredMessages() +{ + m_framework.GetTrafficManager().PurgeExpiredMessages(); +} + +void MainWindow::OnClearCache() +{ + m_trafficPanel->GetTimer().Reset(); + m_framework.GetTrafficManager().Clear(); +} + +void MainWindow::OnCloseTrafficSample() +{ + // TODO(mgsergio): + // If not saved, ask a user if he/she wants to save. + // OnSaveTrafficSample() + +#ifdef openlr_obsolete + m_goldifyMatchedPathAction->setEnabled(false /* enabled */); + m_saveTrafficSampleAction->setEnabled(false /* enabled */); + m_closeTrafficSampleAction->setEnabled(false /* enabled */); + m_startEditingAction->setEnabled(false /* enabled */); + m_commitPathAction->setEnabled(false /* enabled */); + m_cancelPathAction->setEnabled(false /* enabled */); + m_ignorePathAction->setEnabled(false /* enabled */); +#endif + + DestroyTrafficPanel(); +} + +void MainWindow::OnSaveTrafficSample() +{ + // TODO(mgsergio): Add default filename. + auto const & fileName = QFileDialog::getSaveFileName(this, "Save sample"); + if (fileName.isEmpty()) + return; + + pugi::xml_document document; + + auto const messageCache = m_framework.GetTrafficManager().GetMessageCache(); + + traffxml::GenerateTraff(messageCache, document); + document.save_file(fileName.toStdString().data(), " " /* indent */); + +#ifdef openlr_obsolete + if (!m_trafficModel->SaveSampleAs(fileName.toStdString())) + { + QMessageBox::critical( + this, "Saving error", + QString("Can't save file: ") + strerror(errno)); + } +#endif +} + +void MainWindow::OnPathEditingStop() +{ +#ifdef openlr_obsolete + m_commitPathAction->setEnabled(false /* enabled */); + m_cancelPathAction->setEnabled(false /* enabled */); + m_cancelPathAction->setEnabled(false /* enabled */); +#endif +} +} // namespace traffxml diff --git a/tools/traff_assessment_tool/mainwindow.hpp b/tools/traff_assessment_tool/mainwindow.hpp new file mode 100644 index 000000000..2e6440ce3 --- /dev/null +++ b/tools/traff_assessment_tool/mainwindow.hpp @@ -0,0 +1,80 @@ +#pragma once + +#include "base/string_utils.hpp" + +#include "traff_assessment_tool/traffic_panel.hpp" + +#include + +#include + +class Framework; +class QHBoxLayout; + +namespace traffxml +{ +class MapWidget; +class TrafficModel; +class WebView; +} + +namespace df +{ +class DrapeApi; +} + +class QDockWidget; + +namespace traffxml +{ +class MainWindow : public QMainWindow +{ + Q_OBJECT + +public: + explicit MainWindow(Framework & framework); + +private: + void CreateTrafficPanel(bool hasMessages); + void DestroyTrafficPanel(); + + /** + * Called when the user requests to open a sample file. + */ + void OnOpenTrafficSample(); + + /** + * Called when the user requests to purge expired messages. + */ + void OnPurgeExpiredMessages(); + + /** + * Called when the user requests to clear the cache. + */ + void OnClearCache(); + void OnCloseTrafficSample(); + void OnSaveTrafficSample(); + void OnPathEditingStop(); + + Framework & m_framework; + + traffxml::TrafficModel * m_trafficModel = nullptr; + QDockWidget * m_dockWidget = nullptr; + TrafficPanel * m_trafficPanel = nullptr; + +#ifdef openlr_obsolete + QAction * m_goldifyMatchedPathAction = nullptr; +#endif + QAction * m_saveTrafficSampleAction = nullptr; + QAction * m_closeTrafficSampleAction = nullptr; +#ifdef openlr_obsolete + QAction * m_startEditingAction = nullptr; + QAction * m_commitPathAction = nullptr; + QAction * m_cancelPathAction = nullptr; + QAction * m_ignorePathAction = nullptr; +#endif + + traffxml::MapWidget * m_mapWidget = nullptr; + QHBoxLayout * m_layout = nullptr; +}; +} // namespace traffxml diff --git a/tools/traff_assessment_tool/map_widget.cpp b/tools/traff_assessment_tool/map_widget.cpp new file mode 100644 index 000000000..59601ae88 --- /dev/null +++ b/tools/traff_assessment_tool/map_widget.cpp @@ -0,0 +1,29 @@ +#include "traff_assessment_tool/map_widget.hpp" + +#include "qt/qt_common/helpers.hpp" + +#include "map/framework.hpp" + +#include + +namespace traffxml +{ +MapWidget::MapWidget(Framework & framework, QWidget * parent) + : Base(framework, false /* screenshotMode */, parent) +{ +} + +void MapWidget::mousePressEvent(QMouseEvent * e) +{ + Base::mousePressEvent(e); + + if (qt::common::IsRightButton(e)) + ShowInfoPopup(e, GetDevicePoint(e)); + + if (m_mode == Mode::TrafficMarkup) + { + auto pt = GetDevicePoint(e); + emit TrafficMarkupClick(m_framework.PtoG(pt), e->button()); + } +} +} // namespace traffxml diff --git a/tools/traff_assessment_tool/map_widget.hpp b/tools/traff_assessment_tool/map_widget.hpp new file mode 100644 index 000000000..7931fd1b0 --- /dev/null +++ b/tools/traff_assessment_tool/map_widget.hpp @@ -0,0 +1,46 @@ +#pragma once + +#include "qt/qt_common/map_widget.hpp" + +namespace +{ +class PointsController; +} // namespace + +class Framework; + +namespace traffxml +{ +class MapWidget : public qt::common::MapWidget +{ + Q_OBJECT + + using Base = qt::common::MapWidget; + +public: + enum class Mode + { + Normal, + TrafficMarkup + }; + + MapWidget(Framework & framework, QWidget * parent); + ~MapWidget() override = default; + + void SetMode(Mode const mode) { m_mode = mode; } + + QSize sizeHint() const override + { + return QSize(800, 600); + } + +signals: + void TrafficMarkupClick(m2::PointD const & p, Qt::MouseButton const b); + +protected: + void mousePressEvent(QMouseEvent * e) override; + +private: + Mode m_mode = Mode::Normal; +}; +} // namespace traffxml diff --git a/tools/traff_assessment_tool/points_controller_delegate_base.hpp b/tools/traff_assessment_tool/points_controller_delegate_base.hpp new file mode 100644 index 000000000..5f838bfa5 --- /dev/null +++ b/tools/traff_assessment_tool/points_controller_delegate_base.hpp @@ -0,0 +1,40 @@ +#pragma once + +#include "indexer/feature.hpp" + +#include "geometry/point2d.hpp" + +#include +#include + +namespace traffxml +{ +using FeaturePoint = std::pair; + +/// This class is responsible for collecting junction points and +/// checking user's clicks. +class PointsControllerDelegateBase +{ +public: + enum class ClickType + { + Miss, + Add, + Remove + }; + + virtual ~PointsControllerDelegateBase() = default; + + virtual std::vector GetAllJunctionPointsInViewport() const = 0; + /// Returns all junction points at a given location in the form of feature id and + /// point index in the feature. + virtual std::pair, m2::PointD> GetCandidatePoints( + m2::PointD const & p) const = 0; + // Returns all points that are one step reachable from |p|. + virtual std::vector GetReachablePoints(m2::PointD const & p) const = 0; + + virtual ClickType CheckClick(m2::PointD const & clickPoint, + m2::PointD const & lastClickedPoint, + std::vector const & reachablePoints) const = 0; +}; +} // namespace traffxml diff --git a/tools/traff_assessment_tool/resumable_timer.cpp b/tools/traff_assessment_tool/resumable_timer.cpp new file mode 100644 index 000000000..a3e93d4d1 --- /dev/null +++ b/tools/traff_assessment_tool/resumable_timer.cpp @@ -0,0 +1,35 @@ +#include "traff_assessment_tool/resumable_timer.hpp" + +namespace base +{ +std::chrono::steady_clock::duration ResumableTimer::TimeElapsed() const +{ + if (m_isRunning) + return std::chrono::steady_clock::now() - m_startTime; + else + return m_prevTimeElapsed; +} + +void ResumableTimer::Pause() +{ + if (!m_isRunning) + return; + m_prevTimeElapsed = TimeElapsed(); + m_isRunning = false; +} + +void ResumableTimer::Resume() +{ + if (m_isRunning) + return; + m_startTime = std::chrono::steady_clock::now() - m_prevTimeElapsed; + m_prevTimeElapsed = std::chrono::steady_clock::duration::zero(); + m_isRunning = true; +} + +void ResumableTimer::Reset() +{ + m_startTime = std::chrono::steady_clock::now(); + m_prevTimeElapsed = std::chrono::steady_clock::duration::zero(); +} +} // namespace base diff --git a/tools/traff_assessment_tool/resumable_timer.hpp b/tools/traff_assessment_tool/resumable_timer.hpp new file mode 100644 index 000000000..f09a46ab3 --- /dev/null +++ b/tools/traff_assessment_tool/resumable_timer.hpp @@ -0,0 +1,81 @@ +#pragma once + +#include + +namespace base +{ +/** + * @brief A timer which can be paused and resumed. + * + * On creation, the timer is in paused state. + * + * Elapsed time can be queried in any state (running or paused). In running state, it will return + * the currently elapsed time, and the result will increase with each subsequent call. In paused + * state, it will return the time elapsed when the timer was last paused, and the result is stable + * between calls as long as the timer remains paused. + */ +class ResumableTimer +{ +public: + std::chrono::steady_clock::duration TimeElapsed() const; + + template + Duration TimeElapsedAs() const + { + return std::chrono::duration_cast(TimeElapsed()); + } + + double ElapsedSeconds() const { return TimeElapsedAs>().count(); } + uint64_t ElapsedMilliseconds() const { return TimeElapsedAs().count(); } + uint64_t ElapsedNanoseconds() const { return TimeElapsedAs().count(); } + + /** + * @brief Pauses the timer, i.e. freezes its current value and stops it from advancing further. + * + * Pausing an already paused timer is a no-op. + */ + void Pause(); + + /** + * @brief Resumes the timer, i.e. causes it to advance further from its previous value. + * + * Resuming a running timer is a no-op. + */ + void Resume(); + + /** + * @brief Resets the timer to zero. + * + * Resetting the timer will not change its paused/running state. That is, resetting a running + * timer will cause it to count upward from zero immediately, whereas resetting a paused timer + * will set its value to zero and keep it there until resumed. + */ + void Reset(); +private: + /** + * @brief The time at which the timer was last started. + * + * Valid only if the timer is running. + * + * When first starting a timer after it has been created or reset, this value is set to current + * time. When resuming a timer, this value us set to current time minus `m_prevTimeElapsed`, so + * that the timer value for a currently running timer is always the difference between this value + * and current time. + */ + std::chrono::steady_clock::time_point m_startTime; + + /** + * @brief Whether the timer is currently running. + */ + bool m_isRunning = false; + + /** + * @brief The time elapsed when the timer was last paused. + * + * Valid only if the timer is paused. + * + * Initially zero, reset to zero when the timer is reset or resumed. + */ + std::chrono::steady_clock::duration m_prevTimeElapsed = std::chrono::steady_clock::duration::zero(); +}; +} // namespace base diff --git a/tools/traff_assessment_tool/traffic_drawer_delegate_base.hpp b/tools/traff_assessment_tool/traffic_drawer_delegate_base.hpp new file mode 100644 index 000000000..756bf1903 --- /dev/null +++ b/tools/traff_assessment_tool/traffic_drawer_delegate_base.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include + +namespace traffxml +{ +/// This class is used to delegate segments drawing to the DrapeEngine. +class TrafficDrawerDelegateBase +{ +public: + virtual ~TrafficDrawerDelegateBase() = default; + + virtual void SetViewportCenter(m2::PointD const & center) = 0; + + virtual void DrawDecodedSegments(std::vector const & points) = 0; + virtual void DrawEncodedSegment(std::vector const & points) = 0; + virtual void DrawGoldenPath(std::vector const & points) = 0; + + virtual void ClearGoldenPath() = 0; + virtual void ClearAllPaths() = 0; + + virtual void VisualizePoints(std::vector const & points) = 0; + virtual void ClearAllVisualizedPoints() = 0; +}; +} // namespace traffxml diff --git a/tools/traff_assessment_tool/traffic_model.cpp b/tools/traff_assessment_tool/traffic_model.cpp new file mode 100644 index 000000000..db89dadd7 --- /dev/null +++ b/tools/traff_assessment_tool/traffic_model.cpp @@ -0,0 +1,812 @@ +#include "traffic_model.hpp" + +#ifdef openlr_obsolete +#include "openlr/openlr_model_xml.hpp" +#endif + +#include "drape_frontend/drape_api.hpp" + +#include "indexer/data_source.hpp" + +#include "map/framework.hpp" + +#include "base/assert.hpp" +#include "base/scope_guard.hpp" + +#include +#include + +namespace traffxml +{ + +constexpr static dp::Color kColorFrom(0x309302ff); +constexpr static dp::Color kColorAt(0x1a5ec1ff); +constexpr static dp::Color kColorVia(0xf19721ff); +constexpr static dp::Color kColorNotVia(0x8c5678ff); +constexpr static dp::Color kColorTo(0xe42300ff); +constexpr static dp::Color kColorDecoded(0x4070ffff); +constexpr static std::string const kDecodedLineId = "decodedPath"; + +namespace +{ +void RemovePointFromPull(m2::PointD const & toBeRemoved, std::vector & pool) +{ + pool.erase( + remove_if(begin(pool), end(pool), + [&toBeRemoved](m2::PointD const & p) { return p.EqualDxDy(toBeRemoved, 1e-6); }), + end(pool)); +} + +std::vector GetReachablePoints(m2::PointD const & srcPoint, + std::vector const path, + PointsControllerDelegateBase const & pointsDelegate, + size_t const lookbackIndex) +{ + auto reachablePoints = pointsDelegate.GetReachablePoints(srcPoint); + if (lookbackIndex < path.size()) + { + auto const & toBeRemoved = path[path.size() - lookbackIndex - 1]; + RemovePointFromPull(toBeRemoved, reachablePoints); + } + return reachablePoints; +} +} // namespace + +#ifdef openlr_obsolete +namespace impl +{ +// static +size_t const RoadPointCandidate::kInvalidId = std::numeric_limits::max(); + +/// This class denotes a "non-deterministic" feature point. +/// I.e. it is a set of all pairs +/// located at a specified coordinate. +/// Only one point at a time is considered active. +RoadPointCandidate::RoadPointCandidate(std::vector const & points, + m2::PointD const & coord) + : m_coord(coord) + , m_points(points) +{ + LOG(LDEBUG, ("Candidate points:", points)); +} + +void RoadPointCandidate::ActivateCommonPoint(RoadPointCandidate const & rpc) +{ + for (auto const & fp1 : m_points) + { + for (auto const & fp2 : rpc.m_points) + { + if (fp1.first == fp2.first) + { + SetActivePoint(fp1.first); + return; + } + } + } + CHECK(false, ("One common feature id should exist.")); +} + +FeaturePoint const & RoadPointCandidate::GetPoint() const +{ + CHECK_NOT_EQUAL(m_activePointIndex, kInvalidId, ("No point is active.")); + return m_points[m_activePointIndex]; +} + +m2::PointD const & RoadPointCandidate::GetCoordinate() const +{ + return m_coord; +} + +void RoadPointCandidate::SetActivePoint(FeatureID const & fid) +{ + for (size_t i = 0; i < m_points.size(); ++i) + { + if (m_points[i].first == fid) + { + m_activePointIndex = i; + return; + } + } + CHECK(false, ("One point should match.")); +} +} // namespace impl +#endif + +QVariant GetCountryAndRoadRef(TraffMessage const & message) +{ + std::string result = ""; + if (message.m_location) + { + if (message.m_location.value().m_country) + result += message.m_location.value().m_country.value(); + if (message.m_location.value().m_roadRef) + { + if (!result.empty()) + result += '\n'; + result += message.m_location.value().m_roadRef.value(); + } + if (message.m_location.value().m_fuzziness) + { + if (!result.empty()) + result += '\n'; + switch (message.m_location.value().m_fuzziness.value()) + { + case traffxml::Fuzziness::LowRes: + result += "LowRes"; + break; + case traffxml::Fuzziness::MediumRes: + result += "MediumRes"; + break; + case traffxml::Fuzziness::EndUnknown: + result += "EndUnknown"; + break; + case traffxml::Fuzziness::StartUnknown: + result += "StartUnknown"; + break; + case traffxml::Fuzziness::ExtentUnknown: + result += "ExtentUnknown"; + break; + default: + result += "(invalid)"; + break; + } + } + } + return QString::fromStdString(result); +} + +/** + * @brief Returns a descriptive text for a point. + * + * The result is the junction name (if any), followed by the junction number or kilometric point (if any). + * + * @param point + * @return + */ +std::string GetPointDetail(Point const & point) +{ + std::string result = point.m_junctionName ? point.m_junctionName.value() : ""; + std::string junctionRefOrKmp = point.m_junctionRef ? point.m_junctionRef.value() : + point.m_distance ? std::format("km {0:.0f}", point.m_distance.value()) : ""; + if (!junctionRefOrKmp.empty()) + { + if (result.empty()) + result = junctionRefOrKmp; + else + result += " (" + junctionRefOrKmp + ")"; + } + return result; +} + +/** + * @brief Returns a descriptive text for the location on the road. + * + * The result is one or two junctions, with an arrow to indicate direction where applicable. + * + * @param location + * @return + */ +std::string GetLocationDetail(TraffLocation const & location) +{ + if (location.m_at) + return GetPointDetail(location.m_at.value()); + + std::string nameFrom = location.m_from ? GetPointDetail(location.m_from.value()) : ""; + std::string nameTo = location.m_from ? GetPointDetail(location.m_to.value()) : ""; + + if (nameFrom == nameTo) + return nameFrom; + else if (!nameFrom.empty()) + { + if (nameTo.empty()) + return nameFrom + ((location.m_directionality == Directionality::OneDirection) ? " →" : " ↔"); + else + return nameFrom + + ((location.m_directionality == Directionality::OneDirection) ? " → " : " ↔ ") + + nameTo; + } + else if (!nameTo.empty()) + return ((location.m_directionality == Directionality::OneDirection) ? "→ " : "↔ ") + nameTo; + else if (location.m_via) + return GetPointDetail(location.m_via.value()); + return ""; +} + +/** + * @brief Returns a description for the events in the message. + * + * @param message + * @return + */ +std::string GetEventText(TraffMessage const & message) +{ + std::string result = ""; + for (auto const & event : message.m_events) + { + if (!result.empty()) + result += ", "; + result += DebugPrint(event.m_type); + // TODO quantifiers (format "q = {}") + // TODO supplementary information (not in struct yet) + if (event.m_length) + result += std::format(" for {0:d} m", event.m_length.value()); + if (event.m_speed) + result += std::format(", speed {0:d} km/h", event.m_speed.value()); + } + return result; +} + +QVariant GetDescription(TraffMessage const & message) +{ + std::string result = ""; + if (message.m_cancellation) + { + result = "Cancellation"; + } + else + { + if (message.m_location) + { + std::string direction = ""; + if (message.m_location.value().m_directionality == Directionality::BothDirections) + direction = "both directions"; + else + { + // TODO determine bearing and convert it to a string (northbound, southeastbound etc.) + /* + std::optional loc1 = message.m_location.value().m_from; + std::optional loc2 = message.m_location.value().m_to; + if (loc2 && !loc1) + loc1 = message.m_location.value().m_at; + else if (loc1 && !loc2) + loc2 = message.m_location.value().m_at; + if (loc1 && loc2) + { + ms::LatLon c1 = loc1.value().m_coordinates; + ms::LatLon c2 = loc2.value().m_coordinates; + // TODO figure out bearing (as string) + } + */ + } + if (message.m_location.value().m_roadName) + { + // roadName, town, direction + if (message.m_location.value().m_town) + result += message.m_location.value().m_town.value() + ", "; + // TODO territory? + result += message.m_location.value().m_roadName.value(); + if (!direction.empty()) + result += ", " + direction; + } + else if (message.m_location.value().m_origin && message.m_location.value().m_destination) + // origin–destination with arrow + result += message.m_location.value().m_origin.value() + + ((message.m_location.value().m_directionality == Directionality::BothDirections) ? " ↔ " : " → ") + + message.m_location.value().m_destination.value(); + else if (!message.m_location.value().m_origin && !message.m_location.value().m_destination) + // direction, if available + result += direction; + else if (message.m_location.value().m_directionality == Directionality::OneDirection) + { + // unidirectional, one endpoint; replacce the other with direction + if (message.m_location.value().m_origin) + result += message.m_location.value().m_origin.value() + " → " + direction; + else + result += direction + " → " + message.m_location.value().m_destination.value(); + } + else + // direction, if available (no meaningful way to use origin or destination) + result += direction; + + auto locationDetail = GetLocationDetail(message.m_location.value()); + if (!locationDetail.empty()) + { + if (!result.empty()) + result += "\n"; + result += locationDetail; + } + } + auto eventText = GetEventText(message); + if (!eventText.empty()) + { + if (!result.empty()) + result += "\n"; + result += eventText; + } + // TODO start/end date + } + if (!result.empty()) + result += "\n"; + result += message.m_id.substr(0, message.m_id.find(':')); + result += "\t" + DebugPrint(message.m_updateTime); + // add an extra line to get bigger rows (default is too small) + result += "\n"; + return QString::fromStdString(result); +} + +// TrafficModel ------------------------------------------------------------------------------------- +TrafficModel::TrafficModel(Framework & framework, + QObject * parent) + : QAbstractTableModel(parent) + , m_framework(framework) + , m_dataSource(framework.GetDataSource()) + , m_drawerDelegate(std::make_unique(framework)) + , m_pointsDelegate(std::make_unique(framework)) + , m_drapeApi(m_framework.GetDrapeApi()) +{ + framework.GetTrafficManager().SetTrafficUpdateCallbackFn([this, &framework](bool final) { + /* + * If final is true, this indicates the queue has been emptied and no further updates are + * imminent. Such updates should always be processed. If final is false, we can optimize by + * selectively skipping updates. + */ + GetPlatform().RunTask(Platform::Thread::Gui, [this, &framework, final]() + { + beginResetModel(); + auto const messageCache = framework.GetTrafficManager().GetMessageCache(); + m_messages.clear(); + m_messages.reserve(messageCache.size()); + + for (auto & entry : messageCache) + m_messages.push_back(std::move(entry.second)); + + endResetModel(); + + // clear markers + auto editSession = m_framework.GetBookmarkManager().GetEditSession(); + editSession.ClearGroup(UserMark::Type::COLORED); + editSession.SetIsVisible(UserMark::Type::COLORED, false); + m_drapeApi.Clear(); + + // restore status bar + if (final) + m_trafficPanel->SetStatus(false, m_messages.size()); + + LOG(LINFO, ("Messages:", m_messages.size())); + }); + }); +} + +// TODO(mgsergio): Check if a path was committed, or commit it. +bool TrafficModel::SaveSampleAs(std::string const & fileName) const +{ + CHECK(!fileName.empty(), ("Can't save to an empty file.")); + + pugi::xml_document result; + result.reset(m_template); + auto root = result.document_element(); + +#ifdef openlr_obsolete + for (auto const & sc : m_segments) + { + auto segment = root.append_child("Segment"); + segment.append_copy(sc.GetPartnerXMLSegment()); + + if (sc.GetStatus() == SegmentCorrespondence::Status::Ignored) + { + segment.append_child("Ignored").text() = true; + } + if (sc.HasMatchedPath()) + { + auto node = segment.append_child("Route"); + openlr::PathToXML(sc.GetMatchedPath(), node); + } + if (sc.HasFakePath()) + { + auto node = segment.append_child("FakeRoute"); + openlr::PathToXML(sc.GetFakePath(), node); + } + if (sc.HasGoldenPath()) + { + auto node = segment.append_child("GoldenRoute"); + openlr::PathToXML(sc.GetGoldenPath(), node); + } + } +#endif + + result.save_file(fileName.data(), " " /* indent */); + return true; +} + +int TrafficModel::rowCount(const QModelIndex & parent) const +{ + return static_cast(m_messages.size()); +} + +int TrafficModel::columnCount(const QModelIndex & parent) const { return 2; } + +QVariant TrafficModel::data(const QModelIndex & index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + if (index.row() >= rowCount()) + return QVariant(); + + if (role != Qt::DisplayRole && role != Qt::EditRole) + return QVariant(); + +#ifdef openlr_obsolete + if (index.column() == 0) + return m_segments[index.row()].GetPartnerSegmentId(); + + if (index.column() == 1) + return static_cast(m_segments[index.row()].GetStatus()); + + if (index.column() == 2) + return m_segments[index.row()].GetPositiveOffset(); + + if (index.column() == 3) + return m_segments[index.row()].GetNegativeOffset(); +#endif + switch (index.column()) + { + case 0: + return GetCountryAndRoadRef(m_messages[index.row()]); + case 1: + return GetDescription(m_messages[index.row()]); + default: + return QVariant(); + } + + UNREACHABLE(); +} + +QVariant TrafficModel::headerData(int section, Qt::Orientation orientation, + int role /* = Qt::DisplayRole */) const +{ + /* + * Qt seems buggy here. Initially, we seem to get called with Qt::Vertical for a horizontal + * header, i.e. a row of column headers, and Qt::Horizontal for a vertical header (column of row + * headers). Using the intuitively correct value will result in incorrect behavior and a lot of + * head-scratching if you use just one type of header. + * However, this (presumed) bug does not seem to be consistent, as updates call us with + * Qt::Vertical and a row number (which can be beyond the number of columns). + */ + if (orientation == Qt::Horizontal && role != Qt::DisplayRole) + return QVariant(); + + switch (section) + { + case 0: return "Road ref"; break; + case 1: return "Description"; break; + } + return QVariant(); +} + +void TrafficModel::OnItemSelected(QItemSelection const & selected, QItemSelection const &) +{ + ASSERT(!selected.empty(), ()); + ASSERT(!m_messages.empty(), ()); + + auto const row = selected.front().top(); + + auto editSession = m_framework.GetBookmarkManager().GetEditSession(); + editSession.ClearGroup(UserMark::Type::COLORED); + m_drapeApi.Clear(); + + if (static_cast(row) >= m_messages.size()) + { + editSession.SetIsVisible(UserMark::Type::COLORED, false); + return; + } + + auto message = &m_messages[row]; + if (!message->m_location) + { + editSession.SetIsVisible(UserMark::Type::COLORED, false); + return; + } + + m2::RectD rect; + + editSession.SetIsVisible(UserMark::Type::COLORED, true); + + for (auto & [coords, color] : { + std::pair{message->m_location.value().m_from, kColorFrom}, + std::pair{message->m_location.value().m_at, kColorAt}, + std::pair{message->m_location.value().m_via, kColorVia}, + std::pair{message->m_location.value().m_notVia, kColorNotVia}, + std::pair{message->m_location.value().m_to, kColorTo} + }) + if (coords) + { + auto point = mercator::FromLatLon(coords.value().m_coordinates); + rect.Add(point); + auto mark = editSession.CreateUserMark(point); + mark->SetColor(color); + } + + for (auto & [mwmId, coloring] : message->m_decoded) { + FeaturesLoaderGuard g(m_dataSource, mwmId); + for (auto & [rsid, sg]: coloring) { + /* + * Not the most efficient way: even if we get multiple segments of the same feature (which + * is typically the case), we fetch the feature from scratch for each segment, parse its + * geometry, retrieve two points from it and add one line per segment. + * It would be more efficient to group by feature, determine its segment range, then fetch + * the feature, parse its geometries, retrieve all points in range and add one line for the + * entire set of points. + * However, since we do this for just one TraFF message at a time, performance is less of a + * concern here, and implementing the above improvements would make the code more complex. + */ + auto f = g.GetOriginalFeatureByIndex(rsid.m_fid); + f->ParseGeometry(FeatureType::BEST_GEOMETRY); + std::vector points = { + f->GetPoint(rsid.m_idx), + f->GetPoint(rsid.m_idx + 1) + }; + m_drapeApi.AddLine( + kDecodedLineId + ", " + DebugPrint(mwmId) + ", " + DebugPrint(rsid), + df::DrapeApiLineData(points, kColorDecoded).Width(3.0f)); + } + } + + if (rect.IsValid()) + { + rect.Scale(1.5); + m_framework.ShowRect(rect, 15 /* maxScale */, true /* animation */, true /* useVisibleViewport */); + } +} + +Qt::ItemFlags TrafficModel::flags(QModelIndex const & index) const +{ + if (!index.isValid()) + return Qt::ItemIsEnabled; + + return QAbstractItemModel::flags(index); +} + +#ifdef openlr_obsolete +void TrafficModel::GoldifyMatchedPath() +{ + if (!m_currentSegment->HasMatchedPath()) + { + QMessageBox::information(nullptr /* parent */, "Error", + "The selected segment does not have a matched path"); + return; + } + + if (!StartBuildingPathChecks()) + return; + + m_currentSegment->SetGoldenPath(m_currentSegment->GetMatchedPath()); + m_goldenPath.clear(); + m_drawerDelegate->DrawGoldenPath(GetPoints(m_currentSegment->GetGoldenPath())); +} + +void TrafficModel::StartBuildingPath() +{ + if (!StartBuildingPathChecks()) + return; + + m_currentSegment->SetGoldenPath({}); + + m_buildingPath = true; + m_drawerDelegate->ClearGoldenPath(); + m_drawerDelegate->VisualizePoints(m_pointsDelegate->GetAllJunctionPointsInViewport()); +} + +void TrafficModel::PushPoint(m2::PointD const & coord, std::vector const & points) +{ + impl::RoadPointCandidate point(points, coord); + if (!m_goldenPath.empty()) + m_goldenPath.back().ActivateCommonPoint(point); + m_goldenPath.push_back(point); +} + +void TrafficModel::PopPoint() +{ + CHECK(!m_goldenPath.empty(), ("Attempt to pop point from an empty path.")); + m_goldenPath.pop_back(); +} + +void TrafficModel::CommitPath() +{ + CHECK(m_currentSegment, ("No segments selected")); + + if (!m_buildingPath) + MYTHROW(TrafficModelError, ("Path building is not started")); + + SCOPE_GUARD(guard, [this] { emit EditingStopped(); }); + + m_buildingPath = false; + m_drawerDelegate->ClearAllVisualizedPoints(); + + if (m_goldenPath.size() == 1) + { + LOG(LDEBUG, ("Golden path is empty")); + return; + } + + CHECK_GREATER(m_goldenPath.size(), 1, ("Path cannot consist of only one point")); + + // Activate last point. Since no more points will be availabe we link it to the same + // feature as the previous one was linked to. + m_goldenPath.back().ActivateCommonPoint(m_goldenPath[GetPointsCount() - 2]); + + openlr::Path path; + for (size_t i = 1; i < GetPointsCount(); ++i) + { + auto const prevPoint = m_goldenPath[i - 1]; + auto point = m_goldenPath[i]; + + // The start and the end of the edge should lie on the same feature. + point.ActivateCommonPoint(prevPoint); + + auto const & prevFt = prevPoint.GetPoint(); + auto const & ft = point.GetPoint(); + + path.push_back(Edge::MakeReal( + ft.first, prevFt.second < ft.second /* forward */, base::checked_cast(prevFt.second), + geometry::PointWithAltitude(prevPoint.GetCoordinate(), 0 /* altitude */), + geometry::PointWithAltitude(point.GetCoordinate(), 0 /* altitude */))); + } + + m_currentSegment->SetGoldenPath(path); + m_goldenPath.clear(); +} + +void TrafficModel::RollBackPath() +{ + CHECK(m_currentSegment, ("No segments selected")); + CHECK(m_buildingPath, ("No path building is in progress.")); + + m_buildingPath = false; + + // TODO(mgsergio): Add a method for common visual manipulations. + m_drawerDelegate->ClearAllVisualizedPoints(); + m_drawerDelegate->ClearGoldenPath(); + if (m_currentSegment->HasGoldenPath()) + m_drawerDelegate->DrawGoldenPath(GetPoints(m_currentSegment->GetGoldenPath())); + + m_goldenPath.clear(); + emit EditingStopped(); +} + +void TrafficModel::IgnorePath() +{ + CHECK(m_currentSegment, ("No segments selected")); + + if (m_currentSegment->HasGoldenPath()) + { + auto const btn = + QMessageBox::question(nullptr /* parent */, "Override warning", + "The selected segment has a golden path. Do you want to discard it?"); + if (btn == QMessageBox::No) + return; + } + + m_buildingPath = false; + + // TODO(mgsergio): Add a method for common visual manipulations. + m_drawerDelegate->ClearAllVisualizedPoints(); + m_drawerDelegate->ClearGoldenPath(); + + m_currentSegment->Ignore(); + m_goldenPath.clear(); + emit EditingStopped(); +} + +size_t TrafficModel::GetPointsCount() const +{ + return m_goldenPath.size(); +} + +m2::PointD const & TrafficModel::GetPoint(size_t const index) const +{ + return m_goldenPath[index].GetCoordinate(); +} + +m2::PointD const & TrafficModel::GetLastPoint() const +{ + CHECK(!m_goldenPath.empty(), ("Attempt to get point from an empty path.")); + return m_goldenPath.back().GetCoordinate(); +} + +std::vector TrafficModel::GetGoldenPathPoints() const +{ + std::vector coordinates; + for (auto const & roadPoint : m_goldenPath) + coordinates.push_back(roadPoint.GetCoordinate()); + return coordinates; +} + +// TODO(mgsergio): Draw the first point when the path size is 1. +void TrafficModel::HandlePoint(m2::PointD clickPoint, Qt::MouseButton const button) +{ + if (!m_buildingPath) + return; + + auto const currentPathLength = GetPointsCount(); + auto const lastClickedPoint = currentPathLength != 0 + ? GetLastPoint() + : m2::PointD::Zero(); + + auto const & p = m_pointsDelegate->GetCandidatePoints(clickPoint); + auto const & candidatePoints = p.first; + clickPoint = p.second; + if (candidatePoints.empty()) + return; + + auto reachablePoints = GetReachablePoints(clickPoint, GetGoldenPathPoints(), *m_pointsDelegate, + 0 /* lookBackIndex */); + auto const & clickablePoints = currentPathLength != 0 + ? GetReachablePoints(lastClickedPoint, GetGoldenPathPoints(), *m_pointsDelegate, + 1 /* lookbackIndex */) + // TODO(mgsergio): This is not quite correct since view port can change + // since first call to visualize points. But it's ok in general. + : m_pointsDelegate->GetAllJunctionPointsInViewport(); + + using ClickType = PointsControllerDelegateBase::ClickType; + switch (m_pointsDelegate->CheckClick(clickPoint, lastClickedPoint, clickablePoints)) + { + case ClickType::Add: + // TODO(mgsergio): Think of refactoring this with if (accumulator.empty) + // instead of pushing point first ad then removing last selection. + PushPoint(clickPoint, candidatePoints); + + if (currentPathLength > 0) + { + // TODO(mgsergio): Should I remove lastClickedPoint from clickablePoints + // as well? + RemovePointFromPull(lastClickedPoint, reachablePoints); + m_drawerDelegate->DrawGoldenPath(GetGoldenPathPoints()); + } + + m_drawerDelegate->ClearAllVisualizedPoints(); + m_drawerDelegate->VisualizePoints(reachablePoints); + m_drawerDelegate->VisualizePoints({clickPoint}); + break; + case ClickType::Remove: // TODO(mgsergio): Rename this case. + if (button == Qt::MouseButton::LeftButton) // RemovePoint + { + m_drawerDelegate->ClearAllVisualizedPoints(); + m_drawerDelegate->ClearGoldenPath(); + + PopPoint(); + if (m_goldenPath.empty()) + { + m_drawerDelegate->VisualizePoints(m_pointsDelegate->GetAllJunctionPointsInViewport()); + } + else + { + m_drawerDelegate->VisualizePoints(GetReachablePoints( + GetLastPoint(), GetGoldenPathPoints(), *m_pointsDelegate, 1 /* lookBackIndex */)); + } + + if (GetPointsCount() > 1) + m_drawerDelegate->DrawGoldenPath(GetGoldenPathPoints()); + } + else if (button == Qt::MouseButton::RightButton) + { + CommitPath(); + } + break; + case ClickType::Miss: + // TODO(mgsergio): This situation should be handled by checking candidatePoitns.empty() above. + // Not shure though if all cases are handled by that check. + return; + } +} + +bool TrafficModel::StartBuildingPathChecks() const +{ + CHECK(m_currentSegment, ("A segment should be selected before path building is started.")); + + if (m_buildingPath) + MYTHROW(TrafficModelError, ("Path building already in progress.")); + + if (m_currentSegment->HasGoldenPath()) + { + auto const btn = QMessageBox::question( + nullptr /* parent */, "Override warning", + "The selected segment already has a golden path. Do you want to override?"); + if (btn == QMessageBox::No) + return false; + } + + return true; +} +#endif +} // namespace traffxml diff --git a/tools/traff_assessment_tool/traffic_model.hpp b/tools/traff_assessment_tool/traffic_model.hpp new file mode 100644 index 000000000..d8baa50bc --- /dev/null +++ b/tools/traff_assessment_tool/traffic_model.hpp @@ -0,0 +1,152 @@ +#pragma once + +#include "mainwindow.hpp" +#include "points_controller_delegate_base.hpp" +#ifdef openlr_obsolete +#include "segment_correspondence.hpp" +#endif +#include "traffic_drawer_delegate_base.hpp" +#include "traffic_panel.hpp" + +#ifdef openlr_obsolete +#include "openlr/decoded_path.hpp" +#endif +#include "traffxml/traff_model.hpp" + +#include "indexer/data_source.hpp" + +#include "base/exception.hpp" + +#include + +#include +#include +#include + +#include + + +class QItemSelection; +class Selection; + +DECLARE_EXCEPTION(TrafficModelError, RootException); + +namespace traffxml +{ +#ifdef openlr_obsolete +namespace impl +{ +/// This class denotes a "non-deterministic" feature point. +/// I.e. it is a set of all pairs +/// located at a specified coordinate. +/// Only one point at a time is considered active. +class RoadPointCandidate +{ +public: + RoadPointCandidate(std::vector const & points, + m2::PointD const & coord); + + void ActivateCommonPoint(RoadPointCandidate const & rpc); + openlr::FeaturePoint const & GetPoint() const; + m2::PointD const & GetCoordinate() const; + +private: + static size_t const kInvalidId; + + void SetActivePoint(FeatureID const & fid); + + m2::PointD m_coord = m2::PointD::Zero(); + std::vector m_points; + + size_t m_activePointIndex = kInvalidId; +}; +} // namespace impl +#endif + +/// This class is used to map sample ids to real data +/// and change sample evaluations. +class TrafficModel : public QAbstractTableModel +{ + Q_OBJECT + +public: + // TODO(mgsergio): Check we are on the right mwm. I.e. right mwm version and everything. + TrafficModel(Framework & framework, + QObject * parent = Q_NULLPTR); + + bool SaveSampleAs(std::string const & fileName) const; + void SetTrafficPanel(TrafficPanel * panel) { m_trafficPanel = panel; } + + int rowCount(const QModelIndex & parent = QModelIndex()) const Q_DECL_OVERRIDE; + int columnCount(const QModelIndex & parent = QModelIndex()) const Q_DECL_OVERRIDE; + QVariant headerData(int section, Qt::Orientation orientation, + int role = Qt::DisplayRole) const Q_DECL_OVERRIDE; + + QVariant data(const QModelIndex & index, int role) const Q_DECL_OVERRIDE; + + Qt::ItemFlags flags(QModelIndex const & index) const Q_DECL_OVERRIDE; + + bool IsBuildingPath() const { return m_buildingPath; } +#ifdef openlr_obsolete + void GoldifyMatchedPath(); + void StartBuildingPath(); + void PushPoint(m2::PointD const & coord, + std::vector const & points); + void PopPoint(); + void CommitPath(); + void RollBackPath(); + void IgnorePath(); + + size_t GetPointsCount() const; + m2::PointD const & GetPoint(size_t const index) const; + m2::PointD const & GetLastPoint() const; + std::vector GetGoldenPathPoints() const; +#endif + +public slots: + void OnItemSelected(QItemSelection const & selected, QItemSelection const &); + void OnClick(m2::PointD const & clickPoint, Qt::MouseButton const button) + { +#ifdef openlr_obsolete + HandlePoint(clickPoint, button); +#endif + } + +signals: + void EditingStopped(); + void SegmentSelected(int segmentId); + +private: +#ifdef openlr_obsolete + void HandlePoint(m2::PointD clickPoint, Qt::MouseButton const button); + bool StartBuildingPathChecks() const; +#endif + + Framework & m_framework; + DataSource const & m_dataSource; + df::DrapeApi & m_drapeApi; + TrafficPanel * m_trafficPanel = nullptr; +#ifdef openlr_obsolete + std::vector m_segments; + // Non-owning pointer to an element of m_segments. + SegmentCorrespondence * m_currentSegment = nullptr; +#endif + std::vector m_messages; + + /** + * Non-owning pointer to an element of m_messages. + */ + TraffMessage * m_message = nullptr; + + std::unique_ptr m_drawerDelegate; + std::unique_ptr m_pointsDelegate; + + bool m_buildingPath = false; +#ifdef openlr_obsolete + std::vector m_goldenPath; +#endif + + // Clone this document and add things to its clone when saving sample. + pugi::xml_document m_template; +}; +} // namespace traffxml diff --git a/tools/traff_assessment_tool/traffic_panel.cpp b/tools/traff_assessment_tool/traffic_panel.cpp new file mode 100644 index 000000000..c72898b6e --- /dev/null +++ b/tools/traff_assessment_tool/traffic_panel.cpp @@ -0,0 +1,113 @@ +#include "traff_assessment_tool/traffic_panel.hpp" + +#include +#include +#include +#include +#include +#include + +namespace traffxml +{ +// ComboBoxDelegate -------------------------------------------------------------------------------- +ComboBoxDelegate::ComboBoxDelegate(QObject * parent) + : QStyledItemDelegate(parent) +{ +} + +QWidget * ComboBoxDelegate::createEditor(QWidget * parent, QStyleOptionViewItem const & option, + QModelIndex const & index) const +{ + auto * editor = new QComboBox(parent); + editor->setFrame(false); + editor->setEditable(false); + editor->addItems({"Unevaluated", "Positive", "Negative", "RelPositive", "RelNegative", "Ignore"}); + + return editor; +} + +void ComboBoxDelegate::setEditorData(QWidget * editor, QModelIndex const & index) const +{ + auto const value = index.model()->data(index, Qt::EditRole).toString(); + static_cast(editor)->setCurrentText(value); +} + +void ComboBoxDelegate::setModelData(QWidget * editor, QAbstractItemModel * model, + QModelIndex const & index) const +{ + model->setData(index, static_cast(editor)->currentText(), Qt::EditRole); +} + +void ComboBoxDelegate::updateEditorGeometry(QWidget * editor, QStyleOptionViewItem const & option, + QModelIndex const & index) const +{ + editor->setGeometry(option.rect); +} + +// TrafficPanel ------------------------------------------------------------------------------------ +TrafficPanel::TrafficPanel(QAbstractItemModel * trafficModel, QWidget * parent) + : QWidget(parent) +{ + CreateTable(trafficModel); + + auto * layout = new QVBoxLayout(); + layout->addWidget(m_table); + m_progressBar = new QProgressBar(); + m_progressBar->setMinimum(0); + m_progressBar->setMaximum(0); + layout->addWidget(m_progressBar); + m_status = new QLabel("0 messages"); + layout->addWidget(m_status); + m_progressBar->hide(); + setLayout(layout); + + // Select first segment by default; + auto const & index = m_table->model()->index(0, 0); + m_table->selectionModel()->select(index, QItemSelectionModel::Select); +} + +void TrafficPanel::SetStatus(bool inProgress, std::optional messageCount) +{ + if (inProgress) + { + m_status->hide(); + m_progressBar->show(); + } + else + { + GetTimer().Pause(); + if (messageCount) + m_status->setText(QString("Messages: %1\tDecoded in %2 s").arg(messageCount.value()).arg(GetTimer().ElapsedSeconds())); + m_progressBar->hide(); + m_status->show(); + } +} + +void TrafficPanel::CreateTable(QAbstractItemModel * trafficModel) +{ + m_table = new QTableView(); + m_table->setFocusPolicy(Qt::NoFocus); + m_table->setAlternatingRowColors(true); + m_table->setShowGrid(false); + m_table->setSelectionBehavior(QAbstractItemView::SelectionBehavior::SelectRows); + m_table->setSelectionMode(QAbstractItemView::SelectionMode::SingleSelection); + m_table->setModel(trafficModel); + m_table->setItemDelegate(new ComboBoxDelegate()); + + // the model must be set before we can set dimensions and headers + m_table->verticalHeader()->setVisible(false); + m_table->horizontalHeader()->setVisible(true); + //m_table->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); + m_table->setColumnWidth(0, 80); + m_table->setColumnWidth(1, 300); + + connect(m_table->selectionModel(), + SIGNAL(selectionChanged(QItemSelection const &, QItemSelection const &)), + trafficModel, SLOT(OnItemSelected(QItemSelection const &, QItemSelection const &))); + connect(trafficModel, &QAbstractItemModel::modelReset, + m_table, [this]() { + m_table->resizeRowsToContents(); + //m_table->resizeColumnsToContents(); + }); +} +} // namespace traffxml diff --git a/tools/traff_assessment_tool/traffic_panel.hpp b/tools/traff_assessment_tool/traffic_panel.hpp new file mode 100644 index 000000000..a568d504a --- /dev/null +++ b/tools/traff_assessment_tool/traffic_panel.hpp @@ -0,0 +1,59 @@ +#pragma once + +#include "traff_assessment_tool/resumable_timer.hpp" + +#include +#include +#include + +class QAbstractItemModel; +class QComboBox; +class QTableView; +class QWidget; + +namespace traffxml +{ +class ComboBoxDelegate : public QStyledItemDelegate +{ + Q_OBJECT + +public: + ComboBoxDelegate(QObject * parent = 0); + + QWidget * createEditor(QWidget * parent, QStyleOptionViewItem const & option, + QModelIndex const & index) const Q_DECL_OVERRIDE; + + void setEditorData(QWidget * editor, QModelIndex const & index) const Q_DECL_OVERRIDE; + + void setModelData(QWidget * editor, QAbstractItemModel * model, + QModelIndex const & index) const Q_DECL_OVERRIDE; + + void updateEditorGeometry(QWidget * editor, QStyleOptionViewItem const & option, + QModelIndex const & index) const Q_DECL_OVERRIDE; +}; + +class TrafficPanel : public QWidget +{ + Q_OBJECT + +public: + explicit TrafficPanel(QAbstractItemModel * trafficModel, QWidget * parent); + base::ResumableTimer & GetTimer() { return m_timer; } + void SetStatus(bool inProgress, std::optional messageCount = std::nullopt); + +private: + void CreateTable(QAbstractItemModel * trafficModel); + void FillTable(); + +signals: + +public slots: + // void OnCheckBoxClicked(int row, int state); + +private: + QTableView * m_table = Q_NULLPTR; + QProgressBar * m_progressBar = Q_NULLPTR; + base::ResumableTimer m_timer; + QLabel * m_status = Q_NULLPTR; +}; +} // namespace traffxml diff --git a/tools/traff_assessment_tool/trafficmodeinitdlg.cpp b/tools/traff_assessment_tool/trafficmodeinitdlg.cpp new file mode 100644 index 000000000..b017b4179 --- /dev/null +++ b/tools/traff_assessment_tool/trafficmodeinitdlg.cpp @@ -0,0 +1,59 @@ +#include "traff_assessment_tool/trafficmodeinitdlg.h" +#include "ui_trafficmodeinitdlg.h" + +#include "platform/settings.hpp" + +#include +#include + +#include + +namespace +{ +std::string const kDataFilePath = "LastTraffAssessmentDataFilePath"; +} // namespace + +namespace traffxml +{ +TrafficModeInitDlg::TrafficModeInitDlg(QWidget * parent) : + QDialog(parent), + m_ui(new Ui::TrafficModeInitDlg) +{ + m_ui->setupUi(this); + + QString directory = {}; + std::string lastDataFilePath; + if (settings::Get(kDataFilePath, lastDataFilePath)) + { + m_ui->dataFileName->setText(QString::fromStdString(lastDataFilePath)); + directory = QFileInfo(QString::fromStdString(lastDataFilePath)).absolutePath(); + } + + connect(m_ui->chooseDataFileButton, &QPushButton::clicked, [this, directory](bool) { + SetFilePathViaDialog(*m_ui->dataFileName, tr("Choose data file"), directory, "*.xml"); + }); +} + +TrafficModeInitDlg::~TrafficModeInitDlg() +{ + delete m_ui; +} + +void TrafficModeInitDlg::accept() +{ + m_dataFileName = m_ui->dataFileName->text().trimmed().toStdString(); + settings::Set(kDataFilePath, m_dataFileName); + QDialog::accept(); +} + +void TrafficModeInitDlg::SetFilePathViaDialog(QLineEdit & dest, QString const & title, + QString const & directory, QString const & filter) +{ + QFileDialog openFileDlg(nullptr, title, directory, filter); + openFileDlg.exec(); + if (openFileDlg.result() != QDialog::DialogCode::Accepted) + return; + + dest.setText(openFileDlg.selectedFiles().first()); +} +} // namespace traffxml diff --git a/tools/traff_assessment_tool/trafficmodeinitdlg.h b/tools/traff_assessment_tool/trafficmodeinitdlg.h new file mode 100644 index 000000000..5377ee444 --- /dev/null +++ b/tools/traff_assessment_tool/trafficmodeinitdlg.h @@ -0,0 +1,36 @@ +#pragma once + +#include + +#include + +class QLineEdit; + +namespace Ui { +class TrafficModeInitDlg; +} + +namespace traffxml +{ +class TrafficModeInitDlg : public QDialog +{ + Q_OBJECT + +public: + explicit TrafficModeInitDlg(QWidget * parent = nullptr); + ~TrafficModeInitDlg(); + + std::string GetDataFilePath() const { return m_dataFileName; } + +private: + void SetFilePathViaDialog(QLineEdit & dest, QString const & title, const QString & directory, + QString const & filter = {}); +public slots: + void accept() override; + +private: + Ui::TrafficModeInitDlg * m_ui; + + std::string m_dataFileName; +}; +} // namespace traffxml diff --git a/tools/traff_assessment_tool/trafficmodeinitdlg.ui b/tools/traff_assessment_tool/trafficmodeinitdlg.ui new file mode 100644 index 000000000..441d771c2 --- /dev/null +++ b/tools/traff_assessment_tool/trafficmodeinitdlg.ui @@ -0,0 +1,114 @@ + + + TrafficModeInitDlg + + + + 0 + 0 + 482 + 122 + + + + + 0 + 0 + + + + Select traffic files + + + true + + + + + 20 + 10 + 441 + 101 + + + + + + + Cancel + + + + + + + Choose... + + + + + + + Ok + + + true + + + + + + + + + + true + + + <html><head/><body><p>Data file:</p></body></html> + + + + + + + + dataFileName + chooseDataFileButton + + + + + cancelButton + clicked() + TrafficModeInitDlg + reject() + + + 290 + 103 + + + 164 + 98 + + + + + okButton + clicked() + TrafficModeInitDlg + accept() + + + 405 + 105 + + + 59 + 94 + + + + + diff --git a/xcode/CoMaps.xcworkspace/contents.xcworkspacedata b/xcode/CoMaps.xcworkspace/contents.xcworkspacedata index 292dc0328..7dec07172 100644 --- a/xcode/CoMaps.xcworkspace/contents.xcworkspacedata +++ b/xcode/CoMaps.xcworkspace/contents.xcworkspacedata @@ -157,6 +157,9 @@ + + diff --git a/xcode/traffxml/traffxml.xcodeproj/project.pbxproj b/xcode/traffxml/traffxml.xcodeproj/project.pbxproj new file mode 100644 index 000000000..6f731cdd9 --- /dev/null +++ b/xcode/traffxml/traffxml.xcodeproj/project.pbxproj @@ -0,0 +1,266 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 2747204F2E439E3000C516DF /* traff_decoder.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 2747203F2E439DDE00C516DF /* traff_decoder.cpp */; }; + 274720502E439E3000C516DF /* traff_source.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 274720452E439DDE00C516DF /* traff_source.cpp */; }; + 274720512E439E3000C516DF /* traff_model.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 274720412E439DDE00C516DF /* traff_model.cpp */; }; + 274720522E439E3000C516DF /* traff_model_xml.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 274720432E439DDE00C516DF /* traff_model_xml.cpp */; }; + 274720532E439E3000C516DF /* traff_storage.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 274720472E439DDE00C516DF /* traff_storage.cpp */; }; + 274720542E439E3000C516DF /* traff_decoder.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 2747203E2E439DDE00C516DF /* traff_decoder.hpp */; }; + 274720552E439E3000C516DF /* traff_model.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 274720402E439DDE00C516DF /* traff_model.hpp */; }; + 274720562E439E3000C516DF /* traff_storage.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 274720462E439DDE00C516DF /* traff_storage.hpp */; }; + 274720572E439E3000C516DF /* traff_source.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 274720442E439DDE00C516DF /* traff_source.hpp */; }; + 274720582E439E3000C516DF /* traff_model_xml.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 274720422E439DDE00C516DF /* traff_model_xml.hpp */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 2747203E2E439DDE00C516DF /* traff_decoder.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = traff_decoder.hpp; sourceTree = ""; }; + 2747203F2E439DDE00C516DF /* traff_decoder.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = traff_decoder.cpp; sourceTree = ""; }; + 274720402E439DDE00C516DF /* traff_model.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = traff_model.hpp; sourceTree = ""; }; + 274720412E439DDE00C516DF /* traff_model.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = traff_model.cpp; sourceTree = ""; }; + 274720422E439DDE00C516DF /* traff_model_xml.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = traff_model_xml.hpp; sourceTree = ""; }; + 274720432E439DDE00C516DF /* traff_model_xml.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = traff_model_xml.cpp; sourceTree = ""; }; + 274720442E439DDE00C516DF /* traff_source.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = traff_source.hpp; sourceTree = ""; }; + 274720452E439DDE00C516DF /* traff_source.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = traff_source.cpp; sourceTree = ""; }; + 274720462E439DDE00C516DF /* traff_storage.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = traff_storage.hpp; sourceTree = ""; }; + 274720472E439DDE00C516DF /* traff_storage.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = traff_storage.cpp; sourceTree = ""; }; + 67BECB4A1DDA43AF00FC4E99 /* libtraffxml.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libtraffxml.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 67BECB581DDA43FB00FC4E99 /* common-debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "common-debug.xcconfig"; path = "../common-debug.xcconfig"; sourceTree = ""; }; + 67BECB591DDA440100FC4E99 /* common-release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "common-release.xcconfig"; path = "../common-release.xcconfig"; sourceTree = ""; }; + FAFD413F26BBE3FC00D1DE1B /* libbase.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = libbase.a; sourceTree = BUILT_PRODUCTS_DIR; }; + FAFD414126BBE3FC00D1DE1B /* libcoding.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = libcoding.a; sourceTree = BUILT_PRODUCTS_DIR; }; + FAFD414326BBE88000D1DE1B /* libplatform.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = libplatform.a; sourceTree = BUILT_PRODUCTS_DIR; }; + FAFD414526BBE88700D1DE1B /* libz.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = usr/lib/libz.tbd; sourceTree = SDKROOT; }; + FAFD414926BBE98B00D1DE1B /* libindexer.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = libindexer.a; sourceTree = BUILT_PRODUCTS_DIR; }; + FAFD414B26BBE9BB00D1DE1B /* libicu.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = libicu.a; sourceTree = BUILT_PRODUCTS_DIR; }; + FAFD414D26BBE9C300D1DE1B /* libgeometry.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = libgeometry.a; sourceTree = BUILT_PRODUCTS_DIR; }; + FAFD414F26BBE9C800D1DE1B /* libprotobuf.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = libprotobuf.a; sourceTree = BUILT_PRODUCTS_DIR; }; + FAFD415126BBE9E000D1DE1B /* librouting.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = librouting.a; sourceTree = BUILT_PRODUCTS_DIR; }; + FAFD415326BBE9E800D1DE1B /* libsuccinct.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = libsuccinct.a; sourceTree = BUILT_PRODUCTS_DIR; }; + FAFD415526BBEA5600D1DE1B /* librouting_common.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = librouting_common.a; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 67BECB471DDA43AF00FC4E99 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 67BECB411DDA43AF00FC4E99 = { + isa = PBXGroup; + children = ( + 67BECB581DDA43FB00FC4E99 /* common-debug.xcconfig */, + 67BECB591DDA440100FC4E99 /* common-release.xcconfig */, + 67BECB5A1DDA449C00FC4E99 /* traffxml */, + 67BECB4B1DDA43AF00FC4E99 /* Products */, + 67BECB831DDA474400FC4E99 /* Frameworks */, + ); + indentWidth = 2; + sourceTree = ""; + tabWidth = 2; + }; + 67BECB4B1DDA43AF00FC4E99 /* Products */ = { + isa = PBXGroup; + children = ( + 67BECB4A1DDA43AF00FC4E99 /* libtraffxml.a */, + ); + name = Products; + sourceTree = ""; + }; + 67BECB5A1DDA449C00FC4E99 /* traffxml */ = { + isa = PBXGroup; + children = ( + 2747203E2E439DDE00C516DF /* traff_decoder.hpp */, + 2747203F2E439DDE00C516DF /* traff_decoder.cpp */, + 274720402E439DDE00C516DF /* traff_model.hpp */, + 274720412E439DDE00C516DF /* traff_model.cpp */, + 274720422E439DDE00C516DF /* traff_model_xml.hpp */, + 274720432E439DDE00C516DF /* traff_model_xml.cpp */, + 274720442E439DDE00C516DF /* traff_source.hpp */, + 274720452E439DDE00C516DF /* traff_source.cpp */, + 274720462E439DDE00C516DF /* traff_storage.hpp */, + 274720472E439DDE00C516DF /* traff_storage.cpp */, + ); + name = traffxml; + path = ../../libs/traffxml; + sourceTree = ""; + }; + 67BECB831DDA474400FC4E99 /* Frameworks */ = { + isa = PBXGroup; + children = ( + FAFD415526BBEA5600D1DE1B /* librouting_common.a */, + FAFD415326BBE9E800D1DE1B /* libsuccinct.a */, + FAFD415126BBE9E000D1DE1B /* librouting.a */, + FAFD414F26BBE9C800D1DE1B /* libprotobuf.a */, + FAFD414D26BBE9C300D1DE1B /* libgeometry.a */, + FAFD414B26BBE9BB00D1DE1B /* libicu.a */, + FAFD414926BBE98B00D1DE1B /* libindexer.a */, + FAFD414526BBE88700D1DE1B /* libz.tbd */, + FAFD414326BBE88000D1DE1B /* libplatform.a */, + FAFD413F26BBE3FC00D1DE1B /* libbase.a */, + FAFD414126BBE3FC00D1DE1B /* libcoding.a */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 67BECB481DDA43AF00FC4E99 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 274720542E439E3000C516DF /* traff_decoder.hpp in Headers */, + 274720552E439E3000C516DF /* traff_model.hpp in Headers */, + 274720562E439E3000C516DF /* traff_storage.hpp in Headers */, + 274720572E439E3000C516DF /* traff_source.hpp in Headers */, + 274720582E439E3000C516DF /* traff_model_xml.hpp in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 67BECB491DDA43AF00FC4E99 /* traffxml */ = { + isa = PBXNativeTarget; + buildConfigurationList = 67BECB551DDA43B000FC4E99 /* Build configuration list for PBXNativeTarget "traffxml" */; + buildPhases = ( + 67BECB461DDA43AF00FC4E99 /* Sources */, + 67BECB471DDA43AF00FC4E99 /* Frameworks */, + 67BECB481DDA43AF00FC4E99 /* Headers */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = traffxml; + productName = traffxml; + productReference = 67BECB4A1DDA43AF00FC4E99 /* libtraffxml.a */; + productType = "com.apple.product-type.library.static"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 67BECB421DDA43AF00FC4E99 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + DefaultBuildSystemTypeForWorkspace = Latest; + LastUpgradeCheck = 1640; + TargetAttributes = { + 67BECB491DDA43AF00FC4E99 = { + CreatedOnToolsVersion = 8.1; + ProvisioningStyle = Automatic; + }; + }; + }; + buildConfigurationList = 67BECB451DDA43AF00FC4E99 /* Build configuration list for PBXProject "traffxml" */; + compatibilityVersion = "Xcode 12.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 67BECB411DDA43AF00FC4E99; + productRefGroup = 67BECB4B1DDA43AF00FC4E99 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 67BECB491DDA43AF00FC4E99 /* traffxml */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXSourcesBuildPhase section */ + 67BECB461DDA43AF00FC4E99 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2747204F2E439E3000C516DF /* traff_decoder.cpp in Sources */, + 274720502E439E3000C516DF /* traff_source.cpp in Sources */, + 274720512E439E3000C516DF /* traff_model.cpp in Sources */, + 274720522E439E3000C516DF /* traff_model_xml.cpp in Sources */, + 274720532E439E3000C516DF /* traff_storage.cpp in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 67BECB531DDA43B000FC4E99 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 67BECB581DDA43FB00FC4E99 /* common-debug.xcconfig */; + buildSettings = { + }; + name = Debug; + }; + 67BECB541DDA43B000FC4E99 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 67BECB591DDA440100FC4E99 /* common-release.xcconfig */; + buildSettings = { + }; + name = Release; + }; + 67BECB561DDA43B000FC4E99 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + EXECUTABLE_PREFIX = lib; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "$(OMIM_ROOT)/3party/pugixml/pugixml/src", + ); + MACOSX_DEPLOYMENT_TARGET = 11.0; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 67BECB571DDA43B000FC4E99 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + EXECUTABLE_PREFIX = lib; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "$(OMIM_ROOT)/3party/pugixml/pugixml/src", + ); + MACOSX_DEPLOYMENT_TARGET = 11.0; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 67BECB451DDA43AF00FC4E99 /* Build configuration list for PBXProject "traffxml" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 67BECB531DDA43B000FC4E99 /* Debug */, + 67BECB541DDA43B000FC4E99 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 67BECB551DDA43B000FC4E99 /* Build configuration list for PBXNativeTarget "traffxml" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 67BECB561DDA43B000FC4E99 /* Debug */, + 67BECB571DDA43B000FC4E99 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 67BECB421DDA43AF00FC4E99 /* Project object */; +}