Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds support to animateToBearing and animateToViewingAngle ( IOS + Android ) #1544

Conversation

sandinosaso
Copy link
Contributor

@sandinosaso sandinosaso commented Aug 11, 2017

First of all, thank you all guys for maintaining this awesome repo, it helps a lot to get started using Maps with ReactNative.

For those who are not familiar with the names of the functions this PR adds, Bearing is the direction or course of motion itself, and Viewing angle is the angle of the camera attached to the MapView.
This PR allows you to change the bearing dynamically (for example for apps that requires navigation features like Waze does) and also can change the map view angle (so you get like building shape kinda 3D).

This PR adds support for IOS (for now, I plan to do for Android soon). I am pushing this so if anyone can help with feedback or code for Android I appreciate it 👍.
Updated: This PR adds full support for IOS (both native MapKit map and Google Maps) and Android.

Google Map Documentation for both methods is available here: https://developers.google.com/maps/documentation/ios-sdk/reference/interface_g_m_s_map_view.html#a5e59e999c9fa8222ecc8b9b45ce95976

I have no experience on Objective-C so they may be a better way of doing this, I just want to contribute trying to share something that worked for me :)

I attach a .gif trying to show the bearing effect (see how the map rotates towards the direction of motion), is hard to appreciate using a gif (it looks nice on real device when driving :)

animatetobearing2-480

@sandinosaso sandinosaso force-pushed the AnimateToBearing-And-AnimateToViewingAngle branch 2 times, most recently from b00f8fd to 7006e3a Compare August 11, 2017 04:29
@mileung
Copy link

mileung commented Aug 14, 2017

Thanks a lot for this! Looking forward to the Android support.

@christopherdro
Copy link
Collaborator

Is this only possible with Google Maps on iOS? Can we add this to native maps as well?

