Skip to content

[Bug] Hover on SolidPolygon as sub-layer not triggering hovering effect on the whole polygon. #10005

@Abdul-Aziz-Fahim

Description

@Abdul-Aziz-Fahim

Description

Here is my code

export function createHighZoomVesselLayer(
  setHoverInfo,
  mapInstanceRef,
  zoom,
  cacheBuster = "",
  setSelectedVessel,
  markerClickedRef,
) {
  const dataUrl = `${API_ENDPOINTS.TILES}${
    cacheBuster ? `?t=${cacheBuster}` : ""
  }`;
  const z = Math.round(zoom);
  return new MVTLayer({
    id: "vessels-poly",
    data: dataUrl,
    binary: false,
    pickable: true,
    maxCacheSize: 0, // force no cache in memory for high zoom level
    onHover: (info) => onHoverShip(info, setHoverInfo, mapInstanceRef),
    onClick: (info) =>
      onClickShip(info, setSelectedVessel, setHoverInfo, z, markerClickedRef),
    renderSubLayers: (subProps) => {
      const { data, id } = subProps;

      if (!data) return null;

      const hullData = data.filter((f) => {
        const p = f.properties || {};
        return !isCircleShape(p);
      });

      // Create the hull fill layer
      const hullFill = new SolidPolygonLayer({
        id: `${id}-hull`,
        data: hullData,
        pickable: true,
        onHover: (info) => onHoverShip(info, setHoverInfo, mapInstanceRef),
        onClick: (info) =>
          onClickShip(
            info,
            setSelectedVessel,
            setHoverInfo,
            z,
            markerClickedRef,
          ),
        autoHighlight: false,
        coordinateSystem: COORDINATE_SYSTEM.LNGLAT,
        getPolygon: (f) => {
          const p = f.properties || {};
          const lon = Number(p.lon);
          const lat = Number(p.lat);
          if (!Number.isFinite(lon) || !Number.isFinite(lat)) return null;

          const headingDeg = getRotation({ properties: p, calibrate: false });

          return makeShipPolygonLngLat({
            lon,
            lat,
            toBow: Number(p.distancetobow || 0),
            toStern: Number(p.distancetostern || 0),
            toPort: Number(p.distancetoport || 0),
            toStarboard: Number(p.distancetostarboard || 0),
            headingDeg,
            styleHoleMeters: 2.2,
            zoom: z,
          });
        },
        filled: true,
        getFillColor: (f) => {
          const s = Number(f.properties?.speed || 0);
          const hex = getVesselColor(s);
          const r = parseInt(hex.slice(1, 3), 16);
          const g = parseInt(hex.slice(3, 5), 16);
          const b = parseInt(hex.slice(5, 7), 16);
          return [r, g, b, 200];
        },
        stroked: false,
        lineWidthMinPixels: 1,
        lineJointRounded: true,
        lineCapRounded: true,
        lineWidthUnits: "pixels",
        parameters: { depthTest: false },
      });

      // Combine all layers and return
      // Transponder layer is placed last to ensure it's rendered on top
      return [hullFill];
    },
    updateTriggers: {
      renderSubLayers: z,
    },
  });
}

function makeShipPolygonLngLat(props) {
  const {
    lon,
    lat,
    toBow,
    toStern,
    toPort,
    toStarboard,
    headingDeg,
    zoom = 14,
  } = props;

  const bow = toBow;
  const stern = toStern;
  const port = toPort;
  const stbd = toStarboard;

  const norm = normalizeOffsetsForDisplay({
    toBow: bow,
    toStern: stern,
    toPort: port,
    toStarboard: stbd,
    minLen: MIN_SHIP_LENGTH_M,
    minBeam: MIN_SHIP_BEAM_M,
    zoom,
  });

  const outerLocal = shipOuterFromOffsets({
    toBow: norm.bow,
    toStern: norm.stern,
    toPort: norm.port,
    toStarboard: norm.starBoard,
  });

  const outer = outerLocal.map(([x, y]) => {
    const [xr, yr] = rotateXYClockwise(x, y, headingDeg, 0, 0);
    const [dLon, dLat] = metersToLngLat(lat, xr, yr);
    return [lon + dLon, lat + dLat];
  });

  return outer;
}


