Skip to content

Commit

Permalink
Merge pull request #157 from Maroon-Rides/route-planning
Browse files Browse the repository at this point in the history
Route planning
  • Loading branch information
bwees authored Jul 29, 2024
2 parents 76054c1 + f8a7de9 commit 1a469e4
Show file tree
Hide file tree
Showing 15 changed files with 3,260 additions and 1,932 deletions.
236 changes: 218 additions & 18 deletions app/components/map/MapView.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import React, { useEffect, useRef, useState } from "react";
import { Dimensions, TouchableOpacity } from "react-native";
import { Dimensions, TouchableOpacity, View } from "react-native";
import MapView, { LatLng, Polyline, Region } from 'react-native-maps';
import * as Location from 'expo-location';
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
import { IMapRoute } from "../../../utils/interfaces";
import { IMapRoute, RoutePlanMapMarker, RoutePlanPolylinePoint } from "../../../utils/interfaces";
import useAppStore from "../../data/app_state";
import BusMarker from "./markers/BusMarker";
import StopMarker from "./markers/StopMarker";
import { useVehicles } from "../../data/api_query";
import { FontAwesome6, Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
import { decode } from "@googlemaps/polyline-codec";
import RoutePlanMarker from "./markers/RoutePlanMarker";

const Map: React.FC = () => {
const mapViewRef = useRef<MapView>(null);
Expand All @@ -19,11 +22,18 @@ const Map: React.FC = () => {
const drawnRoutes = useAppStore((state) => state.drawnRoutes);
const setZoomToStopLatLng = useAppStore((state) => state.setZoomToStopLatLng);
const selectedRouteDirection = useAppStore(state => state.selectedRouteDirection);
const selectedRoutePlan = useAppStore(state => state.selectedRoutePlan);
const selectedRoutePlanPathPart = useAppStore(state => state.selectedRoutePlanPathPart);
const theme = useAppStore((state) => state.theme);
const poppedUpStopCallout = useAppStore((state) => state.poppedUpStopCallout);

const [isViewCenteredOnUser, setIsViewCenteredOnUser] = useState(false);

const [selectedRoutePlanPath, setSelectedRoutePlanPath] = useState<RoutePlanPolylinePoint[]>([]);
const [highlightedRoutePlanPath, setHighlightedRoutePlanPath] = useState<RoutePlanPolylinePoint[]>([]);
const [fadedRoutePlanPath, setFadedRoutePlanPath] = useState<RoutePlanPolylinePoint[][]>([]);
const [routePlanMapMarkers, setRoutePlanMapMarkers] = useState<RoutePlanMapMarker[]>([]);

const { data: buses } = useVehicles(selectedRoute?.key ?? "");

const defaultMapRegion: Region = {
Expand All @@ -32,8 +42,6 @@ const Map: React.FC = () => {
latitudeDelta: 0.10,
longitudeDelta: 0.01
};



function selectRoute(route: IMapRoute, directionKey: string) {

Expand All @@ -50,7 +58,151 @@ const Map: React.FC = () => {
// center the view on the drawn routes
useEffect(() => {
centerViewOnRoutes();
}, [drawnRoutes]);
}, [drawnRoutes, selectedRoutePlanPath]);

// Generate the path points for the selected route plan
useEffect(() => {
if (!selectedRoutePlan) {
setSelectedRoutePlanPath([]);
setHighlightedRoutePlanPath([]);
setFadedRoutePlanPath([]);
setRoutePlanMapMarkers([]);
return;
}

var polyline: RoutePlanPolylinePoint[] = [];
selectedRoutePlan?.instructions.forEach((instruction, index) => {
if (instruction.polyline) {
decode(instruction.polyline).forEach((point) => {
polyline.push({
latitude: point[0],
longitude: point[1],
stepIndex: index,
pathIndex: polyline.length
});
});
}
})

setSelectedRoutePlanPath(polyline);

// clear the highlighted path if no route plan is selected

setHighlightedRoutePlanPath(polyline);
setFadedRoutePlanPath([]);

if (polyline.length === 0) {
setRoutePlanMapMarkers([]);
return;
}

setRoutePlanMapMarkers([
{
icon: <MaterialCommunityIcons name="circle" size={14} color="white" />,
latitude: polyline[polyline.length-1]!.latitude,
longitude: polyline[polyline.length-1]!.longitude
},
{
icon: <MaterialCommunityIcons name="circle" size={10} color="white" />,
latitude: polyline[0]!.latitude,
longitude: polyline[0]!.longitude,
isOrigin: true
}
]);
}, [selectedRoutePlan])

// Adjust the zoom and the path to show the selected part of the route plan
useEffect(() => {
if (!selectedRoutePlan) return;

// filter the selected route plan path to only show the selected part
var highlighted: RoutePlanPolylinePoint[] = [];
if (selectedRoutePlanPathPart === -1) {
highlighted = selectedRoutePlanPath;
setHighlightedRoutePlanPath(selectedRoutePlanPath);
setFadedRoutePlanPath([]);
centerViewOnRoutes();
}

// filter the selected route plan path to only show the selected part
if (selectedRoutePlanPathPart >= 0) {
highlighted = selectedRoutePlanPath.filter((point) => point.stepIndex === selectedRoutePlanPathPart);
setHighlightedRoutePlanPath(highlighted);

// break the path into two parts, before and after the selected part
var faded: RoutePlanPolylinePoint[][] = [[], []];
selectedRoutePlanPath.forEach((point) => {
if (point.stepIndex < selectedRoutePlanPathPart) {
faded[0]!.push(point);
} else if (point.stepIndex > selectedRoutePlanPathPart) {
faded[1]!.push(point);
}
});

setFadedRoutePlanPath(faded);
}

if (highlighted.length === 0) {
// get the last point of the path index - 1 and zoom to that point
// do this by finding the last point that has a stepIndex of selectedRoutePlanPathPart - 1
const lastPoint = [...selectedRoutePlanPath].reverse().find((point) => point.stepIndex === selectedRoutePlanPathPart - 1);

if (lastPoint) {
// if its the last location, show the finish flag
if (lastPoint.pathIndex == selectedRoutePlanPath.length - 1) {
setRoutePlanMapMarkers([
{
icon: <MaterialCommunityIcons name="circle" size={14} color="white" />,
latitude: lastPoint.latitude,
longitude: lastPoint.longitude
}
]);
} else {
setRoutePlanMapMarkers([
{
icon: <Ionicons name="time" size={16} color="white" style={{transform: [{rotate: "-45deg"}]}} />,
latitude: lastPoint.latitude,
longitude: lastPoint.longitude
}
]);
}

mapViewRef.current?.animateToRegion({
latitude: lastPoint.latitude - .002,
longitude: lastPoint.longitude,
latitudeDelta: 0.005,
longitudeDelta: 0.005
});
}

return;
} else {
setRoutePlanMapMarkers([
{
icon:<MaterialCommunityIcons name="circle" size={14} color="white" />,
latitude: highlighted[highlighted.length-1]!.latitude,
longitude: highlighted[highlighted.length-1]!.longitude
},
{
icon: <MaterialCommunityIcons name="circle" size={10} color="white" />,
latitude: highlighted[0]!.latitude,
longitude: highlighted[0]!.longitude,
isOrigin: true
}
]);
}

// animate to the selected part of the route plan
mapViewRef.current?.fitToCoordinates(highlighted, {
edgePadding: {
top: Dimensions.get("window").height * 0.05,
right: 40,
bottom: Dimensions.get("window").height * 0.45 + 8,
left: 40
},
animated: true
});
}, [selectedRoutePlanPathPart])

// handle weird edge case where map does not pick up on the initial region
useEffect(() => {
Expand Down Expand Up @@ -97,13 +249,21 @@ const Map: React.FC = () => {
})
});

// add the selected route plan path to coords
selectedRoutePlanPath.forEach((point) => {
coords.push({
latitude: point.latitude,
longitude: point.longitude
});
})

if (coords.length > 0) {
mapViewRef.current?.fitToCoordinates(coords, {
edgePadding: {
top: Dimensions.get("window").height * 0.05,
right: 20,
right: 40,
bottom: Dimensions.get("window").height * 0.45 + 8,
left: 20
left: 40
},
animated: true
});
Expand Down Expand Up @@ -196,7 +356,7 @@ const Map: React.FC = () => {
tintColor={selectedRoute?.directionList[0]?.lineColor ?? "#FFFF"}
active={patternPath.directionKey === selectedRouteDirection}
route={selectedRoute}
direction={patternPath.directionKey}
direction={selectedRoute?.directionList[0]!.direction.key}
isCalloutShown={poppedUpStopCallout?.stopCode === stop.stopCode}
/>
);
Expand All @@ -206,6 +366,36 @@ const Map: React.FC = () => {
})
))}

