package net.osmand.plus.routing; import java.io.IOException; import java.util.List; import java.util.concurrent.ConcurrentHashMap; import net.osmand.Location; import net.osmand.binary.RouteDataObject; import net.osmand.data.PointDescription; import net.osmand.plus.ApplicationMode; import net.osmand.plus.OsmandSettings; import net.osmand.plus.helpers.WaypointHelper.LocationPointWrapper; import net.osmand.plus.routing.AlarmInfo.AlarmInfoType; import net.osmand.plus.routing.RouteCalculationResult.NextDirectionInfo; import net.osmand.plus.voice.AbstractPrologCommandPlayer; import net.osmand.plus.voice.CommandBuilder; import net.osmand.plus.voice.CommandPlayer; import net.osmand.router.RouteSegmentResult; import net.osmand.router.TurnType; import net.osmand.util.Algorithms; import net.osmand.util.MapUtils; import alice.tuprolog.Struct; import alice.tuprolog.Term; import android.media.AudioManager; import android.media.SoundPool; public class VoiceRouter { private static final int STATUS_UTWP_TOLD = -1; private static final int STATUS_UNKNOWN = 0; private static final int STATUS_LONG_PREPARE = 1; private static final int STATUS_PREPARE = 2; private static final int STATUS_TURN_IN = 3; private static final int STATUS_TURN = 4; private static final int STATUS_TOLD = 5; private final RoutingHelper router; private static CommandPlayer player; private final OsmandSettings settings; private static boolean mute = false; private static int currentStatus = STATUS_UNKNOWN; private static boolean playedAndArriveAtTarget = false; private static float playGoAheadDist = 0; private static long lastAnnouncedSpeedLimit = 0; private static long waitAnnouncedSpeedLimit = 0; private static long lastAnnouncedOffRoute = 0; private static long waitAnnouncedOffRoute = 0; private static boolean suppressDest = false; private static boolean announceBackOnRoute = false; // private static long lastTimeRouteRecalcAnnounced = 0; // Remember when last announcement was made private static long lastAnnouncement = 0; // Default speed to have comfortable announcements (Speed in m/s) protected float DEFAULT_SPEED = 12; protected float TURN_DEFAULT_SPEED = 5; protected int PREPARE_LONG_DISTANCE = 0; protected int PREPARE_LONG_DISTANCE_END = 0; protected int PREPARE_DISTANCE = 0; protected int PREPARE_DISTANCE_END = 0; protected int TURN_IN_DISTANCE = 0; protected int TURN_IN_DISTANCE_END = 0; protected int TURN_DISTANCE = 0; protected static VoiceCommandPending pendingCommand = null; private static RouteDirectionInfo nextRouteDirection; private Term empty; public interface VoiceMessageListener { void onVoiceMessage(); } private ConcurrentHashMap<VoiceMessageListener, Integer> voiceMessageListeners; public VoiceRouter(RoutingHelper router, final OsmandSettings settings) { this.router = router; this.settings = settings; this.mute = settings.VOICE_MUTE.get(); empty = new Struct(""); voiceMessageListeners = new ConcurrentHashMap<VoiceRouter.VoiceMessageListener, Integer>(); } public void setPlayer(CommandPlayer player) { this.player = player; if (pendingCommand != null && player != null) { CommandBuilder newCommand = getNewCommandPlayerToPlay(); if (newCommand != null) { pendingCommand.play(newCommand); } pendingCommand = null; } } public CommandPlayer getPlayer() { return player; } public void setMute(boolean mute) { this.mute = mute; } public boolean isMute() { return mute; } protected CommandBuilder getNewCommandPlayerToPlay() { if (player == null) { return null; } lastAnnouncement = System.currentTimeMillis(); return player.newCommandBuilder(); } public void updateAppMode() { // Turn prompt starts either at distance, or additionally (TURN_IN and TURN only) if actual-lead-time(currentSpeed) < maximum-lead-time(defined by default speed) if (router.getAppMode().isDerivedRoutingFrom(ApplicationMode.CAR)) { PREPARE_LONG_DISTANCE = 3500; // [105 sec @ 120 km/h] // Issue 1411: Do not play prompts for PREPARE_LONG_DISTANCE, not needed. PREPARE_LONG_DISTANCE_END = 3000 + 1000; // [ 90 sec @ 120 km/h] PREPARE_DISTANCE = 1500; // [125 sec] PREPARE_DISTANCE_END = 1200; // [100 sec] TURN_IN_DISTANCE = 300; // 23 sec TURN_IN_DISTANCE_END = 210; // 16 sec TURN_DISTANCE = 50; // 7 sec TURN_DEFAULT_SPEED = 7f; // 25 km/h DEFAULT_SPEED = 13; // 48 km/h } else if (router.getAppMode().isDerivedRoutingFrom(ApplicationMode.BICYCLE)) { PREPARE_LONG_DISTANCE = 500; // [100 sec] // Do not play: PREPARE_LONG_DISTANCE_END = 300 + 1000; // [ 60 sec] PREPARE_DISTANCE = 200; // [ 40 sec] PREPARE_DISTANCE_END = 120; // [ 24 sec] TURN_IN_DISTANCE = 80; // 16 sec TURN_IN_DISTANCE_END = 60; // 12 sec TURN_DISTANCE = 30; // 6 sec. Check if this works with GPS accuracy! TURN_DEFAULT_SPEED = DEFAULT_SPEED = 5; // 18 km/h } else if (router.getAppMode().isDerivedRoutingFrom(ApplicationMode.PEDESTRIAN)) { // prepare_long_distance warning not needed for pedestrian, but for goAhead prompt PREPARE_LONG_DISTANCE = 500; // Do not play: PREPARE_LONG_DISTANCE_END = 300 + 300; // Prepare distance is not needed for pedestrian PREPARE_DISTANCE = 200; // [100 sec] // Do not play: PREPARE_DISTANCE_END = 150 + 100; // [ 75 sec] TURN_IN_DISTANCE = 50; // 25 sec TURN_IN_DISTANCE_END = 30; // 15 sec TURN_DISTANCE = 15; // 7,5sec. Check if this works with GPS accuracy! TURN_DEFAULT_SPEED = DEFAULT_SPEED = 2f; // 7,2 km/h } else { DEFAULT_SPEED = router.getAppMode().getDefaultSpeed(); TURN_DEFAULT_SPEED = DEFAULT_SPEED / 2; PREPARE_LONG_DISTANCE = (int) (DEFAULT_SPEED * 270); // Do not play: PREPARE_LONG_DISTANCE_END = (int) (DEFAULT_SPEED * 230) * 2; PREPARE_DISTANCE = (int) (DEFAULT_SPEED * 115); PREPARE_DISTANCE_END = (int) (DEFAULT_SPEED * 92); TURN_IN_DISTANCE = (int) (DEFAULT_SPEED * 23); TURN_IN_DISTANCE_END = (int) (DEFAULT_SPEED * 16); TURN_DISTANCE = (int) (DEFAULT_SPEED * 7); } } private double btScoDelayDistance = 0; public boolean isDistanceLess(float currentSpeed, double dist, double etalon, float defSpeed) { if (defSpeed <= 0) { defSpeed = DEFAULT_SPEED; } if (currentSpeed <= 0) { currentSpeed = DEFAULT_SPEED; } // Trigger close prompts earlier if delayed for BT SCO connection establishment if ((settings.AUDIO_STREAM_GUIDANCE.getModeValue(router.getAppMode()) == 0) && !AbstractPrologCommandPlayer.btScoStatus) { btScoDelayDistance = currentSpeed * (double) settings.BT_SCO_DELAY.get() / 1000; } if ((dist < etalon + btScoDelayDistance) || ((dist - btScoDelayDistance) / currentSpeed) < (etalon / defSpeed)) { return true; } return false; } public int calculateImminent(float dist, Location loc) { float speed = DEFAULT_SPEED; if (loc != null && loc.hasSpeed()) { speed = loc.getSpeed(); } if (isDistanceLess(speed, dist, TURN_DISTANCE, 0f)) { return 0; } else if (dist <= PREPARE_DISTANCE) { return 1; } else if (dist <= PREPARE_LONG_DISTANCE) { return 2; } else { return -1; } } private void nextStatusAfter(int previousStatus) { //STATUS_UNKNOWN=0 -> STATUS_LONG_PREPARE=1 -> STATUS_PREPARE=2 -> STATUS_TURN_IN=3 -> STATUS_TURN=4 -> STATUS_TOLD=5 if (previousStatus != STATUS_TOLD) { this.currentStatus = previousStatus + 1; } else { this.currentStatus = previousStatus; } } private boolean statusNotPassed(int statusToCheck) { return currentStatus <= statusToCheck; } public void announceOffRoute(double dist) { long ms = System.currentTimeMillis(); if (waitAnnouncedOffRoute == 0 || ms - lastAnnouncedOffRoute > waitAnnouncedOffRoute) { CommandBuilder p = getNewCommandPlayerToPlay(); if (p != null) { notifyOnVoiceMessage(); p.offRoute(dist).play(); announceBackOnRoute = true; } if (waitAnnouncedOffRoute == 0) { waitAnnouncedOffRoute = 60000; } else { waitAnnouncedOffRoute *= 2.5; } lastAnnouncedOffRoute = ms; } } public void announceBackOnRoute() { CommandBuilder p = getNewCommandPlayerToPlay(); if (announceBackOnRoute == true) { if (p != null) { notifyOnVoiceMessage(); p.backOnRoute().play(); } announceBackOnRoute = false; } } public void approachWaypoint(Location location, List<LocationPointWrapper> points) { CommandBuilder p = getNewCommandPlayerToPlay(); if (p == null) { return; } notifyOnVoiceMessage(); double[] dist = new double[1]; makeSound(); String text = getText(location, points, dist); p.goAhead(dist[0], null).andArriveAtWayPoint(text).play(); } public void approachFavorite(Location location, List<LocationPointWrapper> points) { CommandBuilder p = getNewCommandPlayerToPlay(); if (p == null) { return; } notifyOnVoiceMessage(); double[] dist = new double[1]; makeSound(); String text = getText(location, points, dist); p.goAhead(dist[0], null).andArriveAtFavorite(text).play(); } public void approachPoi(Location location, List<LocationPointWrapper> points) { CommandBuilder p = getNewCommandPlayerToPlay(); if (p == null) { return; } notifyOnVoiceMessage(); double[] dist = new double[1]; String text = getText(location, points, dist); p.goAhead(dist[0], null).andArriveAtPoi(text).play(); } public void announceWaypoint(List<LocationPointWrapper> points) { CommandBuilder p = getNewCommandPlayerToPlay(); if (p == null) { return; } notifyOnVoiceMessage(); makeSound(); String text = getText(null, points,null); p.arrivedAtWayPoint(text).play(); } public void announceFavorite(List<LocationPointWrapper> points) { CommandBuilder p = getNewCommandPlayerToPlay(); if (p == null) { return; } notifyOnVoiceMessage(); makeSound(); String text = getText(null, points,null); p.arrivedAtFavorite(text).play(); } public void announcePoi(List<LocationPointWrapper> points) { CommandBuilder p = getNewCommandPlayerToPlay(); if (p == null) { return; } notifyOnVoiceMessage(); String text = getText(null, points,null); p.arrivedAtPoi(text).play(); } protected String getText(Location location, List<LocationPointWrapper> points, double[] dist) { String text = ""; for (LocationPointWrapper point : points) { // Need to calculate distance to nearest point if (text.length() == 0) { if (location != null && dist != null) { dist[0] = point.getDeviationDistance() + MapUtils.getDistance(location.getLatitude(), location.getLongitude(), point.getPoint().getLatitude(), point.getPoint().getLongitude()); } } else { text += ", "; } text += PointDescription.getSimpleName(point.getPoint(), router.getApplication()); } return text; } public void announceAlarm(AlarmInfo info, float speed) { AlarmInfoType type = info.getType(); if (type == AlarmInfoType.SPEED_LIMIT) { announceSpeedAlarm(info.getIntValue(), speed); } else if (type == AlarmInfoType.SPEED_CAMERA) { if (router.getSettings().SPEAK_SPEED_CAMERA.get()) { CommandBuilder p = getNewCommandPlayerToPlay(); if (p != null) { notifyOnVoiceMessage(); p.attention(type+"").play(); } } } else if (type == AlarmInfoType.PEDESTRIAN) { if (router.getSettings().SPEAK_PEDESTRIAN.get()) { CommandBuilder p = getNewCommandPlayerToPlay(); if (p != null) { notifyOnVoiceMessage(); p.attention(type+"").play(); } } } else { if (router.getSettings().SPEAK_TRAFFIC_WARNINGS.get()) { CommandBuilder p = getNewCommandPlayerToPlay(); if (p != null) { notifyOnVoiceMessage(); p.attention(type+"").play(); } // See Issue 2377: Announce destination again - after some motorway tolls roads split shortly after the toll if (type == AlarmInfoType.TOLL_BOOTH) { suppressDest = false; } } } } public void announceSpeedAlarm(int maxSpeed, float speed) { long ms = System.currentTimeMillis(); if (waitAnnouncedSpeedLimit == 0) { // Wait 10 seconds before announcement if (ms - lastAnnouncedSpeedLimit > 120 * 1000) { waitAnnouncedSpeedLimit = ms; } } else { // If we wait before more than 20 sec (reset counter) if (ms - waitAnnouncedSpeedLimit > 20 * 1000) { waitAnnouncedSpeedLimit = 0; } else if (router.getSettings().SPEAK_SPEED_LIMIT.get() && ms - waitAnnouncedSpeedLimit > 10 * 1000 ) { CommandBuilder p = getNewCommandPlayerToPlay(); if (p != null) { notifyOnVoiceMessage(); lastAnnouncedSpeedLimit = ms; waitAnnouncedSpeedLimit = 0; p.speedAlarm(maxSpeed, speed).play(); } } } } private boolean isTargetPoint(NextDirectionInfo info) { boolean in = info != null && info.intermediatePoint; boolean target = info == null || info.directionInfo == null || info.directionInfo.distance == 0; return in || target; } private boolean needsInforming() { final Integer repeat = settings.KEEP_INFORMING.get(); if (repeat == null || repeat == 0) return false; final long notBefore = lastAnnouncement + repeat * 60 * 1000L; return System.currentTimeMillis() > notBefore; } /** * Updates status of voice guidance * @param currentLocation */ protected void updateStatus(Location currentLocation, boolean repeat) { // Directly after turn: goAhead (dist), unless: // < PREPARE_LONG_DISTANCE (e.g. 3500m): playPrepareTurn (-not played any more-) // < PREPARE_DISTANCE (e.g. 1500m): playPrepareTurn ("Turn after ...") // < TURN_IN_DISTANCE (e.g. 390m or 30sec): playMakeTurnIn ("Turn in ...") // < TURN_DISTANCE (e.g. 50m or 7sec): playMakeTurn ("Turn ...") float speed = DEFAULT_SPEED; if (currentLocation != null && currentLocation.hasSpeed()) { speed = Math.max(currentLocation.getSpeed(), speed); } NextDirectionInfo nextInfo = router.getNextRouteDirectionInfo(new NextDirectionInfo(), true); RouteSegmentResult currentSegment = router.getCurrentSegmentResult(); if (nextInfo.directionInfo == null) { return; } int dist = nextInfo.distanceTo; RouteDirectionInfo next = nextInfo.directionInfo; // If routing is changed update status to unknown if (next != nextRouteDirection) { nextRouteDirection = next; currentStatus = STATUS_UNKNOWN; suppressDest = false; playedAndArriveAtTarget = false; announceBackOnRoute = false; if (playGoAheadDist != -1) { playGoAheadDist = 0; } } if (!repeat) { if (dist <= 0) { return; } else if (needsInforming()) { playGoAhead(dist, getSpeakableStreetName(currentSegment, next, false)); return; } else if (currentStatus == STATUS_TOLD) { // nothing said possibly that's wrong case we should say before that // however it should be checked manually ? return; } } if (currentStatus == STATUS_UNKNOWN) { // Play "Continue for ..." if (1) after route calculation no other prompt is due, or (2) after a turn if next turn is more than PREPARE_LONG_DISTANCE away if ((playGoAheadDist == -1) || (dist > PREPARE_LONG_DISTANCE)) { playGoAheadDist = dist - 3 * TURN_DISTANCE; } } NextDirectionInfo nextNextInfo = router.getNextRouteDirectionInfoAfter(nextInfo, new NextDirectionInfo(), true); //I think "true" is correct here, not "!repeat" // Note: getNextRouteDirectionInfoAfter(nextInfo, x, y).distanceTo is distance from nextInfo, not from current position! // STATUS_TURN = "Turn (now)" if ((repeat || statusNotPassed(STATUS_TURN)) && isDistanceLess(speed, dist, TURN_DISTANCE, TURN_DEFAULT_SPEED)) { if (nextNextInfo.distanceTo < TURN_IN_DISTANCE_END && nextNextInfo != null) { playMakeTurn(currentSegment, next, nextNextInfo); } else { playMakeTurn(currentSegment, next, null); } if (!next.getTurnType().goAhead() && isTargetPoint(nextNextInfo)) { // !goAhead() avoids isolated "and arrive.." prompt, as goAhead() is not pronounced if (nextNextInfo.distanceTo < TURN_IN_DISTANCE_END) { // Issue #2865: Ensure a distance associated with the destination arrival is always announced, either here, or in subsequent "Turn in" prompt // Distance fon non-straights already announced in "Turn (now)"'s nextnext code above if ((nextNextInfo != null) && (nextNextInfo.directionInfo != null) && nextNextInfo.directionInfo.getTurnType().goAhead()) { playThen(); playGoAhead(nextNextInfo.distanceTo, empty); } playAndArriveAtDestination(nextNextInfo); } else if (nextNextInfo.distanceTo < 1.2f * TURN_IN_DISTANCE_END) { // 1.2 is safety margin should the subsequent "Turn in" prompt not fit in amy more playThen(); playGoAhead(nextNextInfo.distanceTo, empty); playAndArriveAtDestination(nextNextInfo); } } nextStatusAfter(STATUS_TURN); // STATUS_TURN_IN = "Turn in ..." } else if ((repeat || statusNotPassed(STATUS_TURN_IN)) && isDistanceLess(speed, dist, TURN_IN_DISTANCE, 0f)) { if (repeat || dist >= TURN_IN_DISTANCE_END) { if ((isDistanceLess(speed, nextNextInfo.distanceTo, TURN_DISTANCE, 0f) || nextNextInfo.distanceTo < TURN_IN_DISTANCE_END) && nextNextInfo != null) { playMakeTurnIn(currentSegment, next, dist - (int) btScoDelayDistance, nextNextInfo.directionInfo); } else { playMakeTurnIn(currentSegment, next, dist - (int) btScoDelayDistance, null); } playGoAndArriveAtDestination(repeat, nextInfo, currentSegment); } nextStatusAfter(STATUS_TURN_IN); // STATUS_PREPARE = "Turn after ..." } else if ((repeat || statusNotPassed(STATUS_PREPARE)) && (dist <= PREPARE_DISTANCE)) { if (repeat || dist >= PREPARE_DISTANCE_END) { if (!repeat && (next.getTurnType().keepLeft() || next.getTurnType().keepRight())) { // Do not play prepare for keep left/right } else { playPrepareTurn(currentSegment, next, dist); playGoAndArriveAtDestination(repeat, nextInfo, currentSegment); } } nextStatusAfter(STATUS_PREPARE); // STATUS_LONG_PREPARE = also "Turn after ...", we skip this now, users said this is obsolete } else if ((repeat || statusNotPassed(STATUS_LONG_PREPARE)) && (dist <= PREPARE_LONG_DISTANCE)) { if (repeat || dist >= PREPARE_LONG_DISTANCE_END) { playPrepareTurn(currentSegment, next, dist); playGoAndArriveAtDestination(repeat, nextInfo, currentSegment); } nextStatusAfter(STATUS_LONG_PREPARE); // STATUS_UNKNOWN = "Continue for ..." if (1) after route calculation no other prompt is due, or (2) after a turn if next turn is more than PREPARE_LONG_DISTANCE away } else if (statusNotPassed(STATUS_UNKNOWN)) { // Strange how we get here but nextStatusAfter(STATUS_UNKNOWN); } else if (repeat || (statusNotPassed(STATUS_PREPARE) && dist < playGoAheadDist)) { playGoAheadDist = 0; playGoAhead(dist, getSpeakableStreetName(currentSegment, next, false)); } } public void announceCurrentDirection(Location currentLocation) { synchronized (router) { if (currentStatus != STATUS_UTWP_TOLD) { updateStatus(currentLocation, true); } else if (playMakeUTwp()) { playGoAheadDist = 0; } } } private boolean playMakeUTwp() { CommandBuilder play = getNewCommandPlayerToPlay(); if (play != null) { notifyOnVoiceMessage(); play.makeUTwp().play(); return true; } return false; } private void playThen() { CommandBuilder play = getNewCommandPlayerToPlay(); if (play != null) { notifyOnVoiceMessage(); play.then().play(); } } private void playGoAhead(int dist, Term streetName) { CommandBuilder play = getNewCommandPlayerToPlay(); if (play != null) { notifyOnVoiceMessage(); play.goAhead(dist, streetName).play(); } } public Term getSpeakableStreetName(RouteSegmentResult currentSegment, RouteDirectionInfo i, boolean includeDest) { if (i == null || !router.getSettings().SPEAK_STREET_NAMES.get()) { return empty; } if (player != null && player.supportsStructuredStreetNames()) { Term next = empty; // Issue 2377: Play Dest here only if not already previously announced, to avoid repetition if (includeDest == true) { next = new Struct(new Term[] { getTermString(getSpeakablePointName(i.getRef())), getTermString(getSpeakablePointName(i.getStreetName())), getTermString(getSpeakablePointName(i.getDestinationName())) }); } else { next = new Struct(new Term[] { getTermString(getSpeakablePointName(i.getRef())), getTermString(getSpeakablePointName(i.getStreetName())), empty }); } Term current = empty; if (currentSegment != null) { // Issue 2377: Play Dest here only if not already previously announced, to avoid repetition if (includeDest == true) { RouteDataObject obj = currentSegment.getObject(); current = new Struct(new Term[] { getTermString(getSpeakablePointName(obj.getRef(settings.MAP_PREFERRED_LOCALE.get(), settings.MAP_TRANSLITERATE_NAMES.get(), currentSegment.isForwardDirection()))), getTermString(getSpeakablePointName(obj.getName(settings.MAP_PREFERRED_LOCALE.get(), settings.MAP_TRANSLITERATE_NAMES.get()))), getTermString(getSpeakablePointName(obj.getDestinationName(settings.MAP_PREFERRED_LOCALE.get(), settings.MAP_TRANSLITERATE_NAMES.get(), currentSegment.isForwardDirection()))) }); } else { RouteDataObject obj = currentSegment.getObject(); current = new Struct(new Term[] { getTermString(getSpeakablePointName(obj.getRef(settings.MAP_PREFERRED_LOCALE.get(), settings.MAP_TRANSLITERATE_NAMES.get(), currentSegment.isForwardDirection()))), getTermString(getSpeakablePointName(obj.getName(settings.MAP_PREFERRED_LOCALE.get(), settings.MAP_TRANSLITERATE_NAMES.get()))), empty }); } } Struct voice = new Struct("voice", next, current ); return voice; } else { Term rf = getTermString(getSpeakablePointName(i.getRef())); if (rf == empty) { rf = getTermString(getSpeakablePointName(i.getStreetName())); } return rf; } } private Term getTermString(String s) { if (!Algorithms.isEmpty(s)) { return new Struct(s); } return empty; } public String getSpeakablePointName(String pn) { // Replace characters which may produce unwanted tts sounds: if (pn != null) { pn = pn.replace('-', ' '); pn = pn.replace(':', ' '); pn = pn.replace(";", ", "); // Trailing blank prevents punctuation being pronounced. Replace by comma for better intonation. pn = pn.replace("/", ", "); // Slash is actually pronounced by many TTS engines, ceeating an awkward voice prompt, better replace by comma. if ((player != null) && (!"de".equals(player.getLanguage()))) { pn = pn.replace("\u00df", "ss"); // Helps non-German tts voices to pronounce German Strasse (=street) } if ((player != null) && ("en".startsWith(player.getLanguage()))) { pn = pn.replace("SR", "S R"); // Avoid SR (as for State Route or Strada Regionale) be pronounced as "Senior" in English tts voice } } return pn; } private void playPrepareTurn(RouteSegmentResult currentSegment, RouteDirectionInfo next, int dist) { CommandBuilder play = getNewCommandPlayerToPlay(); if (play != null) { String tParam = getTurnType(next.getTurnType()); if (tParam != null) { notifyOnVoiceMessage(); play.prepareTurn(tParam, dist, getSpeakableStreetName(currentSegment, next, true)).play(); } else if (next.getTurnType().isRoundAbout()) { notifyOnVoiceMessage(); play.prepareRoundAbout(dist, next.getTurnType().getExitOut(), getSpeakableStreetName(currentSegment, next, true)).play(); } else if (next.getTurnType().getValue() == TurnType.TU || next.getTurnType().getValue() == TurnType.TRU) { notifyOnVoiceMessage(); play.prepareMakeUT(dist, getSpeakableStreetName(currentSegment, next, true)).play(); } } } private void playMakeTurnIn(RouteSegmentResult currentSegment, RouteDirectionInfo next, int dist, RouteDirectionInfo pronounceNextNext) { CommandBuilder play = getNewCommandPlayerToPlay(); if (play != null) { String tParam = getTurnType(next.getTurnType()); boolean isPlay = true; if (tParam != null) { play.turn(tParam, dist, getSpeakableStreetName(currentSegment, next, true)); suppressDest = true; } else if (next.getTurnType().isRoundAbout()) { play.roundAbout(dist, next.getTurnType().getTurnAngle(), next.getTurnType().getExitOut(), getSpeakableStreetName(currentSegment, next, true)); // Other than in prepareTurn, in prepareRoundabout we do not announce destination, so we can repeat it one more time suppressDest = false; } else if (next.getTurnType().getValue() == TurnType.TU || next.getTurnType().getValue() == TurnType.TRU) { play.makeUT(dist, getSpeakableStreetName(currentSegment, next, true)); suppressDest = true; } else { isPlay = false; } // 'then keep' preparation for next after next. (Also announces an interim straight segment, which is not pronounced above.) if (pronounceNextNext != null) { TurnType t = pronounceNextNext.getTurnType(); isPlay = true; if (t.getValue() != TurnType.C && next.getTurnType().getValue() == TurnType.C) { play.goAhead(dist, getSpeakableStreetName(currentSegment, next, true)); } if (t.getValue() == TurnType.TL || t.getValue() == TurnType.TSHL || t.getValue() == TurnType.TSLL || t.getValue() == TurnType.TU || t.getValue() == TurnType.KL ) { play.then().bearLeft( getSpeakableStreetName(currentSegment, next, false)); } else if (t.getValue() == TurnType.TR || t.getValue() == TurnType.TSHR || t.getValue() == TurnType.TSLR || t.getValue() == TurnType.TRU || t.getValue() == TurnType.KR) { play.then().bearRight( getSpeakableStreetName(currentSegment, next, false)); } } if (isPlay) { notifyOnVoiceMessage(); play.play(); } } } private void playGoAndArriveAtDestination(boolean repeat, NextDirectionInfo nextInfo, RouteSegmentResult currentSegment) { RouteDirectionInfo next = nextInfo.directionInfo; if (isTargetPoint(nextInfo) && (!playedAndArriveAtTarget || repeat)) { if (next.getTurnType().goAhead()) { playGoAhead(nextInfo.distanceTo, getSpeakableStreetName(currentSegment, next, false)); playAndArriveAtDestination(nextInfo); playedAndArriveAtTarget = true; } else if (nextInfo.distanceTo <= 2 * TURN_IN_DISTANCE) { playAndArriveAtDestination(nextInfo); playedAndArriveAtTarget = true; } } } private void playAndArriveAtDestination(NextDirectionInfo info) { if (isTargetPoint(info)) { String pointName = info == null ? "" : info.pointName; CommandBuilder play = getNewCommandPlayerToPlay(); if (play != null) { notifyOnVoiceMessage(); if (info != null && info.intermediatePoint) { play.andArriveAtIntermediatePoint(getSpeakablePointName(pointName)).play(); } else { play.andArriveAtDestination(getSpeakablePointName(pointName)).play(); } } } } private void playMakeTurn(RouteSegmentResult currentSegment, RouteDirectionInfo next, NextDirectionInfo nextNextInfo) { CommandBuilder play = getNewCommandPlayerToPlay(); if (play != null) { String tParam = getTurnType(next.getTurnType()); boolean isplay = true; if (tParam != null) { play.turn(tParam, getSpeakableStreetName(currentSegment, next, !suppressDest)); } else if (next.getTurnType().isRoundAbout()) { play.roundAbout(next.getTurnType().getTurnAngle(), next.getTurnType().getExitOut(), getSpeakableStreetName(currentSegment, next, !suppressDest)); } else if (next.getTurnType().getValue() == TurnType.TU || next.getTurnType().getValue() == TurnType.TRU) { play.makeUT(getSpeakableStreetName(currentSegment, next, !suppressDest)); // Do not announce goAheads //} else if (next.getTurnType().getValue() == TurnType.C)) { // play.goAhead(); } else { isplay = false; } // Add turn after next if ((nextNextInfo != null) && (nextNextInfo.directionInfo != null)) { // This case only needed should we want a prompt at the end of straight segments (equivalent of makeTurn) when nextNextInfo should be announced again there. if (nextNextInfo.directionInfo.getTurnType().getValue() != TurnType.C && next.getTurnType().getValue() == TurnType.C) { play.goAhead(); isplay = true; } String t2Param = getTurnType(nextNextInfo.directionInfo.getTurnType()); if (t2Param != null) { if (isplay) { play.then(); play.turn(t2Param, nextNextInfo.distanceTo, empty); } } else if (nextNextInfo.directionInfo.getTurnType().isRoundAbout()) { if (isplay) { play.then(); play.roundAbout(nextNextInfo.distanceTo, nextNextInfo.directionInfo.getTurnType().getTurnAngle(), nextNextInfo.directionInfo.getTurnType().getExitOut(), empty); } } else if (nextNextInfo.directionInfo.getTurnType().getValue() == TurnType.TU) { if (isplay) { play.then(); play.makeUT(nextNextInfo.distanceTo, empty); } } } if (isplay) { notifyOnVoiceMessage(); play.play(); } } } private String getTurnType(TurnType t) { if (TurnType.TL == t.getValue()) { return AbstractPrologCommandPlayer.A_LEFT; } else if (TurnType.TSHL == t.getValue()) { return AbstractPrologCommandPlayer.A_LEFT_SH; } else if (TurnType.TSLL == t.getValue()) { return AbstractPrologCommandPlayer.A_LEFT_SL; } else if (TurnType.TR == t.getValue()) { return AbstractPrologCommandPlayer.A_RIGHT; } else if (TurnType.TSHR == t.getValue()) { return AbstractPrologCommandPlayer.A_RIGHT_SH; } else if (TurnType.TSLR == t.getValue()) { return AbstractPrologCommandPlayer.A_RIGHT_SL; } else if (TurnType.KL == t.getValue()) { return AbstractPrologCommandPlayer.A_LEFT_KEEP; } else if (TurnType.KR == t.getValue()) { return AbstractPrologCommandPlayer.A_RIGHT_KEEP; } return null; } public void gpsLocationLost() { CommandBuilder play = getNewCommandPlayerToPlay(); if (play != null) { notifyOnVoiceMessage(); play.gpsLocationLost().play(); } } public void gpsLocationRecover() { CommandBuilder play = getNewCommandPlayerToPlay(); if (play != null) { notifyOnVoiceMessage(); play.gpsLocationRecover().play(); } } public void newRouteIsCalculated(boolean newRoute) { CommandBuilder play = getNewCommandPlayerToPlay(); if (play != null) { notifyOnVoiceMessage(); if (!newRoute) { play.routeRecalculated(router.getLeftDistance(), router.getLeftTime()).play(); } else { play.newRouteCalculated(router.getLeftDistance(), router.getLeftTime()).play(); } } else if (player == null) { pendingCommand = new VoiceCommandPending(!newRoute ? VoiceCommandPending.ROUTE_RECALCULATED : VoiceCommandPending.ROUTE_CALCULATED, this); } if (newRoute) { playGoAheadDist = -1; } currentStatus = STATUS_UNKNOWN; suppressDest = false; nextRouteDirection = null; } public void arrivedDestinationPoint(String name) { CommandBuilder play = getNewCommandPlayerToPlay(); if (play != null) { notifyOnVoiceMessage(); play.arrivedAtDestination(getSpeakablePointName(name)).play(); } } public void arrivedIntermediatePoint(String name) { CommandBuilder play = getNewCommandPlayerToPlay(); if (play != null) { notifyOnVoiceMessage(); play.arrivedAtIntermediatePoint(getSpeakablePointName(name)).play(); } } // This is not needed, used are only arrivedIntermediatePoint (for points on the route) or announceWaypoint (for points near the route=) //public void arrivedWayPoint(String name) { // CommandBuilder play = getNewCommandPlayerToPlay(); // if (play != null) { // notifyOnVoiceMessage(); // play.arrivedAtWayPoint(getSpeakablePointName(name)).play(); // } //} public void onApplicationTerminate() { if (player != null) { player.clear(); } } public void interruptRouteCommands() { if (player != null) { player.stop(); } } /** * Command to wait until voice player is initialized */ private class VoiceCommandPending { public static final int ROUTE_CALCULATED = 1; public static final int ROUTE_RECALCULATED = 2; protected final int type; private final VoiceRouter voiceRouter; public VoiceCommandPending(int type, VoiceRouter voiceRouter) { this.type = type; this.voiceRouter = voiceRouter; } public void play(CommandBuilder newCommand) { int left = voiceRouter.router.getLeftDistance(); int time = voiceRouter.router.getLeftTime(); if (left > 0) { if (type == ROUTE_CALCULATED) { notifyOnVoiceMessage(); newCommand.newRouteCalculated(left, time).play(); } else if (type == ROUTE_RECALCULATED) { notifyOnVoiceMessage(); newCommand.routeRecalculated(left, time).play(); } } } } private void makeSound() { if (isMute()) { return; } SoundPool sp = new SoundPool(5, AudioManager.STREAM_MUSIC, 0); int soundClick = -1; boolean success = true; try { // Taken unaltered from https://freesound.org/people/Corsica_S/sounds/91926/ under license http://creativecommons.org/licenses/by/3.0/ : soundClick = sp.load(settings.getContext().getAssets().openFd("sounds/ding.ogg"), 1); } catch (IOException e) { e.printStackTrace(); success = false; } if (success) { sp.play(soundClick, 1 ,1, 0, 0, 1); } } public void addVoiceMessageListener(VoiceMessageListener voiceMessageListener) { voiceMessageListeners.put(voiceMessageListener, 0); } public void removeVoiceMessageListener(VoiceMessageListener voiceMessageListener) { voiceMessageListeners.remove(voiceMessageListener); } public void notifyOnVoiceMessage() { if (settings.WAKE_ON_VOICE_INT.get() > 0) { router.getApplication().runInUIThread(new Runnable() { @Override public void run() { for (VoiceMessageListener lnt : voiceMessageListeners.keySet()) { lnt.onVoiceMessage(); } } }); } } }