reactjsopenstreetmapoverpass-api

Select Multiple Buildings using Overpass API Endpoint


I have the code below that shows the surface area of a building in OpenStreetMap when hovered. Here's the code:

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: any, wait: any) => {
  let timeout: any;

  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 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').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 };
        }
      });

      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]);

  // 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>
      <div id="map" style={{ height: '420px', width: '420px' }}></div>
    </div>
  );
};

export default MapComponent;

I want it to be selectable on click and have that value then saved. Multiple buildings need to be saved. In addition, is there a way to get the roof angle data as well?

I need the angle data as I need to calculate the true area of the roof of a building, not just the space it occupies on a 2D map. This specific true area value needs to then be saved.

How can I fix this?


Solution

  • Is there a way to get the roof angle data as well? No, I can't find the solution. I check Overpass documentation, There are no angle of building or roof data.

    This demo can handle Multiple buildings save data by mouse click over the build area.

    Save format example

    Area: 197.70, Latitude: 59.13330, Longitude: 9.72721, Tags: {"building":"house","ref:bygningsnr":"300001805"}
    

    Adding feature Tags and shows all of saved build information by Clicking.

      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]);
      };
    

    Call this call back function from map's <div> tag by onClick() event.

    <div id="map" style={{ height: '800px', width: '100%', maxWidth: '1000px' }} onClick={handleMouseClick}></div>
    

    Getting Tags information from Overpass API

    if (element.type === 'way' && element.tags) {
       setBuildingTag(JSON.stringify(element.tags));
    }
    

    Data source from Overpass

    {
      "type": "way",
      "id": 944874632,
      "nodes": [
        8747572457,
        8747572456,
        8747572455,
        8747572454,
        8747572453,
        8747557628,
        8747557629,
        8747572452,
        8747572451,
        8747572457
      ],
      "tags": {
        "building": "house",
        "building:levels": "2",
        "ref:bygningsnr": "22480235"
      }
    }
    

    Demo code

    import React, { 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;
    

    Result

    enter image description here