Grantis
Grantis

Reputation: 53

How to access overlapping geojson/polygons to display in infobox

I'm using Google Maps API to load multiple polygons into the map using the geoJSON data layer. Some of these polygons overlap in certain regions. When a user clicks on a point that is inside of multiple polygons, I want to display the properties (name, tags, etc) in an InfoBox with the click event.

I'm wanting to display the properties of all the polygons for a given point. Currently when I click on a point I can only see one polygon, despite the point being inside of multiple polygons.

How can I access all the properties of all the polygons with Google Maps API v3?

const map = useGoogleMap(); // google map instance
const polygons; // an array of polygons, example snippet below. 

map.data.addGeoJson(polygons);

map.data.addListener('click', function(event) {
   // how can i access other features underneath this clicked point
   console.log(event.feature); // only returns "Geofence 1"
})

example GeoJson:

polygons = {
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "properties": {
      "name": "Geofence 1"
      },
      "geometry": {
        "type": "Polygon",
        "coordinates": [
          [
            [
              -8.96484375,
              -9.96885060854611
            ],
            [
              3.955078125,
              -9.96885060854611
            ],
            [
              3.955078125,
              -0.17578097424708533
            ],
            [
              -8.96484375,
              -0.17578097424708533
            ],
            [
              -8.96484375,
              -9.96885060854611
            ]
          ]
        ]
      }
    },
    {
      "type": "Feature",
      "properties": {
      "name": "Geofence 2"
      },
      "geometry": {
        "type": "Polygon",
        "coordinates": [
          [
            [
              -6.591796875,
              -8.320212289522944
            ],
            [
              2.197265625,
              -8.320212289522944
            ],
            [
              2.197265625,
              -1.9332268264771106
            ],
            [
              -6.591796875,
              -1.9332268264771106
            ],
            [
              -6.591796875,
              -8.320212289522944
            ]
          ]
        ]
      }
    },
    {
      "type": "Feature",
      "properties": {
      "name": "Geofence 3"
      },
      "geometry": {
        "type": "Polygon",
        "coordinates": [
          [
            [
              -4.39453125,
              -6.926426847059551
            ],
            [
              0.263671875,
              -6.926426847059551
            ],
            [
              0.263671875,
              -3.337953961416472
            ],
            [
              -4.39453125,
              -3.337953961416472
            ],
            [
              -4.39453125,
              -6.926426847059551
            ]
          ]
        ]
      }
    }
  ]
}

Upvotes: 3

Views: 1171

Answers (3)

Noah Fraiture
Noah Fraiture

Reputation: 33

If anyone would like to still use the information from a kmz/kml file, here's an example to parse it :

    async function fetchAndParseKMZ(url) {
      const response = await fetch(url);
      const arrayBuffer = await response.arrayBuffer();
      const zip = await JSZip.loadAsync(arrayBuffer);
      const kmlFile = Object.keys(zip.files).find((fileName) =>
        fileName.endsWith(".kml"),
      );
    
      if (!kmlFile) {
        throw new Error("No KML file found in the KMZ archive.");
      }
    
      const kmlContent = await zip.files[kmlFile].async("text");
      return parseKML(kmlContent);
    }
    
    function parseKML(kmlContent) {
      const parser = new DOMParser();
      const xmlDoc = parser.parseFromString(kmlContent, "text/xml");
      const placemarks = xmlDoc.getElementsByTagName("Placemark");
      const polygonsCoord = [];
      const polygonsNames = [];
    
      for (let placemark of placemarks) {
        const polygon = placemark.getElementsByTagName("Polygon")[0];
        if (polygon) {
          const coordinates = polygon
            .getElementsByTagName("coordinates")[0]
            .textContent.trim();
          const coordsArray = coordinates.split(/\s+/).map((coord) => {
            const [lng, lat] = coord.split(",").map(Number);
            return { lat, lng };
          });
          polygonsCoord.push(coordsArray);
    
          const name =
            placemark.getElementsByTagName("name")[0]?.textContent || "Unnamed";
          polygonsNames.push(name);
        }
      }
    
      return { polygonsCoord, polygonsNames };
    }

You can then easily use them as mentionned above :

    const { polygonsCoord, polygonsNames } = await fetchAndParseKMZ(src);
    const polygonsGoogle = [];
    
    polygonsCoord.forEach((polygonCoords, index) => {
      const polygon = new google.maps.Polygon({
        paths: polygonCoords,
        clickable: false,
        map: map,
      });
      polygon.name = polygonsNames[index]; // Add name to the polygon
      polygonsGoogle.push(polygon);
    });

Upvotes: 0

Grantis
Grantis

Reputation: 53

I was able to get this POC working with React and am sharing for next person who might be interested:

