diff --git a/.gitignore b/.gitignore index 4312d39..553c65c 100644 --- a/.gitignore +++ b/.gitignore @@ -25,4 +25,6 @@ android .DS_Store # Temporary files created by Metro to check the health of the file watcher -.metro-health-check* \ No newline at end of file +.metro-health-check* + +pnpm-lock.yaml diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx index bf004ef..03465fc 100644 --- a/app/(tabs)/index.tsx +++ b/app/(tabs)/index.tsx @@ -4,7 +4,7 @@ import * as turf from "@turf/turf"; import { Stack } from "expo-router"; import { useCallback, useMemo, useRef, useState, useEffect } from "react"; import { View } from "react-native"; -import MapView, { Polygon, Marker, LatLng } from "react-native-maps"; +import MapView, { Polygon, Marker, LatLng, Polyline } from "react-native-maps"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import Toast from "react-native-toast-message"; @@ -17,6 +17,7 @@ import { useAvoidanceAreas, useConstructionAreas, useInsertAvoidanceArea, + getRoute } from "~/utils/api-hooks"; import useMapIcons from "~/utils/useMapIcons"; @@ -27,6 +28,7 @@ import { type LocationDetailsBottomSheetRef, } from "~/components/LocationDetailsBottomSheet"; import { searchPlaces, getPlaceDetails } from "~/utils/googlePlaces"; +import decode from "~/utils/decode_polyline"; export default function Home() { // hooks @@ -44,6 +46,7 @@ export default function Home() { const [clickedPoint, setClickedPoint] = useState(null); const [reportStep, setReportStep] = useState(0); const [zoomLevel, setZoomLevel] = useState(15); + const [Route, setRoute] = useState(null); // Minimum zoom level to show POIs (higher = more zoomed in) const MIN_ZOOM_FOR_POIS = 16; @@ -55,6 +58,7 @@ export default function Home() { const { data: constructionAreas } = useConstructionAreas(); const { data: POIs } = usePOIs(); const { mutateAsync: insertAvoidanceArea } = useInsertAvoidanceArea(); + // getRoute([[-97.733785,30.282635],[-97.733731,30.285145]], [[[-97.734269,30.284691],[-97.733454,30.284654],[-97.733669,30.283366],[-97.734708,30.283932],[-97.734269,30.284691]]]); const testGooglePlaces = async () => { console.log("Testing Google Places..."); @@ -71,6 +75,7 @@ export default function Home() { testGooglePlaces(); }, []); + const getMapIcon = useCallback( (poiType: any, metadata: any) => { switch (poiType) { @@ -125,6 +130,7 @@ export default function Home() { // Handle avoidance area click const handleAvoidanceAreaPress = (polygonId: string) => { + if (polygonId[0] == 'C') return; // construction areas if (isReportMode) return; avoidanceAreaBottomSheetRef.current?.present({ id: polygonId }); }; @@ -133,8 +139,6 @@ export default function Home() { const handlePOIPress = (poi: any) => { if (isReportMode) return; poiBottomSheetRef.current?.present({ poi }); - if (polygonId[0] == 'C') return; // construction areas - bottomSheetRef.current?.present({ id: polygonId }); }; const polygons = useMemo( @@ -184,8 +188,8 @@ export default function Home() { const markers = useMemo( () => { if (POIs && !isReportMode) { - console.log("Pois"); - console.log(POIs); + // console.log("Pois"); + // console.log(POIs); } const poiMarkers = !isReportMode && zoomLevel >= MIN_ZOOM_FOR_POIS @@ -199,7 +203,7 @@ export default function Home() { icon: getMapIcon(poi.poi_type, poi.metadata) || undefined, }; // 📝 ADDED CONSOLE LOGGING HERE - console.log(`POI Marker for ID ${marker.id}:`, marker); + // console.log(`POI Marker for ID ${marker.id}:`, marker); return marker; }) : []; @@ -228,6 +232,30 @@ export default function Home() { [POIs, aaPointsReport, mapIcons, getMapIcon, isReportMode, clickedPoint, zoomLevel], ); + + const getDirections = (target: any[]) => { + const UT_TOWER = [-97.73942, 30.28614]; + let res = getRoute([UT_TOWER, target.slice(0, 2)], + polygons.map((poly) => poly.coordinates.map((coord: any) => [coord.longitude, coord.latitude])) + ) + + // console.log(decode({value: res.routes.geometry})); + res.then((result) => { + // console.log(decode(result.routes[0].geometry)); + setRoute(decode(result.routes[0].geometry).map( + (coord) => ({ + latitude: coord[1], + longitude: coord[0] + }) + )); + }); + // console.log(target.slice(0, 2)); + // console.log(polygons.map((poly) => poly.coordinates.map((coord: any) => [coord.longitude, coord.latitude]))) + + // console.log(res); + } + + const handleSelectLocation = async (location: { id: string; name: string; @@ -304,7 +332,10 @@ export default function Home() { {/* POI Bottom Sheet */} - + + + {/* Routing Mode Overlay */} + {/* */} {/* Location Details Bottom Sheet */} @@ -341,6 +372,17 @@ export default function Home() { /> ))} + {/* Render Polylines */} + {(Route !== null) && ( + + )} + {/* Render markers */} {markers.map((marker) => ( ; allPOIs: any[]; + getDirections: (target: any[]) => void; } interface POIContentProps { poi: any; allPOIs: any[]; + getDirections: (target: any[]) => void; } -const POIContent = ({ poi, allPOIs }: POIContentProps) => { +const POIContent = ({ poi, allPOIs, getDirections }: POIContentProps) => { const mapIcons = useMapIcons(); const [selectedEntrance, setSelectedEntrance] = useState(""); const [hours, setHours] = useState("Loading..."); @@ -294,7 +296,8 @@ const POIContent = ({ poi, allPOIs }: POIContentProps) => { backgroundColor: "#BF5700", height: 41.32, paddingHorizontal: 8, borderRadius: 9.31, alignItems: "center", flexDirection: "row", justifyContent: "center", marginBottom: 8, - }}> + }} + onPress={() => getDirections(poi.location_geojson.coordinates)}> Get Directions @@ -304,7 +307,7 @@ const POIContent = ({ poi, allPOIs }: POIContentProps) => { ); }; -const POIBottomSheet = ({ ref, allPOIs }: POIBottomSheetProps) => { +const POIBottomSheet = ({ ref, allPOIs, getDirections }: POIBottomSheetProps) => { const bottomTabBarHeight = useBottomTabBarHeight(); return ( @@ -319,7 +322,7 @@ const POIBottomSheet = ({ ref, allPOIs }: POIBottomSheetProps) => { > {({ data }) => { if (!data?.poi) return null; - return ; + return ; }} ); diff --git a/components/RouteOverlaySheet.tsx b/components/RouteOverlaySheet.tsx new file mode 100644 index 0000000..ce12c13 --- /dev/null +++ b/components/RouteOverlaySheet.tsx @@ -0,0 +1,49 @@ +import { BottomSheetModal } from "@gorhom/bottom-sheet"; +import { useBottomTabBarHeight } from "@react-navigation/bottom-tabs"; +import { ForwardedRef } from "react"; + +import colors from "~/types/colors"; + + +interface RouteContentProps { + route: any; +} + +const RouteContent = ({ route } : RouteContentProps) => { + return ( + <> + ) +} + + + +interface RouteOverlaySheetProps { + ref: ForwardedRef; +} + +interface RouteData { + route: any; +} + +const RouteOverlaySheet = ({ ref }: RouteOverlaySheetProps) => { + const bottomTabBarHeight = useBottomTabBarHeight(); + + return ( + + ref={ref} + bottomInset={bottomTabBarHeight} + backgroundStyle={{ borderRadius: 32 }} + enableDynamicSizing={false} + snapPoints={["50%"]} + handleIndicatorStyle={{ backgroundColor: colors.theme.majorgridline, width: 80 }} + enableContentPanningGesture={false} + > + {({ data }) => { + if (!data?.route) return null; + return ; + }} + + ); +}; + +export default RouteOverlaySheet; \ No newline at end of file diff --git a/utils/api-client.ts b/utils/api-client.ts index 4bbedcd..e04a110 100644 --- a/utils/api-client.ts +++ b/utils/api-client.ts @@ -47,6 +47,43 @@ class ApiClient { return await response.text(); } + + async getRoute(waypoints: any[], avoiding: any[]) { + const FEATURE_URL = "https://api.openrouteservice.org/v2/directions/wheelchair"; + const TOKEN = process.env.EXPO_PUBLIC_OPENROUTE_KEY || ""; + + // multipoly format reference: https://en.wikipedia.org/wiki/GeoJSON + + let res = await fetch( + FEATURE_URL, + { + method: "post", + headers: { + 'Accept': 'application/json, application/geo+json, application/gpx+xml, img/png; charset=utf-8', + 'Authorization': TOKEN, + 'Content-Type': 'application/json; charset=utf-8' + }, + body: JSON.stringify( + {"coordinates":waypoints, + "options":{ + "avoid_polygons":{ + "type":"MultiPolygon", + "coordinates":avoiding.map((poly) => [poly]) + } + } + } + ) + + } + ); + console.log(res); + if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`); + const json = await res.json(); + console.log(json); + return json; + } + + // Get profile by ID async getProfile(id: number) { return this.request(`/profiles?id=${id}`); diff --git a/utils/api-hooks.ts b/utils/api-hooks.ts index 5503622..2e2fb73 100644 --- a/utils/api-hooks.ts +++ b/utils/api-hooks.ts @@ -1,6 +1,6 @@ // TanStack Query hooks for the Hono backend import { useBottomTabBarHeight } from "@react-navigation/bottom-tabs"; -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { useMutation, useQuery, useQueryClient, UseQueryResult } from "@tanstack/react-query"; import { Polygon } from "geojson"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import Toast from "react-native-toast-message"; @@ -17,6 +17,14 @@ export const queryKeys = { profile: (id: number) => ["profile", id] as const, }; + +// get route between 2+ points +export async function getRoute(waypoints: any[], avoiding: any[]) { + // TODO implement caching later + return await apiClient.getRoute(waypoints, avoiding) +} + + // fetch all POIs export function usePOIs() { return useQuery({ diff --git a/utils/decode_polyline.ts b/utils/decode_polyline.ts new file mode 100644 index 0000000..9a821aa --- /dev/null +++ b/utils/decode_polyline.ts @@ -0,0 +1,48 @@ +// Source - https://stackoverflow.com/q/40694161 +// Posted by Neil Simpson +// Retrieved 2026-03-08, License - CC BY-SA 3.0 + +export default function decode( value: any ) { + + var values = decode.integers( value ) + var points = [] + + for( var i = 0; i < values.length; i += 2 ) { + points.push([ + ( values[ i + 1 ] += ( values[ i - 1 ] || 0 ) ) / 1e5, + ( values[ i + 0 ] += ( values[ i - 2 ] || 0 ) ) / 1e5, + ]) + } + + return points + +} + +decode.sign = function( value: any ) { + return value & 1 ? ~( value >>> 1 ) : ( value >>> 1 ) +} + +decode.integers = function( value: any ) { + + var values = [] + var byte = 0 + var current = 0 + var bits = 0 + + for( var i = 0; i < value.length; i++ ) { + + byte = value.charCodeAt( i ) - 63 + current = current | (( byte & 0x1F ) << bits ) + bits = bits + 5 + + if( byte < 0x20 ) { + values.push( decode.sign( current ) ) + current = 0 + bits = 0 + } + + } + + return values + +}