Nov 19, 2011

Android :: google maps (MapView) hacks, tricks, workarounds

MapView isn't very developer friendly now is it?

Here are some hacks I've had to work with:

1) When tapping on the overlay, the app crashes if you're trying to display the dialog.
Unable to add window -- token null is not for an application
Yes, this is due to context. Especially if you're trying to do this in a seperate thread or AsyncTask.
Turns out the context you need to pass to your dialog is mapView.getContext();
AlertDialog.Builder dialog = new AlertDialog.Builder(mapView.getContext());




2) Displaying only the overlays which are within map bounds
Oh yeah, several problems here. You've probably come across at least one of these:

  • Wrong map bounds in onCreate (0, 360000000),
    Yeah, in onCreate those haven't been calculated yet. onStart doesn't help either. Try this:
Runnable waitForMapTimeTask = new Runnable() {
  public void run() {
    if(mapView.getLatitudeSpan()==0||mapView.getLongitudeSpan()== 360000000) {
      mapView.postDelayed(this, 100);
    } else {
      redrawMarkers(); // draw here
    }
  }
};
mapView.postDelayed(waitForMapTimeTask, 100);

You create a new thread and wait until you get the right bounds. Recursively call it again.
  • Map bounds ???
    Don't worry, it's simple.
public Rect getMapBounds() {
return new Rect(
mapView.getMapCenter().getLongitudeE6() - mapView.getLongitudeSpan()/2,
mapView.getMapCenter().getLatitudeE6() - mapView.getLatitudeSpan()/2,
mapView.getMapCenter().getLongitudeE6() + mapView.getLongitudeSpan()/2,
mapView.getMapCenter().getLatitudeE6() + mapView.getLatitudeSpan()/2
);
}

