Please help me to decide if having multiple google maps data layers is a good approach for the following case:
- There are different groups of vector objects on the map: each group has its own polygons, markers etc.
- The objects within a group would often have to be manipulated together, for example change the fill color.
- For some changes in group 1, group 2 would also have to be updated. For example, if group 1 switches to a certain fill color, group 2 will change to another fill color.
So, I want to be able to change each group (layer) individually. But at the same time, there are some dependencies between the state of the layers.
I will not use the google maps react components, so I will use the native Google Maps JS API in a react/typescript project.
Here is the code sandbox, please use your google maps api key to enable the demo.
Here is a gif video with the demo:
Here is part of the code in the sandbox:
<code>import React, { useEffect, useRef, useState } from "react";
import {
geoJsonLayer1,
geoJsonLayer2,
geoJsonLayer2FeatureToAdd,
layer1Style,
layer2Style,
} from "./constants";
interface GoogleMapProps {
apiKey: string;
}
const GoogleMap: React.FC<GoogleMapProps> = ({ apiKey }) => {
// Boilerplate
const googleMapRef = useRef<HTMLDivElement>(null);
const [googleMap, setGoogleMap] = useState<google.maps.Map | undefined>();
// State for the 2 data layers
const dataLayerRef1 = useRef<google.maps.Data | null>(null);
const dataLayerRef2 = useRef<google.maps.Data | null>(null);
// State that will be updated by the first layer and the second layer will
// change its properties (shared cross-cutting logic)
const [fillColorState, setFillColorState] = useState("");
// Boilerplate
useEffect(() => {
const googleMapScript = document.createElement("script");
googleMapScript.src = `https://maps.googleapis.com/maps/api/js?key=${apiKey}&libraries=places`;
googleMapScript.async = true;
window.document.body.appendChild(googleMapScript);
googleMapScript.onload = () => {
initializeGoogleMap();
};
}, [apiKey]);
useEffect(() => {
if (googleMap) {
// Instantiate the data layer objects
dataLayerRef1.current = new google.maps.Data();
dataLayerRef2.current = new google.maps.Data();
// Configure the objects, add styles, data etc.
dataLayerRef1.current.setStyle(layer1Style);
dataLayerRef2.current.setStyle(layer2Style);
dataLayerRef1.current.addGeoJson(geoJsonLayer1);
dataLayerRef2.current.addGeoJson(geoJsonLayer2);
// Attach the data layers to the map
dataLayerRef1.current.setMap(googleMap);
dataLayerRef2.current.setMap(googleMap);
}
}, [googleMap]);
// Effect for the second layer to track if the shared state changed.
useEffect(() => {
if (fillColorState === "purple") {
setTimeout(() => {
dataLayerRef2.current?.setStyle({
...dataLayerRef2.current.getStyle(),
fillColor: "yellow",
});
}, 2000);
}
}, [fillColorState]);
// Boilerplate
const initializeGoogleMap = () => {
if (googleMapRef.current) {
let googleMapApi = new window.google.maps.Map(googleMapRef.current, {
zoom: 12,
center: { lat: 48.8566, lng: 2.3522 },
});
setGoogleMap(googleMapApi);
}
};
// We change the data layer 1 without having to filter features by id etc.
// that would be needed if we would only have the default data layer.
const changeLayer1 = () => {
dataLayerRef1.current?.setStyle({
...dataLayerRef1.current.getStyle(),
fillColor: "pink",
});
};
// We change the data layer 2 without having to filter features by id etc.
// that would be needed if we would only have the default data layer.
const changeLayer2 = () => {
dataLayerRef2.current?.addGeoJson(geoJsonLayer2FeatureToAdd);
};
// We change data layer 1 but this change should also affect data layer 2.
// So we have a shared state (cross-cutting concerns)
const changeLayer1UpdateLayer2 = () => {
dataLayerRef1.current?.setStyle({
...dataLayerRef1.current.getStyle(),
fillColor: "purple",
});
setFillColorState("purple");
};
return (
<>
<button onClick={changeLayer1}>change layer 1</button>
<button onClick={changeLayer2}>change layer 2</button>
<button onClick={changeLayer1UpdateLayer2}>
change layer 1 that triggers change in layer 2
</button>
<div ref={googleMapRef} style={{ width: "800px", height: "500px" }} />
</>
);
};
export default GoogleMap;
</code>
<code>import React, { useEffect, useRef, useState } from "react";
import {
geoJsonLayer1,
geoJsonLayer2,
geoJsonLayer2FeatureToAdd,
layer1Style,
layer2Style,
} from "./constants";
interface GoogleMapProps {
apiKey: string;
}
const GoogleMap: React.FC<GoogleMapProps> = ({ apiKey }) => {
// Boilerplate
const googleMapRef = useRef<HTMLDivElement>(null);
const [googleMap, setGoogleMap] = useState<google.maps.Map | undefined>();
// State for the 2 data layers
const dataLayerRef1 = useRef<google.maps.Data | null>(null);
const dataLayerRef2 = useRef<google.maps.Data | null>(null);
// State that will be updated by the first layer and the second layer will
// change its properties (shared cross-cutting logic)
const [fillColorState, setFillColorState] = useState("");
// Boilerplate
useEffect(() => {
const googleMapScript = document.createElement("script");
googleMapScript.src = `https://maps.googleapis.com/maps/api/js?key=${apiKey}&libraries=places`;
googleMapScript.async = true;
window.document.body.appendChild(googleMapScript);
googleMapScript.onload = () => {
initializeGoogleMap();
};
}, [apiKey]);
useEffect(() => {
if (googleMap) {
// Instantiate the data layer objects
dataLayerRef1.current = new google.maps.Data();
dataLayerRef2.current = new google.maps.Data();
// Configure the objects, add styles, data etc.
dataLayerRef1.current.setStyle(layer1Style);
dataLayerRef2.current.setStyle(layer2Style);
dataLayerRef1.current.addGeoJson(geoJsonLayer1);
dataLayerRef2.current.addGeoJson(geoJsonLayer2);
// Attach the data layers to the map
dataLayerRef1.current.setMap(googleMap);
dataLayerRef2.current.setMap(googleMap);
}
}, [googleMap]);
// Effect for the second layer to track if the shared state changed.
useEffect(() => {
if (fillColorState === "purple") {
setTimeout(() => {
dataLayerRef2.current?.setStyle({
...dataLayerRef2.current.getStyle(),
fillColor: "yellow",
});
}, 2000);
}
}, [fillColorState]);
// Boilerplate
const initializeGoogleMap = () => {
if (googleMapRef.current) {
let googleMapApi = new window.google.maps.Map(googleMapRef.current, {
zoom: 12,
center: { lat: 48.8566, lng: 2.3522 },
});
setGoogleMap(googleMapApi);
}
};
// We change the data layer 1 without having to filter features by id etc.
// that would be needed if we would only have the default data layer.
const changeLayer1 = () => {
dataLayerRef1.current?.setStyle({
...dataLayerRef1.current.getStyle(),
fillColor: "pink",
});
};
// We change the data layer 2 without having to filter features by id etc.
// that would be needed if we would only have the default data layer.
const changeLayer2 = () => {
dataLayerRef2.current?.addGeoJson(geoJsonLayer2FeatureToAdd);
};
// We change data layer 1 but this change should also affect data layer 2.
// So we have a shared state (cross-cutting concerns)
const changeLayer1UpdateLayer2 = () => {
dataLayerRef1.current?.setStyle({
...dataLayerRef1.current.getStyle(),
fillColor: "purple",
});
setFillColorState("purple");
};
return (
<>
<button onClick={changeLayer1}>change layer 1</button>
<button onClick={changeLayer2}>change layer 2</button>
<button onClick={changeLayer1UpdateLayer2}>
change layer 1 that triggers change in layer 2
</button>
<div ref={googleMapRef} style={{ width: "800px", height: "500px" }} />
</>
);
};
export default GoogleMap;
</code>
import React, { useEffect, useRef, useState } from "react";
import {
geoJsonLayer1,
geoJsonLayer2,
geoJsonLayer2FeatureToAdd,
layer1Style,
layer2Style,
} from "./constants";
interface GoogleMapProps {
apiKey: string;
}
const GoogleMap: React.FC<GoogleMapProps> = ({ apiKey }) => {
// Boilerplate
const googleMapRef = useRef<HTMLDivElement>(null);
const [googleMap, setGoogleMap] = useState<google.maps.Map | undefined>();
// State for the 2 data layers
const dataLayerRef1 = useRef<google.maps.Data | null>(null);
const dataLayerRef2 = useRef<google.maps.Data | null>(null);
// State that will be updated by the first layer and the second layer will
// change its properties (shared cross-cutting logic)
const [fillColorState, setFillColorState] = useState("");
// Boilerplate
useEffect(() => {
const googleMapScript = document.createElement("script");
googleMapScript.src = `https://maps.googleapis.com/maps/api/js?key=${apiKey}&libraries=places`;
googleMapScript.async = true;
window.document.body.appendChild(googleMapScript);
googleMapScript.onload = () => {
initializeGoogleMap();
};
}, [apiKey]);
useEffect(() => {
if (googleMap) {
// Instantiate the data layer objects
dataLayerRef1.current = new google.maps.Data();
dataLayerRef2.current = new google.maps.Data();
// Configure the objects, add styles, data etc.
dataLayerRef1.current.setStyle(layer1Style);
dataLayerRef2.current.setStyle(layer2Style);
dataLayerRef1.current.addGeoJson(geoJsonLayer1);
dataLayerRef2.current.addGeoJson(geoJsonLayer2);
// Attach the data layers to the map
dataLayerRef1.current.setMap(googleMap);
dataLayerRef2.current.setMap(googleMap);
}
}, [googleMap]);
// Effect for the second layer to track if the shared state changed.
useEffect(() => {
if (fillColorState === "purple") {
setTimeout(() => {
dataLayerRef2.current?.setStyle({
...dataLayerRef2.current.getStyle(),
fillColor: "yellow",
});
}, 2000);
}
}, [fillColorState]);
// Boilerplate
const initializeGoogleMap = () => {
if (googleMapRef.current) {
let googleMapApi = new window.google.maps.Map(googleMapRef.current, {
zoom: 12,
center: { lat: 48.8566, lng: 2.3522 },
});
setGoogleMap(googleMapApi);
}
};
// We change the data layer 1 without having to filter features by id etc.
// that would be needed if we would only have the default data layer.
const changeLayer1 = () => {
dataLayerRef1.current?.setStyle({
...dataLayerRef1.current.getStyle(),
fillColor: "pink",
});
};
// We change the data layer 2 without having to filter features by id etc.
// that would be needed if we would only have the default data layer.
const changeLayer2 = () => {
dataLayerRef2.current?.addGeoJson(geoJsonLayer2FeatureToAdd);
};
// We change data layer 1 but this change should also affect data layer 2.
// So we have a shared state (cross-cutting concerns)
const changeLayer1UpdateLayer2 = () => {
dataLayerRef1.current?.setStyle({
...dataLayerRef1.current.getStyle(),
fillColor: "purple",
});
setFillColorState("purple");
};
return (
<>
<button onClick={changeLayer1}>change layer 1</button>
<button onClick={changeLayer2}>change layer 2</button>
<button onClick={changeLayer1UpdateLayer2}>
change layer 1 that triggers change in layer 2
</button>
<div ref={googleMapRef} style={{ width: "800px", height: "500px" }} />
</>
);
};
export default GoogleMap;
Best regards,
MadJoRR