Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Android: Getting a Rotated OSM Map to Fill the Entire Screen

I'm working on an application that uses the OpenStreetMap (OSM) API to display various points of interest on an offline map, composed of static tiles.

One of the features that I am currently implementing is having the map rotate in accordance with the bearing determined by the phone (via GPS). I was able to implement the actual rotation without too much effort, but since my code rotates the entire canvas -- perhaps a rather naive approach -- I now have blank corners on the screen where no new tiles are being loaded to compensate for the fact that the rotated tiles no longer fill these pixels.

After a bit of Googling, I found a few suggestions as to how I might be able to solve this issue, but so far no luck.

In one of Mr. Romain Guy's posts, he mentions the following:

I have done this in the past and it requires to create a custom ViewGroup that rotates the Canvas in the dispatchDraw() method. You also need to increase the size of the MapView (so that it draws enough pixels when rotated.) You will also need to rotate the touch events in dispatchTouchEvent(). Or if you use Android 3.0 you can simply call theMapView.rotate()

Taking his advice on map rotation, I have implemented dispatchDraw() and dispatchTouchEvent() as suggested, but I'm having trouble with the part where he mentions that I need to increase the size of the MapView?

Is this something that I do in the XML file, like suggested in this thread?

Or, can I somehow override the onMeasure() function in my subclassed RelativeLayout class that handles my map rotation?

Suggestions and hints are most welcome.

UPDATE:

In an attempt to find an acceptable solution to this problem, I tried to change the size of the canvas. The thinking was that with a canvas size that is bigger than the actual screen size, I might be able to move the blank corners off the screen entirely. Unfortunately, there does not appear to be an actual canvas.size() option; the best I found was canvas.scale().

Using canvas.scale(), I was able to increase the scale of the canvas by a factor of 2 in both the horizontal as well as vertical dimensions. This means, however, that the image is effectively zoomed in, causing unacceptable pixelation to the map tiles.

Does anyone know where the size of the canvas gets declared, and if changing the size of the canvas might actually solve my problem?

like image 811
Tim Severeijns Avatar asked Aug 29 '11 23:08

Tim Severeijns


1 Answers

I ended up following Romain Guy's advice (the same advice that I posted in my question). That is, I created my own custom ViewGroup by extending the RelativeLayout, and I then increased the size of my mapView to provide coverage of the entire screen. Extending the RelativeLayout ViewGroup was necessary so that I could override the dispatchDraw(...), the onMeasure(...), as well as the dispatchTouchEvent(...) functions to enable the desired map rotation features for my application.

The dispatchDraw(...) function essentially intercepts calls to the onDraw(...) function, performs a few specific manipulations on the input to that function and then releases it to be processed. In our case, we'll want to rotate the mapView canvas before it makes its way to the actual onDraw(...) function. This is why we'll need to override this function.

Specifically, the dispatchDraw(...) function takes as input a canvas object, which (in this case) represents the OSM mapView object (as defined in the XML file below). If a rotation is to be applied to the canvas, we'll want to locate the center of the map, translate (i.e. move) the map so that the center of the map sits on the origin of the coordinate system, rotated the map about the origin of the coordinate system, and then, finally, we'll want to dispatch this modified canvas to the next stage in the rendering pipeline.

My code for this is below; note that Manager is my own singleton creation that won't exist in your implementation unless you write one yourself!

    /**
 * @param pCanvas
 * @return void
 * 
 * This function intercepts all dispatches to the onDraw function of the 
 * super, and it rotates the canvas in accordance with the user's wishes
 * using the phone bearing as determined either through the magnetometer
 * or GPS fixes.
 */
