itachi1706/SingBuses

View on GitHub
app/src/main/java/com/itachi1706/busarrivalsg/NTUBusActivity.java

Summary

Maintainability
A
3 hrs
Test Coverage
package com.itachi1706.busarrivalsg;

import android.Manifest;
import android.annotation.SuppressLint;
import android.app.AlertDialog;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.preference.PreferenceManager;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;

import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.collection.ArrayMap;
import androidx.core.app.ActivityCompat;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;

import com.google.android.gms.maps.CameraUpdateFactory;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.MapView;
import com.google.android.gms.maps.UiSettings;
import com.google.android.gms.maps.model.BitmapDescriptor;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.Marker;
import com.google.android.gms.maps.model.MarkerOptions;
import com.google.android.gms.maps.model.PolylineOptions;
import com.google.android.material.bottomsheet.BottomSheetBehavior;
import com.google.android.material.switchmaterial.SwitchMaterial;
import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
import com.itachi1706.busarrivalsg.AsyncTasks.GetNTUData;
import com.itachi1706.busarrivalsg.AsyncTasks.GetNTUPublicBusData;
import com.itachi1706.busarrivalsg.AsyncTasks.QueryNTUStops;
import com.itachi1706.busarrivalsg.Services.LocManager;
import com.itachi1706.busarrivalsg.gsonObjects.sgLTA.BusArrivalArrayObject;
import com.itachi1706.busarrivalsg.gsonObjects.sgLTA.BusArrivalArrayObjectEstimate;
import com.itachi1706.busarrivalsg.gsonObjects.sgLTA.BusArrivalMain;
import com.itachi1706.busarrivalsg.gsonObjects.sgLTA.BusStopJSON;
import com.itachi1706.busarrivalsg.objects.CommonEnums;
import com.itachi1706.busarrivalsg.objects.gson.ntubuses.NTUBus;
import com.itachi1706.busarrivalsg.util.BusesUtil;
import com.itachi1706.busarrivalsg.util.NTURouteCacher;
import com.itachi1706.busarrivalsg.util.OnMapViewReadyListener;
import com.itachi1706.busarrivalsg.util.StaticVariables;
import com.itachi1706.helperlib.concurrent.Constants;
import com.itachi1706.helperlib.concurrent.CoroutineAsyncTask;
import com.itachi1706.helperlib.helpers.LogHelper;

import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;

public class NTUBusActivity extends AppCompatActivity implements OnMapViewReadyListener.OnGlobalMapReadyListener, GoogleMap.OnInfoWindowClickListener {

    SwitchMaterial campusRed, campusBlue, campusRider, campusWeekend, traffic, sbs;
    MapView mapView;
    private GoogleMap mMap;

    private static final String TAG = "NTUBus";

    public static final String RECEIVE_NTU_DATA_EVENT = "RecieveNTUDataEvent";
    public static final String RECEIVE_NTU_PUBLIC_BUS_DATA_EVENT = "RecieveNTUBDataEvent";

    private final BusesUtil busesUtil = BusesUtil.INSTANCE;
    private int autoRefreshDelay = -1;
    private SharedPreferences sp;

    private BottomSheetBehavior<View> bottomSheetBehavior;
    private View bottomSheet;