@@ -64,6 +64,8 @@ To access event data, you will need to use `e.nativeEvent`. For example, `onPres
|---|---|---|
| `animateToRegion` | `region: Region`, `duration: Number` |
| `animateToCoordinate` | `coordinate: LatLng`, `duration: Number` |
| `animateToBearing` | `bearing: Number`, `duration: Number` |
| `animateToViewingAngle` | `angle: Number`, `duration: Number` |
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add note about it only being available on Google maps for iOS.

Copy link
Contributor Author

@sandinosaso sandinosaso Aug 16, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@christopherdro I think MapKit also allows you to set the bearing in degress, seems that they only call it differently (heading): https://developer.apple.com/documentation/mapkit/mkmapcamera

var heading: CLLocationDirection
The heading of the camera (measured in degrees) relative to true north.

I feel that bearing (https://en.wikipedia.org/wiki/Bearing_(navigation)) is a more proper name.
For the Viewing Angle of the camera I am not so sure if ViewingAngle is better than pitch (how MapKit on IOS call it):

var pitch: CGFloat
The viewing angle of the camera, measured in degrees.

I think is possible to add the support for MapKit (IOS provided) as I did for GoogleMaps, the only thing is I want to know your position regarding naming? Should we follow GoogleMap or MapKit name convention?

Copy link
Contributor Author

@sandinosaso sandinosaso Aug 26, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added support for the IOS native map...Basically, I do (for both animations) the difference is that in one I set some property on the camera before: Pitch (ViewAngle on GoogleMaps) and Heading (Bearing on GoogleMaps):

[AIRMap animateWithDuration:duration/1000 animations:^{
    [mapView setCamera:mapCamera animated:YES];
}];

This is the commit: af3ff1e

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@christopherdro I addressed your PR comments :)

@sandinosaso
Copy link
Contributor Author

sandinosaso commented Aug 26, 2017

Guys I need feedback on the commit that adds support for IOS native map. The thing is that the animation is working ok when called for JS but I am experiencing a strange (or maybe not) behavior on my app.

I am tracking user geolocation and when that changes I am centering the Map to his position (using animateToRegion) and rotating the map to the direction of move (using animateToBearing).

My pseudo code is as follows:

this.watchID = navigator.geolocation.watchPosition((position) => {
... // I calculate distance moved, the angle he moved, etc

// At the end I do both animations (centering and rotate the map)
this.map.animateToRegion(newRegion);
this.map.animateToBearing(rotationAngle);
});

When using provider={GOOGLE_MAPS} everything works nice (as I showed in the gif on this PR)
But when using MapKit (native IOS map provider) seems that only the last animation has effect (in this case I only got the rotation of the map working but not centering it) ...if I switch those 2 lines I got only the centering working and not the map rotation.

I am trying to figure out why is this happening but looking at MapKit code, but as I said I am not a Swift expert, not sure, I guess there is some batch processing of the animations and as they happen too quick I am not able to see the first one? Maybe my last commit is not doing well? and I am calling another Map instance? ...I am suspecting of something like that.

I appreciate your comments :)

@sandinosaso
Copy link
Contributor Author

sandinosaso commented Aug 27, 2017

Another thing I noticed is that Native MapKit on IOS does not show 3D like of building, it just rotates the map camera but I do not see same as when using GoogleMaps.

Do you know guys if this is supported ?

Example (on IOS using MapKit) of this.map.animateToViewingAngle(45) on this gif:
animatetoviewangleonmapkit

The same code just with provider={PROVIDER_GOOGLE}:
animatetoviewangleongooglemap

@sandinosaso
Copy link
Contributor Author

@mileung added Android support on the latest commit :)
Do you guys let me know what do you think about it.
This is an example showing support on Android:

animatetoviewangleongooglemapandroid

@sandinosaso sandinosaso changed the title Adds support to animateToBearing and animateToViewingAngle Adds support to animateToBearing and animateToViewingAngle ( IOS + Android ) Aug 27, 2017
@sandinosaso sandinosaso force-pushed the AnimateToBearing-And-AnimateToViewingAngle branch from a78e112 to 12aa7fe Compare August 28, 2017 13:38
@@ -220,6 +222,18 @@ public void receiveCommand(AirMapView view, int commandId, @Nullable ReadableArr
view.animateToCoordinate(new LatLng(lat, lng), duration);
break;

case ANIMATE_TO_VIEWING_ANGLE:
angle = (float)args.getDouble(0);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not like the casting to float. I wonder if you guys know a better way of getting a float argument? Appreciate any help :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't a huge deal. The cast has to happen at some point. There is usually a space after a type cast though.

@EricPKerr
Copy link

@sandinosaso does this support passing in the bearing to the <MapView region={} ... /> prop? Or does it require that you call animateToRegion and animateToBearing? I ask because there seems to be conflicting direction around the preferred way to update the map's viewport (via props or via methods), so it would be nice to simplify that.

@sandinosaso
Copy link
Contributor Author

@EricPKerr this PR only support programmatically calling the animateToRegion and animateToBearing methods for changing the camera.
If you need by default to have those, you can call methods just in componentDidMount

I did not know about a discussion about the best approach to updating the map's viewport (via props or via methods) can you point me out the link where the discussion is being held?

Regards.

@EricPKerr
Copy link

EricPKerr commented Aug 29, 2017

@sandinosaso there isn't really a discussion on this yet, but all of the examples I see for react-native-maps seem to favor using initialRegion and updating the region dynamically with methods instead of ever using the region prop. If that's the case then the region prop should potentially be deprecated. However, I'm not aware of any other react-native libraries that favor this this._ref.method() approach over props.

@sandinosaso
Copy link
Contributor Author

sandinosaso commented Aug 29, 2017

@EricPKerr I understand your concern, and I think is a topic to discuss (a new thread). For this particular PR I am doing I think having the ability to change the MapView camera (orientation/pitch/view-angle) programmatically is a MUST. If you plan your app to do some kind of Navigation feature (following user position, car driving, car parking, etc) you will need to call it programmatically, also not using Props allow me to do not do another render(). When changing camera orientation (programmatically and not doing a change on props) I am using same Map instance, so you do not need to force a re-render().
I know that if we use props we can change the componentShouldUpdate and tell the Map to not render under certain props change. That is useful (indeed I am doing something to not render the map as the user moves around on my app when having a lot of markers). But I like that kind of decisions to be pure performance improvements instead of something I forced to do because the way I am passing props to the component.
I am not sure if my explanation is clear. Let me know what do you think.
Thank you for the feedback :)

@vivianmauer
Copy link

@sandinosaso I'm getting the following error with the last commit when i tried to run-android

/node_modules/react-native-maps/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapManager.java:226: error: cannot find symbol
        angle = (float)args.getDouble(0);
        ^
  symbol:   variable angle
  location: class AirMapManager
/node_modules/react-native-maps/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapManager.java:228: error: cannot find symbol
        view.animateToViewingAngle(angle, duration);
                                   ^
  symbol:   variable angle
  location: class AirMapManager
/node_modules/react-native-maps/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapManager.java:232: error: cannot find symbol
        bearing = (float)args.getDouble(0);
        ^
  symbol:   variable bearing
  location: class AirMapManager
/node_modules/react-native-maps/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapManager.java:234: error: cannot find symbol
        view.animateToBearing(bearing, duration);
                              ^
  symbol:   variable bearing
  location: class AirMapManager
Note: Some input files use or override a deprecated API.
Note: Recompile with -Xlint:deprecation for details.
Note: Some input files use unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
4 errors
:react-native-maps:compileReleaseJavaWithJavac FAILED

On ios it seems ok

@sandinosaso sandinosaso force-pushed the AnimateToBearing-And-AnimateToViewingAngle branch from 12aa7fe to ecfc89b Compare September 3, 2017 03:08
@sandinosaso
Copy link
Contributor Author

@jonathanmauer Thanks you for trying and finding out that error. It worked for me because I was pointing directly to my local changes, I forgot to push a commit. I updated the PR, could you try again and let me know if it works now?
Thanks

@vivianmauer
Copy link

vivianmauer commented Sep 4, 2017

@sandinosaso Its working now, but i have an strange bug. When the map angle is 45 in android, fitToCoordinates does not work. In ios its ok. For now, i have to use animateToViewingAngle after fitToCoordinates is complete. But it would be great if the angle could be 45(or any other) on app start

edit -------------
Sorry, I made a mistake. I was triggering animateToViewingAngle immediately after fitToCoordinates. It seems like everything is ok

Great job! Waiting for an release

@aaronbuchanan
Copy link

Can't wait for this to land, really appreciate the work here guys!

@thayanithik
Copy link

@sandinosaso Hey, i have used it on android.

But it returns, an error as " undefined is not a function(evaluating this._map.animateToBearing() ) "

Can you help me to enable this feature clearly on android.

I have configured all the settings what you have told on that fix.
But it returns error..pls help on this

@sandinosaso
Copy link
Contributor Author

@thayanithik That error message makes me think that you are not pointing to the code on this PR.
How did you manage to install react-native-maps on your project making it point to this PR (so you can use this feature)? Or did you tried all the changes by yourself in the code?
Thanks...Also if you can share some gist or code where it is used

@connorzg
Copy link

Nice work @sandinosaso. This is something I'd like to use ASAP. @christopherdro is there anything else this PR is waiting on?

@jojonarte
Copy link

Great work @sandinosaso I've been trying to do this myself.

@raihanrazi
Copy link

Can't wait to have this merged!

@aaronbuchanan
Copy link

Anything that we can do to help move this along? Is there work going on to solve this on another thread somewhere?

@sandinosaso
Copy link
Contributor Author

@aaronbuchanan I have the same question, maybe you are working on something similar or this is not the approach you guys want? Let me know, any feedback is appreciated I have heard nothing about why this is so quiet :)
@christopherdro any thoughts on this? Thank you :)

@aaronbuchanan
Copy link

It feels like the owners might just be innundated, I've been maintaining my own fork that cobbles together a few fixes that are sitting on PRs right now but it's going to end badly if those things don't make it soon lol (sigh, it's true)

🍺

@@ -220,6 +222,18 @@ public void receiveCommand(AirMapView view, int commandId, @Nullable ReadableArr
view.animateToCoordinate(new LatLng(lat, lng), duration);
break;

case ANIMATE_TO_VIEWING_ANGLE:
angle = (float)args.getDouble(0);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't a huge deal. The cast has to happen at some point. There is usually a space after a type cast though.

@gpeal gpeal merged commit 2ef4c36 into react-native-maps:master Oct 5, 2017
@aaronbuchanan
Copy link

THANK YOU @gpeal !!

@echap
Copy link

echap commented Oct 30, 2017

Hi guys, facing an issue with certainely this PR, I have the message TypeError: undefined is not an object (evaluating 'this._mapManagerCommand(name).apply') (see #1743 ), would you have any clue on this ?

@echap
Copy link

echap commented Nov 2, 2017

Cf the issue I was dropping (#1743), I would like to inform you that react-native-maps@0.17.0 (embedded in expo) doesn't work on ios nor android to animate bearing or viewing angle.

Could we get deeper on this ?

@bacancy-suresh
Copy link

@sandinosaso Can you please share react native code like this image ?
29201348-dc03ab02-7e33-11e7-8a4f-b55030ff395a

Because i have tried much way with your PR but not success
Thanks

@ghost
Copy link

ghost commented Dec 6, 2017

Please share the code like the image
32448956-52e99262-c336-11e7-9168-d1061599cb08

Need it badly, Please help

Thanks

@the-simian
Copy link

Thank you for working on this! Great feature

@sandinosaso
Copy link
Contributor Author

sandinosaso commented Jan 17, 2018

@krunalpanchal @bacancy-suresh I can share code with you to behave like that example.
Sorry I answered now, last 2 months I had lot of changes, moved to a different country / city / job / etc.

What I am doing is:

      this.watchID = navigator.geolocation.watchPosition((position) => {
          const animationPosition = getPositionWithOffset({ latitude: position.coords.latitude, longitude: position.coords.longitude }, { dn: 0, de: 0 });
          const newRegion = {
            latitude: animationPosition.latitude,
            longitude: animationPosition.longitude,
            latitudeDelta: LATITUDE_DELTA_CURRENT_POSITION,
            longitudeDelta: LONGITUDE_DELTA_CURRENT_POSITION,
          };

          // const previousCoords = { latitude: this.state.region.latitude, longitude: this.state.region.longitude };
          const distanceMoved = distanceBetweenPoints(this.state.latestUpdatePosition, position.coords);
          const rotationAngle = getRotationAngle(this.state.latestUpdatePosition, position.coords);
          console.log('Got new position, distanceMoved, rotationAngle', position, distanceMoved, rotationAngle);

          const userCurrentPosition = {
            latitude: position.coords.latitude,
            longitude: position.coords.longitude,
          };

          if (distanceMoved > MIN_METERS_NEEDED_FOR_UPDATE_MARKERS) {
            console.log('Going to call getNewMarkers with userCurrentPosition:', userCurrentPosition);
            // If more than MIN_METERS_NEEDED_FOR_UPDATE_MARKERS meters then get new markers
            this.props.getNewMarkers(userCurrentPosition.latitude, userCurrentPosition.longitude);

            this.setState({
              latestUpdatePosition: userCurrentPosition,
            });
          }

          console.log('Called navigator.geolocation.watchPosition. distanceMoved, rotationAngle, userCurrentPosition:', distanceMoved, rotationAngle, userCurrentPosition);
          this.setState({
            distanceMoved,
            rotationAngle,
            userCurrentPosition,
          });

          this.map.animateToRegion(newRegion);
          this.map.animateToBearing(rotationAngle);
          this.map.animateToViewingAngle(45);
        },
        (error) => alert(error.message),
        { enableHighAccuracy: GEOLOCATION_HIGH_ACCURACY_ENABLED, timeout: GEOLOCATION_TIMEOUT_MS, maximumAge: GEOLOCATION_MAXAGE_MS, distanceFilter: GEOLOCATION_DISTANCE_FILTER_METERS }
        );

The idea is watch the position with the geolocation.watchPosition I have small value for the distanceFilter (so I am checking it every 10 meters the user moves)...then calculate the angle he is moving and how much it moved (I use the how much it moved to avoid too much re-renders by overriding the shouldComponentUpdate).

Here is the function that calculate the angle to apply the this.map.animateToBearing():

const getRotationAngle = (previousPosition, currentPosition) => {
  const x1 = previousPosition.latitude;
  const y1 = previousPosition.longitude;
  const x2 = currentPosition.latitude;
  const y2 = currentPosition.longitude;

  const xDiff = x2 - x1;
  const yDiff = y2 - y1;

  return (Math.atan2(yDiff, xDiff) * 180.0) / Math.PI;
};

Let me know if that helps you guys, if not let me know what are the problem you have when using this functions.

Regards.
Sandino.

@ghost
Copy link

ghost commented Jan 17, 2018

Hi Sandino,
Thanks for the code, do you have any working app available ? I mean on playstore / appstore?

Also is it possible to share the code complete for the map animation, sorry I am new to RN so couldn't make it.
If possible please share the code on knpddit@gmail.com.
Thanks

@juergengunz
Copy link

@sandinosaso I tested your example and it seems that in general, animatetoRegion is resetting the bearing and it gives a weird animation due to that reason. Do you experience the same problem? Especially on android. On iOS it seems to work like it should

@ajaykumar97
Copy link

@juergengunz Same issue. animateToRegion() is resetting the bearing in android. Please provide getPositionWithOffset() function code and if other things to be kept in mind while using animateToRegion() or animateToBearing().

@jackphuongvu
Copy link

@sandinosaso I used and imported your code however it doesn't work like you so much. It's not smooth like you and for that reason, would you mind sharing your code to display exactly What happens in your image included in Updating to get new markers as well.
It's really good and helpful for other people working with React-native-maps if you can create an example repository about that?
Thanks so much and have a good day!

@veselinnguyen
Copy link

Does this works on Android ?

@Eramirez06
Copy link

@sandinosaso could you provide what you do in this function

distanceBetweenPoints

@sandinosaso
Copy link
Contributor Author

Sorry, @Eramirez06 years without seeing this. I just see this because a coworker used this library and I showed him this old PR :)

I basically calculate distance between 2 points using geolib library.
I can share some helpers functions that I use (pure math calculations):

Hoping this help someone coming from the future ;)

import Polyline from '@mapbox/polyline';
import geolib from 'geolib';
// Converts numeric degrees to radians
const toRad = (value) => (value * Math.PI) / 180;

// This function takes in latitude and longitude of two location and returns the distance between them as the crow flies (in meters)
const distanceBetweenPoints = (origen, destino) => {
  const distance = geolib.getDistance(origen, destino);
  return distance;
};

const getPositionWithOffset = ({ latitude: lat, longitude: lon }, { dn = 100, de = 100 }) => {
  const R = 6371 * 1000; // Earth Radius in Km * 1000 = meters
  // Coordinate offsets in radians
  const dLat = dn / R;
  const dLon = de / (R * Math.cos((Math.PI * lat) / 180));

  // OffsetPosition, decimal degrees
  const latO = lat + (dLat * (180 / Math.PI));
  const lonO = lon + (dLon * (180 / Math.PI));

  return { latitude: latO, longitude: lonO };
};

const getRotationAngle = (previousPosition, currentPosition) => {
  const x1 = previousPosition.latitude;
  const y1 = previousPosition.longitude;
  const x2 = currentPosition.latitude;
  const y2 = currentPosition.longitude;

  const xDiff = x2 - x1;
  const yDiff = y2 - y1;

  return (Math.atan2(yDiff, xDiff) * 180.0) / Math.PI;
};

const convertRouteToPolyline = (route) => {
  const points = Polyline.decode(route.overview_polyline.points);
  const coords = points.map((point, index) => ({
    latitude: point[0],
    longitude: point[1],
  }));
  return coords;
};

const getMissingVertices = (point1, point2) => {
  const missingVertice1 = {
    latitude: (point1.latitude + point2.latitude + point2.longitude - point1.longitude) / 2,
    longitude: (point1.longitude + point2.longitude + point1.latitude - point2.latitude) / 2,
  }
  const missingVertice2 = {
    latitude: (point1.latitude + point2.latitude + point1.longitude - point2.longitude) / 2,
    longitude: (point1.longitude + point2.longitude + point2.latitude - point1.latitude) / 2,
  }

  return { missingVertice1, missingVertice2 };
}

const getClosestPointOnLines = (pXy, polyline) => {
  var minDist;
  var fTo;
  var fFrom;
  var x;
  var y;
  var i;
  var dist;

  if (polyline.length > 1) {
    for (var n = 1 ; n < polyline.length ; n++) {
      if (polyline[n].latitude != polyline[n - 1].latitude) {
          var a = (polyline[n].longitude - polyline[n - 1].longitude) / (polyline[n].latitude - polyline[n - 1].latitude);
          var b = polyline[n].longitude - a * polyline[n].latitude;
          dist = Math.abs(a * pXy.latitude + b - pXy.longitude) / Math.sqrt(a * a + 1);
      } else {
        dist = Math.abs(pXy.latitude - polyline[n].latitude)
      }

      // length^2 of line segment
      var rl2 = Math.pow(polyline[n].longitude - polyline[n - 1].longitude, 2) + Math.pow(polyline[n].latitude - polyline[n - 1].latitude, 2);

      // distance^2 of pt to end line segment
      var ln2 = Math.pow(polyline[n].longitude - pXy.longitude, 2) + Math.pow(polyline[n].latitude - pXy.latitude, 2);

      // distance^2 of pt to begin line segment
      var lnm12 = Math.pow(polyline[n - 1].longitude - pXy.longitude, 2) + Math.pow(polyline[n - 1].latitude - pXy.latitude, 2);

      // minimum distance^2 of pt to infinite line
      var dist2 = Math.pow(dist, 2);

      // calculated length^2 of line segment
      var calcrl2 = ln2 - dist2 + lnm12 - dist2;

      // redefine minimum distance to line segment (not infinite line) if necessary
      if (calcrl2 > rl2)
          dist = Math.sqrt(Math.min(ln2, lnm12));

      if ((minDist == null) || (minDist > dist)) {
          if (calcrl2 > rl2) {
              if (lnm12 < ln2) {
                  fTo = 0;//nearer to previous point
                  fFrom = 1;
              }
              else {
                  fFrom = 0;//nearer to current point
                  fTo = 1;
              }
          }
          else {
              // perpendicular from point intersects line segment
              fTo = ((Math.sqrt(lnm12 - dist2)) / Math.sqrt(rl2));
              fFrom = ((Math.sqrt(ln2 - dist2)) / Math.sqrt(rl2));
          }
          minDist = dist;
          i = n;
      }
    }

    var dx = polyline[i - 1].latitude - polyline[i].latitude;
    var dy = polyline[i - 1].longitude - polyline[i].longitude;

    x = polyline[i - 1].latitude - (dx * fTo);
    y = polyline[i - 1].longitude - (dy * fTo);

  }

  return { 'latitude': x, 'longitude': y, 'i': i, 'fTo': fTo, 'fFrom': fFrom };
}

export {
  convertRouteToPolyline,
  distanceBetweenPoints,
  getPositionWithOffset,
  getRotationAngle,
  getMissingVertices,
  getClosestPointOnLines,
};

@mustapha-ghlissi
Copy link

mustapha-ghlissi commented Jan 14, 2021

@sandinosaso according to your previous code exampel

I nead to get rotation angle % to a polyline route

How can I do that according to these Geo functions ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet