Nativescript: Reverse Geocoding

Created on 3 Nov 2016  路  7Comments  路  Source: NativeScript/NativeScript

Did you verify this is a real problem by searching [Stack Overflow]

Yes

Tell us about the problem

Accessing native API to perform Geolocation from coordinates it's possible in Android but on iOS the native method takes a completion function as input and execute the geocoding asynchronously making impossible to update angular 2 variables.

Which platform(s) does your issue occur on?

iOS

Please provide the following version numbers that your issue occurs with:

  • CLI: 2.3.0
  • Cross-platform modules: 2.3.0
  • Runtime(s): tns-android: 2.3.0
  • Plugin(s):

    "nativescript-cardview": "^1.2.0",
    "nativescript-dom": "^1.0.7",
    "nativescript-floatingactionbutton": "^2.2.5",
    "nativescript-geolocation": "0.0.13",
    "nativescript-google-maps-sdk": "^1.3.14",
    "nativescript-material-icons": "^1.0.1",
    "nativescript-ng2-fonticon": "^1.2.6",
    "nativescript-phone": "^1.2.2",
    "nativescript-snackbar": "^1.1.4",
    "nativescript-social-share": "^1.3.1",
    "nativescript-sqlite": "^1.1.2",
    "nativescript-telerik-ui": "^1.4.1",
    "nativescript-theme-core": "^0.1.3",
    "tns-platform-declarations": "^2.3.0"

Please tell us how to recreate the issue in as much detail as possible.

I've created a simple util to perform a coordinates geocoding:

export function geocode(args: { location: Location }): Promise<IPosition> {
    let position: IPosition;
    if(!args.location)
       return Promise.resolve(position);

    if (args.location.android) {
        var locale = java.util.Locale.getDefault();
        var geocoder = new android.location.Geocoder(application.android.currentContext, locale);
        var addresses = geocoder.getFromLocation(args.location.latitude, args.location.longitude, 1);
        if (addresses != null && addresses.size() > 0) {
            var address = addresses.get(0);
            position = <IPosition>{
                latitude: address.getLatitude(),
                longitude: address.getLongitude(),
                subThoroughfare: address.getThoroughfare(),
                thoroughfare: address.getSubThoroughfare(),
                locality: address.getLocality(),
                subLocality: address.getSubLocality(),
                adminArea: address.getAdminArea(),
                subAdminArea: address.getSubAdminArea(),
                postalCode: address.getPostalCode(),
                country: address.getCountryName(),
                countryCode: address.getCountryCode(),
                addressLines: []
            }

            for (var i = 0; i <= address.getMaxAddressLineIndex(); i++) {
                position.addressLines.push(address.getAddressLine(i));
            }
        }
    }
    if (args.location.ios) {
        let geocoder = new CLGeocoder();
        geocoder.reverseGeocodeLocationCompletionHandler(
            args.location.ios, 
            (placemarks, error) => {
                if (error) {
                    console.log(error);
                    return;
                } else if (placemarks && placemarks.count > 0) {
                    let pm = placemarks[0];
                    let addressDictionary = pm.addressDictionary;
                    let address = ABCreateStringWithAddressDictionary(addressDictionary, false); 
                    position = <IPosition>{
                        latitude: args.location.latitude,
                        longitude: args.location.longitude,
                        subThoroughfare: addressDictionary.objectForKey('SubThoroughfare'),
                        thoroughfare: addressDictionary.objectForKey('Thoroughfare'),
                        locality: addressDictionary.objectForKey('City'),
                        subLocality: addressDictionary.objectForKey('SubLocality'),
                        adminArea: addressDictionary.objectForKey('State'),
                        subAdminArea: addressDictionary.objectForKey('SubAdministrativeArea'),
                        postalCode: addressDictionary.objectForKey('ZIP'),
                        country: addressDictionary.objectForKey('Country'),
                        countryCode: addressDictionary.objectForKey('CountryCode'),
                        addressLines: []
                    };

                    let lines = addressDictionary.objectForKey('FormattedAddressLines');
                    for (var i = 0; i < lines.count; i++) {
                        position.addressLines.push(lines[i]);
                    }
                }
            });
    }

    return Promise.resolve(position);
}

