diff options
author | mvglasow <michael@vonglasow.com> | 2021-01-31 15:32:52 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-01-31 15:32:52 +0100 |
commit | 37f22dbec6aa0a10daedef36d28d4c1de7c1eabe (patch) | |
tree | 35c8d6072825facd383c9ed6833ac4c027b7381d | |
parent | 91222b892b2f796b8c18dff13eb15f888b276f3b (diff) | |
parent | 3aaab0aa1788a660604ae26a5f794f8bbde67337 (diff) | |
download | navit-37f22dbec6aa0a10daedef36d28d4c1de7c1eabe.tar.gz |
Merge pull request #1083 from navit-gps/traff_0_8
Add TraFF 0.8 support
-rw-r--r-- | navit/android/src/org/navitproject/navit/NavitTraff.java | 361 | ||||
-rw-r--r-- | navit/attr.c | 4 | ||||
-rw-r--r-- | navit/navit.c | 70 | ||||
-rw-r--r-- | navit/route.c | 27 | ||||
-rw-r--r-- | navit/route_protected.h | 1 | ||||
-rw-r--r-- | navit/traffic.c | 31 | ||||
-rw-r--r-- | navit/traffic.h | 24 | ||||
-rw-r--r-- | navit/traffic/dummy/traffic_dummy.c | 1 | ||||
-rw-r--r-- | navit/traffic/null/traffic_null.c | 1 | ||||
-rw-r--r-- | navit/traffic/traff_android/traffic_traff_android.c | 179 | ||||
-rw-r--r-- | navit/util.c | 13 | ||||
-rw-r--r-- | navit/xmlconfig.h | 2 |
12 files changed, 672 insertions, 42 deletions
diff --git a/navit/android/src/org/navitproject/navit/NavitTraff.java b/navit/android/src/org/navitproject/navit/NavitTraff.java index c82d7d293..fd499b70b 100644 --- a/navit/android/src/org/navitproject/navit/NavitTraff.java +++ b/navit/android/src/org/navitproject/navit/NavitTraff.java @@ -25,11 +25,18 @@ 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.database.Cursor; +import android.net.Uri; +import android.os.Bundle; import android.util.Log; +import java.util.HashMap; +import java.util.Iterator; import java.util.List; +import java.util.Map; /** * The TraFF receiver implementation. @@ -39,11 +46,46 @@ import java.util.List; */ public class NavitTraff extends BroadcastReceiver { + private static final String ACTION_TRAFF_GET_CAPABILITIES = "org.traffxml.traff.GET_CAPABILITIES"; + private static final String ACTION_TRAFF_HEARTBEAT = "org.traffxml.traff.HEARTBEAT"; private static final String ACTION_TRAFF_FEED = "org.traffxml.traff.FEED"; private static final String ACTION_TRAFF_POLL = "org.traffxml.traff.POLL"; + private static final String ACTION_TRAFF_SUBSCRIBE = "org.traffxml.traff.SUBSCRIBE"; + private static final String ACTION_TRAFF_SUBSCRIPTION_CHANGE = "org.traffxml.traff.SUBSCRIPTION_CHANGE"; + private static final String ACTION_TRAFF_UNSUBSCRIBE = "org.traffxml.traff.UNSUBSCRIBE"; + private static final String COLUMN_DATA = "data"; + private static final String CONTENT_SCHEMA = "content"; + private 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)" + }; + private static final String EXTRA_CAPABILITIES = "capabilities"; private static final String EXTRA_FEED = "feed"; + private static final String EXTRA_FILTER_LIST = "filter_list"; + private static final String EXTRA_PACKAGE = "package"; + private static final String EXTRA_SUBSCRIPTION_ID = "subscription_id"; + private static final String MIME_TYPE_TRAFF = "vnd.android.cursor.dir/org.traffxml.message"; + private static final int RESULT_OK = -1; + private static final int RESULT_INTERNAL_ERROR = 7; + private static final int RESULT_INVALID = 1; + private static final int RESULT_SUBSCRIPTION_REJECTED = 2; + private static final int RESULT_NOT_COVERED = 3; + private static final int RESULT_PARTIALLY_COVERED = 4; + private static final int RESULT_SUBSCRIPTION_UNKNOWN = 5; + private static final String TAG = "NavitTraff"; private final long mCbid; + private final Context context; + + /** Active subscriptions (key is the subscription ID, value is the package ID). */ + private Map<String, String> subscriptions = new HashMap<String, String>(); + /** * Forwards a newly received TraFF feed to the traffic module for processing. * @@ -65,39 +107,322 @@ public class NavitTraff extends BroadcastReceiver { */ NavitTraff(Context context, long cbid) { this.mCbid = cbid; + this.context = context.getApplicationContext(); + + /* An intent filter for TraFF 0.7 events. */ + IntentFilter traffFilter07 = new IntentFilter(); + traffFilter07.addAction(ACTION_TRAFF_FEED); - /* An intent filter for TraFF events. */ - IntentFilter traffFilter = new IntentFilter(); - traffFilter.addAction(ACTION_TRAFF_FEED); - traffFilter.addAction(ACTION_TRAFF_POLL); + /* An intent filter for TraFF 0.8 events. */ + IntentFilter traffFilter08 = new IntentFilter(); + traffFilter08.addAction(ACTION_TRAFF_FEED); + traffFilter08.addDataScheme(CONTENT_SCHEMA); + try { + traffFilter08.addDataType(MIME_TYPE_TRAFF); + } catch (MalformedMimeTypeException e) { + // as long as the constant is a well-formed MIME type, this exception never gets thrown + e.printStackTrace(); + } - context.registerReceiver(this, traffFilter); - /* TODO unregister receiver on exit */ + this.context.registerReceiver(this, traffFilter07); + this.context.registerReceiver(this, traffFilter08); - /* Broadcast a poll intent */ + /* Broadcast a poll intent to all TraFF 0.7-only receivers */ Intent outIntent = new Intent(ACTION_TRAFF_POLL); - PackageManager pm = context.getPackageManager(); - List<ResolveInfo> receivers = pm.queryBroadcastReceivers(outIntent, 0); - if (receivers != null) { - for (ResolveInfo receiver : receivers) { + PackageManager pm = this.context.getPackageManager(); + List<ResolveInfo> receivers07 = pm.queryBroadcastReceivers(outIntent, 0); + List<ResolveInfo> receivers08 = pm.queryBroadcastReceivers(new Intent(ACTION_TRAFF_GET_CAPABILITIES), 0); + if (receivers07 != null) { + /* get receivers which support only TraFF 0.7 and poll them */ + if (receivers08 != null) + receivers07.removeAll(receivers08); + for (ResolveInfo receiver : receivers07) { ComponentName cn = new ComponentName(receiver.activityInfo.applicationInfo.packageName, receiver.activityInfo.name); outIntent = new Intent(ACTION_TRAFF_POLL); outIntent.setComponent(cn); - context.sendBroadcast(outIntent, Manifest.permission.ACCESS_COARSE_LOCATION); + this.context.sendBroadcast(outIntent, Manifest.permission.ACCESS_COARSE_LOCATION); + } + } + } + + void close() { + for (Map.Entry<String, String> subscription : subscriptions.entrySet()) { + Bundle extras = new Bundle(); + extras.putString(EXTRA_SUBSCRIPTION_ID, subscription.getKey()); + sendTraffIntent(this.context, ACTION_TRAFF_UNSUBSCRIBE, null, extras, subscription.getValue(), + Manifest.permission.ACCESS_COARSE_LOCATION, this); + } + this.context.unregisterReceiver(this); + } + + void onFilterUpdate(String filterList) { + /* change existing subscriptions */ + for (Map.Entry<String, String> entry : subscriptions.entrySet()) { + Log.d(TAG, String.format("changing subscription %s (%s)", entry.getKey(), entry.getValue())); + Bundle extras = new Bundle(); + extras.putString(EXTRA_SUBSCRIPTION_ID, entry.getKey()); + extras.putString(EXTRA_FILTER_LIST, filterList); + sendTraffIntent(context, ACTION_TRAFF_SUBSCRIPTION_CHANGE, null, extras, + entry.getValue(), + Manifest.permission.ACCESS_COARSE_LOCATION, this); + } + + /* set up missing subscriptions */ + PackageManager pm = this.context.getPackageManager(); + List<ResolveInfo> receivers = pm.queryBroadcastReceivers(new Intent(ACTION_TRAFF_GET_CAPABILITIES), 0); + if (receivers != null) { + /* filter out receivers to which we are already subscribed */ + Iterator<ResolveInfo> iter = receivers.iterator(); + while (iter.hasNext()) { + ResolveInfo receiver = iter.next(); + if (subscriptions.containsValue(receiver.activityInfo.applicationInfo.packageName)) + iter.remove(); + } + + for (ResolveInfo receiver : receivers) { + Log.d(TAG, "subscribing to " + receiver.activityInfo.applicationInfo.packageName); + Bundle extras = new Bundle(); + extras.putString(EXTRA_PACKAGE, context.getPackageName()); + extras.putString(EXTRA_FILTER_LIST, filterList); + sendTraffIntent(context, ACTION_TRAFF_SUBSCRIBE, null, extras, + receiver.activityInfo.applicationInfo.packageName, + Manifest.permission.ACCESS_COARSE_LOCATION, this); } } } @Override public void onReceive(Context context, Intent intent) { - if ((intent != null) && (intent.getAction().equals(ACTION_TRAFF_FEED))) { - String feed = intent.getStringExtra(EXTRA_FEED); - if (feed == null) { - Log.w(this.getClass().getSimpleName(), "empty feed, ignoring"); - } else { - onFeedReceived(mCbid, feed); + if (intent != null) { + if (intent.getAction().equals(ACTION_TRAFF_FEED)) { + Uri uri = intent.getData(); + if (uri != null) { + /* 0.8 feed */ + String subscriptionId = intent.getStringExtra(EXTRA_SUBSCRIPTION_ID); + if (subscriptions.containsKey(subscriptionId)) + fetchMessages(context, uri); + else { + /* + * If we don’t recognize the subscription, skip processing and unsubscribe. + * Note: if EXTRA_PACKAGE is not set, sendTraffIntent() sends the request to every + * manifest-declared receiver which handles the request. + */ + Log.d(TAG, + String.format("got a feed from %s for unknown subscription %s, URI %s; unsubscribing", + intent.getStringExtra(EXTRA_PACKAGE), subscriptionId, uri)); + Bundle extras = new Bundle(); + extras.putString(EXTRA_SUBSCRIPTION_ID, subscriptionId); + sendTraffIntent(context, ACTION_TRAFF_UNSUBSCRIBE, null, extras, + intent.getStringExtra(EXTRA_PACKAGE), + Manifest.permission.ACCESS_COARSE_LOCATION, this); + } + } else { + /* 0.7 feed */ + String packageName = intent.getStringExtra(EXTRA_PACKAGE); + /* + * If the feed comes from a TraFF 0.8+ source and we are subscribed, skip it. + * As a side effect of the current implementation, if a “bilingual” TraFF 0.7/0.8 + * source sends a broadcast feed before we have subscribed to it, we would process + * the whole feed first, and then subscribe to a subset of that data. + * If that turns out to be an issue, we would need to detect TraFF 0.8-capable + * sources and discard broadcast feeds from them. + */ + if ((packageName != null) && subscriptions.containsValue(packageName)) + return; + String feed = intent.getStringExtra(EXTRA_FEED); + if (feed == null) { + Log.w(this.getClass().getSimpleName(), "empty feed, ignoring"); + } else { + onFeedReceived(mCbid, feed); + } + } // uri != null + } else if (intent.getAction().equals(ACTION_TRAFF_SUBSCRIBE)) { + if (this.getResultCode() != RESULT_OK) { + Bundle extras = this.getResultExtras(true); + if (extras != null) + Log.e(this.getClass().getSimpleName(), String.format("subscription to %s failed, %s", + extras.getString(EXTRA_PACKAGE), formatTraffError(this.getResultCode()))); + else + Log.e(this.getClass().getSimpleName(), String.format("subscription failed, %s", + formatTraffError(this.getResultCode()))); + return; + } + Bundle extras = this.getResultExtras(true); + String data = this.getResultData(); + String packageName = extras.getString(EXTRA_PACKAGE); + String subscriptionId = extras.getString(EXTRA_SUBSCRIPTION_ID); + if (subscriptionId == null) { + Log.e(this.getClass().getSimpleName(), + String.format("subscription to %s failed: no subscription ID returned", packageName)); + return; + } else if (packageName == null) { + Log.e(this.getClass().getSimpleName(), "subscription failed: no package name"); + return; + } else if (data == null) { + Log.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)); + subscriptions.put(subscriptionId, packageName); + return; + } + Log.d(TAG, "subscription to " + packageName + " successful, ID: " + subscriptionId); + subscriptions.put(subscriptionId, packageName); + fetchMessages(context, Uri.parse(data)); + } else if (intent.getAction().equals(ACTION_TRAFF_SUBSCRIPTION_CHANGE)) { + if (this.getResultCode() != RESULT_OK) { + Bundle extras = this.getResultExtras(true); + if (extras != null) + Log.e(this.getClass().getSimpleName(), + String.format("subscription change for %s failed: %s", + extras.getString(EXTRA_SUBSCRIPTION_ID), + formatTraffError(this.getResultCode()))); + else + Log.e(this.getClass().getSimpleName(), + String.format("subscription change failed: %s", + formatTraffError(this.getResultCode()))); + return; + } + Bundle extras = intent.getExtras(); + String data = this.getResultData(); + String subscriptionId = extras.getString(EXTRA_SUBSCRIPTION_ID); + if (subscriptionId == null) { + Log.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 (data == null) { + Log.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; + } else if (!subscriptions.containsKey(subscriptionId)) { + Log.e(this.getClass().getSimpleName(), + "subscription change failed: unknown subscription ID " + subscriptionId); + return; + } + Log.d(TAG, "subscription change for " + subscriptionId + " successful"); + fetchMessages(context, Uri.parse(data)); + } else if (intent.getAction().equals(ACTION_TRAFF_UNSUBSCRIBE)) { + /* + * 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(ACTION_TRAFF_HEARTBEAT)) { + String subscriptionId = intent.getStringExtra(EXTRA_SUBSCRIPTION_ID); + if (subscriptions.containsKey(subscriptionId)) { + Log.d(TAG, + String.format("got a heartbeat from %s for subscription %s; sending result", + intent.getStringExtra(EXTRA_PACKAGE), subscriptionId)); + this.setResult(RESULT_OK, null, null); + } else { + /* + * If we don’t recognize the subscription, skip reply and unsubscribe. + * Note: if EXTRA_PACKAGE is not set, sendTraffIntent() sends the request to every + * manifest-declared receiver which handles the request. + */ + Log.d(TAG, + String.format("got a heartbeat from %s for unknown subscription %s; unsubscribing", + intent.getStringExtra(EXTRA_PACKAGE), subscriptionId)); + Bundle extras = new Bundle(); + extras.putString(EXTRA_SUBSCRIPTION_ID, subscriptionId); + sendTraffIntent(context, ACTION_TRAFF_UNSUBSCRIBE, null, extras, + intent.getStringExtra(EXTRA_PACKAGE), + Manifest.permission.ACCESS_COARSE_LOCATION, this); + } + } // intent.getAction() + } // intent != null + } + + /** + * Fetches 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[] {COLUMN_DATA}, null, null, null); + if (cursor == null) + return; + if (cursor.getCount() < 1) { + cursor.close(); + return; } + StringBuilder builder = new StringBuilder("<feed>\n"); + while (cursor.moveToNext()) + builder.append(cursor.getString(cursor.getColumnIndex(COLUMN_DATA))).append("\n"); + builder.append("</feed>"); + cursor.close(); + onFeedReceived(mCbid, builder.toString()); + } catch (Exception e) { + Log.w(TAG, String.format("Unable to fetch messages from %s", uri.toString()), e); + e.printStackTrace(); } } + + /** + * Sends a TraFF intent to a source. This encapsulates most of the low-level Android handling. + * + * <p>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. + * + * <p>All intents are sent as explicit ordered broadcasts. This means two things: + * + * <p>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. + * + * <p>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 #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 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 + */ + /* From traff-consumer-android, by the same author and re-licensed under GPL2 for Navit */ + 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<ResolveInfo> 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, + RESULT_INTERNAL_ERROR, // initialCode, + null, // initialData, + null); + } + } + + private 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/navit/attr.c b/navit/attr.c index de2a508f0..b9d54899f 100644 --- a/navit/attr.c +++ b/navit/attr.c @@ -680,6 +680,10 @@ attr_generic_prepend_attr(struct attr **attrs, struct attr *attr) { * * If `attrs` does not contain `attr`, this function is a no-op. * + * Attributes are matched based on their `type` and `u.data` members, thus `attr` can be a shallow copy + * of the attribute, and can match multiple attributes in the list. The `attr` argument itself is not + * changed. + * * @param attrs The attribute list * @param attr The attribute to remove from the list * diff --git a/navit/navit.c b/navit/navit.c index 0886778f0..d94e123cb 100644 --- a/navit/navit.c +++ b/navit/navit.c @@ -1613,8 +1613,6 @@ void navit_set_destination(struct navit *this_, struct pcoord *c, const char *de } g_free(destination_file); - callback_list_call_attr_0(this_->attr_cbl, attr_destination); - if (this_->route) { struct attr attr; int dstcount; @@ -1637,10 +1635,12 @@ void navit_set_destination(struct navit *this_, struct pcoord *c, const char *de g_free(pc); g_free(destination_file); } - - if (this_->ready == 3 && !(this_->flags & 4)) - navit_draw(this_); } + + callback_list_call_attr_0(this_->attr_cbl, attr_destination); + + if (this_->route && this_->ready == 3 && !(this_->flags & 4)) + navit_draw(this_); } /** @@ -1683,15 +1683,30 @@ void navit_set_destinations(struct navit *this_, struct pcoord *c, int count, co g_free(destination_file); } else this_->destination_valid=0; - callback_list_call_attr_0(this_->attr_cbl, attr_destination); - if (this_->route) { + if (this_->route) route_set_destinations(this_->route, c, count, async); - if (this_->ready == 3) - navit_draw(this_); - } + callback_list_call_attr_0(this_->attr_cbl, attr_destination); + if (this_->route && this_->ready == 3) + navit_draw(this_); } +/** + * @brief Retrieves destinations from the route + * + * Prior to calling this method, you may want to retrieve the number of destinations by calling + * {@link navit_get_destination_count(struct navit *)} and assigning a buffer of sufficient capacity. + * + * If the return value equals `count`, the buffer was either just large enough or too small to hold the + * entire list of destinations; there is no way to tell from the result which is the case. + * + * If the Navit instance does not have a route, the result is 0. + * + * @param this_ The Navit instance + * @param pc Pointer to an array of projected coordinates which will receive the destination coordinates + * @param count Capacity of `pc` + * @return The number of destinations stored in `pc`, never greater than `count` + */ int navit_get_destinations(struct navit *this_, struct pcoord *pc, int count) { if(!this_->route) return 0; @@ -1699,6 +1714,12 @@ int navit_get_destinations(struct navit *this_, struct pcoord *pc, int count) { } +/** + * @brief Get the destinations count for the route + * + * @param this The Navit instance + * @return destination count for the route, or 0 if the Navit instance has no route + */ int navit_get_destination_count(struct navit *this_) { if(!this_->route) return 0; @@ -3685,7 +3706,36 @@ int navit_get_blocked(struct navit *this_) { void navit_destroy(struct navit *this_) { dbg(lvl_debug,"enter %p",this_); + GList *mapsets; + struct map * map; + struct attr attr; graphics_draw_cancel(this_->gra, this_->displaylist); + + mapsets = this_->mapsets; + while (mapsets) { + GList *maps = NULL; + struct mapset_handle *msh; + msh = mapset_open(mapsets->data); + while (msh && (map = mapset_next(msh, 0))) { + /* Add traffic map (identified by the `attr_traffic` attribute) to list of maps to remove */ + if (map_get_attr(map, attr_traffic, &attr, NULL)) + maps = g_list_append(maps, map); + } + mapset_close(msh); + + /* Remove traffic maps, if any */ + while (maps) { + attr.type = attr_map; + attr.u.map = maps->data; + mapset_remove_attr(mapsets->data, &attr); + attr_free_content(&attr); + maps = g_list_next(maps); + } + if (maps) + g_list_free(maps); + mapsets = g_list_next(mapsets); + } + callback_list_call_attr_1(this_->attr_cbl, attr_destroy, this_); attr_list_free(this_->attrs); diff --git a/navit/route.c b/navit/route.c index 8e2ef91ec..4c85e3fe9 100644 --- a/navit/route.c +++ b/navit/route.c @@ -939,6 +939,17 @@ struct map_selection *route_selection; /** * @brief Returns a single map selection + * + * The boundaries of the selection are determined as follows: First a rectangle spanning `c1` and `c2` is + * built (the two coordinates can be any two opposite corners of the rectangle). Then its maximum extension + * (height or width) is determined and multiplied with the percentage specified by `rel`. The resulting + * amount of padding is added to each edge. After that, the amount specified by `abs` is added to each edge. + * + * @param order Map order (deepest tile level) to select + * @param c1 First coordinate + * @param c2 Second coordinate + * @param rel Relative padding to add to the selection rectangle, in percent + * @param abs Absolute padding to add to the selection rectangle */ struct map_selection * route_rect(int order, struct coord *c1, struct coord *c2, int rel, int abs) { @@ -1063,7 +1074,7 @@ struct map_selection * route_get_selection(struct route * this_) { * * @param sel Start of the list to be destroyed */ -static void route_free_selection(struct map_selection *sel) { +void route_free_selection(struct map_selection *sel) { struct map_selection *next; while (sel) { next=sel->next; @@ -1130,6 +1141,20 @@ void route_set_destinations(struct route *this, struct pcoord *dst, int count, i profile(0,"end"); } +/** + * @brief Retrieves destinations from the route + * + * Prior to calling this method, you may want to retrieve the number of destinations by calling + * {@link route_get_destination_count(struct route *)} and assigning a buffer of sufficient capacity. + * + * If the return value equals `count`, the buffer was either just large enough or too small to hold the + * entire list of destinations; there is no way to tell from the result which is the case. + * + * @param this The route instance + * @param pc Pointer to an array of projected coordinates which will receive the destination coordinates + * @param count Capacity of `pc` + * @return The number of destinations stored in `pc`, never greater than `count` + */ int route_get_destinations(struct route *this, struct pcoord *pc, int count) { int ret=0; GList *l=this->destinations; diff --git a/navit/route_protected.h b/navit/route_protected.h index 586fde91d..3ce6c4663 100644 --- a/navit/route_protected.h +++ b/navit/route_protected.h @@ -160,6 +160,7 @@ struct route_graph { /* prototypes */ struct route_graph * route_get_graph(struct route *this_); struct map_selection * route_get_selection(struct route * this_); +void route_free_selection(struct map_selection *sel); void route_add_traffic_distortion(struct route *this_, struct item *item); void route_remove_traffic_distortion(struct route *this_, struct item *item); void route_change_traffic_distortion(struct route *this_, struct item *item); diff --git a/navit/traffic.c b/navit/traffic.c index f19560829..2359fea06 100644 --- a/navit/traffic.c +++ b/navit/traffic.c @@ -3842,31 +3842,33 @@ static int traffic_message_is_valid(struct traffic_message * this_) { return 0; } if (!this_->receive_time || !this_->update_time) { - dbg(lvl_debug, "receive_time or update_time not supplied"); + dbg(lvl_debug, "%s: receive_time or update_time not supplied", this_->id); return 0; } if (!this_->is_cancellation) { if (!this_->expiration_time && !this_->end_time) { - dbg(lvl_debug, "not a cancellation, but neither expiration_time nor end_time supplied"); + dbg(lvl_debug, "%s: not a cancellation, but neither expiration_time nor end_time supplied", + this_->id); return 0; } if (!this_->location) { - dbg(lvl_debug, "not a cancellation, but no location supplied"); + dbg(lvl_debug, "%s: not a cancellation, but no location supplied", this_->id); return 0; } if (!traffic_location_is_valid(this_->location)) { - dbg(lvl_debug, "not a cancellation, but location is invalid"); + dbg(lvl_debug, "%s: not a cancellation, but location is invalid", this_->id); return 0; } if (!this_->event_count || !this_->events) { - dbg(lvl_debug, "not a cancellation, but no events supplied"); + dbg(lvl_debug, "%s: not a cancellation, but no events supplied", this_->id); return 0; } for (i = 0; i < this_->event_count; i++) if (this_->events[i]) has_valid_events |= traffic_event_is_valid(this_->events[i]); if (!has_valid_events) { - dbg(lvl_debug, "not a cancellation, but all events (%d in total) are invalid", this_->event_count); + dbg(lvl_debug, "%s: not a cancellation, but all events (%d in total) are invalid", + this_->id, this_->event_count); return 0; } } @@ -4626,7 +4628,6 @@ static struct traffic * traffic_new(struct attr *parent, struct attr **attrs) { navit_object_destroy((struct navit_object *) this_); return NULL; } - navit_object_ref((struct navit_object *) this_); dbg(lvl_debug,"return %p", this_); // TODO do this once and cycle through all plugins @@ -4888,7 +4889,7 @@ static void traffic_xml_end(xml_context *dummy, const char *tag_name, void *data count, (struct traffic_event **) children); if (!traffic_message_is_valid(message)) { - dbg(lvl_error, "malformed message detected, skipping"); + dbg(lvl_error, "%s: malformed message detected, skipping", message->id); traffic_message_destroy(message); } else state->messages = g_list_append(state->messages, message); @@ -4945,7 +4946,9 @@ static void traffic_xml_end(xml_context *dummy, const char *tag_name, void *data state->si = NULL; /* TODO preserve unknown (and thus invalid) events if they have maxspeed set */ if (!traffic_event_is_valid(event)) { - dbg(lvl_debug, "invalid or unknown event detected, skipping"); + dbg(lvl_debug, "invalid or unknown event %s/%s detected, skipping", + traffic_xml_get_attr("class", el->names, el->values), + traffic_xml_get_attr("type", el->names, el->values)); traffic_event_destroy(event); } else state->events = g_list_append(state->events, event); @@ -5800,7 +5803,6 @@ struct map * traffic_get_map(struct traffic *this_) { attrs[4] = NULL; this_->shared->map = map_new(NULL, attrs); - navit_object_ref((struct navit_object *) this_->shared->map); /* populate map with previously stored messages */ filename = g_strjoin(NULL, navit_get_user_data_directory(TRUE), "/traffic.xml", NULL); @@ -5938,6 +5940,13 @@ void traffic_set_route(struct traffic *this_, struct route *rt) { this_->shared->rt = rt; } +void traffic_destroy(struct traffic *this_) { + if (this_->meth.destroy) + this_->meth.destroy(this_->priv); + attr_list_free(this_->attrs); + g_free(this_); +} + struct object_func traffic_func = { attr_traffic, (object_func_new)traffic_new, @@ -5948,7 +5957,7 @@ struct object_func traffic_func = { (object_func_add_attr)navit_object_add_attr, (object_func_remove_attr)navit_object_remove_attr, (object_func_init)NULL, - (object_func_destroy)navit_object_destroy, + (object_func_destroy)traffic_destroy, (object_func_dup)NULL, (object_func_ref)navit_object_ref, (object_func_unref)navit_object_unref, diff --git a/navit/traffic.h b/navit/traffic.h index bf0ca907e..f695ad156 100644 --- a/navit/traffic.h +++ b/navit/traffic.h @@ -58,6 +58,24 @@ extern "C" { #endif /** + * @brief Translates a Navit tile order to a minimum road class as used in TraFF. + * + * This can be used to translate a map selection into a TraFF filter. + * + * The tile order is the lowest tile level in which an object of a certain type can be placed (higher numbers + * correspond to lower levels). Currently, 8 is the maximum order for `highway_city`, `highway_land` and + * `street_n_lanes`, equivalent to `MOTORWAY` and `TRUNK`. 10 is the maximum order for `street_4_city` and + * `street_4_land` (`SECONDARY`), 12 for `street_3_city` and `street_3_land` (`TERTIARY`). All others can + * be placed in any tile level. + * + * This macro returns `PRIMARY`, `SECONDARY` and `TERTIARY` for the three bins above these cut-off orders, + * corresponding to one level below the lowest road class we expect to find there. (Not considering that + * low-level roads can be placed into higher-level tiles if they cross a tile boundary of the next lower + * level.) Below the lowest cut-off order, the macro returns NULL. + */ +#define order_to_min_road_class(x) (x <= 8 ? "PRIMARY" : x <= 10 ? "SECONDARY" : x <= 12 ? "TERTIARY" : NULL) + +/** * @brief Classes for events. */ /* If additional event classes are introduced, traffic_event_is_valid() must be adapted to recognize them. */ @@ -239,6 +257,7 @@ struct traffic_message_priv; */ struct traffic_methods { struct traffic_message **(* get_messages)(struct traffic_priv * this_); /**< Retrieves new messages from the traffic plugin */ + void (*destroy)(struct traffic_priv * this_); /**< Destructor for the traffic plugin */ }; /** @@ -989,6 +1008,11 @@ void traffic_set_mapset(struct traffic *this_, struct mapset *ms); */ void traffic_set_route(struct traffic *this_, struct route *rt); +/** + * @brief Destructor. + */ +void traffic_destroy(struct traffic *this_); + /* end of prototypes */ #ifdef __cplusplus } diff --git a/navit/traffic/dummy/traffic_dummy.c b/navit/traffic/dummy/traffic_dummy.c index b838752dc..2ab4073d4 100644 --- a/navit/traffic/dummy/traffic_dummy.c +++ b/navit/traffic/dummy/traffic_dummy.c @@ -154,6 +154,7 @@ struct traffic_message ** traffic_dummy_get_messages(struct traffic_priv * this_ */ static struct traffic_methods traffic_dummy_meth = { traffic_dummy_get_messages, + NULL, }; /** diff --git a/navit/traffic/null/traffic_null.c b/navit/traffic/null/traffic_null.c index 94546a666..02fc461c6 100644 --- a/navit/traffic/null/traffic_null.c +++ b/navit/traffic/null/traffic_null.c @@ -65,6 +65,7 @@ struct traffic_message ** traffic_null_get_messages(struct traffic_priv * this_) */ static struct traffic_methods traffic_null_meth = { traffic_null_get_messages, + NULL, }; /** diff --git a/navit/traffic/traff_android/traffic_traff_android.c b/navit/traffic/traff_android/traffic_traff_android.c index 266f51a0c..e0862ec5c 100644 --- a/navit/traffic/traff_android/traffic_traff_android.c +++ b/navit/traffic/traff_android/traffic_traff_android.c @@ -22,7 +22,7 @@ * * @brief The TraFF plugin for Android * - * This plugin receives TraFF feeds via Android broadcasts. + * This plugin receives TraFF feeds via Android broadcasts and content providers. */ #include <string.h> @@ -36,13 +36,31 @@ #include "item.h" #include "attr.h" #include "coord.h" +#include "map.h" +#include "route_protected.h" +#include "route.h" +#include "transform.h" #include "xmlconfig.h" #include "android.h" #include "traffic.h" #include "plugin.h" #include "callback.h" +#include "vehicle.h" #include "debug.h" #include "navit.h" +#include "util.h" + +/** + * @brief Minimum area around the current position for which to retrieve traffic updates. + * + * 100000 is equivalent to around 50 km on each side of the current position. The actual subscription area + * can be larger, allowing for a subscription area to be kept over multiple position updates. + * + * The actual subscription area around the current location is stored in + * {@link struct traffic_priv::position_rect} and updated in + * {@link traffic_traff_android_position_callback(struct traffic_priv *, struct navit *, struct vehicle *)}. + */ +#define POSITION_RECT_SIZE 100000 /** * @brief Stores information about the plugin instance. @@ -50,13 +68,38 @@ struct traffic_priv { struct navit * nav; /**< The navit instance */ struct callback * cbid; /**< The callback function for TraFF feeds **/ + int position_valid; /**< Whether Navit currently has a valid position */ + struct coord_rect * position_rect; /**< Rectangle around last known vehicle position (in `projection_mg`) */ + struct map_selection * route_map_sel; /**< Map selection for the current route */ jclass NavitTraffClass; /**< The `NavitTraff` class */ jobject NavitTraff; /**< An instance of `NavitTraff` */ }; +void traffic_traff_android_destroy(struct traffic_priv * this_); struct traffic_message ** traffic_traff_android_get_messages(struct traffic_priv * this_); /** + * @brief Destructor. + */ +void traffic_traff_android_destroy(struct traffic_priv * this_) { + jmethodID cid; + + cid = (*jnienv)->GetMethodID(jnienv, this_->NavitTraffClass, "close", "()V"); + if (cid == NULL) { + dbg(lvl_error,"no method found"); + return; /* exception thrown */ + } + (*jnienv)->CallVoidMethod(jnienv, this_->NavitTraff, cid); + + if (this_->position_rect) + g_free(this_->position_rect); + this_->position_rect = NULL; + if (this_->route_map_sel) + route_free_selection(this_->route_map_sel); + this_->route_map_sel = NULL; +} + +/** * @brief Returns an empty traffic report. * * @return Always `NULL` @@ -70,6 +113,7 @@ struct traffic_message ** traffic_traff_android_get_messages(struct traffic_priv */ static struct traffic_methods traffic_traff_android_meth = { traffic_traff_android_get_messages, + traffic_traff_android_destroy, }; @@ -109,12 +153,133 @@ static void traffic_traff_android_on_feed_received(struct traffic_priv * this_, /** + * @brief Sets the route map selection + * + * @param this_ The instance which will handle the selection update + */ +static void traffic_traff_android_set_selection(struct traffic_priv * this_) { + struct route * route; + struct coord_geo lu, rl; + gchar *filter_list; + jstring j_filter_list; + gchar *min_road_class; + jmethodID cid; + + if (this_->route_map_sel) + route_free_selection(this_->route_map_sel); + this_->route_map_sel = NULL; + if (navit_get_destination_count(this_->nav) && (route = (navit_get_route(this_->nav)))) + this_->route_map_sel = route_get_selection(route); + + /* start building the filter list */ + filter_list = g_strconcat_printf(NULL, "<filter_list>\n"); + if (this_->position_rect) { + transform_to_geo(projection_mg, &this_->position_rect->lu, &lu); + transform_to_geo(projection_mg, &this_->position_rect->rl, &rl); + filter_list = g_strconcat_printf(filter_list, " <filter bbox=\"%.5f %.5f %.5f %.5f\"/>\n", + rl.lat, lu.lng, lu.lat, rl.lng); + } + for (struct map_selection * sel = this_->route_map_sel; sel; sel = sel->next) { + transform_to_geo(projection_mg, &sel->u.c_rect.lu, &lu); + transform_to_geo(projection_mg, &sel->u.c_rect.rl, &rl); + min_road_class = order_to_min_road_class(sel->order); + if (!min_road_class) + filter_list = g_strconcat_printf(filter_list, " <filter bbox=\"%.5f %.5f %.5f %.5f\"/>\n", + rl.lat, lu.lng, lu.lat, rl.lng); + else + filter_list = g_strconcat_printf(filter_list, " <filter min_road_class=\"%s\" bbox=\"%.5f %.5f %.5f %.5f\"/>\n", + min_road_class, rl.lat, lu.lng, lu.lat, rl.lng); + } + /* the trailing \0 is required for NewStringUTF */ + filter_list = g_strconcat_printf(filter_list, "</filter_list>\0"); + j_filter_list = (*jnienv)->NewStringUTF(jnienv, filter_list); + cid = (*jnienv)->GetMethodID(jnienv, this_->NavitTraffClass, "onFilterUpdate", "(Ljava/lang/String;)V"); + if (cid) + (*jnienv)->CallVoidMethod(jnienv, this_->NavitTraff, cid, j_filter_list); + g_free(filter_list); +} + + +/** + * @brief Callback for destination changes + * + * @param this_ The instance which will handle the destination update + */ +static void traffic_traff_android_destination_callback(struct traffic_priv * this_) { + traffic_traff_android_set_selection(this_); +} + + +/** + * @brief Callback for navigation status changes + * + * This callback is necessary to force an update of existing subscriptions when Navit acquires a new + * position (after not having had valid position information), as the map selection will change when + * the current position becomes known for the first time. + * + * @param this_ The instance which will handle the navigation status update + * @param status The status of the navigation engine (the value of the {@code nav_status} attribute) + */ +static void traffic_traff_android_status_callback(struct traffic_priv * this_, int status) { + int new_position_valid = (status != 1); + if (new_position_valid && !this_->position_valid) { + this_->position_valid = new_position_valid; + traffic_traff_android_set_selection(this_); + } else if (new_position_valid != this_->position_valid) + this_->position_valid = new_position_valid; +} + + +/** + * @brief Callback for position changes + * + * This updates {@link struct traffic_priv::position_rect} if the vehicle has moved far enough from its + * center to be within {@link POSITION_RECT_SIZE} of one of its boundaries. The new rectangle is created + * with twice that amount of padding, allowing the vehicle to move for at least that distance before the + * subscription needs to be updated again. + * + * @param this_ The instance which will handle the position update + * @param navit The Navit instance + * @param vehicle The vehicle which delivered the position update and from which the position can be queried + */ +static void traffic_traff_android_position_callback(struct traffic_priv * this_, struct navit *navit, + struct vehicle *vehicle) { + struct attr attr; + struct coord c; + struct coord_rect cr; + jmethodID cid; + if (!vehicle_get_attr(vehicle, attr_position_coord_geo, &attr, NULL)) + return; + transform_from_geo(projection_mg, attr.u.coord_geo, &c); + cr.lu = c; + cr.rl = c; + cr.lu.x -= POSITION_RECT_SIZE; + cr.rl.x += POSITION_RECT_SIZE; + cr.lu.y += POSITION_RECT_SIZE; + cr.rl.y -= POSITION_RECT_SIZE; + if (!this_->position_rect) + this_->position_rect = g_new0(struct coord_rect, 1); + if (!coord_rect_contains(this_->position_rect, &cr.lu) || !coord_rect_contains(this_->position_rect, &cr.rl)) { + cr.lu.x -= POSITION_RECT_SIZE; + cr.rl.x += POSITION_RECT_SIZE; + cr.lu.y += POSITION_RECT_SIZE; + cr.rl.y -= POSITION_RECT_SIZE; + *(this_->position_rect) = cr; + traffic_traff_android_set_selection(this_); + } +} + + +/** * @brief Initializes a traff_android plugin * * @return True on success, false on failure */ static int traffic_traff_android_init(struct traffic_priv * this_) { jmethodID cid; + struct route * route; + struct attr attr; + struct navigation * navigation; if (!android_find_class_global("org/navitproject/navit/NavitTraff", &this_->NavitTraffClass)) return 0; @@ -131,6 +296,15 @@ static int traffic_traff_android_init(struct traffic_priv * this_) { if (this_->NavitTraff) this_->NavitTraff = (*jnienv)->NewGlobalRef(jnienv, this_->NavitTraff); + /* register callbacks for position and destination changes */ + navit_add_callback(this_->nav, callback_new_attr_1(callback_cast(traffic_traff_android_position_callback), + attr_position_coord_geo, this_)); + navit_add_callback(this_->nav, callback_new_attr_1(callback_cast(traffic_traff_android_destination_callback), + attr_destination, this_)); + if ((navigation = navit_get_navigation(this_->nav))) + navigation_register_callback(navigation, attr_nav_status, + callback_new_attr_1(callback_cast(traffic_traff_android_status_callback), attr_nav_status, this_)); + return 1; } @@ -154,6 +328,9 @@ static struct traffic_priv * traffic_traff_android_new(struct navit *nav, struct ret = g_new0(struct traffic_priv, 1); ret->nav = nav; ret->cbid = callback_new_1(callback_cast(traffic_traff_android_on_feed_received), ret); + ret->position_valid = 0; + ret->position_rect = NULL; + ret->route_map_sel = NULL; /* TODO populate members, if any */ *meth = traffic_traff_android_meth; diff --git a/navit/util.c b/navit/util.c index cf4412938..0dfdab20e 100644 --- a/navit/util.c +++ b/navit/util.c @@ -479,6 +479,19 @@ GList *g_hash_to_list_keys(GHashTable *h) { return ret; } +/** + * @brief Appends a formatted string and appends it to an existing one. + * + * Usage is similar to the familiar C functions that take a format string and a variable argument list. + * + * Return value is a concatenation of `buffer` (unless it is NULL) and `fmt`, with the remaining arguments + * inserted into `fmt`. + * + * @param buffer An existing string, can be null and will be freed by this function + * @param fmt A format string (will not be altered) + * + * @return A newly allocated string, see description. The caller is responsible for freeing the returned string. + */ gchar *g_strconcat_printf(gchar *buffer, gchar *fmt, ...) { gchar *str,*ret; va_list ap; diff --git a/navit/xmlconfig.h b/navit/xmlconfig.h index d5697d53c..bcd0ceec9 100644 --- a/navit/xmlconfig.h +++ b/navit/xmlconfig.h @@ -116,7 +116,7 @@ extern struct object_func map_func, mapset_func, navit_func, osd_func, tracking_ layout_func, roadprofile_func, vehicleprofile_func, layer_func, config_func, profile_option_func, script_func, log_func, speech_func, navigation_func, route_func, traffic_func; -#define HAS_OBJECT_FUNC(x) ((x) == attr_map || (x) == attr_mapset || (x) == attr_navit || (x) == attr_osd || (x) == attr_trackingo || (x) == attr_vehicle || (x) == attr_maps || (x) == attr_layout || (x) == attr_roadprofile || (x) == attr_vehicleprofile || (x) == attr_layer || (x) == attr_config || (x) == attr_profile_option || (x) == attr_script || (x) == attr_log || (x) == attr_speech || (x) == attr_navigation || (x) == attr_route) +#define HAS_OBJECT_FUNC(x) ((x) == attr_map || (x) == attr_mapset || (x) == attr_navit || (x) == attr_osd || (x) == attr_trackingo || (x) == attr_vehicle || (x) == attr_maps || (x) == attr_layout || (x) == attr_roadprofile || (x) == attr_vehicleprofile || (x) == attr_layer || (x) == attr_config || (x) == attr_profile_option || (x) == attr_script || (x) == attr_log || (x) == attr_speech || (x) == attr_navigation || (x) == attr_route || (x) == attr_traffic) #define NAVIT_OBJECT struct object_func *func; int refcount; struct attr **attrs; struct navit_object { |