Convert a Region latitudeDelta / longitudeDelta into an approximate zoomLevel - react-native

I use the great react-native-maps from Airbnb on a react-native app.
I got a list of markers on a JSON file where each of these markers have a property zoom which is a integer of an approximate zoom level where the marker should display / hide on the map.
Is there a way based on the latitudeDelta and longitudeDelta of a Region to get an approximate double/integer of the current zoom level as we have on Google Maps (1 to 20) ?
Thanks

Ok I come with an handy solution, I don't know if we can do better.
I added the onRegionChange event to retrieve the region, then I use some math :
<MapView
style={styles.map}
initialRegion={this.state.region}
onRegionChange={region => {
clearTimeout(this.timerForMap)
this.timerForMap = setTimeout(() => {
this.showMarkers(region)
}, 100)
}}>
...
Then :
showMarkers(region) {
let zoom = Math.round(Math.log(360 / region.longitudeDelta) / Math.LN2)
...
}
If someone have a better way to do it, feel free to comment !
Thanks.

you can get the zoom level from getCamera() using onRegionChange in MapView
const [zoom, setZoom] = useState(''); //initiates variable zoom
const getZoom = async () => {
const coords = await mapRef.getCamera();
setZoom(coords.center.zoom); // sets variable zoom the value under coords.center.zoom
}
<MapView>
ref = {(ref) => mapRef = ref}
onRegionChange = {() => {getZoom();}}
</MapView>

Related

Marker not moving on map even after rerendering

I am fetching coordinates from my database every 5 seconds, and my marker is dynamic, this is my render method
console.log('render'+this.state.driverLocation.latitude +' '+this.state.driverLocation.longitude);
let marker = null;
marker = <MapView.Marker title='This is you' coordinate={this.state.driverLocation } />;
return (
<View style={styles.container}>
<MapView
initialRegion={this.state.focusedLocation}
region={!this.state.locationChosen ? this.state.focusedLocation : null}
style={styles.map}
onPress={this.pickLocationHandler}
ref={ref => this.map = ref}
>
{marker}
</MapView>
The log statement in render shows driverLocation changes every 5 sec as it should but the marker stays at the initial position (coordinates given while defining state).
Is there any problem with my code? Do I need to add something? Any help would be appreciated.
The problem was that android relies only on the key to change in order to update the custom marker and it can be solved by assigning a random key to the marker every time there's a need for rerendering.
So, something like this:
marker = <MapView.Marker
title='This is you'
coordinate={this.state.driverLocation}
key={ this.GenerateRandomNumber() }
} />;
While:
GenerateRandomNumber=()=>
{
var RandomNumber = Math.floor(Math.random() * 100) + 1 ;
return RandomNumber;
}

React native maps get visible markers

On iOS when using map kit we are able to get the current annotations inside of the visible map rect. Here is a small snippet using swift
Array(myMap.annotations(in: myMap.visibleMapRect))
Is there any comparable method or callback in this library?
My basic use is to know when a user changed region (pinch to zoom etc) and to get an array of the current markers still visible.
There is a getMarkersFrames function to do that, but it is iOS only, and that's why I never used one in my apps.
I solved the same problem by using the RBush library to store markers: https://github.com/mourner/rbush. It allows to easily select points in the selected region. And it does it fast by using an R-tree. Example:
getInitialState() {
return {
region: {
latitude: 37.78825,
longitude: -122.4324,
latitudeDelta: 0.0922,
longitudeDelta: 0.0421,
},
};
}
componentWillMount() {
this.tree = new RBush();
fetch().then((points) => {
// RBush expects the point to have minX, maxX, minY, maxY
// but it could be configured.
this.tree.load(points);
});
}
onRegionChangeComplete(region) {
// Get points that are inside the region.
visibleItems = this.tree.search({
// Provide the coordinates of the south-west, north-east corners of the region.
minX: region.longitude - region.longitudeDelta,
minY: region.latitude - region.latitudeDelta,
maxX: region.longitude + region.longitudeDelta,
maxY: region.latitude + region.latitudeDelta
});
this.setState({ region, visibleItems });
}
render() {
return (
<MapView
region={this.state.region}
onRegionChangeComplete={this.onRegionChangeComplete}
/>
);
}
My app uses the same approach except I do initial markers loading and search in redux and redux-saga.
If you doesn't need to show that many markers, you can use simpler approach by storing points in the array. Then on each region change loop through all of the places and select those that are inside the region.

React Native FlatList with inverted option enabled

