Skip to content

Commit

Permalink
Merge pull request #16 from Expensify/hayata-implement-MapView
Browse files Browse the repository at this point in the history
Implement basic map view
  • Loading branch information
thienlnam authored Aug 11, 2023
2 parents cd720e9 + 352805a commit 9ef450c
Show file tree
Hide file tree
Showing 13 changed files with 1,036 additions and 27 deletions.
21 changes: 5 additions & 16 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ module.exports = {
},
],
'no-invalid-this': 'error',
'prefer-arrow-callback': 'off',
'react/function-component-definition': [
'error',
{
Expand All @@ -26,23 +27,8 @@ module.exports = {
selector: 'AwaitExpression',
message: 'async/await is not allowed',
},
{
selector: 'FunctionDeclaration[async=true]',
message: 'async functions are not allowed',
},
{
selector: 'FunctionExpression[async=true]',
message: 'async functions are not allowed',
},
{
selector: 'ArrowFunctionExpression[async=true]',
message: 'async functions are not allowed',
},
{
selector: 'MethodDefinition[async=true]',
message: 'async methods are not allowed',
},
],
'react/jsx-props-no-spreading': 'off',
},
overrides: [
{
Expand All @@ -53,6 +39,9 @@ module.exports = {
parserOptions: {
project: './tsconfig.json',
},
rules: {
'@typescript-eslint/no-floating-promises': 'off',
},
},
],
};
717 changes: 717 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,11 @@
},
"homepage": "https://github.com/Expensify/react-native-x-maps#readme",
"peerDependencies": {
"@math.gl/web-mercator": "^3.6.3",
"@rnmapbox/maps": "^10.0.11",
"mapbox-gl": "^2.15.0",
"react": "^18.2.0",
"react-map-gl": "^7.1.3",
"react-native": "^0.72.3"
},
"devDependencies": {
Expand All @@ -40,6 +44,7 @@
"@babel/preset-env": "^7.22.9",
"@babel/preset-react": "^7.22.5",
"@babel/preset-typescript": "^7.22.5",
"@types/mapbox-gl": "^2.7.12",
"@types/react": "^18.2.18",
"@typescript-eslint/parser": "^6.2.1",
"eslint": "^8.46.0",
Expand Down
10 changes: 10 additions & 0 deletions src/MapView/CONST.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import {MapViewProps} from './MapViewTypes';

const DEFAULT_ZOOM = 10;
const DEFAULT_COORDINATE: [number, number] = [-122.4021, 37.7911];

export const PADDING = 50;
export const DEFAULT_INITIAL_STATE: MapViewProps['initialState'] = {
location: DEFAULT_COORDINATE,
zoom: DEFAULT_ZOOM,
};
31 changes: 31 additions & 0 deletions src/MapView/Direction.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import Mapbox from '@rnmapbox/maps';

function Direction({coordinates}: {coordinates: Array<[number, number]>}) {
if (coordinates.length < 1) {
return null;
}

return (
<Mapbox.ShapeSource
id="routeSource"
shape={{
type: 'Feature',
properties: {},
geometry: {
type: 'LineString',
coordinates,
},
}}
>
<Mapbox.LineLayer
id="routeFill"
style={{
lineColor: 'blue',
lineWidth: 3,
}}
/>
</Mapbox.ShapeSource>
);
}

export default Direction;
36 changes: 36 additions & 0 deletions src/MapView/Direction.web.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import {Layer, Source} from 'react-map-gl';
import {View} from 'react-native';

function Direction({coordinates}: {coordinates: Array<[number, number]>}) {
if (coordinates.length < 1) {
return null;
}
return (
<View>
{coordinates && (
<Source
id="route"
type="geojson"
data={{
type: 'Feature',
properties: {},
geometry: {
type: 'LineString',
coordinates,
},
}}
>
<Layer
id="route"
type="line"
source="route"
layout={{'line-join': 'round', 'line-cap': 'round'}}
paint={{'line-color': '#888', 'line-width': 4}}
/>
</Source>
)}
</View>
);
}

export default Direction;
81 changes: 81 additions & 0 deletions src/MapView/MapView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import {forwardRef, useEffect, useImperativeHandle, useMemo, useRef} from 'react';
import Mapbox, {MarkerView} from '@rnmapbox/maps';
import {View} from 'react-native';
import {MapViewProps, MapViewHandle} from './MapViewTypes';
import Direction from './Direction';
import Utils from './utils';

const MapView = forwardRef<MapViewHandle, MapViewProps>(function MapView(
{accessToken, style, styleURL, pitchEnabled, mapPadding, initialState, waypoints, markerComponent: MarkerComponent, directionCoordinates},
ref,
) {
const cameraRef = useRef<Mapbox.Camera>(null);

const bounds = useMemo(() => {
if (!waypoints || waypoints.length === 0) {
return undefined;
}

if (waypoints.length === 1) {
cameraRef.current?.flyTo(waypoints[0]);
cameraRef.current?.zoomTo(15);
return undefined;
}

const {southWest, northEast} = Utils.getBounds(waypoints);
return {
ne: northEast,
sw: southWest,
paddingTop: mapPadding,
paddingRight: mapPadding,
paddingBottom: mapPadding,
paddingLeft: mapPadding,
};
}, [waypoints]);

useImperativeHandle(
ref,
() => ({
flyTo: (location: [number, number], animationDuration?: number) => cameraRef.current?.flyTo(location, animationDuration),
}),
[],
);

// Initialize Mapbox on first mount
useEffect(() => {
Mapbox.setAccessToken(accessToken);
}, []);

return (
<View style={style}>
<Mapbox.MapView
styleURL={styleURL}
pitchEnabled={pitchEnabled}
style={{flex: 1}}
>
<Mapbox.Camera
ref={cameraRef}
defaultSettings={{
centerCoordinate: initialState?.location,
zoomLevel: initialState?.zoom,
}}
bounds={bounds}
/>
{MarkerComponent &&
waypoints &&
waypoints.map((waypoint) => (
<MarkerView
id={`${waypoint[0]},${waypoint[1]}`}
key={`${waypoint[0]},${waypoint[1]}`}
coordinate={waypoint}
>
<MarkerComponent />
</MarkerView>
))}
{directionCoordinates && <Direction coordinates={directionCoordinates} />}
</Mapbox.MapView>
</View>
);
});

export default MapView;
100 changes: 100 additions & 0 deletions src/MapView/MapView.web.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import Map, {MapRef, Marker} from 'react-map-gl';
import {RefObject, forwardRef, useEffect, useImperativeHandle, useRef, useState} from 'react';
import WebMercatorViewport from '@math.gl/web-mercator';
import {View} from 'react-native';
import {MapViewHandle, MapViewProps} from './MapViewTypes';
import Utils from './utils';
import 'mapbox-gl/dist/mapbox-gl.css';
import Direction from './Direction';
import {DEFAULT_INITIAL_STATE} from './CONST';

const getMapDimension = (mapRef: RefObject<MapRef>): {width: number; height: number} | undefined => {
if (!mapRef.current?.getMap()) {
return undefined;
}

const {clientWidth, clientHeight} = mapRef.current.getCanvas();
return {width: clientWidth, height: clientHeight};
};

const MapView = forwardRef<MapViewHandle, MapViewProps>(function MapView(
{accessToken, waypoints, style, mapPadding, markerComponent: MarkerComponent, directionCoordinates, initialState = DEFAULT_INITIAL_STATE},
ref,
) {
const mapRef = useRef<MapRef>(null);
const [bounds, setBounds] = useState<{
longitude: number;
latitude: number;
zoom: number;
}>();

useEffect(() => {
if (!waypoints || waypoints.length === 0) {
return;
}

if (waypoints.length === 1) {
mapRef.current?.flyTo({
center: waypoints[0],
zoom: 15,
});
return;
}

const {northEast, southWest} = Utils.getBounds(waypoints);
const {width, height} = getMapDimension(mapRef) || {
width: 0,
height: 0,
};
const viewport = new WebMercatorViewport({height, width});

const {latitude, longitude, zoom} = viewport.fitBounds([southWest, northEast], {
padding: mapPadding,
});

setBounds({latitude, longitude, zoom});
}, [waypoints]);

useImperativeHandle(
ref,
() => ({
flyTo: (location: [number, number], animationDuration?: number) =>
mapRef.current?.flyTo({
center: location,
duration: animationDuration,
}),
}),
[],
);

return (
<View style={style}>
<Map
ref={mapRef}
mapboxAccessToken={accessToken}
initialViewState={{
longitude: initialState?.location[0],
latitude: initialState?.location[1],
zoom: initialState?.zoom,
}}
mapStyle="mapbox://styles/mapbox/streets-v9"
{...bounds}
>
{MarkerComponent &&
waypoints &&
waypoints.map((waypoint) => (
<Marker
key={`${waypoint[0]},${waypoint[1]}`}
longitude={waypoint[0]}
latitude={waypoint[1]}
>
<MarkerComponent />
</Marker>
))}
{directionCoordinates && <Direction coordinates={directionCoordinates} />}
</Map>
</View>
);
});

export default MapView;
30 changes: 30 additions & 0 deletions src/MapView/MapViewTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import {ComponentType} from 'react';
import type {StyleProp, ViewStyle} from 'react-native';

export type MapViewProps = {
// Public access token to be used to fetch map data from Mapbox.
accessToken: string;
// Style applied to MapView component. Note some of the View Style props are not available on ViewMap
style: StyleProp<ViewStyle>;
// Link to the style JSON document.
styleURL?: string;
// Whether map can tilt in the vertical direction.
pitchEnabled?: boolean;
// Padding to apply when the map is adjusted to fit waypoints and directions
mapPadding?: number;
// Initial coordinate and zoom level
initialState?: {
location: [number, number];
zoom: number;
};
// Locations on which to put markers
waypoints?: Array<[number, number]>;
// React component to use for the marker. If not provided, markers are not displayed for waypoints.
markerComponent?: ComponentType;
// List of coordinates which together forms a direction.
directionCoordinates?: Array<[number, number]>;
};

export type MapViewHandle = {
flyTo: (location: [number, number], animationDuration?: number) => void;
};
4 changes: 4 additions & 0 deletions src/MapView/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import MapView from './MapView';

export type {MapViewProps, MapViewHandle} from './MapViewTypes';
export default MapView;
13 changes: 13 additions & 0 deletions src/MapView/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
function getBounds(waypoints: Array<[number, number]>): {southWest: [number, number]; northEast: [number, number]} {
const lngs = waypoints.map((waypoint) => waypoint[0]);
const lats = waypoints.map((waypoint) => waypoint[1]);

return {
southWest: [Math.min(...lngs), Math.min(...lats)],
northEast: [Math.max(...lngs), Math.max(...lats)],
};
}

export default {
getBounds,
};
4 changes: 4 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import MapView from './MapView';

export * from './MapView';
export default MapView;
11 changes: 0 additions & 11 deletions src/index.tsx

This file was deleted.

0 comments on commit 9ef450c

Please sign in to comment.