    private boolean announceErr = false;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_ntubus_with_sheet);

        if (getSupportActionBar() != null) {
            getSupportActionBar().setDisplayHomeAsUpEnabled(true);
            Bitmap home = busesUtil.vectorToBitmap(R.drawable.ic_ntu_coa, this, null);
            Drawable d = new BitmapDrawable(getResources(), Bitmap.createScaledBitmap(home, (int) busesUtil.pxFromDp(26, getResources()),
                    (int) busesUtil.pxFromDp(32, getResources()), true));
            getSupportActionBar().setHomeAsUpIndicator(d);
        }

        mapView = findViewById(R.id.mapView);
        campusRed = findViewById(R.id.ntu_clr_switch);
        campusBlue = findViewById(R.id.ntu_clb_switch);
        campusRider = findViewById(R.id.ntu_cr_switch);
        campusWeekend = findViewById(R.id.ntu_crw_switch);
        sbs = findViewById(R.id.ntu_sbs_switch);
        traffic = findViewById(R.id.ntu_traffic_switch);

        // Init Bottom Sheet
        bottomSheet = findViewById(R.id.bottom_sheet);
        bottomSheetBehavior = BottomSheetBehavior.from(bottomSheet);
        bottomSheetBehavior.setPeekHeight(200);
        bottomSheetBehavior.setHideable(true);
        bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);

        // Init Map
        mapView.onCreate(savedInstanceState);
        new OnMapViewReadyListener(mapView, this);
        LogHelper.i(TAG, "Creating Map");

        trafficEnabled = traffic.isChecked();
        traffic.setOnCheckedChangeListener((buttonView, isChecked) -> {
            trafficEnabled = isChecked;
            mMap.setTrafficEnabled(trafficEnabled);
        });

        sp = PreferenceManager.getDefaultSharedPreferences(this);
        // Manually set checked state
        campusRed.setChecked(sp.getBoolean("ntu_bus_red", false));
        campusBlue.setChecked(sp.getBoolean("ntu_bus_blue", false));
        campusRider.setChecked(sp.getBoolean("ntu_bus_green", false));
        campusWeekend.setChecked(sp.getBoolean("ntu_bus_brown", false));
        sbs.setChecked(sp.getBoolean("ntu_bus_sbs", false));

        campusRed.setOnCheckedChangeListener((buttonView, isChecked) -> {
            sp.edit().putBoolean("ntu_bus_red", isChecked).apply();
            getData(false);
        });
        campusBlue.setOnCheckedChangeListener((buttonView, isChecked) -> {
            sp.edit().putBoolean("ntu_bus_blue", isChecked).apply();
            getData(false);
        });
        campusRider.setOnCheckedChangeListener((buttonView, isChecked) -> {
            sp.edit().putBoolean("ntu_bus_green", isChecked).apply();
            getData(false);
        });
        campusWeekend.setOnCheckedChangeListener((buttonView, isChecked) -> {
            sp.edit().putBoolean("ntu_bus_brown", isChecked).apply();
            getData(false);
        });
        sbs.setOnCheckedChangeListener((buttonView, isChecked) -> {
            sp.edit().putBoolean("ntu_bus_sbs", isChecked).apply();
            getData(false);
        });
    }

    @Override
    public void onResume() {
        super.onResume();

        mapView.onResume();
        LocalBroadcastManager.getInstance(this).registerReceiver(receiver, new IntentFilter(RECEIVE_NTU_DATA_EVENT));
        LocalBroadcastManager.getInstance(this).registerReceiver(publicBusReceiver, new IntentFilter(RECEIVE_NTU_PUBLIC_BUS_DATA_EVENT));

        autoRefreshDelay = Integer.parseInt(sp.getString("ntushuttlerefrate", "10"));
        if (autoRefreshDelay < 5) autoRefreshDelay = 5;
        shouldAutoRefresh = true;
    }

    @Override
    public void onPause() {
        super.onPause();

        mapView.onPause();
        shouldAutoRefresh = false;
        if (refreshHandler != null) refreshHandler.removeMessages(REFRESH_TASK);
        if (receiver != null) LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver);
        if (publicBusReceiver != null) LocalBroadcastManager.getInstance(this).unregisterReceiver(publicBusReceiver);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.menu_ntu_buses, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        int id = item.getItemId();

        if (id == R.id.action_settings) startActivity(new Intent(this, MainSettings.class));
        else if (id == android.R.id.home) finish();
        else if (id == R.id.refresh) {
            LogHelper.i(TAG, "Manually refreshing bus data at " + StaticVariables.INSTANCE.convertDateToString(new Date(System.currentTimeMillis())));
            getData(true);
        } else return super.onOptionsItemSelected(item);

        return true;
    }

    private boolean trafficEnabled = false;
    private boolean mapReady = false;

    @Override
    public void onMapReady(GoogleMap googleMap) {
        LogHelper.d(TAG, "onMapReady()");
        mMap = googleMap;
        mMap.setTrafficEnabled(trafficEnabled);
        checkIfYouHaveGpsPermissionForThis();
        mMap.setOnInfoWindowClickListener(this);
        UiSettings settings = mMap.getUiSettings();
        settings.setZoomControlsEnabled(true);
        settings.setMapToolbarEnabled(false);
        mapReady = true;

        LogHelper.d(TAG, "Map Created");

        // Enable all toggles
        campusWeekend.setEnabled(true);
        campusRider.setEnabled(true);
        campusRed.setEnabled(true);
        campusBlue.setEnabled(true);
        traffic.setEnabled(true);

        mMap.setOnMapLoadedCallback(() -> mMap.animateCamera(CameraUpdateFactory.newLatLngZoom(new LatLng(1.3478184567642855, 103.68342014685716), 15.4f))); // Hardcode center of school

        refreshHandler = new Handler();
        getData(false);

        // Bottom Sheet handler
        mMap.setOnMapClickListener(latLng -> bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN));
        mMap.setOnInfoWindowClickListener(this::updateBottomSheetIfAny);
        mMap.setOnMarkerClickListener(marker -> {
            updateBottomSheetIfAny(marker);
            return true;
        });
    }

    private void updateBottomSheetIfAny(Marker marker) {
        marker.showInfoWindow();
        if (!marker.getSnippet().startsWith("Next Stop: ")) {
            bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN); // Make sure its gone
            return; // Don't do anything as it is not stop markers
        }

        TextView main = bottomSheet.findViewById(R.id.detail_name);
        TextView sub = bottomSheet.findViewById(R.id.detail_subtext);
        ProgressBar inProgress = bottomSheet.findViewById(R.id.progress_loading);
        TextView result = bottomSheet.findViewById(R.id.timings);
        sub.setText(marker.getSnippet());
        main.setText(marker.getTitle());
        inProgress.setVisibility(View.VISIBLE);
        result.setVisibility(View.GONE);
        if (marker.getTag() == null || !(marker.getTag() instanceof NTUBus.MapNodes)) {
            // Nothing alr
            result.setText("No Timings Data Found\nDebug Error: Invalid Tag");
            result.setVisibility(View.VISIBLE);
            inProgress.setVisibility(View.GONE);
        } else {
            NTUBus.MapNodes n = (NTUBus.MapNodes) marker.getTag();
            sub.setText(marker.getSnippet() + "\nCurrent Stop ID: " + n.getId());
            if (n.getId() == 0) {
                // This is an error so we tell the user to clear cache if automatic clearing fails
                new NTURouteCacher(this).clearAllCachedFile();
                result.setText("An error has occurred. We have cleared your cache files. Please reopen the application and try again." +
                        "\n\nIf it still fails, try clearing the application cache.\n" +
                        "You can do so by going to your Phone Settings -> Applications -> Bus Arrivals @ SG -> Storage -> Clear Cache\n" +
                        "Reopen the application afterwards and timing data should appear");
                result.setVisibility(View.VISIBLE);
                inProgress.setVisibility(View.GONE);
            } else new QueryNTUStops(this, sub.getText().toString(), (error, resultText, title, subtext) -> {
                if (!error) {
                    main.setText(title);
                    sub.setText(subtext);
                }
                result.setText(resultText);
                inProgress.setVisibility(View.GONE);
                result.setVisibility(View.VISIBLE);
            }).execute(n.getId());
        }
        bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
    }

    private void getData(boolean refresh) {
        List<String> get = new ArrayList<>();
        if (campusRider.isChecked()) get.add("green");
        if (campusRed.isChecked()) get.add("red");
        if (campusBlue.isChecked()) get.add("blue");
        if (campusWeekend.isChecked()) get.add("brown");

        if (!mapReady) return;
        if (get.isEmpty()) {
            mMap.clear();
            if (sbs.isChecked()) new GetNTUPublicBusData(this, false).execute(); // Do not need to autorefresh every few seconds as it doesnt update fast anyway
            return;
        }
        if (!refresh) {
            campusRed.setEnabled(false);
            campusBlue.setEnabled(false);
            campusRider.setEnabled(false);
            campusWeekend.setEnabled(false);
        }
        if (runningBus == null || runningBus.getStatus().equals(Constants.Status.FINISHED) || runningBus.isCancelled()) {
            runningBus = new GetNTUData(this, refresh);
            runningBus.execute(get.toArray(new String[0]));
        }
        if (sbs.isChecked() && (runningPBus == null || runningPBus.getStatus().equals(Constants.Status.FINISHED) || runningPBus.isCancelled())) {
            runningPBus = new GetNTUPublicBusData(this, refresh);
            runningPBus.execute();
        }
        if (!refreshHandler.hasMessages(REFRESH_TASK) && shouldAutoRefresh) {
            Message ref = Message.obtain(refreshHandler, refreshTask);
            ref.what = REFRESH_TASK;
            refreshHandler.sendMessageDelayed(ref, autoRefreshDelay * 1000);
        }
    }

    private void checkIfYouHaveGpsPermissionForThis() {
        int rc = ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION);
        if (rc == PackageManager.PERMISSION_GRANTED) {
            mMap.setMyLocationEnabled(true);
        } else {
            requestGpsPermission();
        }
    }

    private void requestGpsPermission() {
        LogHelper.w(LocManager.TAG, "GPS permission is not granted. Requesting permission");
        final String[] permissions = new String[]{Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION};

        if (!ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.ACCESS_COARSE_LOCATION)) {
            requestGps.launch(permissions);
            return;
        }

        new AlertDialog.Builder(this).setTitle(R.string.dialog_title_request_permission_gps)
                .setMessage(R.string.dialog_message_request_permission_gps_view_map_rationale)
                .setPositiveButton(android.R.string.ok, (dialog, which) -> requestGps.launch(permissions)).show();
    }

    @SuppressLint("MissingPermission") // This is basically a permission check alr
    private final ActivityResultLauncher<String[]> requestGps = registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(),
            result -> {
                boolean hasPerm = StaticVariables.INSTANCE.checkIfCoraseLocationGranted(result);

                if (hasPerm) {
                    LogHelper.d(LocManager.TAG, "Location permission granted - enabling my location");
                    // we have permission, so set my location to enabled
                    mMap.setMyLocationEnabled(true);
                } else {
                    LogHelper.e(LocManager.TAG, "Permission not granted");
                    Toast.makeText(this, "No Permission for current location", Toast.LENGTH_SHORT).show();
                }
            });

    @Override
    public void onInfoWindowClick(Marker marker) {
        if (marker.getTag() == null) return;
        Object type = marker.getTag();
        LogHelper.d("NTU-BUS-MAP", "Indo clicked of " + type.getClass());
        if (type instanceof BusStopJSON) {
            BusStopJSON json = (BusStopJSON) type;
            json.getBusStopCode();
            json.getDescription();

            Intent pBusIntent = new Intent(this, BusServicesAtStopRecyclerActivity.class);
            pBusIntent.putExtra("stopCode", json.getBusStopCode());
            pBusIntent.putExtra("stopName", json.getDescription());
            startActivity(pBusIntent);
        }

    }

    private final ArrayList<Marker> busMarkers = new ArrayList<>();
    private final ArrayList<Marker> publicBusMarkers = new ArrayList<>();

    private CoroutineAsyncTask<String, Void, Integer> runningBus = null;
    private CoroutineAsyncTask<Void, Void, Integer> runningPBus = null;

    private Handler refreshHandler;
    private boolean shouldAutoRefresh = false;
    public static final int REFRESH_TASK = 3000;
    private final Runnable refreshTask = () -> {
        LogHelper.i(TAG, "Auto-refreshing bus data at " + StaticVariables.INSTANCE.convertDateToString(new Date(System.currentTimeMillis())));
        getData(true);
    };

    /**
     * Parsing and processing the data received from the API call
     * Might also make it in a async task in the future
     */
    private final BroadcastReceiver receiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            boolean err = intent.getBooleanExtra("err", false);
            if (err) {
                LogHelper.e(TAG, "Some error occurred");
                if (!announceErr) {
                    announceErr = true;
                    new AlertDialog.Builder(context).setTitle("Unable to contact NTU Bus API")
                            .setMessage("We are having trouble connecting to the API. It may be down currently. For more information, please check with NTUSU")
                            .setPositiveButton(R.string.dialog_action_positive_close, null).show();
                }
            }
            String data = intent.getStringExtra("data");
            int update = intent.getIntExtra("update", 0);
            if (data == null) return;
            Gson gson = new Gson();
            NTUBus busObj;
            try {
                busObj = gson.fromJson(data, NTUBus.class);
            } catch (JsonSyntaxException e) {
                Toast.makeText(context, "An error occurred. Please try again later", Toast.LENGTH_LONG).show();
                return;
            }
            if (busObj == null) return;
            assert busObj.getRoutes() != null;
            if (busObj.getRoutes().length <= 0) return;

            if (update == 0) {
                mMap.clear();
                // Readd public bus markers if any
                if (publicBusMarkers.size() > 0) {
                    List<Marker> tmp = new ArrayList<>();
                    BitmapDescriptor bus = busesUtil.vectorToBitmapDescriptor(R.drawable.ic_bus, context, getRouteColor(199179));
                    for (Marker m : publicBusMarkers) {
                        tmp.add(mMap.addMarker(new MarkerOptions().position(m.getPosition()).title(m.getTitle()).snippet(m.getSnippet()).icon(bus)));
                    }
                    publicBusMarkers.clear();
                    publicBusMarkers.addAll(tmp);
                }
                busMarkers.clear();

                @Nullable NTUBus.MapPoints centerOn = null;
                if (busObj.getRoutes() != null) {
                    List<LatLng> mapToDraw = new ArrayList<>();
                    for (NTUBus.Route r : busObj.getRoutes()) {
                        if (r.getRoute() != null) {
                            mapToDraw.clear();
                            assert r.getRoute().getCenter() != null;
                            assert r.getRoute().getNodes() != null;
                            if (r.getRoute().getCenter().length > 0)
                                centerOn = r.getRoute().getCenter()[0];
                            BitmapDescriptor stop = busesUtil.vectorToBitmapDescriptor(R.drawable.ic_circle, context, getRouteColor(r.getId()));
                            for (NTUBus.MapNodes node : r.getRoute().getNodes()) {
                                if (node.is_stop_point()) {
                                    mMap.addMarker(new MarkerOptions().position(new LatLng(node.getLat(), node.getLon()))
                                            .title(node.getName())
                                            .snippet("Next Stop: " + node.getShort_direction())
                                            .icon(stop)).setTag(node);
                                }
                                assert node.getPoints() != null;
                                if (node.getPoints().length > 0) {
                                    for (NTUBus.MapPoints p : node.getPoints()) {
                                        mapToDraw.add(new LatLng(p.getLat(), p.getLon()));
                                    }
                                }
                            }

                            // Check for bus objects
                            busMarkers.addAll(addBusesIntoRoute(r));

                            // Draw on Map Object
                            PolylineOptions polylineOptions = new PolylineOptions();
                            polylineOptions.addAll(mapToDraw);
                            polylineOptions.width(10);
                            // Set Colors
                            polylineOptions.color(getRouteColor(r.getId()));
                            mMap.addPolyline(polylineOptions);

                            LogHelper.i(TAG, "Generated " + r.getRoutename());
                        }
                    }
                }

                LatLng myLatLng;
                if (centerOn != null) myLatLng = new LatLng(centerOn.getLat(), centerOn.getLon());
                else myLatLng = new LatLng(1.3478184567642855, 103.68342014685716); // Hardcode center of school
                mMap.setOnMapLoadedCallback(() -> mMap.animateCamera(CameraUpdateFactory.newLatLngZoom(myLatLng, 15.4f)));
            } else {
                // Only update buses
                for (Marker m : busMarkers) {
                    m.remove();
                }
                busMarkers.clear();
                if (busObj.getRoutes() != null) {
                    for (NTUBus.Route r : busObj.getRoutes()) {
                        busMarkers.addAll(addBusesIntoRoute(r));
                    }
                }
            }

            campusWeekend.setEnabled(true);
            campusRider.setEnabled(true);
            campusRed.setEnabled(true);
            campusBlue.setEnabled(true);
        }
    };

    private List<Marker> addBusesIntoRoute(NTUBus.Route r) {
        List<Marker> markers = new ArrayList<>();
        if (r.getVehicles() != null && r.getVehicles().length > 0) {
            BitmapDescriptor bus = busesUtil.vectorToBitmapDescriptor(R.drawable.ic_bus, this, getRouteColor(r.getId()));
            for (NTUBus.Vehicles v : r.getVehicles()) {
                // TODO: Find a way to do the bearing lol
                /*Bitmap arrow = busesUtil.vectorToBitmap(R.drawable.ic_chevron, getResources(), getRouteColor(r.getId()));
                Matrix matrix = new Matrix();
                matrix.setRotate(v.getBearing());
                arrow = Bitmap.createBitmap(arrow, 0, 0, arrow.getWidth(), arrow.getHeight(), matrix, true);*/
                markers.add(mMap.addMarker(new MarkerOptions().position(new LatLng(v.getLatVal(), v.getLonVal()))
                        .title(v.getLicense_no()).snippet("Speed: " + v.getSpeed() + " km/h | Bearing: " + v.getBearing() + " (" + getBDirection(v.getBearing()) + ")")
                        .icon(bus)));
            }
        }
        return markers;
    }

    private String getBDirection(int bearing) {
        String[] directions = {"N", "NE", "E", "SE", "S", "SW", "W", "NW"};
        return directions[ (int)Math.round((  ((double) Math.abs(bearing) % 360) / 45)) % 8 ];
    }

    private int getRouteColor(int id) {
        switch (id) {
            case 44478: return Color.RED;
            case 44479: return Color.BLUE;
            case 44480: return Color.GREEN;
            case 44481: return Color.parseColor("#964B00");
            case 199179: return Color.parseColor("#800080"); // SBST
            default: return Color.BLACK;
        }
    }

    /**
     * Parsing and processing the data received for public API calls
     */
    private final BroadcastReceiver publicBusReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            String data = intent.getStringExtra("data");
            boolean update = intent.getBooleanExtra("update", false);
            if (data == null) return;
            Gson gson = new Gson();
            if (!update) {
                BusStopJSON[] tmpJSON;
                try {
                    tmpJSON = gson.fromJson(data, BusStopJSON[].class);
                }catch (JsonSyntaxException e) {
                    Toast.makeText(context, "An error occurred parsing public bus stops. Please try again later", Toast.LENGTH_LONG).show();
                    return;
                }
                // Convert to something workable and unique
                ArrayMap<String, BusStopJSON> busStops = new ArrayMap<>();
                for (BusStopJSON j : tmpJSON) {
                    busStops.put(j.getBusStopCode(), j);
                }
                BitmapDescriptor stop = busesUtil.vectorToBitmapDescriptor(R.drawable.ic_circle, context, getRouteColor(199179));
                for (ArrayMap.Entry<String, BusStopJSON> entry : busStops.entrySet()) {
                    BusStopJSON node = entry.getValue();
                    String svcWork = node.getServices();
                    String[] svces = svcWork.split(",");
                    StringBuilder s = new StringBuilder();
                    for (String s1 : svces) {
                        String[] svcTmp = s1.split(":");
                        s.append(svcTmp[0]).append(", ");
                    }
                    String services = s.toString();
                    services = services.replaceAll(", $", "");
                    mMap.addMarker(new MarkerOptions().position(new LatLng(node.getLatitude(), node.getLongitude()))
                            .title(node.getDescription() + " (" + node.getRoadName() + ")")
                            .snippet("Bus Svcs: " + services)
                            .icon(stop)).setTag(node);
                    LogHelper.i(TAG, "Generated Public Bus Stops");
                }
            } else {
                BusArrivalMain[] busObjsArr;
                try {
                    busObjsArr = gson.fromJson(data, BusArrivalMain[].class);
                } catch (JsonSyntaxException e) {
                    Toast.makeText(context, "An error occurred parsing public buses. Please try again later", Toast.LENGTH_LONG).show();
                    return;
                }

                for (BusArrivalMain busObjs : busObjsArr) {
                    if (busObjs == null || busObjs.getServices() == null) continue;
                    if (busObjs.getServices().length <= 0) continue;

                    BusArrivalArrayObject o = busObjs.getServices()[0];
                    // Remove bus markers for this specific service
                    Iterator<Marker> iter = publicBusMarkers.iterator();
                    while (iter.hasNext()) {
                        Marker m = iter.next();

                        if (m.getTitle().equals(o.getServiceNo() + " (" + o.getOperator() + ")")) {
                            m.remove();
                            iter.remove();
                        }
                    }
                    BusArrivalArrayObjectEstimate e1 = o.getNextBus();
                    addPublicBuses(e1, o);
                    BusArrivalArrayObjectEstimate e2 = o.getNextBus2();
                    addPublicBuses(e2, o);
                    BusArrivalArrayObjectEstimate e3 = o.getNextBus3();
                    addPublicBuses(e3, o);
                    LogHelper.i(TAG, "Displaying Public Bus Locations for " + o.getServiceNo());
                }
            }
        }
    };

    private String getLoadString(int load) {
        switch (load){
            case CommonEnums.BUS_SEATS_AVAIL: return "Seats Available";
            case CommonEnums.BUS_STANDING_AVAIL: return "Standing Spots Available";
            case CommonEnums.BUS_LIMITED_SEATS: return "Limited Seats";
            default: return "Unknown";
        }
    }

    private void addPublicBuses(BusArrivalArrayObjectEstimate e1, BusArrivalArrayObject o) {
        if (e1 != null && e1.getEstimatedArrival() != null) {
            String load = getLoadString(e1.getLoadInt());
            BitmapDescriptor bus = busesUtil.vectorToBitmapDescriptor(R.drawable.ic_bus, this, getRouteColor(199179));
            publicBusMarkers.add(mMap.addMarker(new MarkerOptions().position(new LatLng(e1.getLatitudeD(), e1.getLongitudeD()))
                    .title(o.getServiceNo() + " (" + o.getOperator() + ")").snippet(load + " (" + BusesUtil.INSTANCE.getType(e1.getTypeInt()) + ")")
                    .icon(bus)));
        }
    }
}