Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Zoom to and show user location #37

Merged
merged 7 commits into from
Apr 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,5 @@ prisma/*.tgz
prisma/backup/*
last_modified_log.txt
prisma/*.db-journal

certificates
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{
"name": "irish-buses",
"version": "0.8.10",
"version": "0.9.0",
"type": "module",
"scripts": {
"dev": "next dev",
"dev": "next dev --experimental-https",
"build": "next build",
"start": "next start",
"lint": "next lint",
Expand Down
37 changes: 37 additions & 0 deletions src/components/Map/LeafletControl.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import L from "leaflet";
import React, { ReactNode, useEffect, useRef } from "react";

const POSITION_CLASSES = {
bottomleft: "leaflet-bottom leaflet-left",
bottomright: "leaflet-bottom leaflet-right",
topleft: "leaflet-top leaflet-left",
topright: "leaflet-top leaflet-right",
};

type ControlPosition = keyof typeof POSITION_CLASSES;
interface LeafLetControlProps {
position?: ControlPosition;
children: ReactNode;
}

const LeafletControl: React.FC<LeafLetControlProps> = ({
position,
children,
}) => {
const divRef = useRef(null);

useEffect(() => {
if (divRef.current) {
L.DomEvent.disableClickPropagation(divRef.current);
L.DomEvent.disableScrollPropagation(divRef.current);
}
});

return (
<div ref={divRef} className={position && POSITION_CLASSES[position]}>
<div className={"leaflet-control"}>{children}</div>
</div>
);
};

export default LeafletControl;
24 changes: 10 additions & 14 deletions src/components/Map/MapContentLayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,8 @@ import {
useMap,
LayersControl,
LayerGroup,
Marker,
Popup,
Pane,
useMapEvents,
Tooltip,
} from "react-leaflet";
import { LatLngTuple } from "leaflet";
import {
Expand All @@ -22,33 +19,24 @@ import {
useRef,
useState,
} from "react";
import { useLocalStorage } from "usehooks-ts";

import type { Route, Stop, StopTime } from "@prisma/client";
import {
formatReadableDelay,
getDelayedTime,
parseDatetimeLocale,
timeSinceLastVehicleUpdate,
} from "@/lib/timeHelpers";
import { parseDatetimeLocale } from "@/lib/timeHelpers";
import Bus from "./Bus";
import usePrevious from "@/hooks/usePrevious";
import isEqual from "react-fast-compare";
import { DateTime } from "luxon";
import useTripUpdates from "@/hooks/useTripUpdates";
import useStopId from "@/hooks/useStopId";
import { SavedStop } from "../SavedStops";
import { Position } from "@turf/helpers";
import MarkerClusterGroup from "./MarkerClusterGroup";
import StopMarker from "./StopMarker";
import StopPopup from "./StopPopup";
import useVehicleUpdates from "@/hooks/useVehicleUpdates";
import { Button } from "../ui/button";
import { TripHandler } from "@/pages";
import LiveText from "../LiveText";
import L from "leaflet";
import GPSGhost from "./GPSGhost";
import { MAP_DEFAULT_ZOOM } from ".";
import UserLocation from "./UserLocation";

export type ValidStop = Stop & {
stopLat: NonNullable<Stop["stopLat"]>;
Expand All @@ -67,6 +55,7 @@ type Props = {
center: LatLngTuple;
routesById: Map<string, Route>;
shape: Position[] | undefined;
showFooter: boolean;
stopsById: Map<string, Stop>;
stopTimes: StopTime[] | undefined;
stopTimesByStopId: Map<StopTime["tripId"], StopTime>;
Expand All @@ -89,6 +78,7 @@ function MapContentLayer({
stopTimesByStopId,
setShowSavedStops,
shape,
showFooter,
stops,
stopsById,
selectedStopId,
Expand Down Expand Up @@ -137,6 +127,7 @@ function MapContentLayer({

const { selectedStop } = useStopId(selectedStopId);

// Set map center location on new route selection
useEffect(() => {
if (!stopIds.length) return;

Expand All @@ -155,6 +146,7 @@ function MapContentLayer({
}
}, [center, map, prevCenter, previousStopIds, stopIds]);

// Set map dimensions onload/onresize
useEffect(() => {
if (map != null) {
const mapContainer = map.getContainer();
Expand Down Expand Up @@ -299,6 +291,7 @@ function MapContentLayer({
))}
</FeatureGroup>
</LayersControl.Overlay>

{/* Route stop markers */}
<LayersControl.Overlay name="Stops" checked>
<FeatureGroup ref={markerGroupRef}>
Expand All @@ -320,6 +313,9 @@ function MapContentLayer({
</FeatureGroup>
</LayersControl.Overlay>

{/* User Location dot */}
<UserLocation className={showFooter ? "!mb-24" : ""} />

{/* Trip line shape */}
{!!shape && (
<LayersControl.Overlay name="Route Path" checked>
Expand Down
8 changes: 4 additions & 4 deletions src/components/Map/StopPopup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -144,19 +144,19 @@ function StopPopup({
)}

<div className="flex flex-col gap-2 mt-4">
{/* <Button
<Button
onClick={() => handleSelectedStop(stopId, false)}
disabled={isPastThisStop}
>
Board here
</Button> */}
</Button>

<Button onClick={() => handleSelectedStop(stopId)}>View trips</Button>
{/* {isValidDestination && (
{isValidDestination && (
<Button onClick={() => handleDestinationStop(stopId)}>
set Destination
</Button>
)} */}
)}
<Button
onClick={() => handleSaveStop(stopId, stopName)}
className="flex flex-row justify-between gap-1"
Expand Down
88 changes: 88 additions & 0 deletions src/components/Map/UserLocation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { useEffect, useRef, useState } from "react";
import { useMap, CircleMarker, Circle, LayerGroup } from "react-leaflet";
import { Locate } from "lucide-react";
import LeafletControl from "./LeafletControl";
import { Button } from "../ui/button";
import { LocationEvent } from "leaflet";
import { cn } from "@/lib/utils";

type Props = { className?: string };

function UserLocation({ className = "" }: Props) {
const map = useMap();
const [userLocation, setUserLocation] = useState<L.LatLng | null>(null);
const [radius, setRadius] = useState(0);
const prevLocation = useRef<L.LatLng | null>();

// Watch current user location
useEffect(() => {
const handleSetLocation = (e: LocationEvent) => {
if (
!prevLocation.current ||
e.latlng.lat !== prevLocation.current.lat ||
e.latlng.lng !== prevLocation.current.lng
) {
prevLocation.current = userLocation;
setUserLocation(e.latlng);
setRadius(e.accuracy);
}
};

map
.locate({
watch: true,
enableHighAccuracy: true,
maximumAge: 10_000,
timeout: 15_000,
})
.on("locationerror", (e) => console.error(e))
.on("locationfound", (e) => handleSetLocation(e));

return () => {
map.stopLocate();
map.off("locationfound", handleSetLocation);
map.off("locationerror");
};
}, [map, userLocation]);

return (
<LeafletControl position={"bottomright"}>
<Button
onClick={() => {
if (userLocation) {
map.flyTo(userLocation);
}
}}
size={"icon"}
className={cn(className, "mb-4 mr-2 lg:mb-10 lg:mr-10")}
>
<Locate />
</Button>
<LayerGroup>
{!!userLocation && (
<>
{radius > 100 && (
<Circle
center={userLocation}
radius={radius}
stroke={false}
fillOpacity={0.2}
/>
)}
<CircleMarker
center={userLocation}
radius={6}
color="white"
stroke
weight={2}
fillColor="#1869E5"
fillOpacity={1}
/>
</>
)}
</LayerGroup>
</LeafletControl>
);
}

export default UserLocation;
5 changes: 4 additions & 1 deletion src/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,8 @@ export default function Home() {
setDestId(null);
};

const showFooter = useMemo(() => !!routeId, [routeId]);

return (
<main className="flex min-h-[100svh] flex-col items-center justify-between text-slate-950 dark:text-white">
<div className="relative w-full">
Expand Down Expand Up @@ -366,6 +368,7 @@ export default function Home() {
selectedDateTime={selectedDateTime}
selectedStopId={stopId}
selectedDestinationStopId={destId}
showFooter={showFooter}
stopTimes={stopTimes}
stopTimesByStopId={stopTimesByStopId}
setShowSavedStops={setShowSavedStops}
Expand Down Expand Up @@ -431,7 +434,7 @@ export default function Home() {
/>
)}

{!!routeId && (
{!!showFooter && (
<Footer
destination={destinationStop}
route={selectedRoute}
Expand Down