Skip to content

Commit

Permalink
format distance as expected for locale, fixes #363
Browse files Browse the repository at this point in the history
Had to add small dictionary covering supported languages in util.py. Alternative
would be to use some external package, but those bring way more data than is needed
now.
  • Loading branch information
rinigus committed Jun 22, 2020
1 parent 4016a94 commit 68538c4
Show file tree
Hide file tree
Showing 3 changed files with 64 additions and 44 deletions.
15 changes: 11 additions & 4 deletions poor/narrative.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ def _format_verbal_alert(self, text, dist_offset, time_offset, speed):
"""Return `text` formatted as a verbal alert."""
dist_offset = max(dist_offset, time_offset * speed)
distance = poor.util.round_distance(dist_offset, n=1)
distance = poor.util.format_distance(distance, short=False)
distance = poor.util.format_distance(distance, short=False, lang=self.language)
return (__("In {distance}, {direction}", self.language)
.format(distance=distance, direction=text))

Expand Down Expand Up @@ -467,6 +467,10 @@ def _remove_overlapping_verbals(self):
if remove == i + 1 and i + 1 < len(self.verbals)
else -1)

def set_language(self, language):
"""Set language to use for directions."""
self.language = language

def set_maneuvers(self, maneuvers):
"""
Set maneuver points and corresponding narrative.
Expand Down Expand Up @@ -609,10 +613,9 @@ def _set_verbals(self, prompts):
# Remove the least important of overlapping prompts.
self._remove_overlapping_verbals()

def set_voice(self, language, gender="male"):
def set_voice(self, gender="male"):
"""Set TTS engine and voice to use for directions."""
self.language = language
self.voice_generator.set_voice(language, gender)
self.voice_generator.set_voice(self.language, gender)
# Generate standard messages.
self.voice_std_prompts = {
"std:new route found": __("New route found", self.language),
Expand All @@ -638,6 +641,10 @@ def unset(self):
self.x = []
self.y = []

def unset_voice(self):
"""Set TTS engine and voice to use for directions."""
self.voice_generator.set_voice(None, None)

@property
def voice_engine(self):
return self.voice_generator.current_engine
88 changes: 50 additions & 38 deletions poor/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,72 +170,84 @@ def find_closest(xs, ys, x, y, subset=None):
min_dist = dist
return min_index

def format_distance(meters, n=2, short=True):
def format_decimal(value, n=0, lang=None):
"""Format decimal to `n` significant digits"""
# All language designations as used by routers
decimal = {
"ca": ",",
"cs": ",",
"cz": ",",
"de": ",",
"de_DE": ",",
"en": ".",
"en_GB": ".",
"en_US": ".",
"en-US-x-pirate": ".",
"es": ",",
"es_ES": ",",
"es_MX": ".",
"fr": ",",
"fr_CA": ",",
"fr_FR": ",",
"hi": ".",
"it": ",",
"pt": ",",
"ru": ",",
"ru_RU": ",",
"sl": ",",
"sv": ",",
}

ndigits = n - get_ndigits(value)
ndigits = max(0, ndigits)
value = round(value, ndigits)
if lang is not None:
result = "{{:.{:d}f}}".format(max(0, ndigits)).format(value)
return result.replace('.', decimal.get(lang, '.'))
return locale.format("%.{:d}f".format(max(0, ndigits)), value)

def format_distance(meters, n=2, short=True, lang=None):
"""Format `meters` to `n` significant digits and unit label."""
if not math.isfinite(meters):
return ""
if poor.conf.units == "american":
feet = 3.28084 * meters
return format_distance_american(feet, n, short)
return format_distance_american(feet, n, short, lang)
if poor.conf.units == "british":
yards = 1.09361 * meters
return format_distance_british(yards, n, short)
return format_distance_metric(meters, n, short)
return format_distance_british(yards, n, short, lang)
return format_distance_metric(meters, n, short, lang)

def format_distance_american(feet, n=2, short=True):
def format_distance_american(feet, n=2, short=True, lang=None):
"""Format `feet` to `n` significant digits and unit label."""
if (n > 1 and feet >= 1000) or feet >= 5280:
distance = feet / 5280
ndigits = n - get_ndigits(distance)
ndigits = max(0, ndigits)
distance = round(distance, ndigits)
distance = "{{:.{:d}f}}".format(max(0, ndigits)).format(distance)
distance = format_decimal(feet / 5280, n, lang)
units = _("mi") if short else _("miles")
return "{distance} {units}".format(**locals())
else:
distance = feet
ndigits = n - get_ndigits(distance)
ndigits = min(0, ndigits)
distance = round(distance, ndigits)
distance = "{{:.{:d}f}}".format(max(0, ndigits)).format(distance)
distance = format_decimal(feet, n, lang)
units = _("ft") if short else _("feet")
return "{distance} {units}".format(**locals())

def format_distance_british(yards, n=2, short=True):
def format_distance_british(yards, n=2, short=True, lang=None):
"""Format `yards` to `n` significant digits and unit label."""
if (n > 1 and yards >= 400) or yards >= 1760:
distance = yards / 1760
ndigits = n - get_ndigits(distance)
ndigits = max(0, ndigits)
distance = round(distance, ndigits)
distance = "{{:.{:d}f}}".format(max(0, ndigits)).format(distance)
distance = format_decimal(yards / 1760, n, lang)
units = _("mi") if short else _("miles")
return "{distance} {units}".format(**locals())
else:
distance = yards
ndigits = n - get_ndigits(distance)
ndigits = min(0, ndigits)
distance = round(distance, ndigits)
distance = "{{:.{:d}f}}".format(max(0, ndigits)).format(distance)
distance = format_decimal(yards, n, lang)
units = _("yd") if short else _("yards")
return "{distance} {units}".format(**locals())

def format_distance_metric(meters, n=2, short=True):
def format_distance_metric(meters, n=2, short=True, lang=None):
"""Format `meters` to `n` significant digits and unit label."""
if meters >= 1000:
distance = meters / 1000
ndigits = n - get_ndigits(distance)
ndigits = max(0, ndigits)
distance = round(distance, ndigits)
distance = "{{:.{:d}f}}".format(max(0, ndigits)).format(distance)
distance = format_decimal(meters / 1000, n, lang)
units = _("km") if short else _("kilometers")
return "{distance} {units}".format(**locals())
else:
distance = meters
ndigits = n - get_ndigits(distance)
ndigits = min(0, ndigits)
distance = round(distance, ndigits)
distance = "{{:.{:d}f}}".format(max(0, ndigits)).format(distance)
distance = format_decimal(meters, n, lang)
units = _("m") if short else _("meters")
return "{distance} {units}".format(**locals())

Expand Down Expand Up @@ -483,7 +495,7 @@ def read_gpx(path):
file=sys.stderr)
raise # Exception
return x, y

def read_json(path):
"""Read data from JSON file at `path`."""
try:
Expand Down
5 changes: 3 additions & 2 deletions qml/Map.qml
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,7 @@ MapboxMap {
"x": route.x,
"y": route.y
};
py.call("poor.app.narrative.set_language", [route.language || "en"], null);
py.call("poor.app.narrative.set_mode", [route.mode || "car"], null);
py.call("poor.app.narrative.set_route", [route.x, route.y], function() {
map.hasRoute = true;
Expand Down Expand Up @@ -535,7 +536,7 @@ MapboxMap {
function initVoiceNavigation() {
// Initialize a TTS engine for the current routing instructions.
if (app.conf.voiceNavigation) {
var args = [map.route.language, app.conf.voiceGender];
var args = [app.conf.voiceGender];
py.call_sync("poor.app.narrative.set_voice", args);
var engine = py.evaluate("poor.app.narrative.voice_engine");
if (engine) {
Expand All @@ -544,7 +545,7 @@ MapboxMap {
} else
notification.flash(app.tr("Voice navigation unavailable: missing Text-to-Speech (TTS) engine for selected language"), "mapVoice");
} else {
py.call_sync("poor.app.narrative.set_voice", [null, null]);
py.call_sync("poor.app.narrative.unset_voice", []);
}
}

Expand Down

0 comments on commit 68538c4

Please sign in to comment.