renderFrame()

in src/lib/components/molecules/canvas-map/lib/renderers/TextLayerRenderer.js [38:189]


  renderFrame(frameState, canvas) {
    if (this.layer.opacity === 0) return null

    const { declutterTree } = frameState
    const { projection, viewPortSize, sizeInPixels, visibleExtent, transform } =
      frameState.viewState

    // set opacity
    this._element.style.opacity = `${this.layer.opacity}`

    const source = this.layer.source
    const features = source.getFeaturesInExtent(visibleExtent)

    /** @type {CanvasRenderingContext2D} */
    let canvasCtx

    const textElements = []

    for (const feature of features) {
      // get point geometry
      const geometries = feature.getProjectedGeometries(projection)
      const point = geometries.find((d) => d.type === "Point")
      if (!point) {
        throw new Error(
          `Expected Point geometry for feature in TextLayer: ${feature}`,
        )
      }

      const styleFunction =
        feature.getStyleFunction() || this.layer.getStyleFunction()

      const featureStyle = styleFunction(
        feature,
        transform.k,
        this._hoveredFeature === feature,
      )

      // get text element
      const textElement = this.getTextElementWithID(feature.uid)
      textElement.innerText = featureStyle.text.content

      const [canvasX, canvasY] = featureStyle.text.callout
        ? transform.apply(
            projection([
              featureStyle.text.callout.offsetTo.x,
              featureStyle.text.callout.offsetTo.y,
            ]),
          )
        : transform.apply(point.coordinates)

      const [relativeX, relativeY] = [
        canvasX / sizeInPixels[0],
        canvasY / sizeInPixels[1],
      ]

      const position = {
        left: `${relativeX * 100}%`,
        top: `${relativeY * 100}%`,
      }

      // Apply style to text element, and receive measured size from DOM
      const elementDimens = this.styleTextElement(
        textElement,
        featureStyle.text,
        position,
      )

      const bbox = this.getElementBBox(
        elementDimens,
        featureStyle.text,
        {
          x: relativeX * viewPortSize[0],
          y: relativeY * viewPortSize[1],
        },
        this.layer.declutterBoundingBoxPadding,
      )

      // skip item if it collides with existing elements
      if (declutterTree) {
        if (declutterTree.collides(bbox)) {
          continue
        }

        // add element to declutter tree to prevent collisions
        declutterTree.insert(bbox)
      }

      const callout = featureStyle?.text?.callout
      const icon = featureStyle?.text?.icon

      if (callout || icon) {
        canvasCtx ??= canvas.getContext("2d")
      }

      if (callout) {
        const [originalX, originalY] = transform.apply(point.coordinates)

        const offsetDiffX = canvasX - originalX
        const offsetDiffY = canvasY - originalY

        canvasCtx.beginPath()

        canvasCtx.moveTo(originalX, originalY)
        canvasCtx.lineTo(originalX + offsetDiffX / 2, originalY + offsetDiffY)
        canvasCtx.moveTo(originalX + offsetDiffX / 2, canvasY)
        canvasCtx.lineTo(canvasX, canvasY)

        canvasCtx.strokeStyle = callout.leaderColor
        canvasCtx.lineWidth = callout.leaderWidth
        canvasCtx.stroke()

        canvasCtx.closePath()
      }

      // TODO: should we draw icons with SVG? add them into the textlayer? it'd make a lot of the
      // maths easier!
      if (icon) {
        canvasCtx.beginPath()
        canvasCtx.save()

        let iconPosX = relativeX * viewPortSize[0]
        let iconPosY = relativeY * viewPortSize[1]

        if (icon.position === "right") {
          iconPosX += elementDimens.width
        } else if (icon.position === "left") {
          iconPosX += icon.padding + icon.size / 2
        }

        canvasCtx.translate(
          iconPosX * window.devicePixelRatio,
          iconPosY * window.devicePixelRatio,
        )

        this.drawTextIcon(canvasCtx, icon)

        canvasCtx.restore()
        canvasCtx.closePath()
      }

      if (this.layer.drawCollisionBoxes) {
        const collisionBoxDebugElement = this.getCollisionBoxElement(bbox)
        textElements.push(collisionBoxDebugElement)
      }

      textElements.push(textElement)
    }

    replaceChildren(this._element, textElements)

    return this._element
  }