diff --git a/README.md b/README.md
index 8421dfb..97311d1 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
-# react-native-geocoder
+# react-native-geocoder
[](https://circleci.com/gh/devfd/react-native-geocoder/tree/master)
@@ -114,13 +114,17 @@ both iOS and Android will return the following object:
```js
{
position: {lat, lng},
+ region: {
+ center: {lat, lng},
+ radius: Number,
+ } | null,
formattedAddress: String, // the full address
feature: String | null, // ex Yosemite Park, Eiffel Tower
streetNumber: String | null,
streetName: String | null,
postalCode: String | null,
locality: String | null, // city name
- country: String,
+ country: String,
countryCode: String
adminArea: String | null
subAdminArea: String | null,
@@ -135,5 +139,4 @@ iOS does not allow sending multiple geocoding requests simultaneously, unless yo
### Android
geocoding may not work on older android devices (4.1) and will not work if Google play services are not available.
-
-
+`region` will always be `null` on Android since it doesn't support the feature. In this case, `Geocoder.geocodePositionFallback` and `Geocoder.geocodeAddressFallback` may be used to get the `region` value.
diff --git a/android/src/main/java/com/devfd/RNGeocoder/RNGeocoderModule.java b/android/src/main/java/com/devfd/RNGeocoder/RNGeocoderModule.java
index 642ebfb..37741b7 100644
--- a/android/src/main/java/com/devfd/RNGeocoder/RNGeocoderModule.java
+++ b/android/src/main/java/com/devfd/RNGeocoder/RNGeocoderModule.java
@@ -1,8 +1,10 @@
package com.devfd.RNGeocoder;
+import android.content.Context;
import android.location.Address;
import android.location.Geocoder;
+import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
@@ -15,14 +17,20 @@
import java.io.IOException;
import java.util.List;
+import java.util.Locale;
public class RNGeocoderModule extends ReactContextBaseJavaModule {
+ private Context context;
+ private Locale locale;
private Geocoder geocoder;
public RNGeocoderModule(ReactApplicationContext reactContext) {
super(reactContext);
- geocoder = new Geocoder(reactContext.getApplicationContext());
+ context = reactContext.getApplicationContext();
+
+ locale = context.getResources().getConfiguration().locale;
+ geocoder = new Geocoder(context, locale);
}
@Override
@@ -30,6 +38,20 @@ public String getName() {
return "RNGeocoder";
}
+ @ReactMethod
+ public void setLanguage(String language, Callback callback) {
+ Locale target = new Locale(language);
+ if (!context.getResources().getConfiguration().locale.equals(target) && !locale.equals(target)) {
+ locale = target;
+ geocoder = new Geocoder(context, locale);
+
+ callback.invoke(language);
+ return;
+ }
+
+ callback.invoke((String) null);
+ }
+
@ReactMethod
public void geocodeAddress(String addressName, Promise promise) {
if (!geocoder.isPresent()) {
@@ -39,7 +61,12 @@ public void geocodeAddress(String addressName, Promise promise) {
try {
List
addresses = geocoder.getFromLocationName(addressName, 20);
- promise.resolve(transform(addresses));
+ if(addresses != null && addresses.size() > 0) {
+ promise.resolve(transform(addresses));
+ } else {
+ promise.reject("NOT_AVAILABLE", "Geocoder returned an empty list");
+ }
+
}
catch (IOException e) {
@@ -74,6 +101,9 @@ WritableArray transform(List addresses) {
position.putDouble("lng", address.getLongitude());
result.putMap("position", position);
+ // There is no region field in the `Address` object.
+ result.putString("region", null);
+
final String feature_name = address.getFeatureName();
if (feature_name != null && !feature_name.equals(address.getSubThoroughfare()) &&
!feature_name.equals(address.getThoroughfare()) &&
diff --git a/ios/RNGeocoder/RNGeocoder.h b/ios/RNGeocoder/RNGeocoder.h
index 8abb862..98ce8fa 100644
--- a/ios/RNGeocoder/RNGeocoder.h
+++ b/ios/RNGeocoder/RNGeocoder.h
@@ -1,5 +1,5 @@
-#import "RCTBridgeModule.h"
-#import "RCTConvert.h"
+#import
+#import
#import
diff --git a/ios/RNGeocoder/RNGeocoder.m b/ios/RNGeocoder/RNGeocoder.m
index 5ad122b..796ac4a 100644
--- a/ios/RNGeocoder/RNGeocoder.m
+++ b/ios/RNGeocoder/RNGeocoder.m
@@ -2,7 +2,7 @@
#import
-#import "RCTConvert.h"
+#import
@implementation RCTConvert (CoreLocation)
@@ -22,6 +22,17 @@ @implementation RNGeocoder
RCT_EXPORT_MODULE();
+RCT_EXPORT_METHOD(setLanguage:(NSString *)language
+ callback:(RCTResponseSenderBlock)callback)
+{
+ NSString *deviceLanguage = [[NSLocale preferredLanguages] objectAtIndex:0];
+ if ([deviceLanguage isEqualToString:language]) {
+ return callback(@[[NSNull null]]);
+ }
+
+ callback(@[language]);
+}
+
RCT_EXPORT_METHOD(geocodePosition:(CLLocation *)location
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
@@ -81,6 +92,7 @@ - (NSArray *)placemarksToDictionary:(NSArray *)placemarks {
for (int i = 0; i < placemarks.count; i++) {
CLPlacemark* placemark = [placemarks objectAtIndex:i];
+ CLCircularRegion* region = placemark.region;
NSString* name = [NSNull null];
@@ -100,6 +112,13 @@ - (NSArray *)placemarksToDictionary:(NSArray *)placemarks {
@"lat": [NSNumber numberWithDouble:placemark.location.coordinate.latitude],
@"lng": [NSNumber numberWithDouble:placemark.location.coordinate.longitude],
},
+ @"region": placemark.region ? @{
+ @"center": @{
+ @"lat": [NSNumber numberWithDouble:region.center.latitude],
+ @"lng": [NSNumber numberWithDouble:region.center.longitude],
+ },
+ @"radius": [NSNumber numberWithDouble:region.radius],
+ } : [NSNull null],
@"country": placemark.country ?: [NSNull null],
@"countryCode": placemark.ISOcountryCode ?: [NSNull null],
@"locality": placemark.locality ?: [NSNull null],
@@ -109,7 +128,7 @@ - (NSArray *)placemarksToDictionary:(NSArray *)placemarks {
@"postalCode": placemark.postalCode ?: [NSNull null],
@"adminArea": placemark.administrativeArea ?: [NSNull null],
@"subAdminArea": placemark.subAdministrativeArea ?: [NSNull null],
- @"formattedAddress": [lines componentsJoinedByString:@", "]
+ @"formattedAddress": [lines componentsJoinedByString:@", "],
};
[results addObject:result];
diff --git a/js/geocoder.js b/js/geocoder.js
index da2c3b7..388426c 100644
--- a/js/geocoder.js
+++ b/js/geocoder.js
@@ -1,23 +1,46 @@
-import { NativeModules } from 'react-native';
+import { NativeModules, Platform } from 'react-native';
import GoogleApi from './googleApi.js';
const { RNGeocoder } = NativeModules;
export default {
apiKey: null,
+ language: null,
fallbackToGoogle(key) {
this.apiKey = key;
},
+ setLanguage(language) {
+ RNGeocoder.setLanguage(language, (result) => {
+ this.language = result;
+ });
+ },
+
+ geocodePositionFallback(position) {
+ if (!this.apiKey) { throw new Error("Google API key required"); }
+
+ return GoogleApi.geocodePosition(this.apiKey, position, this.language);
+ },
+
+ geocodeAddressFallback(address) {
+ if (!this.apiKey) { throw new Error("Google API key required"); }
+
+ return GoogleApi.geocodeAddress(this.apiKey, address, this.language);
+ },
+
geocodePosition(position) {
if (!position || !position.lat || !position.lng) {
return Promise.reject(new Error("invalid position: {lat, lng} required"));
}
+ if (this.language && (Platform.OS === 'ios')) {
+ return this.geocodePositionFallback(position);
+ }
+
return RNGeocoder.geocodePosition(position).catch(err => {
- if (!this.apiKey || err.code !== 'NOT_AVAILABLE') { throw err; }
- return GoogleApi.geocodePosition(this.apiKey, position);
+ if (err.code !== 'NOT_AVAILABLE') { throw err; }
+ return this.geocodePositionFallback(position);
});
},
@@ -26,9 +49,13 @@ export default {
return Promise.reject(new Error("address is null"));
}
+ if (this.language && (Platform.OS === 'ios')) {
+ return this.geocodeAddressFallback(address);
+ }
+
return RNGeocoder.geocodeAddress(address).catch(err => {
- if (!this.apiKey || err.code !== 'NOT_AVAILABLE') { throw err; }
- return GoogleApi.geocodeAddress(this.apiKey, address);
+ if (err.code !== 'NOT_AVAILABLE') { throw err; }
+ return this.geocodeAddressFallback(address);
});
},
}
diff --git a/js/googleApi.js b/js/googleApi.js
index 4358de7..80b1563 100644
--- a/js/googleApi.js
+++ b/js/googleApi.js
@@ -1,77 +1,111 @@
-const googleUrl = 'https://maps.google.com/maps/api/geocode/json';
-
-function format(raw) {
- const address = {
- position: {},
- formattedAddress: raw.formatted_address || '',
- feature: null,
- streetNumber: null,
- streetName: null,
- postalCode: null,
- locality: null,
- country: null,
- countryCode: null,
- adminArea: null,
- subAdminArea: null,
- subLocality: null,
- };
-
- if (raw.geometry && raw.geometry.location) {
- address.position = {
- lat: raw.geometry.location.lat,
- lng: raw.geometry.location.lng,
- }
- }
+import { getCenterOfBounds, getDistance } from 'geolib';
- raw.address_components.forEach(component => {
- if (component.types.indexOf('route') !== -1) {
- address.streetName = component.long_name;
- }
- else if (component.types.indexOf('street_number') !== -1) {
- address.streetNumber = component.long_name;
- }
- else if (component.types.indexOf('country') !== -1) {
- address.country = component.long_name;
- address.countryCode = component.short_name;
- }
- else if (component.types.indexOf('locality') !== -1) {
- address.locality = component.long_name;
- }
- else if (component.types.indexOf('postal_code') !== -1) {
- address.postalCode = component.long_name;
- }
- else if (component.types.indexOf('administrative_area_level_1') !== -1) {
- address.adminArea = component.long_name;
- }
- else if (component.types.indexOf('administrative_area_level_2') !== -1) {
- address.subAdminArea = component.long_name;
- }
- else if (component.types.indexOf('sublocality') !== -1 || component.types.indexOf('sublocality_level_1') !== -1) {
- address.subLocality = component.long_name;
- }
- else if (component.types.indexOf('point_of_interest') !== -1 || component.types.indexOf('colloquial_area') !== -1) {
- address.feature = component.long_name;
+export default {
+ googleUrl: 'https://maps.googleapis.com/maps/api/geocode/json',
+
+ format(raw) {
+ const address = {
+ position: {},
+ region: null,
+ formattedAddress: raw.formatted_address || '',
+ feature: null,
+ streetNumber: null,
+ streetName: null,
+ postalCode: null,
+ locality: null,
+ country: null,
+ countryCode: null,
+ adminArea: null,
+ subAdminArea: null,
+ subLocality: null,
+ };
+
+ if (raw.geometry && raw.geometry.location) {
+ address.position = {
+ lat: raw.geometry.location.lat,
+ lng: raw.geometry.location.lng,
+ }
+
+ if (raw.geometry.viewport) {
+ const northEast = {
+ latitude: raw.geometry.viewport.northeast.lat,
+ longitude: raw.geometry.viewport.northeast.lng,
+ };
+ const southWest = {
+ latitude: raw.geometry.viewport.southwest.lat,
+ longitude: raw.geometry.viewport.southwest.lng,
+ };
+ const center = getCenterOfBounds([northEast, southWest]);
+ const radius = Math.max(getDistance(center, northEast), getDistance(center, southWest));
+
+ address.region = {
+ center: {
+ lat: center.latitude,
+ lng: center.longitude,
+ },
+ radius,
+ }
+ }
}
- });
- return address;
-}
+ raw.address_components.forEach(component => {
+ if (component.types.indexOf('route') !== -1) {
+ address.streetName = component.long_name;
+ }
+ else if (component.types.indexOf('street_number') !== -1) {
+ address.streetNumber = component.long_name;
+ }
+ else if (component.types.indexOf('country') !== -1) {
+ address.country = component.long_name;
+ address.countryCode = component.short_name;
+ }
+ else if (component.types.indexOf('locality') !== -1) {
+ address.locality = component.long_name;
+ }
+ else if (component.types.indexOf('postal_code') !== -1) {
+ address.postalCode = component.long_name;
+ }
+ else if (component.types.indexOf('administrative_area_level_1') !== -1) {
+ address.adminArea = component.long_name;
+ }
+ else if (component.types.indexOf('administrative_area_level_2') !== -1) {
+ address.subAdminArea = component.long_name;
+ }
+ else if (component.types.indexOf('sublocality') !== -1 || component.types.indexOf('sublocality_level_1') !== -1) {
+ address.subLocality = component.long_name;
+ }
+ else if (component.types.indexOf('point_of_interest') !== -1 || component.types.indexOf('colloquial_area') !== -1) {
+ address.feature = component.long_name;
+ }
+ });
-export default {
- geocodePosition(apiKey, position) {
+ return address;
+ },
+
+ geocodePosition(apiKey, position, language = null) {
if (!apiKey || !position || !position.lat || !position.lng) {
return Promise.reject(new Error("invalid apiKey / position"));
}
- return this.geocodeRequest(`${googleUrl}?key=${apiKey}&latlng=${position.lat},${position.lng}`);
+ let url = `${this.googleUrl}?key=${apiKey}&latlng=${position.lat},${position.lng}`;
+ if (language) {
+ url = `${url}&language=${language}`;
+ }
+
+ return this.geocodeRequest(url);
},
- geocodeAddress(apiKey, address) {
+ geocodeAddress(apiKey, address, language = null) {
if (!apiKey || !address) {
return Promise.reject(new Error("invalid apiKey / address"));
}
- return this.geocodeRequest(`${googleUrl}?key=${apiKey}&address=${encodeURI(address)}`);
+ let url = `${this.googleUrl}?key=${apiKey}&address=${encodeURI(address)}`;
+ if (language) {
+ url = `${url}&language=${language}`;
+ }
+
+ return this.geocodeRequest(url);
},
async geocodeRequest(url) {
@@ -82,6 +116,6 @@ export default {
return Promise.reject(new Error(`geocoding error ${json.status}, ${json.error_message}`));
}
- return json.results.map(format);
+ return json.results.map(this.format);
}
}
diff --git a/package.json b/package.json
index 913f14a..e7d9a14 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "react-native-geocoder",
- "version": "0.4.5",
+ "version": "0.5.0",
"description": "react native geocoding and reverse geocoding",
"main": "index.js",
"scripts": {
@@ -39,5 +39,12 @@
"sinon": "^1.17.5",
"sinon-chai": "^2.8.0",
"wd": "^0.4.0"
+ },
+ "dependencies": {
+ "geolib": "^2.0.21"
+ },
+ "peerDependencies": {
+ "react": ">=15.4.0",
+ "react-native": ">=0.40"
}
}
diff --git a/test/unit/geocoder.test.js b/test/unit/geocoder.test.js
index 7759e53..497b2fb 100644
--- a/test/unit/geocoder.test.js
+++ b/test/unit/geocoder.test.js
@@ -11,6 +11,7 @@ describe('geocoder', function() {
};
RNGeocoder = {
+ setLanguage: sinon.stub().callsArgWith(1, null),
geocodePosition: sinon.stub().returns(Promise.resolve()),
geocodeAddress: sinon.stub().returns(Promise.resolve()),
};
@@ -21,6 +22,7 @@ describe('geocoder', function() {
'./googleApi.js': GoogleApi,
'react-native': {
NativeModules: { RNGeocoder },
+ Platform: { OS: 'android' },
}
}).default;
});
@@ -52,6 +54,16 @@ describe('geocoder', function() {
expect(RNGeocoder.geocodePosition).to.have.been.calledWith(position);
});
+ it ('returns geocoding results with a specific language', async function() {
+ const position = {lat: 1.234, lng: 4.567};
+ const language = 'ko';
+ RNGeocoder.setLanguage = sinon.stub().callsArgWith(1, language);
+ Geocoder.setLanguage(language);
+ expect(RNGeocoder.setLanguage).to.have.been.calledWith(language);
+ await Geocoder.geocodePosition(position);
+ expect(RNGeocoder.geocodePosition).to.have.been.calledWith(position);
+ });
+
it ('does not call google api if no apiKey', function() {
const position = {lat: 1.234, lng: 4.567};
RNGeocoder.geocodePosition = sinon.stub().returns(Promise.reject());
@@ -70,6 +82,27 @@ describe('geocoder', function() {
expect(ret).to.eql('google');
});
+ it ('fallbacks to google api when with a specific language on iOS', async function() {
+ Geocoder = proxyquire
+ .noCallThru()
+ .load('../../js/geocoder.js', {
+ './googleApi.js': GoogleApi,
+ 'react-native': {
+ NativeModules: { RNGeocoder },
+ Platform: { OS: 'ios' },
+ }
+ }).default;
+ const position = {lat: 1.234, lng: 4.567};
+ const language = 'ko';
+ RNGeocoder.setLanguage = sinon.stub().callsArgWith(1, language);
+ Geocoder.setLanguage(language);
+ expect(RNGeocoder.setLanguage).to.have.been.calledWith(language);
+ Geocoder.fallbackToGoogle('myGoogleMapsAPIKey');
+ const ret = await Geocoder.geocodePosition(position);
+ expect(GoogleApi.geocodePosition).to.have.been.calledWith('myGoogleMapsAPIKey', position);
+ expect(ret).to.eql('google');
+ });
+
it ('does not fallback to google api on error', function() {
const position = {lat: 1.234, lng: 4.567};
RNGeocoder.geocodePosition = sinon.stub().returns(Promise.reject(new Error('something wrong')));
diff --git a/test/unit/googleApi.test.js b/test/unit/googleApi.test.js
index f02e8b9..19991e0 100644
--- a/test/unit/googleApi.test.js
+++ b/test/unit/googleApi.test.js
@@ -18,14 +18,28 @@ describe('googleApi', function() {
it ('position', async function() {
let ret = await GoogleApi.geocodePosition('myKey', {lat: 1.234, lng: 1.14});
expect(geocodeRequest).to.have.been.calledWith(
- 'https://maps.google.com/maps/api/geocode/json?key=myKey&latlng=1.234,1.14');
+ 'https://maps.googleapis.com/maps/api/geocode/json?key=myKey&latlng=1.234,1.14');
+ expect(ret).to.eql('yo');
+ });
+
+ it ('position with a specific language', async function() {
+ let ret = await GoogleApi.geocodePosition('myKey', {lat: 1.234, lng: 1.14}, 'ko');
+ expect(geocodeRequest).to.have.been.calledWith(
+ 'https://maps.googleapis.com/maps/api/geocode/json?key=myKey&latlng=1.234,1.14&language=ko');
expect(ret).to.eql('yo');
});
it ('address', async function() {
- let ret = await GoogleApi.geocodeAddress('myKey', "london");
+ let ret = await GoogleApi.geocodeAddress('myKey', 'london');
+ expect(geocodeRequest).to.have.been.calledWith(
+ 'https://maps.googleapis.com/maps/api/geocode/json?key=myKey&address=london');
+ expect(ret).to.eql('yo');
+ });
+
+ it ('address with a specific language', async function() {
+ let ret = await GoogleApi.geocodeAddress('myKey', 'london', 'ko');
expect(geocodeRequest).to.have.been.calledWith(
- 'https://maps.google.com/maps/api/geocode/json?key=myKey&address=london');
+ 'https://maps.googleapis.com/maps/api/geocode/json?key=myKey&address=london&language=ko');
expect(ret).to.eql('yo');
});
});
@@ -39,10 +53,10 @@ describe('googleApi', function() {
}
it ('throws if invalid results', function() {
- mockFetch({ status: "NOT_OK" });
+ mockFetch({ status: 'NOT_OK' });
return GoogleApi.geocodeRequest().then(
() => { throw new Error('cannot be there') },
- (err) => { expect(err.message).to.contain("NOT_OK"); }
+ (err) => { expect(err.message).to.contain('NOT_OK'); }
);
});
@@ -51,6 +65,9 @@ describe('googleApi', function() {
it ('for waterloo-bridge', async function() {
mockFetch(require('./fixtures/waterloo-bridge.js'));
let [first, ...ret] = await GoogleApi.geocodeRequest();
+ expect(first.region.center.lat).to.eql(51.506349);
+ expect(first.region.center.lng).to.eql(-0.114699);
+ expect(first.region.radius).to.eql(177);
expect(first.countryCode).to.eql('GB');
expect(first.feature).to.be.eql(null);
expect(first.locality).to.eql('London');
@@ -61,6 +78,9 @@ describe('googleApi', function() {
it ('for yosemite park', async function() {
mockFetch(require('./fixtures/yosemite-park.js'));
let [first, ...ret] = await GoogleApi.geocodeRequest();
+ expect(first.region.center.lat).to.eql(37.865101);
+ expect(first.region.center.lng).to.eql(-119.538329);
+ expect(first.region.radius).to.eql(191);
expect(first.countryCode).to.eql('US');
expect(first.feature).to.be.eql('Yosemite National Park');
expect(first.streetName).to.be.eql(null);