de.tlabs.ssr.g1.client.SourcesView.java Source code

Java tutorial

Introduction

Here is the source code for de.tlabs.ssr.g1.client.SourcesView.java

Source

/******************************************************************************
 * Copyright (c) 2006-2012 Quality & Usability Lab                            *
 *                         Deutsche Telekom Laboratories, TU Berlin           *
 *                         Ernst-Reuter-Platz 7, 10587 Berlin, Germany        *
 *                                                                            *
 * This file is part of the SoundScape Renderer (SSR).                        *
 *                                                                            *
 * The SSR is free software:  you can redistribute it and/or modify it  under *
 * the terms of the  GNU  General  Public  License  as published by the  Free *
 * Software Foundation, either version 3 of the License,  or (at your option) *
 * any later version.                                                         *
 *                                                                            *
 * The SSR is distributed in the hope that it will be useful, but WITHOUT ANY *
 * WARRANTY;  without even the implied warranty of MERCHANTABILITY or FITNESS *
 * FOR A PARTICULAR PURPOSE.                                                  *
 * See the GNU General Public License for more details.                       *
 *                                                                            *
 * You should  have received a copy  of the GNU General Public License  along *
 * with this program.  If not, see <http://www.gnu.org/licenses/>.            *
 *                                                                            *
 * The SSR is a tool  for  real-time  spatial audio reproduction  providing a *
 * variety of rendering algorithms.                                           *
 *                                                                            *
 * http://tu-berlin.de/?id=ssr                  SoundScapeRenderer@telekom.de *
 ******************************************************************************/

/* 
 * $LastChangedDate$
 * $LastChangedRevision$
 * $LastChangedBy$
 */

package de.tlabs.ssr.g1.client;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;

import org.apache.http.util.EncodingUtils;

import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Picture;
import android.hardware.SensorManager;
import android.os.SystemClock;
import android.util.AttributeSet;
import android.util.FloatMath;
import android.util.Log;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.OrientationEventListener;
import android.view.View;
import android.view.GestureDetector.OnDoubleTapListener;
import android.view.GestureDetector.OnGestureListener;

/**
 * Class containing GUI and logic for display and manipulation of sound sources.
 * 
 * @author Peter Bartz
 */
public class SourcesView extends View implements OnGestureListener, OnDoubleTapListener {
    private static final String TAG = "SourcesView";

    // source won't be select if touch point outside this radius (in pixels)
    public static final float SOURCE_SELECT_RADIUS = 70f;
    // wait at least 10ms between two position update requests
    private static final long MAX_POSITION_UPDATE_FREQ = 30;
    // border when calculation "fit scene into screen"
    private static final float FIT_SCENE_PIXEL_BORDER = 60;
    private static final float FIT_SCENE_PIXEL_BORDER_2 = FIT_SCENE_PIXEL_BORDER * 2.0f;

    public enum TransformationMode {
        TRANSLATE, ROTATE
    }

    private static Paint paint;
    private Picture sizeScalePicture = new Picture();
    private Matrix viewportTransformation;
    private Matrix inverseViewportTransformation;
    private Matrix newViewportTransformation;
    private float[] selectionOffset;
    private float[] touchPoint;
    private float[] firstScrollPoint = { 0.0f, 0.0f };
    private float[] point = { 0.0f, 0.0f }; // for common use, to avoid object creation
    private SoundSource lastTouchSoundSource;
    private ByteBuffer buffer;
    private float currentScaling = 0.5f;
    private float currentInverseScaling = 1.0f / currentScaling;
    private float[] currentTranslation = { 0.0f, 80.0f };
    private float[] currentSavedTranslation = { 0.0f, 0.0f };
    private boolean scrolling = false;
    private float currentCenterRotation = 0.0f;
    private long lastSendTime = 0;
    private int currentOrientation;
    private TransformationMode transformationMode = TransformationMode.TRANSLATE;
    protected TimedInterpolator scalingInterpolator;
    protected TimedInterpolator translationXInterpolator;
    protected TimedInterpolator translationYInterpolator;
    protected TimedInterpolator rotationInterpolator;
    protected TimedInterpolator centerRotationInterpolator;
    private OrientationEventListener orientationEventListener;
    private GestureDetector gestureDetector;
    private boolean viewSizeInitialized = false;