I have a FlatList as shown below:
<FlatList
inverted
data={messages}
keyExtractor={this._keyExtractor}
renderItem={({ item }) => (
<Text style={styles.item}>{item}</Text>
)}
onEndReached={this.handleLoadMore}
onEndReachedThreshold={30}
/>
But here the OnEndReached method does not gets called when I reach the top of the flatlist.
Please help
OnEndReachedThreshold must be a number between 0 and 1. Since you are inverting your flatlist, onEndReachedThreshold would be the distance the user is from the top of the list [in percents]. Therefore a value of 0.5 would trigger the OnEndReached function when the user has scrolled through 50% of the viewable list.
To trigger the function at 50% your code should read something like this:
<FlatList
inverted
data={messages}
keyExtractor={this._keyExtractor}
renderItem={({ item }) => (
<Text style={styles.item}>{item}</Text>
)}
onEndReached={this.handleLoadMore}
onEndReachedThreshold={0.5}
/>
All I could figure out was using of onScroll (performance beware) in here: https://snack.expo.io/#zvona/inverted-list-onbeginreached
The actual function looks like this:
checkIfBeginningReached = ({ nativeEvent }) => {
const { layoutMeasurement, contentOffset } = nativeEvent;
const currentPos = layoutMeasurement.height + contentOffset.y;
const listLength = ITEM_HEIGHT * this.state.items.length;
const reactThreshold = listLength - (ITEM_HEIGHT * THRESHOLD);
if (currentPos >= reactThreshold) {
this.fetchMoreItems(this.state.items.length);
}
}
On that, we pick up necessary info from nativeEvent (which kind of holds everything relevant). Then we just calculate the current position in pixels, length of whole list content in pixels and then threshold point.
In all, this particular solution requires two things:
1) list has fixed and same size of elements
2) list is not multi-column.
All the other functionality in the demo is just faking / mimicking one use case (of fetching 50 more items from server with 500ms delay). But I'll improve my answer if possible. But this should get you started.
My solution is here:
isCloseToBottom = ({layoutMeasurement, contentOffset, contentSize}) => {
const paddingToBottom = 1
return layoutMeasurement.height + contentOffset.y >=
contentSize.height - paddingToBottom}
just set onEndReachedThreshold={0.1} or onEndReachedThreshold={0.2}

Flatlist visible position

I'm trying to render an curved vertical list like this iOS component: https://github.com/makotokw/CocoaWZYCircularTableView
That component (written in Obj-c) iterates the visible cells when laying them out, and sets the frame (i.e. indent) using asin.
I know in React Native I can set the leftMargin style in the renderItem callback, but I can't figure out how to get the on-screen index of the item - all I have is the index into the source data. And also, at that point, I don't think I have access to the absolute position.
Any ideas?
The function you are looking for is
onViewableItemsChanged.
You can use it with viewabilityConfig which provides us with
minimumViewTime,viewAreaCoveragePercentThreshold,waitForInteraction
which can be set accordingly
const VIEWABILITY_CONFIG = {
minimumViewTime: 3000,
viewAreaCoveragePercentThreshold: 100,
waitForInteraction: true,
};
_onViewableItemsChanged = (info: {
changed: Array<{
key: string,
isViewable: boolean,
item: any,
index: ?number,
section?: any,
}>
}
){
//here you can have the index which is visible to you
}
<FlatList
renderItem={this.renderItem}
data={this.state.data}
onViewableItemsChanged={this._onViewableItemsChanged}
viewabilityConfig={VIEWABILITY_CONFIG}
/>
Thanks for both answers.
What I have ended up doing is deriving the visible items using the scroll offset of the list. This is simple because the list items all have the same height.
I do this in the onScroll handler, and at that point I calculate the horizontal offset for each item (and I use leftMargin / rightMargin to render this). It's not perfect, but it does give me an elliptical list.
_handleScroll = (event) => {
const topItemIndex = Math.floor(event.nativeEvent.contentOffset.y / LIST_ITEM_HEIGHT);
const topItemSpare = LIST_ITEM_HEIGHT-(event.nativeEvent.contentOffset.y % LIST_ITEM_HEIGHT);
const positionFromEllipseTop = (forIndex-topItemIndex)*LIST_ITEM_HEIGHT+topItemSpare;
const positionFromOrigin = Math.floor(Math.abs(yRadius - positionFromEllipseTop));
const angle = Math.asin(positionFromOrigin / yRadius);
if (orientation === 'Left') {
marginLeft = 0;
marginRight = ((xRadius * Math.cos(angle)))-LIST_ITEM_HEIGHT;
alignSelf = 'flex-end';
}
else if (orientation === 'Right') {
marginLeft = (xRadius * Math.cos(angle))-LIST_ITEM_HEIGHT;
marginRight = 0;
alignSelf = 'flex-start';
}
}
React-native's FlatList component has a prop called onLayout. You can get the position of the component on screen with this prop.
onLayout
Invoked on mount and layout changes with:
{nativeEvent: { layout: {x, y, width, height}}}
This event is fired immediately once the layout has been calculated,
but the new layout may not yet be reflected on the screen at the time
the event is received, especially if a layout animation is in
progress.

