Вращение ImageView как компас (с «северным полюсом», установленным в другом месте) - PullRequest
34 голосов
/ 02 ноября 2011

Я озадачен тем, как реализовать «персональный компас», т. Е. Компас, который указывает на конкретный ориентир вместо стандартного «северного полюса» ... к сожалению, моя нынешняя попытка не удалась (не указать на данный подшипник). Он также подключен к ускорителю, чтобы иметь возможность динамически настраиваться в зависимости от того, в каком направлении поворачивает пользователь.

Вот моя текущая попытка (метод onSensorChanged(), который обновляет стрелку):

public void onSensorChanged( SensorEvent event ) {

            // If we don't have a Location, we break out
            if ( LocationObj == null ) return;

            float azimuth = event.values[0];
                            float baseAzimuth = azimuth;

            GeomagneticField geoField = new GeomagneticField( Double
                    .valueOf( LocationObj.getLatitude() ).floatValue(), Double
                    .valueOf( LocationObj.getLongitude() ).floatValue(),
                    Double.valueOf( LocationObj.getAltitude() ).floatValue(),
                    System.currentTimeMillis() );
            azimuth += geoField.getDeclination(); // converts magnetic north into true north

            //Correct the azimuth
            azimuth = azimuth % 360;

            //This is where we choose to point it
            float direction = azimuth + LocationObj.bearingTo( destinationObj );
            rotateImageView( arrow, R.drawable.arrow, direction );

            //Set the field
            if( baseAzimuth > 0 && baseAzimuth < 45 ) fieldBearing.setText("S");
            else if( baseAzimuth >= 45 && baseAzimuth < 90 ) fieldBearing.setText("SW");
            else if( baseAzimuth > 0 && baseAzimuth < 135 ) fieldBearing.setText("W");
            else if( baseAzimuth > 0 && baseAzimuth < 180 ) fieldBearing.setText("NW");
            else if( baseAzimuth > 0 && baseAzimuth < 225 ) fieldBearing.setText("N");
            else if( baseAzimuth > 0 && baseAzimuth < 270 ) fieldBearing.setText("NE");
            else if( baseAzimuth > 0 && baseAzimuth < 315 ) fieldBearing.setText("E");
            else if( baseAzimuth > 0 && baseAzimuth < 360 ) fieldBearing.setText("SE");
            else fieldBearing.setText("?"); 

        }

А вот метод, который вращает ImageView (rotateImageView()):

private void rotateImageView( ImageView imageView, int drawable, float rotate ) {

    // Decode the drawable into a bitmap
    Bitmap bitmapOrg = BitmapFactory.decodeResource( getResources(),
            drawable );

    // Get the width/height of the drawable
    DisplayMetrics dm = new DisplayMetrics(); getWindowManager().getDefaultDisplay().getMetrics(dm);
    int width = bitmapOrg.getWidth(), height = bitmapOrg.getHeight();

    // Initialize a new Matrix
    Matrix matrix = new Matrix();

    // Decide on how much to rotate
    rotate = rotate % 360;

    // Actually rotate the image
    matrix.postRotate( rotate, width, height );

    // recreate the new Bitmap via a couple conditions
    Bitmap rotatedBitmap = Bitmap.createBitmap( bitmapOrg, 0, 0, width, height, matrix, true );
    //BitmapDrawable bmd = new BitmapDrawable( rotatedBitmap );

    //imageView.setImageBitmap( rotatedBitmap );
    imageView.setImageDrawable(new BitmapDrawable(getResources(), rotatedBitmap));
    imageView.setScaleType( ScaleType.CENTER );
}

Любая помощь будет принята с благодарностью, поскольку я не совсем понимаю, как поступить. «Показания», которые я получаю, пытаясь это сделать, несколько неточны и указывают в неправильном направлении. Я что-то действительно отключаю или у меня был действительно плохой тест-запуск?

Ответы [ 3 ]

54 голосов
/ 08 ноября 2011

Ваша функция rotateImageView должна работать нормально, однако есть некоторые вещи, которые необходимо изменить в ваших вычислениях вращения.

//This is where we choose to point it
float direction = azimuth + LocationObj.bearingTo( destinationObj );
rotateImageView( arrow, R.drawable.arrow, direction );

Проблема в том, что Bearing To даст вам диапазон от -180 до 180, что немного запутает. Нам потребуется преобразовать это значение в диапазон от 0 до 360, чтобы получить правильное вращение.

Это таблица того, что мы действительно хотим, в сравнении с тем, что нам дает

+-----------+--------------+
| bearingTo | Real bearing |
+-----------+--------------+
| 0         | 0            |
+-----------+--------------+
| 90        | 90           |
+-----------+--------------+
| 180       | 180          |
+-----------+--------------+
| -90       | 270          |
+-----------+--------------+
| -135      | 225          |
+-----------+--------------+
| -180      | 180          |
+-----------+--------------+

Несмотря на то, что азимут находится в диапазоне от -180 до 180, 0 по-прежнему истинный север, что оставит нас в этом расчете:

// Store the bearingTo in the bearTo variable
float bearTo = LocationObj.bearingTo( destinationObj );

// If the bearTo is smaller than 0, add 360 to get the rotation clockwise.
if (bearTo < 0) {
    bearTo = bearTo + 360;
}

Если мы добавим несколько фиктивных значений для проверки нашей новой формулы:

float bearTo = -100;
// This will now equal to true
if (-100 < 0) {
    bearTo = -100 + 360 = 360 - 100 = 260;
}

Теперь мы разобрались с азимутом. Давайте перейдем к азимуту!

Вам нужно вычесть склонение вместо того, чтобы добавить его, так как мы хотим, чтобы азимут был равен 0, когда мы указываем телефон прямо на истинный север, вместо того, чтобы склонение было добавлено к азимуту, что затем даст нам удвоение склонения, когда мы Направьте телефон на истинный север. Исправьте это, вычтя склонение вместо того, чтобы добавить его.

azimuth -= geoField.getDeclination(); // converts magnetic north into true north

Когда мы сейчас повернем телефон на истинный север, азимут будет равен 0

Ваш код для исправления азимута больше не нужен.

// Remove / uncomment this line
azimuth = azimuth % 360;

Теперь мы продолжим до точки, где мы вычисляем реальное вращение. Но сначала я подведу итог, какой тип ценностей у нас сейчас, и объясню, что они на самом деле:

bearTo = Угол от истинного севера до места назначения от точки, в которой мы сейчас находимся.

азимут = угол, на который вы повернули телефон с истинного севера.

Сказав это, если вы направите свой телефон прямо на истинный север, мы действительно хотим, чтобы стрелка повернула на угол, установленный для bearTo. Если вы укажете свой телефон на 45 градусов от истинного севера, мы хотим, чтобы стрелка повернулась на 45 градусов меньше, чем BearTo. Это оставляет нас для следующих расчетов:

float direction = bearTo - azimuth;

Однако, если мы введем несколько фиктивных значений: bearTo = 45; азимут = 180;

direction = 45 - 180 = -135;

Это означает, что стрелка должна повернуться на 135 градусов против часовой стрелки. Нам нужно будет ввести похожее условие if, как мы это делали с bearTo!

// If the direction is smaller than 0, add 360 to get the rotation clockwise.
if (direction < 0) {
    direction = direction + 360;
}

Ваш несущий текст, N, E, S и W отключены, поэтому я исправил их в последнем методе ниже.

Ваш метод onSensorChanged должен выглядеть следующим образом:

public void onSensorChanged( SensorEvent event ) {

    // If we don't have a Location, we break out
    if ( LocationObj == null ) return;

    float azimuth = event.values[0];
    float baseAzimuth = azimuth;

    GeomagneticField geoField = new GeomagneticField( Double
        .valueOf( LocationObj.getLatitude() ).floatValue(), Double
        .valueOf( LocationObj.getLongitude() ).floatValue(),
        Double.valueOf( LocationObj.getAltitude() ).floatValue(),
        System.currentTimeMillis() );

    azimuth -= geoField.getDeclination(); // converts magnetic north into true north

    // Store the bearingTo in the bearTo variable
    float bearTo = LocationObj.bearingTo( destinationObj );

    // If the bearTo is smaller than 0, add 360 to get the rotation clockwise.
    if (bearTo < 0) {
        bearTo = bearTo + 360;
    }

    //This is where we choose to point it
    float direction = bearTo - azimuth;

    // If the direction is smaller than 0, add 360 to get the rotation clockwise.
    if (direction < 0) {
        direction = direction + 360;
    }

    rotateImageView( arrow, R.drawable.arrow, direction );

    //Set the field
    String bearingText = "N";

    if ( (360 >= baseAzimuth && baseAzimuth >= 337.5) || (0 <= baseAzimuth && baseAzimuth <= 22.5) ) bearingText = "N";
    else if (baseAzimuth > 22.5 && baseAzimuth < 67.5) bearingText = "NE";
    else if (baseAzimuth >= 67.5 && baseAzimuth <= 112.5) bearingText = "E";
    else if (baseAzimuth > 112.5 && baseAzimuth < 157.5) bearingText = "SE";
    else if (baseAzimuth >= 157.5 && baseAzimuth <= 202.5) bearingText = "S";
    else if (baseAzimuth > 202.5 && baseAzimuth < 247.5) bearingText = "SW";
    else if (baseAzimuth >= 247.5 && baseAzimuth <= 292.5) bearingText = "W";
    else if (baseAzimuth > 292.5 && baseAzimuth < 337.5) bearingText = "NW";
    else bearingText = "?";

    fieldBearing.setText(bearingText);

}
3 голосов
/ 07 ноября 2011

Вы должны быть в состоянии установить матрицу для ImageView без необходимости каждый раз воссоздавать растровое изображение, и ... нормализовать (это слово?) Показания.

float b = mLoc.getBearing();
if(b < 0)
    b = 360 + b;
float h = item.mHeading;
if(h < 0)
    h = 360 + h;
float r = (h - b) - 360;
matrix.reset();
matrix.postRotate(r, width/2, height/2);

В приведенном выше примере mLoc - это местоположение, возвращаемое провайдером GPS, а getBearing возвращает количество градусов к востоку от севера от текущего направления движения. Item.mHeading был рассчитан с использованием функции Location.bearingTo () с использованием mLoc и местоположением элемента. ширина и высота - размеры изображения.

Итак, убедитесь, что ваши переменные указаны в градусах, а не в радианах, и попробуйте «нормализовать» (получая заголовки в диапазоне 0-360, а не -180-180). Кроме того, если результаты отклоняются на 180 градусов, убедитесь, что вы приближаетесь к цели, а не к градусам от нее к вам.

Приведенную выше матрицу можно затем установить в ImageView с ScaleType.Matrix

.
imageView.setMatrix(matrix);
imageview.setScaleType(ScaleType.Matrix);

Поскольку вы вращаетесь вокруг центральной точки imageView (ширина / 2, высота / 2 в postRotate), ваш чертеж должен быть направлен вверх и будет вращаться во время рисования, а не заново создавать новый растровое изображение каждый раз.

1 голос
/ 07 ноября 2011

Я провел около 40 часов в один из выходных, пытаясь это сделать.

Боль в заднице, надеюсь, я избавлю вас от этой боли.

Хорошо, я предупреждаю вас, это некрасивый код. Я был в затруднении, чтобы закончить это, у него нет схем именования, но я попытался прокомментировать это как можно лучше для вас.

Он был использован для размещения больших куч орехов, разложенных на полях для хранения

Используя текущую широту и долготу телефонов, широту / долготу пункта назначения, датчик компаса и некоторую алгебру, я смог вычислить направление к пункту назначения.

Значения широты / долготы и показания датчика взяты из класса MainApplication

Это часть кода для arrow.class, который я использовал для рисования стрелки на холсте в направлении.

    //The location you want to go to//
    //"Given North"
    double lat=0;
    double lon=0;
    //////////////////////////////////
    protected void onDraw(Canvas canvas) {

    //Sensor values from another class managing Sensor
    float[] v = MainApplication.getValues();

    //The current location of the device, retrieved from another class managing GPS
    double ourlat=  MainApplication.getLatitudeD();
    double ourlon=  MainApplication.getLongitudeD(); 

    //Manually calculate the direction of the pile from the device
    double a= Math.abs((lon-ourlon));
    double b= Math.abs((lat-ourlat));
    //archtangent of a/b is equal to the angle of the device from 0-degrees in the first quadrant. (Think of a unit circle)
    double thetaprime= Math.atan(a/b);
    double theta= 0;

    //Determine the 'quadrant' that the desired location is in
    //ASTC (All, Sin, Tan, Cos)  Determines which value is positive
    //Gotta love Highschool algebra

    if((lat<ourlat)&&(lon>ourlon)){//-+ 
        //theta is 180-thetaprime because it is in the 2nd quadrant
        theta= ((Math.PI)-thetaprime); 

        //subtract theta from the compass value retrieved from the sensor to get our final direction
        theta=theta - Math.toRadians(v[0]);

    }else if((lat<ourlat)&&(lon<ourlon)){//--
        //Add 180 degrees because it is in the third quadrant
        theta= ((Math.PI)+thetaprime);

        //subtract theta from the compass value retreived from the sensor to get our final direction
        theta=theta - Math.toRadians(v[0]);

    }else if((lat>ourlat)&&(lon>ourlon)){ //++
        //No change is needed in the first quadrant
        theta= thetaprime; 

        //subtract theta from the compass value retreived from the sensor to get our final direction
        theta=theta - Math.toRadians(v[0]);

    }else if((lat>ourlat)&&(lon<ourlon)){ //+-
        //Subtract thetaprime from 360 in the fourth quadrant
        theta= ((Math.PI*2)-thetaprime);

        //subtract theta from the compass value retreived from the sensor to get our final direction
        theta=theta - Math.toRadians(v[0]);

    }

    canvas.drawBitmap(_bitmap, 0, 0, paint);
    float[] results = {0}; //Store data
    Location.distanceBetween(ourlat, ourlon, lat, lon, results);
    try{

        //Note, pileboundary is a value retreived from a database
        //This changes the color of the canvas based upon how close you are to the destination
        //Green < 100 (or database value), Yellow < (100)*2, Otherwise red
        if((results[0])<(pileboundary==0?100:pileboundary)){
            _canvas.drawColor(Color.GREEN);
        }else if((results[0])<(pileboundary==0?100:pileboundary)*2){
            _canvas.drawColor(Color.YELLOW);
        }else{
            _canvas.drawColor(Color.rgb(0xff, 113, 116)); //RED-ish
        }
        //Draw the distance(in feet) from the destination
        canvas.drawText("Distance: "+Integer.toString((int) (results[0]*3.2808399))+ " Feet", 3, height-3, textpaint);
    }catch(IllegalArgumentException ex){
        //im a sloppy coder 
    }
    int w = canvas.getWidth();
    int h = height;
    int x = w / 2; //put arrow in center
    int y = h / 2;
    canvas.translate(x, y);
    if (v != null) {

         // Finally, we rotate the canvas to the desired direction
         canvas.rotate((float)Math.toDegrees(theta));


    }
    //Draw the arrow!
    canvas.drawPath(thearrow, paint);
}   


//Some of my declarations, once again sorry :P
GeomagneticField gf;
Bitmap _bitmap;
Canvas _canvas;
int _height;
int _width; 
Bitmap b;
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    //Get the current GeomagneticField (Should be valid until 2016, according to android docs)
    gf = new GeomagneticField((float)lat,(float)lon,(float)MainApplication.getAltitude(),System.currentTimeMillis());
    _height = View.MeasureSpec.getSize(heightMeasureSpec);
    _width = View.MeasureSpec.getSize(widthMeasureSpec);
    setMeasuredDimension(_width, _height);
    _bitmap = Bitmap.createBitmap(_width, _height, Bitmap.Config.ARGB_8888);
    _canvas = new Canvas(_bitmap);
    b=Bitmap.createBitmap(_bitmap);
    drawBoard();
    invalidate();
}


//Here is the code to draw the arrow 
    thearrow.moveTo(0, -50);
    thearrow.lineTo(-20, 50);
    thearrow.lineTo(0, 50);
    thearrow.lineTo(20, 50);
    thearrow.close();
    thearrow.setFillType(FillType.EVEN_ODD);

Надеюсь, вам удастся прочитать мой код ... Если у меня будет время, я сделаю его немного красивее.

Если вам нужно какое-либо объяснение, дайте мне знать.

-MrZander

...