{/* Route Plan Highlighted */}
{selectedRoutePlan &&
<Polyline
key={"highlighted-route-plan"}
coordinates={highlightedRoutePlanPath}
strokeColor={theme.myLocation}
strokeWidth={5}
/>
}

{/* Route Plan Faded */}
{selectedRoutePlan && fadedRoutePlanPath.map((path, index) => {
return <Polyline
key={`faded-route-plan-${index}`}
coordinates={path}
strokeColor={theme.myLocation + "60"}
strokeWidth={5}
/>
})}

{/* Route Plan Markers */}
{selectedRoutePlan && routePlanMapMarkers.map((marker, index) => {
return (
<RoutePlanMarker
key={`route-plan-marker-${index}`}
marker={marker}
/>
)
})}

{/* Buses */}
{selectedRoute && buses?.map((bus) => {
const color = selectedRoute.directionList[0]?.lineColor ?? "#500000"
Expand All @@ -220,7 +410,8 @@ const Map: React.FC = () => {
})}
</MapView>

<TouchableOpacity
{/* map buttons */}
<View
style={{
top: 60,
alignContent: 'center',
Expand All @@ -230,17 +421,26 @@ const Map: React.FC = () => {
overflow: 'hidden',
borderRadius: 8,
backgroundColor: theme.background,
padding: 12,
zIndex: 10000,
padding: 12,
zIndex: 1000,
}}
onPress={() => recenterView()}
>
{isViewCenteredOnUser ?
<MaterialIcons name="my-location" size={24} color="gray" />
:
<MaterialIcons name="location-searching" size={24} color="gray" />
}
</TouchableOpacity>
<TouchableOpacity onPress={() => recenterView()}>
{isViewCenteredOnUser ?
<MaterialIcons name="my-location" size={24} color="gray" />
:
<MaterialIcons name="location-searching" size={24} color="gray" />
}
</TouchableOpacity>

{/* Divider */}
<View style={{ height: 1, backgroundColor: theme.divider, marginVertical: 8, marginHorizontal: -2 }} />

<TouchableOpacity onPress={() => presentSheet("inputRoute")}>
<FontAwesome6 name="diamond-turn-right" size={24} color="gray" />
</TouchableOpacity>
</View>

</>
)
}
Expand Down
67 changes: 67 additions & 0 deletions app/components/map/markers/RoutePlanMarker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import React, { memo } from 'react';
import { MapMarker, Marker } from 'react-native-maps';
import { RoutePlanMapMarker } from 'utils/interfaces';
import { View } from 'react-native';
import { getLighterColor } from 'app/utils';
import useAppStore from 'app/data/app_state';

