From 753d03e4ae6b1590fc80397067934fbefacb5146 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 27 Jan 2026 12:26:44 +0000 Subject: [PATCH 1/3] feat: add Mapbox compare slider for Drawing Mode - Installed mapbox-gl-compare and added type declarations. - Implemented dual map synchronization in Mapbox component. - Enabled split-screen comparison when entering Drawing Mode. - Synced drawn geometries and measurement labels between both maps. - Added pointer-events optimization for drawing over the comparison layer. - Ensured proper cleanup of map instances and comparison controls. Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com> --- bun.lock | 5 + components/map/mapbox-map.tsx | 198 ++++++++++++++++++++++++++++++- lib/types/mapbox-gl-compare.d.ts | 21 ++++ package.json | 1 + 4 files changed, 221 insertions(+), 4 deletions(-) create mode 100644 lib/types/mapbox-gl-compare.d.ts diff --git a/bun.lock b/bun.lock index 936916d2..9dd12088 100644 --- a/bun.lock +++ b/bun.lock @@ -61,6 +61,7 @@ "lottie-react": "^2.4.1", "lucide-react": "^0.507.0", "mapbox-gl": "^3.11.0", + "mapbox-gl-compare": "^0.4.2", "next": "15.3.6", "next-themes": "^0.3.0", "open-codex": "^0.1.30", @@ -385,6 +386,8 @@ "@mapbox/mapbox-gl-supported": ["@mapbox/mapbox-gl-supported@3.0.0", "", {}, "sha512-2XghOwu16ZwPJLOFVuIOaLbN0iKMn867evzXFyf0P22dqugezfJwLmdanAgU25ITvz1TvOfVP4jsDImlDJzcWg=="], + "@mapbox/mapbox-gl-sync-move": ["@mapbox/mapbox-gl-sync-move@0.3.1", "", {}, "sha512-Y3PMyj0m/TBJa9OkQnO2TiVDu8sFUPmLF7q/THUHrD/g42qrURpMJJ4kufq4sR60YFMwZdCGBshrbgK5v2xXWw=="], + "@mapbox/point-geometry": ["@mapbox/point-geometry@1.1.0", "", {}, "sha512-YGcBz1cg4ATXDCM/71L9xveh4dynfGmcLDqufR+nQQy3fKwsAZsWd/x4621/6uJaeB9mwOHE6hPeDgXz9uViUQ=="], "@mapbox/tiny-sdf": ["@mapbox/tiny-sdf@2.0.7", "", {}, "sha512-25gQLQMcpivjOSA40g3gO6qgiFPDpWRoMfd+G/GoppPIeP6JDaMMkMrEJnMZhKyyS6iKwVt5YKu02vCUyJM3Ug=="], @@ -1789,6 +1792,8 @@ "mapbox-gl": ["mapbox-gl@3.17.0", "", { "dependencies": { "@mapbox/jsonlint-lines-primitives": "^2.0.2", "@mapbox/mapbox-gl-supported": "^3.0.0", "@mapbox/point-geometry": "^1.1.0", "@mapbox/tiny-sdf": "^2.0.6", "@mapbox/unitbezier": "^0.0.1", "@mapbox/vector-tile": "^2.0.4", "@mapbox/whoots-js": "^3.1.0", "@types/geojson": "^7946.0.16", "@types/geojson-vt": "^3.2.5", "@types/mapbox__point-geometry": "^0.1.4", "@types/pbf": "^3.0.5", "@types/supercluster": "^7.1.3", "cheap-ruler": "^4.0.0", "csscolorparser": "~1.0.3", "earcut": "^3.0.1", "geojson-vt": "^4.0.2", "gl-matrix": "^3.4.4", "grid-index": "^1.1.0", "kdbush": "^4.0.2", "martinez-polygon-clipping": "^0.7.4", "murmurhash-js": "^1.0.0", "pbf": "^4.0.1", "potpack": "^2.0.0", "quickselect": "^3.0.0", "supercluster": "^8.0.1", "tinyqueue": "^3.0.0" } }, "sha512-nCrDKRlr5di6xUksUDslNWwxroJ5yv1hT8pyVFtcpWJOOKsYQxF/wOFTMie8oxMnXeFkrz1Tl1TwA1XN1yX0KA=="], + "mapbox-gl-compare": ["mapbox-gl-compare@0.4.2", "", { "dependencies": { "@mapbox/mapbox-gl-sync-move": "^0.3.1" } }, "sha512-MhKUYJri3KIfeB2rsLUh2JzPutdfehU3vqdfgp2+uocKjNYQwCUvQ0T8u7fBwIJvJKdfWg3FiDq+5oZC4AtAGg=="], + "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="], "marked": ["marked@15.0.12", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA=="], diff --git a/components/map/mapbox-map.tsx b/components/map/mapbox-map.tsx index 3dd390cd..07f6ae94 100644 --- a/components/map/mapbox-map.tsx +++ b/components/map/mapbox-map.tsx @@ -1,7 +1,9 @@ 'use client' -import { useEffect, useRef, useCallback } from 'react' // Removed useState +import { useEffect, useRef, useCallback, useState } from 'react' // Removed useState import mapboxgl from 'mapbox-gl' +import Compare from 'mapbox-gl-compare' +import 'mapbox-gl-compare/dist/mapbox-gl-compare.css' import MapboxDraw from '@mapbox/mapbox-gl-draw' import * as turf from '@turf/turf' import { toast } from 'react-toastify' @@ -17,7 +19,11 @@ mapboxgl.accessToken = process.env.NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN as string; export const Mapbox: React.FC<{ position?: { latitude: number; longitude: number; } }> = ({ position }) => { const mapContainer = useRef(null) + const afterMapContainer = useRef(null) const map = useRef(null) + const afterMap = useRef(null) + const compareRef = useRef(null) + const afterMapMarkersRef = useRef([]) const { setMap } = useMap() const drawRef = useRef(null) const rotationFrameRef = useRef(null) @@ -61,6 +67,107 @@ export const Mapbox: React.FC<{ position?: { latitude: number; longitude: number }, []) // Create measurement labels for all features + const syncDrawingsToAfterMap = useCallback(() => { + if (!afterMap.current || !afterMap.current.getSource('draw-mirror')) return; + + const source = afterMap.current.getSource('draw-mirror') as mapboxgl.GeoJSONSource; + const features = mapData.drawnFeatures?.map(df => df.geometry) || []; + + source.setData({ + type: 'FeatureCollection', + features: features.map((g, i) => ({ + type: 'Feature', + geometry: g, + properties: mapData.drawnFeatures?.[i] || {} + })) + }); + + // Mirror labels + afterMapMarkersRef.current.forEach(m => m.remove()); + afterMapMarkersRef.current = []; + + mapData.drawnFeatures?.forEach(feature => { + let coords: [number, number] | null = null; + if (feature.type === 'Polygon') { + const centroid = turf.centroid(feature.geometry); + coords = centroid.geometry.coordinates as [number, number]; + } else if (feature.type === 'LineString') { + const line = feature.geometry.coordinates; + const midIndex = Math.floor(line.length / 2) - 1; + coords = (midIndex >= 0 ? line[midIndex] : line[0]) as [number, number]; + } + + if (coords && afterMap.current) { + const el = document.createElement('div'); + el.className = feature.type === 'Polygon' ? 'area-label' : 'distance-label'; + el.style.background = 'rgba(255, 255, 255, 0.8)' + el.style.padding = '4px 8px' + el.style.borderRadius = '4px' + el.style.fontSize = '12px' + el.style.fontWeight = 'bold' + el.style.color = '#333333' + el.style.boxShadow = '0 2px 4px rgba(0,0,0,0.2)' + el.style.pointerEvents = 'none' + el.textContent = feature.measurement; + + const marker = new mapboxgl.Marker({ element: el }) + .setLngLat(coords) + .addTo(afterMap.current); + afterMapMarkersRef.current.push(marker); + } + }); + }, [mapData.drawnFeatures]); + + const setupAfterMapLayers = useCallback(() => { + if (!afterMap.current) return; + + // Add source for mirrored drawings + afterMap.current.addSource('draw-mirror', { + type: 'geojson', + data: { + type: 'FeatureCollection', + features: [] + } + }); + + // Add layers for polygons and lines + afterMap.current.addLayer({ + id: 'draw-mirror-polygons', + type: 'fill', + source: 'draw-mirror', + filter: ['==', '$type', 'Polygon'], + paint: { + 'fill-color': '#fbb03b', + 'fill-opacity': 0.1 + } + }); + + afterMap.current.addLayer({ + id: 'draw-mirror-polygons-outline', + type: 'line', + source: 'draw-mirror', + filter: ['==', '$type', 'Polygon'], + paint: { + 'line-color': '#fbb03b', + 'line-width': 2 + } + }); + + afterMap.current.addLayer({ + id: 'draw-mirror-lines', + type: 'line', + source: 'draw-mirror', + filter: ['==', '$type', 'LineString'], + paint: { + 'line-color': '#fbb03b', + 'line-width': 2 + } + }); + + // Initial sync + syncDrawingsToAfterMap(); + }, [syncDrawingsToAfterMap]); + const updateMeasurementLabels = useCallback(() => { if (!map.current || !drawRef.current) return @@ -335,6 +442,71 @@ export const Mapbox: React.FC<{ position?: { latitude: number; longitude: number } }, [setMapData]) + // Handle comparison map initialization + useEffect(() => { + if (mapType === MapToggleEnum.DrawingMode && map.current && afterMapContainer.current && !afterMap.current) { + const center = map.current.getCenter(); + const zoom = map.current.getZoom(); + const pitch = map.current.getPitch(); + const bearing = map.current.getBearing(); + + afterMap.current = new mapboxgl.Map({ + container: afterMapContainer.current, + style: 'mapbox://styles/mapbox/streets-v12', + center: [center.lng, center.lat], + zoom: zoom, + pitch: pitch, + bearing: bearing, + maxZoom: 22, + attributionControl: false, + }); + + afterMap.current.on('load', () => { + if (!map.current || !afterMap.current || !afterMapContainer.current?.parentElement) return; + + // Create the compare control + compareRef.current = new Compare( + map.current, + afterMap.current, + afterMapContainer.current.parentElement, + { + orientation: 'vertical', + mousemove: false + } + ); + + // Setup layers on afterMap + setupAfterMapLayers(); + }); + } + + // Cleanup when leaving DrawingMode + if (mapType !== MapToggleEnum.DrawingMode) { + if (compareRef.current) { + compareRef.current.remove(); + compareRef.current = null; + } + if (afterMap.current) { + afterMap.current.remove(); + afterMap.current = null; + } + afterMapMarkersRef.current.forEach(m => m.remove()); + afterMapMarkersRef.current = []; + + if (map.current) { + const container = map.current.getContainer(); + if (container) container.style.clip = ''; + } + } + }, [mapType, setupAfterMapLayers]); + + // Effect to sync drawings when they change + useEffect(() => { + if (mapType === MapToggleEnum.DrawingMode) { + syncDrawingsToAfterMap(); + } + }, [mapData.drawnFeatures, mapType, syncDrawingsToAfterMap]); + // Set up idle rotation checker useEffect(() => { const checkIdle = setInterval(() => { @@ -510,6 +682,18 @@ export const Mapbox: React.FC<{ position?: { latitude: number; longitude: number stopRotation() setIsMapLoaded(false) // Reset map loaded state on cleanup setMap(null) // Clear map instance from context + + if (compareRef.current) { + compareRef.current.remove() + compareRef.current = null + } + if (afterMap.current) { + afterMap.current.remove() + afterMap.current = null + } + afterMapMarkersRef.current.forEach(m => m.remove()); + afterMapMarkersRef.current = []; + map.current.remove() map.current = null } @@ -633,14 +817,20 @@ export const Mapbox: React.FC<{ position?: { latitude: number; longitude: number return ( -
+
+ {mapType === MapToggleEnum.DrawingMode && ( +
+ )}
) } diff --git a/lib/types/mapbox-gl-compare.d.ts b/lib/types/mapbox-gl-compare.d.ts new file mode 100644 index 00000000..7a749641 --- /dev/null +++ b/lib/types/mapbox-gl-compare.d.ts @@ -0,0 +1,21 @@ +declare module 'mapbox-gl-compare' { + import { Map } from 'mapbox-gl'; + + export interface CompareOptions { + orientation?: 'vertical' | 'horizontal'; + mousemove?: boolean; + } + + export default class Compare { + constructor( + a: Map, + b: Map, + container: string | HTMLElement, + options?: CompareOptions + ); + remove(): void; + on(type: string, fn: Function): this; + off(type: string, fn: Function): this; + setSlider(x: number): void; + } +} diff --git a/package.json b/package.json index a263674e..c7902891 100644 --- a/package.json +++ b/package.json @@ -73,6 +73,7 @@ "lottie-react": "^2.4.1", "lucide-react": "^0.507.0", "mapbox-gl": "^3.11.0", + "mapbox-gl-compare": "^0.4.2", "next": "15.3.6", "next-themes": "^0.3.0", "open-codex": "^0.1.30", From 2ebeba0bb26136b80078195b7f1089bf53474049 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 27 Jan 2026 12:40:51 +0000 Subject: [PATCH 2/3] fix: resolve SSR crash and improve comparison map layout - Fixed ReferenceError: window is not defined by late-importing mapbox-gl-compare on the client side. - Updated map container layout to use inset-0 for more reliable rendering. - Addressed PR feedback regarding map rendering issues. - Cleaned up comments and refined synchronization logic. Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com> --- components/map/mapbox-map.tsx | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/components/map/mapbox-map.tsx b/components/map/mapbox-map.tsx index 07f6ae94..caf8c513 100644 --- a/components/map/mapbox-map.tsx +++ b/components/map/mapbox-map.tsx @@ -1,10 +1,16 @@ 'use client' -import { useEffect, useRef, useCallback, useState } from 'react' // Removed useState +import { useEffect, useRef, useCallback, useState } from 'react' import mapboxgl from 'mapbox-gl' -import Compare from 'mapbox-gl-compare' import 'mapbox-gl-compare/dist/mapbox-gl-compare.css' +import type CompareClass from 'mapbox-gl-compare' import MapboxDraw from '@mapbox/mapbox-gl-draw' + +// Late-import mapbox-gl-compare to avoid SSR issues +let Compare: any = null; +if (typeof window !== 'undefined') { + Compare = require('mapbox-gl-compare'); +} import * as turf from '@turf/turf' import { toast } from 'react-toastify' import 'react-toastify/dist/ReactToastify.css' @@ -22,7 +28,7 @@ export const Mapbox: React.FC<{ position?: { latitude: number; longitude: number const afterMapContainer = useRef(null) const map = useRef(null) const afterMap = useRef(null) - const compareRef = useRef(null) + const compareRef = useRef(null) const afterMapMarkersRef = useRef([]) const { setMap } = useMap() const drawRef = useRef(null) @@ -820,7 +826,7 @@ export const Mapbox: React.FC<{ position?: { latitude: number; longitude: number
)}
From 63ee1795e5d0612887335e913fe0cb01cc967a9c Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 28 Jan 2026 09:18:09 +0000 Subject: [PATCH 3/3] fix: resolve mapbox rendering and SSR crash - Moved mapbox-gl-compare requirement into a client-side useEffect to prevent window ReferenceError during SSR. - Robustified the Compare constructor access to handle potential default export wrapping. - Ensured stable map container dimensions by using h-full w-full by default. - Added error handling for compare control initialization. Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com> --- components/map/mapbox-map.tsx | 45 ++++++++++++++++++----------------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/components/map/mapbox-map.tsx b/components/map/mapbox-map.tsx index caf8c513..8381ecda 100644 --- a/components/map/mapbox-map.tsx +++ b/components/map/mapbox-map.tsx @@ -3,14 +3,7 @@ import { useEffect, useRef, useCallback, useState } from 'react' import mapboxgl from 'mapbox-gl' import 'mapbox-gl-compare/dist/mapbox-gl-compare.css' -import type CompareClass from 'mapbox-gl-compare' import MapboxDraw from '@mapbox/mapbox-gl-draw' - -// Late-import mapbox-gl-compare to avoid SSR issues -let Compare: any = null; -if (typeof window !== 'undefined') { - Compare = require('mapbox-gl-compare'); -} import * as turf from '@turf/turf' import { toast } from 'react-toastify' import 'react-toastify/dist/ReactToastify.css' @@ -28,7 +21,7 @@ export const Mapbox: React.FC<{ position?: { latitude: number; longitude: number const afterMapContainer = useRef(null) const map = useRef(null) const afterMap = useRef(null) - const compareRef = useRef(null) + const compareRef = useRef(null) const afterMapMarkersRef = useRef([]) const { setMap } = useMap() const drawRef = useRef(null) @@ -470,19 +463,27 @@ export const Mapbox: React.FC<{ position?: { latitude: number; longitude: number afterMap.current.on('load', () => { if (!map.current || !afterMap.current || !afterMapContainer.current?.parentElement) return; - // Create the compare control - compareRef.current = new Compare( - map.current, - afterMap.current, - afterMapContainer.current.parentElement, - { - orientation: 'vertical', - mousemove: false - } - ); - - // Setup layers on afterMap - setupAfterMapLayers(); + try { + // Late-import mapbox-gl-compare to avoid SSR issues + const CompareModule = require('mapbox-gl-compare'); + const CompareConstructor = CompareModule.default || CompareModule; + + // Create the compare control + compareRef.current = new CompareConstructor( + map.current, + afterMap.current, + afterMapContainer.current.parentElement, + { + orientation: 'vertical', + mousemove: false + } + ); + + // Setup layers on afterMap + setupAfterMapLayers(); + } catch (error) { + console.error('Error initializing mapbox-gl-compare:', error); + } }); } @@ -826,7 +827,7 @@ export const Mapbox: React.FC<{ position?: { latitude: number; longitude: number