Search code examples
reactjsopenstreetmapoverpass-api

Calculate Area on a Map Using Overpass API Endpoint


I'm trying to calculate the surface area of a building using the Overpass API, OpenStreetMap API, ReactJS. Here's the code:

import { useState, useEffect } from 'react';
import axios from 'axios';
import L from 'leaflet';
import 'leaflet/dist/leaflet.css'; // Import Leaflet CSS
import 'leaflet/dist/leaflet-src.esm'; // Import Leaflet as ES module
import * as turf from '@turf/turf'; // Import turf library

const MapComponent = () => {
  const [buildingData, setBuildingData] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        // Make a request to the Overpass API
        const response = await axios.get('https://api.openstreetmap.org/api/interpreter');
        setBuildingData(response.data);
      } catch (error) {
        console.error('Error fetching data:', error);
      }
    };

    fetchData();
  }, []); // Run once on component mount

  const calculateArea = () => {
    if (!buildingData || buildingData.features.length === 0) {
      return { areaSquareMeters: 0, areaSquareKilometers: 0 };
    }

    // Assuming buildingData is in a suitable format, adjust accordingly
    const osmCoordinates = buildingData.features[0].geometry.coordinates[0];

    // Convert OSM coordinates to Leaflet-compatible coordinates
    const leafletCoordinates = osmCoordinates.map(([lon, lat]) => [lat, lon]);

    // Create a Leaflet map with a simple base layer
    const map = L.map('map').setView([59.132659900251944, 9.727169813491393], 12);

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

    // Create a Leaflet polygon
    const polygon = new L.Polygon(leafletCoordinates);

    // Add the polygon to the map
    polygon.addTo(map);

    // Convert Leaflet polygon coordinates to GeoJSON format
    const geoJsonPolygon = {
      type: 'Polygon',
      coordinates: [leafletCoordinates],
    };

    // Calculate the area using the turf library
    const areaSquareMeters = turf.area(geoJsonPolygon);

    return {
      areaSquareMeters
    };
  };

  const area = calculateArea();

  return (
    <div style={{ width: "100%", height: "100vh", display: "flex", alignItems: "center", justifyContent: "center", flexDirection: "column" }}>
      <p>Area: {area.areaSquareMeters} square meters</p>
      <div id="map" style={{ height: '500px', width: '500px', border: "3.2px solid red" }}></div>
    </div>
  );
};

export default MapComponent;

I'm expecting a map to show (id="map" container) but it doesn't, and the area outputs 0. I've figured out that the problem lies with the Overpass API Endpoint (https://api.openstreetmap.org/api/interpreter).

Is there a way to fix this?


Solution

  • You Component needs there change two items

    API Endpoint: Later, you switched to using https://overpass-api.de/api/interpreter, which is specifically designed for complex and extensive queries on the OpenStreetMap database. And not GET , should be POST call.

    Query Format: The query format in Overpass QL was likely enhanced to include more specific parameters, filters, or bounding box coordinates to fetch more targeted data like buildings within a certain radius.

    More documentation in here

    Overpass QL

    Overpass API Wiki

    Overpass Turbo

    Wrong URL

    https://api.openstreetmap.org/api/interpreter
    

    enter image description here

    Correct URL

    https://overpass-api.de/api/interpreter
    

    Body with text format

    [out:json];
    (
      // Fetch buildings in a specific area
      way["building"](around:3000,59.132659900251944, 9.727169813491393); 
    );
    out body;
    >;
    out skel qt;
    

    enter image description here

    Demo component

    Save as 'MapComponent.js'

    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 MapComponent = () => {
      const [buildingData, setBuildingData] = useState(null);
      const [area, setArea] = useState(0);
      const mapRef = useRef(null);
    
      // Initialize the map
      useEffect(() => {
        if (!mapRef.current) {
          mapRef.current = L.map('map').setView([59.132659900251944, 9.727169813491393], 13); // Centered on Paris
          L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
            attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
          }).addTo(mapRef.current);
        }
      }, []);
    
      // Fetch building data
      useEffect(() => {
        const fetchData = async () => {
          try {
    
            const response = await axios.post(
              'https://overpass-api.de/api/interpreter',
              `[out:json];(way["building"](around:3000,59.132659900251944, 9.727169813491393););out body;>;out skel qt;`,
              {
                headers: { 'Content-Type': 'text/plain' }
              }
            );
      
            setBuildingData(response.data);
    
          } catch (error) {
            console.error('Error fetching data:', error);
          }
        };
    
        fetchData();
      }, []);
    
      const calculateAndDisplayArea = useCallback(() => {
        if (buildingData && mapRef.current) {
          let totalArea = 0;
          const nodeMapping = {}; // Assuming you have this mapping from node ID to { lat, lon }
      
          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];
              });
      
              if (coordinates.length > 0) {
                L.polygon(coordinates).addTo(mapRef.current); // Add polygon to map
      
                const geoJsonPolygon = {
                  type: 'Polygon',
                  coordinates: [coordinates.map(coord => [coord[1], coord[0]])], // Convert to [lon, lat]
                };
      
                totalArea += turf.area(geoJsonPolygon);
              }
            }
          });
      
          setArea(totalArea);
        }
      }, [buildingData]);
      
    
      useEffect(() => {
        calculateAndDisplayArea();
      }, [calculateAndDisplayArea, buildingData]);
    
      return (
        <div>
          <p>Area: {area.toFixed(2)} square meters</p>
          <div id="map" style={{ height: '800px', width: '1000px', margin: '0 auto'}}></div>
        </div>
      );
    };
    
    export default MapComponent;
    

    Result

    enter image description here

    The text "Area: 303102.35 square meters" represents This means that the total area covered by the buildings or features you queried is approximately 303,102.35 square meters. It's a quantitative measure of how much surface area these buildings occupy.

    Zoom in for seeing detail polygon enter image description here


    Update Component Two different color building drawing

    the calculateAndDisplayArea function checks the building tag of each feature and applies a blue color for "office" buildings and a red color for "industrial" buildings.

    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 MapComponent = () => {
      const [buildingData, setBuildingData] = useState(null);
      const [area, setArea] = useState(0);
      const mapRef = useRef(null);
    
      // Initialize the map
      useEffect(() => {
        if (!mapRef.current) {
          mapRef.current = L.map('map').setView([59.132659900251944, 9.727169813491393], 13); // Centered on Paris
          L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
            attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
          }).addTo(mapRef.current);
        }
      }, []);
    
      // Fetch building data
      useEffect(() => {
        const fetchData = async () => {
          try {
    
            const query = `[out:json];
              (
                way["building"="office"](around:3000,59.132659900251944, 9.727169813491393);
                way["building"="industrial"](around:3000,59.132659900251944, 9.727169813491393);
              );
              out body; >; out skel qt;`;
    
            const response = await axios.post(
              'https://overpass-api.de/api/interpreter', query, {
                headers: { 'Content-Type': 'text/plain' }
              }
            );
      
            setBuildingData(response.data);
    
          } catch (error) {
            console.error('Error fetching data:', error);
          }
        };
    
        fetchData();
      }, []);
    
      const calculateAndDisplayArea = useCallback(() => {
        if (buildingData && mapRef.current) {
          let totalArea = 0;
          const nodeMapping = {}; // Assuming you have this mapping from node ID to { lat, lon }
      
          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];
              });
      
              if (coordinates.length > 0) {
                // Check building type and apply color
                const buildingType = feature.tags?.building;
                const color = buildingType === 'office' ? 'blue' : buildingType === 'industrial' ? 'red' : 'grey';
    
                L.polygon(coordinates, { color }).addTo(mapRef.current); // Apply color to polygon
    
      
                const geoJsonPolygon = {
                  type: 'Polygon',
                  coordinates: [coordinates.map(coord => [coord[1], coord[0]])], // Convert to [lon, lat]
                };
      
                totalArea += turf.area(geoJsonPolygon);
              }
            }
          });
      
          setArea(totalArea);
        }
      }, [buildingData]);
      
    
      useEffect(() => {
        calculateAndDisplayArea();
      }, [calculateAndDisplayArea, buildingData]);
    
      return (
        <div>
          <p>Area: {area.toFixed(2)} square meters</p>
          <div id="map" style={{ height: '800px', width: '1000px', margin: '0 auto'}}></div>
        </div>
      );
    };
    
    export default MapComponent;
    

    Result

    enter image description here