interface Props {
marker: RoutePlanMapMarker
}

// Stop marker with callout
const RoutePlanMarker: React.FC<Props> = ({ marker }) => {
const markerRef = React.useRef<MapMarker>(null);
const theme = useAppStore(state => state.theme);

return (
<Marker
ref={markerRef}
coordinate={{
latitude: marker.latitude,
longitude: marker.longitude
}}
tracksViewChanges={false}
anchor={{x: 1, y: 1}}
pointerEvents="auto"
>
{ marker.isOrigin ? (
<View style={{
alignItems: 'center',
justifyContent: 'center',
width: 24,
height: 24,
backgroundColor: theme.myLocation,
borderColor: getLighterColor(theme.myLocation),
borderWidth: 2,
borderRadius: 999,
transform: [{ rotate: '45deg' }],
zIndex: 800,
elevation: 800
}}>
{marker.icon}
</View>
) : (
<View style={{
alignItems: 'center',
justifyContent: 'center',
width: 30,
height: 30,
borderTopLeftRadius: 15,
borderTopRightRadius: 15,
borderBottomLeftRadius: 15,
backgroundColor: theme.myLocation,
borderColor: getLighterColor(theme.myLocation),
borderWidth: 2,
transform: [{translateY: -20}, { rotate: '45deg' }],
zIndex: 800,
elevation: 800
}}>
{marker.icon}
</View>
)}
</Marker>
);
};

export default memo(RoutePlanMarker);
Loading

0 comments on commit 1a469e4

Please sign in to comment.