...
if(!s.drawn && rect.contains(point.getLongitudeE6(), point.getLatitudeE6())) {


  • Yeah, okay, but what about panning / zooming?
    Well, there are no methods, like onPan or onZoom, but some people found their way around this problem. There is no perfect solution, you'll see.
    Check these links out:
  1. http://stackoverflow.com/questions/2328650/how-can-i-detect-if-an-android-mapview-has-been-panned-or-zoomed
  2. http://bricolsoftconsulting.com/2011/10/31/extending-mapview-to-add-a-change-event/
  3. http://stackoverflow.com/questions/3567420/how-to-catch-that-map-panning-and-zoom-are-really-finished
  • Zoom in on double tap?
    Click here: http://dev.kafol.net/2011/11/how-hard-is-it-to-make-simple-zoom-in.html.
  • Overlays don't get drawn immediately!
    Try this:
    mapView.postInvalidate();
    or this:
    mapView.invalidate();

    But keep in mind, that invalidate() needs to be called from an UI! If you're trying to get it working from a thread, use postInvalidate()!
  • MapView java.util.ConcurrentModificationException when adding new overlays
    Not sure if I solved this one, but it seems to work now. I read somewhere that this could happen if you add overlays in a non UI thread. I moved the 
    Nope, sorry, this one was my bad. I was doing some crazy async sorting and all hell broke loose.
itemizedOverlay.populateNow();
mapOverlays.add(itemizedOverlay);
mapView.postInvalidate();

From doInBackground to  onPostExecute in  AsyncTask.


You could also run something in a UI thread like this:

runOnUiThread(new Runnable() {
      @Override
       public void run() {
           //do stuff here
       }
});



Nov 18, 2011

Android :: google maps on double tap zoom in

How hard is it to make a simple zoom in call on double tap in MapView in Android?

Not very.

How hard is it to get the information on how to do it?

Very.

Here's what you probably didn't know:
You need to extend the MapView and use this extended class in the Android XML layout file.
In the extended class you instantiate the gesture detector and set on double tap listener.
In the Map Activity you implement OnGestureListener and OnDoubleTapListener.

Example:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:gravity="center_horizontal" >
    
 <net.kafol.vlaki.ExtMapView
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:id="@+id/mapview"
     android:layout_width="fill_parent"
     android:layout_height="fill_parent"
     android:clickable="true"
     android:enabled="true"
     android:apiKey=""
 />

</RelativeLayout>
package net.kafol.vlaki;

import android.content.Context;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.GestureDetector.OnDoubleTapListener;
import android.view.MotionEvent;
import android.view.GestureDetector.OnGestureListener;
import com.google.android.maps.MapView;

public class ExtMapView extends MapView {
 private Context context;
 private GestureDetector gestureDetector;

 public ExtMapView(Context c, AttributeSet attrs) {
  super(c, attrs);
  context = c;

  gestureDetector = new GestureDetector((OnGestureListener) context);
  gestureDetector.setOnDoubleTapListener((OnDoubleTapListener) context);
 }

 public boolean onTouchEvent(MotionEvent ev) {
  if (this.gestureDetector.onTouchEvent(ev))
   return true;
  else
   return super.onTouchEvent(ev);
 }
}
public class Map extends MapActivity implements OnGestureListener, OnDoubleTapListener {
...
 @Override
 public boolean onDoubleTap(MotionEvent e) {
     int x = (int)e.getX(), y = (int)e.getY();;
     Projection p = mapView.getProjection();
     mapView.getController().animateTo(p.fromPixels(x, y)); // zoom in to a point you tapped 
     mapView.getController().zoomIn();
  return true;
 }

Android : Checkbox ListView - (un) check all

This recycling of views in android is pretty insane. Pain to work with.

So the problem is how to check all check boxes in a list view, if a list view only contains the visible items.

Iterating through the adapter or array of holders was pretty unreliable, some checkboxes weren't affected.

What I did was actually add an attribute to the data object and iterate through this object array (the same way it get's added in the adapter)-

Here are some functions:
 public void toggleCheck(Boolean val) {
  
  for(Station s : stations.list) {
   s.checked = val;
   editor.putBoolean("PF_"+s.getID(), val);
  }
  
  for(int i=0 ; i < lv.getChildCount() ; i++) {
   CheckBox cb = (CheckBox) lv.getChildAt(i).findViewById(R.id.cb);
   cb.setChecked(val);
  }

  editor.apply();
 }
 
    private class StationCBViewHolder {
        public CheckBox cb;
        public Station s;
    }
 
 private class StationListAdapter extends ArrayAdapter {
     private ArrayList items;
     private Context context;
     
     public StationListAdapter(Context context, int tvResId, ArrayList items) {
         super(context, tvResId, items);
         this.items = items;
         this.context = context;
     }

     @Override
     public View getView(int pos, View v, ViewGroup parent) {
      final StationCBViewHolder holder;
      final Station item = items.get(pos);
      
         if (v == null) {
             LayoutInflater vi = ((Activity)context).getLayoutInflater();
             v = vi.inflate(R.layout.stationcheckboxitem, parent, false);
             
             holder = new StationCBViewHolder();
             holder.cb = (CheckBox) v.findViewById(R.id.cb);
             holder.s = item;
             
             item.checked = prefs.getBoolean("PF_"+item.getID(),true);
             
             //holder.cb.setTag(holder);
             holder.cb.setTag(item);
             holder.cb.setChecked(item.checked);
             holder.cb.setOnCheckedChangeListener(new OnCheckedChangeListener() {
     @Override
     public void onCheckedChanged(CompoundButton v, boolean isChecked) {
      Station s = (Station) v.getTag();
      editor.putBoolean("PF_"+s.getID(), isChecked);
      editor.apply();
     }
    });
                        
             v.setTag(holder);
         } else {
          holder = (StationCBViewHolder) v.getTag(); 
         }

         holder.cb.setText(item.toString());
         holder.cb.setChecked(item.checked);

         return v;
     }

 }


Full code here: http://pastebin.com/8NMbHqRV


The code still has some bugs related to SharedPreferences and SharedPreferences.Editor, but at least it checks and unchecks all checkboxes.

Sep 14, 2011

URL shortening - make your own URL shortener

If you ever thought about URL shortening, you've probably already figured out, how it works.

Those random letters and numbers which serve as a key (in hashtable data structures) are basically encoded integers, which are AUTO_INCREMENT values in the database.

Here are the encoding and decoding functions:
const ALLOWED_CHARS = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';

function id2short($integer, $base = self::ALLOWED_CHARS) {
	$out = '';
	$length = strlen($base);
	while($integer > $length-1) {
		$out = $base[fmod($integer, $length)].$out;
		$integer = floor( $integer / $length);
	}
	return $base[$integer].$out;
}

function short2id($string, $base = self::ALLOWED_CHARS) {
	$length = strlen($base);
	$size = strlen($string)-1;
	$string = str_split($string);
	$out = strpos($base, array_pop($string));
	foreach($string as $i=>$char) {
		$out += strpos($base, $char) * pow($length, $size - $i);
	}
	return $out;
}

Full code here.

Sep 3, 2011

MySQL error 1236: Client requested master to start replication from impossible position

Symptoms:
[1236] Got fatal error 1236 from master when reading data from binary log: 'Client requested master to start replication from impossible position'

110902 16:47:08 [ERROR] Error reading packet from server: Client requested master to start replication from impossible position ( server_errno=1236)
110902 16:47:08 [ERROR] Slave I/O: Got fatal error 1236 from master when reading data from binary log: 'Client requested master to start replication from impossible position', Error_code: 1236
110902 16:47:08 [Note] Slave I/O thread exiting, read up to log 'mysql-bin.000033', position 4621679


on the master
root@dimko:/var/lib/mysql# ls -la mysql-bin.000033
-rw-rw---- 1 mysql mysql 4620018 2011-09-01 13:45 mysql-bin.000033

4620018 is less than 4621679, therefore it's an invalid position.


Causes:
Master server has crashed and the binlog cache has not been flushed to disk. Slave has recieved a new position, did not recieve data, and data gets lost in a crash (however it might have been written to table, but not in binlog).


Solution:
Use this CHANGE MASTER statement on the slave.
CHANGE MASTER TO MASTER_LOG_FILE=[NEXT FILE], MASTER_LOG_POS=4;
SLAVE START;

in my case that would be

CHANGE MASTER TO MASTER_LOG_FILE='mysql-bin.000034', MASTER_LOG_POS=4;
SLAVE START;

I don't know why the master log position needs to be 4 for the new file.

What happens:
When the master server restarts it logs binary changes to a new binlog file, so that we minimize data loss by skipping to the next file (everything from the previous file was written already).

Prevention:
Add this line to my.cnf:

sync_binlog = 1

 With this setting the master server flushes cache in the binlog after every write, so that in case of a crash you can lose one statement at most.