Angelos Orfanakos

GoToCoordinates component for React Leaflet

React Leaflet is a React library that exposes Leaflet classes as React components, making it very easy to add interactive maps to React web apps.

This is the fourth in a series of posts on how I use the library. My intention is to share back some of the things I’ve learned and implemented in the hope of them being useful to others.

In this post, I present the GoToCoordinates component which accepts a pair of latitude and longitude coordinates and centers the map, drawing a marker on the exact point. You can see it in action on vouna.gr.

First, the component:

import React from 'react';
import PropTypes from 'prop-types';
import { useMap } from 'react-leaflet';

const MAP_BOUNDS = [[31.08, 14.26], [44.91, 34.69]]; // Greece
const MIN_ZOOM = 13;
const PLACEHOLDER = '37.984167, 23.728056'; // Athens, center
const COORDINATES_REGEX =
  /^\s*-?(90|[1-8]?[0-9])(\.\d+)?\s*°?(\s*,\s*|\s+)-?(180|1[0-7][0-9]|[1-9]?[0-9])(\.\d+)?\s*°?\s*$/;

function coordinatesWithinBounds(latitude, longitude) {
  const [[minLat, minLon], [maxLat, maxLon]] = MAP_BOUNDS;

  return (
    latitude >= minLat &&
    latitude <= maxLat &&
    longitude >= minLon &&
    longitude <= maxLon
  );
}

function GoToCoordinates(props) {
  const { setPointCoordinates, clearPointCoordinates } = props;

  const [coordinates, setCoordinates] = React.useState('');

  const map = useMap();

  function handleCoordinatesChange(event) {
    const {
      target: { value },
    } = event;

    setCoordinates(value);

    if (value.length === 0 || !COORDINATES_REGEX.test(value)) {      clearPointCoordinates();      return;    }
    const [latitudeStr, longitudeStr] = value.split(/\s*,\s*/);
    const latitude = parseFloat(latitudeStr, 10);
    const longitude = parseFloat(longitudeStr, 10);

    if (!coordinatesWithinBounds(latitude, longitude)) {      clearPointCoordinates();      return;    }
    setPointCoordinates([latitude, longitude]);

    const zoom = Math.max(MIN_ZOOM, map.getZoom());
    map.setView([latitude, longitude], zoom);
  }

  function handleClearChange() {
    setCoordinates('');
    clearPointCoordinates();
  }

  return (
    <div className="leaflet-control">
      <input
        type="text"
        size="15"
        title="Go to coordinates"
        placeholder={PLACEHOLDER}
        value={coordinates}
        onChange={handleCoordinatesChange}
      />
      {coordinates.length > 0 && (        <button onClick={handleClearChange} title="Καθαρισμός">          &times;        </button>      )}    </div>
  );
}

GoToCoordinates.propTypes = {
  setPointCoordinates: PropTypes.func.isRequired,
  clearPointCoordinates: PropTypes.func.isRequired,
};

export default GoToCoordinates;

Things to note:

  • Lines 36-39: If text is empty or not a valid coordinate pair, the marker is removed
  • Lines 45-48: If text is a valid coordinate pair but not within the desired bounds (in this example, Greece), the marker is removed
  • Lines 71-75: If text is present, a clear “x” button is shown that empties the text field and removes the marker when clicked

Then, the necessary styles for the clear “x” button:

.leaflet-control.go-to-coordinates button {
  border: none;
  background: transparent;
  display: inline-block;
  margin-left: -30px;
  width: 30px;
  text-align: center;
  font-size: 1.5em;
  cursor: pointer;
  opacity: 50%;
}

And finally, here’s how you’d use it in a map:

import React from 'react';
import { Marker } from 'react-leaflet';

import GoToCoordinates from './GoToCoordinates';

const MAP_BOUNDS = [[31.08, 14.26], [44.91, 34.69]]; // Greece

function round(number, precision = 0) {
  return (
    Math.round(number * Math.pow(10, precision) + Number.EPSILON) /
    Math.pow(10, precision)
  );
}

function formatLatitude(latitude) {
  const direction = latitude > 0 ? 'N' : 'S';
  return `${round(Math.abs(latitude), 6)}° ${direction}`;
}

function formatLongitude(longitude) {
  const direction = longitude > 0 ? 'E' : 'W';
  return `${round(Math.abs(longitude), 6)}° ${direction}`;
}

function MyMap() {
  const [goToPoint, setGoToPoint] = React.useState(null);

  function clearPointCoordinates() {
    setGoToPoint(null);
  }

  return (
    <MapContainer
      bounds={MAP_BOUNDS}
      style={{ width: '100%', height: '100vh' }}
    >
      <TileLayer
        url="https://{s}.tile.osm.org/{z}/{x}/{y}.png"
        attribution='&copy; <a href="https://osm.org/copyright">OpenStreetMap</a> contributors'
      />
      {goToPoint !== null && (        <Marker position={goToPoint}>          <Tooltip>            {formatLatitude(goToPoint[0])}, {formatLongitude(goToPoint[1])}          </Tooltip>        </Marker>      )}      <div className="leaflet-bottom leaflet-left">
        <GoToCoordinates          setPointCoordinates={setGoToPoint}          clearPointCoordinates={clearPointCoordinates}        />      </div>
    </MapContainer>
  );
}

export default MyMap;

Things to note:

  • Lines 41-47: The marker is shown only if its coordinates are present. When shown, its tooltip displays its formatted coordinates.
  • Lines 49-52: The component in action