import { useState, useRef, useEffect } from "react";
// Icons
import DirectionsBikeIcon from "@material-ui/icons/DirectionsBike";
import DirectionsRunIcon from "@material-ui/icons/DirectionsRun"; // Not really necessary, but oh wells
import WcIcon from "@material-ui/icons/Wc";
import MyLocationIcon from "@material-ui/icons/MyLocation";
import AddLocationIcon from "@material-ui/icons/AddLocation";
import SettingsIcon from "@material-ui/icons/Settings";
import BookmarkIcon from "@material-ui/icons/Bookmark";
// Core Components
import CustomInput from "/components/CustomInput/CustomInput.js";
import InputAdornment from "@material-ui/core/InputAdornment";
import GridContainer from "/components/Grid/GridContainer.js";
import GridItem from "/components/Grid/GridItem.js";
import Scroll from "react-scroll";
import IconButton from "@material-ui/core/IconButton";
import CustomTabs from "/components/CustomTabs/CustomTabs.js";
import Badge from "/components/Badge/Badge.js";
var Element = Scroll.Element;
// Styling
import SearchIcon from "@material-ui/icons/Search";
import { makeStyles } from "@material-ui/core/styles";
import styles from "/styles/jss/nextjs-material-kit/pages/components.js";
import Success from "/components/Typography/Success.js";
import TravelModeSelection from "./TravelModeSelection";
// Get Request
import httpGet from "/data/getRequest";
import { getAllToilets, toiletLink } from "/data/retrievetoilets";
import { getAllWaterpoints, waterpointLink } from "/data/retrievewaterpoints";
import { getAllFoodPoints, restaurantLink } from "/data/retrievefoodpoints";
// Destination Cards
import WaypointCard from "./WaypointCard";
// Google Maps API
import {
GoogleMap,
useLoadScript,
Marker,
Autocomplete,
DirectionsService,
DirectionsRenderer,
DistanceMatrixService,
} from "@react-google-maps/api";
import { REACT_APP_GOOGLE_MAPS_API_KEY } from "../../env";
// import { Button, Grid } from "@material-ui/core";
import Button from "/components/CustomButtons/Button.js";
import classNames from "classnames";
// Toggles
import ToggleMap from "./ToggleMap";
import POIMarkers from "./MarkerToggle";
import SavedRoutesTab from "./SavedRoutesTab";
const useStyles = makeStyles(styles);
/**
* The `Map` function handles mapping functionalities including waypoints, search,
* current location, markers for toilets, food points, and water points, as well as displaying routes
* and settings.
* @param props - The `Map` component you provided is a functional component that takes `props` as a
* parameter. The `props` object contains various properties that can be passed to the `Map` component
* when it is used in a parent component. These properties can be accessed within the `Map` component
* using object
* @returns The `Map` component is being returned, which consists of various functionalities related to
* mapping, including search functionality, route planning, saved routes, settings toggles for
* different points of interest (toilets, food points, water points), and the actual map display with
* markers and directions based on selected waypoints. The component also includes handlers for adding,
* deleting, and swapping waypoints, as well as functions to
*/
export function Map(props) {
const { userID = null } = props;
// Use States
const [selectedPlace, setSelectedPlace] = useState(null);
const [searchLngLat, setSearchLngLat] = useState(null);
const [currentLocation, setCurrentLocation] = useState(null);
const [clickedLocation, setClickLocation] = useState(null);
const [travelMode, setTravelMode] = useState({
mode: "BICYCLING",
icon: DirectionsBikeIcon,
});
const autocompleteRef = useRef(null);
const [response, setResponse] = useState(null);
const classes = useStyles();
const [waypoints, setwaypoints] = useState([]);
const [middlepoints, setmiddlepoints] = useState([]);
const [waypointsOnChange, setwaypointsOnChange] = useState(false);
const [segmentPath, setSegmentPath] = useState([]);
const [totalPathDistance, settotalPathDistance] = useState(0);
const [totalPathDuration, settotalPathDuration] = useState(0);
// Toggles
// ***************
// Toilet
const [allToilets, setAlltoilets] = useState([]);
const [toiletIcon, setToiletIcon] = useState(null);
const [toiletToggle, setToiletToggle] = useState(false);
// Food points
const [allFood, setAllfood] = useState([]);
const [foodIcon, setFoodIcon] = useState(null);
const [FoodToggle, setFoodToggle] = useState(false);
// Water points
const [allWater, setAllWater] = useState([]);
const [waterIcon, setWaterIcon] = useState(null);
const [WaterToggle, setWaterToggle] = useState(false);
// When waypoints are added or removed
useEffect(() => {
// console.log("Waypoints have changed: ", waypoints);
handleMiddlePoints();
setwaypointsOnChange(true);
console.log(
"Full Array of Waypoints: ",
waypoints.slice().map((waypoint) => {
return waypoint.location;
})
);
}, [waypoints]);
// When travelMode changed from running to cycling, vice-versa
useEffect(() => {
setwaypointsOnChange(true);
}, [travelMode]);
useEffect(() => {
setSelectedPlace(null);
setSearchLngLat(null);
setCurrentLocation(null);
}, [clickedLocation]);
// laod script for google map
const { isLoaded } = useLoadScript({
// googleMapsApiKey: process.env.REACT_APP_GOOGLE_MAPS_API_KEY, // Load your own API key
googleMapsApiKey: REACT_APP_GOOGLE_MAPS_API_KEY, // Load your own API key
libraries: ["places"],
});
if (!isLoaded) return <div>Loading....</div>;
// static lat and lng
const center = { lat: 1.2838566998730194, lng: 103.83347494758458 };
// Swap Waypoint Handler
// Markers
// ************************
// *** Toilet ***
// Load the toilets
if (allToilets.length < 1 && toiletToggle) {
getToilets().then((array) => {
setAlltoilets(array);
});
}
// Obtain ToiletLink
if (toiletIcon == null && toiletToggle) {
toiletLink().then((string) => {
// console.log("Toilet Icon link: ", string);
setToiletIcon(string);
});
}
// *** Water Points ***
// Get all water points from database
async function getWaterPoint() {
var waterpoints = await getAllWaterpoints();
// console.log(toilets);
return waterpoints;
}
// Load the water points
if (allWater.length < 1 && FoodToggle) {
getWaterPoint().then((array) => {
setAllWater(array);
});
}
// Obtain water point link
if (waterIcon == null && FoodToggle) {
waterpointLink().then((string) => {
// console.log("Toilet Icon link: ", string);
setWaterIcon(string);
});
}
// *** Food points ***
// Get all toilets from database
async function getFoodPoints() {
var foodpoints = await getAllFoodPoints();
// console.log(toilets);
return foodpoints;
}
// Load the toilets
if (allFood.length < 1 && FoodToggle) {
getFoodPoints().then((array) => {
setAllfood(array);
});
}
// Obtain ToiletLink
if (foodIcon == null && FoodToggle) {
restaurantLink().then((string) => {
// console.log("Toilet Icon link: ", string);
setFoodIcon(string);
});
}
return (
<div
style={{
// display: "flex",
// flexDirection: "row",
// justifyContent: "center",
// alignItems: "center",
// gap: "20px",
// flexGrow: 1,
// width: "100%",
padding: "1%",
}}
className={classNames(classes.main)}
>
{/* search component */}
<GridContainer>
<GridItem xs={4}>
<CustomTabs
headerColor="info"
tabs={[
{
tabName: "Route",
tabIcon: AddLocationIcon,
tabContent: (
<span>
<div>
<Autocomplete
onLoad={(autocomplete) => {
console.log("Autocomplete loaded:", autocomplete);
autocompleteRef.current = autocomplete;
}}
onPlaceChanged={handlePlaceChanged}
options={{
fields: ["address_components", "geometry", "name"],
}}
>
<CustomInput
labelText="Search for an exact location"
id="material"
formControlProps={{
fullWidth: true,
}}
inputProps={{
endAdornment: (
<InputAdornment position="end">
<SearchIcon />
</InputAdornment>
),
}}
/>
</Autocomplete>
<GridContainer>
<GridItem xs={5}>
<Button
color="info"
onClick={() => {
if (selectedPlace != null) {
setwaypoints((waypoints) => [
...waypoints,
{
location: {
lat: selectedPlace.geometry.location.lat(),
lng: selectedPlace.geometry.location.lng(),
},
name: selectedPlace.name,
},
]);
} else if (currentLocation != null) {
setwaypoints((waypoints) => [
...waypoints,
{
location: currentLocation, // Already in float
name: "Current Location",
},
]);
} else if (clickedLocation != null) {
setwaypoints((waypoints) => [
...waypoints,
{
location: {
lat: clickedLocation.location.lat(),
lng: clickedLocation.location.lng(),
},
name:
clickedLocation.name ||
"Unknown Location",
},
]);
}
}}
>
Add waypoint
</Button>
</GridItem>
<GridItem xs={5}>
<TravelModeSelection
travelMode={travelMode}
setTravelMode={setTravelMode}
/>
</GridItem>
<GridItem xs={2}>
<IconButton onClick={handleGetLocationClick}>
<MyLocationIcon />
</IconButton>
</GridItem>
</GridContainer>
{totalPathDistance > 0 && (
<div>
{/* <Success> */}
Total Distance: {Math.floor(totalPathDistance / 1000)}
km | Total Duration:{" "}
{new Date(totalPathDuration * 1000)
.toISOString()
.substring(11, 16)}
{/* </Success> */}
</div>
)}
</div>
<div>
<Badge color="warning">
Selected Location: {selectedPlace && selectedPlace.name}{" "}
{currentLocation && "Current Location"}
{clickedLocation &&
`${clickedLocation.name || "Unknown Location"}`}
</Badge>
</div>
</span>
),
},
{
tabName: "Saved Routes",
tabIcon: BookmarkIcon,
tabContent: (
<p>
<SavedRoutesTab
waypoints={waypoints}
setwaypoints={setwaypoints}
routeResponse={response}
userID={userID}
totalPathDistance={totalPathDistance}
totalPathDuration={totalPathDuration}
/>
</p>
),
},
{
tabName: "Settings",
tabIcon: SettingsIcon,
tabContent: (
<div>
<ToggleMap
state={toiletToggle}
setState={setToiletToggle}
toggleName={"Toilets"}
/>
<ToggleMap
state={FoodToggle}
setState={setFoodToggle}
toggleName={"Food Points"}
/>
<ToggleMap
state={WaterToggle}
setState={setWaterToggle}
toggleName={"Water Points"}
/>
</div>
),
},
]}
/>
<Element
name="test7"
className="element"
id="containerElement"
style={{
position: "relative",
height: "300px",
overflow: "scroll",
// marginBottom: "100px",
}}
>
{waypoints.length > 0 &&
waypoints.map((waypoint, index) => {
// console.log("Waypoint: ", waypoint);
return (
<WaypointCard
name={waypoint.name}
distance={
index <= segmentPath.length && index > 0
? segmentPath[index - 1].distance
: null
}
duration={
index <= segmentPath.length && index > 0
? segmentPath[index - 1].duration
: null
}
index={index}
deleteHandler={deleteWaypointHandler}
length={waypoints.length}
swapHandler={swapWappointHandler}
/>
);
})}
</Element>
</GridItem>
{/* map component */}
<GridItem xs={8}>
<GoogleMap
zoom={currentLocation || selectedPlace ? 18 : 12}
center={currentLocation || searchLngLat || center}
mapContainerClassName="map"
mapContainerStyle={{
// width: "60%",
height: "600px",
// margin: "auto",
}}
onClick={(e) => {
extractClickedLocation(e);
}}
// onLoad={onMapLoad}
>
{selectedPlace && <Marker position={searchLngLat} />}
{clickedLocation && clickedLocation.name == null && (
<Marker
position={clickedLocation.location}
title={`Lat: ${clickedLocation.location.lat()}, Lng: ${clickedLocation.location.lng()}`}
/>
)}
{currentLocation && (
<Marker
position={currentLocation}
title={"I'm here!"}
label={"Label!"}
icon={"public/map-icons/toilet_icon.png"}
/>
)}
{toiletToggle && (
<POIMarkers
setClickLocation={setClickLocation}
allPOI={allToilets}
image={toiletIcon}
poiname={"Toilet"}
/>
)}
{WaterToggle && (
<POIMarkers
setClickLocation={setClickLocation}
allPOI={allWater}
image={waterIcon}
poiname={"Water point"}
/>
)}
{FoodToggle && (
<POIMarkers
setClickLocation={setClickLocation}
allPOI={allFood}
image={foodIcon}
poiname={"Food point"}
/>
)}
{waypoints.length > 1 && waypointsOnChange && (
<div>
<DirectionsService
options={{
destination: waypoints[waypoints.length - 1].location,
origin: waypoints[0].location,
travelMode: travelMode.mode,
waypoints: middlepoints,
}}
callback={directionsCallback}
onLoad={(directionsService) => {
console.log(
"DirectionsService onLoad directionsService: ",
directionsService
);
setwaypointsOnChange(false);
}}
onUnmount={(directionsService) => {
console.log(
"DirectionsService onUnmount directionsService: ",
directionsService
);
}}
/>
<DistanceMatrixService
options={{
origins: waypoints.slice(0, -1).map((waypoint) => {
console.log("Origin: ", waypoint.location);
return waypoint.location;
}),
destinations: waypoints
.slice(1 - waypoints.length)
.map((waypoint) => {
console.log("Destination: ", waypoint.location);
return waypoint.location;
}),
travelMode: travelMode.mode,
}}
callback={directionMatrixCallback}
/>
</div>
)}
{response && waypoints.length > 1 && (
<DirectionsRenderer
options={{
directions: response,
// draggable: true,
}}
onLoad={(directionsRenderer) => {
console.log(
"DirectionsRenderer onLoad directionsRenderer: ",
directionsRenderer
);
}}
// optional
onUnmount={(directionsRenderer) => {
console.log(
"DirectionsRenderer onUnmount directionsRenderer: ",
directionsRenderer
);
}}
/>
)}
</GoogleMap>
</GridItem>
</GridContainer>
</div>
);
}
/**
* The `handleMiddlePoints` function sets up middle points based on the input waypoints array and
* logs the resulting middle points.
*/
export const handleMiddlePoints = () => {
setmiddlepoints((middlepoints) => []);
waypoints.forEach((value, index) => {
if (index > 0 && index < waypoints.length - 1) {
setmiddlepoints((middlepoints) => [
...middlepoints,
{
location: value.location,
stopover: true,
},
]);
}
});
// console.log(waypoints);
console.log("MiddlePoints: ", middlepoints);
};
/**
* The function `handlePlaceChanged` retrieves and sets the selected place's details, including its
* geometry, and updates the search latitude and longitude accordingly.
*/
export const handlePlaceChanged = () => {
const place = autocompleteRef.current.getPlace();
// console.log(place);
// console.log(place.hasOwnProperty("geometry"));
setSelectedPlace(place);
if (place != undefined && place.hasOwnProperty("geometry")) {
setSearchLngLat({
lat: place.geometry.location.lat(),
lng: place.geometry.location.lng(),
});
}
setCurrentLocation(null);
setClickLocation(null);
};
/**
* The `handleGetLocationClick` function uses the Geolocation API to get the current position of the
* user and update the current location accordingly.
*/
export const handleGetLocationClick = () => {
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(
(position) => {
const { latitude, longitude } = position.coords;
setSelectedPlace(null);
setSearchLngLat(null);
setClickLocation(null);
setCurrentLocation({ lat: latitude, lng: longitude });
},
(error) => {
console.log(error);
}
);
} else {
console.log("Geolocation is not supported by this browser.");
}
};
/**
* The `directionsCallback` function checks the response status and logs the response accordingly.
* @param response - The `response` parameter in the `directionsCallback` function is typically the
* response object returned by a DirectionsService request in a web mapping application. This
* response object contains information such as the status of the request and the directions data.
*/
export const directionsCallback = (response) => {
if (response !== null) {
if (response.status === "OK") {
setResponse(response);
console.log("DirectionsService Response: ", response);
} else {
console.log("response: ", response);
}
}
};
/**
* The function `getPlaceDisplayName` makes an HTTP GET request to retrieve the display name of a
* place using the Google Places API. Get displayname from fetch()
* @param placeId - The `placeId` parameter is a unique identifier for a specific place in the Google
* Places API. It is used to retrieve information about that particular place, such as its display
* name.
*/
export const getPlaceDisplayName = (placeId) =>
httpGet(
`https://places.googleapis.com/v1/places/${placeId}?fields=id,displayName&key=${REACT_APP_GOOGLE_MAPS_API_KEY}`
).then((place) => {
return place.displayName.text;
});
/**
* The function `extractClickedLocation` extracts the clicked location's latitude and longitude,
* retrieves the place name if available, and sets the click location with the name and location
* data.
* @param response - The `response` parameter in the `extractClickedLocation` function seems to
* contain information related to a location, such as latitude and longitude (`latLng`) and possibly
* a `placeId`. The function extracts the location and name of the clicked place from the response
* object and sets them in the state variables
*/
export const extractClickedLocation = async (response) => {
var location = response.latLng;
if (response.hasOwnProperty("placeId")) {
var name = await getPlaceDisplayName(response.placeId);
} else {
var name = null;
}
setSelectedPlace(null);
setSearchLngLat(null);
setCurrentLocation(null);
setClickLocation({ name: name, location: location });
console.log(clickedLocation);
};
/**
* The function `directionMatrixCallback` processes a response from a directions API to extract
* segment information, total distance, and total duration of a path.
* @param response - The `response` parameter in the `directionMatrixCallback` function likely
* contains information about the directions matrix API response. This could include details such as
* origin addresses, destination addresses, distance, and duration between different points. The
* response is structured in a way that allows you to access this information for further processing
* @param status - The `status` parameter in the `directionMatrixCallback` function is used to
* indicate the status of the response received from the directions matrix API. In this case, the
* function checks if the status is equal to "OK" before processing the response data. If the status
* is "OK", it means
*/
export const directionMatrixCallback = (response, status) => {
if (status == "OK") {
var segmentInfo = [];
var origins = response.originAddresses;
var destinations = response.destinationAddresses;
var totalDistance = 0;
var totalDuration = 0;
for (var i = 0; i < origins.length; i++) {
var results = response.rows[i].elements;
var element = results[i];
var distance = element.distance.text;
var duration = element.duration.text;
segmentInfo = [
...segmentInfo,
{ distance: distance, duration: duration },
];
totalDistance += element.distance.value;
totalDuration += element.duration.value;
}
// console.log("segmentInfo", segmentInfo);
setSegmentPath(segmentInfo);
settotalPathDistance(totalDistance);
settotalPathDuration(totalDuration);
}
};
/**
* The `deleteWaypointHandler` function removes a specific waypoint from an array of waypoints in
* JavaScript.
* @param index - The `index` parameter in the `deleteWaypointHandler` function represents the index
* of the waypoint that needs to be deleted from the `waypoints` array.
*/
export const deleteWaypointHandler = (index) => {
// console.log("Before: ", waypoints);
if (index > 0) {
waypoints.splice(index, index);
} else {
waypoints.shift();
}
setwaypoints((waypoints) => [...waypoints]);
// console.log("After: ", waypoints);
};
/**
* The `swapWaypointHandler` function swaps a waypoint with either the previous or next waypoint
* based on the specified direction.
* @param index - The `index` parameter in the `swapWaypointHandler` function represents the position
* of the waypoint that you want to swap with its adjacent waypoint. It is used to identify the
* specific waypoint in the array of waypoints that you want to move either up or down.
* @param direction - The `direction` parameter in the `swapWaypointHandler` function determines
* whether to move the waypoint up or down in the list. Where 0 = go up, 1 = go down.
*/
export const swapWappointHandler = (index, direction) => {
// direction = 0 -> go up
// direction = 1 -> go down
var tempWaypoints = waypoints;
if (direction == 0) {
var previousPoint = tempWaypoints[index - 1];
tempWaypoints[index - 1] = tempWaypoints[index];
tempWaypoints[index] = previousPoint;
} else {
var nextPoint = tempWaypoints[index + 1];
tempWaypoints[index + 1] = tempWaypoints[index];
tempWaypoints[index] = nextPoint;
}
setwaypoints((waypoints) => [...tempWaypoints]);
};
/**
* The function `getToilets` asynchronously retrieves all toilets and returns them.
* @returns The `getToilets` function is returning the result of the `getAllToilets` function after
* awaiting its completion.
*/
export async function getToilets() {
var toilets = await getAllToilets();
// console.log(toilets);
return toilets;
}
/**
* The function `getWaterPoint` asynchronously retrieves all water points and returns them.
* @returns The `getWaterPoint` function is returning the result of the `getAllWaterpoints` function after
* awaiting its completion.
*/
export async function getWaterPoint() {
var waterpoints = await getAllWaterpoints();
// console.log(toilets);
return waterpoints;
}
/**
* The function `getFoodPoints` asynchronously retrieves all food points and returns them.
* @returns The `getFoodPoints` function is returning the result of the `getAllFoodPoints` function after
* awaiting its completion.
*/
export async function getFoodPoints() {
var foodpoints = await getAllFoodPoints();
// console.log(toilets);
return foodpoints;
}