diff --git a/CHANGELOG.md b/CHANGELOG.md index d0facd61f..10926ada0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,12 +14,12 @@ The setup was changed - see install instructions for more details. In a nuthsell * Default implementation is `maplibre` as it requires not further setup. *WARNING* using mapbox styles from `maplibre` has different pricing than mapbox native sdk-s. * On Podfile `$RNMBGL.(pre|post)_install` was changed `$RNMapboxMaps.(pre|post)_install` * Package name was changed from `@react-native-mapbox-gl/maps` to `@rnmapbox/maps`. If you just testing with the v10 version you can use something like [babel-plugin-transform-rename-import](https://www.npmjs.com/package/babel-plugin-transform-rename-import) to keep using the old imports for a while. - * `MapboxGL.setAccessToken` now requires `MapboxGL.setWellKnownTileServer` on maplibre. #### Changes: +- Convert Camera component to TypeScript and update related documentation generation and tests ([#2057](https://github.com/rnmapbox/maps/pull/2057)) - Implement clustering properties to ShapeSource ([#1745](https://github.com/react-native-mapbox-gl/maps/pull/1745)) - Initial Mapbox V10 support ([#1750](https://github.com/rnmapbox/maps/pull/1750)) - Updated MapLibre on Android to 9.5.2 ([#1780](https://github.com/rnmapbox/maps/pull/1780)) diff --git a/__tests__/components/Camera.test.js b/__tests__/components/Camera.test.js index 720302bd8..2bf58172f 100644 --- a/__tests__/components/Camera.test.js +++ b/__tests__/components/Camera.test.js @@ -1,1147 +1,71 @@ import React from 'react'; import { render } from '@testing-library/react-native'; -import Camera from '../../javascript/components/Camera'; +import { Camera } from '../../javascript/components/Camera'; -const cameraWithoutFollowDefault = { - ...Camera.defaultProps, - animationDuration: 2000, - animationMode: 'easeTo', - centerCoordinate: [-111.8678, 40.2866], - zoomLevel: 16, - followUserLocation: false, - followUserMode: 'normal', - isUserInteraction: false, -}; +const coordinate1 = [-111.8678, 40.2866]; -const cameraWithoutFollowChanged = { - ...Camera.defaultProps, - animationDuration: 1000, - animationMode: 'easeTo', - centerCoordinate: [-110.8678, 37.2866], - zoomLevel: 13, - followUserLocation: false, - followUserMode: 'normal', - isUserInteraction: false, +const bounds1 = { + ne: [-74.12641, 40.797968], + sw: [-74.143727, 40.772177], }; -const cameraWithFollowCourse = { - ...Camera.defaultProps, - animationDuration: 2000, - animationMode: 'easeTo', - defaultSettings: { - centerCoordinate: [-111.8678, 40.2866], - zoomLevel: 16, - }, - followUserLocation: true, - followUserMode: 'course', - isUserInteraction: false, +const paddingZero = { + paddingTop: 0, + paddingRight: 0, + paddingBottom: 0, + paddingLeft: 0, }; -const cameraWithBounds = { - ...Camera.defaultProps, - animationDuration: 2000, - animationMode: 'easeTo', - bounds: { - ne: [-74.12641, 40.797968], - sw: [-74.143727, 40.772177], - }, - isUserInteraction: false, - maxZoomLevel: 19, +const toFeature = (position) => { + return { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: position, + }, + properties: {}, + }; }; +const toFeatureCollection = (bounds) => { + return { + type: 'FeatureCollection', + features: [toFeature(bounds.ne), toFeature(bounds.sw)], + }; +}; describe('Camera', () => { - describe('render', () => { - test('renders correctly', () => { - const { getByTestId } = render(); - - expect(getByTestId('Camera')).toBeDefined(); - }); - - test('has proper default props', () => { - const { getByTestId } = render(); - - expect(getByTestId('Camera').props).toStrictEqual({ - children: undefined, - testID: 'Camera', - followUserLocation: undefined, - followUserMode: undefined, - followPitch: undefined, - followHeading: undefined, - followZoomLevel: undefined, - stop: { - mode: 'Ease', - pitch: undefined, - heading: undefined, - duration: 2000, - zoom: undefined, - paddingBottom: 0, - paddingLeft: 0, - paddingRight: 0, - paddingTop: 0, - }, - maxZoomLevel: undefined, - minZoomLevel: undefined, - maxBounds: null, - defaultStop: null, - onUserTrackingModeChange: undefined, - }); + test('defaults are set', () => { + const result = render(); + const { props } = result.queryByTestId('Camera'); + expect(props.stop).toStrictEqual({ + ...paddingZero, }); }); - - describe('class', () => { - test('correct "UserTrackingModes" statics', () => { - expect(Camera.UserTrackingModes).toStrictEqual({ - Follow: 'normal', - FollowWithCourse: 'course', - FollowWithHeading: 'compass', - }); + test('set location by center', () => { + const result = render( + , + ); + const { props } = result.queryByTestId('Camera'); + props.stop.centerCoordinate = JSON.parse(props.stop.centerCoordinate); + expect(props.stop).toStrictEqual({ + centerCoordinate: toFeature(coordinate1), + zoom: 14, + ...paddingZero, }); }); - - describe('methods', () => { - describe('#_handleCameraChange', () => { - let camera; - - beforeEach(() => { - camera = new Camera(); - - // set up fake ref - camera.refs = { - camera: { - setNativeProps: jest.fn(), - }, - }; - - // set up fake props - // we only do this, because we want to test the class methods! - camera.props = {}; - - jest.spyOn(camera, '_setCamera'); - jest.spyOn(camera, '_hasCameraChanged'); - jest.spyOn(camera, '_hasBoundsChanged'); - }); - - test('does not call "#_setCamera" or "#setNativeProps" when "nextCamera" has no changes to "currentCamera"', () => { - camera._handleCameraChange( - cameraWithoutFollowDefault, - cameraWithoutFollowDefault, - ); - - expect(camera._hasCameraChanged).toHaveBeenCalled(); - expect(camera._setCamera).not.toHaveBeenCalled(); - expect(camera.refs.camera.setNativeProps).not.toHaveBeenCalled(); - }); - - test('sets "followUserLocation" to false when it was removed on "nextCamera"', () => { - camera._handleCameraChange( - cameraWithFollowCourse, - cameraWithoutFollowDefault, - ); - - expect(camera._hasCameraChanged).toHaveBeenCalled(); - expect(camera._setCamera).not.toHaveBeenCalled(); - - expect(camera.refs.camera.setNativeProps).toHaveBeenCalledTimes(1); - - expect(camera.refs.camera.setNativeProps).toHaveBeenCalledWith({ - followUserLocation: false, - }); - }); - - test('sets "followUserLocation" to true when it was added on "nextCamera"', () => { - camera._handleCameraChange( - cameraWithoutFollowDefault, - cameraWithFollowCourse, - ); - - expect(camera._hasCameraChanged).toHaveBeenCalled(); - expect(camera._setCamera).not.toHaveBeenCalled(); - - expect(camera.refs.camera.setNativeProps).toHaveBeenCalledTimes(2); - - expect(camera.refs.camera.setNativeProps).toHaveBeenNthCalledWith(1, { - followUserLocation: true, - }); - expect(camera.refs.camera.setNativeProps).toHaveBeenNthCalledWith(2, { - followHeading: undefined, - followPitch: undefined, - followUserMode: 'course', - followZoomLevel: undefined, - }); - }); - - test('calls "#_setCamera" when "nextCamera" "hasChanged" without bounds', () => { - camera._handleCameraChange( - cameraWithoutFollowDefault, - cameraWithoutFollowChanged, - ); - - expect(camera._hasCameraChanged).toHaveBeenCalled(); - expect(camera._hasBoundsChanged).toHaveBeenCalled(); - expect(camera._setCamera).toHaveBeenCalledWith({ - animationDuration: 1000, - animationMode: 'easeTo', - centerCoordinate: [-110.8678, 37.2866], - heading: undefined, - pitch: undefined, - zoomLevel: 13, - }); - }); - - test('calls "#_hasBoundsChanged" & "#_setCamera" when "nextCamera" "hasChanged" with bounds', () => { - camera._handleCameraChange( - cameraWithoutFollowDefault, - cameraWithBounds, - ); - - expect(camera._hasCameraChanged).toHaveBeenCalled(); - expect(camera._hasBoundsChanged).toHaveBeenCalledTimes(2); - expect(camera._setCamera).toHaveBeenCalledWith({ - animationDuration: 2000, - animationMode: 'easeTo', - bounds: { ne: [-74.12641, 40.797968], sw: [-74.143727, 40.772177] }, - heading: undefined, - pitch: undefined, - zoomLevel: undefined, - }); - }); - }); - - describe('#_hasCameraChanged', () => { - let camera; - - beforeEach(() => { - camera = new Camera(); - - // set up fake ref - camera.refs = { - camera: { - setNativeProps: jest.fn(), - }, - }; - - // set up fake props - // we only do this, because we want to test the class methods! - camera.props = {}; - - jest.spyOn(camera, '_hasCenterCoordinateChanged'); - jest.spyOn(camera, '_hasBoundsChanged'); - }); - - test('returns true if "hasDefaultPropsChanged"', () => { - const testCases = [ - [{ heading: 120 }, { heading: 121 }], - [ - { - centerCoordinate: [-111.8678, 40.2866], - }, - { - centerCoordinate: [-111.8678, 38.2866], - }, - ], - [ - { - bounds: { - ne: [-74.12641, 40.797968], - sw: [-74.143727, 40.772177], - }, - }, - { - bounds: { - ne: [-64.12641, 40.797968], - sw: [-74.143727, 40.772177], - }, - }, - ], - [ - { - pitch: 45, - }, - { - pitch: 55, - }, - ], - [ - { - zoomLevel: 10, - }, - { - zoomLevel: 15, - }, - ], - [ - // using the usecase in /example - { - triggerKey: 1582486618640, // Date.now() - }, - { - triggerKey: 1582486626818, // Date.now() - }, - ], - ]; - - testCases.forEach((c) => { - expect(camera._hasCameraChanged(c[0], c[1])).toBe(true); - }); - }); - - test('returns true if "hasFollowPropsChanged"', () => { - const testCases = [ - [{ followUserLocation: false }, { followUserLocation: true }], - [{ followUserMode: 'normal' }, { followUserMode: 'course' }], - [{ followZoomLevel: 10 }, { followZoomLevel: 13 }], - [{ followHeading: 100 }, { followHeading: 110 }], - [{ followPitch: 40 }, { followPitch: 49 }], - ]; - - testCases.forEach((c) => { - expect(camera._hasCameraChanged(c[0], c[1])).toBe(true); - }); - }); - - test('returns true if "hasAnimationPropsChanged"', () => { - const testCases = [ - [{ animationDuration: 3000 }, { animationDuration: 1000 }], - [{ animationMode: 'flyTo' }, { animationMode: 'easeTo' }], - ]; - - testCases.forEach((c) => { - expect(camera._hasCameraChanged(c[0], c[1])).toBe(true); - }); - }); - }); - - describe('#_hasCenterCoordinateChanged', () => { - const camera = new Camera(); - - test('returns false when centerCoordinates are missing', () => { - expect(camera._hasCenterCoordinateChanged({}, {})).toBe(false); - }); - - test('returns false when centerCoordinates have not changed', () => { - expect( - camera._hasCenterCoordinateChanged( - [-111.8678, 40.2866], - [-111.8678, 40.2866], - ), - ).toBe(false); - }); - - test('returns true when centerCoordinates have changed', () => { - expect( - camera._hasCenterCoordinateChanged([-111.8678, 40.2866], undefined), - ).toBe(true); - - expect( - camera._hasCenterCoordinateChanged(undefined, [-111.8678, 40.2866]), - ).toBe(true); - - // isLngDiff - expect( - camera._hasCenterCoordinateChanged( - [-111.2678, 40.2866], - [-111.8678, 40.2866], - ), - ).toBe(true); - - // isLatDiff - expect( - camera._hasCenterCoordinateChanged( - [-111.2678, 40.2866], - [-111.8678, 33.2866], - ), - ).toBe(true); - }); - }); - - describe('#_hasBoundsChanged', () => { - const camera = new Camera(); - const bounds = { - ne: [-74.12641, 40.797968], - sw: [-74.143727, 40.772177], - paddingTop: 5, - paddingLeft: 5, - paddingRight: 5, - paddingBottom: 5, - }; - - test('returns false when bounds are missing', () => { - expect(camera._hasBoundsChanged(undefined, undefined)).toBe(false); - }); - - test('returns false when bounds have not changed', () => { - expect(camera._hasBoundsChanged(bounds, bounds)).toBe(false); - }); - - test('returns true when bound props have changed', () => { - // ne[0] - expect( - camera._hasBoundsChanged(bounds, { - ...bounds, - ne: [-34.12641, 40.797968], - }), - ).toBe(true); - - // ne[1] - expect( - camera._hasBoundsChanged(bounds, { - ...bounds, - ne: [-74.12641, 30.797968], - }), - ).toBe(true); - - // sw[0] - expect( - camera._hasBoundsChanged(bounds, { - ...bounds, - sw: [-74.143723, 40.772177], - }), - ).toBe(true); - - // sw[1] - expect( - camera._hasBoundsChanged(bounds, { - ...bounds, - sw: [-74.143727, 40.772137], - }), - ).toBe(true); - - // paddingTop - expect( - camera._hasBoundsChanged(bounds, { - ...bounds, - paddingTop: 3, - }), - ).toBe(true); - - // paddingLeft - expect( - camera._hasBoundsChanged(bounds, { - ...bounds, - paddingLeft: 3, - }), - ).toBe(true); - - // paddingRight - expect( - camera._hasBoundsChanged(bounds, { - ...bounds, - paddingRight: 3, - }), - ).toBe(true); - - // paddingBottom - expect( - camera._hasBoundsChanged(bounds, { - ...bounds, - paddingBottom: 3, - }), - ).toBe(true); - }); - - describe('does work with maxBounds', () => { - const currentMaxBounds = { - ne: [-74.12641, 40.797968], - sw: [-74.143727, 40.772177], - }; - - const nextMaxBounds = { - ne: [-83.12641, 42.797968], - sw: [-64.143727, 35.772177], - }; - - test('returns true if changed', () => { - expect( - camera._hasBoundsChanged(currentMaxBounds, nextMaxBounds), - ).toBe(true); - }); - - test('returns false if unchanged', () => { - expect( - camera._hasBoundsChanged(currentMaxBounds, currentMaxBounds), - ).toBe(false); - }); - - test('returns false if both undefined', () => { - expect(camera._hasBoundsChanged(undefined, undefined)).toBe(false); - }); - - test('does work with currentBounds being undefined', () => { - expect(camera._hasBoundsChanged(undefined, nextMaxBounds)).toBe(true); - }); - - test('does work with nextBounds being undefined', () => { - expect(camera._hasBoundsChanged(currentMaxBounds, undefined)).toBe( - true, - ); - }); - }); - }); - - describe('#fitBounds', () => { - const camera = new Camera(); - const ne = [-63.12641, 39.797968]; - const sw = [-74.143727, 40.772177]; - - beforeEach(() => { - camera.setCamera = jest.fn(); - }); - - test('works without provided "padding" and/ or "animationDuration"', () => { - // FIXME: animationDuration and padding of null lead to malformed setCamera config - - const expectedCallResults = [ - { - animationDuration: null, - animationMode: 'easeTo', - bounds: { - ne: [-63.12641, 39.797968], - sw: [-74.143727, 40.772177], - }, - padding: { - paddingBottom: null, - paddingLeft: null, - paddingRight: null, - paddingTop: null, - }, - }, - { - animationDuration: 0, - animationMode: 'easeTo', - bounds: { - ne: [-63.12641, 39.797968], - sw: [-74.143727, 40.772177], - }, - padding: { - paddingBottom: null, - paddingLeft: null, - paddingRight: null, - paddingTop: null, - }, - }, - { - animationDuration: 0, - animationMode: 'easeTo', - bounds: { - ne: [-63.12641, 39.797968], - sw: [-74.143727, 40.772177], - }, - padding: { - paddingBottom: 0, - paddingLeft: 0, - paddingRight: 0, - paddingTop: 0, - }, - }, - ]; - - camera.fitBounds(ne, sw, null, null); - camera.fitBounds(ne, sw, null); - camera.fitBounds(ne, sw); - - camera.setCamera.mock.calls.forEach((call, i) => { - expect(call[0]).toStrictEqual(expectedCallResults[i]); - }); - }); - - // TODO: Refactor #fitBounds to throw when ne or sw aren't provided - // This is a public method and people will call it with all sorts of data - test.skip('throws when "ne" or "sw" are missing', () => {}); - - test('works with "padding" being a single number', () => { - const expectedCallResult = { - animationDuration: 500, - animationMode: 'easeTo', - bounds: { - ne: [-63.12641, 39.797968], - sw: [-74.143727, 40.772177], - }, - padding: { - paddingBottom: 3, - paddingLeft: 3, - paddingRight: 3, - paddingTop: 3, - }, - }; - - camera.fitBounds(ne, sw, 3, 500); - expect(camera.setCamera).toHaveBeenCalledWith(expectedCallResult); - }); - - test('works with "padding" being an array of two numbers', () => { - const expectedCallResult = { - animationDuration: 500, - animationMode: 'easeTo', - bounds: { - ne: [-63.12641, 39.797968], - sw: [-74.143727, 40.772177], - }, - padding: { - paddingBottom: 3, - paddingLeft: 5, - paddingRight: 5, - paddingTop: 3, - }, - }; - - camera.fitBounds(ne, sw, [3, 5], 500); - expect(camera.setCamera).toHaveBeenCalledWith(expectedCallResult); - }); - - test('works with "padding" being an array of four numbers', () => { - const expectedCallResult = { - animationDuration: 500, - animationMode: 'easeTo', - bounds: { - ne: [-63.12641, 39.797968], - sw: [-74.143727, 40.772177], - }, - padding: { - paddingBottom: 8, - paddingLeft: 10, - paddingRight: 5, - paddingTop: 3, - }, - }; - - camera.fitBounds(ne, sw, [3, 5, 8, 10], 500); - expect(camera.setCamera).toHaveBeenCalledWith(expectedCallResult); - }); - }); - - describe('#flyTo', () => { - const camera = new Camera(); - - beforeEach(() => { - camera.setCamera = jest.fn(); - }); - - test.skip('throws when no coordinates are provided', () => { - // TODO: Refactor #flyTo to throw when coordinates aren't provided - // This is a public method and people will call it with all sorts of data - }); - - test('sets default "animationDuration" when called without it', () => { - camera.flyTo([-111.8678, 40.2866]); - expect(camera.setCamera).toHaveBeenCalledWith({ - animationDuration: 2000, - animationMode: 'flyTo', - centerCoordinate: [-111.8678, 40.2866], - }); - }); - - test('calls "setCamera" with correct config', () => { - camera.flyTo([-111.8678, 40.2866], 5000); - expect(camera.setCamera).toHaveBeenCalledWith({ - animationDuration: 5000, - animationMode: 'flyTo', - centerCoordinate: [-111.8678, 40.2866], - }); - }); - }); - - describe('#moveTo', () => { - const camera = new Camera(); - - beforeEach(() => { - // FIXME: Why is moveTo calling #_setCamera instead of #setCamera? - // let's be consistent here - have all methods use one of both - camera._setCamera = jest.fn(); - }); - - test.skip('throws when no coordinates are provided', () => { - // TODO: Refactor #moveTo to throw when coordinates aren't provided - // This is a public method and people will call it with all sorts of data - }); - - test('sets default "animationDuration" when called without it', () => { - camera.moveTo([-111.8678, 40.2866]); - expect(camera._setCamera).toHaveBeenCalledWith({ - animationDuration: 0, - centerCoordinate: [-111.8678, 40.2866], - }); - }); - - test('calls "_setCamera" with correct config', () => { - camera.moveTo([-111.8678, 40.2866], 5000); - expect(camera._setCamera).toHaveBeenCalledWith({ - animationDuration: 5000, - centerCoordinate: [-111.8678, 40.2866], - }); - }); - }); - - describe('#zoomTo', () => { - const camera = new Camera(); - - beforeEach(() => { - camera._setCamera = jest.fn(); - }); - - test.skip('throws when no zoomLevel is provided', () => { - // TODO: Refactor #moveTo to throw when coordinates aren't provided - // This is a public method and people will call it with all sorts of data - }); - - test('sets default "animationDuration" when called without it', () => { - camera.zoomTo(10); - expect(camera._setCamera).toHaveBeenCalledWith({ - animationDuration: 2000, - zoomLevel: 10, - animationMode: 'flyTo', - }); - }); - - test('calls "_setCamera" with correct config', () => { - camera.zoomTo(10, 3000); - expect(camera._setCamera).toHaveBeenCalledWith({ - animationDuration: 3000, - zoomLevel: 10, - animationMode: 'flyTo', - }); - }); - }); - - describe('#setCamera', () => { - const camera = new Camera(); - - beforeEach(() => { - camera._setCamera = jest.fn(); - }); - - test('sets default empty "config" when called without one', () => { - camera.setCamera(); - expect(camera._setCamera).toHaveBeenCalledWith({}); - }); - - test('calls "_setCamera" with passed config', () => { - const config = { - animationDuration: 500, - animationMode: 'easeTo', - bounds: { - ne: [-63.12641, 39.797968], - paddingBottom: 8, - paddingLeft: 10, - paddingRight: 5, - paddingTop: 3, - sw: [-74.143727, 40.772177], - }, - }; - - camera.setCamera(config); - expect(camera._setCamera).toHaveBeenCalledWith(config); - }); - }); - - describe('#_setCamera', () => { - const camera = new Camera(); - - beforeEach(() => { - jest.spyOn(Camera.prototype, '_createStopConfig'); - - // set up fake ref - camera.refs = { - camera: { - setNativeProps: jest.fn(), - }, - }; - - // set up fake props - // we only do this, because we want to test the class methods! - camera.props = {}; - - jest.clearAllMocks(); - }); - - test('calls "_createStopConfig" and passes stopConfig to "setNativeProps"', () => { - const config = { - animationDuration: 500, - animationMode: 'easeTo', - bounds: { - ne: [-63.12641, 39.797968], - paddingBottom: 8, - paddingLeft: 10, - paddingRight: 5, - paddingTop: 3, - sw: [-74.143727, 40.772177], - }, - heading: 100, - pitch: 45, - zoomLevel: 11, - }; - - camera._setCamera(config); - - expect(camera._createStopConfig).toHaveBeenCalledWith({ - animationDuration: 500, - animationMode: 'easeTo', - bounds: { - ne: [-63.12641, 39.797968], - paddingBottom: 8, - paddingLeft: 10, - paddingRight: 5, - paddingTop: 3, - sw: [-74.143727, 40.772177], - }, - heading: 100, - pitch: 45, - zoomLevel: 11, - }); - - expect(camera._createStopConfig).toHaveBeenCalledTimes(1); - - expect(camera.refs.camera.setNativeProps).toHaveBeenCalledWith({ - stop: { - bounds: - '{"type":"FeatureCollection","features":[{"type":"Feature","properties":{},"geometry":{"type":"Point","coordinates":[-63.12641,39.797968]}},{"type":"Feature","properties":{},"geometry":{"type":"Point","coordinates":[-74.143727,40.772177]}}]}', - paddingBottom: 8, - paddingLeft: 10, - paddingRight: 5, - paddingTop: 3, - duration: 500, - heading: 100, - mode: 'Ease', - pitch: 45, - zoom: 11, - }, - }); - }); - - test('creates multiple stops when provided', () => { - const config = { - stops: [ - { - animationDuration: 50, - animationMode: 'easeTo', - bounds: { - ne: [-63.12641, 39.797968], - paddingBottom: 2, - paddingLeft: 2, - paddingRight: 2, - paddingTop: 2, - sw: [-74.143727, 40.772177], - }, - heading: 20, - pitch: 25, - zoomLevel: 16, - }, - { - animationDuration: 3000, - animationMode: 'flyTo', - bounds: { - ne: [-63.12641, 59.797968], - paddingBottom: 8, - paddingLeft: 10, - paddingRight: 5, - paddingTop: 3, - sw: [-71.143727, 40.772177], - }, - heading: 40, - pitch: 45, - zoomLevel: 8, - }, - { - animationDuration: 500, - animationMode: 'easeTo', - bounds: { - ne: [-63.12641, 39.797968], - paddingBottom: 8, - paddingLeft: 10, - paddingRight: 5, - paddingTop: 3, - sw: [-74.143727, 40.772177], - }, - heading: 100, - pitch: 45, - zoomLevel: 11, - }, - ], - }; - - camera._setCamera(config); - - expect(camera._createStopConfig).toHaveBeenCalledTimes(3); - - expect(camera._createStopConfig).toHaveBeenCalledWith({ - animationDuration: 500, - animationMode: 'easeTo', - bounds: { - ne: [-63.12641, 39.797968], - paddingBottom: 8, - paddingLeft: 10, - paddingRight: 5, - paddingTop: 3, - sw: [-74.143727, 40.772177], - }, - heading: 100, - pitch: 45, - zoomLevel: 11, - }); - - expect(camera.refs.camera.setNativeProps).toHaveBeenCalledWith({ - stop: { - stops: [ - { - bounds: - '{"type":"FeatureCollection","features":[{"type":"Feature","properties":{},"geometry":{"type":"Point","coordinates":[-63.12641,39.797968]}},{"type":"Feature","properties":{},"geometry":{"type":"Point","coordinates":[-74.143727,40.772177]}}]}', - paddingBottom: 2, - paddingLeft: 2, - paddingRight: 2, - paddingTop: 2, - duration: 50, - heading: 20, - mode: 'Ease', - pitch: 25, - zoom: 16, - }, - { - bounds: - '{"type":"FeatureCollection","features":[{"type":"Feature","properties":{},"geometry":{"type":"Point","coordinates":[-63.12641,59.797968]}},{"type":"Feature","properties":{},"geometry":{"type":"Point","coordinates":[-71.143727,40.772177]}}]}', - paddingBottom: 8, - paddingLeft: 10, - paddingRight: 5, - paddingTop: 3, - duration: 3000, - heading: 40, - mode: 'Flight', - pitch: 45, - zoom: 8, - }, - { - bounds: - '{"type":"FeatureCollection","features":[{"type":"Feature","properties":{},"geometry":{"type":"Point","coordinates":[-63.12641,39.797968]}},{"type":"Feature","properties":{},"geometry":{"type":"Point","coordinates":[-74.143727,40.772177]}}]}', - paddingBottom: 8, - paddingLeft: 10, - paddingRight: 5, - paddingTop: 3, - duration: 500, - heading: 100, - mode: 'Ease', - pitch: 45, - zoom: 11, - }, - ], - }, - }); - }); - }); - - describe('#_createDefaultCamera', () => { - const camera = new Camera(); - - beforeEach(() => {}); - - test('returns null without "defaultSettings"', () => { - camera.props = {}; - expect(camera._createDefaultCamera()).toBe(null); - }); - - test('returns "defaultCamera" with "defaultSettings" and sets property', () => { - camera.props = { - defaultSettings: { - centerCoordinate: [-111.8678, 40.2866], - zoomLevel: 16, - animationMode: 'moveTo', - }, - }; - - const defaultCamera = { - centerCoordinate: - '{"type":"Feature","properties":{},"geometry":{"type":"Point","coordinates":[-111.8678,40.2866]}}', - duration: 0, - heading: undefined, - mode: 'Move', - pitch: undefined, - zoom: 16, - paddingBottom: 0, - paddingLeft: 0, - paddingRight: 0, - paddingTop: 0, - }; - - expect(camera.defaultCamera).toStrictEqual(undefined); - expect(camera._createDefaultCamera()).toStrictEqual(defaultCamera); - expect(camera.defaultCamera).toStrictEqual(defaultCamera); - }); - }); - - describe('#_createStopConfig', () => { - const camera = new Camera(); - const configWithoutBounds = { - animationDuration: 2000, - animationMode: 'easeTo', - pitch: 45, - heading: 110, - zoomLevel: 9, - }; - - const configWithBounds = { - animationDuration: 500, - animationMode: 'easeTo', - bounds: { - ne: [-63.12641, 39.797968], - paddingBottom: 8, - paddingLeft: 10, - paddingRight: 5, - paddingTop: 3, - sw: [-74.143727, 40.772177], - }, - heading: 100, - pitch: 45, - zoomLevel: 11, - }; - - beforeEach(() => { - jest.spyOn(Camera.prototype, '_getNativeCameraMode'); - - jest.clearAllMocks(); - }); - - test('returns null with "followUserLocation" prop and "!ignoreFollowUserLocation"', () => { - camera.props = { - followUserLocation: true, - }; - expect(camera._createStopConfig()).toBe(null); - }); - - test('returns correct "stopConfig" without bounds', () => { - camera.props = { - followUserLocation: true, - }; - - expect( - camera._createStopConfig(configWithoutBounds, true), - ).toStrictEqual({ - duration: 2000, - heading: 110, - mode: 'Ease', - pitch: 45, - zoom: 9, - paddingBottom: 0, - paddingLeft: 0, - paddingRight: 0, - paddingTop: 0, - }); - - // with centerCoordinate - expect( - camera._createStopConfig( - { ...configWithoutBounds, centerCoordinate: [-111.8678, 40.2866] }, - true, - ), - ).toStrictEqual({ - centerCoordinate: - '{"type":"Feature","properties":{},"geometry":{"type":"Point","coordinates":[-111.8678,40.2866]}}', - duration: 2000, - heading: 110, - mode: 'Ease', - pitch: 45, - zoom: 9, - paddingBottom: 0, - paddingLeft: 0, - paddingRight: 0, - paddingTop: 0, - }); - }); - - test('returns correct "stopConfig" with bounds', () => { - camera.props = { - followUserLocation: true, - }; - - expect(camera._createStopConfig(configWithBounds, true)).toStrictEqual({ - bounds: - '{"type":"FeatureCollection","features":[{"type":"Feature","properties":{},"geometry":{"type":"Point","coordinates":[-63.12641,39.797968]}},{"type":"Feature","properties":{},"geometry":{"type":"Point","coordinates":[-74.143727,40.772177]}}]}', - paddingBottom: 8, - paddingLeft: 10, - paddingRight: 5, - paddingTop: 3, - duration: 500, - heading: 100, - mode: 'Ease', - pitch: 45, - zoom: 11, - }); - - // with centerCoordinate - expect( - camera._createStopConfig( - { ...configWithBounds, centerCoordinate: [-111.8678, 40.2866] }, - true, - ), - ).toStrictEqual({ - bounds: - '{"type":"FeatureCollection","features":[{"type":"Feature","properties":{},"geometry":{"type":"Point","coordinates":[-63.12641,39.797968]}},{"type":"Feature","properties":{},"geometry":{"type":"Point","coordinates":[-74.143727,40.772177]}}]}', - paddingBottom: 8, - paddingLeft: 10, - paddingRight: 5, - paddingTop: 3, - centerCoordinate: - '{"type":"Feature","properties":{},"geometry":{"type":"Point","coordinates":[-111.8678,40.2866]}}', - duration: 500, - heading: 100, - mode: 'Ease', - pitch: 45, - zoom: 11, - }); - }); - }); - - describe('#_getNativeCameraMode', () => { - const camera = new Camera(); - - test('returns "Flight" for "flyTo"', () => { - expect( - camera._getNativeCameraMode({ animationMode: 'flyTo' }), - ).toStrictEqual('Flight'); - }); - - test('returns "None" for "moveTo"', () => { - expect( - camera._getNativeCameraMode({ animationMode: 'moveTo' }), - ).toStrictEqual('Move'); - }); - - test('returns "Ease" as default', () => { - expect(camera._getNativeCameraMode({})).toStrictEqual('Ease'); - }); - }); - - describe('#_getMaxBounds', () => { - const camera = new Camera(); - - test('returns null if no "maxBounds"', () => { - camera.props = {}; - expect(camera._getMaxBounds()).toStrictEqual(null); - - camera.props = { - maxBounds: { - ne: [-74.12641, 40.797968], - }, - }; - expect(camera._getMaxBounds()).toStrictEqual(null); - - camera.props = { - maxBounds: { - sw: [-74.143727, 40.772177], - }, - }; - expect(camera._getMaxBounds()).toStrictEqual(null); - }); - - test('returns maxBounds when "maxBounds" property is set', () => { - camera.props = { - maxBounds: { - ne: [-74.12641, 40.797968], - sw: [-74.143727, 40.772177], - }, - }; - - expect(camera._getMaxBounds()).toStrictEqual( - '{"type":"FeatureCollection","features":[{"type":"Feature","properties":{},"geometry":{"type":"Point","coordinates":[-74.12641,40.797968]}},{"type":"Feature","properties":{},"geometry":{"type":"Point","coordinates":[-74.143727,40.772177]}}]}', - ); - }); + test('set location by bounds', () => { + const result = render(); + const { props } = result.queryByTestId('Camera'); + props.stop.bounds = JSON.parse(props.stop.bounds); + expect(props.stop).toStrictEqual({ + bounds: toFeatureCollection(bounds1), + ...paddingZero, }); }); + test('animation mode', () => { + const result = render(); + const { props } = result.queryByTestId('Camera'); + expect(props.stop.mode).toEqual('Move'); + }); }); diff --git a/docs/Atmosphere.md b/docs/Atmosphere.md index 65f5c3d4b..706fb07db 100644 --- a/docs/Atmosphere.md +++ b/docs/Atmosphere.md @@ -5,7 +5,7 @@ ### props | Prop | Type | Default | Required | Description | | ---- | :--: | :-----: | :------: | :----------: | -| style | `FIX ME UNKNOWN TYPE` | `none` | `true` | FIX ME NO DESCRIPTION | +| style | `AtmosphereLayerStyleProps` | `none` | `true` | FIX ME NO DESCRIPTION | ### styles diff --git a/docs/Camera.md b/docs/Camera.md index c52100eb8..b4560d274 100644 --- a/docs/Camera.md +++ b/docs/Camera.md @@ -1,162 +1,34 @@ - + ## -### +### Controls the perspective from which the user sees the map.

To use imperative methods, pass in a ref object:

```
const camera = useRef(null);

useEffect(() => {
camera.current?.setCamera({
centerCoordinate: [lon, lat],
});
}, []);

return (

);
``` ### props | Prop | Type | Default | Required | Description | | ---- | :--: | :-----: | :------: | :----------: | -| allowUpdates | `bool` | `true` | `false` | If false, the camera will not send any props to the native module. Intended to be used to prevent unnecessary tile fetching and improve performance when the map is not visible. Defaults to true. | -| animationDuration | `number` | `2000` | `false` | The duration a camera update takes (in ms) | -| animationMode | `enum` | `'easeTo'` | `false` | The animation style when the camara updates. One of:
`flyTo`: A complex flight animation, affecting both position and zoom.
`easeTo`: A standard damped curve.
`linearTo`: An even linear transition.
`none`: An instantaneous change (v10 only).
`moveTo`: An instantaneous change (The `bounds.padding*` properties are deprecated; use root `padding` property instead. | -|     ne | `array` | `none` | `true` | North east coordinate of bound | -|     sw | `array` | `none` | `true` | South west coordinate of bound | -|     paddingLeft | `number` | `none` | `false` | Left padding in points (deprecated; use root `padding` property instead) | -|     paddingRight | `number` | `none` | `false` | Right padding in points (deprecated; use root `padding` property instead) | -|     paddingTop | `number` | `none` | `false` | Top padding in points (deprecated; use root `padding` property instead) | -|     paddingBottom | `number` | `none` | `false` | Bottom padding in points (deprecated; use root `padding` property instead) | -|   onUserTrackingModeChange | `func` | `none` | `false` | Callback that is triggered on user tracking mode changes | -|   zoomLevel | `number` | `none` | `false` | Zoom level of the map | -| centerCoordinate | `array` | `none` | `false` | Center coordinate on map [lng, lat] | -| padding | `shape` | `none` | `false` | Padding around edges of map in points | -|   paddingLeft | `number` | `none` | `false` | Left padding in points | -|   paddingRight | `number` | `none` | `false` | Right padding in points | -|   paddingTop | `number` | `none` | `false` | Top padding in points | -|   paddingBottom | `number` | `none` | `false` | Bottom padding in points | -| heading | `number` | `none` | `false` | Heading on map | -| pitch | `number` | `none` | `false` | Pitch on map | -| bounds | `shape` | `none` | `false` | Represents a rectangle in geographical coordinates marking the visible area of the map.
The `bounds.padding*` properties are deprecated; use root `padding` property instead. | -|   ne | `array` | `none` | `true` | North east coordinate of bound | -|   sw | `array` | `none` | `true` | South west coordinate of bound | -|   paddingLeft | `number` | `none` | `false` | Left padding in points (deprecated; use root `padding` property instead) | -|   paddingRight | `number` | `none` | `false` | Right padding in points (deprecated; use root `padding` property instead) | -|   paddingTop | `number` | `none` | `false` | Top padding in points (deprecated; use root `padding` property instead) | -|   paddingBottom | `number` | `none` | `false` | Bottom padding in points (deprecated; use root `padding` property instead) | -| onUserTrackingModeChange | `func` | `none` | `false` | Callback that is triggered on user tracking mode changes | -| zoomLevel | `number` | `none` | `false` | Zoom level of the map | -| minZoomLevel | `number` | `none` | `false` | The minimum zoom level of the map | -| maxZoomLevel | `number` | `none` | `false` | The maximum zoom level of the map | -| maxBounds | `shape` | `none` | `false` | Restrict map panning so that the center is within these bounds | -|   ne | `array` | `none` | `true` | northEastCoordinates - North east coordinate of bound | -|   sw | `array` | `none` | `true` | southWestCoordinates - South west coordinate of bound | -| followUserLocation | `bool` | `none` | `false` | Should the map orientation follow the user's. | -| followUserMode | `enum` | `none` | `false` | The mode used to track the user location on the map. One of; "normal", "compass", "course". Each mode string is also available as a member on the `MapboxGL.UserTrackingModes` object. `Follow` (normal), `FollowWithHeading` (compass), `FollowWithCourse` (course). NOTE: `followUserLocation` must be set to `true` for any of the modes to take effect. [Example](../example/src/examples/SetUserTrackingModes.js) | -| followZoomLevel | `number` | `none` | `false` | The zoomLevel on map while followUserLocation is set to `true` | -| followPitch | `number` | `none` | `false` | The pitch on map while followUserLocation is set to `true` | -| followHeading | `number` | `none` | `false` | The heading on map while followUserLocation is set to `true` | -| triggerKey | `any` | `none` | `false` | Manually update the camera - helpful for when props did not update, however you still want the camera to move | - -### methods -#### fitBounds(northEastCoordinates, southWestCoordinates[, padding][, animationDuration]) - -Map camera transitions to fit provided bounds - -##### arguments -| Name | Type | Required | Description | -| ---- | :--: | :------: | :----------: | -| `northEastCoordinates` | `Array` | `Yes` | North east coordinate of bound | -| `southWestCoordinates` | `Array` | `Yes` | South west coordinate of bound | -| `padding` | `Number` | `No` | Camera padding for bound | -| `animationDuration` | `Number` | `No` | Duration of camera animation | - - - -```javascript -this.camera.fitBounds([lng, lat], [lng, lat]) -this.camera.fitBounds([lng, lat], [lng, lat], 20, 1000) // padding for all sides -this.camera.fitBounds([lng, lat], [lng, lat], [verticalPadding, horizontalPadding], 1000) -this.camera.fitBounds([lng, lat], [lng, lat], [top, right, bottom, left], 1000) -``` - - -#### flyTo(coordinates[, animationDuration]) - -Map camera will fly to new coordinate - -##### arguments -| Name | Type | Required | Description | -| ---- | :--: | :------: | :----------: | -| `coordinates` | `Array` | `Yes` | Coordinates that map camera will jump too | -| `animationDuration` | `Number` | `No` | Duration of camera animation | - - - -```javascript -this.camera.flyTo([lng, lat]) -this.camera.flyTo([lng, lat], 12000) -``` - - -#### moveTo(coordinates[, animationDuration]) - -Map camera will move to new coordinate at the same zoom level - -##### arguments -| Name | Type | Required | Description | -| ---- | :--: | :------: | :----------: | -| `coordinates` | `Array` | `Yes` | Coordinates that map camera will move too | -| `animationDuration` | `Number` | `No` | Duration of camera animation | - - - -```javascript -this.camera.moveTo([lng, lat], 200) // eases camera to new location based on duration -this.camera.moveTo([lng, lat]) // snaps camera to new location without any easing -``` - - -#### zoomTo(zoomLevel[, animationDuration]) - -Map camera will zoom to specified level - -##### arguments -| Name | Type | Required | Description | -| ---- | :--: | :------: | :----------: | -| `zoomLevel` | `Number` | `Yes` | Zoom level that the map camera will animate too | -| `animationDuration` | `Number` | `No` | Duration of camera animation | - - - -```javascript -this.camera.zoomTo(16) -this.camera.zoomTo(16, 100) -``` - - -#### setCamera(config) - -Map camera will perform updates based on provided config. Advanced use only! - -##### arguments -| Name | Type | Required | Description | -| ---- | :--: | :------: | :----------: | -| `config` | `Object` | `Yes` | Camera configuration | - - - -```javascript -this.camera.setCamera({ - centerCoordinate: [lng, lat], - zoomLevel: 16, - animationDuration: 2000, -}) - -this.camera.setCamera({ - stops: [ - { pitch: 45, animationDuration: 200 }, - { heading: 180, animationDuration: 300 }, - ] -}) -``` - +| type | `literal` | `none` | `false` | Allows static check of the data type. For internal use only. | +| centerCoordinate | `Position` | `none` | `false` | The location on which the map should center. | +| bounds | `intersection` | `none` | `false` | The corners of a box around which the map should bound. Contains padding props for backwards
compatibility; the root `padding` prop should be used instead. | +| heading | `number` | `none` | `false` | The heading (orientation) of the map. | +| pitch | `number` | `none` | `false` | The pitch of the map. | +| zoomLevel | `number` | `none` | `false` | The zoom level of the map. | +| padding | `{paddingLeft:number;paddingRight:number;paddingTop:number;paddingBottom:number;}` | `none` | `false` | The viewport padding in points. | +| animationDuration | `number` | `none` | `false` | The duration the map takes to animate to a new configuration. | +| animationMode | `\| 'flyTo' +\| 'easeTo' +\| 'linearTo' +\| 'moveTo' +\| 'none'` | `none` | `false` | The easing or path the camera uses to animate to a new configuration. | +| followUserMode | `UserTrackingMode` | `none` | `false` | The mode used to track the user location on the map. | +| followUserLocation | `boolean` | `none` | `false` | Whether the map orientation follows the user location. | +| followZoomLevel | `number` | `none` | `false` | The zoom level used when following the user location. | +| followPitch | `number` | `none` | `false` | The pitch used when following the user location. | +| followHeading | `number` | `none` | `false` | The heading used when following the user location. | +| minZoomLevel | `number` | `none` | `false` | The lowest allowed zoom level. | +| maxZoomLevel | `number` | `none` | `false` | The highest allowed zoom level. | +| maxBounds | `{ne:Position;sw:Position;}` | `none` | `false` | The corners of a box defining the limits of where the camera can pan or zoom. | +| defaultSettings | `FIX ME FORMAT BIG OBJECT` | `none` | `false` | The configuration that the camera falls back on, if no other values are specified. | +| allowUpdates | `boolean` | `none` | `false` | Whether the camera should send any configuration to the native module. Prevents unnecessary tile
fetching and improves performance when the map is not visible. Defaults to `true`. | +| triggerKey | `string \| number` | `none` | `false` | Any arbitrary primitive value that, when changed, causes the camera to retry moving to its target
configuration. (Not yet implemented.) | +| onUserTrackingModeChange | `UserTrackingModeChangeCallback` | `none` | `false` | Executes when user tracking mode changes. | diff --git a/docs/docs.json b/docs/docs.json index 82b503f12..27438db50 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -7,12 +7,12 @@ { "name": "style", "required": true, - "type": "FIX ME UNKNOWN TYPE", + "type": "AtmosphereLayerStyleProps", "default": "none", "description": "FIX ME NO DESCRIPTION" } ], - "fileName": "Atmosphere.tsx", + "fileNameWithExt": "Atmosphere.tsx", "name": "Atmosphere", "styles": [ { @@ -243,7 +243,7 @@ "composes": [ "../utils" ], - "fileName": "BackgroundLayer.js", + "fileNameWithExt": "BackgroundLayer.js", "name": "BackgroundLayer", "styles": [ { @@ -372,615 +372,163 @@ "composes": [ "../utils" ], - "fileName": "Callout.js", + "fileNameWithExt": "Callout.js", "name": "Callout" }, "Camera": { - "description": "", + "description": "Controls the perspective from which the user sees the map.\n\nTo use imperative methods, pass in a ref object:\n\n```\nconst camera = useRef(null);\n\nuseEffect(() => {\n camera.current?.setCamera({\n centerCoordinate: [lon, lat],\n });\n}, []);\n\nreturn (\n \n);\n```", "displayName": "Camera", - "methods": [ - { - "name": "fitBounds", - "docblock": "Map camera transitions to fit provided bounds\n\n@example\nthis.camera.fitBounds([lng, lat], [lng, lat])\nthis.camera.fitBounds([lng, lat], [lng, lat], 20, 1000) // padding for all sides\nthis.camera.fitBounds([lng, lat], [lng, lat], [verticalPadding, horizontalPadding], 1000)\nthis.camera.fitBounds([lng, lat], [lng, lat], [top, right, bottom, left], 1000)\n\n@param {Array} northEastCoordinates - North east coordinate of bound\n@param {Array} southWestCoordinates - South west coordinate of bound\n@param {Number=} padding - Camera padding for bound\n@param {Number=} animationDuration - Duration of camera animation\n@return {void}", - "modifiers": [], - "params": [ - { - "name": "northEastCoordinates", - "description": "North east coordinate of bound", - "type": { - "name": "Array", - "elements": [ - { - "name": "Number" - } - ] - }, - "optional": false - }, - { - "name": "southWestCoordinates", - "description": "South west coordinate of bound", - "type": { - "name": "Array", - "elements": [ - { - "name": "Number" - } - ] - }, - "optional": false - }, - { - "name": "padding", - "description": "Camera padding for bound", - "type": { - "name": "Number" - }, - "optional": true - }, - { - "name": "animationDuration", - "description": "Duration of camera animation", - "type": { - "name": "Number" - }, - "optional": true - } - ], - "returns": { - "description": null, - "type": { - "name": "void" - } - }, - "description": "Map camera transitions to fit provided bounds", - "examples": [ - "\nthis.camera.fitBounds([lng, lat], [lng, lat])\nthis.camera.fitBounds([lng, lat], [lng, lat], 20, 1000) // padding for all sides\nthis.camera.fitBounds([lng, lat], [lng, lat], [verticalPadding, horizontalPadding], 1000)\nthis.camera.fitBounds([lng, lat], [lng, lat], [top, right, bottom, left], 1000)\n\n" - ] - }, - { - "name": "flyTo", - "docblock": "Map camera will fly to new coordinate\n\n@example\nthis.camera.flyTo([lng, lat])\nthis.camera.flyTo([lng, lat], 12000)\n\n @param {Array} coordinates - Coordinates that map camera will jump too\n @param {Number=} animationDuration - Duration of camera animation\n @return {void}", - "modifiers": [], - "params": [ - { - "name": "coordinates", - "description": "Coordinates that map camera will jump too", - "type": { - "name": "Array", - "elements": [ - { - "name": "Number" - } - ] - }, - "optional": false - }, - { - "name": "animationDuration", - "description": "Duration of camera animation", - "type": { - "name": "Number" - }, - "optional": true - } - ], - "returns": { - "description": null, - "type": { - "name": "void" - } - }, - "description": "Map camera will fly to new coordinate", - "examples": [ - "\nthis.camera.flyTo([lng, lat])\nthis.camera.flyTo([lng, lat], 12000)\n\n " - ] - }, - { - "name": "moveTo", - "docblock": "Map camera will move to new coordinate at the same zoom level\n\n@example\nthis.camera.moveTo([lng, lat], 200) // eases camera to new location based on duration\nthis.camera.moveTo([lng, lat]) // snaps camera to new location without any easing\n\n @param {Array} coordinates - Coordinates that map camera will move too\n @param {Number=} animationDuration - Duration of camera animation\n @return {void}", - "modifiers": [], - "params": [ - { - "name": "coordinates", - "description": "Coordinates that map camera will move too", - "type": { - "name": "Array", - "elements": [ - { - "name": "Number" - } - ] - }, - "optional": false - }, - { - "name": "animationDuration", - "description": "Duration of camera animation", - "type": { - "name": "Number" - }, - "optional": true - } - ], - "returns": { - "description": null, - "type": { - "name": "void" - } - }, - "description": "Map camera will move to new coordinate at the same zoom level", - "examples": [ - "\nthis.camera.moveTo([lng, lat], 200) // eases camera to new location based on duration\nthis.camera.moveTo([lng, lat]) // snaps camera to new location without any easing\n\n " - ] - }, + "methods": [], + "props": [ { - "name": "zoomTo", - "docblock": "Map camera will zoom to specified level\n\n@example\nthis.camera.zoomTo(16)\nthis.camera.zoomTo(16, 100)\n\n@param {Number} zoomLevel - Zoom level that the map camera will animate too\n@param {Number=} animationDuration - Duration of camera animation\n@return {void}", - "modifiers": [], - "params": [ - { - "name": "zoomLevel", - "description": "Zoom level that the map camera will animate too", - "type": { - "name": "Number" - }, - "optional": false - }, - { - "name": "animationDuration", - "description": "Duration of camera animation", - "type": { - "name": "Number" - }, - "optional": true - } - ], - "returns": { - "description": null, - "type": { - "name": "void" - } - }, - "description": "Map camera will zoom to specified level", - "examples": [ - "\nthis.camera.zoomTo(16)\nthis.camera.zoomTo(16, 100)\n\n" - ] + "name": "type", + "required": false, + "type": "literal", + "default": "none", + "description": "Allows static check of the data type. For internal use only." }, { - "name": "setCamera", - "docblock": "Map camera will perform updates based on provided config. Advanced use only!\n\n@example\nthis.camera.setCamera({\n centerCoordinate: [lng, lat],\n zoomLevel: 16,\n animationDuration: 2000,\n})\n\nthis.camera.setCamera({\n stops: [\n { pitch: 45, animationDuration: 200 },\n { heading: 180, animationDuration: 300 },\n ]\n})\n\n @param {Object} config - Camera configuration", - "modifiers": [], - "params": [ - { - "name": "config", - "description": "Camera configuration", - "type": { - "name": "Object" - }, - "optional": false - } - ], - "returns": null, - "description": "Map camera will perform updates based on provided config. Advanced use only!", - "examples": [ - "\nthis.camera.setCamera({\n centerCoordinate: [lng, lat],\n zoomLevel: 16,\n animationDuration: 2000,\n})\n\nthis.camera.setCamera({\n stops: [\n { pitch: 45, animationDuration: 200 },\n { heading: 180, animationDuration: 300 },\n ]\n})\n\n " - ] - } - ], - "props": [ - { - "name": "allowUpdates", + "name": "centerCoordinate", "required": false, - "type": "bool", - "default": "true", - "description": "If false, the camera will not send any props to the native module. Intended to be used to prevent unnecessary tile fetching and improve performance when the map is not visible. Defaults to true." + "type": "Position", + "default": "none", + "description": "The location on which the map should center." }, { - "name": "animationDuration", + "name": "bounds", "required": false, - "type": "number", - "default": "2000", - "description": "The duration a camera update takes (in ms)" + "type": "intersection", + "default": "none", + "description": "The corners of a box around which the map should bound. Contains padding props for backwards\ncompatibility; the root `padding` prop should be used instead." }, { - "name": "animationMode", + "name": "heading", "required": false, - "type": "enum", - "default": "'easeTo'", - "description": "The animation style when the camara updates. One of:\n`flyTo`: A complex flight animation, affecting both position and zoom.\n`easeTo`: A standard damped curve.\n`linearTo`: An even linear transition.\n`none`: An instantaneous change (v10 only).\n`moveTo`: An instantaneous change ( { +const randPadding = (): CameraPadding => { const randNum = () => { const items = [0, 150, 300]; return items[Math.floor(Math.random() * items.length)]; @@ -72,64 +66,70 @@ const randPadding = () => { }; }; -const toPosition = (coordinate) => { +const toPosition = (coordinate: Coordinate): Position => { return [coordinate.longitude, coordinate.latitude]; }; -const CameraAnimation = (props) => { - const initialCoordinate = { - latitude: 40.759211, - longitude: -73.984638, - }; - - const [animationMode, setAnimationMode] = useState('moveTo'); - const [coordinates, setCoordinates] = useState([initialCoordinate]); - const [padding, setPadding] = useState(zeroPadding); +const CameraAnimation = memo((props: BaseExampleProps) => { + const [animationMode, setAnimationMode] = + useState('moveTo'); + const [coordinates, setCoordinates] = useState([ + initialCoordinate, + ]); + const [padding, setPadding] = useState(zeroPadding); const paddingDisplay = useMemo(() => { return `L ${padding.paddingLeft} | R ${padding.paddingRight} | T ${padding.paddingTop} | B ${padding.paddingBottom}`; }, [padding]); - const move = (_animationMode, shouldCreateMultiple) => { - setAnimationMode(_animationMode); + const move = useCallback( + (_animationMode: CameraAnimationMode, shouldCreateMultiple: boolean) => { + setAnimationMode(_animationMode); - if (shouldCreateMultiple) { - const _centerCoordinate = { - latitude: initialCoordinate.latitude + Math.random() * 0.2, - longitude: initialCoordinate.longitude + Math.random() * 0.2, - }; - const _coordinates = Array(10) - .fill(0) - .map((_) => { - return { - latitude: _centerCoordinate.latitude + Math.random() * 0.2, - longitude: _centerCoordinate.longitude + Math.random() * 0.2, - }; - }); - setCoordinates(_coordinates); - } else { - setCoordinates([ - { + if (shouldCreateMultiple) { + const _centerCoordinate = { latitude: initialCoordinate.latitude + Math.random() * 0.2, longitude: initialCoordinate.longitude + Math.random() * 0.2, - }, - ]); - } - }; + }; + const _coordinates = Array(10) + .fill(0) + .map((_) => { + return { + latitude: _centerCoordinate.latitude + Math.random() * 0.2, + longitude: _centerCoordinate.longitude + Math.random() * 0.2, + }; + }); + setCoordinates(_coordinates); + } else { + setCoordinates([ + { + latitude: initialCoordinate.latitude + Math.random() * 0.2, + longitude: initialCoordinate.longitude + Math.random() * 0.2, + }, + ]); + } + }, + [], + ); - const features = useMemo(() => { + const features = useMemo((): Feature[] => { return coordinates.map((p) => { - return { + const feature: Feature = { type: 'Feature', geometry: { type: 'Point', coordinates: toPosition(p), }, + properties: {}, }; + return feature; }); }, [coordinates]); - const centerOrBounds = useMemo(() => { + const centerOrBounds = useMemo((): { + centerCoordinate?: Position; + bounds?: CameraBounds; + } => { if (coordinates.length === 1) { return { centerCoordinate: toPosition(coordinates[0]), @@ -178,11 +178,11 @@ const CameraAnimation = (props) => { animationMode={animationMode} /> - {features.map((f) => { - const id = JSON.stringify(f.geometry.coordinates); + {features.map((feature) => { + const id = JSON.stringify(feature.geometry); return ( - - + + ); })} @@ -243,6 +243,30 @@ const CameraAnimation = (props) => { ); -}; +}); + +const styles = StyleSheet.create({ + map: { + flex: 1, + }, + sheet: { + paddingTop: 10, + paddingHorizontal: 10, + }, + content: { + padding: 10, + }, + buttonRow: { + flex: 0, + flexDirection: 'row', + justifyContent: 'space-around', + }, + divider: { + marginVertical: 10, + }, + fadedText: { + color: 'gray', + }, +}); export default CameraAnimation; diff --git a/example/src/examples/V10/TerrainSkyAtmosphere.tsx b/example/src/examples/V10/TerrainSkyAtmosphere.tsx index b0f71c1c1..45e089e38 100644 --- a/example/src/examples/V10/TerrainSkyAtmosphere.tsx +++ b/example/src/examples/V10/TerrainSkyAtmosphere.tsx @@ -4,11 +4,11 @@ import { Button } from 'react-native'; import { MapView, SkyLayer, - Camera, Logger, Terrain, RasterDemSource, Atmosphere, + Camera, } from '@rnmapbox/maps'; import Page from '../common/Page'; diff --git a/index.d.ts b/index.d.ts index b11e94099..2f5a404f8 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,6 +1,6 @@ declare module 'react-native-mapbox-gl__maps'; -import { Component, ReactNode, SyntheticEvent } from 'react'; +import { Component, ReactNode } from 'react'; import { ViewProps, ViewStyle, @@ -22,7 +22,23 @@ import { FeatureCollection, } from '@turf/helpers'; -import type _Atmosphere from './javascript/components/Atmosphere'; +import { + Camera as _Camera, + CameraStop as _CameraStop, + CameraFollowConfig as _CameraFollowConfig, + CameraMinMaxConfig as _CameraMinMaxConfig, + CameraBounds as _CameraBounds, + CameraPadding as _CameraPadding, + CameraBoundsWithPadding as _CameraBoundsWithPadding, + CameraStops as _CameraStops, + CameraAnimationMode as _CameraAnimationMode, +} from './javascript/components/Camera'; +import { Atmosphere as _Atmosphere } from './javascript/components/Atmosphere'; +import type { + MapboxGLEvent as _MapboxGLEvent, + UserTrackingMode as _UserTrackingMode, + UserTrackingModeChangeCallback as _UserTrackingModeChangeCallback, +} from './javascript/types/index'; import type { requestAndroidLocationPermissions as _requestAndroidLocationPermissions } from './javascript/requestAndroidLocationPermissions'; // prettier-ignore @@ -85,12 +101,6 @@ type NamedStyles = { | BackgroundLayerStyle; }; -export type MapboxGLEvent< - T extends string, - P = GeoJSON.Feature, - V = Element, -> = SyntheticEvent; - export type OnPressEvent = { features: Array; coordinates: { @@ -113,8 +123,24 @@ declare namespace MapboxGL { function setConnected(connected: boolean): void; const requestAndroidLocationPermissions = _requestAndroidLocationPermissions; + + const Camera = _Camera; + type Camera = _Camera; + type CameraStop = _CameraStop; + type CameraFollowConfig = _CameraFollowConfig; + type CameraMinMaxConfig = _CameraMinMaxConfig; + type CameraBounds = _CameraBounds; + type CameraPadding = _CameraPadding; + type CameraBoundsWithPadding = _CameraBoundsWithPadding; + type CameraStops = _CameraStops; + type CameraAnimationMode = _CameraAnimationMode; + const Atmosphere = _Atmosphere; + type MapboxGLEvent = _MapboxGLEvent; + type UserTrackingMode = _UserTrackingMode; + type UserTrackingModeChangeCallback = _UserTrackingModeChangeCallback; + const offlineManager: OfflineManager; const snapshotManager: SnapshotManager; const locationManager: LocationManager; @@ -246,18 +272,6 @@ declare namespace MapboxGL { } type Padding = number | [number, number] | [number, number, number, number]; - export class Camera extends Component { - fitBounds( - northEastCoordinates: GeoJSON.Position, - southWestCoordinates: GeoJSON.Position, - padding?: Padding, - duration?: number, - ): void; - flyTo(coordinates: GeoJSON.Position, duration?: number): void; - moveTo(coordinates: GeoJSON.Position, duration?: number): void; - zoomTo(zoomLevel: number, duration?: number): void; - setCamera(config: CameraSettings): void; - } class UserLocation extends Component {} @@ -581,54 +595,6 @@ export interface MapViewProps extends ViewProps { onUserTrackingModeChange?: () => void; } -export interface CameraProps extends CameraSettings, ViewProps { - allowUpdates?: boolean; - animationDuration?: number; - animationMode?: 'flyTo' | 'easeTo' | 'linearTo' | 'moveTo' | 'none'; - defaultSettings?: CameraSettings; - minZoomLevel?: number; - maxZoomLevel?: number; - maxBounds?: { ne: [number, number]; sw: [number, number] }; - followUserLocation?: boolean; - followUserMode?: 'normal' | 'compass' | 'course'; - followZoomLevel?: number; - followPitch?: number; - followHeading?: number; - triggerKey?: any; - alignment?: number[]; - onUserTrackingModeChange?: ( - event: MapboxGLEvent< - 'usertrackingmodechange', - { - followUserLocation: boolean; - followUserMode: 'normal' | 'compass' | 'course' | null; - } - >, - ) => void; -} - -export interface CameraPadding { - paddingLeft?: number; - paddingRight?: number; - paddingTop?: number; - paddingBottom?: number; -} - -export interface CameraSettings { - centerCoordinate?: GeoJSON.Position; - heading?: number; - pitch?: number; - padding?: CameraPadding; - bounds?: CameraPadding & { - ne: GeoJSON.Position; - sw: GeoJSON.Position; - }; - zoomLevel?: number; - animationDuration?: number; - animationMode?: 'flyTo' | 'easeTo' | 'linearTo' | 'moveTo'; - stops?: CameraSettings[]; -} - export interface UserLocationProps { androidRenderMode?: 'normal' | 'compass' | 'gps'; animated?: boolean; @@ -1057,10 +1023,26 @@ export class Logger { } export import MapView = MapboxGL.MapView; + export import Camera = MapboxGL.Camera; +export import CameraStop = MapboxGL.CameraStop; +export import CameraFollowConfig = MapboxGL.CameraFollowConfig; +export import CameraMinMaxConfig = MapboxGL.CameraMinMaxConfig; +export import CameraBounds = MapboxGL.CameraBounds; +export import CameraPadding = MapboxGL.CameraPadding; +export import CameraBoundsWithPadding = MapboxGL.CameraBoundsWithPadding; +export import CameraStops = MapboxGL.CameraStops; +export import CameraAnimationMode = MapboxGL.CameraAnimationMode; + +export import Atmosphere = MapboxGL.Atmosphere; export import Terrain = MapboxGL.Terrain; export import RasterDemSource = MapboxGL.RasterDemSource; export import SkyLayer = MapboxGL.SkyLayer; -export import Atmosphere = MapboxGL.Atmosphere; +export import ShapeSource = MapboxGL.ShapeSource; +export import CircleLayer = MapboxGL.CircleLayer; + +export import MapboxGLEvent = MapboxGL.MapboxGLEvent; +export import UserTrackingMode = MapboxGL.UserTrackingMode; +export import UserTrackingModeChangeCallback = MapboxGL.UserTrackingModeChangeCallback; export default MapboxGL; diff --git a/javascript/components/Atmosphere.tsx b/javascript/components/Atmosphere.tsx index ff92d3839..dd1cbc28a 100644 --- a/javascript/components/Atmosphere.tsx +++ b/javascript/components/Atmosphere.tsx @@ -10,7 +10,7 @@ type Props = { style: AtmosphereLayerStyleProps; }; -const Atmosphere = memo((props: Props) => { +export const Atmosphere = memo((props: Props) => { const baseProps = useMemo(() => { return { ...props, @@ -26,5 +26,3 @@ const RCTMGLAtmosphere: HostComponent<{ reactStyle?: { [key: string]: StyleValue }; style?: undefined; }> = requireNativeComponent(NATIVE_MODULE_NAME); - -export default Atmosphere; diff --git a/javascript/components/Camera.js b/javascript/components/Camera.js deleted file mode 100644 index f1596be8d..000000000 --- a/javascript/components/Camera.js +++ /dev/null @@ -1,674 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { NativeModules, requireNativeComponent } from 'react-native'; - -import { toJSONString, viewPropTypes, existenceChange } from '../utils'; -import * as geoUtils from '../utils/geoUtils'; - -const MapboxGL = NativeModules.MGLModule; - -export const NATIVE_MODULE_NAME = 'RCTMGLCamera'; - -const SettingsPropTypes = { - /** - * Center coordinate on map [lng, lat] - */ - centerCoordinate: PropTypes.arrayOf(PropTypes.number), - - /** - * Padding around edges of map in points - */ - padding: PropTypes.shape({ - /** - * Left padding in points - */ - paddingLeft: PropTypes.number, - - /** - * Right padding in points - */ - paddingRight: PropTypes.number, - - /** - * Top padding in points - */ - paddingTop: PropTypes.number, - - /** - * Bottom padding in points - */ - paddingBottom: PropTypes.number, - }), - - /** - * Heading on map - */ - heading: PropTypes.number, - - /** - * Pitch on map - */ - pitch: PropTypes.number, - - /** - * Represents a rectangle in geographical coordinates marking the visible area of the map. - * The `bounds.padding*` properties are deprecated; use root `padding` property instead. - */ - bounds: PropTypes.shape({ - /** - * North east coordinate of bound - */ - ne: PropTypes.arrayOf(PropTypes.number).isRequired, - - /** - * South west coordinate of bound - */ - sw: PropTypes.arrayOf(PropTypes.number).isRequired, - - /** - * Left padding in points (deprecated; use root `padding` property instead) - */ - paddingLeft: PropTypes.number, - - /** - * Right padding in points (deprecated; use root `padding` property instead) - */ - paddingRight: PropTypes.number, - - /** - * Top padding in points (deprecated; use root `padding` property instead) - */ - paddingTop: PropTypes.number, - - /** - * Bottom padding in points (deprecated; use root `padding` property instead) - */ - paddingBottom: PropTypes.number, - }), - - /** - * Callback that is triggered on user tracking mode changes - */ - onUserTrackingModeChange: PropTypes.func, - - /** - * Zoom level of the map - */ - zoomLevel: PropTypes.number, -}; - -class Camera extends React.Component { - static propTypes = { - ...viewPropTypes, - - /** - * If false, the camera will not send any props to the native module. Intended to be used to prevent unnecessary tile fetching and improve performance when the map is not visible. Defaults to true. - */ - allowUpdates: PropTypes.bool, - - /** - * The duration a camera update takes (in ms) - */ - animationDuration: PropTypes.number, - - /** - * The animation style when the camara updates. One of: - * `flyTo`: A complex flight animation, affecting both position and zoom. - * `easeTo`: A standard damped curve. - * `linearTo`: An even linear transition. - * `none`: An instantaneous change (v10 only). - * `moveTo`: An instantaneous change (} northEastCoordinates - North east coordinate of bound - * @param {Array} southWestCoordinates - South west coordinate of bound - * @param {Number=} padding - Camera padding for bound - * @param {Number=} animationDuration - Duration of camera animation - * @return {void} - */ - fitBounds( - northEastCoordinates, - southWestCoordinates, - padding = 0, - animationDuration = 0.0, - ) { - const pad = { - paddingLeft: 0, - paddingRight: 0, - paddingTop: 0, - paddingBottom: 0, - }; - - if (Array.isArray(padding)) { - if (padding.length === 2) { - pad.paddingTop = padding[0]; - pad.paddingBottom = padding[0]; - pad.paddingLeft = padding[1]; - pad.paddingRight = padding[1]; - } else if (padding.length === 4) { - pad.paddingTop = padding[0]; - pad.paddingRight = padding[1]; - pad.paddingBottom = padding[2]; - pad.paddingLeft = padding[3]; - } - } else { - pad.paddingLeft = padding; - pad.paddingRight = padding; - pad.paddingTop = padding; - pad.paddingBottom = padding; - } - - return this.setCamera({ - bounds: { - ne: northEastCoordinates, - sw: southWestCoordinates, - }, - padding: pad, - animationDuration, - animationMode: Camera.Mode.Ease, - }); - } - - /** - * Map camera will fly to new coordinate - * - * @example - * this.camera.flyTo([lng, lat]) - * this.camera.flyTo([lng, lat], 12000) - * - * @param {Array} coordinates - Coordinates that map camera will jump too - * @param {Number=} animationDuration - Duration of camera animation - * @return {void} - */ - flyTo(coordinates, animationDuration = 2000) { - return this.setCamera({ - centerCoordinate: coordinates, - animationDuration, - animationMode: Camera.Mode.Flight, - }); - } - - /** - * Map camera will move to new coordinate at the same zoom level - * - * @example - * this.camera.moveTo([lng, lat], 200) // eases camera to new location based on duration - * this.camera.moveTo([lng, lat]) // snaps camera to new location without any easing - * - * @param {Array} coordinates - Coordinates that map camera will move too - * @param {Number=} animationDuration - Duration of camera animation - * @return {void} - */ - moveTo(coordinates, animationDuration = 0) { - return this.setCamera({ - centerCoordinate: coordinates, - animationDuration, - }); - } - - /** - * Map camera will zoom to specified level - * - * @example - * this.camera.zoomTo(16) - * this.camera.zoomTo(16, 100) - * - * @param {Number} zoomLevel - Zoom level that the map camera will animate too - * @param {Number=} animationDuration - Duration of camera animation - * @return {void} - */ - zoomTo(zoomLevel, animationDuration = 2000) { - return this.setCamera({ - zoomLevel, - animationDuration, - animationMode: Camera.Mode.Flight, - }); - } - - /** - * Map camera will perform updates based on provided config. Advanced use only! - * - * @example - * this.camera.setCamera({ - * centerCoordinate: [lng, lat], - * zoomLevel: 16, - * animationDuration: 2000, - * }) - * - * this.camera.setCamera({ - * stops: [ - * { pitch: 45, animationDuration: 200 }, - * { heading: 180, animationDuration: 300 }, - * ] - * }) - * - * @param {Object} config - Camera configuration - */ - setCamera(config = {}) { - this._setCamera(config); - } - - _setCamera(config = {}) { - let cameraConfig = {}; - - if (config.stops) { - cameraConfig.stops = []; - - for (const stop of config.stops) { - cameraConfig.stops.push(this._createStopConfig(stop)); - } - } else { - cameraConfig = this._createStopConfig(config); - } - - this.refs.camera.setNativeProps({ stop: cameraConfig }); - } - - _createDefaultCamera() { - if (this.defaultCamera) { - return this.defaultCamera; - } - if (!this.props.defaultSettings) { - return null; - } - - this.defaultCamera = this._createStopConfig( - { - ...this.props.defaultSettings, - animationMode: Camera.Mode.Move, - }, - true, - ); - return this.defaultCamera; - } - - _createStopConfig(config = {}, ignoreFollowUserLocation = false) { - if (this.props.followUserLocation && !ignoreFollowUserLocation) { - return null; - } - - const stopConfig = { - mode: this._getNativeCameraMode(config), - pitch: config.pitch, - heading: config.heading, - duration: config.animationDuration || 0, - zoom: config.zoomLevel, - }; - - if (config.centerCoordinate) { - stopConfig.centerCoordinate = toJSONString( - geoUtils.makePoint(config.centerCoordinate), - ); - } - - if (config.bounds && config.bounds.ne && config.bounds.sw) { - const { ne, sw } = config.bounds; - stopConfig.bounds = toJSONString(geoUtils.makeLatLngBounds(ne, sw)); - } - - stopConfig.paddingTop = - config.padding?.paddingTop || config.bounds?.paddingTop || 0; - stopConfig.paddingRight = - config.padding?.paddingRight || config.bounds?.paddingRight || 0; - stopConfig.paddingBottom = - config.padding?.paddingBottom || config.bounds?.paddingBottom || 0; - stopConfig.paddingLeft = - config.padding?.paddingLeft || config.bounds?.paddingLeft || 0; - - return stopConfig; - } - - _getNativeCameraMode(config) { - switch (config.animationMode) { - case Camera.Mode.Flight: - return MapboxGL.CameraModes.Flight; - case Camera.Mode.Ease: - return MapboxGL.CameraModes.Ease; - case Camera.Mode.Linear: - return MapboxGL.CameraModes.Linear; - case Camera.Mode.None: - return MapboxGL.CameraModes.None; - case Camera.Mode.Move: - return MapboxGL.CameraModes.Move; - default: - return MapboxGL.CameraModes.Ease; - } - } - - _getMaxBounds() { - const bounds = this.props.maxBounds; - if (!bounds || !bounds.ne || !bounds.sw) { - return null; - } - return toJSONString(geoUtils.makeLatLngBounds(bounds.ne, bounds.sw)); - } - - render() { - const props = Object.assign({}, this.props); - - const callbacks = { - onUserTrackingModeChange: props.onUserTrackingModeChange, - }; - - return ( - - ); - } -} - -const RCTMGLCamera = requireNativeComponent(NATIVE_MODULE_NAME, Camera, { - nativeOnly: { - stop: true, - }, -}); - -Camera.UserTrackingModes = { - Follow: 'normal', - FollowWithHeading: 'compass', - FollowWithCourse: 'course', -}; - -export default Camera; diff --git a/javascript/components/Camera.tsx b/javascript/components/Camera.tsx new file mode 100644 index 000000000..659b515ac --- /dev/null +++ b/javascript/components/Camera.tsx @@ -0,0 +1,535 @@ +import React, { + forwardRef, + memo, + useCallback, + useImperativeHandle, + useMemo, + useRef, +} from 'react'; +import { NativeModules, requireNativeComponent } from 'react-native'; +import { Position } from '@turf/helpers'; + +import { UserTrackingModeChangeCallback, UserTrackingMode } from '../types'; +import { makeLatLngBounds, makePoint } from '../utils/geoUtils'; + +const NativeModule = NativeModules.MGLModule; + +/** + * Converts the provided React Native animation mode into the corresponding native enum value. + */ +const nativeAnimationMode = ( + mode?: CameraAnimationMode, +): NativeAnimationMode => { + const NativeCameraModes = NativeModule.CameraModes; + + switch (mode) { + case 'flyTo': + return NativeCameraModes.Flight; + case 'easeTo': + return NativeCameraModes.Ease; + case 'linearTo': + return NativeCameraModes.Linear; + case 'moveTo': + return NativeCameraModes.Move; + case 'none': + return NativeCameraModes.None; + default: + return NativeCameraModes.Ease; + } +}; + +export const NATIVE_MODULE_NAME = 'RCTMGLCamera'; + +// Native module types. + +type NativeAnimationMode = 'flight' | 'ease' | 'linear' | 'none' | 'move'; + +interface NativeCameraProps extends CameraFollowConfig { + testID?: string; + stop: NativeCameraStop | null; + defaultStop?: NativeCameraStop | null; + minZoomLevel?: number; + maxZoomLevel?: number; + maxBounds?: string | null; + onUserTrackingModeChange?: UserTrackingModeChangeCallback; +} + +interface NativeCameraStop { + centerCoordinate?: string; + bounds?: string; + heading?: number; + pitch?: number; + zoom?: number; + paddingLeft?: number; + paddingRight?: number; + paddingTop?: number; + paddingBottom?: number; + duration?: number; + mode?: NativeAnimationMode; +} + +export interface CameraRef { + setCamera: (config: CameraStop | CameraStops) => void; + fitBounds: ( + ne: Position, + sw: Position, + paddingConfig?: number | number[], + animationDuration?: number, + ) => void; + flyTo: (centerCoordinate: Position, animationDuration?: number) => void; + moveTo: (centerCoordinate: Position, animationDuration?: number) => void; + zoomTo: (zoomLevel: number, animationDuration?: number) => void; +} + +export type CameraStop = { + /** Allows static check of the data type. For internal use only. */ + readonly type?: 'CameraStop'; + /** The location on which the map should center. */ + centerCoordinate?: Position; + /** The corners of a box around which the map should bound. Contains padding props for backwards + * compatibility; the root `padding` prop should be used instead. */ + bounds?: CameraBoundsWithPadding; + /** The heading (orientation) of the map. */ + heading?: number; + /** The pitch of the map. */ + pitch?: number; + /** The zoom level of the map. */ + zoomLevel?: number; + /** The viewport padding in points. */ + padding?: CameraPadding; + /** The duration the map takes to animate to a new configuration. */ + animationDuration?: number; + /** The easing or path the camera uses to animate to a new configuration. */ + animationMode?: CameraAnimationMode; +}; + +export type CameraFollowConfig = { + /** The mode used to track the user location on the map. */ + followUserMode?: UserTrackingMode; + /** Whether the map orientation follows the user location. */ + followUserLocation?: boolean; + /** The zoom level used when following the user location. */ + followZoomLevel?: number; + /** The pitch used when following the user location. */ + followPitch?: number; + /** The heading used when following the user location. */ + followHeading?: number; +}; + +export type CameraMinMaxConfig = { + /** The lowest allowed zoom level. */ + minZoomLevel?: number; + /** The highest allowed zoom level. */ + maxZoomLevel?: number; + /** The corners of a box defining the limits of where the camera can pan or zoom. */ + maxBounds?: { + ne: Position; + sw: Position; + }; +}; + +export interface CameraProps + extends CameraStop, + CameraFollowConfig, + CameraMinMaxConfig { + /** The configuration that the camera falls back on, if no other values are specified. */ + defaultSettings?: CameraStop; + /** Whether the camera should send any configuration to the native module. Prevents unnecessary tile + * fetching and improves performance when the map is not visible. Defaults to `true`. */ + allowUpdates?: boolean; + /** Any arbitrary primitive value that, when changed, causes the camera to retry moving to its target + * configuration. (Not yet implemented.) */ + triggerKey?: string | number; + /** Executes when user tracking mode changes. */ + onUserTrackingModeChange?: UserTrackingModeChangeCallback; +} + +export type CameraBounds = { + ne: Position; + sw: Position; +}; + +export type CameraPadding = { + paddingLeft: number; + paddingRight: number; + paddingTop: number; + paddingBottom: number; +}; + +export type CameraBoundsWithPadding = Partial & CameraBounds; + +export type CameraStops = { + /** Allows static check of the data type. For internal use only. */ + readonly type: 'CameraStops'; + stops: CameraStop[]; +}; + +export type CameraAnimationMode = + | 'flyTo' + | 'easeTo' + | 'linearTo' + | 'moveTo' + | 'none'; + +/** + * Controls the perspective from which the user sees the map. + * + * To use imperative methods, pass in a ref object: + * + * ``` + * const camera = useRef(null); + * + * useEffect(() => { + * camera.current?.setCamera({ + * centerCoordinate: [lon, lat], + * }); + * }, []); + * + * return ( + * + * ); + * ``` + */ +export const Camera = memo( + forwardRef( + (props: CameraProps, ref: React.ForwardedRef) => { + const { + centerCoordinate, + bounds, + heading, + pitch, + zoomLevel, + padding, + animationDuration, + animationMode, + minZoomLevel, + maxZoomLevel, + maxBounds, + followUserLocation, + followUserMode, + followZoomLevel, + followPitch, + followHeading, + defaultSettings, + allowUpdates = true, + triggerKey, + onUserTrackingModeChange, + } = props; + + // @ts-expect-error This avoids a type/value mismatch. + const nativeCamera = useRef(null); + + const nativeDefaultStop = useMemo((): NativeCameraStop | null => { + if (!defaultSettings) { + return null; + } + const _defaultStop: NativeCameraStop = { + centerCoordinate: JSON.stringify(defaultSettings.centerCoordinate), + bounds: JSON.stringify(defaultSettings.bounds), + heading: defaultSettings.heading ?? 0, + pitch: defaultSettings.pitch ?? 0, + zoom: defaultSettings.zoomLevel ?? 11, + paddingTop: defaultSettings.padding?.paddingTop ?? 0, + paddingBottom: defaultSettings.padding?.paddingBottom ?? 0, + paddingLeft: defaultSettings.padding?.paddingLeft ?? 0, + paddingRight: defaultSettings.padding?.paddingRight ?? 0, + duration: defaultSettings.animationDuration ?? 2000, + mode: nativeAnimationMode(defaultSettings.animationMode), + }; + return _defaultStop; + }, [defaultSettings]); + + const buildNativeStop = useCallback( + ( + stop: CameraStop, + ignoreFollowUserLocation = false, + ): NativeCameraStop | null => { + stop = { + ...stop, + type: 'CameraStop', + }; + + if (props.followUserLocation && !ignoreFollowUserLocation) { + return null; + } + + const _nativeStop: NativeCameraStop = { ...nativeDefaultStop }; + + if (stop.pitch !== undefined) _nativeStop.pitch = stop.pitch; + if (stop.heading !== undefined) _nativeStop.heading = stop.heading; + if (stop.zoomLevel !== undefined) _nativeStop.zoom = stop.zoomLevel; + if (stop.animationMode !== undefined) + _nativeStop.mode = nativeAnimationMode(stop.animationMode); + if (stop.animationDuration !== undefined) + _nativeStop.duration = stop.animationDuration; + + if (stop.centerCoordinate) { + _nativeStop.centerCoordinate = JSON.stringify( + makePoint(stop.centerCoordinate), + ); + } + + if (stop.bounds && stop.bounds.ne && stop.bounds.sw) { + const { ne, sw } = stop.bounds; + _nativeStop.bounds = JSON.stringify(makeLatLngBounds(ne, sw)); + } + + _nativeStop.paddingTop = + stop.padding?.paddingTop ?? stop.bounds?.paddingTop ?? 0; + _nativeStop.paddingRight = + stop.padding?.paddingRight ?? stop.bounds?.paddingRight ?? 0; + _nativeStop.paddingBottom = + stop.padding?.paddingBottom ?? stop.bounds?.paddingBottom ?? 0; + _nativeStop.paddingLeft = + stop.padding?.paddingLeft ?? stop.bounds?.paddingLeft ?? 0; + + return _nativeStop; + }, + [props.followUserLocation, nativeDefaultStop], + ); + + const nativeStop = useMemo(() => { + return buildNativeStop({ + type: 'CameraStop', + centerCoordinate, + bounds, + heading, + pitch, + zoomLevel, + padding, + animationDuration, + animationMode, + }); + }, [ + centerCoordinate, + bounds, + heading, + pitch, + zoomLevel, + padding, + animationDuration, + animationMode, + buildNativeStop, + ]); + + const nativeMaxBounds = useMemo(() => { + if (!maxBounds?.ne || !maxBounds?.sw) { + return null; + } + return JSON.stringify(makeLatLngBounds(maxBounds.ne, maxBounds.sw)); + }, [maxBounds]); + + const _setCamera: CameraRef['setCamera'] = (config) => { + if (!allowUpdates) { + return; + } + + if (!config.type) + // @ts-expect-error The compiler doesn't understand that the `config` union type is guaranteed + // to be an object type. + config = { + ...config, + // @ts-expect-error Allows JS files to pass in an invalid config (lacking the `type` property), + // which would raise a compilation error in TS files. + type: config.stops ? 'CameraStops' : 'CameraStop', + }; + + if (config.type === 'CameraStops') { + for (const _stop of config.stops) { + let _nativeStops: NativeCameraStop[] = []; + const _nativeStop = buildNativeStop(_stop); + if (_nativeStop) { + _nativeStops = [..._nativeStops, _nativeStop]; + } + nativeCamera.current.setNativeProps({ + stop: { stops: _nativeStops }, + }); + } + } else if (config.type === 'CameraStop') { + const _nativeStop = buildNativeStop(config); + if (_nativeStop) { + nativeCamera.current.setNativeProps({ stop: _nativeStop }); + } + } + }; + const setCamera = useCallback(_setCamera, [ + allowUpdates, + buildNativeStop, + ]); + + const _fitBounds: CameraRef['fitBounds'] = ( + ne, + sw, + paddingConfig = 0, + _animationDuration = 0, + ) => { + let _padding = { + paddingTop: 0, + paddingBottom: 0, + paddingLeft: 0, + paddingRight: 0, + }; + + if (typeof paddingConfig === 'object') { + if (paddingConfig.length === 2) { + _padding = { + paddingTop: paddingConfig[0], + paddingBottom: paddingConfig[0], + paddingLeft: paddingConfig[1], + paddingRight: paddingConfig[1], + }; + } else if (paddingConfig.length === 4) { + _padding = { + paddingTop: paddingConfig[0], + paddingBottom: paddingConfig[2], + paddingLeft: paddingConfig[3], + paddingRight: paddingConfig[1], + }; + } + } else if (typeof paddingConfig === 'number') { + _padding = { + paddingTop: paddingConfig, + paddingBottom: paddingConfig, + paddingLeft: paddingConfig, + paddingRight: paddingConfig, + }; + } + + setCamera({ + type: 'CameraStop', + bounds: { + ne, + sw, + }, + padding: _padding, + animationDuration: _animationDuration, + animationMode: 'easeTo', + }); + }; + const fitBounds = useCallback(_fitBounds, [setCamera]); + + const _flyTo: CameraRef['flyTo'] = ( + _centerCoordinate, + _animationDuration = 2000, + ) => { + setCamera({ + type: 'CameraStop', + centerCoordinate: _centerCoordinate, + animationDuration: _animationDuration, + }); + }; + const flyTo = useCallback(_flyTo, [setCamera]); + + const _moveTo: CameraRef['moveTo'] = ( + _centerCoordinate, + _animationDuration = 0, + ) => { + setCamera({ + type: 'CameraStop', + centerCoordinate: _centerCoordinate, + animationDuration: _animationDuration, + animationMode: 'easeTo', + }); + }; + const moveTo = useCallback(_moveTo, [setCamera]); + + const _zoomTo: CameraRef['zoomTo'] = ( + _zoomLevel, + _animationDuration = 2000, + ) => { + setCamera({ + type: 'CameraStop', + zoomLevel: _zoomLevel, + animationDuration: _animationDuration, + animationMode: 'flyTo', + }); + }; + const zoomTo = useCallback(_zoomTo, [setCamera]); + + useImperativeHandle(ref, () => ({ + /** + * Sets any camera properties, with default fallbacks if unspecified. + * + * @example + * camera.current?.setCamera({ + * centerCoordinate: [lon, lat], + * }); + * + * @param {CameraStop | CameraStops} config + */ + setCamera, + /** + * Set the camera position to enclose the provided bounds, with optional + * padding and duration. + * + * @example + * camera.fitBounds([lon, lat], [lon, lat]); + * camera.fitBounds([lon, lat], [lon, lat], [20, 0], 1000); + * + * @param {Position} ne Northeast coordinate of bounding box + * @param {Position} sw Southwest coordinate of bounding box + * @param {number | number[]} paddingConfig The viewport padding, specified as a number (all sides equal), a 2-item array ([vertical, horizontal]), or a 4-item array ([top, right, bottom, left]) + * @param {number} animationDuration The transition duration + */ + fitBounds, + /** + * Sets the camera to center around the provided coordinate using a realistic 'travel' + * animation, with optional duration. + * + * @example + * camera.flyTo([lon, lat]); + * camera.flyTo([lon, lat], 12000); + * + * @param {Position} centerCoordinate The coordinate to center in the view + * @param {number} animationDuration The transition duration + */ + flyTo, + /** + * Sets the camera to center around the provided coordinate, with optional duration. + * + * @example + * camera.moveTo([lon, lat], 200); + * camera.moveTo([lon, lat]); + * + * @param {Position} centerCoordinate The coordinate to center in the view + * @param {number} animationDuration The transition duration + */ + moveTo, + /** + * Zooms the camera to the provided level, with optional duration. + * + * @example + * camera.zoomTo(16); + * camera.zoomTo(16, 100); + * + * @param {number} zoomLevel The target zoom + * @param {number} animationDuration The transition duration + */ + zoomTo, + })); + + return ( + + ); + }, + ), +); + +const RCTMGLCamera = + requireNativeComponent(NATIVE_MODULE_NAME); + +export type Camera = CameraRef; diff --git a/javascript/index.js b/javascript/index.js index 7c8bdef94..00406a90e 100644 --- a/javascript/index.js +++ b/javascript/index.js @@ -1,12 +1,13 @@ import { NativeModules } from 'react-native'; +import { Camera } from './components/Camera'; +import { Atmosphere } from './components/Atmosphere'; import MapView from './components/MapView'; import Light from './components/Light'; import PointAnnotation from './components/PointAnnotation'; import Annotation from './components/annotations/Annotation'; import Callout from './components/Callout'; import UserLocation from './components/UserLocation'; -import Camera from './components/Camera'; import VectorSource from './components/VectorSource'; import ShapeSource from './components/ShapeSource'; import RasterSource from './components/RasterSource'; @@ -23,7 +24,6 @@ import SymbolLayer from './components/SymbolLayer'; import RasterLayer from './components/RasterLayer'; import BackgroundLayer from './components/BackgroundLayer'; import Terrain from './components/Terrain'; -import Atmosphere from './components/Atmosphere'; import locationManager from './modules/location/locationManager'; import offlineManager from './modules/offline/offlineManager'; import snapshotManager from './modules/snapshot/snapshotManager'; diff --git a/javascript/types/index.ts b/javascript/types/index.ts new file mode 100644 index 000000000..1673e2e75 --- /dev/null +++ b/javascript/types/index.ts @@ -0,0 +1,23 @@ +import { SyntheticEvent } from 'react'; + +// General. + +export type MapboxGLEvent< + T extends string, + P = GeoJSON.Feature, + V = Element, +> = SyntheticEvent; + +// Camera. + +export type UserTrackingMode = 'normal' | 'compass' | 'course'; + +export type UserTrackingModeChangeCallback = ( + event: MapboxGLEvent< + 'usertrackingmodechange', + { + followUserLocation: boolean; + followUserMode: UserTrackingMode | null; + } + >, +) => void; diff --git a/package.json b/package.json index 59f274630..3dd71aad2 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "ejs-lint": "^1.1.0", "eslint": "^7.32.0", "eslint-config-prettier": "^8.5.0", + "eslint-plugin-flowtype": "^8.0.3", "eslint-plugin-fp": "^2.3.0", "eslint-plugin-import": "2.25.3", "expo-module-scripts": "^2.0.0", @@ -77,7 +78,7 @@ "node-dir": "0.1.17", "prettier": "2.6.2", "react": "17.0.2", - "react-docgen": "^5.0.0-beta.1", + "react-docgen": "6.0.0-alpha.3", "react-native": "0.67.0", "react-test-renderer": "17.0.2", "typescript": "^4.4.3" @@ -105,4 +106,4 @@ "lint-staged": { "*.{js,jsx,ts,tsx}": "eslint --fix" } -} +} \ No newline at end of file diff --git a/scripts/autogenHelpers/DocJSONBuilder.js b/scripts/autogenHelpers/DocJSONBuilder.js index efc97518b..6b497d8fb 100644 --- a/scripts/autogenHelpers/DocJSONBuilder.js +++ b/scripts/autogenHelpers/DocJSONBuilder.js @@ -26,24 +26,26 @@ const IGNORE_FILES = [ const IGNORE_METHODS = ['setNativeProps']; +const fileExtensionsRegex = /.(js|tsx|(? { return { value, doc: prop.doc.values[value].doc }; @@ -109,13 +116,42 @@ class DocJSONBuilder { return result; } + function tsTypeDesc(tsType) { + if (!tsType?.name) { + return null; + } + + if (tsType.name === 'signature') { + if (tsType.raw.length < 200) { + return `${tsType.raw + .replace(/(\n|\s)/g, '') + .replace(/(\|)/g, '\\|')}`; + } else { + return 'FIX ME FORMAT BIG OBJECT'; + } + } else if (tsType.name === 'union') { + if (tsType.raw) { + // Props + return tsType.raw.replace(/\|/g, '\\|'); + } else if (tsType.elements) { + // Methods + return tsType.elements.map((e) => e.name).join(' \\| '); + } + } else { + return tsType.name; + } + } + function mapProp(propMeta, propName, array) { let result = {}; if (!array) { result = { name: propName || 'FIX ME NO NAME', required: propMeta.required || false, - type: (propMeta.type && propMeta.type.name) || 'FIX ME UNKNOWN TYPE', + type: + propMeta.type?.name || + tsTypeDesc(propMeta.tsType) || + 'FIX ME UNKNOWN TYPE', default: !propMeta.defaultValue ? 'none' : propMeta.defaultValue.value.replace(/\n/g, ''), @@ -129,7 +165,9 @@ class DocJSONBuilder { result.required = propMeta.required; } result.type = - (propMeta.type && propMeta.type.name) || 'FIX ME UNKNOWN TYPE'; + (propMeta.type && propMeta.type.name) || + tsTypeDesc(propMeta.tsType) || + 'FIX ME UNKNOWN TYPE'; if (propMeta.defaultValue) { result.default = propMeta.defaultValue.value.replace(/\n/g, ''); } @@ -204,6 +242,18 @@ class DocJSONBuilder { component.methods = component.methods.filter( (method) => !privateMethods.includes(method.name), ); + + component.methods.forEach((method) => { + method.params.forEach((param) => { + param.type = { name: tsTypeDesc(param.type) }; + }); + }); + + console.log( + `Processed ${component.name} (${component.props?.length ?? 0} props, ${ + component.methods?.length ?? 0 + } methods)`, + ); } generateReactComponentsTask(results, filePath) { @@ -211,22 +261,26 @@ class DocJSONBuilder { dir.readFiles( filePath, this.options, - (err, content, fileName, next) => { + (err, content, fileNameWithExt, next) => { if (err) { return reject(err); } - let componentName = fileName.replace(/.(js|tsx)/, ''); - if (IGNORE_FILES.includes(componentName)) { + let fileName = fileNameWithExt.replace(/.(js)/, ''); + + if (IGNORE_FILES.includes(fileName)) { next(); return; } - results[componentName] = docgen.parse(content, undefined, undefined, { + let parsed = docgen.parse(content, undefined, undefined, { filename: fileName, }); - results[componentName].fileName = fileName; - this.postprocess(results[componentName], componentName); + fileName = fileName.replace(fileExtensionsRegex, ''); + parsed.fileNameWithExt = fileNameWithExt; + results[fileName] = parsed; + + this.postprocess(results[fileName], fileName); next(); }, @@ -252,9 +306,12 @@ class DocJSONBuilder { .charAt(0) .toLowerCase()}${module.name.substring(1)}`; + const pathParts = module.context.file.split('/'); + const fileNameWithExt = pathParts[pathParts.length - 1]; + results[name] = { - fileName: `${name}.js`, name, + fileNameWithExt, description: node.getText(), props: [], styles: [], @@ -268,7 +325,7 @@ class DocJSONBuilder { }); } - generate() { + async generate() { this.generateModulesTask({}, MODULES_PATH); const results = {}; diff --git a/scripts/templates/component.md.ejs b/scripts/templates/component.md.ejs index 40e092427..a462a3536 100644 --- a/scripts/templates/component.md.ejs +++ b/scripts/templates/component.md.ejs @@ -1,7 +1,7 @@ <% const component = locals.component; -%> - + ## /> ### <%- replaceNewLine(component.description) %>