We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
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
The text was updated successfully, but these errors were encountered:
No branches or pull requests
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
The text was updated successfully, but these errors were encountered: