Dynamically Update Overpass API Map

102 Views Asked by At

I have a function that, when given the house address of someone, will find the coordinates on a map and return them. I also have another function that calculates the surface area of a building on a map using the Overpass API. Here's the code:

Find coords function:

const [addressInput, setAddressInput] = React.useState('');
  const [selectedAddress, setSelectedAddress] = React.useState('');
  const [suggestions, setSuggestions] = React.useState([]);
  const [coords, setCoords] = React.useState([]);

  const handleAddressChange = async (query) => {
    setAddressInput(query);

    if (query.length >= 3) {
      try {
        // Specify the bounding box for Vestfold og Telemark (adjusted coordinates)
        const boundingBox = '8.0,58.5,11.0,59.5';

        const response = await fetch(
          `https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(
            query
          )}&bounded=1&viewbox=${boundingBox}&countrycodes=NO&limit=6`
        );

        const data = await response.json();
        setSuggestions(data);
      } catch (error) {
        console.error('Error fetching address suggestions:', error);
      }
    } else {
      setSuggestions([]);
    }
  };

  const handleAddressSelect = (address: any) => {
    setAddressInput(address.display_name);
    setSelectedAddress(address.display_name);
    setCoords([address.lat, address.lon]);
  }

Map & area function:

import { useState, useEffect, useRef, useCallback } from 'react';
import axios from 'axios';
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';
import * as turf from '@turf/turf';

const debounce = (func, wait) => {
  let timeout;

  return function executedFunction(...args) {
    const later = () => {
      clearTimeout(timeout);
      func(...args);
    };

    clearTimeout(timeout);
    timeout = setTimeout(later, wait);
  };
};

const MapComponent = () => {
  const [area, setArea] = useState(0);
  const [mousePosition, setMousePosition] = useState({ lat: 0, lon: 0 });
  const [dataToSave, setDataToSave] = useState([]);
  const [buildingTag, setBuildingTag] = useState("");
  const mapRef = useRef(null);

  // Initialize the map
  useEffect(() => {
    if (!mapRef.current) {
      const map = L.map('map').setView([59.132659900251944, 9.727169813491393], 18);
      L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
        attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
      }).addTo(map);

      map.on('mousemove', (e) => {
        setMousePosition({ lat: e.latlng.lat, lon: e.latlng.lng });
      });

      mapRef.current = map;
    }
  }, []);

  const displayBuildings = useCallback((buildingData) => {
    if (buildingData && mapRef.current) {
      mapRef.current.eachLayer((layer) => {
        if (layer instanceof L.Polygon) {
          mapRef.current.removeLayer(layer);
        }
      });

      let totalArea = 0;
      const nodeMapping = {};

      buildingData.elements.forEach(element => {
        if (element.type === 'node') {
          nodeMapping[element.id] = { lat: element.lat, lon: element.lon };
        }
        if (element.type === 'way' && element.tags) {
          setBuildingTag(JSON.stringify(element.tags));
        }
      });

      const features = buildingData.elements.filter(element => element.type === 'way');

      features.forEach(feature => {
        if (feature.nodes && feature.nodes.length > 0) {
          const coordinates = feature.nodes.map(nodeId => {
            const node = nodeMapping[nodeId];
            return [node.lat, node.lon]; // Lon, Lat format for Leaflet
          });

          if (coordinates.length > 0) {
            L.polygon(coordinates, { color: 'blue' }).addTo(mapRef.current);

            const geoJsonPolygon = {
              type: 'Polygon',
              coordinates: [coordinates],
            };

            totalArea += turf.area(geoJsonPolygon);
          }
        }
      });

      setArea(totalArea);
    }
  }, []);

  // Function to fetch and display building data
  const fetchAndDisplayBuildingData = useCallback(async (lat, lon) => {
    try {
      const response = await axios.post(
        'https://overpass-api.de/api/interpreter',
        `[out:json];
          (
            is_in(${lat},${lon});
            area._[building];
          );
          out body; >; out skel qt;`,
        {
          headers: { 'Content-Type': 'text/plain' }
        }
      );

      displayBuildings(response.data);
    } catch (error) {
      console.error('Error fetching data:', error);
    }
  }, [displayBuildings]);

  const handleMouseClick = () => {
    const newDataItem = {
      area: area.toFixed(2),
      geo_location: {
        lat: mousePosition.lat.toFixed(5),
        longitude: mousePosition.lon.toFixed(5)
      },
      tags: buildingTag
    };

    setDataToSave([...dataToSave, newDataItem]);
  };

  const handleClearData = () => {
    setDataToSave([]);
  };

  // Debounced version of fetchAndDisplayBuildingData
  const debouncedFetchAndDisplay = useCallback(debounce(fetchAndDisplayBuildingData, 100), [
    fetchAndDisplayBuildingData
  ]);

  // Handle mouse movement
  useEffect(() => {
    if (mapRef.current) {
      mapRef.current.on('mousemove', (e) => {
        setMousePosition({ lat: e.latlng.lat, lon: e.latlng.lng });
        debouncedFetchAndDisplay(e.latlng.lat, e.latlng.lng);
      });
    }
  }, [debouncedFetchAndDisplay]);

  return (
    <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
      <p style={{ fontSize: '24px', textAlign: 'center' }}>Area: {area.toFixed(2)} square meters</p>
      <p style={{ fontSize: '24px', textAlign: 'center' }}>Mouse Position: Latitude: {mousePosition.lat.toFixed(5)}, Longitude: {mousePosition.lon.toFixed(5)}</p>
      <p style={{ fontSize: '24px', textAlign: 'center' }}>Tag: {buildingTag}</p>
      <div id="map" style={{ height: '800px', width: '100%', maxWidth: '1000px' }} onClick={handleMouseClick}></div>
      <button onClick={handleClearData}>Clear data</button>
      <div style={{ maxHeight: '400px', overflowY: 'auto' }}>
        <h2>Saved Data</h2>
        <ul>
          {dataToSave.map((data, index) => (
            <li key={index}>
              Area: {data.area}, Latitude: {data.geo_location.lat}, Longitude: {data.geo_location.longitude}, Tags: {data.tags}
            </li>
          ))}
        </ul>
      </div>
    </div>
  );
};

export default MapComponent;

Display suggestions & get user input function

<section className={styles.houseSection} id="body">
  <Fade duration={750}>
    <text className={styles.sectionTitle}>Find The Solution For You</text>
    <text className={styles.houseText}>Lorem ipsum dolor sit amet consectetur adipisicing elit. Libero voluptates minima quas necessitatibus totam. Aperiam delectus, fugiat autem fugit totam ipsa et velit aut eius nesciunt. Alias, amet! Iste, quibusdam.</text>
    <div className={styles.houseFormContainer}>
      <input className={styles.houseInput} placeholder="Input Home Address" value={addressInput} onChange={(e) => handleAddressChange(e.target.value)} type="text" autoComplete="street-address" />
    </div>
    <div className={styles.houseSuggestionsContainer}>
     {suggestions.map((suggestion) => (
       <HashLink smooth to="/#address-info" style={linkStyle}>
         <div key={suggestion.place_id} className={styles.houseSuggestion} onClick={() => handleAddressSelect(suggestion)}>{suggestion.display_name}</div>
       </HashLink>
      ))}
    </div>
  </Fade>
</section>

I'm expecting the user to input an address, have it autocomplete, then after the user selects the address, show the map centered at the coords. Then, the user can select one or two (max 3) buildings on the map, outputting the total area after each select. If the user changes the address, the map needs to refresh.

How can I fix this?

0

There are 0 best solutions below