Reputation: 53
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
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
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
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.
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);
})
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);
})
// 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