Source: pages-sections/Routeplanner-Sections/Map.js

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;
}