@Override
protected void dispatchDraw(final Canvas pCanvas) {
    final long startMs = System.currentTimeMillis();

    // If automatic map rotation has been enabled, get bearing from phone:
    if (Manager.getInstance().getMapRotationMode() != Constants.DISABLED) {
        mBearing = Manager.getInstance().getPhoneBearing();

        // Save the state of the transformation matrix:
        pCanvas.save(Canvas.MATRIX_SAVE_FLAG);

        // getWidth() and getHeight() return the size of the canvas as 
        // defined in the XML file, and not the size of the screen!
        int canvasOffsetX = -(getWidth() / 2) + (screenWidth / 2);
        int canvasOffsetY = -(getHeight() / 2) + (screenHeight / 2);

        // Set origin of canvas to center of map:
        pCanvas.translate(canvasOffsetX, canvasOffsetY); 

        // Rotate the canvas to the correct bearing:
        pCanvas.rotate(-mBearing, getWidth() / 2, getHeight() / 2);

        // Pass on the rotated canvas, and restore after that:
        super.dispatchDraw(pCanvas);

        // Balance out the call to save, and restore the matrix to 
        // saved state:
        pCanvas.restore();          
    } // end if

    else { // If map rotation has not been enabled:
        super.dispatchDraw(pCanvas);
    } // end else

    final long endMs = System.currentTimeMillis();
    if (LOG_ENABLED) {
        Log.i(TAG, "mapView Dispatch Time: " + (endMs - startMs) + "ms");
    } // end if
} // end dispatchDraw()

Next we'll need to override dispatchTouchEvent(...), because any rotation of the OSM mapView canvas ends up rotating not only the graphical representation of the map, but also everything else related to that Activity (this occurs as a side effect of my specific implementation); that is, touch event coordinates remain relative to the mapView canvas after being rotated and not relative to the actual phone. For example, if we imagine the canvas being rotated by 180 degrees, then if the user attempts to pan the map to the left, it will instead move to the map to the right, since everything is upside-down!

In code, you might correct for this problem as follows:

/**
 * @param event
 * @return boolean
 * 
 * This function intercepts all interactions with the touch display (that is, 
 * all touchEvents), and for each finger on the screen, or pointer, the
 * function applies the necessary rotation to counter the rotation of the 
 * map. The coordinate of each pointer is also modified so that it returns
 * the correct location on the enlarged canvas. This was necessary to return
 * the correct coordinate for actions such as double-tap, and proper icon 
 * identification upon clicking an icon.
 */
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
    // Get the number of pointers (i.e. fingers on screen) from the passed
    // in MotionEvent:
    float degrees = Manager.getInstance().getPhoneBearing();
    int numPointers = event.getPointerCount();
    int[] pointerIDs = new int[numPointers];
    PointerCoords[] pointerCoords = new PointerCoords[numPointers];

    // Extract all pointers from the touch event:
    for (int i = 0; i < numPointers; i++) {
        pointerIDs[i] = event.getPointerId(i);
        pointerCoords[i] = new PointerCoords();

        event.getPointerCoords(i, pointerCoords[i]);
    } // end for

    // Correct each pointer coordinate set for map rotation:
    for (int i = 0; i < numPointers; i++) {
        // x and y end up representing points on the canvas, although they
        // are derived from points on the screen:
        float x = pointerCoords[i].x;
        float y = pointerCoords[i].y;

        // Get the center of the MapView:
        int centerX = getWidth() / 2;
        int centerY = getHeight() / 2;

        // Convert to radians
        float rad = (float) ((degrees * Math.PI) / 180f);
        float s = (float) Math.sin(rad);
        float c = (float) Math.cos(rad);

        // Translate point to origin:
        x -= centerX;
        y -= centerY;

        // Apply rotation
        float tmpX = x * c - y * s;
        float tmpY = x * s + y * c;
        x = tmpX;
        y = tmpY;           

        // Offset the coordinates to compensate for the fact that the
        // canvas is 1200 by 1200, the phone screen is smaller, and
        // they don't overlap nicely:
        x += (600 - (screenWidth / 2)) * c - (600 - (screenHeight / 2)) * s;
        y += (600 - (screenWidth / 2)) * s + (600 - (screenHeight / 2)) * c;

        // Translate point back:
        x += centerX;
        y += centerY;

        pointerCoords[i].x = x;
        pointerCoords[i].y = y;

        // Catlog:
        if (LOG_ENABLED) Log.i(TAG, "(" + x + ", " + y + ")");
    } // end for

    // Create new event to pass along the modified pointers.
    // Need API level 9 or higher to make this work!
    MotionEvent newEvent = MotionEvent.obtain(event.getDownTime(), event
            .getEventTime(), event.getAction(), event.getPointerCount(),
            pointerIDs, pointerCoords, event.getMetaState(), event
                    .getXPrecision(), event.getYPrecision(), event
                    .getDeviceId(), event.getEdgeFlags(),
            event.getSource(), event.getFlags());

    // Dispatch the newly modified touch event:
    return super.dispatchTouchEvent(newEvent);
} // end dispatchTouchEvent()

