← Back to articles

Mapping Water Infrastructure with Custom Data

Path: Computer Tech/Development/Tech Companies/Google/Google Maps Platform/Workflows/Mapping Water Infrastructure with Custom Data.mdUpdated: 2/3/2026

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:

csv
facility_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:

python
import 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):

csv
line_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)

csv
site_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

python
import 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

python
def 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)

python
import 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):

javascript
map.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:

javascript
let 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

javascript
function 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

javascript
async 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.log to inspect feature properties
  • Verify condition values match your style logic

Related Workflows

Resources