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

Support edgePadding with camera, set initial frame to fullscreen #1057

Merged
merged 7 commits into from
Oct 16, 2020

Conversation

mfazekas
Copy link
Contributor

@mfazekas mfazekas commented Oct 5, 2020

Fixes: #1032, #841

Test code:

import React from 'react';
import {View, Button} from 'react-native';
import MapboxGL from '@react-native-mapbox-gl/maps';

const latitude = 40.723279;
const longitude = -73.970895;

const boundsEdge = 0.002;

/*
The error is:
Mapbox error [event]:General [code]:-1 [message]:Unable to calculate
appropriate zoom level for bounds. Vertical or horizontal padding is
greater than map's height or width.
Tested on:
- iOS Simulator, iPhone 11
- iOS Simulator, iPhone SE
- Android Emulator, Pixel 3 (API 30)
*/

// iOS: No error, but padding has no effect.
// Android: No error, and padding works.
const padding1 = {
  paddingTop: 0,
  paddingBottom: 0,
};

// iOS: No error, but padding has no effect.
// Android: No error, and padding works.
const padding2 = {
  paddingTop: 0,
  paddingBottom: 63,
};

// iOS: Error, and padding has no effect.
// Android: No error, and padding works.
const padding3 = {
  paddingTop: 0,
  paddingBottom: 64,
};

// iOS: Error, and padding has no effect.
// Android: No error, and padding works.
const padding4 = {
  paddingTop: 1,
  paddingBottom: 63,
};

// iOS: No error, but padding has no effect.
// Android: No error, and padding works.
const padding5 = {
  paddingTop: 31,
  paddingBottom: 32,
};

// iOS: Error, and padding has no effect.
// Android: No error, and padding works.
const padding6 = {
  paddingTop: 32,
  paddingBottom: 32,
};

const initialPadding = padding3;

class MapView extends React.Component {
  static propTypes = {
  };

  state = {
    padding: initialPadding,
  }

  constructor(props) {
    super(props);
  }

  renderContents = () => {
    const point = {
      "type": "Point",
      "coordinates": [longitude, latitude]
    };

    return (
      <MapboxGL.ShapeSource
        id={'source'}
        shape={point}
      >
        <MapboxGL.CircleLayer
          id={'circle-big'}
          style={{
            circleRadius: 20,
            circleColor: 'white',
            circleStrokeColor: 'black',
            circleStrokeWidth: 1,
            iconAllowOverlap: true,
            circlePitchScale: 'viewport',
          }}
        />
      </MapboxGL.ShapeSource>
    );
  };

  render() {
    let {padding} = this.state;
    return (
      <View style={{flex: 1}}>
        <View>
          <Button title="padding1" onPress={() => this.setState({padding: padding1})} />
          <Button title="padding2" onPress={() => this.setState({padding: padding2})} />
          <Button title="padding3" onPress={() => this.setState({padding: padding3})} />
          <Button title="padding4" onPress={() => this.setState({padding: padding4})} />
        </View>
        <MapboxGL.MapView
          ref={(c) => (this._map = c)}
          onPress={this.onPress}
          style={{flex: 1}}
        >
          <MapboxGL.Camera
            zoomLevel={9}
            bounds={{
              ne: [longitude - boundsEdge, latitude + boundsEdge],
              sw: [longitude + boundsEdge, latitude - boundsEdge],
              ...padding,
            }}
          />

          {this.renderContents()}
        </MapboxGL.MapView>
      </View>
      
    );
  }
}

export default MapView;

@mfazekas mfazekas changed the title Support edgePadding with camera, set intiial frame to larger Support edgePadding with camera, set initial frame to fullscreen Oct 5, 2020
@mfazekas mfazekas requested a review from ferdicus October 5, 2020 17:28
@mfazekas
Copy link
Contributor Author

mfazekas commented Oct 6, 2020

Updated testcase:

import React from 'react';
import {View, Button} from 'react-native';
import MapboxGL from '@react-native-mapbox-gl/maps';

const latitude = 40.723279;
const longitude = -73.970895;

