IN THIS ARTICLE
This is the final part of our four-part series on building realtime maps with geolocation tracking using the Google Maps API and PubNub.
What are Flight Paths?
In this tutorial, we’ll add flightpaths using Polylines, which allow you to dynamically draw an updated path through user-specified points on the map. To demo the capabilities, we place a single polyline and update its endpoint on the fly based on random positions within a bounding box.
Tutorial Overview
The code for this example is available in our GitHub repository here.
If you haven’t already, you first need to take care of a couple of prerequisites we covered in Parts One, Two and Three, where we set up our Android environment and covered map markers and location tracking.
Once you’ve done so, move onto the next part.
Android Activities
As previously stated, the MainActivity file is responsible for collecting the username preference, creating the tabbed view (including three tabs), and initializing the PubNub library for realtime communications. In this tutorial, we’ll focus on the third tab, which is responsible for displaying a live updating Polyline.
In this case, the FlightPathsTabContentFragment has handlers for fragment creation and Google Map initialization. We want to make sure that the view is using the correct layout, and that fragment is properly passed in for getMapAsync
callback handler.
public class FlightPathsTabContentFragment extends Fragment implements OnMapReadyCallback { ... @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_flightpaths, container, false); SupportMapFragment mapFragment = (SupportMapFragment) getChildFragmentManager() .findFragmentById(R.id.map_flightpaths); mapFragment.getMapAsync(this); return view; } ...
The Google Maps initialization callback function populates the map attribute with the initialized GoogleMap object and takes care of subscribing to PubNub message events pertaining to location updates. We also invoke a custom scheduleRandomUpdates() method that periodically publishes new location updates to the channel.
In a real-world application, the data source would likely be different – in many cases, location updates will be published by a backend system or another mobile client entirely.
... @Override public void onMapReady(GoogleMap map) { this.map = map; pubNub.addListener(new FlightPathsPnCallback(new FlightPathsMapAdapter((Activity) this.getContext(), map), Constants.FLIGHTPATHS_CHANNEL_NAME)); pubNub.subscribe().channels(Arrays.asList(Constants.FLIGHTPATHS_CHANNEL_NAME)).execute(); scheduleRandomUpdates(); } ...
In the code above, you see two primary interactions with the PubNub API. We register a listener with the realtime data streams to receive events (from one or more channels), and then subscribe to a specific channel (or channels) to receive updates.
PubNub Events
There are two key things to note about the PubNub subscription and listener. We create a FlightPathsPnCallback object to satisfy the PubNub callback API. We create a FlightPathsMapAdapter to handle updates to the UI.
The reason for this separation is that the PubNub events are coming in asynchronously, whereas UI updates need to be propagated and queued for running on the main UI thread.
The FlightPathsPnCallback is event-driven and has no dynamic state – it just parses messages (only from the relevant channel) and propagates logical events to the Adapter class.
public class FlightPathsPnCallback extends SubscribeCallback { ... @Override public void message(PubNub pubnub, PNMessageResult message) { if (!message.getChannel().equals(watchChannel)) { return; } try { Log.d(TAG, "message: " + message.toString()); Map<String, String> newLocation = JsonUtil.fromJson(message.getMessage().toString(), LinkedHashMap.class); flightPathsMapAdapter.locationUpdated(newLocation); } catch (Exception e) { throw new RuntimeException(e); } } ... }
The FlightPathsMapAdapter receives the incoming events and updates the UI accordingly. For the sake of simplicity, we use a java.util.Map to pass data between the PubNub callback and the Adapter.
In a larger example, one should consider using a specialized value object (bean) for type safety and enforce the contract between PnCallback and Adapter.
Polylines
The key thing to remember in this code is the need to perform UI updates in the UI thread – we reduce the amount of code in the doUiUpdate method so that it processes as quickly as possible.
public class FlightPathsMapAdapter { ... private List<LatLng> polylinePoints; ... public void locationUpdated(final Map<String, String> newLocation) { if (newLocation.containsKey("lat") && newLocation.containsKey("lng")) { String lat = newLocation.get("lat"); String lng = newLocation.get("lng"); doUiUpdate(new LatLng(Double.parseDouble(lat), Double.parseDouble(lng))); } else { Log.w(TAG, "message ignored: " + newLocation.toString()); } } private void doUiUpdate(final LatLng location) { activity.runOnUiThread(new Runnable() { @Override public void run() { polylinePoints.add(location); if (polyline != null) { polyline.setPoints(polylinePoints); } else { polyline = map.addPolyline(new PolylineOptions().addAll(polylinePoints)); } map.moveCamera(CameraUpdateFactory.newLatLng(location)); } }); } }
At this point, we have integrated realtime flight paths into our Android application. For reference, we’ll also show you the implementation of the scheduleRandomUpdates() method that publishes the location updates to which the application subscribes.
Location Publish Functionality
In the code below, we set up a single-threaded repeating task to publish updates to the channel. Each message contains a new latitude and longitude pair that is moving northeast (increasing latitude and longitude).
In your application, you would likely have a different mechanism for determining location – via Android location API, or a user’s self-reported location for example.
public class FlightPathsTabContentFragment extends Fragment implements OnMapReadyCallback { ... private static ImmutableMap<String, String> getNewLocationMessage(String userName, int randomLat, int randomLng, long elapsedTime) { String newLat = Double.toString(generatorOriginLat + ((randomLat + elapsedTime) * 0.000003)); String newLng = Double.toString(generatorOriginLng + ((randomLng + elapsedTime) * 0.00001)); return ImmutableMap.<String, String>of("who", userName, "lat", newLat, "lng", newLng); } ... private void scheduleRandomUpdates() { this.executorService = Executors.newSingleThreadScheduledExecutor(); this.startTime = System.currentTimeMillis(); this.executorService.scheduleAtFixedRate(new Runnable() { @Override public void run() { ((Activity) FlightPathsTabContentFragment.this.getContext()).runOnUiThread(new Runnable() { @Override public void run() { int randomLat = random.nextInt(10); int randomLng = random.nextInt(10); long elapsedTime = System.currentTimeMillis() - startTime; final Map<String, String> message = getNewLocationMessage(userName, randomLat, randomLng, elapsedTime); pubNub.publish().channel(Constants.FLIGHTPATHS_CHANNEL_NAME).message(message).async( new PNCallback<PNPublishResult>() { @Override public void onResponse(PNPublishResult result, PNStatus status) { try { if (!status.isError()) { Log.v(TAG, "publish(" + JsonUtil.asJson(result) + ")"); } else { Log.v(TAG, "publishErr(" + status.toString() + ")"); } } catch (Exception e) { e.printStackTrace(); } } } ); } }); } }, 0, 5, TimeUnit.SECONDS); } }
Wrapping Up
And that’s it! We hope you found this tutorial series helpful. PubNub and the Google Maps API work wonderfully well together, Google Maps delivering powerful mapping features and functionality, and PubNub connecting them to devices in realtime.
Please reach out if you find any issues, we want to hear from you!