As you can see the android method is synchronous let me return a Promise when geocoding is completed. The iOS method is asyncronous so I can't return a Promise in the same way.

How can I achieve that?
Thank you very much.

question

Most helpful comment

Hi @bm-software,
Excuse me for the delay in reply.
I reviewed your code again and found that there is a problem, with the way you are using the Promise. You should return new Promise and to return resolve, when you receive the correct result or to reject the Promise, when there is a problem. I am attaching sample code, where has been shown, how to use it for iOS.

Sample.

main-page.xml

<Page xmlns="http://schemas.nativescript.org/tns.xsd" navigatingTo="navigatingTo">
    <!--
    The StackLayout stacks UI components on the screen鈥攅ither vertically or horizontally.
    In this case, the StackLayout does vertical stacking; you can change the stacking to
    horizontal by applying a orientation="horizontal" attribute to the <StackLayout> element.
    You can learn more about NativeScript layouts at
    https://docs.nativescript.org/ui/layout-containers.

    These components make use of several CSS class names that are part of the NativeScript
    core theme, such as p-20, btn, h2, and text-center. You can view a full list of the
    class names available for styling your app at https://docs.nativescript.org/ui/theme.
    -->
    <StackLayout class="p-20">
        <Label text="Tap the button" class="h1 text-center"/>
        <Button text="TAP" tap="onTap" class="btn btn-primary btn-active"/>
        <Label text="{{ message }}" class="h2 text-center" textWrap="true"/>
    </StackLayout>
</Page>

main-page.ts

import { EventData } from 'data/observable'; import { Page } from 'ui/page'; import { HelloWorldModel } from './main-view-model'; import {isIOS, isAndroid} from "platform"; import {Promise, BaseError} from "ts-promise"; var application = require("application"); import {Location, getCurrentLocation, enableLocationRequest} from "nativescript-geolocation"

export function navigatingTo(args: EventData) {

    let page = <Page>args.object;

    enableLocationRequest();
    page.bindingContext = new HelloWorldModel(); }

export function onTap(){
    getCurrentLocation({ timeout: 155000 })
            .then(location => {
                // console.log('Location received: ' + location);
                    geocode({location:location})
                    .then(function(result){
                        console.log("---------------------Promice---------------------------------");
                        console.dump(result)
                    })
                    .catch(function(e){console.log(e)});

            }).catch(error => {
                console.log('Location error received: ' + error);
                alert('Location error received: ' + error);
            });

    var location = new Location();
    location.latitude=40.7127837;
    location.longitude=-74.00594130000002;

}