const boundsEdge = 0.002;

const aLine = {
  type: 'LineString',
  coordinates: [
    [longitude-boundsEdge, latitude-boundsEdge],
    [longitude-boundsEdge, latitude+boundsEdge],
    [longitude+boundsEdge, latitude+boundsEdge],
    [longitude+boundsEdge, latitude-boundsEdge],
    [longitude-boundsEdge, latitude-boundsEdge],
  ],
};

/*
The error is:
Mapbox error [event]:General [code]:-1 [message]:Unable to calculate
appropriate zoom level for bounds. Vertical or horizontal padding is
greater than map's height or width.
Tested on:
- iOS Simulator, iPhone 11
- iOS Simulator, iPhone SE
- Android Emulator, Pixel 3 (API 30)
*/


// iOS: No error, but padding has no effect.
// Android: No error, and padding works.
const padding1 = {
  paddingTop: 0,
  paddingBottom: 0,
};

// iOS: No error, but padding has no effect.
// Android: No error, and padding works.
const padding2 = {
  paddingTop: 0,
  paddingBottom: 663,
};

// iOS: Error, and padding has no effect.
// Android: No error, and padding works.
const padding3 = {
  paddingTop: 0,
  paddingBottom: 664,
};

// iOS: Error, and padding has no effect.
// Android: No error, and padding works.
const padding4 = {
  paddingTop: 1,
  paddingBottom: 663,
};

// iOS: No error, but padding has no effect.
// Android: No error, and padding works.
const padding5 = {
  paddingTop: 31,
  paddingBottom: 32,
};

// iOS: Error, and padding has no effect.
// Android: No error, and padding works.
const padding6 = {
  paddingTop: 32,
  paddingBottom: 32,
};

const initialPadding = padding3;

class MapView extends React.Component {
  static propTypes = {
  };

  state = {
    padding: initialPadding,
  }

  constructor(props) {
    super(props);
  }

  renderContents = () => {
    const point = {
      "type": "Point",
      "coordinates": [longitude, latitude]
    };

    return (
      <MapboxGL.ShapeSource
        id={'source'}
        shape={point}
      >
        <MapboxGL.CircleLayer
          id={'circle-big'}
          style={{
            circleRadius: 20,
            circleColor: 'white',
            circleStrokeColor: 'black',
            circleStrokeWidth: 1,
            iconAllowOverlap: true,
            circlePitchScale: 'viewport',
          }}
        />
      </MapboxGL.ShapeSource>
    );
  };

  render() {
    let {padding} = this.state;
    return (
      <View style={{flex: 1}}>
        <View style={{flexDirection: 'row'}}>
          <Button title="p1" onPress={() => this.setState({padding: padding1})} />
          <Button title="p2" onPress={() => this.setState({padding: padding2})} />
          <Button title="p3" onPress={() => this.setState({padding: padding3})} />
          <Button title="p4" onPress={() => this.setState({padding: padding4})} />
        </View>
        <MapboxGL.MapView
          ref={(c) => (this._map = c)}
          onPress={this.onPress}
          style={{flex: 1}}
        >
          {/* zoomLevel={9} */}
            
          <MapboxGL.Camera
            bounds={{
              ne: [longitude - boundsEdge, latitude + boundsEdge],
              sw: [longitude + boundsEdge, latitude - boundsEdge],
              ...padding,
            }}
          />

          {this.renderContents()}
          <MapboxGL.ShapeSource id="idStreetLayer" shape={aLine}>
            <MapboxGL.LineLayer id="idStreetLayer" />
          </MapboxGL.ShapeSource>
        </MapboxGL.MapView>
      </View>
      
    );
  }
}

export default MapView;

Copy link
Collaborator

@naftalibeder naftalibeder left a comment

Choose a reason for hiding this comment

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

This is working great, thank you! The padding works perfectly, and I left a note about the one issue I was having.

Comment on lines 69 to 86
- (UIEdgeInsets)_clippedPadding:(UIEdgeInsets)padding forView:(RCTMGLMapView*)mapView
{
UIEdgeInsets result = padding;
if ((result.top + result.bottom) > mapView.frame.size.height) {
double extra = mapView.frame.size.height / 4.0 + (result.top + result.bottom) - mapView.frame.size.height;
if (result.top == 0.0) {
result.bottom -= extra;
} else if (result.bottom == 0.0) {
result.top -= extra;
} else {
result.top -= extra/2.0;
result.bottom -= extra/2.0;
}
}
if ((result.left + result.right) > mapView.frame.size.width) {
double extra = mapView.frame.size.width / 4.0 + (result.left + result.right) - mapView.frame.size.width;
if (result.left == 0.0) {
result.right -= extra;
} else if (result.right == 0.0) {
result.left -= extra;
} else {
result.left -= extra/2.0;
result.right -= extra/2.0;
}
}
return result;
}

Copy link
Collaborator

@naftalibeder naftalibeder Oct 8, 2020

Choose a reason for hiding this comment

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

@mfazekas I was still getting the overflow error, and I did a little investigation - it looks like the error occurs if the cumulative padding along one axis is equal to the map dimension (as well as if it exceeds it).

Here's the code I got working correctly - I believe it's a little simpler, but feel free to integrate it however you like.