Finally, the trick to getting the corresponding XML for the map activity to work properly is to utilize a FrameLayout as the parent to all other GUI elements in my layout. This allowed me to make the mapView dimensions considerably larger than the dimensions of the display on my Nexus One (480 by 800). This solution also allowed me to nest a RelativeLayout inside my FrameLayout while still respecting the device's actual display dimensions when using match_parent and similar parameters.

The relevant portion of my XML layout looks like this:

<?xml version="1.0" encoding="utf-8"?>

<!--Note that the layout width and height is defined in px and not dip!-->

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/MapViewLayout">

<a.relevant.path.RotatingRelativeLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="1200px"
    android:layout_height="1200px">

    <org.osmdroid.views.MapView
        android:id="@+id/mapview"
        android:layout_width="1200px" 
        android:layout_height="1200px"
        android:enabled="true"      
        android:clickable="true"/>          
</a.relevant.path.RotatingRelativeLayout>   

<RelativeLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <RelativeLayout
        android:id="@+id/leftSlideHandleButton" 
        android:layout_width="match_parent"
        android:layout_height="60dip" 
        android:layout_centerHorizontal="true"
        android:background="#D0000000">

        <Button 
            android:id="@+id/mapZoomOutButton"
            android:layout_height="wrap_content"
            android:layout_width="wrap_content"
            android:background="@drawable/zoom_out_button"
            android:layout_alignParentLeft="true"
            android:onClick="zoomOutButton"/>

        <Button 
            android:id="@+id/mapZoomInButton"
            android:layout_height="wrap_content"
            android:layout_width="wrap_content"
            android:background="@drawable/zoom_in_button"
            android:layout_alignParentRight="true"
            android:onClick="zoomInButton"/>

        <TextView 
            android:id="@+id/headerSpeedText"
            android:layout_height="wrap_content"
            android:layout_width="wrap_content"
            android:textColor="#33B5E5"
            android:text="Speed: "
            android:textSize="12sp"
            android:paddingLeft="15dip"
            android:layout_toRightOf="@id/mapZoomOutButton"/>

        <TextView 
            android:id="@+id/headerSpeedReading"
            android:layout_height="wrap_content"
            android:layout_width="wrap_content"
            android:textColor="#33B5E5"
            android:text="N/A"
            android:textSize="12sp"
            android:paddingLeft="27dip"
            android:layout_toRightOf="@id/headerSpeedText"/>

        <TextView 
            android:id="@+id/headerBearingText"
            android:layout_height="wrap_content"
            android:layout_width="wrap_content"
            android:textColor="#33B5E5"
            android:text="Bearing: "
            android:paddingLeft="15dip"
            android:textSize="12sp"
            android:layout_toRightOf="@id/mapZoomOutButton"
            android:layout_below="@id/headerSpeedText"/>

        <!-- Et Cetera... -->

    </RelativeLayout>
</FrameLayout>

I'd like to remark that this solution is by no means the best solution, but that it worked out fine for my proof-of-concept application!

like image 122
Tim Severeijns Avatar answered Oct 27 '22 04:10

Tim Severeijns