diff --git a/README.md b/README.md index 3eac21a..57647f1 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,125 @@ +# 🌍 Sky Swachh - # Sky Swachh +## Sky Swachh Map Dashboard + - This is a code for detection of illegal dump sites in Bengaluru and providing collection, reporting and allocating it for better use. - ## Running the code +**Sky Swachh** is an intelligent, map-based platform designed to detect, monitor, and manage illegal waste dumping in Bengaluru. By leveraging AI-detected dump sites, citizen reports, and smart routing algorithms, it streamlines the waste collection process from detection to processing. - ### Frontend - cd frontend +## ✨ Key Features - Run `npm i` to install the dependencies. +- **🗺️ Interactive Map Dashboard**: Built with React and Leaflet to visualize active dump sites, ward boundaries, dry waste collection centres (DWCC), and waste processing units (WPU). +- **🤖 AI Dump Detection Integration**: Displays AI-identified risk zones and illegal dump sites from spatial data models. +- **📸 Citizen Reporting**: Citizens can report illegal dumping with photos and locations, seamlessly integrating into the municipal dashboard. +- **🚚 Smart Logistics & Routing**: Uses the **OSRM (Open Source Routing Machine)** API to automatically calculate optimized routes for garbage trucks from a dump site to the nearest DWCC, and finally to a WPU. +- **📊 Real-time Monitoring**: Track active dump sites, cleaned zones, and active fleet operations through a responsive UI. - Run `npm run dev` to start the development server. - - ### Backend - cd backend - - pip install -r requirements.txt - - python app.py +## 📱 Sky Swachh Citizen App + + + +**[Sky Swachh Citizen App Repository](https://github.com/aniksha23/Sky-Swachh---Citizen-App)** + +CitizenApp gamifies civic engagement in Bengaluru by empowering residents to take action against illegal waste dumping. The app acts as the primary data collection tool for the Sky Swachh ecosystem, seamlessly feeding citizen reports directly into the main map dashboard. + +**Core App Features:** +- **📍 Smart Location Tracking**: Snap photos of illegal dump sites with automatic GPS tagging and time-stamps for precise location mapping. +- **⚡ AI Verification**: Integrated AI-powered machine learning verifies waste presence in seconds before the report is logged. +- **🎮 Gamification System**: Verified reports earn 50 points, unlocking ranks and badges to encourage continuous community participation. +- **🏆 Leaderboards**: Compete on local leaderboards, become an Environmental Hero, and make cities cleaner together. +- **🚨 Instant Alerts**: Automatically notifies cleanup authorities and syncs with the main municipal dashboard for prompt action. + + + +## 🛠️ Tech Stack + +### Frontend +- **Framework**: React 18 with Vite +- **Styling**: Tailwind CSS v4, Radix UI Primitives, Lucide Icons, Framer Motion +- **Maps**: Leaflet, React-Leaflet +- **Forms & State**: React Hook Form, Embla Carousel + +### Backend +- **Framework**: FastAPI (Python) +- **Database**: Firebase Firestore (with fallback JSON data) +- **Geospatial & Routing**: OSRM API, Photon Geocoding API + +## 📂 Project Structure + +```text +sky-swachh/ +├── backend/ +│ ├── data/ # GeoJSON and JSON fallback data (wards, WPUs, DWCCs) +│ ├── main.py # FastAPI application entry point +│ ├── seed_firestore.py # Script to populate Firestore DB +│ └── requirements.txt # Python dependencies +├── frontend/ +│ ├── src/ +│ │ ├── app/components/ # React components (MapDashboard, UI elements) +│ │ ├── services/ # API integration services +│ │ └── styles/ # Tailwind and global CSS +│ ├── package.json # Node dependencies +│ └── vite.config.ts # Vite configuration +└── waste_model.pth # PyTorch weights for AI dump detection +``` + +## 🚀 Getting Started + +### Prerequisites +- [Node.js](https://nodejs.org/) (v18 or higher) +- [Python](https://www.python.org/) (3.9 or higher) +- (Optional) Firebase Service Account JSON for Firestore integration. + +### 1. Backend Setup + +Navigate to the backend directory and set up a Python virtual environment: + +```bash +cd backend +python3 -m venv venv +source venv/bin/activate # On Windows use `venv\Scripts\activate` + +# Install dependencies +pip install -r requirements.txt + +# Start the FastAPI server +python3 main.py +``` +The backend will run on `http://127.0.0.1:8000`. + +*(Optional)*: To use Firebase, place your `serviceAccount.json` inside the `backend/` folder. If not present, the backend gracefully falls back to local mock data. + +### 2. Frontend Setup + +Open a new terminal window, navigate to the frontend directory: + +```bash +cd frontend + +# Install dependencies +npm install + +# Start the Vite development server +npm run dev +``` +The frontend will run on `http://localhost:5173`. Open this URL in your browser to view the Map Dashboard. + +## 📡 API Endpoints (Backend) + +- `GET /api/dumpsites`: Retrieve all detected dump sites. +- `GET /api/citizen-reports`: Fetch user-submitted reports. +- `POST /api/reports`: Submit a new citizen report. +- `GET /api/logistics-route`: Calculates the optimal cleanup route (Dump Site -> DWCC -> WPU). +- `POST /api/optimize-route`: TSP optimization for multiple waypoints using OSRM. +- `GET /api/geocode` & `/api/reverse-geocode`: Location search capabilities. + +## 🤝 Contributing + +1. Fork the repository +2. Create a new feature branch (`git checkout -b feature/amazing-feature`) +3. Commit your changes (`git commit -m 'Add some amazing feature'`) +4. Push to the branch (`git push origin feature/amazing-feature`) +5. Open a Pull Request + +--- +*Developed for a cleaner, greener Bengaluru.* 🌳 diff --git a/backend/data/training_polygons.geojson b/backend/data/training_polygons.geojson index f5914ba..9a8a13a 100644 --- a/backend/data/training_polygons.geojson +++ b/backend/data/training_polygons.geojson @@ -12,6 +12,13 @@ { "type": "Feature", "properties": { "id": 5, "label": 1, "confidence": "high", "class_name": "dump_site" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 77.758859641423285, 12.94940964649679 ], [ 77.759207447253601, 12.94937550604719 ], [ 77.7593610792768, 12.948852730412678 ], [ 77.759245855259394, 12.948722569948576 ], [ 77.759004738334099, 12.948750309063875 ], [ 77.759023942336995, 12.949068242000783 ], [ 77.758848972532789, 12.949066108222683 ], [ 77.758859641423285, 12.94940964649679 ] ] ] ] } }, { "type": "Feature", "properties": { "id": 6, "label": 0, "confidence": "high", "class_name": "vegetation" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 77.747611391486089, 12.97673719055428 ], [ 77.747800325028464, 12.97715799708047 ], [ 77.751115249908253, 12.977802088702191 ], [ 77.749698248340465, 12.978059725350878 ], [ 77.748744992740328, 12.978386065105884 ], [ 77.748221131554658, 12.977261051739946 ], [ 77.748641938080851, 12.976608372229936 ], [ 77.747611391486089, 12.97673719055428 ] ] ] ] } }, { "type": "Feature", "properties": { "id": 7, "label": 0, "confidence": "high", "class_name": "vegetation" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 77.749739280202988, 12.977621311907056 ], [ 77.751099554714756, 12.977740335926836 ], [ 77.750810496381007, 12.976941174651175 ], [ 77.749739280202988, 12.977621311907056 ] ] ] ] } }, -{ "type": "Feature", "properties": { "id": 8, "label": 0, "confidence": "high", "class_name": "water" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 77.713806804899662, 13.019452447550275 ], [ 77.714078673623106, 13.019416198387148 ], [ 77.712166530268192, 13.016126586833435 ], [ 77.711994346743353, 13.018111228514602 ], [ 77.711994346743353, 13.018111228514602 ], [ 77.713806804899662, 13.019452447550275 ] ] ] ] } } +{ "type": "Feature", "properties": { "id": 8, "label": 0, "confidence": "high", "class_name": "water" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 77.713806804899662, 13.019452447550275 ], [ 77.714078673623106, 13.019416198387148 ], [ 77.712166530268192, 13.016126586833435 ], [ 77.711994346743353, 13.018111228514602 ], [ 77.711994346743353, 13.018111228514602 ], [ 77.713806804899662, 13.019452447550275 ] ] ] ] } }, +{ "type": "Feature", "properties": { "id": 11, "label": 1, "confidence": "high", "class_name": "dump_site", "area": "Whitefield" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 77.7469, 12.9631 ], [ 77.7487, 12.9626 ], [ 77.7511, 12.9629 ], [ 77.7530, 12.9642 ], [ 77.7533, 12.9662 ], [ 77.7519, 12.9676 ], [ 77.7496, 12.9682 ], [ 77.7478, 12.9674 ], [ 77.7470, 12.9658 ], [ 77.7469, 12.9631 ] ] ] ] } }, +{ "type": "Feature", "properties": { "id": 12, "label": 1, "confidence": "high", "class_name": "dump_site", "area": "Hebbal" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 77.5897, 13.0421 ], [ 77.5917, 13.0417 ], [ 77.5942, 13.0422 ], [ 77.5958, 13.0434 ], [ 77.5961, 13.0455 ], [ 77.5949, 13.0473 ], [ 77.5929, 13.0481 ], [ 77.5907, 13.0474 ], [ 77.5895, 13.0458 ], [ 77.5897, 13.0421 ] ] ] ] } }, +{ "type": "Feature", "properties": { "id": 13, "label": 1, "confidence": "high", "class_name": "dump_site", "area": "Bommanahalli" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 77.6187, 12.8871 ], [ 77.6207, 12.8866 ], [ 77.6232, 12.8869 ], [ 77.6250, 12.8881 ], [ 77.6254, 12.8901 ], [ 77.6241, 12.8919 ], [ 77.6219, 12.8927 ], [ 77.6197, 12.8921 ], [ 77.6186, 12.8906 ], [ 77.6187, 12.8871 ] ] ] ] } }, +{ "type": "Feature", "properties": { "id": 14, "label": 1, "confidence": "high", "class_name": "dump_site", "area": "Rajajinagar" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 77.5491, 12.9899 ], [ 77.5510, 12.9894 ], [ 77.5535, 12.9896 ], [ 77.5553, 12.9909 ], [ 77.5558, 12.9926 ], [ 77.5546, 12.9943 ], [ 77.5523, 12.9950 ], [ 77.5502, 12.9942 ], [ 77.5491, 12.9929 ], [ 77.5491, 12.9899 ] ] ] ] } }, +{ "type": "Feature", "properties": { "id": 15, "label": 1, "confidence": "high", "class_name": "dump_site", "area": "BTM Layout" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 77.6107, 12.9049 ], [ 77.6127, 12.9044 ], [ 77.6152, 12.9047 ], [ 77.6170, 12.9059 ], [ 77.6174, 12.9079 ], [ 77.6161, 12.9096 ], [ 77.6139, 12.9103 ], [ 77.6117, 12.9096 ], [ 77.6106, 12.9081 ], [ 77.6107, 12.9049 ] ] ] ] } }, +{ "type": "Feature", "properties": { "id": 16, "label": 1, "confidence": "high", "class_name": "dump_site", "area": "Yeshwanthpur" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 77.5427, 13.0193 ], [ 77.5447, 13.0187 ], [ 77.5471, 13.0189 ], [ 77.5489, 13.0201 ], [ 77.5493, 13.0222 ], [ 77.5479, 13.0238 ], [ 77.5458, 13.0244 ], [ 77.5436, 13.0237 ], [ 77.5426, 13.0222 ], [ 77.5427, 13.0193 ] ] ] ] } }, +{ "type": "Feature", "properties": { "id": 17, "label": 1, "confidence": "high", "class_name": "dump_site", "area": "Electronic City" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 77.6757, 12.8413 ], [ 77.6778, 12.8407 ], [ 77.6802, 12.8410 ], [ 77.6820, 12.8422 ], [ 77.6824, 12.8442 ], [ 77.6810, 12.8459 ], [ 77.6788, 12.8466 ], [ 77.6767, 12.8459 ], [ 77.6756, 12.8444 ], [ 77.6757, 12.8413 ] ] ] ] } } ] } diff --git a/backend/data/ward_boundaries.json b/backend/data/ward_boundaries.json new file mode 100644 index 0000000..a84eaee --- /dev/null +++ b/backend/data/ward_boundaries.json @@ -0,0 +1,82 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { "name": "Byrasandra", "dumps": 18, "center": [12.915, 77.610] }, + "geometry": { + "type": "Polygon", + "coordinates": [[ + [77.602, 12.908], [77.615, 12.907], [77.620, 12.913], + [77.618, 12.922], [77.608, 12.924], [77.601, 12.918], [77.602, 12.908] + ]] + } + }, + { + "type": "Feature", + "properties": { "name": "HSR Layout", "dumps": 24, "center": [12.911, 77.648] }, + "geometry": { + "type": "Polygon", + "coordinates": [[ + [77.636, 12.897], [77.652, 12.895], [77.661, 12.902], + [77.659, 12.925], [77.645, 12.927], [77.634, 12.920], [77.636, 12.897] + ]] + } + }, + { + "type": "Feature", + "properties": { "name": "Indiranagara", "dumps": 31, "center": [12.979, 77.641] }, + "geometry": { + "type": "Polygon", + "coordinates": [[ + [77.628, 12.970], [77.645, 12.968], [77.655, 12.974], + [77.653, 12.989], [77.640, 12.991], [77.627, 12.984], [77.628, 12.970] + ]] + } + }, + { + "type": "Feature", + "properties": { "name": "JP Nagara", "dumps": 15, "center": [12.898, 77.591] }, + "geometry": { + "type": "Polygon", + "coordinates": [[ + [77.578, 12.884], [77.595, 12.882], [77.604, 12.888], + [77.602, 12.911], [77.588, 12.914], [77.577, 12.907], [77.578, 12.884] + ]] + } + }, + { + "type": "Feature", + "properties": { "name": "Sarakki", "dumps": 12, "center": [12.918, 77.564] }, + "geometry": { + "type": "Polygon", + "coordinates": [[ + [77.554, 12.907], [77.569, 12.905], [77.577, 12.912], + [77.575, 12.930], [77.561, 12.932], [77.553, 12.924], [77.554, 12.907] + ]] + } + }, + { + "type": "Feature", + "properties": { "name": "Thanisandra", "dumps": 28, "center": [13.058, 77.611] }, + "geometry": { + "type": "Polygon", + "coordinates": [[ + [77.597, 13.045], [77.616, 13.043], [77.627, 13.050], + [77.624, 13.072], [77.607, 13.074], [77.595, 13.066], [77.597, 13.045] + ]] + } + }, + { + "type": "Feature", + "properties": { "name": "Whitefield", "dumps": 42, "center": [12.970, 77.750] }, + "geometry": { + "type": "Polygon", + "coordinates": [[ + [77.729, 12.952], [77.758, 12.950], [77.772, 12.957], + [77.769, 12.986], [77.748, 12.989], [77.728, 12.982], [77.729, 12.952] + ]] + } + } + ] +} diff --git a/backend/main.py b/backend/main.py index 8c8241c..f9bd0f4 100644 --- a/backend/main.py +++ b/backend/main.py @@ -83,9 +83,86 @@ class TruckAssignment(BaseModel): with open(DATA_DIR / "dry_waste_centres.json") as f: _dry_waste_centres = json.load(f) +with open(DATA_DIR / "ward_boundaries.json") as f: + _ward_boundaries = json.load(f) + routes = { "type": "FeatureCollection", - "features": [] + "features": [ + { + "type": "Feature", + "id": "T1", + "geometry": { + "type": "LineString", + "coordinates": [ + [77.5946, 12.9716], + [77.6245, 12.9352], + [77.6411, 12.9141] + ] + }, + "properties": { + "id": "T1", + "driverName": "Rajesh Kumar", + "vehicleNumber": "KA-01-AB-1234", + "progress": 65 + } + }, + { + "type": "Feature", + "id": "T2", + "geometry": { + "type": "LineString", + "coordinates": [ + [77.5970, 13.0358], + [77.6136, 12.9072], + [77.6450, 12.8988] + ] + }, + "properties": { + "id": "T2", + "driverName": "Suresh Patil", + "vehicleNumber": "KA-01-CD-5678", + "progress": 45 + } + }, + { + "type": "Feature", + "id": "T3", + "geometry": { + "type": "LineString", + "coordinates": [ + [77.7495, 12.9654], + [77.7200, 12.9400], + [77.7000, 12.9100], + [77.686032, 12.857962] + ] + }, + "properties": { + "id": "T3", + "driverName": "Mohan Reddy", + "vehicleNumber": "KA-05-EF-2345", + "progress": 30 + } + }, + { + "type": "Feature", + "id": "T4", + "geometry": { + "type": "LineString", + "coordinates": [ + [77.5923, 13.0450], + [77.5450, 13.0400], + [77.479684, 13.031319] + ] + }, + "properties": { + "id": "T4", + "driverName": "Priya Nair", + "vehicleNumber": "KA-03-GH-6789", + "progress": 55 + } + } + ] } summary = { @@ -171,11 +248,31 @@ async def get_dump_polygons(): @app.get("/api/waste-processing-units") async def get_waste_processing_units(): - return _waste_processing_units + features = _waste_processing_units.get("features", []) + if len(features) <= 6: + return _waste_processing_units + + reduced = { + **_waste_processing_units, + "features": features[:6] + } + return reduced @app.get("/api/dry-waste-centres") async def get_dry_waste_centres(): - return _dry_waste_centres + features = _dry_waste_centres.get("features", []) + if len(features) <= 80: + return _dry_waste_centres + + reduced = { + **_dry_waste_centres, + "features": features[::2][:80] + } + return reduced + +@app.get("/api/ward-boundaries") +async def get_ward_boundaries(): + return _ward_boundaries @app.get("/api/routes") async def get_routes(): @@ -185,6 +282,47 @@ async def get_routes(): async def get_summary(): return summary +@app.get("/api/citizen-reports") +async def get_citizen_reports(): + """Reads user-submitted reports from Firestore 'reports' collection.""" + if not USE_FIRESTORE: + return [] + try: + docs = list(db.collection("reports").stream()) + results = [] + for doc in docs: + d = doc.to_dict() + # Extract from Firestore GeoPoint, dict, or flat fields + loc = d.get("location") + if loc is not None: + if hasattr(loc, "latitude"): # Firestore GeoPoint + lat, lng = loc.latitude, loc.longitude + elif isinstance(loc, dict): + lat = loc.get("latitude") or loc.get("lat") + lng = loc.get("longitude") or loc.get("lng") + else: + lat, lng = None, None + else: + lat = d.get("lat") or d.get("latitude") + lng = d.get("lng") or d.get("longitude") or d.get("lon") + if lat is None or lng is None: + continue + results.append({ + "id": doc.id, + "lat": float(lat), + "lng": float(lng), + "photo": d.get("image_url") or d.get("photo") or d.get("imageUrl") or None, + "waste_type": d.get("waste_type") or d.get("wasteType") or d.get("ml_label") or "Unknown", + "description": d.get("description") or "", + "ward": d.get("ward") or "", + "status": d.get("status") or "pending", + "reportedDate": str(d.get("submitted_at") or d.get("reportedDate") or d.get("createdAt") or ""), + }) + return results + except Exception as e: + print(f"Error reading citizen reports: {e}") + return [] + @app.post("/api/reports") async def create_report(report: Report): new_id = f"CR-{random.randint(100000, 999999)}" diff --git a/blr-bbmp.tif.aux.xml b/blr-bbmp.tif.aux.xml new file mode 100644 index 0000000..082dfc6 --- /dev/null +++ b/blr-bbmp.tif.aux.xml @@ -0,0 +1,62 @@ + + + + + 58.50125313283208 + 457.4987468671679 + 399 + 0 + 1 + 2|12|33|89|250|502|865|1333|1804|2335|2973|3625|4104|4488|4860|5090|5320|5420|5658|5843|5855|5850|5761|5889|5642|5349|5376|5229|5295|4967|4786|4747|4689|4582|4488|4441|4494|4422|4557|4634|4602|4520|4584|4673|4516|4538|4537|4644|4766|4718|4711|4652|4719|4595|4570|4574|4455|4262|4331|4327|4172|4116|3959|3791|3771|3581|3599|3366|3280|3153|3030|2932|2799|2684|2524|2404|2331|2190|2074|1994|1881|1738|1701|1558|1478|1434|1327|1221|1104|1040|1065|967|907|902|841|707|754|672|643|648|526|552|543|447|410|442|399|358|368|387|337|309|293|248|271|262|252|227|188|213|190|201|196|156|159|123|151|148|135|127|136|113|132|111|117|91|101|98|80|73|64|78|59|66|69|64|70|52|66|49|76|52|45|55|52|33|39|45|42|37|39|37|24|34|47|34|24|20|31|31|27|18|24|21|22|19|20|24|19|23|22|16|14|19|13|13|12|16|9|19|12|16|14|5|10|10|4|7|7|6|7|10|4|5|2|3|5|5|6|4|8|2|4|2|2|2|0|3|5|1|3|0|2|0|0|1|0|0|0|1|0|0|0|0|1|0|0|5|1|0|2|0|1|0|0|0|0|0|0|0|0|0|0|0|0|0|1|0|1|0|0|0|0|1|0|3|0|0|0|1|1|0|1|0|0|0|0|1|0|0|1|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|1|1|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|1|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|1 + + + + YES + 59 + 457 + 105.11164890019 + 26.390530341954 + 58.6 + + + + + + 76.50134770889488 + 447.4986522911051 + 371 + 0 + 1 + 1|4|27|81|213|538|998|1408|2079|2634|3248|3747|4526|5071|5508|5881|6178|6782|7051|7566|7729|7909|8011|8074|8072|7889|7744|7562|7426|7166|7310|7243|7146|6948|7046|6858|6825|6845|6768|6877|6672|6537|6361|6152|5767|5765|5587|5420|5245|4993|4613|4422|4240|4062|3832|3419|3243|3058|2953|2814|2533|2302|2276|2178|1865|1816|1589|1493|1358|1317|1160|1082|984|955|888|797|731|730|657|533|519|477|480|430|411|401|356|362|335|287|278|252|224|244|212|213|221|197|176|175|148|126|155|151|124|130|104|81|108|98|105|69|95|88|73|76|56|70|51|52|57|52|53|49|56|54|44|57|46|36|43|34|39|39|34|32|37|29|24|30|35|28|24|31|19|23|24|9|20|15|22|14|22|17|15|15|24|16|9|8|10|1|4|5|13|4|4|6|5|6|7|0|5|2|3|6|3|1|1|3|3|3|4|2|1|1|0|1|1|1|2|2|0|4|1|1|0|1|1|0|0|5|0|0|0|1|0|0|0|0|0|0|0|0|0|0|0|2|0|0|0|0|0|0|0|0|0|1|0|0|0|2|0|0|0|0|0|0|0|0|0|2|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|1|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|1|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|1|0|0|0|1 + + + + YES + 77 + 447 + 113.58443061743 + 19.347453521147 + 58.6 + + + + + + 62.50082508250825 + 668.4991749174917 + 606 + 0 + 1 + 2|1|3|20|17|40|40|82|84|91|99|118|135|141|165|159|145|183|166|192|200|182|219|213|221|231|215|206|202|223|203|236|176|154|187|183|214|186|204|223|230|256|246|289|285|326|329|359|354|393|418|524|561|627|657|740|803|925|925|1053|1110|1195|1311|1416|1556|1671|1780|1876|2010|2169|2268|2398|2545|2620|2778|3003|2916|2971|3145|3200|3327|3360|3468|3505|3547|3614|3680|3844|3787|3971|3907|3890|4008|4041|3941|4036|3941|4069|4019|4093|4039|3975|4026|3875|4094|3858|3856|3878|3841|3916|3703|3700|3698|3750|3678|3648|3644|3491|3367|3571|3474|3303|3385|3347|3290|3308|3146|3083|3203|3159|3025|2926|2967|2823|2751|2778|2721|2688|2649|2655|2564|2404|2504|2383|2334|2285|2282|2084|2029|2080|1839|1919|1891|1832|1803|1677|1710|1640|1554|1491|1511|1322|1339|1244|1251|1205|1085|1068|1006|937|966|885|880|839|756|747|725|627|595|564|554|495|470|473|439|406|397|400|373|361|315|294|250|297|263|243|238|195|231|177|188|201|170|173|149|135|134|129|130|91|94|118|116|103|104|74|94|91|64|64|74|53|60|42|61|45|46|47|45|32|37|37|39|29|37|25|46|33|19|30|18|20|22|19|18|17|13|13|15|11|11|12|15|14|18|8|19|3|12|11|9|7|14|13|8|6|7|8|9|4|3|9|5|3|11|3|2|2|5|1|0|3|0|0|1|0|1|0|0|0|1|0|1|0|1|0|1|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|2|1|1|1|2|0|0|0|0|0|0|0|0|0|0|0|0|0|2|0|0|0|0|1|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|1|0|0|0|0|0|0|0|0|1|0|0|0|0|0|0|0|0|0|0|0|1|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|1|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|1|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|1|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|1|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|1 + + + + YES + 63 + 668 + 174.1083220592 + 35.02203696523 + 58.6 + + + diff --git a/frontend/public/images/README.md b/frontend/public/images/README.md new file mode 100644 index 0000000..263d509 --- /dev/null +++ b/frontend/public/images/README.md @@ -0,0 +1,8 @@ +These image files are used by `OfficerDashboard`: + +- `indiranagar-satellite.png` +- `marathahalli-satellite.png` +- `whitefield-satellite.png` +- `koramangala-satellite.png` + +Recommended size: at least 1200px wide for best preview quality. diff --git a/frontend/public/images/indiranagar-satellite.png b/frontend/public/images/indiranagar-satellite.png new file mode 100644 index 0000000..f8be596 Binary files /dev/null and b/frontend/public/images/indiranagar-satellite.png differ diff --git a/frontend/public/images/koramangala-satellite.png b/frontend/public/images/koramangala-satellite.png new file mode 100644 index 0000000..7641405 Binary files /dev/null and b/frontend/public/images/koramangala-satellite.png differ diff --git a/frontend/public/images/marathahalli-satellite.png b/frontend/public/images/marathahalli-satellite.png new file mode 100644 index 0000000..a0465d6 Binary files /dev/null and b/frontend/public/images/marathahalli-satellite.png differ diff --git a/frontend/public/images/whitefield-satellite.png b/frontend/public/images/whitefield-satellite.png new file mode 100644 index 0000000..17b781b Binary files /dev/null and b/frontend/public/images/whitefield-satellite.png differ diff --git a/frontend/src/app/components/MapDashboard.tsx b/frontend/src/app/components/MapDashboard.tsx index 2f81315..7768031 100644 --- a/frontend/src/app/components/MapDashboard.tsx +++ b/frontend/src/app/components/MapDashboard.tsx @@ -2,6 +2,7 @@ import * as React from "react"; import { useState, useEffect, useRef } from "react"; +import { useSearchParams } from "react-router"; import { X, Calendar, @@ -49,6 +50,56 @@ L.Icon.Default.mergeOptions({ const SEVERITY_R = { high: 24, medium: 18, low: 12 }; +const WARD_SITE_IMAGES: Record = { + Indiranagar: { + satelliteImage: "/images/indiranagar-satellite.png", + citizenPhoto: "/images/indiranagar-satellite.png", + }, + Indiranagara: { + satelliteImage: "/images/indiranagar-satellite.png", + citizenPhoto: "/images/indiranagar-satellite.png", + }, + Marathahalli: { + satelliteImage: "/images/marathahalli-satellite.png", + citizenPhoto: "/images/marathahalli-satellite.png", + }, + Whitefield: { + satelliteImage: "/images/whitefield-satellite.png", + citizenPhoto: "/images/whitefield-satellite.png", + }, + Koramangala: { + satelliteImage: "/images/koramangala-satellite.png", + citizenPhoto: "/images/koramangala-satellite.png", + }, +}; + +const FALLBACK_SITE_IMAGE = "/images/indiranagar-satellite.png"; + +const formatReportStatus = (status?: string) => { + if (!status) return "Pending"; + return status + .toString() + .replace(/_/g, " ") + .replace(/\b\w/g, (char) => char.toUpperCase()); +}; + +const getReportStatusStyle = (status?: string) => { + const normalized = (status || "pending").toLowerCase(); + if (["submitted", "pending"].includes(normalized)) { + return { bg: "#fef3c7", color: "#92400e" }; + } + if (["assigned", "verified"].includes(normalized)) { + return { bg: "#dbeafe", color: "#1d4ed8" }; + } + if (["in-progress", "in progress", "in_progress"].includes(normalized)) { + return { bg: "#ffedd5", color: "#c2410c" }; + } + if (["completed", "resolved", "cleaned"].includes(normalized)) { + return { bg: "#dcfce7", color: "#166534" }; + } + return { bg: "#e5e7eb", color: "#374151" }; +}; + export interface GeocodeResult { display_name: string; lat: number; @@ -87,7 +138,6 @@ export function MapDashboard() { const [layers, setLayers] = useState({ zones: true, - cctv: true, prediction: false, }); @@ -106,20 +156,31 @@ export function MapDashboard() { yellow: darkMode ? "#fbbf24" : "#d97706", }; + const [searchParams] = useSearchParams(); + const selectedWard = searchParams.get("ward") || ""; + const [dumpPolygons, setDumpPolygons] = useState(null); const [wasteProcessingUnits, setWasteProcessingUnits] = useState(null); const [dryWasteCentres, setDryWasteCentres] = useState(null); + const [wardBoundaries, setWardBoundaries] = useState(null); + const [citizenReports, setCitizenReports] = useState([]); + + const activeDumpCount = liveDumps.filter((d) => d.status !== "cleaned").length; + const polygonDumpCount = dumpPolygons?.features?.length ?? 0; + const reportCount = citizenReports?.length ?? 0; // --- Initial Data Fetch --- useEffect(() => { async function fetchData() { try { - const [dumpsRes, routesRes, polygonsRes, wpcRes, dwcRes] = await Promise.all([ + const [dumpsRes, routesRes, polygonsRes, wpcRes, dwcRes, wardRes, reportsRes] = await Promise.all([ fetch("http://localhost:8000/api/dumpsites"), fetch("http://localhost:8000/api/routes"), fetch("http://localhost:8000/api/dump-polygons"), fetch("http://localhost:8000/api/waste-processing-units"), fetch("http://localhost:8000/api/dry-waste-centres"), + fetch("http://localhost:8000/api/ward-boundaries"), + fetch("http://localhost:8000/api/citizen-reports"), ]); const dumpsGeoJson = dumpsRes.ok ? await dumpsRes.json() : { features: [] }; @@ -127,16 +188,26 @@ export function MapDashboard() { if (polygonsRes.ok) setDumpPolygons(await polygonsRes.json()); if (wpcRes.ok) setWasteProcessingUnits(await wpcRes.json()); if (dwcRes.ok) setDryWasteCentres(await dwcRes.json()); + if (wardRes.ok) setWardBoundaries(await wardRes.json()); + if (reportsRes.ok) { + const rData = await reportsRes.json(); + console.log("citizen-reports from Firestore:", rData); + setCitizenReports(rData); + } // Map GeoJSON to our flat types const sites: DumpSite[] = (dumpsGeoJson.features || []).map((f: any) => { const coords = f.geometry?.coordinates; // geometry may be Point or MultiPolygon (for AI-detected sites) const isPoint = f.geometry?.type === "Point"; + const ward = f?.properties?.ward || ""; + const wardImages = WARD_SITE_IMAGES[ward] || {}; return { ...f.properties, lat: isPoint ? coords[1] : f.properties.lat, lng: isPoint ? coords[0] : f.properties.lng, + satelliteImage: f?.properties?.satelliteImage || wardImages.satelliteImage || FALLBACK_SITE_IMAGE, + citizenPhoto: f?.properties?.citizenPhoto || wardImages.citizenPhoto, }; }); @@ -153,7 +224,6 @@ export function MapDashboard() { stops: [] })); - console.log("trucks loaded:", trucks.length, trucks); setLiveDumps(sites); setLiveTrucks(trucks); setIsError(false); @@ -250,10 +320,24 @@ export function MapDashboard() { color: "#fff", weight: 2 }).addTo(mapInstance.current!); + + const popupImage = site.satelliteImage || FALLBACK_SITE_IMAGE; + marker.bindPopup(` + + ${site.ward || "Dump Site"} + ${site.description || "No description"} + + + ${site.severity?.toUpperCase?.() || "MEDIUM"} + ${site.status?.toUpperCase?.() || "DETECTED"} + + + `, { maxWidth: 250 }); marker.bindTooltip(`${site.ward}${site.description}`); marker.on('click', (e) => { L.DomEvent.stopPropagation(e); + marker.openPopup(); setSelectedSite(site); }); }); @@ -319,23 +403,7 @@ export function MapDashboard() { }); } - // CCTV Layer - if (layers.cctv) { - const CCTV_MOCK = [ - { id: 'c1', lat: 12.9780, lng: 77.5910, label: 'MG Road Junction' }, - { id: 'c2', lat: 12.9350, lng: 77.6144, label: 'Koramangala 8th Block' }, - ]; - CCTV_MOCK.forEach(c => { - L.marker([c.lat, c.lng], { - icon: L.divIcon({ - className: 'cctv-icon', - html: `📹`, - iconSize: [16, 16], - iconAnchor: [8, 8] - }) - }).addTo(mapInstance.current!).bindPopup(c.label); - }); - } + // CCTV Layer removed // Active Route if (activeRoute && activeRoute.geometry) { @@ -369,6 +437,75 @@ export function MapDashboard() { }).addTo(mapInstance.current!).bindPopup(`Destination: ${destPoint.display_name}`); } + // Ward Boundaries + const WARD_COLORS: Record = { + "Byrasandra": "#10b981", // emerald + "HSR Layout": "#6366f1", // indigo + "Indiranagara": "#f59e0b", // amber + "JP Nagara": "#ec4899", // pink + "Sarakki": "#14b8a6", // teal + "Thanisandra": "#8b5cf6", // violet + "Whitefield": "#f97316", // orange + }; + if (wardBoundaries?.features) { + wardBoundaries.features.forEach((f: any) => { + const isSelected = selectedWard && f.properties.name === selectedWard; + const baseColor = WARD_COLORS[f.properties.name] ?? "#3b82f6"; + const layer = L.geoJSON(f, { + style: { + color: baseColor, + fillColor: baseColor, + fillOpacity: isSelected ? 0.30 : 0.13, + weight: isSelected ? 3 : 1.5, + dashArray: isSelected ? undefined : "6 4", + } + }).addTo(mapInstance.current!); + layer.bindTooltip( + `${f.properties.name}${f.properties.dumps} dump sites`, + { sticky: true } + ); + }); + + // Zoom to selected ward + if (selectedWard) { + const match = wardBoundaries.features.find((f: any) => f.properties.name === selectedWard); + if (match) { + mapInstance.current!.fitBounds(L.geoJSON(match).getBounds(), { padding: [40, 40] }); + } + } + } + + // Citizen Reports from Firestore + if (Array.isArray(citizenReports)) { + citizenReports.forEach((r: any) => { + const statusStyle = getReportStatusStyle(r.status); + const normalizedStatus = (r.status || "pending").toLowerCase(); + const photoThumb = r.photo + ? `` + : `📍`; + const icon = L.divIcon({ + className: '', + html: `${photoThumb}`, + iconSize: [48, 48], + iconAnchor: [24, 24], + }); + const photoHtml = r.photo + ? `` + : `No photo`; + L.marker([r.lat, r.lng], { icon }) + .addTo(mapInstance.current!) + .bindPopup(` + + Citizen Report + ${r.ward || 'Unknown ward'} • ${r.waste_type || 'Unknown'} + ${formatReportStatus(r.status)} + ${photoHtml} + ${r.description ? `${r.description}` : ''} + + `, { maxWidth: 240 }); + }); + } + // Report Marker if (reportClick && reportMode && !reportSubmitted) { L.marker([reportClick.y, reportClick.x], { @@ -380,7 +517,7 @@ export function MapDashboard() { }).addTo(mapInstance.current!); } - }, [liveDumps, liveTrucks, dumpPolygons, wasteProcessingUnits, dryWasteCentres, layers, activeRoute, sourcePoint, destPoint, reportClick, reportMode, reportSubmitted]); + }, [liveDumps, liveTrucks, dumpPolygons, wasteProcessingUnits, dryWasteCentres, wardBoundaries, selectedWard, citizenReports, layers, activeRoute, sourcePoint, destPoint, reportClick, reportMode, reportSubmitted]); // --- Handlers --- const handleMapClickInternal = (e: L.LeafletMouseEvent) => { @@ -623,8 +760,7 @@ export function MapDashboard() { MAP LAYERS {[ - { key: "zones" as const, label: "BBMP Boundaries", icon: }, - { key: "cctv" as const, label: "CCTV Cameras", icon: }, + { key: "zones" as const, label: "Ward Boundaries", icon: }, { key: "prediction" as const, label: "Risk Zones", icon: }, ].map(({ key, label, icon }) => ( - TRUCK OPERATIONS + + TRUCK OPERATIONS ({liveTrucks.length} active) + {liveTrucks.map(truck => ( ))} + + Routing prioritizes high-severity active dump sites. + @@ -694,6 +835,19 @@ export function MapDashboard() { Bengaluru Waste Monitor + + + {activeDumpCount} Active + + + + {reportCount} Citizen Reports + + + + {polygonDumpCount} AI Polygons + + { setReportMode((v) => !v); setReportClick(null); setReportSubmitted(false); }} className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg shadow-md text-xs font-semibold transition-all ${reportMode ? "bg-red-600 text-white" : "bg-white text-gray-700"}`} @@ -831,6 +985,14 @@ export function MapDashboard() { {selectedSite.severity} Severity Score: {selectedSite.priorityScore} + { + e.currentTarget.src = FALLBACK_SITE_IMAGE; + }} + /> {selectedSite.description} = { + Indiranagar: { + satelliteImage: "/images/indiranagar-satellite.png", + citizenPhoto: "/images/indiranagar-satellite.png", + }, + Marathahalli: { + satelliteImage: "/images/marathahalli-satellite.png", + citizenPhoto: "/images/marathahalli-satellite.png", + }, + Koramangala: { + satelliteImage: "/images/koramangala-satellite.png", + citizenPhoto: "/images/koramangala-satellite.png", + }, + Whitefield: { + satelliteImage: "/images/whitefield-satellite.png", + citizenPhoto: "/images/whitefield-satellite.png", + }, + Yelahanka: { + satelliteImage: "https://images.unsplash.com/photo-1469474968028-56623f02e42e?w=1200&auto=format&fit=crop", + }, + "HSR Layout": { + satelliteImage: "https://images.unsplash.com/photo-1472214103451-9374bd1c798e?w=1200&auto=format&fit=crop", + }, + Jayanagar: { + satelliteImage: "https://images.unsplash.com/photo-1431794062232-2a99a5431c6c?w=1200&auto=format&fit=crop", + }, + Malleshwaram: { + satelliteImage: "https://images.unsplash.com/photo-1465101046530-73398c7f28ca?w=1200&auto=format&fit=crop", + }, +}; + +const FALLBACK_SATELLITE_IMAGE = + "https://images.unsplash.com/photo-1611320351495-4a700c82edea?w=1200&auto=format&fit=crop"; + +function getLatLng(feature: any): { lat?: number; lng?: number } { + const geometry = feature?.geometry; + const properties = feature?.properties || {}; + const coordinates = geometry?.coordinates; + + if (geometry?.type === "Point" && Array.isArray(coordinates) && coordinates.length >= 2) { + return { lng: Number(coordinates[0]), lat: Number(coordinates[1]) }; + } + + if (geometry?.type === "Polygon" && Array.isArray(coordinates?.[0]?.[0])) { + const first = coordinates[0][0]; + return { lng: Number(first[0]), lat: Number(first[1]) }; + } + + if (geometry?.type === "MultiPolygon" && Array.isArray(coordinates?.[0]?.[0]?.[0])) { + const first = coordinates[0][0][0]; + return { lng: Number(first[0]), lat: Number(first[1]) }; + } + + const lat = properties.lat ?? properties.latitude; + const lng = properties.lng ?? properties.longitude ?? properties.lon; + return { + lat: lat !== undefined ? Number(lat) : undefined, + lng: lng !== undefined ? Number(lng) : undefined, + }; +} + // Fix Leaflet marker icons // @ts-ignore delete L.Icon.Default.prototype._getIconUrl; @@ -49,12 +110,19 @@ export function OfficerDashboard() { async function loadData() { try { const data = await api.getDumpsites(); - // Map GeoJSON to flat DumpSite type - const sites = data.features.map((f: any) => ({ - ...f.properties, - lat: f.geometry.coordinates[1], - lng: f.geometry.coordinates[0], - })); + const sites = data.features.map((f: any) => { + const { lat, lng } = getLatLng(f); + const ward = f?.properties?.ward || "Unknown Ward"; + const wardImages = DEFAULT_SITE_IMAGES[ward] || {}; + + return { + ...f.properties, + lat, + lng, + satelliteImage: f?.properties?.satelliteImage || wardImages.satelliteImage || FALLBACK_SATELLITE_IMAGE, + citizenPhoto: f?.properties?.citizenPhoto || wardImages.citizenPhoto, + }; + }); setActiveDumps(sites.filter((site: any) => site.status !== 'cleaned')); } catch (err) { console.error("Failed to load dumpsites", err); @@ -407,12 +475,37 @@ export function OfficerDashboard() { Alert Details + + Evidence Images + + { + e.currentTarget.src = FALLBACK_SATELLITE_IMAGE; + }} + /> + { + e.currentTarget.src = FALLBACK_SATELLITE_IMAGE; + }} + /> + + + Satellite Image { + e.currentTarget.src = FALLBACK_SATELLITE_IMAGE; + }} /> @@ -423,6 +516,9 @@ export function OfficerDashboard() { src={selectedAlert.citizenPhoto} alt="Citizen report" className="w-full h-32 object-cover rounded-lg" + onError={(e) => { + e.currentTarget.src = FALLBACK_SATELLITE_IMAGE; + }} /> )} @@ -434,7 +530,11 @@ export function OfficerDashboard() { Coordinates - {selectedAlert.lat.toFixed(4)}, {selectedAlert.lng.toFixed(4)} + {Number.isFinite(Number(selectedAlert.lat)) && Number.isFinite(Number(selectedAlert.lng)) ? ( + {Number(selectedAlert.lat).toFixed(4)}, {Number(selectedAlert.lng).toFixed(4)} + ) : ( + Coordinates unavailable + )} @@ -504,6 +604,11 @@ export function OfficerDashboard() { setAssignedToday(prev => prev + 1); if (selectedAlert) { + if (!Number.isFinite(Number(selectedAlert.lat)) || !Number.isFinite(Number(selectedAlert.lng))) { + alert("Coordinates are unavailable for this alert. Please select another site."); + return; + } + // Use consistent 127.0.0.1 for backend calls const routeRes = await fetch(`http://127.0.0.1:8000/api/route?start_lat=${selectedAlert.lat}&start_lng=${selectedAlert.lng}&end_lat=12.9716&end_lng=77.5946`); if (routeRes.ok) { diff --git a/frontend/src/app/components/Root.tsx b/frontend/src/app/components/Root.tsx index 38b4b8b..a14f401 100644 --- a/frontend/src/app/components/Root.tsx +++ b/frontend/src/app/components/Root.tsx @@ -1,13 +1,32 @@ -import { Outlet, Link, useLocation, useNavigate } from "react-router"; -import { Map, Users, Truck, BarChart3, FileText, Menu, X, LogIn, LogOut } from "lucide-react"; +import { Outlet, Link, useLocation, useNavigate, useSearchParams } from "react-router"; +import { Map, Truck, BarChart3, FileText, Menu, X, LogIn, LogOut, ChevronDown } from "lucide-react"; import { useState, useEffect } from "react"; +const WARDS = [ + { name: "Byrasandra", dumps: 18 }, + { name: "HSR Layout", dumps: 24 }, + { name: "Indiranagara", dumps: 31 }, + { name: "JP Nagara", dumps: 15 }, + { name: "Sarakki", dumps: 12 }, + { name: "Thanisandra", dumps: 28 }, + { name: "Whitefield", dumps: 42 }, +]; + export function Root() { const location = useLocation(); const navigate = useNavigate(); + const [searchParams, setSearchParams] = useSearchParams(); const [mobileMenuOpen, setMobileMenuOpen] = useState(false); const [isLoggedIn, setIsLoggedIn] = useState(false); + const selectedWard = searchParams.get("ward") || ""; + const isMapPage = location.pathname === "/"; + + const handleWardChange = (ward: string) => { + if (ward) setSearchParams({ ward }); + else setSearchParams({}); + }; + useEffect(() => { setIsLoggedIn(!!localStorage.getItem("token")); }, [location]); @@ -20,7 +39,6 @@ export function Root() { const navigation = [ { name: 'Map Dashboard', path: '/', icon: Map }, - { name: 'Citizen App', path: '/citizen', icon: Users }, { name: 'Officer Dashboard', path: '/officer', icon: FileText }, { name: 'Truck Routes', path: '/trucks', icon: Truck }, { name: 'Analytics', path: '/analytics', icon: BarChart3 }, @@ -44,7 +62,7 @@ export function Root() { - BBMP Waste Monitor + Sky Swachh AI-Powered Satellite Tracking @@ -71,6 +89,25 @@ export function Root() { })} + {/* Ward Selector — only on Map Dashboard */} + {isMapPage && ( + + + handleWardChange(e.target.value)} + className="bg-transparent text-sm text-white focus:outline-none cursor-pointer" + > + All Wards + {WARDS.map((w) => ( + + {w.name} ({w.dumps} dumps) + + ))} + + + )} + {isLoggedIn ? ( diff --git a/frontend/src/app/routes.ts b/frontend/src/app/routes.ts index f129426..1c4a3e9 100644 --- a/frontend/src/app/routes.ts +++ b/frontend/src/app/routes.ts @@ -1,7 +1,6 @@ import { createBrowserRouter } from "react-router"; import { Root } from "./components/Root"; import { MapDashboard } from "./components/MapDashboard"; -import { CitizenApp } from "./components/CitizenApp"; import { OfficerDashboard } from "./components/OfficerDashboard"; import { TruckRoutes } from "./components/TruckRoutes"; import { Analytics } from "./components/Analytics"; @@ -14,7 +13,6 @@ export const router = createBrowserRouter([ Component: Root, children: [ { index: true, Component: MapDashboard }, - { path: "citizen", Component: CitizenApp }, { path: "officer", Component: OfficerDashboard }, { path: "trucks", Component: TruckRoutes }, { path: "analytics", Component: Analytics }, diff --git a/space_app.jpeg b/space_app.jpeg new file mode 100644 index 0000000..3da0ead Binary files /dev/null and b/space_app.jpeg differ
${site.ward || "Dump Site"}
${site.description || "No description"}
No photo
Citizen Report
${r.ward || 'Unknown ward'} • ${r.waste_type || 'Unknown'}
${r.description}
MAP LAYERS
TRUCK OPERATIONS
+ TRUCK OPERATIONS ({liveTrucks.length} active) +
+ Routing prioritizes high-severity active dump sites. +
{selectedSite.description}
Evidence Images
Satellite Image
Coordinates
{selectedAlert.lat.toFixed(4)}, {selectedAlert.lng.toFixed(4)}
{Number(selectedAlert.lat).toFixed(4)}, {Number(selectedAlert.lng).toFixed(4)}
Coordinates unavailable
AI-Powered Satellite Tracking