NativeScript / ios-jsc

NativeScript for iOS using JavaScriptCore
http://docs.nativescript.org/runtimes/ios
Apache License 2.0
298 stars 59 forks source link

Unable to properly marshall data in iOS for type coordinates: interop.Pointer | interop.Reference<CLLocationCoordinate2D> #1263

Closed Tyler-V closed 4 years ago

Tyler-V commented 4 years ago

Environment Provide version numbers for the following components (information can be retrieved by running tns info in your project folder or by inspecting the package.json of the project):

Describe the bug When attempting to call methods in the mapbox sdk, I am unable to marshall the data into the correct format shown in the examples depicted in Mapbox's documentation for iOS.

In this documentation provided:

I am unable to create an object of type coordinates: interop.Pointer | interop.Reference<CLLocationCoordinate2D>

  setCameraToCoordinates(latLngs: LatLng[], padding?: number, duration?: number): Promise<void> {
    return new Promise((resolve, reject) => {
      const mapView: MGLMapView = this.view.mapView;

      let insets: UIEdgeInsets = {
        top: padding ? padding : 0,
        left: padding ? padding : 0,
        bottom: padding ? padding : 0,
        right: padding ? padding : 0,
      };

      const coordinates: CLLocationCoordinate2D[] = [];
      for (let latLng of latLngs) {
        const coordinate = CLLocationCoordinate2DMake(latLng.lng, latLng.lat);
        coordinates.push(coordinate);
      }

      const array: any = NSArray.arrayWithArray([coordinates]);

      mapView.setVisibleCoordinatesCountEdgePaddingDirectionDurationAnimationTimingFunctionCompletionHandler(
        array,
        coordinates.length,
        insets,
        0,
        duration / 1000,
        CAMediaTimingFunction.functionWithName(kCAMediaTimingFunctionEaseInEaseOut),
        () => {
          resolve();
        }
      );
    });
  }

This calls the method without error, but what is passed in is an array of CLLocationCoordinate2D that is marshalled incorrectly. I know this because instead of the provided latLngs array taking me to a central part of the US which works on Android, it takes me off the coast of Africa.

image image

To Reproduce Try to create a coordinates object of type interop.Pointer | interop.Reference, inspect the values created and coordinates are not correct.

Expected behavior I should be able to create an object of type interop.Pointer | interop.Reference where the value is properly mapped and doesn't result in an improper object.

Sample project I would be more than happy to invite you into my plugin's project which has a branch already setup and a demo-angular app which will let you bootstrap and debug this issue with minimal effort to see for yourself. Please let me know otherwise this should be easily reproducible given the steps listed above.

facetious commented 4 years ago

The function you are using has the signature:

CLLocationCoordinate2D CLLocationCoordinate2DMake(CLLocationDegrees latitude, CLLocationDegrees longitude);

You have passed the arguments:

CLLocationCoordinate2DMake(latLng.lng, latLng.lat);

Presumably, your .lng maps to a longitude and .lat maps to a latitude, which is the wrong order for the signature you're invoking.

Tyler-V commented 4 years ago

@facetious good catch but unfortunately that's residual code leftover on trying to make it set the visible region properly.

CLLocationCoordinate2DMake(latLng.lng, latLng.lat) = Just south of Nigeria over the ocean. CLLocationCoordinate2DMake(latLng.lat, latLng.lng) = Further south west from Africa.

NickIliev commented 4 years ago

@Tyler-V see this blog post which pretty much covers the case with CLLocationCoordinate2DMake

GMSServices.provideAPIKey(APIKEY);

console.log("Creating map view...");
camera = GMSCameraPosition.cameraWithLatitudeLongitudeZoom(-33.86, 151.20, 6);
mapView = GMSMapView.mapWithFrameCamera(CGRectZero, camera);

console.log("Setting a marker...");
marker = GMSMarker.alloc().init();
// Note that in-line functions such as CLLocationCoordinate2DMake are not exported.
marker.position = { latitude: -33.86, longitude: 151.20 }
marker.title ="Sydney";
marker.snippet ="Australia";
Tyler-V commented 4 years ago

Hey @NickIliev thanks for the resource.

I don't think there is anything in that blog post that applies to my situation, sorry if my issue was unclear. I don't have any issues with creating a CLLocationCoordinate, that appears to be working just fine in other areas of my plugin.

The real issue at hand here is the marshalling of data and how to create the object that it is asking for.

I have an array of CLLocationCoordinates.

The Mapbox SDK wants the following input parameter:

(Objective-C) coordinates: interop.Pointer | interop.Reference<CLLocationCoordinate2D>, count: number

(Swift) _ coordinates: UnsafePointer<CLLocationCoordinate2D>, count: UInt

As a workaround, I somehow cobbled together something that actually calls this API method:

      const coordinates: CLLocationCoordinate2D[] = [];
      for (let latLng of latLngs) {
        const coordinate = CLLocationCoordinate2DMake(latLng.lng, latLng.lat);
        coordinates.push(coordinate);
      }

      const array: any = NSArray.arrayWithArray([coordinates]);

      mapView.setVisibleCoordinatesCountEdgePaddingDirectionDurationAnimationTimingFunctionCompletionHandler(
        array,
        coordinates.length,
        insets,
        0,
        duration / 1000,
        CAMediaTimingFunction.functionWithName(kCAMediaTimingFunctionEaseInEaseOut),
        () => {
          resolve();
        }
      );

However, it sends my map to southwest Africa instead of where the latLngs are (US)... swapping the lat/lngs sends me further south from Africa over the ocean.

mbektchiev commented 4 years ago

I cannot quickly test this, but I think that you should not wrap the JS array in another JS array. If you replace const array: any = NSArray.arrayWithArray([coordinates]); with const array: any = NSArray.arrayWithArray(coordinates); in the above snippet, I think it should work.

It seems I responded too fast. The above is not correct for multiple reasons:

  1. It's not possible to directly add structs as members of an NSArray (see https://stackoverflow.com/a/4517970 if you're curious to know how it can be done)
  2. The argument that has to be provided is not an NSArray but a nonnull const CLLocationCoordinate2D * coordinates.

The right way to construct an interop.Reference to an array of CLLocationCoordinate2D (as well as any other C struct) is similar to the following:

function toCArrayOfLocations(a: CLLocationCoordinate2D[]): interop.Reference<CLLocationCoordinate2D> {
    const ref = new interop.Reference<CLLocationCoordinate2D>(CLLocationCoordinate2D, interop.alloc(interop.sizeof(CLLocationCoordinate2D) * coordinates.length));
    for (let i = 0; i < coordinates.length; i++) {
        ref[i] = coordinates[i];
    }

    return ref;
}

const coordinates: CLLocationCoordinate2D[] = [CLLocationCoordinate2DMake(1, 2), CLLocationCoordinate2DMake(60, 190)];

const array = toCArrayOfLocations(coordinates);
mbektchiev commented 4 years ago

Also, you can create a more generic helper function like this:

function toReferenceToCArray<T>(a: T[], type: interop.Type<T>): interop.Reference<T> {
    const ref = new interop.Reference<T>(type, interop.alloc(interop.sizeof(type) * a.length));
    for (let i = 0; i < a.length; i++) {
        ref[i] = a[i];
    }

    return ref;
}

const coordinates: CLLocationCoordinate2D[] = [CLLocationCoordinate2DMake(1, 2), CLLocationCoordinate2DMake(60, 190)];

const refCoords = toReferenceToCArray(coordinates, CLLocationCoordinate2D);
hazalozturk commented 4 years ago

@mbektchiev Thank you so much, that worked!

Tyler-V commented 4 years ago
export function toReferenceToCArray<T>(input: T[], type: interop.Type<T>): interop.Reference<T> {
  const ref = new interop.Reference<T>(type, interop.alloc(interop.sizeof(CLLocationCoordinate2D) * input.length));
  for (let i = 0; i < input.length; i++) {
    ref[i] = input[i];
  }

  return ref;
}

That indeed marshalls javascript arrays into interop.Reference<T> thank you very much for the example, I couldn't find anything in the documentation this is great thanks again @mbektchiev

mbektchiev commented 4 years ago

Oops, I noticed an error, if you intend to use this function with other types you should replace CLLocationCoordinate2D with type ... 😆