Reputation: 2880
I have mapbox with 3 layers:
When I hover or click on the PanelID layer (circle) which should remove all the panels, it triggers also the panel ( rectangle ) that's under the PanelID layer.
"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
Upvotes: 0
Views: 64
Reputation: 398
The mapboxGL.Map component has an interactiveLayerIds
property that controls the active layers on which onClick
or onHover
events can be triggered.
You will need to assign an array of active layer IDs to the interactiveLayerIds
property to make it work on specific layers. Please refer to the section of onClick event from react-map-gl documentation.
Upvotes: 0