import { InfoWindow, useGoogleMap } from '@react-google-maps/api';

export const GeofencesContainer = () => {
  const map = useGoogleMap();
  // geofence = geoJSON feature collections
  const [geofenceClickEventLatLng, setGeofenceClickEventLatLng] = useState({});
  const [geofencesProperties, setGeofencesProperties] = useState([]);
  const [isInfoWindowOpen, setIsInfoWindowOpen] = useState(false);
  const [polygonArray, setPolygonArray] = useState([]);
  const ref = useRef();

  useEffect(() => {
    let addFeatureListener;
    const getGeofences = async () => {
      if (true) {
        if (ref.current !== 'geofenceEnabled') {
          // get geofences from api
          const geofences = await fetch.getGeofences(); 

          // a listener to add feature, where we create a google.maps.polygon for every added feature, then add it to the polygon array
          const addedPolygons = [];
          addFeatureListener = map.data.addListener('addfeature', e => {
            const featureGeometry = e.feature.getGeometry().getArray();

            // for each geometry get the latLng array, which will be used to form a polygon
            featureGeometry.forEach(latLngArray => {
              const polygon = new window.google.maps.Polygon({
                map,
                paths: latLngArray.getArray(),
                strokeWeight: 1,
                fillColor: 'green',
                clickable: false,
                name: e.feature.getProperty('name')
              });
              addedPolygons.push(polygon);
            });
            setPolygonArray(addedPolygons);
          });

          // add the polygon to the map data layer (this will show the polygon on the map and cause the addfeature listener to fire)
          map.data.addGeoJson(geofences);
          map.data.setStyle({ fillColor: 'green', strokeWeight: 1 });
          // we set map data to null so that we don't end up with 2 polygons on top of each other
          map.data.setMap(null);
          ref.current = 'geofenceEnabled';
        }
      } else {
        ref.current = null;
        for (const polygon of polygonArray) {
          polygon.setMap(null);
        }
        setIsInfoWindowOpen(false);
      }
    };
    getGeofences();

    return () => {
      if (addFeatureListener) {
        addFeatureListener.remove();
      }
    };
  }, [activeKmls]);

  useEffect(() => {

    let clickListener;
    if (true) {
      // a listener on click that checks whether the point is in a polygon and updates the necessary state to show the proper info window
      clickListener = map.addListener('click', function(event) {
        // this state is updated to identify the place where the info window will open
        setGeofenceClickEventLatLng(event.latLng);

        // for every polygon in the created polygons array check if it includes the clicked point, then update what to display in the info window
        const selectedGeofences = [];
        for (const polygon of polygonArray) {
          if (window.google.maps.geometry.poly.containsLocation(event.latLng, polygon)) {
            const name = polygon.name;
        
            selectedGeofences.push({ key: name, name });
          }
        }
        if (selectedGeofences.length) {
          setIsInfoWindowOpen(true);
        } else {
          setIsInfoWindowOpen(false);
        }
        setGeofencesProperties(selectedGeofences);
      });
    }

    return () => {
      if (clickListener) {
        clickListener.remove();
      }
    };
  }, [activeKmls, polygonArray, altitudeUnit, map]);

  return (
    <Fragment>
      {isInfoWindowOpen && (
        <InfoWindow
          position={geofenceClickEventLatLng}
          onCloseClick={() => setIsInfoWindowOpen(false)}
          zIndex={0}
          options={{ maxWidth: '450' }}
        >
          <Fragment>
            {geofencesProperties.map(geofenceProperties => {
              return (
                <Fragment key={geofenceProperties.key}>
                  <Title level={4}>{geofenceProperties.name}</Title>
                </Fragment>
              );
            })}
          </Fragment>
        </InfoWindow>
      )}
    </Fragment>
  );
};
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>

Upvotes: 0

geocodezip
geocodezip

Reputation: 161404

One option would be to use the containsLocation method in the geometry library.

containsLocation(point, polygon) Parameters:
point: LatLng
polygon: Polygon
Return Value: boolean
Computes whether the given point lies inside the specified polygon.

