Skip to content

Commit d398766

Browse files
authored
Merge pull request #28 from rezahedi/direction-polyline
Show Plan Direction Route on Map
2 parents 53d92ca + 86d8dca commit d398766

File tree

11 files changed

+240
-15
lines changed

11 files changed

+240
-15
lines changed

src/Components/Map/Map.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ import { debounce, getThemeColor } from "@/lib/utils";
1515

1616
// Bay Area
1717
const MAP_INITIAL_VIEW = {
18-
defaultCenter: { lat: 37.70580795161106, lng: -122.51368137617244 },
19-
defaultZoom: 11,
18+
defaultCenter: { lat: 37.78895639668091, lng: -122.43377970356813 },
19+
defaultZoom: 13,
2020
};
2121

2222
const Map = ({

src/Components/Map/Polyline.tsx

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
/* eslint-disable no-undef */
2+
3+
import {
4+
forwardRef,
5+
useContext,
6+
useEffect,
7+
useImperativeHandle,
8+
useMemo,
9+
useRef,
10+
} from "react";
11+
12+
import { GoogleMapsContext, useMapsLibrary } from "@vis.gl/react-google-maps";
13+
14+
import type { Ref } from "react";
15+
16+
type PolylineEventProps = {
17+
onClick?: (e: google.maps.MapMouseEvent) => void;
18+
onDrag?: (e: google.maps.MapMouseEvent) => void;
19+
onDragStart?: (e: google.maps.MapMouseEvent) => void;
20+
onDragEnd?: (e: google.maps.MapMouseEvent) => void;
21+
onMouseOver?: (e: google.maps.MapMouseEvent) => void;
22+
onMouseOut?: (e: google.maps.MapMouseEvent) => void;
23+
};
24+
25+
type PolylineCustomProps = {
26+
/**
27+
* this is an encoded string for the path, will be decoded and used as a path
28+
*/
29+
encodedPath?: string;
30+
};
31+
32+
export type PolylineProps = google.maps.PolylineOptions &
33+
PolylineEventProps &
34+
PolylineCustomProps;
35+
36+
export type PolylineRef = Ref<google.maps.Polyline | null>;
37+
38+
function usePolyline(props: PolylineProps) {
39+
const {
40+
onClick,
41+
onDrag,
42+
onDragStart,
43+
onDragEnd,
44+
onMouseOver,
45+
onMouseOut,
46+
encodedPath,
47+
...polylineOptions
48+
} = props;
49+
// This is here to avoid triggering the useEffect below when the callbacks change (which happen if the user didn't memoize them)
50+
const callbacks = useRef<Record<string, (e: unknown) => void>>({});
51+
Object.assign(callbacks.current, {
52+
onClick,
53+
onDrag,
54+
onDragStart,
55+
onDragEnd,
56+
onMouseOver,
57+
onMouseOut,
58+
});
59+
60+
const geometryLibrary = useMapsLibrary("geometry");
61+
62+
const polyline = useRef(new google.maps.Polyline()).current;
63+
// update PolylineOptions (note the dependencies aren't properly checked
64+
// here, we just assume that setOptions is smart enough to not waste a
65+
// lot of time updating values that didn't change)
66+
useMemo(() => {
67+
polyline.setOptions(polylineOptions);
68+
}, [polyline, polylineOptions]);
69+
70+
const map = useContext(GoogleMapsContext)?.map;
71+
72+
// update the path with the encodedPath
73+
useMemo(() => {
74+
if (!encodedPath || !geometryLibrary) return;
75+
const path = geometryLibrary.encoding.decodePath(encodedPath);
76+
polyline.setPath(path);
77+
}, [polyline, encodedPath, geometryLibrary]);
78+
79+
// create polyline instance and add to the map once the map is available
80+
useEffect(() => {
81+
if (!map) {
82+
if (map === undefined)
83+
console.error("<Polyline> has to be inside a Map component.");
84+
85+
return;
86+
}
87+
88+
polyline.setMap(map);
89+
90+
return () => {
91+
polyline.setMap(null);
92+
};
93+
}, [map]);
94+
95+
// attach and re-attach event-handlers when any of the properties change
96+
useEffect(() => {
97+
if (!polyline) return;
98+
99+
// Add event listeners
100+
const gme = google.maps.event;
101+
[
102+
["click", "onClick"],
103+
["drag", "onDrag"],
104+
["dragstart", "onDragStart"],
105+
["dragend", "onDragEnd"],
106+
["mouseover", "onMouseOver"],
107+
["mouseout", "onMouseOut"],
108+
].forEach(([eventName, eventCallback]) => {
109+
gme.addListener(polyline, eventName, (e: google.maps.MapMouseEvent) => {
110+
const callback = callbacks.current[eventCallback];
111+
if (callback) callback(e);
112+
});
113+
});
114+
115+
return () => {
116+
gme.clearInstanceListeners(polyline);
117+
};
118+
}, [polyline]);
119+
120+
return polyline;
121+
}
122+
123+
/**
124+
* Component to render a polyline on a map
125+
*/
126+
export const Polyline = forwardRef((props: PolylineProps, ref: PolylineRef) => {
127+
const polyline = usePolyline(props);
128+
129+
useImperativeHandle(ref, () => polyline, []);
130+
131+
return null;
132+
});
133+
134+
Polyline.displayName = "Polyline";

src/context/ItineraryContext.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ type contextType = {
1111
setTitle: (title: string) => void;
1212
setDescription: (description: string) => void;
1313
addImage: (image: string) => void;
14+
setPolyline: (polyline: string) => void;
1415
addPlace: (place: Place) => void;
1516
removePlace: (placeId: string) => void;
1617
createPlan: (plan: PlanType) => void;
@@ -80,6 +81,12 @@ const ItineraryProvider = ({ children }: { children: React.ReactNode }) => {
8081
// dispatch({ type: "addImage", payload: image, init: plan.images || [] });
8182
};
8283

84+
const setPolyline = (polyline: string) => {
85+
if (!plan || !polyline) return;
86+
87+
setPlan({ ...plan, polyline });
88+
};
89+
8390
const addPlace = (place: Place) => {
8491
if (!plan || !place) return;
8592

@@ -96,6 +103,7 @@ const ItineraryProvider = ({ children }: { children: React.ReactNode }) => {
96103
<ItineraryContext.Provider
97104
value={{
98105
plan,
106+
setPolyline,
99107
setTitle,
100108
setDescription,
101109
addImage,

src/context/PlanTypes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export interface PlanType {
1414
images?: string[];
1515
cities?: CityType[];
1616
stops?: PlaceType[];
17+
polyline?: string;
1718
}
1819

1920
export interface CityType {

src/pages/ExplorePage/MapBox.tsx

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from "react";
1+
import React, { useEffect } from "react";
22
import { Map, LocateMeButton, InfoWindow } from "@/Components/Map";
33
import { usePlans } from "./PlansContext";
44
import Markers from "./Markers";
@@ -7,6 +7,8 @@ import PlanPopup from "./PlanPopup";
77
import { Plan } from "@/types";
88
import Popup from "./places/Popup";
99
import { MapMouseEvent } from "@vis.gl/react-google-maps";
10+
import { fetchData } from "@/util";
11+
import { Polyline } from "@/Components/Map/Polyline";
1012

1113
const MapBox = ({
1214
className = "",
@@ -15,11 +17,28 @@ const MapBox = ({
1517
className?: string;
1618
children?: React.ReactNode;
1719
}) => {
18-
const { plans, selection, setSelection, setBoundingBox } = usePlans();
20+
const { plans, setPlanPolyline, selection, setSelection, setBoundingBox } =
21+
usePlans();
1922

2023
const selectedPlan: Plan | null =
2124
plans.find((p) => p._id === selection?.placeId) || null;
2225

26+
useEffect(() => {
27+
if (!selectedPlan || selectedPlan.polyline) return;
28+
29+
(async () => {
30+
// fetch planDetails
31+
const planDetail: Plan | null = await fetchData(
32+
`plans/${selectedPlan._id}`,
33+
null,
34+
() => {},
35+
);
36+
if (!planDetail) return;
37+
38+
setPlanPolyline(planDetail._id, planDetail.polyline || "");
39+
})();
40+
}, [selection]);
41+
2342
const handlePopupClose = () => {
2443
setSelection(null);
2544
};
@@ -50,12 +69,22 @@ const MapBox = ({
5069
)}
5170
<Markers />
5271
{selectedPlan && (
53-
<InfoWindow
54-
position={selectedPlan.startLocation}
55-
onClose={handlePopupClose}
56-
>
57-
<PlanPopup plan={selectedPlan} />
58-
</InfoWindow>
72+
<>
73+
<InfoWindow
74+
position={selectedPlan.startLocation}
75+
onClose={handlePopupClose}
76+
>
77+
<PlanPopup plan={selectedPlan} />
78+
</InfoWindow>
79+
{selectedPlan.polyline && (
80+
<Polyline
81+
strokeWeight={5}
82+
strokeOpacity={0.7}
83+
strokeColor={"#fea403"}
84+
encodedPath={selectedPlan.polyline}
85+
/>
86+
)}
87+
</>
5988
)}
6089
<LocateMeButton />
6190
</Map>

src/pages/ExplorePage/PlansContext.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ type ItemType = {
2222

2323
type PlansContextType = {
2424
plans: Plan[];
25+
setPlanPolyline: (planId: string, polyline: string) => void;
2526
places: Place[];
2627
isLoading: boolean;
2728
error: string | null;
@@ -86,10 +87,17 @@ const PlansProvider = ({ children }: { children: React.ReactNode }) => {
8687
})();
8788
}, [map, boundingBox, token]);
8889

90+
const setPlanPolyline = (planId: string, polyline: string) => {
91+
setPlans((prev) =>
92+
prev.map((plan) => (plan._id === planId ? { ...plan, polyline } : plan)),
93+
);
94+
};
95+
8996
return (
9097
<PlansContext.Provider
9198
value={{
9299
plans,
100+
setPlanPolyline,
93101
places,
94102
isLoading,
95103
error,

src/pages/PlanPage/MapBox.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,16 @@ import { APIProvider, Map } from "@vis.gl/react-google-maps";
33
import { Stop as StopType } from "@/types";
44
import { calculateBounds, cn, getThemeColor, throttle } from "@/lib/utils";
55
import Markers from "./Markers";
6+
import { Polyline } from "@/Components/Map/Polyline";
67

78
const MapBox = ({
89
className,
910
stops,
11+
polyline,
1012
}: {
1113
className?: string;
1214
stops: StopType[];
15+
polyline: string;
1316
}) => {
1417
const mapRef = useRef<HTMLDivElement | null>(null);
1518
const defaultBounds =
@@ -50,6 +53,14 @@ const MapBox = ({
5053
fullscreenControl={false}
5154
colorScheme={getThemeColor()}
5255
>
56+
{polyline && (
57+
<Polyline
58+
strokeWeight={5}
59+
strokeOpacity={0.7}
60+
strokeColor={"#fea403"}
61+
encodedPath={polyline}
62+
/>
63+
)}
5364
<Markers stops={stops} />
5465
</Map>
5566
</APIProvider>

src/pages/PlanPage/PlanDetails.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const PlanDetails = ({ plan }: { plan: PlanWithStops }) => {
2323
images,
2424
cities,
2525
stops,
26+
polyline,
2627
type,
2728
stopCount,
2829
distance,
@@ -112,7 +113,7 @@ const PlanDetails = ({ plan }: { plan: PlanWithStops }) => {
112113
<MapSyncProvider>
113114
<ListProvider>
114115
<div className="flex gap-4 my-4 flex-col md:flex-row-reverse">
115-
<MapBox stops={stops} />
116+
<MapBox stops={stops} polyline={polyline || ""} />
116117
<div className="grow md:flex-2/3">
117118
{images?.length > 0 && (
118119
<ImageBlock

src/pages/dashboard/create/MapBox.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { useItinerary } from "@/context/ItineraryContext";
66
import { calculateBounds, getBoundsFromViewport } from "@/lib/utils";
77
import Markers from "./Markers";
88
import { ListProvider } from "@/context/ListContext";
9+
import { Polyline } from "@/Components/Map/Polyline";
910

1011
const MapBox = () => {
1112
const { setBoundingBox, setSelection } = usePlaces();
@@ -40,6 +41,14 @@ const MapBox = () => {
4041
defaultBounds={defaultBounds}
4142
className="w-full flex-5/12 md:flex-auto"
4243
>
44+
{plan.polyline && (
45+
<Polyline
46+
strokeWeight={5}
47+
strokeOpacity={0.7}
48+
strokeColor={"#fea403"}
49+
encodedPath={plan.polyline}
50+
/>
51+
)}
4352
<ListProvider>
4453
<Markers />
4554
</ListProvider>

0 commit comments

Comments
 (0)