Skip to main content

Fit

Change camera via imperative methods

import React from 'react';
import { View, Text, ScrollView, TouchableOpacity } from 'react-native';
import { isEqual } from 'lodash';
import Mapbox from '@rnmapbox/maps';

import sheet from '../../styles/sheet';

const buildPadding = ([top, right, bottom, left] = [0, 0, 0, 0]) => {
  return {
    paddingLeft: left,
    paddingRight: right,
    paddingTop: top,
    paddingBottom: bottom,
  };
};

const houseBounds = {
  ne: [-74.135379, 40.795909],
  sw: [-74.135449, 40.795578],
};

const townBounds = {
  ne: [-74.12641, 40.797968],
  sw: [-74.143727, 40.772177],
};

const houseCenter = [
  (houseBounds.ne[0] + houseBounds.sw[0]) / 2,
  (houseBounds.ne[1] + houseBounds.sw[1]) / 2,
];
const townCenter = [
  (townBounds.ne[0] + townBounds.sw[0]) / 2,
  (townBounds.ne[1] + townBounds.sw[1]) / 2,
];

const paddingZero = buildPadding();
const paddingTop = buildPadding([200, 40, 40, 40]);
const paddingBottom = buildPadding([40, 40, 200, 40]);

class Fit extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      locationType: 'houseCenter', // houseCenter | houseBounds | townCenter | townBounds
      zoomLevel: 16, // number
      followUserLocation: false,
      padding: paddingZero,
      animationDuration: 500,

      // For updating the UI in this example.
      cachedFlyTo: undefined, // house | town
      cachedZoomLevel: undefined, // number
    };

    this.camera = null;
  }

  componentDidUpdate(prevProps, prevState) {
    const changed = (stateKey) => {
      // Checking if final state is `undefined` prevents another round of zeroing out in
      // second `componentDidUpdate` call.
      return (
        !isEqual(prevState[stateKey], this.state[stateKey]) &&
        this.state[stateKey] !== undefined
      );
    };

    if (changed('followUserLocation') && this.state.followUserLocation) {
      this.setState({
        locationType: undefined,
        zoomLevel: undefined,
        cachedFlyTo: undefined,
        cachedZoomLevel: undefined,
      });
      return;
    }

    if (changed('locationType') || changed('zoomLevel') || changed('padding')) {
      this.setState({
        cachedFlyTo: undefined,
        cachedZoomLevel: undefined,
      });
    } else if (changed('cachedFlyTo') || changed('cachedZoomLevel')) {
      this.setState({
        locationType: undefined,
        zoomLevel: undefined,
        padding: paddingZero,
      });
    }
  }

  renderSection = (title, buttons, fade = false) => {
    return (
      <View style={{ paddingBottom: 5, opacity: fade ? 0.5 : 1 }}>
        <Text>{title}</Text>
        <ScrollView
          horizontal={true}
          style={{
            flex: 0,
            flexDirection: 'row',
            width: '100%',
            paddingVertical: 10,
          }}
        >
          {buttons.map((button) => (
            <TouchableOpacity
              key={button.title}
              style={{
                flex: 0,
                padding: 5,
                marginRight: 5,
                backgroundColor: button.selected ? 'coral' : '#d8d8d8',
                borderRadius: 5,
              }}
              onPress={button.onPress}
            >
              <Text>{button.title}</Text>
            </TouchableOpacity>
          ))}
        </ScrollView>
      </View>
    );
  };

  cameraProps = () => {
    const {
      locationType,
      zoomLevel,
      followUserLocation,
      padding,
      animationDuration,
    } = this.state;

    let p = {
      bounds: undefined,
      centerCoordinate: undefined,
      zoomLevel: undefined,
      followUserLocation,
      padding,
      animationDuration,
    };

    if (locationType === 'houseCenter') {
      p.centerCoordinate = houseCenter;
    } else if (locationType === 'houseBounds') {
      p.bounds = houseBounds;
    } else if (locationType === 'townCenter') {
      p.centerCoordinate = townCenter;
    } else if (locationType === 'townBounds') {
      p.bounds = townBounds;
    }

    if (zoomLevel !== undefined) {
      p.zoomLevel = zoomLevel;
    }

    return p;
  };

  render() {
    const {
      locationType,
      zoomLevel,
      followUserLocation,
      padding,
      cachedFlyTo,
      cachedZoomLevel,
    } = this.state;

    const centerIsSet = locationType?.toLowerCase().includes('center');

    const locationTypeButtons = [
      ['House (center)', 'houseCenter'],
      ['House (bounds)', 'houseBounds'],
      ['Town (center)', 'townCenter'],
      ['Town (bounds)', 'townBounds'],
      ['undef', undefined],
    ].map((o) => {
      return {
        title: `${o[0]}`,
        selected: locationType === o[1],
        onPress: () => this.setState({ locationType: o[1] }),
      };
    });

    const zoomConfigButtons = [14, 15, 16, 17, 18, 19, 20, undefined].map(
      (n) => {
        return {
          title: n ? `${n}` : 'undef',
          selected: zoomLevel === n,
          onPress: () => this.setState({ zoomLevel: n }),
        };
      },
    );

    const zoomToButtons = [14, 15, 16, 17, 18, 19, 20].map((n) => {
      return {
        title: `${n}`,
        selected: cachedZoomLevel === n,
        onPress: () => {
          this.camera.zoomTo(n, 1000);
          this.setState({ cachedZoomLevel: n });
        },
      };
    });

    return (
      <>
        <Mapbox.MapView
          styleURL={Mapbox.StyleURL.Satellite}
          style={sheet.matchParent}
        >
          <Mapbox.Camera
            ref={(ref) => (this.camera = ref)}
            {...this.cameraProps()}
          />
          <View style={{ flex: 1, ...padding }}>
            <View style={{ flex: 1, borderColor: 'white', borderWidth: 4 }} />
          </View>
        </Mapbox.MapView>

        <ScrollView
          style={{
            flex: 0,
            width: '100%',
            maxHeight: 350,
            backgroundColor: 'white',
          }}
          contentContainerStyle={{
            padding: 10,
            paddingBottom: 20,
          }}
        >
          {this.renderSection('Location type', locationTypeButtons)}

          {this.renderSection(
            'Zoom' +
              (centerIsSet ? '' : ' (only used if center coordinate is set)'),
            zoomConfigButtons,
            !centerIsSet,
          )}

          {this.renderSection('Follow user location', [
            {
              title: followUserLocation ? 'Enabled' : 'Disabled',
              selected: followUserLocation,
              onPress: () =>
                this.setState({ followUserLocation: !followUserLocation }),
            },
          ])}

          {this.renderSection('Fly to (imperative)', [
            {
              title: 'House',
              selected: cachedFlyTo === 'house',
              onPress: () => {
                this.camera.flyTo(houseCenter);
                this.setState({ cachedFlyTo: 'house' });
              },
            },
            {
              title: 'Town',
              selected: cachedFlyTo === 'town',
              onPress: () => {
                this.camera.flyTo(townCenter);
                this.setState({ cachedFlyTo: 'town' });
              },
            },
          ])}

          {this.renderSection('Zoom to (imperative)', zoomToButtons)}

          {this.renderSection('Padding', [
            {
              title: 'None',
              selected: isEqual(padding, paddingZero),
              onPress: () => this.setState({ padding: paddingZero }),
            },
            {
              title: 'Top',
              selected: isEqual(padding, paddingTop),
              onPress: () => this.setState({ padding: paddingTop }),
            },
            {
              title: 'Bottom',
              selected: isEqual(padding, paddingBottom),
              onPress: () => this.setState({ padding: paddingBottom }),
            },
          ])}
        </ScrollView>
      </>
    );
  }
}

export default Fit;

}