diff --git a/ui/package-lock.json b/ui/package-lock.json index 1da70c4..c2a4afc 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -21,6 +21,7 @@ "@types/node": "^17.0.39", "@types/react": "^18.0.11", "@types/react-dom": "^18.0.5", + "h3-js": "^3.7.2", "react": "^18.1.0", "react-dom": "^18.1.0", "react-scripts": "5.0.1", @@ -8623,6 +8624,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/h3-js": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/h3-js/-/h3-js-3.7.2.tgz", + "integrity": "sha512-LPjlHSwB9zQZrMqKloCZmmmt3yZzIK7nqPcXqwU93zT3TtYG6jP4tZBzAPouxut7lLjdFbMQ75wRBiKfpsnY7w==", + "engines": { + "node": ">=4", + "npm": ">=3", + "yarn": ">=1.3.0" + } + }, "node_modules/handle-thing": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", @@ -23041,6 +23052,11 @@ "duplexer": "^0.1.2" } }, + "h3-js": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/h3-js/-/h3-js-3.7.2.tgz", + "integrity": "sha512-LPjlHSwB9zQZrMqKloCZmmmt3yZzIK7nqPcXqwU93zT3TtYG6jP4tZBzAPouxut7lLjdFbMQ75wRBiKfpsnY7w==" + }, "handle-thing": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", diff --git a/ui/package.json b/ui/package.json index 7a287aa..5df15b8 100644 --- a/ui/package.json +++ b/ui/package.json @@ -16,6 +16,7 @@ "@types/node": "^17.0.39", "@types/react": "^18.0.11", "@types/react-dom": "^18.0.5", + "h3-js": "^3.7.2", "react": "^18.1.0", "react-dom": "^18.1.0", "react-scripts": "5.0.1", diff --git a/ui/src/App.tsx b/ui/src/App.tsx index a3314e7..f48f8a3 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -1,11 +1,12 @@ import React, { ReactElement } from "react"; import "./App.css"; -import { GoogleMap, LoadScript } from "@react-google-maps/api"; +import { GoogleMap, LoadScript, Polygon } from "@react-google-maps/api"; import Marker from "./Marker"; import mapStyles from "./mapStyles.json"; import { ThemeProvider, createTheme } from "@mui/material/styles"; import { Box, + Stack, FormControlLabel, FormGroup, Grid, @@ -29,6 +30,9 @@ import { faker } from "@faker-js/faker"; import listDrivers from "./request/listDrivers"; import getNearestDrivers from "./request/getNearestDrivers"; import updateDriverLocations from "./request/updateDriverLocations"; +import { getPolygons, getPolygonsOutput } from "./getPolygons"; +import ResolutionControl from "./ResolutionControl"; +import Hexagons, { pointsToPaths, buildOptions } from "./Hexagons"; const darkTheme = createTheme({ palette: { @@ -65,7 +69,7 @@ function newDriverName(): string { function MyMap() { const apiKey = process.env.REACT_APP_GOOGLE_MAPS_API_KEY as string; const [focusedDriverId, setFocusedDriverId] = React.useState(""); - const [queryMode, setQueryMode] = React.useState(false); + const [queryMode, setQueryMode] = React.useState(true); const [driverLocationsState, setDriverLocationsState] = React.useState({ byId: {}, @@ -77,6 +81,11 @@ function MyMap() { allIds: [], } as NormalizedDriverLocations); const [searchResults, setSearchResults] = React.useState([]); + const [resolution, setResolution] = React.useState(9); + const [pickupLocation, setPickupLocation] = React.useState({ + latitude: 40.72106092795603, + longitude: -73.95246141893465, + } as LatLng); const newDriverLocations = getDriverLocationsFromState( newDriverLocationsState @@ -92,14 +101,18 @@ function MyMap() { getDriverLocations().catch(console.error); }); + const [polyOut, setPolyOut] = React.useState(); const getNearbyDrivers = async (e: google.maps.MapMouseEvent) => { const lat = e.latLng?.lat() ?? 0; const lng = e.latLng?.lng() ?? 0; - const res = await getNearestDrivers({ + const point: LatLng = { latitude: lat, longitude: lng, - } as LatLng); + }; + setPickupLocation(point); + setPolyOut(getPolygons(pickupLocation, resolution)); + const res = await getNearestDrivers(point); setSearchResults(res?.results ?? []); }; @@ -119,75 +132,89 @@ function MyMap() { ); }; + const onLoad = (polygon: google.maps.Polygon) => { + console.log("polygon: ", polygon); + }; + return ( - - - - setQueryMode(!queryMode)} - /> - } - label="Mode" - /> - - - - {queryMode ? ( - Click anywhere to rank nearby drivers - ) : ( - Click to add new drivers - )} - - - - () => - setFocusedDriverId(driverId)} - buildHandleMouseOut={(driverId: string) => () => - setFocusedDriverId("")} - queryMode={queryMode} - onUpload={() => - updateDriverLocations( - getDriverLocationsFromState(newDriverLocationsState) - ) - } - driverLocations={newDriverLocations} - /> - - - - - - - {driverLocations.map((dl: DriverLocation, i: number) => ( - sr.driver.driverId) - .includes(dl.driverId)} + + + + + + setQueryMode(!queryMode)} /> - ))} - {newDriverLocations.map((dl: DriverLocation, i: number) => ( - - ))} - <> - - - + } + label="Mode" + /> + + + + {queryMode ? ( + Click anywhere to rank nearby drivers + ) : ( + Click to add new drivers + )} + + + + () => + setFocusedDriverId(driverId)} + buildHandleMouseOut={(driverId: string) => () => + setFocusedDriverId("")} + queryMode={queryMode} + onUpload={() => + updateDriverLocations( + getDriverLocationsFromState(newDriverLocationsState) + ) + } + searchResults={searchResults} + driverLocations={newDriverLocations} + /> + + + + + + + {driverLocations.map((dl: DriverLocation, i: number) => ( + sr.driver.driverId) + .includes(dl.driverId)} + /> + ))} + {newDriverLocations.map((dl: DriverLocation, i: number) => ( + + ))} + {pickupLocation && ( + + )} + {polyOut && } + + + + - + ); } diff --git a/ui/src/Hexagons/index.tsx b/ui/src/Hexagons/index.tsx new file mode 100644 index 0000000..8989ad9 --- /dev/null +++ b/ui/src/Hexagons/index.tsx @@ -0,0 +1,62 @@ +import React from "react"; +import { getPolygons, getPolygonsOutput } from "../getPolygons"; +import { LatLng } from "../types"; +import { Polygon } from "@react-google-maps/api"; + +interface HexagonsProps { + polyOut: getPolygonsOutput; +} + +export function buildOptions(k: number): google.maps.PolygonOptions { + const fillColor = k === 0 ? "lightblue" : k === 1 ? "lightblue" : "lightblue"; + return { + fillColor, + fillOpacity: 0.3, + strokeColor: "red", + strokeOpacity: 1, + strokeWeight: 1, + clickable: false, + draggable: false, + editable: false, + geodesic: false, + zIndex: 1, + } as google.maps.PolygonOptions; +} + +export default function Hexagons({ polyOut }: HexagonsProps) { + const onLoad = (polygon: google.maps.Polygon) => { + console.log("polygon: ", polygon); + }; + return ( + <> + {polyOut?.ring0.map((points: LatLng[]) => ( + + )) ?? null} + {polyOut?.ring1.map((points: LatLng[]) => ( + + )) ?? null} + {polyOut?.ring2.map((points: LatLng[]) => ( + + )) ?? null} + + ); +} + +export function pointsToPaths(points: LatLng[]): google.maps.LatLngLiteral[] { + return points.map( + (p: LatLng) => + ({ lat: p.latitude, lng: p.longitude } as google.maps.LatLngLiteral) + ); +} diff --git a/ui/src/Marker/index.tsx b/ui/src/Marker/index.tsx index 83013b4..8e7adff 100644 --- a/ui/src/Marker/index.tsx +++ b/ui/src/Marker/index.tsx @@ -12,32 +12,60 @@ function getIconPath(element: React.ReactElement): string { } interface MyMarkerProps { - driverLocation: DriverLocation; + /** + * The marker's location + */ + location: LatLng; handleMouseOver?: (e: google.maps.MapMouseEvent) => void; + handleMouseOut?: (e: google.maps.MapMouseEvent) => void; handleClick?: (e: google.maps.MapMouseEvent) => void; + + /** + * Whether the marker is for the requested pickup location + */ + pickupLocation?: boolean; + /** + * Whether the marker is for a new driver location that will be created in bulk. + */ cached?: boolean; + /** + * Whether the marker represents a nearby driver to the requested pickup location. + */ isNear?: boolean; } export default function MyMarker(props: MyMarkerProps) { - const { cached, isNear, driverLocation, handleClick, handleMouseOver } = - props; - const color = isNear ? "green" : cached ? "orange" : "#FFD700"; - const p = driverLocation.currentLocation; + const { + pickupLocation, + cached, + isNear, + location: p, + handleClick, + handleMouseOver, + handleMouseOut, + } = props; + let color = "#FFD700"; + if (isNear) color = "green"; + if (cached) color = "orange"; + if (pickupLocation) color = "purple"; + if (pickupLocation) + console.log("rendering marker for pickup", pickupLocation, p); return ( console.log("marker unmount")} title={`(${p.latitude}, ${p.longitude})`} position={{ lat: p.latitude, lng: p.longitude }} opacity={1} onClick={handleClick} onMouseOver={handleMouseOver} + onMouseOut={handleMouseOut} icon={{ path: getIconPath(), fillColor: color, fillOpacity: 0.9, scale: 0.7, strokeColor: color, - strokeWeight: 1, + strokeWeight: 0.5, }} /> ); diff --git a/ui/src/PointList/index.tsx b/ui/src/PointList/index.tsx index 63fbfb7..bf9f107 100644 --- a/ui/src/PointList/index.tsx +++ b/ui/src/PointList/index.tsx @@ -9,7 +9,7 @@ import { Divider, Avatar, } from "@mui/material"; -import { DriverLocation } from "../types"; +import { DriverLocation, SearchResult } from "../types"; function stringToColor(string: string) { let hash = 0; @@ -74,6 +74,7 @@ function Item(props: ItemProps) { } interface DriverListProps { + searchResults: SearchResult[]; driverLocations: DriverLocation[]; onUpload?: MouseEventHandler; /** @@ -86,6 +87,7 @@ interface DriverListProps { export default function DriverList(props: DriverListProps) { const { + searchResults, driverLocations, onUpload: handleClick, queryMode, @@ -95,18 +97,28 @@ export default function DriverList(props: DriverListProps) { return ( - {queryMode ? "Drivers List" : "New Drivers List"} + {queryMode ? "Nearby Drivers List" : "New Drivers List"} - {driverLocations.map((p: DriverLocation, i: number) => ( - - ))} + {queryMode && + searchResults.map((sr: SearchResult, i: number) => ( + + ))} + {!queryMode && + driverLocations.map((p: DriverLocation, i: number) => ( + + ))} {!queryMode && ( diff --git a/ui/src/ResolutionControl/index.tsx b/ui/src/ResolutionControl/index.tsx new file mode 100644 index 0000000..687c34d --- /dev/null +++ b/ui/src/ResolutionControl/index.tsx @@ -0,0 +1,36 @@ +import { IconButton, Stack, Typography } from "@mui/material"; +import { Add as AddIcon, Remove as RemoveIcon } from "@mui/icons-material"; +import { Dispatch, SetStateAction } from "react"; + +interface ResolutionControlProps { + resolution: number; + setResolution: Dispatch>; +} + +const MAX_RESOLUTION = 10; +const MIN_RESOLUTION = 7; + +export default function ResolutionControl(props: ResolutionControlProps) { + const { resolution, setResolution } = props; + return ( + + { + if (resolution > MIN_RESOLUTION) setResolution(resolution - 1); + }} + > + + + {resolution} + { + if (resolution < MAX_RESOLUTION) setResolution(resolution + 1); + }} + > + + + + ); +} diff --git a/ui/src/getPolygons.ts b/ui/src/getPolygons.ts new file mode 100644 index 0000000..9df8ca3 --- /dev/null +++ b/ui/src/getPolygons.ts @@ -0,0 +1,34 @@ +import { LatLng } from "./types"; +import { geoToH3, H3Index, h3ToGeoBoundary, kRing } from "h3-js"; + +export interface getPolygonsOutput { + ring0: LatLng[][]; + ring1: LatLng[][]; + ring2: LatLng[][]; +} + +export function getPolygons(l: LatLng, res: number): getPolygonsOutput { + const index = geoToH3(l.latitude, l.longitude, res); + console.log("index", index); + + const indexes0 = kRing(index, 0); + const indexes1 = kRing(index, 1); + const indexes2 = kRing(index, 2); + + return { + ring0: indexes0.map(getIndexVertexes), + ring1: indexes1.map(getIndexVertexes), + ring2: indexes2.map(getIndexVertexes), + } as getPolygonsOutput; +} + +function getIndexVertexes(index: H3Index): LatLng[] { + const arr = h3ToGeoBoundary(index); + return arr.map( + (p: number[]) => + ({ + latitude: p[0], + longitude: p[1], + } as LatLng) + ); +}