package net.wigle.wigleandroid;
import java.util.Collection;
import java.util.Collections;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Matcher;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.GoogleMap.OnCameraChangeListener;
import com.google.android.gms.maps.model.BitmapDescriptor;
import com.google.android.gms.maps.model.BitmapDescriptorFactory;
import com.google.android.gms.maps.model.CameraPosition;
import com.google.android.gms.maps.model.LatLngBounds;
import com.google.android.gms.maps.model.Marker;
import com.google.android.gms.maps.model.MarkerOptions;
import com.google.maps.android.clustering.Cluster;
import com.google.maps.android.clustering.ClusterManager;
import com.google.maps.android.clustering.view.DefaultClusterRenderer;
import com.google.maps.android.ui.IconGenerator;
import net.wigle.wigleandroid.model.Network;
public class MapRender implements ClusterManager.OnClusterClickListener<Network>,
ClusterManager.OnClusterInfoWindowClickListener<Network>,
ClusterManager.OnClusterItemClickListener<Network>,
ClusterManager.OnClusterItemInfoWindowClickListener<Network> {
private final ClusterManager<Network> mClusterManager;
private final NetworkRenderer networkRenderer;
private final boolean isDbResult;
private final AtomicInteger networkCount = new AtomicInteger();
private final SharedPreferences prefs;
private final GoogleMap map;
private final Matcher ssidMatcher;
private final Set<Network> labeledNetworks = Collections.newSetFromMap(
new ConcurrentHashMap<Network,Boolean>());
private static final String MESSAGE_BSSID = "messageBssid";
private static final BitmapDescriptor DEFAULT_ICON = BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_AZURE);
private static final BitmapDescriptor DEFAULT_ICON_NEW = BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_CYAN);
private static final float DEFAULT_ICON_ALPHA = 0.75f;
private static final float CUSTOM_ICON_ALPHA = 0.80f;
// a % of the cache size can be labels
private static final int MAX_LABELS = MainActivity.getNetworkCache().maxSize() / 15;
private class NetworkRenderer extends DefaultClusterRenderer<Network> {
final IconGenerator iconFactory;
public NetworkRenderer(Context context, GoogleMap map, ClusterManager<Network> clusterManager) {
super(context, map, clusterManager);
iconFactory = new IconGenerator(context);
}
@Override
protected void onBeforeClusterItemRendered(Network network, MarkerOptions markerOptions) {
// Draw a single network.
final BitmapDescriptor icon = getIcon(network);
markerOptions.icon(icon);
if (icon == DEFAULT_ICON || icon == DEFAULT_ICON_NEW) {
markerOptions.alpha(DEFAULT_ICON_ALPHA);
}
else {
markerOptions.alpha(CUSTOM_ICON_ALPHA);
}
markerOptions.title(network.getSsid());
final String newString = network.isNew() ? " (new)" : "";
markerOptions.snippet(network.getBssid() + newString + " - " + network.getChannel() + " - " + network.getCapabilities());
}
private BitmapDescriptor getIcon(final Network network) {
if (showDefaultIcon(network)) {
MapRender.this.labeledNetworks.remove(network);
return network.isNew() ? DEFAULT_ICON_NEW : DEFAULT_ICON;
}
MapRender.this.labeledNetworks.add(network);
if ( network.isNew() ) {
iconFactory.setStyle(IconGenerator.STYLE_WHITE);
}
else {
iconFactory.setStyle(IconGenerator.STYLE_BLUE);
}
return BitmapDescriptorFactory.fromBitmap(iconFactory.makeIcon(network.getSsid()));
}
private boolean showDefaultIcon(final Network network) {
final boolean showLabel = prefs.getBoolean( ListFragment.PREF_MAP_LABEL, true );
if (showLabel) {
final LatLngBounds bounds = map.getProjection().getVisibleRegion().latLngBounds;
// if on screen, and room in labeled networks, we can show the label
if (bounds.contains(network.getLatLng()) && MapRender.this.labeledNetworks.size() <= MAX_LABELS) {
return false;
}
}
return true;
}
@Override
protected void onBeforeClusterRendered(Cluster<Network> cluster, MarkerOptions markerOptions) {
// Draw a cluster
super.onBeforeClusterRendered(cluster, markerOptions);
markerOptions.title(cluster.getSize() + " Networks");
StringBuilder builder = new StringBuilder();
int count = 0;
for (final Network network : cluster.getItems()) {
final String ssid = network.getSsid();
if (!ssid.equals("")) {
if (count > 0) {
builder.append(", ");
}
count++;
builder.append(ssid);
}
if (count > 20) {
break;
}
}
markerOptions.snippet(builder.toString());
}
@Override
protected boolean shouldRenderAsCluster(Cluster<Network> cluster) {
return (prefs.getBoolean( ListFragment.PREF_MAP_CLUSTER, true ) && cluster.getSize() > 4)
|| cluster.getSize() >= 100;
}
protected void updateItem(final Network network) {
final Marker marker = this.getMarker(network);
if (marker != null) {
if (showDefaultIcon(network)) {
if (marker.getAlpha() != DEFAULT_ICON_ALPHA) {
marker.setIcon(getIcon(network));
marker.setAlpha(DEFAULT_ICON_ALPHA);
}
}
else {
if (marker.getAlpha() == DEFAULT_ICON_ALPHA) {
marker.setIcon(getIcon(network));
marker.setAlpha(CUSTOM_ICON_ALPHA);
}
}
}
else if (network.isNew()) {
// handle case where network was not added before because it is not new
final boolean showNewDBOnly = prefs.getBoolean( ListFragment.PREF_MAP_ONLY_NEWDB, false );
if (showNewDBOnly) {
mClusterManager.addItem(network);
mClusterManager.cluster();
}
}
}
private void setupRelabelingTask() {
// setup camera change listener to fire the asynctask
map.setOnCameraChangeListener(new OnCameraChangeListener() {
@Override
public void onCameraChange(CameraPosition position) {
new DynamicallyAddMarkerTask().execute(map.getProjection().getVisibleRegion().latLngBounds);
}
});
}
private class DynamicallyAddMarkerTask extends AsyncTask<LatLngBounds, Integer, Void> {
@Override
protected Void doInBackground(LatLngBounds... bounds) {
final Collection<Network> nets = MainActivity.getNetworkCache().values();
for (final Network network : nets) {
final Marker marker = NetworkRenderer.this.getMarker(network);
if (marker != null && network.getLatLng() != null) {
final boolean inBounds = bounds[0].contains(network.getLatLng());
if (inBounds || MapRender.this.labeledNetworks.contains(network)) {
// MainActivity.info("sendupdate: " + network.getBssid());
sendUpdateNetwork(network.getBssid());
}
}
}
return null;
}
@Override
protected void onPostExecute(Void result) {
mClusterManager.cluster();
}
}
}
public MapRender(final Context context, final GoogleMap map, final boolean isDbResult) {
this.map = map;
this.isDbResult = isDbResult;
prefs = context.getSharedPreferences( ListFragment.SHARED_PREFS, 0 );
ssidMatcher = FilterMatcher.getFilterMatcher( prefs, MappingFragment.MAP_DIALOG_PREFIX );
mClusterManager = new ClusterManager<>(context, map);
networkRenderer = new NetworkRenderer(context, map, mClusterManager);
mClusterManager.setRenderer(networkRenderer);
map.setOnCameraChangeListener(mClusterManager);
map.setOnMarkerClickListener(mClusterManager);
map.setOnInfoWindowClickListener(mClusterManager);
mClusterManager.setOnClusterClickListener(this);
mClusterManager.setOnClusterInfoWindowClickListener(this);
mClusterManager.setOnClusterItemClickListener(this);
mClusterManager.setOnClusterItemInfoWindowClickListener(this);
if (!isDbResult) {
// setup the relabeling background task
networkRenderer.setupRelabelingTask();
}
reCluster();
}
private void addLatestNetworks() {
// add items
int cached = 0;
int added = 0;
final Collection<Network> nets = MainActivity.getNetworkCache().values();
for (final Network network : nets) {
cached++;
if (okForMapTab(network)) {
added++;
mClusterManager.addItem(network);
}
}
MainActivity.info("MapRender cached: " + cached + " added: " + added);
networkCount.getAndAdd(added);
mClusterManager.cluster();
}
public boolean okForMapTab( final Network network ) {
final boolean showNewDBOnly = prefs.getBoolean( ListFragment.PREF_MAP_ONLY_NEWDB, false )
&& ! isDbResult;
if (network.getPosition() != null) {
if (!showNewDBOnly || network.isNew()) {
if (FilterMatcher.isOk(ssidMatcher, prefs, MappingFragment.MAP_DIALOG_PREFIX, network)) {
return true;
}
}
}
return false;
}
@Override
public void onClusterItemInfoWindowClick(Network item) {
// TODO Auto-generated method stub
}
@Override
public boolean onClusterItemClick(Network item) {
// TODO Auto-generated method stub
return false;
}
@Override
public void onClusterInfoWindowClick(Cluster<Network> cluster) {
// TODO Auto-generated method stub
}
@Override
public boolean onClusterClick(Cluster<Network> cluster) {
// TODO Auto-generated method stub
return false;
}
public void addItem(final Network network) {
if (network.getPosition() != null) {
final int count = networkCount.incrementAndGet();
if (count > MainActivity.getNetworkCache().size() * 1.3) {
// getting too large, re-sync with cache
reCluster();
}
else {
mClusterManager.addItem(network);
mClusterManager.cluster();
}
}
}
public void clear() {
MainActivity.info("MapRender: clear");
labeledNetworks.clear();
networkCount.set(0);
mClusterManager.clearItems();
// mClusterManager.setRenderer(networkRenderer);
}
public void reCluster() {
MainActivity.info("MapRender: reCluster");
clear();
if (!isDbResult) {
addLatestNetworks();
}
}
public void updateNetwork(final Network network) {
if (okForMapTab(network)) {
sendUpdateNetwork(network.getBssid());
}
}
private void sendUpdateNetwork(final String bssid) {
final Bundle data = new Bundle();
data.putString(MESSAGE_BSSID, bssid);
Message message = new Message();
message.setData(data);
updateMarkersHandler.sendMessage(message);
}
@SuppressLint("HandlerLeak")
final Handler updateMarkersHandler = new Handler() {
@Override
public void handleMessage(final Message message) {
final String bssid = message.getData().getString(MESSAGE_BSSID);
// MainActivity.info("handleMessage: " + bssid);
final Network network = MainActivity.getNetworkCache().get(bssid);
if (network != null) {
networkRenderer.updateItem(network);
}
}
};
}