function normalizeOffsetsForDisplay({
  toBow,
  toStern,
  toPort,
  toStarboard,
  minLen = MIN_SHIP_LENGTH_M,
  minBeam = MIN_SHIP_BEAM_M,
}) {
  const Tbow = Math.max(0, Number(toBow ?? 0));
  const Tstern = Math.max(0, Number(toStern ?? 0));
  const Tport = Math.max(0, Number(toPort ?? 0));
  const TstarBoard = Math.max(0, Number(toStarboard ?? 0));

  let { bow, stern, port, starBoard } = validateDimension({
    Tbow,
    Tstern,
    Tport,
    TstarBoard,
  });

  const len0 = bow + stern;
  if (len0 < minLen) {
    const add = (minLen - len0) / 2;
    bow += add;
    stern += add;
  }

  const beam0 = port + starBoard;
  if (beam0 < minBeam) {
    const add = (minBeam - beam0) / 2;
    port += add;
    starBoard += add;
  }

  return {
    bow,
    stern,
    port,
    starBoard,
  };
}


function validateDimension({ Tbow, Tstern, Tport, TstarBoard }) {
  const invalid = [Tbow, Tstern, Tport, TstarBoard].some(
    (v) => v === INVALID_DATA,
  );

  let bow = Tbow;
  let stern = Tstern;
  let port = Tport;
  let starBoard = TstarBoard;

  const length = Tbow + Tstern;
  const beam = Tport + TstarBoard;

  if (invalid || length === 0 || length > MAX_SHIP_LENGTH_M) {
    bow = DIMENSIONS_DEFAULTS.bow;
    stern = DIMENSIONS_DEFAULTS.stern;
  }

  if (invalid || beam === 0 || beam > MAX_SHIP_BEAM_M) {
    port = DIMENSIONS_DEFAULTS.port;
    starBoard = DIMENSIONS_DEFAULTS.starBoard;
  }

  return { bow, stern, port, starBoard };
}

function shipOuterFromOffsets({ toBow, toStern, toPort, toStarboard }) {
  const bow0 = Math.max(0, toBow);
  const stern0 = Math.max(0, toStern);
  const port0 = Math.max(0, toPort);
  const stbd0 = Math.max(0, toStarboard);

  const yBow = bow0;
  const yStern = -stern0;

  const xL = -port0;
  const xR = stbd0;

  const L = Math.max(1, yBow - yStern);
  const yShoulder = yStern + L * 0.8;

  const xCenter = (xL + xR) / 2;

  const tipTaper = 0.08;
  const halfWidth = (xR - xL) / 2;
  const tipHalfWidth = halfWidth * tipTaper;

  const xTipL = xCenter - tipHalfWidth;
  const xTipR = xCenter + tipHalfWidth;

  const yApex = yBow + L * 0.01;

  return [
    [xL, yStern],
    [xR, yStern],
    [xR, yShoulder],
    [xTipR, yBow],
    [xCenter, yApex],
    [xTipL, yBow],
    [xL, yShoulder],
    [xL, yStern],
  ];
}

export function rotateXYClockwise(x, y, headingDeg, cx = 0, cy = 0) {
  const t = -(headingDeg * Math.PI) / 180;

  // translate to origin
  const dx = x - cx;
  const dy = y - cy;

  // rotate
  const xr = dx * Math.cos(t) - dy * Math.sin(t);
  const yr = dx * Math.sin(t) + dy * Math.cos(t);

  // translate back
  return [xr + cx, yr + cy];
}


export function metersToLngLat(latDeg, dxMeters, dyMeters) {
  const lat = (latDeg * Math.PI) / 180;
  const metersPerDegLat = 111320;
  const metersPerDegLon = 111320 * Math.cos(lat);
  return [dxMeters / metersPerDegLon, dyMeters / metersPerDegLat];
}

The data is retrieved with MVTLayer and then shown using SolidPolygon layer. I use this function only on 14-18 zoom levels. The problem occurs in 17 and 18 zoom level. The marker then renders perfectly but the hover only works around the exact lat/lon position of the MVTLayer data. All of my data are in points. This issue only happens on 17 and 18 zoom levels and with vessels which have around 200 meters in length. Small vessel works just fine.

Flavors

  • Script tag
  • React
  • Python/Jupyter notebook
  • MapboxOverlay
  • GoogleMapsOverlay
  • CARTO
  • ArcGIS

Expected Behavior

The exacted behavior would be that, the hover method will run anywhere on the solidPolygon layer. Instead it only runs only around the MVTLayer point's exact lat/lon around area.

Another thing is that, this only happens on high length vessels. So, low length vessel marker has absolutely no problem at all and run the hover effect perfectly.

Steps to Reproduce

The re-producing is hard. The MVT data is generated from the database. Any suggestion regarding the code will be very helpful for me.

Environment

  • Framework version: 9.2.2
  • Browser: Version 144.0.7559.133 (Official Build) (64-bit)
  • OS: Windows 11

Logs

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions