Skip to main content

Overview

Mas Agua’s mapping feature provides geographic visualization of water infrastructure using MapLibre GL JS. Display real-time sensor data, pump stations, and tank locations on interactive maps with customizable markers and popups. Map showing water infrastructure markers

Key Features

  • MapLibre GL: Open-source map rendering with vector tiles
  • Real-time Data: 15-second refresh interval for marker values
  • Interactive Markers: Draggable pins with custom labels
  • Popup Information: Live data display in map popups
  • Navigation Controls: Zoom, pan, rotate, and tilt
  • Geolocation: User location tracking
  • Fullscreen Mode: Expand map to full screen

Map Components

MapBase Component

Core map rendering component with configurable controls:
import MapBase from './components/MapBase'

<MapBase
  height="100%"
  width="100%"
  navigationcontrol={true}
  fullScreen={true}
  geolocation={true}
  controlPanel={false}
  markers={markers}
  setMarkers={setMarkers}
  viewState={viewState}
  setViewState={setViewState}
  draggable={false}
  withInfo={true}
/>

Props Reference

PropTypeDefaultDescription
navigationcontrolbooleantrueShow zoom/rotate controls
heightstring’100%‘Map container height
widthstring’100%‘Map container width
fullScreenbooleantrueShow fullscreen button
geolocationbooleantrueShow locate user button
controlPanelbooleantrueShow marker control panel
markersarray[]Array of marker objects
setMarkersfunction-Update markers state
viewStateobject-Map viewport state
setViewStatefunction-Update viewport state
draggablebooleanfalseAllow marker dragging
withInfobooleanfalseFetch and display real-time data

Map Configuration

View State

Defines the map’s viewport:
const [viewState, setViewState] = useState({
  longitude: -62.005196197872266,
  latitude: -30.716256365145455,
  zoom: 14,
  bearing: 0,    // Map rotation (0-360°)
  pitch: 0,      // Map tilt (0-60°)
})

Map Style

Using MapTiler vector tiles:
<Map
  {...viewState}
  style={{ width, height }}
  mapStyle="https://api.maptiler.com/maps/streets/style.json?key=mHpRzO9eugI7vKv1drLO"
  onMove={(e) => setViewState(e.viewState)}
>
  {/* Map controls and markers */}
</Map>

Markers

Marker Structure

const marker = {
  name: 'Tanque Principal',
  latitude: -30.716256,
  longitude: -62.005196,
  popupInfo: {
    lat: -30.716256,
    lng: -62.005196,
    idVar: 15,
    data: {
      id: 15,
      name: 'nivel_tanque_01',
      unit: '%',
      varsInflux: {
        tank_level: {
          calc_field: 'nivel',
          measure: 'tanques'
        }
      }
    },
    value: '78.5 %'  // Updated in real-time
  }
}

Adding Markers

Markers are added through a form interface:
<TextField
  {...register('markerName', {
    required: 'Debe dar un nombre al marcador',
    validate: (value) =>
      !markers.some((marker) => marker.name === value) ||
      'El marcador ya existe',
  })}
  label="Nombre del marcador"
/>

<TextField
  {...register('markerLat', {
    required: 'Debe asignar una latitud',
  })}
  label="Latitud"
/>

<TextField
  {...register('markerLng', {
    required: 'Debe asignar una longitud',
  })}
  label="Longitud"
/>

<SelectVars
  setValue={setValue}
  label="Seleccione una variable"
/>

<Button onClick={saveMarker}>
  Agregar Marcador
</Button>

Generating Markers

const generateMarker = (name, lat, lng, idVar, data) => {
  return {
    name,
    latitude: parseFloat(lat),
    longitude: parseFloat(lng),
    popupInfo: {
      lat: parseFloat(lat),
      lng: parseFloat(lng),
      idVar: idVar,
      data: data || null,
    },
  }
}

Real-time Data Updates

Automatic Refresh

Markers update their values every 15 seconds when withInfo={true}:
useEffect(() => {
  if (!withInfo) return

  fetchMultipleInfluxValues()

  const interval = setInterval(
    fetchMultipleInfluxValues,
    15000  // 15 seconds
  )
  
  return () => clearInterval(interval)
}, [])

Fetching Marker Data

const fetchMultipleInfluxValues = async () => {
  if (!markers || markers.length === 0) return

  // Extract InfluxDB variables from all markers
  const vars = extractInfluxVarsFromMarkers(markers)

  try {
    const { data } = await request(
      `${backend[import.meta.env.VITE_APP_NAME]}/multipleDataInflux`,
      'POST',
      vars
    )

    // Update markers with new values
    const updated = markers.map((marker) => {
      const id = marker.popupInfo.data.id
      const value = data[id] ?? 'Sin datos'

      return {
        ...marker,
        popupInfo: {
          ...marker.popupInfo,
          value: `${value} ${marker.popupInfo.data.unit ?? ''}`
        }
      }
    })

    setMarkers(updated)

  } catch (error) {
    console.error("Error múltiples influx en mapa:", error)
  }
}

Variable Extraction

function extractInfluxVarsFromMarkers(markers) {
  return markers.map((m) => ({
    dataInflux: m.popupInfo.data  // The complete InfluxDB config
  }))
}

Popups

Popups show marker name and real-time value:
{withInfo && marker.popupInfo && marker.popupInfo.data && (
  <Popup
    anchor="top-left"
    closeButton={false}
    latitude={Number(marker.popupInfo.lat)}
    longitude={Number(marker.popupInfo.lng)}
    closeOnClick={false}
    className="!rounded-xl !shadow-md"
  >
    <Typography variant="body3">
      {marker.popupInfo.data.name ?? 'No hay datos'}
    </Typography>
    <Typography variant="body2">
      {formatMarkerValue(marker)}
    </Typography>
  </Popup>
)}

Status Field Formatting

Boolean status fields are interpreted as on/off:
const formatMarkerValue = (marker) => {
  const rawValue = marker.popupInfo.value

  if (rawValue == null) return "No hay datos"

  // Get calc_field from InfluxDB config
  const influxConfig = Object.values(
    marker.popupInfo.data.varsInflux
  )[0]
  const calcField = influxConfig.calc_field

  // Interpret status fields as binary
  if (
    calcField === "status" ||
    calcField === "estados_0" ||
    calcField.includes("estado")
  ) {
    const numeric = Number(rawValue)
    return numeric === 1 ? "Encendido" : "Apagado"
  }

  // Return numeric value with unit
  return rawValue
}

Pin Component

Custom pin marker with label:
import Pin from './components/Pin'

<Pin 
  label="Tanque Norte"
  color="#3498db"
/>

Draggable Markers

When draggable={true}, markers can be repositioned:
<Marker
  longitude={marker.longitude}
  latitude={marker.latitude}
  draggable={draggable}
  onDragEnd={(e) => {
    const { lng, lat } = e.lngLat
    const updatedMarkers = markers.map((m, i) =>
      i === index
        ? { ...m, longitude: lng, latitude: lat }
        : m
    )
    setMarkers(updatedMarkers)
  }}
>
  <Pin label={marker.name} color="#3498db" />
</Marker>

Map Controls

Zoom and rotation buttons:
import { NavigationControl } from 'react-map-gl/maplibre'

<NavigationControl position="top-left" />

Fullscreen Control

Expand map to fullscreen:
import { FullscreenControl } from 'react-map-gl/maplibre'

<FullscreenControl position="top-left" />

Geolocation Control

Locate user position:
import { GeolocateControl } from 'react-map-gl/maplibre'

<GeolocateControl position="top-left" />

Control Panel

Optional marker management panel when controlPanel={true}:
import ControlPanel from './components/ControlPanel'

<ControlPanel 
  markers={markers}
  setMarkers={setMarkers}
/>

Saving Maps

Maps are saved to the backend with all configuration:
const handleSubmit = async () => {
  if (!markers || markers.length === 0) {
    await Swal.fire({
      icon: 'error',
      title: 'Atención!',
      html: '<h3>Debe haber al menos un marcador</h3>',
    })
    return
  }
  
  const mapName = await askMapName()
  if (!mapName) return

  const map = {
    name: mapName,
    viewState,
    markers,
  }

  const result = await saveMap(map)
  
  if (result) {
    await Swal.fire({
      title: 'Éxito',
      icon: 'success',
      html: '<h3>El mapa se guardó con éxito</h3>',
    })
    navigate('/maps')
  }
}

Save API

const saveMap = async (map) => {
  const url = `${backend[import.meta.env.VITE_APP_NAME]}/map`
  const result = await request(url, 'POST', map)
  return result
}

Loading Maps

Load existing map configuration:
const searchMap = async (id) => {
  try {
    const url = `${backend[import.meta.env.VITE_APP_NAME]}/map?id=${id}`
    const { data } = await request(url, 'GET')

    setNameMap(data[0].name)
    
    // Restore view state
    const viewStateObject = {
      longitude: Number(data[0].longitude),
      latitude: Number(data[0].latitude),
      zoom: Number(data[0].zoom),
      bearing: Number(data[0].bearing),
      pitch: Number(data[0].pitch),
    }
    setViewState(viewStateObject)
    
    // Restore markers
    const markers = data[0].MarkersMaps.map((markerMap) => {
      return generateMarker(
        markerMap.name,
        markerMap.latitude,
        markerMap.longitude,
        markerMap.PopUpsMarkers.idVar,
        markerMap.PopUpsMarkers.InfluxVar
      )
    })

    setMarkers(markers)
  } catch (error) {
    console.error('Error loading map:', error)
  }
}

Map Views

View-Only Mode

Display saved map with real-time data:
<MapView 
  create={false}
  search={true}
/>

Edit Mode

Edit existing map configuration:
<MapView 
  create={true}
  search={true}
/>

Create Mode

Create new map:
<MapView 
  create={true}
  search={false}
/>

Responsive Layout

<div className="w-full h-[88vh] flex flex-col gap-1">
  {create && (
    <div className="flex flex-col gap-3 sm:flex-row sm:justify-between">
      <FormLabel className="w-full text-center !text-3xl">
        {create && search ? 'Editar Mapa' : 'Crear Mapa'}
      </FormLabel>
      <Button onClick={handleSubmit} color="success">
        Guardar
      </Button>
    </div>
  )}
  
  <CardCustom className="p-3 rounded-xl flex-1">
    <MapBase
      height="100%"
      markers={markers}
      setMarkers={setMarkers}
      viewState={viewState}
      setViewState={setViewState}
      controlPanel={create}
      draggable={create}
      withInfo={!create}
    />
  </CardCustom>
</div>

Use Cases

Water Distribution Network

Map showing pressure sensors across the network:
const markers = [
  {
    name: 'Presión Norte',
    latitude: -30.715,
    longitude: -62.004,
    popupInfo: {
      data: {
        name: 'presion_norte',
        unit: 'PSI',
        varsInflux: { pressure: { calc_field: 'presion', measure: 'sensores' }}
      }
    }
  },
  // More markers...
]

Tank Monitoring

Tank locations with level indicators:
const markers = [
  {
    name: 'Tanque Elevado',
    latitude: -30.720,
    longitude: -62.010,
    popupInfo: {
      data: {
        name: 'nivel_tanque_01',
        unit: '%',
        varsInflux: { level: { calc_field: 'nivel', measure: 'tanques' }}
      }
    }
  },
]

Pump Stations

Pump locations with on/off status:
const markers = [
  {
    name: 'Estación Bombeo 1',
    latitude: -30.718,
    longitude: -62.008,
    popupInfo: {
      data: {
        name: 'bomba_principal',
        unit: '',
        varsInflux: { status: { calc_field: 'status', measure: 'bombas' }}
      }
    }
  },
]

Best Practices

  • Keep markers under 50 per map for performance
  • Use clustering for dense areas (future feature)
  • Split large networks into regional maps
  • 15-second interval is optimal for real-time monitoring
  • Disable real-time updates (withInfo={false}) for static maps
  • Monitor network usage with many markers
  • Use consistent marker colors for similar asset types
  • Consider using custom icons for different infrastructure
  • Keep popup information concise

Troubleshooting

Markers Not Updating

  1. Check withInfo={true} is set
  2. Verify InfluxDB variable configuration in markers
  3. Check browser console for API errors
  4. Confirm 15-second refresh interval is running

Map Not Loading

  1. Verify MapTiler API key is valid
  2. Check internet connection
  3. Confirm react-map-gl and maplibre-gl versions
  4. Check browser console for WebGL errors

Dragging Issues

  1. Ensure draggable={true} is set on MapBase
  2. Check markers have unique IDs
  3. Verify setMarkers function is provided

Next Steps

Dashboard

Display map data in dashboard charts

Diagram Editor

Create visual network diagrams