function geocode(args: { location: Location }): Promise<any>{
    let position: any;
    if(!args.location)
       return new Promise(function (reject) {return reject("error")});

    if (isAndroid) {
        return new Promise(function(resolve, reject){
            var locale = java.util.Locale.getDefault();
            var geocoder = new android.location.Geocoder(application.android.currentContext, locale);
            var addresses = geocoder.getFromLocation(args.location.latitude, args.location.longitude, 1);
            if (addresses != null && addresses.size() > 0) {
                var address = addresses.get(0);
                position = <any>{
                    latitude: address.getLatitude(),
                    longitude: address.getLongitude(),
                    subThoroughfare: address.getThoroughfare(),
                    thoroughfare: address.getSubThoroughfare(),
                    locality: address.getLocality(),
                    subLocality: address.getSubLocality(),
                    adminArea: address.getAdminArea(),
                    subAdminArea: address.getSubAdminArea(),
                    postalCode: address.getPostalCode(),
                    country: address.getCountryName(),
                    countryCode: address.getCountryCode(),
                    addressLines: []
                }

                for (var i = 0; i <= address.getMaxAddressLineIndex(); i++) {
                    position.addressLines.push(address.getAddressLine(i));
                }
                return resolve(position);
            }
        });
    }
    if (isIOS) {
        return new Promise(function(resolve, reject){
            let geocoder = new CLGeocoder();
            geocoder.reverseGeocodeLocationCompletionHandler(
                args.location.ios, 
                (placemarks, error) => {
                    if (error) {
                        console.log(error);
                        var newerror = new BaseError("error", "error");
                        return reject(newerror);
                    } else if (placemarks && placemarks.count > 0) {
                        let pm = placemarks[0];
                        let addressDictionary = pm.addressDictionary;
                        let address = ABCreateStringWithAddressDictionary(addressDictionary, false); 
                        position = <any>{
                            latitude: args.location.latitude,
                            longitude: args.location.longitude,
                            subThoroughfare: addressDictionary.objectForKey('SubThoroughfare'),
                            thoroughfare: addressDictionary.objectForKey('Thoroughfare'),
                            locality: addressDictionary.objectForKey('City'),
                            subLocality: addressDictionary.objectForKey('SubLocality'),
                            adminArea: addressDictionary.objectForKey('State'),
                            subAdminArea: addressDictionary.objectForKey('SubAdministrativeArea'),
                            postalCode: addressDictionary.objectForKey('ZIP'),
                            country: addressDictionary.objectForKey('Country'),
                            countryCode: addressDictionary.objectForKey('CountryCode'),
                            addressLines: []
                        };

                        let lines = addressDictionary.objectForKey('FormattedAddressLines');
                        for (var i = 0; i < lines.count; i++) {
                            position.addressLines.push(lines[i]);
                        }
                        return resolve(position);
                }
            });
        });

    }

}

Hope this helps

All 7 comments

Hi @bm-software,
Thank you for your interest in NativeScript,
Regarding to your scenario you could use nativescript-geolocation plugin, which provide such a functionality. You could also review the example here.

Hope this helps

@tsonevn Thanks for your assistances. I tried the plugin but it doesn't return any geocoded information like address, country etc. Looking here indeed I don't see any geocoding information.

Hi @bm-software,
Excuse me for the delay in reply.
I reviewed your code again and found that there is a problem, with the way you are using the Promise. You should return new Promise and to return resolve, when you receive the correct result or to reject the Promise, when there is a problem. I am attaching sample code, where has been shown, how to use it for iOS.

Sample.

main-page.xml

<Page xmlns="http://schemas.nativescript.org/tns.xsd" navigatingTo="navigatingTo">
    <!--
    The StackLayout stacks UI components on the screen鈥攅ither vertically or horizontally.
    In this case, the StackLayout does vertical stacking; you can change the stacking to
    horizontal by applying a orientation="horizontal" attribute to the <StackLayout> element.
    You can learn more about NativeScript layouts at
    https://docs.nativescript.org/ui/layout-containers.

    These components make use of several CSS class names that are part of the NativeScript
    core theme, such as p-20, btn, h2, and text-center. You can view a full list of the
    class names available for styling your app at https://docs.nativescript.org/ui/theme.
    -->
    <StackLayout class="p-20">
        <Label text="Tap the button" class="h1 text-center"/>
        <Button text="TAP" tap="onTap" class="btn btn-primary btn-active"/>
        <Label text="{{ message }}" class="h2 text-center" textWrap="true"/>
    </StackLayout>
</Page>

main-page.ts

import { EventData } from 'data/observable'; import { Page } from 'ui/page'; import { HelloWorldModel } from './main-view-model'; import {isIOS, isAndroid} from "platform"; import {Promise, BaseError} from "ts-promise"; var application = require("application"); import {Location, getCurrentLocation, enableLocationRequest} from "nativescript-geolocation"

export function navigatingTo(args: EventData) {

    let page = <Page>args.object;

    enableLocationRequest();
    page.bindingContext = new HelloWorldModel(); }

