Skip to content

Commit

Permalink
Feat: Zoom to and show user location (#37)
Browse files Browse the repository at this point in the history
* fix: reenable board here and set destination stopmarker buttons

* add experimental https to dev server

* add LeafletControl wrapper

* add UserLocation dot

* fix: Location button position when footer visible

* style: change setView to flyTo UserLocation

* bump version to 0.9.0
  • Loading branch information
david-abell authored Apr 23, 2024
1 parent cc9cd4c commit 3a12b30
Show file tree
Hide file tree
Showing 7 changed files with 147 additions and 21 deletions.
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

0 comments on commit 3a12b30

Please sign in to comment.