Unfortunately that only works with native google.maps.Polygon objects not Data.Polygon objects. Translate the data in the feature into native google.maps.Polygon objects, push them on an array, then process through the array to see which polygon(s) the click is in.

  1. create google.maps.Polygon for each polygon in the input (assumes only polygons)
  var polygonArray = [];
  map.data.addListener('addfeature', function(e) {
    e.feature.getGeometry().getArray().forEach(function(latLngArry){
      const polygon = new google.maps.Polygon({
        map: map,
        paths: latLngArry.getArray(),
        clickable: false,
        name: e.feature.getProperty("name") // save the data we want to output as an attribute
      })
      polygonArray.push(polygon);
    })
  1. on click check for which polygon(s) the click was in:
  map.addListener('click', function(event) {
    var content = "";
    for (var i=0;i<polygonArray.length;i++) {
       if (google.maps.geometry.poly.containsLocation(event.latLng, polygonArray[i])) {
          if (content.length!=0) 
            content+=" : "
          content += polygonArray[i].name;
       }
    }
    console.log(content);
  })

proof of concept fiddle

screenshot of resulting map

// This example uses the Google Maps JavaScript API's Data layer
// to create a rectangular polygon with 2 holes in it.
function initMap() {
  const map = new google.maps.Map(document.getElementById("map"));
  const infowindow = new google.maps.InfoWindow();
  var bounds = new google.maps.LatLngBounds();
  var polygonArray = [];
  map.data.addListener('addfeature', function(e) {
    console.log(e.feature.getGeometry().getArray().length);
    e.feature.getGeometry().getArray().forEach(function(latLngArry) {
      console.log(latLngArry.getArray())
      const polygon = new google.maps.Polygon({
        map: map,
        paths: latLngArry.getArray(),
        clickable: false,
        name: e.feature.getProperty("name")
      })
      polygonArray.push(polygon);
    })
    processPoints(e.feature.getGeometry(), bounds.extend, bounds);
    map.fitBounds(bounds);
  });
  const features = map.data.addGeoJson(polygons);
  map.data.setMap(null);
  map.addListener('click', function(event) {
    var content = "";
    for (var i = 0; i < polygonArray.length; i++) {
      if (google.maps.geometry.poly.containsLocation(event.latLng, polygonArray[i])) {
        if (content.length != 0)
          content += " : "
        content += polygonArray[i].name;
      }
    }
    console.log(content);
    document.getElementById('info').innerHTML = content;
    infowindow.setPosition(event.latLng);
    if (content.length == 0) content = "no GeoFence";
    infowindow.setContent(content);
    infowindow.open(map);
  })

  function processPoints(geometry, callback, thisArg) {
    if (geometry instanceof google.maps.LatLng) {
      callback.call(thisArg, geometry);
    } else if (geometry instanceof google.maps.Data.Point) {
      callback.call(thisArg, geometry.get());
    } else {
      geometry.getArray().forEach(function(g) {
        processPoints(g, callback, thisArg);
      });
    }
  }
}
const polygons = {
  "type": "FeatureCollection",
  "features": [{
      "type": "Feature",
      "properties": {
        "name": "Geofence 1"
      },
      "geometry": {
        "type": "Polygon",
        "coordinates": [
          [
            [-8.96484375, -9.96885060854611],
            [
              3.955078125, -9.96885060854611
            ],
            [
              3.955078125, -0.17578097424708533
            ],
            [-8.96484375, -0.17578097424708533],
            [-8.96484375, -9.96885060854611]
          ]
        ]
      }
    },
    {
      "type": "Feature",
      "properties": {
        "name": "Geofence 2"
      },
      "geometry": {
        "type": "Polygon",
        "coordinates": [
          [
            [-6.591796875, -8.320212289522944],
            [
              2.197265625, -8.320212289522944
            ],
            [
              2.197265625, -1.9332268264771106
            ],
            [-6.591796875, -1.9332268264771106],
            [-6.591796875, -8.320212289522944]
          ]
        ]
      }
    },
    {
      "type": "Feature",
      "properties": {
        "name": "Geofence 3"
      },
      "geometry": {
        "type": "Polygon",
        "coordinates": [
          [
            [-4.39453125, -6.926426847059551],
            [
              0.263671875, -6.926426847059551
            ],
            [
              0.263671875, -3.337953961416472
            ],
            [-4.39453125, -3.337953961416472],
            [-4.39453125, -6.926426847059551]
          ]
        ]
      }
    }
  ]
}
/* Always set the map height explicitly to define the size of the div
       * element that contains the map. */

#map {
  height: 90%;
}


/* Optional: Makes the sample page fill the window. */

html,
body {
  height: 100%;
  margin: 0;
  padding: 0;
}
<!DOCTYPE html>
<html>

<head>
  <title>Data Layer: Polygon</title>
  <script src="https://polyfill.io/v3/polyfill.min.js?features=default"></script>
  <script src="https://maps.googleapis.com/maps/api/js?key=AIzaSyCkUOdZ5y7hMm0yrcCQoCvLwzdM6M8s5qk&callback=initMap&libraries=&v=weekly" defer></script>
  <!-- jsFiddle will insert css and js -->
</head>

<body>
  <div id="info"></div>
  <div id="map"></div>
</body>

</html>

Upvotes: 3

Related Questions