This is Part Two of our four-part series on building real-time maps with geolocation tracking using the Google Maps API and PubNub.
What are Map Markers?
In this tutorial, we'll add map markers to our Android map. Map markers identify a location on a map through a customizable icon that is overlayed on the map. To showcase map marker functionality, we'll place a single map marker and update its location on the fly based on random positions within a bounding box.
Tutorial Overview
If you haven't already, you first need to take care of a couple of prerequisites we covered in Part One. These include:
- Creating your PubNub application, including Publish and Subscribe Keys
- Creating your Google Maps API Project and Credentials
The code for this example is available in our GitHub repository here.
Android Activities
Now that we have all of the relevant configuration settings we took care of in Part One, we can look at the actual working code.
The MainActivity file is responsible for collecting the username preference, creating the tabbed view (including 3 tabs), and initializing the PubNub library for real-time communications. In this tutorial, we’ll focus on the first tab, which is responsible for displaying a live updating Map marker.
public class MainActivity extends AppCompatActivity { ... @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mSharedPrefs = getSharedPreferences(Constants.DATASTREAM_PREFS, MODE_PRIVATE); if (!mSharedPrefs.contains(Constants.DATASTREAM_UUID)) { Intent toLogin = new Intent(this, LoginActivity.class); startActivity(toLogin); return; } setContentView(R.layout.activity_main); this.random = new Random(); this.userName = mSharedPrefs.getString(Constants.DATASTREAM_UUID, "anonymous_" + random.nextInt(10000)); this.pubNub = initPubNub(this.userName); TabLayout tabLayout = (TabLayout) findViewById(R.id.tab_layout); tabLayout.addTab(tabLayout.newTab().setText("1_Marker")); tabLayout.addTab(tabLayout.newTab().setText("2_Live_Location")); tabLayout.addTab(tabLayout.newTab().setText("3_Flightpath")); tabLayout.setTabGravity(TabLayout.GRAVITY_FILL); final ViewPager viewPager = (ViewPager) findViewById(R.id.pager); final MainActivityTabManager adapter = new MainActivityTabManager (getSupportFragmentManager(), tabLayout.getTabCount(), pubNub); viewPager.setAdapter(adapter); viewPager.addOnPageChangeListener(new TabLayout.TabLayoutOnPageChangeListener(tabLayout)); tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() { @Override public void onTabSelected(TabLayout.Tab tab) { viewPager.setCurrentItem(tab.getPosition()); } @Override public void onTabUnselected(TabLayout.Tab tab) { } @Override public void onTabReselected(TabLayout.Tab tab) { } }); } @NonNull private PubNub initPubNub(String userName) { PNConfiguration pnConfiguration = new PNConfiguration(); pnConfiguration.setPublishKey(Constants.PUBNUB_PUBLISH_KEY); pnConfiguration.setSubscribeKey(Constants.PUBNUB_SUBSCRIBE_KEY); pnConfiguration.setSecure(true); pnConfiguration.setUuid(userName); return new PubNub(pnConfiguration); } }
When we create the TabManager (a container for the three tab fragments), we pass in the PubNub object so that each tab may be initialized properly.
In this case, the LocationSubscribeTabContentFragment 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 LocationSubscribeTabContentFragment extends Fragment implements OnMapReadyCallback { ... @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_locationsubscribe, container, false); SupportMapFragment mapFragment = (SupportMapFragment) getChildFragmentManager() .findFragmentById(R.id.map_locationsubscribe); 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 LocationSubscribePnCallback(new LocationSubscribeMapAdapter((Activity) this.getContext(), map), Constants.SUBSCRIBE_CHANNEL_NAME)); pubNub.subscribe().channels(Arrays.asList(Constants.SUBSCRIBE_CHANNEL_NAME)).execute(); scheduleRandomUpdates(); } ...
In the code above, you see two primary interactions with the PubNub API. We register a listener with the real-time 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
LocationSubscribePnCallback
object to satisfy the PubNub callback API. - We create a
LocationSubscribeMapAdapter
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 LocationSubscribePnCallback 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 LocationSubscribePnCallback extends SubscribeCallback { ... @Override public void message(PubNub pubnub, PNMessageResult message) { if (!message.getChannel().equals(watchChannel)) { return; } try { Log.d(TAG + "/PN_MESSAGE", "message: " + message.toString()); Map<String, String> newLocation = JsonUtil.fromJson(message.getMessage().toString(), LinkedHashMap.class); locationMapAdapter.locationUpdated(newLocation); } catch (Exception e) { throw new RuntimeException(e); } } ... }
The LocationSubscribeMapAdapter 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. 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 LocationSubscribeMapAdapter { ... 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() { if (marker != null) { marker.setPosition(location); } else { marker = map.addMarker(new MarkerOptions().position(location)); } map.moveCamera(CameraUpdateFactory.newLatLng(location)); } }); } }
At this point, we have integrated real-time location updates 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 (which will cover in Part Three).
public class LocationSubscribeTabContentFragment 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.scheduleAtFixedRate(new Runnable() { @Override public void run() { ((Activity) LocationSubscribeTabContentFragment.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.SUBSCRIBE_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(" + JsonUtil.asJson(status) + ")"); } } catch (Exception e) { e.printStackTrace(); } } } ); } }); } }, 0, 5, TimeUnit.SECONDS); } }
Next Steps
With that, we now have our Android application, and we're able to plot map markers. The next step is to detect the device's location and plot it in real time, moving it as the location changes, which we'll cover in Part 3 on geolocation!