- (UIEdgeInsets)_clippedPadding:(UIEdgeInsets)padding forView:(RCTMGLMapView*)mapView
{
    UIEdgeInsets result = padding;
    if (result.top + result.bottom >= mapView.frame.size.height) {
        double overflow =  result.top + result.bottom - mapView.frame.size.height;
        result.top -= overflow / 2.0 + 1;
        result.bottom -= overflow / 2.0 + 1;
    }
    if (result.left + result.right >= mapView.frame.size.width) {
        double overflow =  result.left + result.right - mapView.frame.size.width;
        result.left -= overflow / 2.0 + 1;
        result.right -= overflow / 2.0 + 1;
    }
    return result;
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@naftalibeder I'm ambivalent about this. It's hard to correct padding in a sensible way once they are over the size of bounds.
Maybe it's correct for the framework to just complain.

I think this correction is something the app is also capable of doing. Eg get the diemnsion of map before setting up padding, and correct padding, if it's too large.

Copy link
Collaborator

Choose a reason for hiding this comment

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

@mfazekas I'd like to try to make the case for it, if you don't mind.

Other layout examples I can think of tend to fail silently, and just try to intelligently guess what the best outcome would be. For example, 'illegal' CSS configurations still allow a page to render, and even ScrollView edge insets that sum to greater than the view dimension in iOS only throw a warning.

And regarding the app handling this, I think a declarative UI is a worse place to need to do it. Callbacks are asynchronous so it requires handling a number of edge cases that this library would not need to handle. I did attempt to do this, and it required a surprising amount of code just to avoid this crash, and I still wasn't confident that I had prevented it completely. The clipped method above, by contrast, actually guarantees the error will never be thrown.

I'm certainly open to working out a slightly different way for the overflow inset to distribute across the two edges, but I feel pretty strongly the app should not crash because of out-of-bounds layout configuration. Thoughts?

Copy link
Member

@ferdicus ferdicus left a comment

Choose a reason for hiding this comment

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

sorry, I'm a bit out of my element here - would have to trust you and @naftalibeder on this one 👍

As I understood it you're adjusting the padding just for iOS - this isn't an issue on Android (did we already adjust for this there)?
Sometimes I wonder why things like this aren't unified by the upstream SDKs 😅

@naftalibeder
Copy link
Collaborator

@ferdicus That's correct, this would only affect iOS. I actually just opened up a PR that fixes the same issue on Android, which you can see linked above.

@mfazekas mfazekas force-pushed the mfazekas/fix-boudnsPadding-ios branch from 123ad52 to 9ff2da7 Compare October 16, 2020 14:42
@mfazekas
Copy link
Contributor Author

@naftalibeder i've update padding code, pls review:

- (UIEdgeInsets)_clippedPadding:(UIEdgeInsets)padding forView:(RCTMGLMapView*)mapView
{
    UIEdgeInsets result = padding;
    if ((result.top + result.bottom) >= mapView.frame.size.height) {
        double totalPadding = result.top + result.bottom;
        double extra = totalPadding - mapView.frame.size.height + 2.0;
        result.top -= (result.top * extra) / totalPadding;
        result.bottom -= (result.bottom * extra) / totalPadding;
    }
    if ((result.left + result.right) >= mapView.frame.size.width) {
        double totalPadding = result.left + result.right;
        double extra = totalPadding - mapView.frame.size.width + 2.0;
        result.left -= (result.left * extra) / totalPadding;
        result.right -= (result.right * extra) / totalPadding;
    }
    return result;
}

The only way it's improvement on yours is that it will not make paddings negative.
With an 500 height and paddingTop:2 paddingBottom:600 this correct to something like paddingTop:1.9 paddingBottom: 496.1 while yours would correct to paddingTop: -52 paddingBottom: 550

@naftalibeder
Copy link
Collaborator

Nice, good catch!

  • Why add 2.0? Is it to have a full extra point on each edge? (Not a problem, just wondering if it's aesthetic or necessary, since these are double values.)
  • Since the lines like (result.top * extra) / totalPadding are really about multiplying a padding element by a ratio, I might consider being more explicit about that (see below).
  • Personally I would prefer something like paddingNew, which only gets mutated and doesn't get referenced, to prevent accidentally introducing side effects.
- (UIEdgeInsets)_clippedPadding:(UIEdgeInsets)padding forView:(RCTMGLMapView*)mapView
{
    UIEdgeInsets paddingNew = padding;
    if ((padding.top + padding.bottom) >= mapView.frame.size.height) {
        double totalPadding = padding.top + padding.bottom;
        double extra = totalPadding - mapView.frame.size.height + 2.0;
        paddingNew.top = padding.top * (1 - extra / totalPadding);
        paddingNew.bottom = padding.bottom * (1 - extra / totalPadding);
    }
    if ((padding.left + padding.right) >= mapView.frame.size.width) {
        double totalPadding = padding.left + padding.right;
        double extra = totalPadding - mapView.frame.size.width + 2.0;
        paddingNew.left = padding.left * (1 - extra / totalPadding);
        paddingNew.right = padding.right * (1 - extra / totalPadding);
    }
    return paddingNew;
}

These tweaks are up to you though. if it works, it works!

@mfazekas
Copy link
Contributor Author

  • Why add 2.0? Is it to have a full extra point on each edge? (Not a problem, just wondering if it's aesthetic or necessary, since these are double values.)

First we need some minimal delta because of floating point math. Then mapbox alert might use int conversion, not sure but using some extra 0.1 was not enough. Using 1.0 seems to worked so changed to that.

  • Since the lines like (result.top * extra) / totalPadding are really about multiplying a padding element by a ratio, I might consider being more explicit about that (see below).

  • Personally I would prefer something like paddingNew, which only gets mutated and doesn't get referenced, to prevent accidentally introducing side effects.

Agree refactored code.

@naftalibeder
Copy link
Collaborator

Looks great! I'll bring this logic into the PR I opened for Android.

@mfazekas mfazekas merged commit 9103f2d into master Oct 16, 2020
@@ -757,6 +757,7 @@
"FB_SONARKIT_ENABLED=1",
);
INFOPLIST_FILE = RNMapboxGLExample/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
Copy link
Member

Choose a reason for hiding this comment

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

why is this in this PR?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Lazyness. Because travis builds are slow.

iOS in travis is broken i assume since the RN 63.3 upgrade, it was an attempt to fix.

Copy link
Member

Choose a reason for hiding this comment

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

:)

Does this also explain why there are no checks in the PRs, or is that unrelated?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@ferdicus i think that should improve now, with travis-ci.org => travis-ci.com migration

@ferdicus ferdicus deleted the mfazekas/fix-boudnsPadding-ios branch August 20, 2021 10:18
@Andarius Andarius mentioned this pull request Jun 30, 2022
5 tasks
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.

Padding error on iOS: Unable to calculate appropriate zoom level for bounds
3 participants