Open alamenai opened 1 month ago
mapbox-gl-js version:
Hi everyone,
I spent two days trying to solve this issue but no way.
I did not open this until I could not find a solution on Google or using ChatGPT.
As you see in the image below, I have stack of layers from bottom to top:
Layer 01 : Roof ( Polygon )
Layer 02: Solar Panels (Polygon )
Layer 03: Roof ID (Cirlce)
As you see in the picture when I hover on the cirlce layer ( Layer 3 ) , it triggers also hover the panel ( Layer 2 ) while It should not happen.
Please could share an example on how to handle this situation?
"use client" import { getRoofSurfaces } from "@/app/actions/roof-surfaces" import { getEnv } from "@/env" import * as turf from "@turf/turf" import { Feature, GeoJsonProperties, Geometry, Position } from "geojson" import mapboxGL, { Map as MapBoxMap } from "mapbox-gl" import proj4 from "proj4" import React, { SyntheticEvent, useEffect, useRef, useState } from "react" import { Input } from "../ui/input" /* eslint-disable */ interface Panel { panelID: number selected: boolean } interface Roof { id: number coords: any holes: any panelCoords: any } interface SelectedPanelsState { [roofId: number]: Panel[] } const FROM_PROJECTION = "+proj=utm +zone=32 +datum=WGS84 +units=m +no_defs" // Define the source and destination projections const TO_PROJECTION = "EPSG:4326" // WGS84 Geographic // Please check the backend response to understand how this function works function convertCoordinates(wkt: any) { // Remove the POLYGON prefix and the outer parentheses, then split into rings const rings = wkt .replace(/POLYGON\s*\(\(/, "") .replace(/\)\)/, "") .split("), (") // Convert the rings into arrays of coordinates const convertedRings = rings.map((ring: string) => { // Map each ring to its coordinates let coordinates = ring.split(",").map((pair) => { let points = pair.trim().split(" ") return proj4(FROM_PROJECTION, TO_PROJECTION).forward([parseFloat(points[0]), parseFloat(points[1])]) }) // Ensure the ring is closed if ( coordinates.length > 1 && (coordinates[0][0] !== coordinates[coordinates.length - 1][0] || coordinates[0][1] !== coordinates[coordinates.length - 1][1]) ) { coordinates.push(coordinates[0]) } return coordinates }) // Structure the result as an object with 'outer' which is the roof and 'inners' which are the holes return { outer: convertedRings[0], // The first array is the outer boundary : Ex: Roof inners: convertedRings.slice(1), // All subsequent arrays are inner boundaries : Ex: Holes } } const BuildingMap = () => { const [address, setAddress] = useState("") const [selectedPanels, setSelectedPanels] = useState<SelectedPanelsState>({}) const mapRef = useRef<MapBoxMap | null>(null) const panelEventHandlers = useRef<{ [key: string]: any }>({}) const roofEventHandlers = useRef<{ [key: string]: any }>({}) const initMap = (token: string) => { if (mapRef.current) return mapRef.current = new mapboxGL.Map({ accessToken: token, container: "map-container", style: "mapbox://styles/mapbox/satellite-v9", zoom: 20, center: [6.934471401630646, 50.96733244414443], antialias: true, pitch: 20, attributionControl: true, }) } const resetMap = () => { if (!mapRef.current) return // Remove all event handlers for panels and roofs Object.keys(panelEventHandlers.current).forEach((panelId) => { const { click, mouseenter, mouseleave } = panelEventHandlers.current[panelId] mapRef.current?.off("click", panelId, click) mapRef.current?.off("mouseenter", panelId, mouseenter) mapRef.current?.off("mouseleave", panelId, mouseleave) }) panelEventHandlers.current = {} Object.keys(roofEventHandlers.current).forEach((roofSourceId) => { const { mouseenter, mouseleave, click, mouseenterCursor, mouseleaveCursor, roofCircleId } = roofEventHandlers.current[roofSourceId] mapRef.current?.off("mouseenter", roofSourceId, mouseenter) mapRef.current?.off("mouseleave", roofSourceId, mouseleave) mapRef.current?.off("click", roofCircleId, click) mapRef.current?.off("mouseenter", roofSourceId, mouseenterCursor) mapRef.current?.off("mouseleave", roofSourceId, mouseleaveCursor) }) roofEventHandlers.current = {} // Remove all sources and layers starting with 'roof' or 'panels' const sources = mapRef.current.getStyle().sources const layers = mapRef.current.getStyle().layers layers.forEach((layer) => { if (layer.id.startsWith("roof") || layer.id.startsWith("panels")) { mapRef.current?.removeLayer(layer.id) } }) for (const sourceId in sources) { if (sourceId.startsWith("roof") || sourceId.startsWith("panels")) { mapRef.current.removeSource(sourceId) } } } const fetchCoordinates = async (event: SyntheticEvent<HTMLFormElement>) => { event.preventDefault() try { const response = await fetch(`/api/address-search/coordinates/?address=${address}`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ address }), }) if (!response.ok) { throw new Error("Failed to fetch coordinates") } const data = await response.json() const [lng, lat] = data.features[0].center mapRef?.current?.flyTo({ center: [lng, lat], }) // Reset the map layout resetMap() setSelectedPanels({}) getRoofSurfaces(address) .then((roofSurfaces: any) => { installPvSystem(roofSurfaces) }) .catch((error: unknown) => { if (error instanceof Error) { console.error("Error fetching coordinates:", error) } }) } catch (error) { console.error("Error fetching coordinates:", error) } } const installPvSystem = (roofSurfaces: any) => { roofSurfaces.forEach((surface: any) => { surface.id = surface.roofId surface.coords = convertCoordinates(surface.roofGeometryWGS84).outer surface.holes = convertCoordinates(surface.roofGeometryWGS84).inners surface.panelCoords = surface.panelGeometriesWGS84.map((panelCoord: any) => convertCoordinates(panelCoord).outer) }) roofSurfaces.forEach((roof: Roof) => { addRoof(roof) }) } // Ensure that the toggleAllPanels function updates the paint properties as well function toggleAllPanels(roof: Roof, selectStatus: boolean) { mapRef.current?.setPaintProperty( `roof-circle-${address}-${roof.id}`, "circle-color", selectStatus ? "#1d4ed8" : "transparent" ) mapRef.current?.setPaintProperty( `roof-${address}-${roof.id}`, "fill-color", selectStatus ? "#8fe03f" : "transparent" ) mapRef.current?.setPaintProperty( `roof-border-${address}-${roof.id}`, "line-color", selectStatus ? "#a3e635" : "#f8fafc" ) mapRef.current?.setPaintProperty( `roof-border-${address}-${roof.id}`, "line-dasharray", selectStatus ? null : [2, 2] ) mapRef.current?.setLayoutProperty( `roof-text-${address}-${roof.id}`, "text-field", selectStatus ? `${roof.id}` : "+" ) mapRef.current?.setPaintProperty(`roof-${address}-${roof.id}`, "fill-opacity", selectStatus ? 0.4 : 0) // Update properties for each panel if (roof?.panelCoords) { roof.panelCoords.forEach((_: any, index: number) => { const panelId = `panels-${address}-${roof.id}-${index}` const panelIdLine = `${panelId}-line` mapRef.current?.setPaintProperty(panelId, "fill-color", selectStatus ? "#020617" : "transparent") mapRef.current?.setPaintProperty(panelIdLine, "line-width", selectStatus ? 0.2 : 0) mapRef.current?.setPaintProperty(panelIdLine, "line-opacity", selectStatus ? 0.5 : 0) // Ensure hover and click events are managed correctly if (!selectStatus) { // Remove hover and click effects if (panelEventHandlers.current[panelId]) { const { click, mouseenter, mouseleave } = panelEventHandlers.current[panelId] mapRef.current?.off("click", panelId, click) mapRef.current?.off("mouseenter", panelId, mouseenter) mapRef.current?.off("mouseleave", panelId, mouseleave) } } else { // Reattach hover and click handlers if selecting const mouseEnterHandler = () => { if (mapRef.current) { const currentColor = mapRef.current?.getPaintProperty(panelId, "fill-color") if (currentColor !== "transparent") { mapRef.current.getCanvas().style.cursor = "pointer" mapRef.current?.setPaintProperty(panelId, "fill-color", "#1e40af") } } } const mouseLeaveHandler = () => { if (mapRef.current) { const currentColor = mapRef.current?.getPaintProperty(panelId, "fill-color") if (currentColor !== "transparent") { mapRef.current.getCanvas().style.cursor = "" mapRef.current?.setPaintProperty(panelId, "fill-color", "#020617") } } } const clickHandler = () => togglePanel(panelId, roof, index) panelEventHandlers.current[panelId] = { click: clickHandler, mouseenter: mouseEnterHandler, mouseleave: mouseLeaveHandler, } mapRef.current?.on("click", panelId, clickHandler) mapRef.current?.on("mouseenter", panelId, mouseEnterHandler) mapRef.current?.on("mouseleave", panelId, mouseLeaveHandler) } }) } // Update selected panels state setSelectedPanels((prevSelectedPanels) => { const updatedRoofPanels = roof.panelCoords.map((_: any, index: number) => ({ panelID: index, selected: selectStatus, })) return { ...prevSelectedPanels, [roof.id]: updatedRoofPanels } }) } const addRoof = (roof: Roof) => { if (!mapRef.current) return const roofPolygonData = createRoofPolygonData(roof) const roofSourceId = `roof-${address}-${roof.id}` addRoofSource(roofSourceId, roofPolygonData) addRoofLayers(roofSourceId, roof) addRoofEventHandlers(roofSourceId, roof) const center = turf.center(turf.center(roofPolygonData)) addCenterSourceAndLayer(roof.id, center) addPanelLayers(roof, roofPolygonData) } const createRoofPolygonData = (roof: Roof): Feature<Geometry, GeoJsonProperties> => { const polygon = turf.polygon([roof.coords]) const roofCoordinates = polygon.geometry.coordinates roof.holes.forEach((hole: Position[]) => { roofCoordinates.push(hole) }) return { type: "Feature", properties: {}, geometry: { type: "Polygon", coordinates: roofCoordinates, }, } } const addRoofSource = (roofSourceId: string, roofPolygonData: Feature<Geometry, GeoJsonProperties>) => { if (mapRef.current?.getSource(roofSourceId)) { const source = mapRef.current.getSource(roofSourceId) as mapboxGL.GeoJSONSource source.setData(roofPolygonData) } else { mapRef.current?.addSource(roofSourceId, { type: "geojson", data: roofPolygonData, }) } } const addRoofLayers = (roofSourceId: string, roof: Roof) => { if (!mapRef.current?.getLayer(roofSourceId)) { mapRef.current?.addLayer({ id: roofSourceId, type: "fill", source: roofSourceId, paint: { "fill-color": "#8fe03f", "fill-opacity": 0.5, }, }) } const roofBorderId = `roof-border-${address}-${roof.id}` if (!mapRef.current?.getLayer(roofBorderId)) { mapRef.current?.addLayer({ id: roofBorderId, type: "line", source: roofSourceId, layout: {}, paint: { "line-color": "#a3e635", "line-width": 2, }, }) } } const addRoofEventHandlers = (roofSourceId: string, roof: Roof) => { const handleMouseEnter = () => handleRoofMouseEnter(roofSourceId, roof) const handleMouseLeave = () => handleRoofMouseLeave(roofSourceId, roof) const handleClick = () => handleRoofClick(roofSourceId, roof) const handleMouseEnterCursor = () => { if (mapRef.current) { mapRef.current.getCanvas().style.cursor = "pointer" } } const handleMouseLeaveCursor = () => { if (mapRef.current) { mapRef.current.getCanvas().style.cursor = "" } } const roofCircleId = `roof-circle-${address}-${roof.id}` // Store event handlers for cleanup roofEventHandlers.current[roofSourceId] = { mouseenter: handleMouseEnter, mouseleave: handleMouseLeave, click: handleClick, mouseenterCursor: handleMouseEnterCursor, mouseleaveCursor: handleMouseLeaveCursor, roofCircleId, } mapRef.current?.on("mouseenter", roofSourceId, handleMouseEnter) mapRef.current?.on("mouseleave", roofSourceId, handleMouseLeave) mapRef.current?.on("click", roofCircleId, handleClick) } const handleRoofMouseEnter = (roofSourceId: string, roof: Roof) => { if (mapRef.current) { if (mapRef.current.getPaintProperty(roofSourceId, "fill-color") === "transparent") { mapRef.current.setPaintProperty(roofSourceId, "fill-color", "#f8fafc") mapRef.current.setPaintProperty(roofSourceId, "fill-opacity", 0.1) return } if (mapRef.current.getPaintProperty(roofSourceId, "fill-color") !== "transparent") { mapRef.current.setPaintProperty(roofSourceId, "fill-color", "#500724") mapRef.current.setPaintProperty(`roof-border-${address}-${roof.id}`, "line-color", "#f8fafc") mapRef.current.setPaintProperty(roofSourceId, "fill-opacity", 0) mapRef.current.setPaintProperty(`roof-circle-${address}-${roof.id}`, "circle-color", "transparent") mapRef.current.setLayoutProperty(`roof-text-${address}-${roof.id}`, "text-field", "X") } } } const handleRoofMouseLeave = (roofSourceId: string, roof: Roof) => { if (mapRef.current) { if (mapRef.current.getPaintProperty(`roof-border-${address}-${roof.id}`, "line-dasharray")) { mapRef.current.setPaintProperty(roofSourceId, "fill-color", "transparent") return } if (mapRef.current.getPaintProperty(roofSourceId, "fill-color") !== "transparent") { mapRef.current.setPaintProperty(roofSourceId, "fill-color", "#8fe03f") mapRef.current.setPaintProperty(`roof-border-${address}-${roof.id}`, "line-color", "#a3e635") mapRef.current.setPaintProperty(roofSourceId, "fill-opacity", 0.4) mapRef.current.setPaintProperty(`roof-circle-${address}-${roof.id}`, "circle-color", "#1e3a8a") mapRef.current.setLayoutProperty(`roof-text-${address}-${roof.id}`, "text-field", `${roof.id}`) } } } const handleRoofClick = (roofSourceId: string, roof: Roof) => { if (mapRef.current) { const currentColor = mapRef.current.getPaintProperty(roofSourceId, "fill-color") if (currentColor !== "#f8fafc" && currentColor !== "transparent") { toggleAllPanels(roof, false) } else { toggleAllPanels(roof, true) } // Stop event propagation to ensure it doesn't affect underlying layers mapRef.current.getCanvas().style.cursor = "" } } const addPanelLayers = (roof: Roof, roofPolygonData: Feature<Geometry, GeoJsonProperties>) => { if (roof.panelCoords) { roof.panelCoords.forEach((panelCoords: any, index: number) => { addPanel(roof, panelCoords, index) }) setSelectedPanels((prevSelectedPanels: SelectedPanelsState) => { const updatedRoofPanels = roof.panelCoords.map((_: any, index: number) => ({ panelID: index, selected: true, })) return { ...prevSelectedPanels, [roof.id]: updatedRoofPanels } }) } } const addCenterSourceAndLayer = (roofId: number, center: Feature<Geometry, GeoJsonProperties>) => { const centerSourceId = `roof-circle-${address}-${roofId}` mapRef.current?.addSource(centerSourceId, { type: "geojson", data: center, }) if (!mapRef.current?.getLayer(centerSourceId)) { mapRef.current?.addLayer({ id: centerSourceId, type: "circle", source: centerSourceId, paint: { "circle-radius": 30, "circle-color": "#1e3a8a", "circle-stroke-width": 4, "circle-stroke-color": "white", }, }) } const textLayerId = `roof-text-${address}-${roofId}` mapRef.current?.addLayer({ id: textLayerId, type: "symbol", source: centerSourceId, layout: { "text-field": `${roofId}`, "text-size": 20, }, paint: { "text-color": "white", }, }) } useEffect(() => { console.log(selectedPanels) }, [selectedPanels]) const togglePanel = (panelId: string, roof: Roof, index: number) => { setSelectedPanels((prevSelectedPanels) => { const updatedRoof = [...prevSelectedPanels[roof.id]] updatedRoof[index] = { ...updatedRoof[index], selected: !updatedRoof[index].selected } const allUnselected = updatedRoof.every((panel) => !panel.selected) // Update the paint properties based on the new selected state const newColor = updatedRoof[index].selected ? "#1e40af" : "transparent" const panelLineId = `${panelId}-line` mapRef.current?.setPaintProperty(panelId, "fill-color", newColor) mapRef.current?.setPaintProperty(panelLineId, "line-width", updatedRoof[index].selected ? 1 : 2) mapRef.current?.setPaintProperty(panelLineId, "line-color", updatedRoof[index].selected ? "white" : "#020617") mapRef.current?.setPaintProperty(panelLineId, "line-opacity", updatedRoof[index].selected ? 0.5 : 1) mapRef.current?.setPaintProperty(panelLineId, "line-dasharray", updatedRoof[index].selected ? null : [2, 2]) if (allUnselected) { toggleAllPanels(roof, false) } return { ...prevSelectedPanels, [roof.id]: updatedRoof } }) } const addPanel = (roof: Roof, panelCoords: any, index: number) => { const panelFeature: Feature<Geometry, GeoJsonProperties> = { type: "Feature", properties: { panelId: index }, geometry: { type: "Polygon", coordinates: [panelCoords] }, } const panelId = `panels-${address}-${roof.id}-${index}` const panelLineId = `${panelId}-line` mapRef.current?.addSource(panelId, { type: "geojson", data: panelFeature, }) // Check if the panel layer exists mapRef.current?.addLayer( { id: panelId, type: "fill", source: panelId, paint: { "fill-color": "#020617", "fill-opacity": 1, }, }, `roof-circle-${address}-${roof.id}` ) mapRef.current?.addLayer( { id: panelLineId, type: "line", source: panelId, paint: { "line-color": "white", "line-opacity": 0.5, }, }, `roof-circle-${address}-${roof.id}` ) const clickHandler = () => togglePanel(panelId, roof, index) const mouseEnterHandler = () => { if (mapRef.current) { mapRef.current.getCanvas().style.cursor = "pointer" const currentColor = mapRef.current?.getPaintProperty(panelId, "fill-color") if (currentColor !== "transparent") { mapRef.current?.setPaintProperty(panelId, "fill-color", "#1e40af") } } } const mouseLeaveHandler = () => { if (mapRef.current) { mapRef.current.getCanvas().style.cursor = "" const currentColor = mapRef.current?.getPaintProperty(panelId, "fill-color") if (currentColor !== "transparent") { mapRef.current?.setPaintProperty(panelId, "fill-color", "#020617") } } } // Store event handlers for cleanup panelEventHandlers.current[panelId] = { click: clickHandler, mouseenter: mouseEnterHandler, mouseleave: mouseLeaveHandler, } mapRef.current?.on("click", panelId, clickHandler) mapRef.current?.on("mouseenter", panelId, mouseEnterHandler) mapRef.current?.on("mouseleave", panelId, mouseLeaveHandler) } useEffect(() => { getEnv().then((env) => { const token = env.NEXT_PUBLIC_DOCKER_MAPBOX_ACCESS_TOKEN! initMap(token) }) return () => mapRef?.current?.remove() }, []) return ( <div className='h-screen flex flex-col items-stretch'> <form onSubmit={fetchCoordinates}> <Input className='my-2 w-1/3 mx-auto' placeholder='Enter address' value={address} onChange={(e) => setAddress(e.target.value)} /> </form> <div id='map-container' className='flex-1'></div> </div> ) } export default BuildingMap
d15a71d1-8f46-4ae6-a951-5aa405b967ae.webm
mapbox-gl-js version:
Question
Hi everyone,
I spent two days trying to solve this issue but no way.
I did not open this until I could not find a solution on Google or using ChatGPT.
As you see in the image below, I have stack of layers from bottom to top:
Layer 01 : Roof ( Polygon )
Layer 02: Solar Panels (Polygon )
Layer 03: Roof ID (Cirlce)
As you see in the picture when I hover on the cirlce layer ( Layer 3 ) , it triggers also hover the panel ( Layer 2 ) while It should not happen.
Please could share an example on how to handle this situation?
Code
Demo
d15a71d1-8f46-4ae6-a951-5aa405b967ae.webm