export function onTap(){
    getCurrentLocation({ timeout: 155000 })
            .then(location => {
                // console.log('Location received: ' + location);
                    geocode({location:location})
                    .then(function(result){
                        console.log("---------------------Promice---------------------------------");
                        console.dump(result)
                    })
                    .catch(function(e){console.log(e)});

            }).catch(error => {
                console.log('Location error received: ' + error);
                alert('Location error received: ' + error);
            });

    var location = new Location();
    location.latitude=40.7127837;
    location.longitude=-74.00594130000002;

}


function geocode(args: { location: Location }): Promise<any>{
    let position: any;
    if(!args.location)
       return new Promise(function (reject) {return reject("error")});

    if (isAndroid) {
        return new Promise(function(resolve, reject){
            var locale = java.util.Locale.getDefault();
            var geocoder = new android.location.Geocoder(application.android.currentContext, locale);
            var addresses = geocoder.getFromLocation(args.location.latitude, args.location.longitude, 1);
            if (addresses != null && addresses.size() > 0) {
                var address = addresses.get(0);
                position = <any>{
                    latitude: address.getLatitude(),
                    longitude: address.getLongitude(),
                    subThoroughfare: address.getThoroughfare(),
                    thoroughfare: address.getSubThoroughfare(),
                    locality: address.getLocality(),
                    subLocality: address.getSubLocality(),
                    adminArea: address.getAdminArea(),
                    subAdminArea: address.getSubAdminArea(),
                    postalCode: address.getPostalCode(),
                    country: address.getCountryName(),
                    countryCode: address.getCountryCode(),
                    addressLines: []
                }

                for (var i = 0; i <= address.getMaxAddressLineIndex(); i++) {
                    position.addressLines.push(address.getAddressLine(i));
                }
                return resolve(position);
            }
        });
    }
    if (isIOS) {
        return new Promise(function(resolve, reject){
            let geocoder = new CLGeocoder();
            geocoder.reverseGeocodeLocationCompletionHandler(
                args.location.ios, 
                (placemarks, error) => {
                    if (error) {
                        console.log(error);
                        var newerror = new BaseError("error", "error");
                        return reject(newerror);
                    } else if (placemarks && placemarks.count > 0) {
                        let pm = placemarks[0];
                        let addressDictionary = pm.addressDictionary;
                        let address = ABCreateStringWithAddressDictionary(addressDictionary, false); 
                        position = <any>{
                            latitude: args.location.latitude,
                            longitude: args.location.longitude,
                            subThoroughfare: addressDictionary.objectForKey('SubThoroughfare'),
                            thoroughfare: addressDictionary.objectForKey('Thoroughfare'),
                            locality: addressDictionary.objectForKey('City'),
                            subLocality: addressDictionary.objectForKey('SubLocality'),
                            adminArea: addressDictionary.objectForKey('State'),
                            subAdminArea: addressDictionary.objectForKey('SubAdministrativeArea'),
                            postalCode: addressDictionary.objectForKey('ZIP'),
                            country: addressDictionary.objectForKey('Country'),
                            countryCode: addressDictionary.objectForKey('CountryCode'),
                            addressLines: []
                        };

                        let lines = addressDictionary.objectForKey('FormattedAddressLines');
                        for (var i = 0; i < lines.count; i++) {
                            position.addressLines.push(lines[i]);
                        }
                        return resolve(position);
                }
            });
        });

    }

}

Hope this helps

Hi @tsonevn thank you very much now it works fine :) I've learned more on how to use Promises.

@tsonevn Thanks a lot for sharing this! I wonder if we can have a blog post about this because I think a lot of people might be having similar problem/question.

@tsonevn : I am getting an error when i run in my ios:
error TS2339: Property 'ios' does not exist on type 'Location'.
I am not able to define ios property from args.location.
Could you please help me

Hi @tsonevn thank you very much now it works fine :) I've learned more on how to use Promises.

@mounthorse-slns Can I get the source codes or repo link of this project as I am not able to resolve the issues e.g. new android.location.Geocoder. I want to implement in my Angular Nativescript app.

Was this page helpful?
0 / 5 - 0 ratings