    // flags
    private boolean viewportFlag = true;
    private boolean orientationFlag = true;
    private boolean transformToFitSceneFlag = false;

    public SourcesView(Context context) {
        super(context);
        init();
    }

    public SourcesView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public SourcesView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init();
    }

    private void init() {
        // make this view focusable
        setFocusable(true);

        // init fields
        viewportTransformation = new Matrix();
        newViewportTransformation = new Matrix();
        inverseViewportTransformation = new Matrix();

        selectionOffset = new float[2];
        touchPoint = new float[2];

        buffer = ByteBuffer.allocate(1024);

        scalingInterpolator = new TimedInterpolator();
        scalingInterpolator.setDuration(800);
        translationXInterpolator = new TimedInterpolator();
        translationXInterpolator.setDuration(800);
        translationYInterpolator = new TimedInterpolator();
        translationYInterpolator.setDuration(800);
        rotationInterpolator = new TimedInterpolator();
        rotationInterpolator.setDuration(800);
        centerRotationInterpolator = new TimedInterpolator();
        centerRotationInterpolator.setDuration(800);

        currentOrientation = getContext().getResources().getConfiguration().orientation;
        setOrientationFlag(true);

        if (SourcesView.paint == null) {
            SourcesView.paint = new Paint();
            SourcesView.paint.setAntiAlias(false);
            SourcesView.paint.setStrokeWidth(0);
            SourcesView.paint.setTextAlign(Paint.Align.CENTER);
            SourcesView.paint.setTextSize(9.0f);
        }

        // set up orientation event listener
        orientationEventListener = new OrientationEventListener(getContext(), SensorManager.SENSOR_DELAY_NORMAL) {
            @Override
            public void onOrientationChanged(int orientation) {
                if ((orientation >= 80 && orientation <= 100) || (orientation >= 260 && orientation <= 280)) { // landscape
                    setOrientation(Configuration.ORIENTATION_LANDSCAPE);
                } else if ((orientation >= 350 || orientation <= 10)
                        || (orientation >= 170 && orientation <= 190)) { // portrait
                    setOrientation(Configuration.ORIENTATION_PORTRAIT);
                }
            }
        };
        if (!GlobalData.orientationTrackingEnabled) // orientation tracking and screen rotation tracking don't go together
            orientationEventListener.enable();

        // set up gesture detector
        gestureDetector = new GestureDetector(getContext(), this);
        gestureDetector.setIsLongpressEnabled(false);
        gestureDetector.setOnDoubleTapListener(this);

        // init viewport transformation matrix
        recalculateViewportTransformation();
    }

    public float getCurrentScaling() {
        return currentScaling;
    }

    public void setCurrentScaling(float currentScaling) {
        this.currentScaling = currentScaling;
        this.currentInverseScaling = 1.0f / currentScaling;
        setViewportFlag(true);
    }

    public float getCurrentCenterRotation() {
        return currentCenterRotation;
    }

    public void setCurrentCenterRotation(float currentCenterRotation) {
        this.currentCenterRotation = currentCenterRotation;
        setViewportFlag(true);
    }

    public float getCurrentInverseScaling() {
        return currentInverseScaling;
    }

    public void setCurrentInverseScaling(float currentInverseScaling) {
        this.currentInverseScaling = currentInverseScaling;
        this.currentScaling = 1.0f / currentInverseScaling;
        setViewportFlag(true);
    }

    public float[] getCurrentTranslation() {
        return currentTranslation;
    }

    public void setCurrentTranslation(float[] currentTranslation) {
        setCurrentTranslation(currentTranslation[0], currentTranslation[1]);
    }

    public void setCurrentTranslation(float currentTranslationX, float currentTranslationY) {
        this.currentTranslation[0] = currentTranslationX;
        this.currentTranslation[1] = currentTranslationY;
        setViewportFlag(true);
    }

    public boolean getAndClearViewportFlag() {
        boolean returnValue = viewportFlag;
        viewportFlag = false;
        return returnValue;
    }

    public void setViewportFlag(boolean viewportFlag) {
        this.viewportFlag = viewportFlag;
    }

    public boolean getAndClearOrientationFlag() {
        boolean returnValue = orientationFlag;
        orientationFlag = false;
        return returnValue;
    }

    public void setOrientationFlag(boolean orientationFlag) {
        this.orientationFlag = orientationFlag;
    }

    public void setTransformationMode(TransformationMode mode) {
        this.transformationMode = mode;
    }

    public void zoomView(float factor) {
        float newScaling;

        if (scalingInterpolator.isActive()) {
            newScaling = scalingInterpolator.getEndValue() * factor;
        } else {
            newScaling = currentScaling * factor;
        }

        scalingInterpolator.setStartEndValues(currentScaling, newScaling);
        scalingInterpolator.startInterpolating();
    }

    public void translateView(float transX, float transY) {
        float newTransX;
        float newTransY;

        if (transX != 0.0f) {
            if (translationXInterpolator.isActive()) {
                newTransX = translationXInterpolator.getEndValue() + transX;
            } else {
                newTransX = currentTranslation[0] + transX;
            }
            translationXInterpolator.setStartEndValues(currentTranslation[0], newTransX);
            translationXInterpolator.startInterpolating();
        }

        if (transY != 0.0f) {
            if (translationYInterpolator.isActive()) {
                newTransY = translationYInterpolator.getEndValue() + transY;
            } else {
                newTransY = currentTranslation[1] + transY;
            }
            translationYInterpolator.setStartEndValues(currentTranslation[1], newTransY);
            translationYInterpolator.startInterpolating();
        }
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        Log.d(TAG, "onSizeChanged");
        super.onSizeChanged(w, h, oldw, oldh);
        setViewportFlag(true);
        viewSizeInitialized = true;
        if (transformToFitSceneFlag) {
            transformToFitSceneFlag = false;
            transformToFitScene();
        }
    }

    private void recalculateViewportTransformation() {
        recalculateViewportTransformation(this.viewportTransformation, currentCenterRotation, currentScaling,
                currentTranslation);
        viewportTransformation.invert(inverseViewportTransformation);
    }

    private void recalculateViewportTransformation(Matrix dstMatrix, float centerRotation, float scaling,
            float[] translation) {
        dstMatrix.reset();
        // translate to center
        dstMatrix.preTranslate((float) getWidth() / 2.0f, (float) getHeight() / 2.0f);
        // rotate around center
        dstMatrix.preRotate(-centerRotation); // negative rotation, because y axis not yet inverted
        // translate
        dstMatrix.preTranslate(translation[0], translation[1]);
        // scale and invert y axis
        dstMatrix.preScale(scaling, -scaling);
        // rotate to look north always (instead of east)
        dstMatrix.preRotate(90.0f);
    }

    private void recalculateSizeScale() {
        float sizeInMeters;
        double upperBoundingPowOfTen;
        double scaleLengthInMeters;
        float scaleLengthInPixels;
        float scaleStartInPixels = 20.0f;

        // get size of short display edge in meters
        sizeInMeters = inverseViewportTransformation
                .mapRadius(Math.min(getWidth(), getHeight()) - scaleStartInPixels * 2.0f);

        // find upper bounding power of ten (0.01, 0.1, 1, 10, 100, etc.)
        if (sizeInMeters > 1.0) {
            upperBoundingPowOfTen = 10.0;
            while (true) {
                if (sizeInMeters / upperBoundingPowOfTen > 1.0f) {
                    upperBoundingPowOfTen *= 10.0;
                } else {
                    break;
                }
            }
        } else {
            upperBoundingPowOfTen = 1.0;
            while (true) {
                if (sizeInMeters / upperBoundingPowOfTen >= 0.1f) {
                    break;
                } else {
                    upperBoundingPowOfTen *= 0.1;
                }
            }
        }

        // map to subdivisions
        scaleLengthInMeters = sizeInMeters / upperBoundingPowOfTen;
        if (scaleLengthInMeters > 0.75)
            scaleLengthInMeters = 0.75;
        else if (scaleLengthInMeters > 0.5)
            scaleLengthInMeters = 0.5;
        else if (scaleLengthInMeters > 0.3)
            scaleLengthInMeters = 0.3;
        else if (scaleLengthInMeters > 0.2)
            scaleLengthInMeters = 0.2;
        else if (scaleLengthInMeters > 0.15)
            scaleLengthInMeters = 0.15;
        else
            scaleLengthInMeters = 0.1;
        scaleLengthInMeters *= upperBoundingPowOfTen;

        // get corresponding scale length in pixels
        scaleLengthInPixels = viewportTransformation.mapRadius((float) scaleLengthInMeters);

        // generate picture
        Canvas canvas = sizeScalePicture.beginRecording(0, 0);
        float y = getHeight() - 5.0f;
        float xHalf = scaleStartInPixels + scaleLengthInPixels / 2.0f;
        float scaleEndInPixels = scaleStartInPixels + scaleLengthInPixels;

        SourcesView.paint.setARGB(255, 255, 255, 255);
        SourcesView.paint.setAntiAlias(true);
        canvas.drawLine(scaleStartInPixels - 1.0f, y, scaleEndInPixels + 1.0f, y, SourcesView.paint);
        canvas.drawLine(scaleStartInPixels, y, scaleStartInPixels, y - 5.0f, SourcesView.paint);
        canvas.drawLine(xHalf, y, xHalf, y - 5.0f, SourcesView.paint);
        canvas.drawLine(scaleEndInPixels, y, scaleEndInPixels, y - 5.0f, SourcesView.paint);

        canvas.drawText("0m", scaleStartInPixels, y - 8.0f, SourcesView.paint);
        canvas.drawText(String.valueOf((float) scaleLengthInMeters / 2.0f) + "m", xHalf, y - 8.0f,
                SourcesView.paint);
        canvas.drawText(String.valueOf((float) scaleLengthInMeters) + "m", scaleEndInPixels, y - 8.0f,
                SourcesView.paint);
    }

    @Override
    public void onDraw(Canvas canvas) {
        // zooming animation
        if (scalingInterpolator.isActive()) {
            setCurrentScaling(scalingInterpolator.getCurrentValue());
        }

        // translation animation
        if (translationXInterpolator.isActive() || translationYInterpolator.isActive()) {
            float transX = currentTranslation[0];
            float transY = currentTranslation[1];
            if (translationXInterpolator.isActive()) {
                transX = translationXInterpolator.getCurrentValue();
            }
            if (translationYInterpolator.isActive()) {
                transY = translationYInterpolator.getCurrentValue();
            }
            setCurrentTranslation(transX, transY);
        }

        // center rotation animation
        if (centerRotationInterpolator.isActive()) {
            setCurrentCenterRotation(centerRotationInterpolator.getCurrentValue());
        }

        // calculate current viewport matrix if necessary
        if (getAndClearViewportFlag()) {
            recalculateViewportTransformation();
            recalculateSizeScale();
        }

        // clear background
        canvas.drawColor(0xFF000000);

        // draw audio scene
        canvas.setMatrix(viewportTransformation);
        synchronized (GlobalData.audioScene) {
            GlobalData.audioScene.draw(canvas, currentInverseScaling);
        }

        // reset matrix
        canvas.setMatrix(null);

        // draw size scale
        sizeScalePicture.draw(canvas);
    }

    public float[][] getSceneBounds(Matrix viewportTransformation) {
        float[] point = { 0.0f, 0.0f };

        // map reference point to screen coordinate system
        viewportTransformation.mapPoints(point);

        // set min/max to reference position
        float[][] minMaxXY = { { point[0], point[1] }, { point[0], point[1] } }; // two points: minXY and maxXY

        synchronized (GlobalData.audioScene) {
            int numSources = GlobalData.audioScene.getNumSoundSources();
            SoundSource s = null;
            for (int i = 0; i < numSources; i++) {
                s = GlobalData.audioScene.getSoundSource(i);
                point[0] = s.getX();
                point[1] = s.getY();

                // map to reference coordinate system
                GlobalData.audioScene.mapPoint(point);
                viewportTransformation.mapPoints(point);

                // check if point lies outwards current bounds
                if (point[0] < minMaxXY[0][0])
                    minMaxXY[0][0] = point[0];
                if (point[0] > minMaxXY[1][0])
                    minMaxXY[1][0] = point[0];
                if (point[1] < minMaxXY[0][1])
                    minMaxXY[0][1] = point[1];
                if (point[1] > minMaxXY[1][1])
                    minMaxXY[1][1] = point[1];
            }
        }

        return minMaxXY;
    }

    public void transformToFitScene() {
        float xScalingFactor = 0.0f;
        float yScalingFactor = 0.0f;
        float xDiff;
        float yDiff;
        float[][] sceneBounds;
        float minBounds[];
        float maxBounds[];

        // if size of this view not yet initialized, wait until it is initialized
        if (!viewSizeInitialized) {
            transformToFitSceneFlag = true;
            return;
        }

        // get scene bounds in screen coordinates
        recalculateViewportTransformation(); // just to be sure we are using the latest values
        sceneBounds = getSceneBounds(viewportTransformation);
        minBounds = sceneBounds[0];
        maxBounds = sceneBounds[1];

        // get max x and y distances in pixels
        xDiff = maxBounds[0] - minBounds[0];
        yDiff = maxBounds[1] - minBounds[1];

        // recalculate scaling
        if (xDiff > 0 || yDiff > 0) {
            // calculate best scaling in x and y direction
            if (xDiff > 0) {
                xScalingFactor = ((float) getWidth() - FIT_SCENE_PIXEL_BORDER_2) / xDiff;
            } else {
                xScalingFactor = Float.POSITIVE_INFINITY;
            }

            if (yDiff > 0) {
                yScalingFactor = ((float) getHeight() - FIT_SCENE_PIXEL_BORDER_2) / yDiff;
            } else {
                yScalingFactor = Float.POSITIVE_INFINITY;
            }

            // set best scaling
            float scalingFactor = Math.min(xScalingFactor, yScalingFactor);
            zoomView(scalingFactor);
            recalculateViewportTransformation(newViewportTransformation, currentCenterRotation,
                    currentScaling * scalingFactor, currentTranslation);
        } else {
            newViewportTransformation.set(viewportTransformation);
        }

        // get scene bounds using new scaling
        sceneBounds = getSceneBounds(newViewportTransformation);
        minBounds = sceneBounds[0];
        maxBounds = sceneBounds[1];

        // get max x and y distances in pixels
        xDiff = maxBounds[0] - minBounds[0];
        yDiff = maxBounds[1] - minBounds[1];

        // calculate translation to center the scene
        float transX = -minBounds[0] + (getWidth() - xDiff) / 2;
        float transY = -minBounds[1] + (getHeight() - yDiff) / 2;
        if (currentOrientation == Configuration.ORIENTATION_PORTRAIT) {
            translateView(transX, transY);
        } else if (currentOrientation == Configuration.ORIENTATION_LANDSCAPE) {
            translateView(transY, -transX);
        }
    }

    public void setOrientation(int newOrientation) {
        if (currentOrientation == newOrientation)
            return;

        if (newOrientation == Configuration.ORIENTATION_LANDSCAPE) { // portrait -> landscape
            centerRotationInterpolator.setStartEndValues(0.0f, -90.0f);
        } else if (newOrientation == Configuration.ORIENTATION_PORTRAIT) { // landscape -> portrait
            centerRotationInterpolator.setStartEndValues(-90.0f, 0.0f);
        } else { // unsupported orientation, this should never happen
            return;
        }

        // save current orientation
        currentOrientation = newOrientation;
        setOrientationFlag(true);

        // start interpolating
        centerRotationInterpolator.startInterpolating();
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // pass event to gesture detector
        gestureDetector.onTouchEvent(event);
        return true;
    }

    @Override
    public boolean onDown(MotionEvent event) {
        scrolling = false;

        synchronized (GlobalData.audioScene) {
            // determine transformed coordinate of touch point
            touchPoint[0] = event.getX();
            touchPoint[1] = event.getY();
            inverseViewportTransformation.mapPoints(touchPoint);
            GlobalData.audioScene.inverseMapPoint(touchPoint);

            // try to find nearest sound source
            lastTouchSoundSource = GlobalData.audioScene.getNearestSoundSource(touchPoint);
            if (lastTouchSoundSource != null) {
                // get distance (touch point to source) in pixels
                selectionOffset[0] = lastTouchSoundSource.getX();
                selectionOffset[1] = lastTouchSoundSource.getY();
                GlobalData.audioScene.mapPoint(selectionOffset);
                viewportTransformation.mapPoints(selectionOffset);
                selectionOffset[0] -= event.getX();
                selectionOffset[1] -= event.getY();
                float distance = FloatMath
                        .sqrt(selectionOffset[0] * selectionOffset[0] + selectionOffset[1] * selectionOffset[1]);

                // select source?
                if (distance > SOURCE_SELECT_RADIUS) {
                    lastTouchSoundSource = null;
                }
            }
        }

        return true;
    }

    @Override
    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
        if (lastTouchSoundSource == null) {
            if (currentOrientation == Configuration.ORIENTATION_PORTRAIT) {
                translateView(velocityX * 0.3f, velocityY * 0.3f);
            } else if (currentOrientation == Configuration.ORIENTATION_LANDSCAPE) {
                translateView(velocityY * 0.3f, -velocityX * 0.3f);
            }
        } else {
            // no flinging for sound sources
        }

        return true;
    }

    @Override
    public void onLongPress(MotionEvent e) {
    }

    @Override
    public boolean onScroll(MotionEvent firstEvent, MotionEvent thisEvent, float distanceX, float distanceY) {

        // transform sound source or surface?
        if (lastTouchSoundSource != null) { // transform sound source
            synchronized (GlobalData.audioScene) {
                // is lastTouchSoundSource selected?
                if (!lastTouchSoundSource.isSelected()) {
                    // select only this source
                    GlobalData.audioScene.deselectAllSoundSources();
                    GlobalData.audioScene.selectSoundSource(lastTouchSoundSource);
                }

                // save positions of sources if this is first scroll event
                if (!scrolling) {
                    scrolling = true;
                    firstScrollPoint[0] = firstEvent.getX();
                    firstScrollPoint[1] = firstEvent.getY();
                    ArrayList<SoundSource> selectedSources = GlobalData.audioScene.getSelectedSoundSources();
                    int numSources = selectedSources.size();
                    for (int i = 0; i < numSources; i++) { // loop through all currently selected sources
                        selectedSources.get(i).getXY(point);
                        GlobalData.audioScene.mapPoint(point);
                        viewportTransformation.mapPoints(point);
                        selectedSources.get(i).savePosition(point);
                    }
                }

                // enough time elapsed to do next position update?
                long currentTime = SystemClock.uptimeMillis();
                if (currentTime - lastSendTime < MAX_POSITION_UPDATE_FREQ)
                    return true;
                lastSendTime = currentTime;

                // translate or rotate?
                if (transformationMode == TransformationMode.TRANSLATE) { // translate
                    // generate server request string
                    String strMsg = "<request>";
                    ArrayList<SoundSource> selectedSources = GlobalData.audioScene.getSelectedSoundSources();
                    int numSources = selectedSources.size();
                    SoundSource soundSource;
                    for (int i = 0; i < numSources; i++) { // loop through all currently selected sources
                        soundSource = selectedSources.get(i);

                        // if source is fixed, skip it
                        if (soundSource.isPositionFixed())
                            continue;

                        strMsg += "<source id='" + soundSource.getId() + "'>";
                        // transform screen coords into object coords, consider offset
                        point[0] = soundSource.getSavedX() + thisEvent.getX() - firstScrollPoint[0];
                        point[1] = soundSource.getSavedY() + thisEvent.getY() - firstScrollPoint[1];
                        inverseViewportTransformation.mapPoints(point);

                        if (soundSource.getSourceModel() == SoundSource.SourceModel.PLANE) { // recalculate orientation for plane waves
                            float norm = FloatMath.sqrt(point[0] * point[0] + point[1] * point[1]); // for plane waves, if source is movable
                            if (norm != 0.0f) {
                                float newAzimuth;
                                if (point[1] >= 0.0f)
                                    newAzimuth = (float) (Math.acos(point[0] / norm) / Math.PI * 180.0f) - 180.0f
                                            + GlobalData.audioScene.getReference().getAzimuth();
                                else
                                    newAzimuth = (float) -(Math.acos(point[0] / norm) / Math.PI * 180.0f) - 180.0f
                                            + GlobalData.audioScene.getReference().getAzimuth();
                                strMsg += "<orientation azimuth='" + String.valueOf(newAzimuth) + "'/>";
                            }
                        }

                        GlobalData.audioScene.inverseMapPoint(point);
                        strMsg += "<position x='" + String.valueOf(point[0]) + "' y='" + String.valueOf(point[1])
                                + "'/>";
                        strMsg += "</source>";
                    }
                    strMsg += "</request>\0";

                    // send changes to server
                    sendToServer(strMsg);
                } else { // rotate
                    // not implemented
                }
            }
        } else { // transform surface
            if (!scrolling) {
                scrolling = true;
                firstScrollPoint[0] = thisEvent.getX();
                firstScrollPoint[1] = thisEvent.getY();
                currentSavedTranslation[0] = currentTranslation[0];
                currentSavedTranslation[1] = currentTranslation[1];
            }

            // translate or rotate?
            if (transformationMode == TransformationMode.TRANSLATE) { // translate
                if (currentOrientation == Configuration.ORIENTATION_PORTRAIT) {
                    point[0] = thisEvent.getX() - firstScrollPoint[0];
                    point[1] = thisEvent.getY() - firstScrollPoint[1];
                } else if (currentOrientation == Configuration.ORIENTATION_LANDSCAPE) {
                    point[0] = thisEvent.getY() - firstScrollPoint[1];
                    point[1] = -(thisEvent.getX() - firstScrollPoint[0]);
                }
                setCurrentTranslation(currentSavedTranslation[0] + point[0], currentSavedTranslation[1] + point[1]);
            } else { // rotate
                // not implemented
            }
        }

        return true;
    }

    @Override
    public void onShowPress(MotionEvent e) {
    }

    @Override
    public boolean onSingleTapUp(MotionEvent e) {
        return true;
    }

    @Override
    public boolean onDoubleTap(MotionEvent e) {
        synchronized (GlobalData.audioScene) {
            if (lastTouchSoundSource == null) { // fit scene
                transformToFitScene();
            } else { // (un)mute sound sources
                // generate server request string
                String strMsg = "<request>";
                if (!lastTouchSoundSource.isSelected()) {
                    // (un)mute one source
                    strMsg += "<source id='" + lastTouchSoundSource.getId() + "' mute='"
                            + (lastTouchSoundSource.isMuted() ? "false" : "true") + "'/>";
                } else {
                    // (un)mute current selected group of sources
                    ArrayList<SoundSource> selectedSources = GlobalData.audioScene.getSelectedSoundSources();
                    int numSources = selectedSources.size();
                    SoundSource soundSource;
                    for (int i = 0; i < numSources; i++) { // loop through all currently selected sources
                        soundSource = selectedSources.get(i);
                        strMsg += "<source id='" + soundSource.getId() + "' mute='"
                                + (soundSource.isMuted() ? "false" : "true") + "'/>";
                    }
                }
                strMsg += "</request>\0";
                sendToServer(strMsg);
            }
        }

        return true;
    }

    @Override
    public boolean onDoubleTapEvent(MotionEvent e) {
        // nothing
        return false;
    }

    @Override
    public boolean onSingleTapConfirmed(MotionEvent event) {
        synchronized (GlobalData.audioScene) {
            // (de)select one or all sound sources
            if (lastTouchSoundSource != null) {
                if (lastTouchSoundSource.isSelected()) {
                    GlobalData.audioScene.deselectSoundSource(lastTouchSoundSource);
                } else {
                    GlobalData.audioScene.selectSoundSource(lastTouchSoundSource);
                }
            } else {
                if (GlobalData.audioScene.getSelectedSoundSources().isEmpty()) {
                    GlobalData.audioScene.selectAllSoundSources();
                } else {
                    GlobalData.audioScene.deselectAllSoundSources();
                }
            }
        }

        return true;
    }

    void sendToServer(String strMsg) {
        byte[] msg = EncodingUtils.getAsciiBytes(strMsg);
        if (msg.length > buffer.capacity()) // check if buffer long enough
            buffer = ByteBuffer.wrap(msg);
        buffer.clear();
        buffer.put(msg);
        buffer.flip();
        try {
            GlobalData.socketChannel.write(buffer);
        } catch (IOException e) {
            Log.d(TAG, "error on write: " + e.getMessage());
        }
    }
}