Rotating an ImageView like a compass (with the “north pole” set elsewhere)

后端 未结 3 393
伪装坚强ぢ
伪装坚强ぢ 2020-12-07 09:11

I\'m stumped regarding how to implement a \"personal compass\", ie a compass that points to a specific bearing instead of the standard \"north pole\"... unfortunatly, my cu

相关标签:
3条回答
  • 2020-12-07 10:07

    Your rotateImageView function should work just fine, however there are some things that needs to be changed in your rotation calculations.

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

    The problem is that bearingTo will give you a range from -180 to 180, which will confuse things a bit. We will need to convert this value into a range from 0 to 360 to get the correct rotation.

    This is a table of what we really want, comparing to what bearingTo gives us

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

    Even though the bearingTo is in the range -180 to 180, 0 is still true north which will leave us to this calculation:

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

    If we add some dummy values to test our new formula:

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

    We've now sorted out the bearingTo, lets head on to the azimuth!

    You need to substract the declination instead of adding it, as we want azimuth to be 0 when we point the phone directly at true north instead of having the declination added to the azimuth, which will then give us double the declination when we point the phone to true north. Correct this by subtracting the declination instead of adding it.

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

    When we turn the phone to true north now, azimuth will then equal to 0

    Your code for correcting the azimuth is no longer necessary.

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

    We will now continue to the point of where we calculate the real rotation. But first i will summarize what type of values we have now and explaining what they really are:

    bearTo = The angle from true north to the destination location from the point we're your currently standing.

    azimuth = The angle that you've rotated your phone from true north.

    By saying this, if you point your phone directly at true north, we really want the arrow to rotate the angle that bearTo is set as. If you point your phone 45 degrees from true north, we want the arrow to rotate 45 degrees less than what bearTo is. This leaves us to the following calculations:

    float direction = bearTo - azimuth;
    

    However, if we put in some dummy values: bearTo = 45; azimuth = 180;

    direction = 45 - 180 = -135;
    

    This means that the arrow should rotate 135 degrees counter clockwise. We will need to put in a similiar if-condition as we did with the bearTo!

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

    Your bearing text, the N, E, S and W is off, so i've corrected them in the final method below.

    Your onSensorChanged method should look like this:

    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);
    
    }
    
    0 讨论(0)
  • 2020-12-07 10:10

    I spent about 40 hours one weekend trying to do this.

    Pain in the butt, hopefully I can spare you that pain.

    Ok, I am warning you, this is some ugly code. I was in a pinch to finish it, it has no naming schemes, but i tried to comment it as best as I could for you.

    It was used to locate large piles of nuts laying out in fields for storage

    Using the phones current latitude and longitude, the lat/lon of the destination, the compass sensor, and some algebra, I was able to calculate the direction to the destination.

    Lat/lon and sensor readings are pulled from the MainApplication class

    This is some of the code for arrow.class, which I used to draw an arrow on a canvas towards a 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);
    

    Hopefully you can manage to read my code... If I get time, I will make it a bit prettier.

    If you need any explaining, let me know.

    -MrZander

    0 讨论(0)
  • 2020-12-07 10:14

    You should be able to set the matrix to the ImageView without having to recreate the bitmap each time, and er.. 'normalise' (is that the word?) the readings.

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

    In the above example mLoc is a Location returned by a gps provider and getBearing returns the number of degrees east of north of the current direction of travel. item.mHeading has been calculated using the Location.bearingTo() function using mLoc and the item's location. width and height are the dimensions of the image view.

    So, make sure your variables are in degrees and not radians, and try 'normalising' (getting headings into the range of 0-360 and not -180-180). Also, if the results are off by 180 degrees, make sure you're getting the bearingTo your target, rather than the degrees from your target to you.

    The above matrix can then be set in an ImageView that has a ScaleType.Matrix

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

    Since you're rotating around the centre point of the imageView (the width/2, height/2 in the postRotate), your drawable should be pointing upwards and will be rotated at draw time, rather than re-creating a new bitmap each time.

    0 讨论(0)
提交回复
热议问题