简体   繁体   中英

How to manage OpenLayers Overlay lifecycle using React?

Given a react application that uses OpenLayers to render an interactive map, I want to provide tooltips for displayed features. The lifecycle should be as follows: there is either no or one tooltip present on the map. If the user clicks within a certain distance of a feature, a tooltip appears close to that feature's position on the map and if there was a previous tooltip, it's replaced with the new one. If a user clicks on a part of the map without a feature close by, the previous tooltip gets destroyed.

I have tried multiple ways of implementing such logic, but the final logical flaw that keeps me from succeeding is when React wants to unmount a tooltip component, it is not able to find it since OpenLayers moves it into its own ol-overlay-container container.

As suggested in an answer to a similar question , a possible solution could be to use react's createPortal to directly render the tooltip into the DOM location that openlayers would copy it into anyway. The problem with this seems to be that openlayers wraps the element nonetheless, making it impossible for react to remove it when unmounting.

So far, this is my code for a popup/tooltip component:


import { Overlay } from "ol"
import { createRef, useEffect, useRef, useState } from "react"
import { createPortal } from "react-dom"
import useMap from "../../Hook/useMap"

const Popup = ({ feature, position, ...props }) => {
  const { map } = useMap()  // custom map context
  const overlay = useRef()
  const [domReady, setDomReady] = useState(false)
  const overlayContainer = useRef()
  const overlayContent = createRef()

  useEffect(() => {
    setDomReady(true)
    overlayContainer.current = document.getElementsByClassName(
      "ol-overlay-container ol-selectable"
    )[0]
    return () => {
      setDomReady(false)
    }
  }, [])

  useEffect(() => {
    if (domReady && overlayContainer.current) {
      overlay.current = new Overlay({
        element: overlayContainer.current,
        id: "popup",
      })
      if (position) {
        overlay.current.setPosition(position)
      }
      map.addOverlay(overlay.current)
      return () => {
        map.removeOverlay(overlay.current)
      }
    }
  }, [domReady, feature, position])

  return domReady
    ? createPortal(
        <div className="test-overlay" ref={overlayContent}>
          {props.children}
        </div>,
        overlayContainer.current
      )
    : null
}

export default Popup

This requires that the overlay container with class name ol-overlay-container ol-selectable exists during the whole lifecycle of the application. By default, it seems it's only created once an overlay is added to the map. Therefore, I initialize my map with an empty overlay whose element is a div I have created in my index.html:


const map = new OlMap({
  overlays: [
    new Overlay({
      id: "preserveOverlayContainer",
      element: document.getElementById("map-overlay"),
      position: [0, 0],
    }),
  ],
  ...
})

Is there no way to have a tool tip with contents managed by react, yet rendered into the map using openlayers?

In the meantime, I have come up with a hacky, but seemingly working solution to this:

return () => {
        map.removeOverlay(overlay.current)
        const outer = document.createElement("div")
        outer.id = "map-overlay-outer"

        document.body.appendChild(outer)

        overlayContainer.current = document.getElementById("map-overlay-outer")
      }

in the cleanup function for the second useEffect , the removeOverlay() method call removes the previously rendered tooltip entirely from the DOM, so we simply recreate it.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM