diff --git a/.eslintrc.js b/.eslintrc.js index c0b6c84..dee79e8 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,8 +1,8 @@ module.exports = { root: true, - extends: ['@react-native-community', 'prettier'], + extends: ['@react-native-community'], rules: { - 'prettier/prettier': 'error', + 'prettier/prettier': 0, 'no-shadow': [ 'error', { builtinGlobals: true, hoist: 'functions', allow: ['tripId'] }, diff --git a/package-lock.json b/package-lock.json index 06bd341..6ef1a9d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3525,6 +3525,12 @@ "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", "dev": true }, + "@types/lodash": { + "version": "4.14.178", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.178.tgz", + "integrity": "sha512-0d5Wd09ItQWH1qFbEyQ7oTQ3GZrMfth5JkbN3EvTKLXcHLRDSXeLnlvlOn0wvxVIwK5o2M8JzP/OWz7T3NRsbw==", + "dev": true + }, "@types/node": { "version": "16.11.11", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.11.tgz", diff --git a/package.json b/package.json index 4636b79..bcf3762 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "@reduxjs/toolkit": "^1.6.2", "@turf/turf": "^6.5.0", "graphql": "^16.0.1", + "lodash": "^4.17.21", "react": "17.0.2", "react-native": "0.66.3", "react-native-dotenv": "^3.3.0", @@ -30,6 +31,7 @@ "@expo/config-plugins": "^4.0.11", "@react-native-community/eslint-config": "^2.0.0", "@types/jest": "^26.0.23", + "@types/lodash": "^4.14.178", "@types/react-native": "^0.66.4", "@types/react-native-dotenv": "^0.2.0", "@types/react-redux": "^7.1.20", diff --git a/src/apollo/client.ts b/src/apollo/client.ts index d46d258..5ab3010 100644 --- a/src/apollo/client.ts +++ b/src/apollo/client.ts @@ -15,7 +15,10 @@ const authMiddleware = new ApolloLink((operation, forward) => { 'x-api-key': GTFS_API_GATEWAY_KEY, }, })); - return forward(operation); + return forward(operation).map(result => { + // console.info(operation.getContext().response); + return result; + }); }); const client = new ApolloClient({ @@ -27,7 +30,9 @@ const client = new ApolloClient({ }, Stop: { keyFields: Stop => - `${Stop.__typename}:${Stop.feedIndex}:${Stop.stopId}`, + `${Stop.__typename}:${Stop.feedIndex}:${ + Stop.parentStation || Stop.stopId + }`, }, Trip: { keyFields: Trip => diff --git a/src/apollo/fragments.ts b/src/apollo/fragments.ts index 0307438..3720034 100644 --- a/src/apollo/fragments.ts +++ b/src/apollo/fragments.ts @@ -12,7 +12,6 @@ export const STOP_FIELDS = gql` feedIndex stopId stopName - parentStation geom { coordinates } @@ -44,10 +43,7 @@ export const TRIP_FIELDS = gql` stop { feedIndex stopId - stopName - geom { - coordinates - } + parentStation } } } diff --git a/src/apollo/queries.ts b/src/apollo/queries.ts index 0b8f1a1..8aaddff 100644 --- a/src/apollo/queries.ts +++ b/src/apollo/queries.ts @@ -15,9 +15,29 @@ export const GET_FEEDS = gql` } `; +export const GET_STATIONS = gql` + ${STOP_FIELDS} + query GetStations($feedIndex: Int!, $stationIds: [String!]) { + stations(feedIndex: $feedIndex, stationIds: $stationIds) { + ...StopFields + } + } +`; + +export const GET_STOPS_BY_LOCATION = gql` + ${STOP_FIELDS} + query GetStopsByLocation($latitude: Float!, $longitude: Float!, $radius: Float!) { + stopsByLocation( + location: [$latitude, $longitude] + radius: $radius + ) { + ...StopFields + } + } +`; + export const GET_TRIPS = gql` ${TRIP_FIELDS} - ${STOP_FIELDS} query GetNextTrips($feedIndex: Int!, $routeId: String!) { nextTrips(feedIndex: $feedIndex, routeId: $routeId) { ...TripFields @@ -25,7 +45,8 @@ export const GET_TRIPS = gql` stopSequence departure stop { - ...StopFields + stopId + parentStation } } } diff --git a/src/components/ErrorView.tsx b/src/components/ErrorView.tsx index d895393..3fd97ed 100644 --- a/src/components/ErrorView.tsx +++ b/src/components/ErrorView.tsx @@ -1,10 +1,10 @@ import React, { FC } from 'react'; import { StyleProp, Text, View } from 'react-native'; -type Props = { +interface Props { message?: string; styles?: StyleProp; -}; +} const ErrorView: FC = ({ message, styles = {} }) => { return ( @@ -16,4 +16,4 @@ const ErrorView: FC = ({ message, styles = {} }) => { ); }; -export default ErrorView; +export default React.memo(ErrorView); diff --git a/src/components/LoadingView.tsx b/src/components/LoadingView.tsx index b0628b3..1127ad3 100644 --- a/src/components/LoadingView.tsx +++ b/src/components/LoadingView.tsx @@ -1,10 +1,10 @@ import React, { FC } from 'react'; import { StyleProp, Text, View } from 'react-native'; -type Props = { +interface Props { message?: string; styles?: StyleProp; -}; +} const LoadingView: FC = ({ message, styles = {} }) => { return ( @@ -14,4 +14,4 @@ const LoadingView: FC = ({ message, styles = {} }) => { ); }; -export default LoadingView; +export default React.memo(LoadingView); diff --git a/src/components/MapView.tsx b/src/components/MapView.tsx index bbb62eb..3395cc1 100644 --- a/src/components/MapView.tsx +++ b/src/components/MapView.tsx @@ -1,14 +1,14 @@ -import React, { FC } from 'react'; +import React, { FC, ReactNode } from 'react'; import { GestureResponderEvent, StyleSheet } from 'react-native'; import MapboxGL, { RegionPayload } from '@react-native-mapbox-gl/maps'; import { Feature, Point, Position } from '@turf/turf'; import { MAPBOX_ACCESS_TOKEN, MAPBOX_STYLE_URL } from '@env'; -type Props = { +interface Props { centerCoordinate: Position; zoomLevel: number; pitch: number; - children: any; + children: ReactNode; onRegionWillChange?: (feature: Feature) => void; onRegionDidChange?: (feature: Feature) => void; onTouchStart?: (e: GestureResponderEvent) => void; @@ -16,7 +16,7 @@ type Props = { onLongPress?: ( feature: Feature, ) => void; -}; +} MapboxGL.setAccessToken(MAPBOX_ACCESS_TOKEN); diff --git a/src/components/StopMarker.tsx b/src/components/StopMarker.tsx index 923b5b9..1e8c83f 100644 --- a/src/components/StopMarker.tsx +++ b/src/components/StopMarker.tsx @@ -4,25 +4,31 @@ import { SvgProps } from 'react-native-svg'; import { Position } from '@turf/turf'; import Pin from 'assets/pin.svg'; -type Props = { +interface Props { feedIndex: number; stopId: string; coordinates: Position; -}; + svgProps?: SvgProps; +} -const svgProps: SvgProps = { +const svgDefaultProps: SvgProps = { width: 50, height: 50, fill: '#cc0000', }; -const StopMarker: FC = ({ feedIndex, stopId, coordinates }) => ( +const StopMarker: FC = ({ + feedIndex, + stopId, + coordinates, + svgProps = {}, +}) => ( - + ); -export default StopMarker; +export default React.memo(StopMarker); diff --git a/src/components/StopShape.tsx b/src/components/StopShape.tsx index f165792..c0f1b93 100644 --- a/src/components/StopShape.tsx +++ b/src/components/StopShape.tsx @@ -3,16 +3,16 @@ import MapboxGL, { CircleLayerStyle } from '@react-native-mapbox-gl/maps'; import { point, Position } from '@turf/turf'; import { StopTimeCallback } from './StopTimeButton'; -type Props = { +interface Props { feedIndex: number; tripId: string; stopId: string; coordinates: Position; color?: string; isActive?: boolean; - aboveLayerId: string; + aboveLayerId?: string; onPress: StopTimeCallback; -}; +} const getCircleStyles = ( color: string, @@ -20,7 +20,7 @@ const getCircleStyles = ( ): CircleLayerStyle => ({ circleRadius: isActive ? 16 : 6, circleColor: `#${isActive ? 'ddd' : color || 'ddd'}`, - circleStrokeColor: `#ddd`, + circleStrokeColor: '#ddd', circleStrokeWidth: 2, circlePitchScale: 'map', circlePitchAlignment: 'map', @@ -61,4 +61,4 @@ const StopShape: FC = ({ ); }; -export default StopShape; +export default React.memo(StopShape); diff --git a/src/components/StopTimeButton.tsx b/src/components/StopTimeButton.tsx index cb378ee..487d3c5 100644 --- a/src/components/StopTimeButton.tsx +++ b/src/components/StopTimeButton.tsx @@ -19,7 +19,7 @@ export interface IStopTimeStyles { departure?: StyleProp; } -type Props = { +interface Props { feedIndex: number; tripId: string; stopId: string; @@ -29,7 +29,7 @@ type Props = { labelStyles?: StyleProp; departureStyles?: StyleProp; onPress: StopTimeCallback; -}; +} const StopTimeButton: FC = ({ feedIndex, @@ -52,4 +52,4 @@ const StopTimeButton: FC = ({ ); }; -export default StopTimeButton; +export default React.memo(StopTimeButton); diff --git a/src/components/TripList.tsx b/src/components/TripList.tsx index 4779032..a690cc4 100644 --- a/src/components/TripList.tsx +++ b/src/components/TripList.tsx @@ -6,12 +6,12 @@ import StopTimeButton, { } from 'components/StopTimeButton'; import { IStopTime } from 'interfaces'; -type Props = { +interface Props { tripId: string; stopTimes: IStopTime[]; styles?: IStopTimeStyles; onPress: StopTimeCallback; -}; +} const getRenderItem = ( tripId: string, @@ -20,12 +20,12 @@ const getRenderItem = ( onPress: StopTimeCallback, ) => { const { stop, departure } = stopTime; - const { feedIndex, stopId, stopName } = stop; + const { feedIndex, stopId, parentStation, stopName } = stop; return ( = ({ tripId, stopTimes, styles = {}, onPress }) => { renderItem={({ item }: ListRenderItemInfo) => getRenderItem(tripId, item, styles, onPress) } - keyExtractor={(stopTime: IStopTime) => stopTime.stop.stopId} + keyExtractor={(stopTime: IStopTime) => { + const { feedIndex, stopId, parentStation } = stopTime.stop; + return `${feedIndex}:${parentStation || stopId}`; + }} /> ); }; -export default TripList; +export default React.memo(TripList); diff --git a/src/components/TripShape.tsx b/src/components/TripShape.tsx index b91d35c..09d30b2 100644 --- a/src/components/TripShape.tsx +++ b/src/components/TripShape.tsx @@ -2,12 +2,12 @@ import MapboxGL, { LineLayerStyle } from '@react-native-mapbox-gl/maps'; import { lineString, Position } from '@turf/turf'; import React, { FC } from 'react'; -type Props = { +interface Props { shapeSourceId: string; layerId: string; color?: string; coordinates?: Position[]; -}; +} const getLineStyles = (color?: string): LineLayerStyle => ({ lineColor: `#${color ? color : 'ddd'}`, @@ -30,4 +30,4 @@ const TripShape: FC = ({ ); }; -export default TripShape; +export default React.memo(TripShape); diff --git a/src/navigation/providers.tsx b/src/navigation/providers.tsx index ed485f6..020d25f 100644 --- a/src/navigation/providers.tsx +++ b/src/navigation/providers.tsx @@ -13,7 +13,7 @@ export const withProviders = store: any, client: any, ): FC

=> - (props: P & Props): ReactElement => + (props: P & Props): ReactElement => ( diff --git a/src/screens/dashboard/DashboardScreen.tsx b/src/screens/dashboard/DashboardScreen.tsx index b45e4db..858a1a5 100644 --- a/src/screens/dashboard/DashboardScreen.tsx +++ b/src/screens/dashboard/DashboardScreen.tsx @@ -18,12 +18,8 @@ import styles from './styles'; const DashboardScreen: FC = () => { const { push } = useNavigation(); - const { loading, error, data } = useQuery<{ feeds: IFeed[] }>(GET_FEEDS); - if (loading) ; - if (error) ; - const { feeds } = data || {}; const renderItem = ({ item }: ListRenderItemInfo) => ( @@ -40,6 +36,8 @@ const DashboardScreen: FC = () => { Dashboard + {loading && } + {error && } { const { activeStop } = useAppSelector(state => state.stops); const dispatch = useAppDispatch(); @@ -50,9 +53,9 @@ const MapScreen: FC = () => { }); // Query shape geometries - const { loading, data } = useQuery<{ shape: IShape }, ShapeVars>(GET_SHAPE, { + const { loading, data } = useQuery<{ shape: IShape }>(GET_SHAPE, { variables: { - shapeId: trip?.shapeId || '', + shapeId: trip?.shapeId, }, }); @@ -81,25 +84,27 @@ const MapScreen: FC = () => { }, }); - setCameraState(state => ({ - ...state, - centerCoordinate: stop?.geom.coordinates || DEFAULT_COORD, - zoomLevel: STOP_ZOOM, - })); + if (stop) { + setCameraState(state => ({ + ...state, + centerCoordinate: stop?.geom.coordinates || DEFAULT_COORD, + zoomLevel: STOP_ZOOM, + })); + } }, [componentId, stop]); - // useEffect(() => { - // const radius = getRadiusByZoomLat( - // cameraState.zoomLevel, - // cameraState.centerCoordinate[0], - // ); - - // console.log({ - // location: cameraState.centerCoordinate, - // radius, - // pitch: cameraState.pitch, - // }); - // }, [cameraState]); + const stopTimes = useMemo(() => { + return trip?.stopTimes.map((stopTime: IStopTime) => ({ + ...stopTime, + stop: client.readFragment({ + id: (() => { + const { feedIndex, parentStation, stopId } = stopTime.stop; + return `Stop:${feedIndex}:${parentStation || stopId}`; + })(), + fragment: STOP_FIELDS, + }), + })); + }, [client, trip?.stopTimes]); const onStopPress = useCallback( ({ stopId, tripId, feedIndex }) => { @@ -119,16 +124,41 @@ const MapScreen: FC = () => { setMarkerVisible(false); }, []); + const [getStopsByLocation, { + data: stopsData, + }] = useLazyQuery<{ stopsByLocation: IStop[] }>(GET_STOPS_BY_LOCATION); + console.log({ stopsData }); + // const { stopsByLocation } = stopsData || {}; + + const radius = parseFloat(getRadiusByZoomLat( + cameraState.zoomLevel, + cameraState.centerCoordinate[0], + ).toFixed(5)); + const onRegionDidChange = useCallback( (feature: Feature) => { + const { geometry, properties } = feature; setMarkerVisible(true); + setCameraState({ - centerCoordinate: feature.geometry.coordinates, - pitch: feature.properties.pitch, - zoomLevel: feature.properties.zoomLevel, + ...cameraState, + pitch: properties.pitch, + zoomLevel: properties.zoomLevel, }); + + const [latitude, longitude] = geometry.coordinates; + if (properties.zoomLevel > 12) { + getStopsByLocation({ + variables: { + latitude, + longitude, + radius: radius, + }, + }); + } }, - [], + // eslint-disable-next-line react-hooks/exhaustive-deps + [cameraState], ); const onLongPress = useCallback( @@ -136,16 +166,15 @@ const MapScreen: FC = () => { const { geometry } = feature; const point = geometry as Point; setCameraState({ - ...cameraState, centerCoordinate: point.coordinates, pitch: 0, zoomLevel: 18, }); }, - [cameraState], + [], ); - const shapeLayerId = `line-layer-${trip?.feedIndex}:${trip?.tripId}`; + const shapeLayerId = 'line-layer-selected-trip'; return ( @@ -164,31 +193,43 @@ const MapScreen: FC = () => { coordinates={stop.geom.coordinates} /> )} - {!loading && (data || trip?.stopTimes) && ( - st.stop.geom.coordinates) - } - /> - )} - {trip?.stopTimes && - trip?.stopTimes.map((st: IStopTime) => ( - + st.stop.geom.coordinates) + } /> - ))} + {stopTimes.map((st: IStopTime) => ( + + ))} + + )} + {/*stopsByLocation && stopsByLocation.map((s: IStop, i: number) => ( + + ))*/} diff --git a/src/screens/trip/TripScreen.tsx b/src/screens/trip/TripScreen.tsx index ea20bc9..cc970a2 100644 --- a/src/screens/trip/TripScreen.tsx +++ b/src/screens/trip/TripScreen.tsx @@ -1,15 +1,18 @@ import React, { FC, useCallback, useContext, useEffect } from 'react'; import { Text, View } from 'react-native'; -import { gql, useApolloClient } from '@apollo/client'; +import { gql, useApolloClient, useQuery } from '@apollo/client'; import { Navigation } from 'react-native-navigation'; import { NavigationContext } from 'react-native-navigation-hooks'; import { useAppDispatch } from 'store'; import { setActiveStop } from 'slices/stops'; import TripList from 'components/TripList'; import { StopTimeCallback } from 'components/StopTimeButton'; -import { TRIP_FIELDS } from 'apollo/fragments'; -import { IRoute, ITrip } from 'interfaces'; +import { STOP_FIELDS, TRIP_FIELDS } from 'apollo/fragments'; +import { IRoute, IStop, IStopTime, ITrip } from 'interfaces'; import styles from './styles'; +import { GET_STATIONS } from 'apollo/queries'; +import LoadingView from 'components/LoadingView'; +import ErrorView from 'components/ErrorView'; type Props = { route: IRoute; @@ -28,6 +31,19 @@ const TripScreen: FC = ({ tripId, route }) => { `, }); + const stationIds = + trip?.stopTimes.map( + (stopTime: IStopTime) => + stopTime.stop.parentStation || stopTime.stop.stopId, + ) || []; + + const { data, loading, error } = useQuery<{ stations: IStop[] }>(GET_STATIONS, { + variables: { + feedIndex: trip?.feedIndex, + stationIds, + }, + }); + useEffect(() => { if (trip) { Navigation.mergeOptions(componentId, { @@ -59,20 +75,36 @@ const TripScreen: FC = ({ tripId, route }) => { [componentId, dispatch], ); + const stopTimes = + (data?.stations && + trip?.stopTimes.map((stopTime: IStopTime) => { + const { feedIndex, stopId, parentStation } = stopTime.stop; + return { + ...stopTime, + stop: client.readFragment({ + id: `Stop:${feedIndex}:${parentStation || stopId}`, + fragment: STOP_FIELDS, + }), + }; + })) || + []; + return ( {route.routeLongName} {route.routeDesc} + {loading && } + {error && } - {trip && ( + {trip && stopTimes && ( {trip.tripHeadsign} -{trip.directionId ? 'Inbound' : 'Outbound'}