diff --git a/package-lock.json b/package-lock.json index 524f51f..83055d2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,16 +29,19 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "framer-motion": "^12.42.2", + "leaflet": "^1.9.4", "lucide-react": "^1.25.0", "react": "^19.2.7", "react-dom": "^19.2.7", "react-hook-form": "^7.82.0", + "react-leaflet": "^5.0.0", "react-router-dom": "^7.18.1", "tailwind-merge": "^3.6.0", "zod": "^4.4.3" }, "devDependencies": { "@tailwindcss/vite": "^4.3.3", + "@types/leaflet": "^1.9.21", "@types/node": "^24.13.3", "@types/react": "^19.2.17", "@types/react-dom": "^19.2.3", @@ -1488,6 +1491,17 @@ "integrity": "sha512-xnXE7wG13PI+cxieVssYXlQJuYVRhH9NBoxt3KNwzghDIA69GMm7d4wXRouHIYjE+KvS6U/MsMO73NdS2MH9ZA==", "license": "MIT" }, + "node_modules/@react-leaflet/core": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-3.0.0.tgz", + "integrity": "sha512-3EWmekh4Nz+pGcr+xjf0KNyYfC3U2JjnkWsh0zcqaexYqmmB5ZhH37kz41JXGmKzpaMZCnPofBBm64i+YrEvGQ==", + "license": "Hippocratic-2.1", + "peerDependencies": { + "leaflet": "^1.9.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + } + }, "node_modules/@rolldown/binding-android-arm64": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", @@ -2378,6 +2392,23 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/leaflet": { + "version": "1.9.21", + "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.21.tgz", + "integrity": "sha512-TbAd9DaPGSnzp6QvtYngntMZgcRk+igFELwR2N99XZn7RXUdKgsXMR+28bUO0rPsWp8MIu/f47luLIQuSLYv/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, "node_modules/@types/node": { "version": "24.13.3", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", @@ -3025,6 +3056,12 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/leaflet": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", + "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", + "license": "BSD-2-Clause" + }, "node_modules/lightningcss": { "version": "1.33.0", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", @@ -3536,6 +3573,20 @@ "react": "^16.8.0 || ^17 || ^18 || ^19" } }, + "node_modules/react-leaflet": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-5.0.0.tgz", + "integrity": "sha512-CWbTpr5vcHw5bt9i4zSlPEVQdTVcML390TjeDG0cK59z1ylexpqC6M1PJFjV8jD7CF+ACBFsLIDs6DRMoLEofw==", + "license": "Hippocratic-2.1", + "dependencies": { + "@react-leaflet/core": "^3.0.0" + }, + "peerDependencies": { + "leaflet": "^1.9.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + } + }, "node_modules/react-remove-scroll": { "version": "2.7.2", "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", diff --git a/package.json b/package.json index ca451da..45f4392 100644 --- a/package.json +++ b/package.json @@ -31,16 +31,19 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "framer-motion": "^12.42.2", + "leaflet": "^1.9.4", "lucide-react": "^1.25.0", "react": "^19.2.7", "react-dom": "^19.2.7", "react-hook-form": "^7.82.0", + "react-leaflet": "^5.0.0", "react-router-dom": "^7.18.1", "tailwind-merge": "^3.6.0", "zod": "^4.4.3" }, "devDependencies": { "@tailwindcss/vite": "^4.3.3", + "@types/leaflet": "^1.9.21", "@types/node": "^24.13.3", "@types/react": "^19.2.17", "@types/react-dom": "^19.2.3", diff --git a/src/components/BusCard/BusCard.tsx b/src/components/BusCard/BusCard.tsx index f44453e..e0511d9 100644 --- a/src/components/BusCard/BusCard.tsx +++ b/src/components/BusCard/BusCard.tsx @@ -5,9 +5,11 @@ import type { BusRoute } from '@/types'; interface BusCardProps { route: BusRoute; + isSelected?: boolean; + onSelect?: () => void; } -export default function BusCard({ route }: BusCardProps) { +export default function BusCard({ route, isSelected, onSelect }: BusCardProps) { const [expanded, setExpanded] = useState(false); return ( @@ -15,8 +17,14 @@ export default function BusCard({ route }: BusCardProps) { layout whileHover={{ y: -3 }} transition={{ duration: 0.2 }} - className="bg-white border border-[#E5E7EB] rounded-2xl overflow-hidden" - style={{ boxShadow: '0 2px 15px -3px rgba(0,0,0,0.07)' }} + onClick={onSelect} + className={`bg-white border rounded-2xl overflow-hidden cursor-pointer transition-all ${ + isSelected ? 'border-transparent shadow-lg scale-[1.01]' : 'border-[#E5E7EB]' + }`} + style={{ + boxShadow: isSelected ? `0 10px 25px -5px ${route.color}25, 0 8px 10px -6px ${route.color}25` : '0 2px 15px -3px rgba(0,0,0,0.07)', + outline: isSelected ? `2px solid ${route.color}` : 'none' + }} > {/* Color Strip */}
@@ -71,7 +79,10 @@ export default function BusCard({ route }: BusCardProps) { {/* Toggle Stops */}
) : ( - - {filteredRoutes.map((route) => ( - - - - ))} - +
+ {/* Routes List */} +
+ + {filteredRoutes.map((route) => ( + + setSelectedBusRoute(route)} + /> + + ))} + +
+ + {/* Interactive Map */} +
+ +
+
)} )} diff --git a/src/types/index.ts b/src/types/index.ts index 70ceadf..7b8388e 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -51,6 +51,8 @@ export interface Faculty { export interface BusStop { name: string; time: string; + lat?: number; + lng?: number; } export interface BusRoute { @@ -63,6 +65,10 @@ export interface BusRoute { arrivalTime: string; stops: BusStop[]; color: string; + from_lat?: number; + from_lng?: number; + to_lat?: number; + to_lng?: number; } // ─── Notes / PYQs ──────────────────────────────────────────────────────────── diff --git a/telegram-bot/coordinates_cache.json b/telegram-bot/coordinates_cache.json new file mode 100644 index 0000000..2d0d88f --- /dev/null +++ b/telegram-bot/coordinates_cache.json @@ -0,0 +1,182 @@ +{ + "7 H Bus Depot": { + "lat": 13.0347162, + "lng": 80.2054383 + }, + "Aazar Khana Bus Stop": { + "lat": 13.0118, + "lng": 80.0214 + }, + "Adambakkam Bus Depot": { + "lat": 13.0118, + "lng": 80.0214 + }, + "Adyar Depot": { + "lat": 12.9984686, + "lng": 80.2561546 + }, + "Agaram": { + "lat": 13.1158603, + "lng": 80.2300407 + }, + "Ajax": { + "lat": 13.1685, + "lng": 80.3005 + }, + "Ajeya Stadium": { + "lat": 13.1327901, + "lng": 80.1021537 + }, + "Alwar Thirunagar": { + "lat": 13.0118, + "lng": 80.0214 + }, + "Ambattur": { + "lat": 13.1196788, + "lng": 80.1496969 + }, + "Aminijkarai": { + "lat": 13.0118, + "lng": 80.0214 + }, + "Amjikarai Market": { + "lat": 13.0118, + "lng": 80.0214 + }, + "Amutha School": { + "lat": 13.0822304, + "lng": 80.1694593 + }, + "Anagaputhur": { + "lat": 12.974841, + "lng": 80.1270021 + }, + "Andarkuppam": { + "lat": 13.1938755, + "lng": 80.2685646 + }, + "Anna Arch": { + "lat": 13.075358, + "lng": 80.2176034 + }, + "Anna Nagar Metro": { + "lat": 13.0961458, + "lng": 80.1986901 + }, + "Anna Nagar Roundtana": { + "lat": 13.085, + "lng": 80.2101 + }, + "Anna Nagar West Depot": { + "lat": 13.0939332, + "lng": 80.1980855 + }, + "Apollo": { + "lat": 13.1189, + "lng": 80.2874 + }, + "Aranganathan Subway": { + "lat": 13.0282195, + "lng": 80.2252655 + }, + "Aranvoyal": { + "lat": 13.1024693, + "lng": 79.9540238 + }, + "Aravind Hospital": { + "lat": 13.0532913, + "lng": 80.1249542 + }, + "Arcot Bus Stand": { + "lat": 13.0118, + "lng": 80.0214 + }, + "Arignar Anna Gov College": { + "lat": 13.0118, + "lng": 80.0214 + }, + "Arul Nagar": { + "lat": 12.9542167, + "lng": 80.1933583 + }, + "Arumbakkam": { + "lat": 13.074371, + "lng": 80.2081312 + }, + "Arumbakkam (SBI Bank)": { + "lat": 13.074371, + "lng": 80.2081312 + }, + "Arumbakkam Panchaliamman Koil": { + "lat": 13.0118, + "lng": 80.0214 + }, + "Arun Excello": { + "lat": 12.8173765, + "lng": 80.1626599 + }, + "Arunmathi theatre": { + "lat": 13.0118, + "lng": 80.0214 + }, + "Aryagowda Road": { + "lat": 13.0118, + "lng": 80.0214 + }, + "Ashok Pillar": { + "lat": 13.0336, + "lng": 80.2114 + }, + "Avadi Checkpost": { + "lat": 13.1166, + "lng": 80.1013 + }, + "Avadi J.P.Garden": { + "lat": 13.1166, + "lng": 80.1013 + }, + "Avadi Ponnu Store": { + "lat": 13.1166, + "lng": 80.1013 + }, + "Avadi mankoil": { + "lat": 13.1166, + "lng": 80.1013 + }, + "Ayanawaram Noor Hotel": { + "lat": 13.0118, + "lng": 80.0214 + }, + "Ayanawaram Railway Quarters": { + "lat": 13.0118, + "lng": 80.0214 + }, + "Ayanawaram Signal": { + "lat": 13.0118, + "lng": 80.0214 + }, + "Ayappakkam Parking": { + "lat": 13.0118, + "lng": 80.0214 + }, + "Ayappakkam Petrol Bunk": { + "lat": 13.0118, + "lng": 80.0214 + }, + "Ayappakkam SBI ATM": { + "lat": 13.0118, + "lng": 80.0214 + }, + "Ayyampettai": { + "lat": 10.8962881, + "lng": 79.1886395 + }, + "Ayyangarkulam": { + "lat": 13.0118, + "lng": 80.0214 + }, + "Ayyapanthangal": { + "lat": 13.041, + "lng": 80.1412 + } +} \ No newline at end of file diff --git a/telegram-bot/geocode_bus_routes.py b/telegram-bot/geocode_bus_routes.py new file mode 100644 index 0000000..2c0d72c --- /dev/null +++ b/telegram-bot/geocode_bus_routes.py @@ -0,0 +1,189 @@ +import os +import json +import time +import requests +import re + +# File Paths +CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) +BUS_ROUTES_PATH = os.path.abspath(os.path.join(CURRENT_DIR, "..", "backend", "src", "main", "resources", "bus_routes.json")) +CACHE_PATH = os.path.join(CURRENT_DIR, "coordinates_cache.json") + +# Predefined coordinates for hard-to-geocode stops or fallback landmarks in Chennai +STATIC_COORDINATES = { + "RIT Campus": {"lat": 13.0118, "lng": 80.0214}, + "Rajalakshmi Institute of Technology": {"lat": 13.0118, "lng": 80.0214}, + "Kuthambakkam": {"lat": 13.0125, "lng": 80.0210}, + "Lift Gate": {"lat": 13.1610, "lng": 80.3015}, + "Wimco Market": {"lat": 13.1782, "lng": 80.3021}, + "Ajax": {"lat": 13.1685, "lng": 80.3005}, + "Theradi": {"lat": 13.1620, "lng": 80.2995}, + "Apollo": {"lat": 13.1189, "lng": 80.2874}, + "Mint": {"lat": 13.1075, "lng": 80.2785}, + "Parry's": {"lat": 13.0898, "lng": 80.2882}, + "Central": {"lat": 13.0827, "lng": 80.2707}, + "Egmore": {"lat": 13.0792, "lng": 80.2598}, + "Dasprakash": {"lat": 13.0811, "lng": 80.2524}, + "Ice House Police Station": {"lat": 13.0520, "lng": 80.2762}, + "Choolaimedu Subway": {"lat": 13.0645, "lng": 80.2224}, + "Kellys Signal": {"lat": 13.0820, "lng": 80.2435}, + "Anna Nagar Roundtana": {"lat": 13.0850, "lng": 80.2101}, + "VR Mall": {"lat": 13.0722, "lng": 80.1989}, + "Mogappair West depot": {"lat": 13.0805, "lng": 80.1654}, + "Nolambur": {"lat": 13.0737, "lng": 80.1614}, + "MGR University": {"lat": 13.0337, "lng": 80.2014}, + "Guindy": {"lat": 13.0067, "lng": 80.2206}, + "Butt Road": {"lat": 13.0084, "lng": 80.1987}, + "Porur": {"lat": 13.0382, "lng": 80.1565}, + "Ayyapanthangal": {"lat": 13.0410, "lng": 80.1412}, + "Poonamallee": {"lat": 13.0473, "lng": 80.0945}, + "Tambaram": {"lat": 12.9238, "lng": 80.1214}, + "Avadi": {"lat": 13.1166, "lng": 80.1013}, + "Chrompet": {"lat": 12.9610, "lng": 80.1462}, + "Koyambedu": {"lat": 13.0728, "lng": 80.2019}, + "Koyambedu Metro": {"lat": 13.0728, "lng": 80.2019}, + "CMBT": {"lat": 13.0678, "lng": 80.2054}, + "Aminjikarai": {"lat": 13.0734, "lng": 80.2195}, + "Skywalk": {"lat": 13.0741, "lng": 80.2185}, + "Maduravoyal": {"lat": 13.0673, "lng": 80.1627}, + "Vanagaram": {"lat": 13.0636, "lng": 80.1548}, + "Kasi Theatre": {"lat": 13.0305, "lng": 80.2132}, + "Ashok Pillar": {"lat": 13.0336, "lng": 80.2114}, + "Olympia": {"lat": 13.0135, "lng": 80.2155}, + "Mandaveli": {"lat": 13.0238, "lng": 80.2687}, + "Santhome": {"lat": 13.0336, "lng": 80.2785}, + "Pattinapakkam": {"lat": 13.0287, "lng": 80.2788}, + "Kovilampakkam": {"lat": 12.9510, "lng": 80.1874}, + "Keelkattalai": {"lat": 12.9687, "lng": 80.2014}, + "Madipakkam": {"lat": 12.9738, "lng": 80.2087}, + "Nanganallur": {"lat": 12.9838, "lng": 80.1987}, + "Saidapet": {"lat": 13.0238, "lng": 80.2287}, + "KK Nagar": {"lat": 13.0373, "lng": 80.2045}, + "Virugampakkam": {"lat": 13.0487, "lng": 80.1914}, + "Ramapuram": {"lat": 13.0287, "lng": 80.1788}, + "Tiruvallur": {"lat": 13.1438, "lng": 79.9087}, +} + +def load_cache(): + if os.path.exists(CACHE_PATH): + try: + with open(CACHE_PATH, "r", encoding="utf-8") as f: + return json.load(f) + except Exception: + pass + return {} + +def save_cache(cache): + with open(CACHE_PATH, "w", encoding="utf-8") as f: + json.dump(cache, f, indent=2) + +def clean_name(name): + # Remove contents inside parentheses + name = re.sub(r'\(.*?\)', '', name) + # Remove descriptors like 'Route', 'Stop', 'Signal', 'Bunk', etc. + name = re.sub(r'\b(stop|signal|bunk|junction|crossing|stage|bus stop|market|railway station|station)\b', '', name, flags=re.IGNORECASE) + # Normalize whitespaces + return " ".join(name.split()).strip() + +def geocode_nominatim(name): + # 1. Check static overrides first + cleaned = clean_name(name) + for key, coords in STATIC_COORDINATES.items(): + if key.lower() == name.lower() or key.lower() == cleaned.lower(): + return coords + + # 2. Try clean geocoding + search_queries = [ + f"{name}, Chennai, Tamil Nadu, India", + f"{cleaned}, Chennai, Tamil Nadu, India", + f"{cleaned}, Tamil Nadu, India" + ] + + headers = { + "User-Agent": "RITFreshersHubBusGeocoderConsolidator/2.0 (mail@ritchennai.edu.in)" + } + + for query in search_queries: + try: + url = "https://nominatim.openstreetmap.org/search" + params = {"q": query, "format": "json", "limit": 1} + print(f"Requesting: {query} ...") + res = requests.get(url, headers=headers, params=params, timeout=10) + if res.status_code == 200: + data = res.json() + if data: + lat = float(data[0]["lat"]) + lng = float(data[0]["lon"]) + print(f"-> Found: {lat}, {lng}") + return {"lat": lat, "lng": lng} + # Rest to respect API limit + time.sleep(1.2) + except Exception as e: + print(f"Error querying {query}: {e}") + time.sleep(1.2) + + # 3. Fallback keywords check + for key, coords in STATIC_COORDINATES.items(): + if key.lower() in name.lower() or key.lower() in cleaned.lower(): + print(f"-> Matching fallback keyword: '{key}' for '{name}'") + return coords + + # Default fallback: RIT Campus coordinates + print(f"-> No match found for '{name}'. Defaulting to RIT Campus.") + return STATIC_COORDINATES["RIT Campus"] + +def geocode_routes(): + if not os.path.exists(BUS_ROUTES_PATH): + print(f"Error: {BUS_ROUTES_PATH} does not exist.") + return + + with open(BUS_ROUTES_PATH, "r", encoding="utf-8") as f: + routes = json.load(f) + + cache = load_cache() + modified = False + + # Collect all unique stop names + unique_names = set() + for r in routes: + unique_names.add(r["from"]) + unique_names.add(r["to"]) + for stop in r.get("stops", []): + unique_names.add(stop["name"]) + + print(f"Total unique stops to geocode: {len(unique_names)}") + + # Geocode each stop + count = 0 + for name in sorted(unique_names): + if name not in cache: + coords = geocode_nominatim(name) + cache[name] = coords + count += 1 + # Save cache incrementally + if count % 5 == 0: + save_cache(cache) + save_cache(cache) + + # Enrich bus_routes.json + for r in routes: + from_coords = cache.get(r["from"], STATIC_COORDINATES["RIT Campus"]) + to_coords = cache.get(r["to"], STATIC_COORDINATES["RIT Campus"]) + r["from_lat"] = from_coords["lat"] + r["from_lng"] = from_coords["lng"] + r["to_lat"] = to_coords["lat"] + r["to_lng"] = to_coords["lng"] + + for stop in r.get("stops", []): + stop_coords = cache.get(stop["name"], STATIC_COORDINATES["RIT Campus"]) + stop["lat"] = stop_coords["lat"] + stop["lng"] = stop_coords["lng"] + + # Write enriched data back + with open(BUS_ROUTES_PATH, "w", encoding="utf-8") as f: + json.dump(routes, f, indent=2) + + print("Geocoding process complete! bus_routes.json updated with coordinates.") + +if __name__ == "__main__": + geocode_routes()