web-dev-qa-db-fra.com

Faire pivoter une ImageView comme une boussole (avec le «pôle nord» réglé ailleurs)

Je suis perplexe quant à la façon de mettre en œuvre une "boussole personnelle", c'est-à-dire une boussole qui pointe vers un relèvement spécifique au lieu du "pôle nord" standard ... malheureusement, ma tentative actuelle a mal tourné (ne pointe pas vers le compte tenu du roulement). Il est également connecté à l'accélérateur pour pouvoir s'ajuster dynamiquement en fonction de la direction dans laquelle l'utilisateur tourne.

Voici ma tentative actuelle (la méthode onSensorChanged()- qui met à jour la flèche):

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("?"); 

        }

Et voici la méthode qui fait pivoter 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 );
}

Toute aide serait très appréciée, car je ne sais pas trop comment procéder. Les "lectures" que j'obtiens en l'essayant sont quelque peu inexactes et pointent dans la mauvaise direction. Suis-je en train de faire quelque chose de vraiment mal, ou ai-je juste eu un très mauvais essai?

35
karllindmark

Votre fonction rotationImageView devrait fonctionner très bien, mais il y a certaines choses qui doivent être modifiées dans vos calculs de rotation.

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

Le problème est que le roulementTo vous donnera une plage de -180 à 180, ce qui confondra un peu les choses. Nous devrons convertir cette valeur dans une plage de 0 à 360 pour obtenir la rotation correcte.

Voici un tableau de ce que nous voulons vraiment, comparé à ce que le roulement nous donne

 + ----------- + -------------- + 
 | portantTo | Relèvement réel | 
 + ----------- + -------------- + 
 | 0 | 0 | 
 + ----------- + -------------- + 
 | 90 | 90 | 
 + ----------- + -------------- + 
 | 180 | 180 | 
 + ----------- + -------------- + 
 | -90 | 270 | 
 + ----------- + -------------- + 
 | -135 | 225 | 
 + ----------- + -------------- + 
 | -180 | 180 | 
 + ----------- + -------------- + 

Même si le relèvement To est compris entre -180 et 180, 0 est toujours le nord vrai, ce qui nous laissera à ce calcul:

// 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;
}

Si nous ajoutons des valeurs fictives pour tester notre nouvelle formule:

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

Nous avons maintenant trié le relèvement À, passons à l'azimut!

Vous devez soustraire la déclinaison au lieu de l'ajouter, car nous voulons que l'azimut soit 0 lorsque nous pointons le téléphone directement vers le nord vrai au lieu d'avoir la déclinaison ajoutée à l'azimut, ce qui nous donnera alors le double de la déclinaison lorsque nous pointons le téléphone au vrai nord. Corrigez cela en soustrayant la déclinaison au lieu de l'ajouter.

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

Lorsque nous tournons le téléphone vers le nord vrai maintenant, l'azimut sera alors égal à 0

Votre code pour corriger l'azimut n'est plus nécessaire.

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

Nous allons maintenant continuer jusqu'au point où nous calculons la rotation réelle. Mais je vais d'abord résumer quel type de valeurs nous avons maintenant et expliquer ce qu'elles sont vraiment:

bearTo = L'angle entre le vrai nord et l'emplacement de destination à partir du point où vous vous trouvez actuellement.

azimut = l'angle que vous avez fait pivoter votre téléphone par rapport au nord vrai.

En disant cela, si vous pointez votre téléphone directement vers le nord vrai, nous voulons vraiment que la flèche fasse pivoter l'angle de BearTo. Si vous pointez votre téléphone à 45 degrés par rapport au nord vrai, nous voulons que la flèche tourne de 45 degrés de moins que BearTo. Cela nous laisse aux calculs suivants:

float direction = bearTo - azimuth;

Cependant, si nous mettons des valeurs fictives: bearTo = 45; azimut = 180;

direction = 45 - 180 = -135;

Cela signifie que la flèche doit tourner de 135 degrés dans le sens antihoraire. Nous devrons mettre dans une condition if similaire comme nous l'avons fait avec le bearTo!

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

Votre texte portant, le N, E, S et W est désactivé, donc je les ai corrigés dans la méthode finale ci-dessous.

Votre méthode onSensorChanged devrait ressembler à ceci:

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);

}
55
chris

Vous devriez pouvoir définir la matrice sur ImageView sans avoir à recréer le bitmap à chaque fois, et euh ... "normaliser" (est-ce le mot?) Les lectures.

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);

Dans l'exemple ci-dessus, mLoc est un emplacement renvoyé par un fournisseur de GPS et getBearing renvoie le nombre de degrés à l'est du nord de la direction de déplacement actuelle. item.mHeading a été calculé à l'aide de la fonction Location.bearingTo () à l'aide de mLoc et de l'emplacement de l'élément. largeur et hauteur sont les dimensions de la vue de l'image.

Donc, assurez-vous que vos variables sont en degrés et non en radians, et essayez de "normaliser" (en obtenant des titres dans la plage de 0 à 360 et non de -180 à 180). De plus, si les résultats sont décalés de 180 degrés, assurez-vous d'obtenir le relèvement de votre cible, plutôt que les degrés de votre cible vers vous.

La matrice ci-dessus peut ensuite être définie dans une ImageView qui a un ScaleType.

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

Puisque vous tournez autour du point central de l'imageView (la largeur/2, la hauteur/2 dans le postRotate), votre dessinable devrait être pointé vers le haut et sera tourné au moment du dessin, plutôt que de recréer un nouveau bitmap à chaque fois .

3
FunkTheMonk

J'ai passé environ 40 heures un week-end à essayer de le faire.

Douleur dans le cul, j'espère que je pourrai vous épargner cette douleur.

Ok, je vous préviens, c'est un code moche. J'étais dans un pincement pour le terminer, il n'a aucun schéma de nommage, mais j'ai essayé de le commenter du mieux que je pouvais pour vous.

Il a été utilisé pour localiser de gros tas de noix disposés dans des champs pour le stockage

En utilisant la latitude et la longitude actuelles du téléphone, le lat/lon de la destination, le capteur de la boussole et une algèbre, j'ai pu calculer la direction de la destination.

Les relevés de lat/lon et de capteur sont extraits de la classe MainApplication

Ceci est une partie du code de arrow.class, que j'ai utilisé pour dessiner une flèche sur une toile vers une direction.

    //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);

Si tout va bien vous pouvez réussir à lire mon code… Si j'obtiens le temps, je le ferai un peu plus joli.

Si vous avez besoin d'explications, faites-le moi savoir.

-MrZander

1
MrZander