React Native Retrieve Actual Image Sizes

I would like to be able to know the actual size of a network-loaded image that has been passed into <Image /> I have tried using onLayout to work out the size (as taken from here https://github.com/facebook/react-native/issues/858) but that seems to return the sanitised size after it's already been pushed through the layout engine.
I tried looking into onLoadStart, onLoad, onLoadEnd, onProgress to see if there was any other information available but cannot seem to get any of these to fire. I have declared them as follows:
onImageLoadStart: function(e){
console.log("onImageLoadStart");
},
onImageLoad: function(e){
console.log("onImageLoad");
},
onImageLoadEnd: function(e){
console.log("onImageLoadEnd");
},
onImageProgress: function(e){
console.log("onImageProgress");
},
onImageError: function(e){
console.log("onImageError");
},
render: function (e) {
return (
<Image
source={{uri: "http://adomain.com/myimageurl.jpg"}}
style={[this.props.style, this.state.style]}
onLayout={this.onImageLayout}
onLoadStart={(e) => {this.onImageLoadStart(e)}}
onLoad={(e) => {this.onImageLoad(e)}}
onLoadEnd={(e) => {this.onImageLoadEnd(e)}}
onProgress={(e) => {this.onImageProgress(e)}}
onError={(e) => {this.onImageError(e)}} />
);
}
Thanks.
Image component now provides a static method to get the size of the image. For example:
Image.getSize(myUri, (width, height) => {this.setState({width, height})});
You can use resolveAssetSource method from the Image component :
import picture from 'pathToYourPicture';
const {width, height} = Image.resolveAssetSource(picture);
This answer is now out of date. See Bill's answer.
Image.getSize(myUri, (width, height) => { this.setState({ width, height }) });
Old Answer (valid for older builds of react native)
Ok, I got it working. Currently this takes some modification of the React-Native installation as it's not natively supported.
I followed the tips in this thread to enabled me to do this.
https://github.com/facebook/react-native/issues/494
Mainly, alter the RCTNetworkImageView.m file: add the following into setImageURL
void (^loadImageEndHandler)(UIImage *image) = ^(UIImage *image) {
NSDictionary *event = #{
#"target": self.reactTag,
#"size": #{
#"height": #(image.size.height),
#"width": #(image.size.width)
}
};
[_eventDispatcher sendInputEventWithName:#"loaded" body:event];
};
Then edit the line that handles the load completion:
[self.layer removeAnimationForKey:#"contents"];
self.layer.contentsScale = image.scale;
self.layer.contents = (__bridge id)image.CGImage;
loadEndHandler();
replace
loadEndHandler();
with
loadImageEndHandler(image);
Then in React-Native you have access to the size via the native events. data from the onLoaded function - note the documentation currently says the function is onLoad but this is incorrect. The correct functions are as follows for v0.8.0:
onLoadStart
onLoadProgress
onLoaded
onLoadError
onLoadAbort
These can be accessed like so:
onImageLoaded: function(data){
try{
console.log("image width:"+data.nativeEvents.size.width);
console.log("image height:"+data.nativeEvents.size.height);
}catch(e){
//error
}
},
...
render: function(){
return (
<View style={{width:1,height:1,overflow='hidden'}}>
<Image source={{uri: yourImageURL}} resizeMode='contain' onLoaded={this.onImageLoaded} style={{width:5000,height:5000}} />
</View>
);
}
Points to note:
I have set a large image window and set it inside a wrapping element of 1x1px this is because the image must fit inside if you are to retrieve meaningful values.
The resize mode must be 'contain' to enable you to get the correct sizes, otherwise the constrained size will be reported.
The image sizes are scaled proportionately to the scale factor of the device, e.g. a 200*200 image on an iPhone6 (not 6 plus) will be reported as 100*100. I assume that this also means it will be reported as 67*67 on an iPhone6 plus but I have not tested this.
I have not yet got this to work for GIF files which traverse a different path on the Obj-C side of the bridge. I will update this answer once I have done that.
I believe there is a PR going through for this at the moment but until it is included in the core then this change will have to be made to the react-native installation every time you update/re-install.
TypeScript example:
import {Image} from 'react-native';
export interface ISize {
width: number;
height: number;
}
function getImageSize(uri: string): Promise<ISize> {
const success = (resolve: (value?: ISize | PromiseLike<ISize>) => void) => (width: number, height: number) => {
resolve({
width,
height
});
};
const error = (reject: (reason?: any) => void) => (failure: Error) => {
reject(failure);
};
return new Promise<ISize>((resolve, reject) => {
Image.getSize(uri, success(resolve), error(reject));
});
}