Search code examples
javascriptreactjsopenlayersopenlayers-6

How to refresh/re-render an OpenLayers 6 Map


I have a React component where I render an Open Layers 6 map. I pass in lat/long coordinates as a prop (props.userEnteredLocation) to the map which get translated to a format that open layers expects, to be able to render the correct location.

My State is as follows:

const transformCentre = (centre) => {
        return transform(centre, "EPSG:4326", "EPSG:3857");
    };

    const [map, setMap] = useState();
    const [centre, setCentre] = useState([74.31071359697442, 31.588167443954312]);
    const [featuresLayer, setFeaturesLayer] = useState();
    const [point, setPoint] = useState();
    const [feature, setFeature] = useState();
    const [transformedCentre, setTransformedCentre] = useState(
        transformCentre(centre)
    );

    // create state ref that can be accessed in OpenLayers onclick callback function
    //  https://stackoverflow.com/a/60643670
    const mapRef = useRef();
    mapRef.current = map;

I have two React hooks

  1. The first of which renders the initial map with a red marker and sets the state.
// initialize map on first render - logic formerly put into componentDidMount
    useEffect(() => {
        console.log("Initialised for the first time");

        // Create a point
        var point = new Point(transformedCentre);
        setPoint(point);

        // points/lines/polygons are features that can be used in the vector layer
        var feature = new Feature({
            geometry: point,
            name: "Your address is shown here",
        });

        var iconStyle = new Style({
            image: new Icon({
                src: markerImg,
            }),
        });

        feature.setStyle(iconStyle);
        setFeature(feature);

        // create vector source itself for the marker on the map
        var vectorSource = new VectorSource({
            features: [feature],
        });

        // create and add vector source layer
        const initalFeaturesLayer = new VectorLayer({
            source: vectorSource,
        });

        // create map
        const locationMap = new Map({
            target: mapRef.current,
            layers: [
                new TileLayer({
                    source: new OSM(),
                }),

                initalFeaturesLayer,
            ],
            view: new View({
                center: transformedCentre,
                zoom: 17,
            }),
            controls: [],
        });
        setMap(locationMap);
        setFeaturesLayer(initalFeaturesLayer);
    }, []);

This works, the map is correctly rendered at the right location Correctly Rendered Map

  1. The second hook should re-render the map whenever the prop representing lat/long coordinates changes, I can see in the console that the state does indeed change whenever new coordinates are passed into the props, but the map itself doesn't seem to update
// update map if user changes geo location
    useEffect(() => {
        console.log("Detected change in User geolocation");
        if (props.userEnteredLocation) {
            var array = props.userEnteredLocation.split(",");
            var newCentre = [parseFloat(array[1]), parseFloat(array[0])];
            setCentre(newCentre);
            setTransformedCentre(transformCentre(centre));
        }

        if (map != null) {
            console.log("Changing Centre to: " + transformedCentre);
            map.getView().setCenter(transformedCentre);
            console.log("CENTRE NOW: " + map.getView().getCenter());
            point.setCoordinates(transformedCentre);
            console.log("POINT CENTRE NOW: " + point.getCoordinates());
            feature.setGeometry(point);
            console.log("Feature geometry now CENTRE NOW: " + feature.getGeometry());
            // set features to map
            featuresLayer.setSource(
                new VectorSource({
                    features: [feature],
                })
            );
            featuresLayer.getSource().changed();
        }
    }, [props.userEnteredLocation]);

//render component
return <div ref={mapRef} className="map-container"></div>;

I've tried map.render() and map.renderSync() as suggested by the FAQ to no avail. I've also tried .changed() and .refresh() on the VectorSource itself, .changed() does nothing and .refresh() removes my red marker completely.

Does anyone know where I'm going wrong here? I'd like for the map to re-render and show the new location coming from the props and show the red map marker in that location.

I'm tempted to just create a new map, i.e. paste everything I do on initial render into the hook that changes on props change, but that seems sub optimal.


Solution

  • For anyone whom this might help, I realised the issue was actually just that useEffect() wasn't using the updated state until the next render of the component. In order for it to use the latest co-ordinates, I assigned the required centre of the map to a var before passing it to the Openlayers view and layout.

    Here's the code that works:

    function BusinessLocationMap(props) {
        // Transform the centre into something openlayers understands
        const transformCentre = (centre) => {
            if (centre != null) {
                return transform(centre, "EPSG:4326", "EPSG:3857");
            }
        };
    
        const [map, setMap] = useState();
        const [centre] = useState([74.31071359697442, 31.588167443954312]);
        const [featuresLayer, setFeaturesLayer] = useState();
        const [point, setPoint] = useState();
        const [feature, setFeature] = useState();
        const [transformedCentre] = useState(transformCentre(centre));
    
        // create state ref that can be accessed in OpenLayers onclick callback function
        //  https://stackoverflow.com/a/60643670
        const mapRef = useRef();
        mapRef.current = map;
    
        // initialize map on first render - logic formerly put into componentDidMount
        useEffect(() => {
            console.log("Initialised for the first time");
            console.log("Initial Centre used: " + transformedCentre);
    
            // Create a point
            var point = new Point(transformedCentre);
            setPoint(point);
    
            // points/lines/polygons are features that can be used in the vector layer
            var feature = new Feature({
                geometry: point,
                name: "Your address is shown here",
            });
    
            var iconStyle = new Style({
                image: new Icon({
                    src: markerImg,
                }),
            });
    
            feature.setStyle(iconStyle);
            setFeature(feature);
    
            // create vector source itself for the marker on the map
            var vectorSource = new VectorSource({
                features: [feature],
            });
    
            // create and add vector source layer
            const initalFeaturesLayer = new VectorLayer({
                source: vectorSource,
            });
    
            // create the initial view
            const initialView = new View({
                center: transformedCentre,
                zoom: 17,
            });
    
            // create map
            const locationMap = new Map({
                target: mapRef.current,
                layers: [
                    new TileLayer({
                        source: new OSM(),
                    }),
    
                    initalFeaturesLayer,
                ],
                view: initialView,
                controls: [],
            });
            setMap(locationMap);
            setFeaturesLayer(initalFeaturesLayer);
        }, []);
    
        // update map if user changes geo location
        useEffect(() => {
            console.log("Detected change in User geolocation");
    
            if (props.userEnteredLocation) {
                var array = props.userEnteredLocation.split(",");
                var newCentre = [parseFloat(array[1]), parseFloat(array[0])];
                var newTransformedCentre = transformCentre(newCentre);
            }
    
            if (map != null) {
                point.setCoordinates(newTransformedCentre);
                feature.setGeometry(point);
    
                map.setView(
                    new View({
                        center: newTransformedCentre,
                        zoom: 17,
                    })
                );
    
                // set features to map
                featuresLayer.setSource(
                    new VectorSource({
                        features: [feature],
                    })
                );
            }
        }, [props.userEnteredLocation]);
    
        //render component
        return <div ref={mapRef} className="map-container"></div>;
    }
    
    export default BusinessLocationMap;