Mapping Water Infrastructure with Custom Data
Mapping Water Infrastructure with Custom Data
Overview
Create an interactive dashboard visualizing water and sewer infrastructure with custom operational data, water quality measurements, and inspection records. Perfect for municipal planning, research analysis, or public transparency initiatives.
What you'll build:
- Interactive map of treatment plants, pump stations, sewer lines, monitoring sites
- Color-coded features based on condition, priority, or water quality
- Click feature β see detailed info (capacity, last inspection, test results)
- Filter by condition, facility type, or date range
- Export reports and inspection routes
Tech stack:
- Google Maps JavaScript API (base map, interactivity)
- Maps Datasets API (store infrastructure GeoJSON)
- Places API / Geocoding API (find facilities, geocode addresses)
- Elevation API (verify gravity flow design)
- Direct data (CSV exports from databases, field measurements)
Time: 6-8 hours for basic dashboard, 12+ hours with advanced features
Pre-Project Checklist
β Infrastructure data collected (facility locations, sewer line geometries, etc.) β Data organized in spreadsheets or database β Coordinates available or addresses ready to geocode β Google Cloud project with APIs enabled (Maps JavaScript, Datasets, Geocoding) β Understanding of GeoJSON format
Step 1: Organize Infrastructure Data
Facilities (Points)
CSV format:
csvfacility_id,name,type,address,latitude,longitude,capacity_mgd,status,last_inspection,condition,notes TJ-WWTP-001,WWTP La Morita,treatment_plant,,32.4552,-116.8605,25,operational,2025-10-15,good,Primary and secondary treatment TJ-PS-042,Pump Station 42,pump_station,,32.4850,-116.9500,3.5,operational,2025-09-20,moderate_wear,Pumps due for service 2026-Q1 TJ-MS-103,Monitoring Site 103,monitoring,,32.5679,-117.1241,,active,2025-11-05,n/a,Estuary mouth sampling location
Geocode addresses if needed:
pythonimport csv import json # Read CSV with addresses with open('facilities.csv', 'r') as f: reader = csv.DictReader(f) facilities = list(reader) # Geocode missing coordinates for facility in facilities: if not facility['latitude']: result = maps_geocode(facility['address']) facility['latitude'] = result['location']['lat'] facility['longitude'] = result['location']['lng'] # Save updated CSV with open('facilities-geocoded.csv', 'w') as f: writer = csv.DictWriter(f, fieldnames=facilities[0].keys()) writer.writeheader() writer.writerows(facilities)
Sewer Lines (LineStrings)
CSV format (simplified - 2 points per line):
csvline_id,upstream_facility,downstream_facility,start_lat,start_lng,end_lat,end_lng,diameter_mm,material,install_date,condition,priority SW-1234,TJ-WWTP-001,TJ-PS-042,32.4552,-116.8605,32.4850,-116.9500,450,PVC,2010-06-15,good,routine SW-1235,TJ-PS-042,TJ-PS-043,32.4850,-116.9500,32.5100,-116.9200,300,PVC,2012-03-20,moderate_wear,monitor
For complex geometries with multiple vertices, use GeoJSON directly (see Step 2).
Water Quality Data (Points with Time Series)
csvsite_id,site_name,latitude,longitude,sample_date,e_coli_cfu,turbidity_ntu,ph,temperature_c,notes TJ-MS-103,Estuary Mouth,32.5679,-117.1241,2025-11-05,180,8.5,7.8,18.2,After rain event TJ-MS-103,Estuary Mouth,32.5679,-117.1241,2025-10-22,45,3.2,7.9,19.1,Dry conditions TJ-MS-104,Industrial Zone,32.4650,-116.9300,2025-11-05,420,15.3,7.2,20.5,Elevated bacteria
Step 2: Convert to GeoJSON
Facilities GeoJSON
pythonimport json import csv def csv_to_geojson(csv_file, output_file): features = [] with open(csv_file, 'r') as f: reader = csv.DictReader(f) for row in reader: feature = { "type": "Feature", "geometry": { "type": "Point", "coordinates": [ float(row['longitude']), float(row['latitude']) ] }, "properties": { "facility_id": row['facility_id'], "name": row['name'], "type": row['type'], "capacity_mgd": float(row['capacity_mgd']) if row['capacity_mgd'] else None, "status": row['status'], "last_inspection": row['last_inspection'], "condition": row['condition'], "notes": row['notes'] } } features.append(feature) geojson = { "type": "FeatureCollection", "features": features } with open(output_file, 'w') as f: json.dump(geojson, f, indent=2) csv_to_geojson('facilities-geocoded.csv', 'facilities.geojson')
Sewer Lines GeoJSON
pythondef sewer_lines_to_geojson(csv_file, output_file): features = [] with open(csv_file, 'r') as f: reader = csv.DictReader(f) for row in reader: feature = { "type": "Feature", "geometry": { "type": "LineString", "coordinates": [ [float(row['start_lng']), float(row['start_lat'])], [float(row['end_lng']), float(row['end_lat'])] ] }, "properties": { "line_id": row['line_id'], "diameter_mm": int(row['diameter_mm']), "material": row['material'], "install_date": row['install_date'], "condition": row['condition'], "priority": row['priority'] } } features.append(feature) geojson = { "type": "FeatureCollection", "features": features } with open(output_file, 'w') as f: json.dump(geojson, f, indent=2) sewer_lines_to_geojson('sewer_lines.csv', 'sewer_lines.geojson')
Water Quality GeoJSON (Latest Sample Only)
pythonimport csv from collections import defaultdict from datetime import datetime def water_quality_to_geojson(csv_file, output_file): # Group by site, keep only latest sample sites = defaultdict(list) with open(csv_file, 'r') as f: reader = csv.DictReader(f) for row in reader: sites[row['site_id']].append(row) # Get latest sample for each site features = [] for site_id, samples in sites.items(): latest = max(samples, key=lambda x: x['sample_date']) feature = { "type": "Feature", "geometry": { "type": "Point", "coordinates": [float(latest['longitude']), float(latest['latitude'])] }, "properties": { "site_id": site_id, "site_name": latest['site_name'], "sample_date": latest['sample_date'], "e_coli_cfu": float(latest['e_coli_cfu']), "turbidity_ntu": float(latest['turbidity_ntu']), "ph": float(latest['ph']), "temperature_c": float(latest['temperature_c']), "contaminated": float(latest['e_coli_cfu']) > 400, # EPA threshold "notes": latest['notes'] } } features.append(feature) geojson = { "type": "FeatureCollection", "features": features } with open(output_file, 'w') as f: json.dump(geojson, f, indent=2) water_quality_to_geojson('water_quality.csv', 'water_quality.geojson')
Step 3: Upload to Maps Datasets API
bash# 1. Create dataset curl -X POST "https://mapsplatformdatasets.googleapis.com/v1alpha/projects/YOUR_PROJECT/datasets" \ -H "Authorization: Bearer $(gcloud auth print-access-token)" \ -H "Content-Type: application/json" \ -d '{ "displayName": "Tijuana Water Infrastructure", "description": "Treatment plants, sewer lines, and monitoring sites" }' # Response includes dataset ID: projects/YOUR_PROJECT/datasets/DATASET_ID # 2. Upload GeoJSON to Cloud Storage gsutil cp facilities.geojson gs://your-bucket/ gsutil cp sewer_lines.geojson gs://your-bucket/ gsutil cp water_quality.geojson gs://your-bucket/ # 3. Import GeoJSON to dataset curl -X POST "https://mapsplatformdatasets.googleapis.com/v1alpha/projects/YOUR_PROJECT/datasets/DATASET_ID:import" \ -H "Authorization: Bearer $(gcloud auth print-access-token)" \ -H "Content-Type: application/json" \ -d '{ "inputFormat": "GEO_JSON", "gcsSource": { "uri": "gs://your-bucket/facilities.geojson" } }' # Repeat for sewer_lines.geojson and water_quality.geojson
Or load directly in JavaScript (for smaller datasets):
javascriptmap.data.loadGeoJSON('facilities.geojson');
Step 4: Build Interactive Dashboard
index.html:
html<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Tijuana Water Infrastructure Dashboard</title> <style> body { margin: 0; font-family: 'Segoe UI', Arial, sans-serif; } #header { background: #1976D2; color: white; padding: 15px; display: flex; justify-content: space-between; align-items: center; } #filters { background: #f5f5f5; padding: 10px; border-bottom: 1px solid #ddd; } #map { height: calc(100vh - 120px); } .legend { background: white; padding: 15px; margin: 10px; border-radius: 5px; box-shadow: 0 2px 6px rgba(0,0,0,0.3); } .legend-item { display: flex; align-items: center; margin: 5px 0; } .legend-color { width: 20px; height: 20px; margin-right: 10px; border-radius: 50%; } </style> </head> <body> <div id="header"> <h1>Tijuana Water Infrastructure</h1> <div> <span id="facility-count">0 facilities</span> | <span id="line-count">0 sewer lines</span> </div> </div> <div id="filters"> <label><input type="checkbox" checked data-layer="facilities"> Treatment Plants & Pump Stations</label> <label><input type="checkbox" checked data-layer="sewer-lines"> Sewer Lines</label> <label><input type="checkbox" checked data-layer="water-quality"> Water Quality Sites</label> <select id="condition-filter"> <option value="all">All Conditions</option> <option value="good">Good</option> <option value="moderate_wear">Moderate Wear</option> <option value="poor">Poor</option> </select> </div> <div id="map"></div> <script src="dashboard.js"></script> <script async src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&callback=initDashboard"> </script> </body> </html>
dashboard.js:
javascriptlet map; let facilitiesLayer; let sewerLinesLayer; let waterQualityLayer; function initDashboard() { // Initialize map centered on Tijuana map = new google.maps.Map(document.getElementById("map"), { center: {lat: 32.4850, lng: -116.9500}, zoom: 11, mapId: "water_infrastructure", mapTypeId: 'hybrid' // Satellite with labels }); // Add legend addLegend(); // Load data layers loadFacilities(); loadSewerLines(); loadWaterQuality(); // Set up filters setupFilters(); } function loadFacilities() { facilitiesLayer = map.data; facilitiesLayer.loadGeoJSON('facilities.geojson', null, (features) => { document.getElementById('facility-count').textContent = `${features.length} facilities`; }); // Style based on facility type and condition facilitiesLayer.setStyle((feature) => { const type = feature.getProperty('type'); const condition = feature.getProperty('condition'); let color; if (condition === 'good') color = '#4CAF50'; else if (condition === 'moderate_wear') color = '#FF9800'; else if (condition === 'poor') color = '#F44336'; else color = '#2196F3'; let scale; if (type === 'treatment_plant') scale = 12; else if (type === 'pump_station') scale = 8; else scale = 6; return { icon: { path: google.maps.SymbolPath.CIRCLE, scale: scale, fillColor: color, fillOpacity: 0.9, strokeColor: '#fff', strokeWeight: 2 } }; }); // Click handler facilitiesLayer.addListener('click', (event) => { const props = {}; event.feature.forEachProperty((value, key) => { props[key] = value; }); showFacilityInfo(props, event.latLng); }); } function loadSewerLines() { // Create separate data layer for sewer lines sewerLinesLayer = new google.maps.Data(); sewerLinesLayer.setMap(map); sewerLinesLayer.loadGeoJSON('sewer_lines.geojson'); // Style based on condition sewerLinesLayer.setStyle((feature) => { const condition = feature.getProperty('condition'); let color; if (condition === 'good') color = '#4CAF50'; else if (condition === 'moderate_wear') color = '#FF9800'; else color = '#F44336'; return { strokeColor: color, strokeWeight: 3, strokeOpacity: 0.7 }; }); } function loadWaterQuality() { waterQualityLayer = new google.maps.Data(); waterQualityLayer.setMap(map); waterQualityLayer.loadGeoJSON('water_quality.geojson'); // Style based on contamination level waterQualityLayer.setStyle((feature) => { const contaminated = feature.getProperty('contaminated'); const e_coli = feature.getProperty('e_coli_cfu'); let color, scale; if (contaminated) { color = '#F44336'; // Red scale = 10; } else if (e_coli > 200) { color = '#FF9800'; // Orange scale = 8; } else { color = '#4CAF50'; // Green scale = 6; } return { icon: { path: google.maps.SymbolPath.FORWARD_CLOSED_ARROW, scale: scale, fillColor: color, fillOpacity: 0.8, strokeColor: '#fff', strokeWeight: 2, rotation: 90 } }; }); // Click handler waterQualityLayer.addListener('click', (event) => { const props = {}; event.feature.forEachProperty((value, key) => { props[key] = value; }); showWaterQualityInfo(props, event.latLng); }); } function showFacilityInfo(props, latLng) { const content = ` <div style="max-width: 300px;"> <h3>${props.name}</h3> <p><strong>Type:</strong> ${props.type.replace('_', ' ')}</p> <p><strong>Capacity:</strong> ${props.capacity_mgd || 'N/A'} MGD</p> <p><strong>Status:</strong> ${props.status}</p> <p><strong>Condition:</strong> ${props.condition}</p> <p><strong>Last Inspection:</strong> ${props.last_inspection}</p> <p><strong>Notes:</strong> ${props.notes}</p> </div> `; const infoWindow = new google.maps.InfoWindow({ content: content, position: latLng }); infoWindow.open(map); } function showWaterQualityInfo(props, latLng) { const safeLevel = props.e_coli_cfu <= 400; const content = ` <div style="max-width: 300px;"> <h3>${props.site_name}</h3> <p><strong>Sample Date:</strong> ${props.sample_date}</p> <p><strong>E. coli:</strong> ${props.e_coli_cfu} CFU/100mL <span style="color: ${safeLevel ? 'green' : 'red'}"> ${safeLevel ? 'β Safe' : 'β Elevated'} </span> </p> <p><strong>Turbidity:</strong> ${props.turbidity_ntu} NTU</p> <p><strong>pH:</strong> ${props.ph}</p> <p><strong>Temperature:</strong> ${props.temperature_c}Β°C</p> <p><strong>Notes:</strong> ${props.notes}</p> </div> `; const infoWindow = new google.maps.InfoWindow({ content: content, position: latLng }); infoWindow.open(map); } function addLegend() { const legend = document.createElement('div'); legend.className = 'legend'; legend.innerHTML = ` <h4 style="margin-top: 0;">Legend</h4> <div class="legend-item"> <div class="legend-color" style="background: #4CAF50;"></div> Good Condition </div> <div class="legend-item"> <div class="legend-color" style="background: #FF9800;"></div> Moderate Wear </div> <div class="legend-item"> <div class="legend-color" style="background: #F44336;"></div> Poor Condition </div> <hr> <div class="legend-item"> <div class="legend-color" style="background: #4CAF50; width: 30px; height: 15px; border-radius: 0;"></div> Safe Water </div> <div class="legend-item"> <div class="legend-color" style="background: #F44336; width: 30px; height: 15px; border-radius: 0;"></div> Contaminated </div> `; map.controls[google.maps.ControlPosition.RIGHT_BOTTOM].push(legend); } function setupFilters() { // Layer visibility toggles document.querySelectorAll('#filters input[type="checkbox"]').forEach(checkbox => { checkbox.addEventListener('change', (e) => { const layer = e.target.dataset.layer; if (layer === 'facilities') { facilitiesLayer.setMap(e.target.checked ? map : null); } else if (layer === 'sewer-lines') { sewerLinesLayer.setMap(e.target.checked ? map : null); } else if (layer === 'water-quality') { waterQualityLayer.setMap(e.target.checked ? map : null); } }); }); // Condition filter document.getElementById('condition-filter').addEventListener('change', (e) => { const condition = e.target.value; facilitiesLayer.setStyle((feature) => { const featureCondition = feature.getProperty('condition'); if (condition === 'all' || featureCondition === condition) { return {visible: true}; } else { return {visible: false}; } }); }); }
Step 5: Add Advanced Features
Export Inspection Report
javascriptfunction exportInspectionReport() { const facilities = []; facilitiesLayer.forEach((feature) => { const props = {}; feature.forEachProperty((value, key) => { props[key] = value; }); facilities.push(props); }); // Generate CSV const csv = [ Object.keys(facilities[0]).join(','), ...facilities.map(f => Object.values(f).join(',')) ].join('\n'); // Download const blob = new Blob([csv], {type: 'text/csv'}); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `infrastructure-report-${new Date().toISOString().split('T')[0]}.csv`; a.click(); }
Calculate Optimal Inspection Route
javascriptasync function calculateInspectionRoute() { // Get all facilities needing inspection const sites = []; facilitiesLayer.forEach((feature) => { const condition = feature.getProperty('condition'); if (condition !== 'good') { const geom = feature.getGeometry(); sites.push({ name: feature.getProperty('name'), coords: `${geom.get().lat()},${geom.get().lng()}` }); } }); // Use MCP tool to calculate optimal route const route = await maps_directions( origin: sites[0].coords, destination: sites[0].coords, // Return to start waypoints: "optimize:true|" + sites.slice(1).map(s => s.coords).join('|'), mode: 'driving' ); // Display route on map const path = google.maps.geometry.encoding.decodePath( route.routes[0].overview_polyline.points ); new google.maps.Polyline({ path: path, strokeColor: '#2196F3', strokeWeight: 5, map: map }); }
Step 6: Deploy and Maintain
Deploy:
- Use GitHub Pages, Netlify, or your own server
- Ensure GeoJSON files are accessible
- Restrict API key to your domain
Maintenance:
- Update GeoJSON files monthly (or after inspections)
- Re-upload to Maps Datasets API
- Map updates automatically
Automation:
bash#!/bin/bash # Daily update script # 1. Export latest data from database psql -d water_db -c "COPY (SELECT * FROM facilities) TO '/tmp/facilities.csv' CSV HEADER" # 2. Convert to GeoJSON python3 convert_to_geojson.py /tmp/facilities.csv facilities.geojson # 3. Upload to Cloud Storage gsutil cp facilities.geojson gs://your-bucket/ # 4. Trigger dataset refresh (API call) curl -X POST "https://mapsplatformdatasets.googleapis.com/v1alpha/.../datasets/...:import" \ -H "Authorization: Bearer $(gcloud auth print-access-token)" \ -H "Content-Type: application/json" \ -d '{"inputFormat": "GEO_JSON", "gcsSource": {"uri": "gs://your-bucket/facilities.geojson"}}'
Troubleshooting
GeoJSON not loading:
- Validate GeoJSON at geojson.io
- Check coordinates are [lng, lat] not [lat, lng]
- Verify file is accessible (CORS if separate domain)
Dataset upload fails:
- Check file size (<5 GB)
- Verify authentication (
gcloud auth) - Ensure Datasets API is enabled
Styling not applying:
- Check property names match GeoJSON properties exactly
- Use
console.logto inspect feature properties - Verify condition values match your style logic
Related Workflows
- Building an Audio Soundwalk Map - Similar techniques for audio data
- Using MCP Tools for Google Maps Integration - Backend data processing
Resources
- Maps Datasets API Documentation
- GeoJSON specification
- QGIS for data preparation and analysis
- PostGIS for spatial database management