Files
MapLibreParticleLab/platform/android/src/text/collator.cpp
Chris Loer 11c333672f [android] Android "collator" implementation
- Uses java.text.Collator for comparison
 - Uses java.util.Locale for locale loading
 - Uses LanguageTag for BCP 47 parsing
 - Falls back to non-locale-aware nunicode/default comparison for case-sensitive/diacritic-insensitive.
 - Testing these changes depends on running Android render tests
 - "collator" is not yet exposed in the SDK bindings.
2018-07-03 10:03:05 -07:00

200 lines
7.7 KiB
C++

#include <mbgl/style/expression/collator.hpp>
#include <mbgl/text/language_tag.hpp>
#include <mbgl/util/platform.hpp>
#include <unaccent.hpp>
#include <jni/jni.hpp>
#include "../attach_env.hpp"
#include "collator_jni.hpp"
namespace mbgl {
namespace android {
void Collator::registerNative(jni::JNIEnv& env) {
javaClass = *jni::Class<Collator>::Find(env).NewGlobalRef(env).release();
}
jni::Class<Collator> Collator::javaClass;
jni::Object<Collator> Collator::getInstance(jni::JNIEnv& env, jni::Object<Locale> locale) {
using Signature = jni::Object<Collator>(jni::Object<Locale>);
auto method = javaClass.GetStaticMethod<Signature>(env, "getInstance");
return javaClass.Call(env, method, locale);
}
void Collator::setStrength(jni::JNIEnv& env, jni::Object<Collator> collator, jni::jint strength) {
using Signature = void(jni::jint);
auto static method = javaClass.GetMethod<Signature>(env, "setStrength");
collator.Call(env, method, strength);
}
jni::jint Collator::compare(jni::JNIEnv& env, jni::Object<Collator> collator, jni::String lhs, jni::String rhs) {
using Signature = jni::jint(jni::String, jni::String);
auto static method = javaClass.GetMethod<Signature>(env, "compare");
return collator.Call(env, method, lhs, rhs);
}
void Locale::registerNative(jni::JNIEnv& env) {
javaClass = *jni::Class<Locale>::Find(env).NewGlobalRef(env).release();
}
/*
We would prefer to use for/toLanguageTag, but they're only available in API level 21+
jni::Object<Locale> Locale::forLanguageTag(jni::JNIEnv& env, jni::String languageTag) {
using Signature = jni::Object<Locale>(jni::String);
auto method = javaClass.GetStaticMethod<Signature>(env, "forLanguageTag");
return javaClass.Call(env, method, languageTag);
}
jni::String Locale::toLanguageTag(jni::JNIEnv& env, jni::Object<Locale> locale) {
using Signature = jni::String();
auto static method = javaClass.GetMethod<Signature>(env, "toLanguageTag");
return locale.Call(env, method);
}
*/
jni::String Locale::getLanguage(jni::JNIEnv& env, jni::Object<Locale> locale) {
using Signature = jni::String();
auto static method = javaClass.GetMethod<Signature>(env, "getLanguage");
return locale.Call(env, method);
}
jni::String Locale::getCountry(jni::JNIEnv& env, jni::Object<Locale> locale) {
using Signature = jni::String();
auto static method = javaClass.GetMethod<Signature>(env, "getCountry");
return locale.Call(env, method);
}
jni::Object<Locale> Locale::getDefault(jni::JNIEnv& env) {
using Signature = jni::Object<Locale>();
auto method = javaClass.GetStaticMethod<Signature>(env, "getDefault");
return javaClass.Call(env, method);
}
jni::Object<Locale> Locale::New(jni::JNIEnv& env, jni::String language) {
static auto constructor = javaClass.GetConstructor<jni::String>(env);
return javaClass.New(env, constructor, language);
}
jni::Object<Locale> Locale::New(jni::JNIEnv& env, jni::String language, jni::String region) {
static auto constructor = javaClass.GetConstructor<jni::String, jni::String>(env);
return javaClass.New(env, constructor, language, region);
}
jni::Class<Locale> Locale::javaClass;
} // namespace android
namespace style {
namespace expression {
class Collator::Impl {
public:
Impl(bool caseSensitive_, bool diacriticSensitive_, optional<std::string> locale_)
: caseSensitive(caseSensitive_)
, diacriticSensitive(diacriticSensitive_)
, env(android::AttachEnv())
{
LanguageTag languageTag = locale_ ? LanguageTag::fromBCP47(*locale_) : LanguageTag();
if (!languageTag.language) {
locale = android::Locale::getDefault(*env).NewGlobalRef(*env);
} else if (!languageTag.region) {
locale = android::Locale::New(*env,
jni::Make<jni::String>(*env, *(languageTag.language)))
.NewGlobalRef(*env);
} else {
locale = android::Locale::New(*env,
jni::Make<jni::String>(*env, *(languageTag.language)),
jni::Make<jni::String>(*env, *(languageTag.region)))
.NewGlobalRef(*env);
}
collator = android::Collator::getInstance(*env, *locale).NewGlobalRef(*env);;
if (!diacriticSensitive && !caseSensitive) {
android::Collator::setStrength(*env, *collator, 0 /*PRIMARY*/);
} else if (diacriticSensitive && !caseSensitive) {
android::Collator::setStrength(*env, *collator, 1 /*SECONDARY*/);
} else if (caseSensitive) {
// If we're case-sensitive and diacritic-sensitive, we use a case-sensitive collator
// and a fallback implementation of diacritic-insensitivity.
android::Collator::setStrength(*env, *collator, 2 /*TERTIARY*/);
}
}
bool operator==(const Impl& other) const {
return caseSensitive == other.caseSensitive &&
diacriticSensitive == other.diacriticSensitive &&
resolvedLocale() == other.resolvedLocale();
}
int compare(const std::string& lhs, const std::string& rhs) const {
bool useUnaccent = !diacriticSensitive && caseSensitive;
// java.text.Collator doesn't support a diacritic-insensitive/case-sensitive collation
// order, so we have to compromise here. We use Android's case-sensitive Collator
// against strings that have been "unaccented" using non-locale-aware nunicode logic.
// Because of the difference in locale-awareness, this means turning on case-sensitivity
// can _potentially_ change compare results for strings that don't actually have any case
// differences.
jni::String jlhs = jni::Make<jni::String>(*env, useUnaccent ?
platform::unaccent(lhs) :
lhs);
jni::String jrhs = jni::Make<jni::String>(*env, useUnaccent ?
platform::unaccent(rhs) :
rhs);
jni::jint result = android::Collator::compare(*env, *collator, jlhs, jrhs);;
jni::DeleteLocalRef(*env, jlhs);
jni::DeleteLocalRef(*env, jrhs);
return result;
}
std::string resolvedLocale() const {
jni::String jLanguage = android::Locale::getLanguage(*env, *locale);
std::string language = jni::Make<std::string>(*env, jLanguage);
jni::DeleteLocalRef(*env, jLanguage);
jni::String jRegion = android::Locale::getCountry(*env, *locale);
std::string region = jni::Make<std::string>(*env, jRegion);
jni::DeleteLocalRef(*env, jRegion);
optional<std::string> resultLanguage;
if (!language.empty()) resultLanguage = language;
optional<std::string> resultRegion;
if (!region.empty()) resultRegion = region;
return LanguageTag(resultLanguage, {}, resultRegion).toBCP47();
}
private:
bool caseSensitive;
bool diacriticSensitive;
android::UniqueEnv env;
jni::UniqueObject<android::Collator> collator;
jni::UniqueObject<android::Locale> locale;
};
Collator::Collator(bool caseSensitive, bool diacriticSensitive, optional<std::string> locale_)
: impl(std::make_shared<Impl>(caseSensitive, diacriticSensitive, std::move(locale_)))
{}
bool Collator::operator==(const Collator& other) const {
return *impl == *(other.impl);
}
int Collator::compare(const std::string& lhs, const std::string& rhs) const {
return impl->compare(lhs, rhs);
}
std::string Collator::resolvedLocale() const {
return impl->resolvedLocale();
}
} // namespace expression
} // namespace style
} // namespace mbgl