import { useEffect, useRef, useState } from "react"; import "mapbox-gl/dist/mapbox-gl.css"; import { Box, Button, DialogActions, DialogContent, DialogTitle, Grid2 as Grid, Icon, IconButton, LinearProgress, MenuItem, Select, Theme } from "@mui/material"; import { HomeMarker } from "models/HomeMarker"; import { useGlobalState, useHomeMarkerAPI } from "providers"; import { useMobile, useSnackbar } from "hooks"; import { useCallback } from "react"; import HomeIcon from "products/AgIcons/HomeIcon"; //import { Theme } from "@material-ui/core/styles/createMuiTheme"; import { GpsFixed, Layers } from "@mui/icons-material"; //import Geocoder from "react-map-gl-geocoder"; import "maps/mapboxStyleOverrides.css"; import { pond } from "protobuf-ts/pond"; import MarkerMove from "products/AgIcons/MarkerMove"; import ResponsiveDialog from "common/ResponsiveDialog"; import Markers, { MarkerData } from "./mapMarkers/Markers"; import GeoMapLayer from "./mapLayers/geoMapLayer"; import Geocoder from "./mapControllers/Geocoder"; import { GeometryMapping, shapeFromCoords } from "models/GeometryMapping"; import { FeatureCollection, Feature } from "geojson"; import DrawController from "./mapControllers/drawController"; //import { Geometry } from "geojson"; import Map, { MapRef, Marker, MarkerDragEvent } from "react-map-gl/mapbox-legacy"; import { getDistanceUnit } from "utils"; import { MapMouseEvent } from "mapbox-gl"; import { makeStyles } from "@mui/styles"; import { Result } from "@mapbox/mapbox-gl-geocoder"; import mapboxgl from "mapbox-gl"; mapboxgl.workerClass = require('worker-loader!mapbox-gl/dist/mapbox-gl-csp-worker').default; //const MAPBOX_TOKEN = process.env.REACT_APP_MAPBOX_ACCESS_TOKEN; const MAPBOX_TOKEN = import.meta.env.VITE_MAPBOX_ACCESS_TOKEN; const mapStyleOptions = [ { label: "Dark", value: "mapbox://styles/mapbox/dark-v10" }, { label: "Satellite with streets", value: "mapbox://styles/mapbox/satellite-streets-v11" } ]; const useStyles = makeStyles((theme: Theme) => ({ iconButtons: { background: theme.palette.background.default, margin: 5, boxShadow: "4px 4px 10px black" }, pin: { borderRadius: "50rem", display: "inline-block", borderBottomRightRadius: "0", transform: "rotate(45deg)", cursor: "pointer", boxShadow: "4px 4px 10px black" }, geoDot: { height: "25px", width: "25px", backgroundColor: "yellow", borderRadius: "50%", border: "5px solid black" }, geoIconColor: { color: "black" }, geoIconSearch: { animation: "$rotation 3s infinite" }, "@keyframes rotation": { from: { transform: "rotate(0deg)" }, to: { transform: "rotate(360deg)" } } })); export interface ViewData { longitude: number; latitude: number; zoom: number; transitionDuration?: number; xOffset?: number; yOffset?: number; } export interface MeasurementData { key: string; geoShape: string; coordinates: number[][]; segmentDistanceKm: number[]; totalDistanceKm: number; } interface Props { mapTools: JSX.Element; mapClick(mapClickEvent: MapMouseEvent): void; currentView: ViewData; ignoreHomeLoad?: boolean; defaultMapStyle?: string; layerOptions?: JSX.Element; markerData?: MarkerData[]; displayMarkerTitles?: boolean; displayMarkerDetails?: boolean; customCursor?: JSX.Element; objectGeoData?: Map; measurementData?: Map; objectTitles?: boolean; editorMode?: "drawPolygon" | "drawLine" | "drawPoint" | "edit" | "none" | "delete"; placingMarker?: boolean; linePointLimit?: number; customSearchEntries?: Result[]; drawMeasurement?: boolean; geocoderResultFunction?: (result: Result) => void; geocoderTransitionFunction?: (result: Result) => void; cutHoleInPolygon?: (object: string, coordinates: any[], geoType: string) => void; editGeoCallback?: (object: string, borders: pond.Shape[], holes?: pond.Shape[]) => void; addNewShape?: (coordinates: any[], geoType: string) => void; addNewMeasurement?: (coordinates: number[][], geoType: string) => void; } export default function MapBase(props: Props) { const [startingView, setStartingView] = useState(); //const [viewport, setViewport] = useState(props.currentView); const transDuration = 3000; //in milliseconds const homeMarkerAPI = useHomeMarkerAPI(); const classes = useStyles(); const isMobile = useMobile(); const [mapCursor, setMapCursor] = useState("auto"); const [hcHovered, setHCHovered] = useState(false); const [gcHovered, setGCHovered] = useState(false); const [dcHovered, setDCHovered] = useState(false); const mapRef = useRef(null); const [haveHome, setHaveHome] = useState(false); const [homeKey, setHomeKey] = useState(""); const [homePin, setHomePin] = useState<{ longitude: number; latitude: number }>(); const [{ user, as }] = useGlobalState(); const { openSnack } = useSnackbar(); const [stateWatch, setStateWatch] = useState(0); const [watching, setWatching] = useState(false); const [locationFound, setLocationFound] = useState(true); const [geoPosition, setGeoPosition] = useState({ longitude: 0, latitude: 0 }); const [dragAllowed, setDragAllowed] = useState(false); const [mapStyle, setMapStyle] = useState( props.defaultMapStyle ?? "mapbox://styles/mapbox/satellite-streets-v11" ); const [styles, setStyles] = useState(false); const [msHovered, setMSHovered] = useState(false); const [layerIDs, setLayerIDs] = useState([]); const [geoCollection, setGeoCollection] = useState(); const [measurementCollection, setMeasurementCollection] = useState(); const [groupSelect, setGroupSelect] = useState(false); const [currentZoom, setCurrentZoom] = useState(props.currentView.zoom); const movePos = () => { navigator.geolocation.getCurrentPosition( pos => { setGeoPosition({ longitude: pos.coords.longitude, latitude: pos.coords.latitude }); setLocationFound(true); if (mapRef.current) { if (!mapRef.current.isMoving()) { mapRef.current.flyTo({ center: [pos.coords.longitude, pos.coords.latitude], duration: transDuration, zoom: 18 }); } } }, () => { //error callback function }, { enableHighAccuracy: true } ); }; const geolocateToggle = () => { if (!watching) { setWatching(true); setStateWatch(navigator.geolocation.watchPosition(e => movePos())); setLocationFound(false); } else { navigator.geolocation.clearWatch(stateWatch); setWatching(false); } }; //used to change the viewport from the parent useEffect(() => { //setViewport(props.currentView); if (mapRef.current) { let xOffset = props.currentView.xOffset ?? 0; let yOffset = props.currentView.yOffset ?? 0; mapRef.current.flyTo({ offset: [xOffset, yOffset], //this is in pixels center: [props.currentView.longitude, props.currentView.latitude], //this is in degrees duration: props.currentView.transitionDuration, zoom: props.currentView.zoom }); } }, [props.currentView]); const homeControl = () => { if (!haveHome) { setHaveHome(true); if (mapRef.current) { setHomePin({ longitude: mapRef.current.getCenter().lng, latitude: mapRef.current.getCenter().lat }); } // save the pin to the database saveHomeMarker(); } else { if (mapRef.current && homePin) { mapRef.current.flyTo({ center: [homePin.longitude, homePin.latitude], duration: transDuration }); } } }; const loadHomeMarkers = useCallback(() => { homeMarkerAPI .listHomeMarkers(1, 0, undefined, undefined, undefined, undefined, as) .then(resp => { if (resp.data.homeMarker.length < 1) { setHomePin(undefined); setHaveHome(false); setStartingView(props.currentView); return; } let hm = HomeMarker.any(resp.data.homeMarker[0]); setHomeKey(hm.key()); setHomePin({ longitude: hm.long(), latitude: hm.lat() }); setHaveHome(true); if (props.ignoreHomeLoad) { setStartingView(props.currentView); } else if (!startingView) { setStartingView({ longitude: hm.long(), latitude: hm.lat(), zoom: user.settings.mapZoom }); } else if (mapRef.current) { mapRef.current.flyTo({ center: [hm.long(), hm.lat()], zoom: user.settings.mapZoom, duration: 0 }); } }) .catch(err => { openSnack("Could not load Home Pin"); }); }, [as, homeMarkerAPI, openSnack, props.ignoreHomeLoad, user]); // eslint-disable-line react-hooks/exhaustive-deps const saveHomeMarker = () => { if (mapRef.current) { let hm = HomeMarker.create(); hm.settings.userId = as ? as : user.id(); hm.settings.longitude = mapRef.current.getCenter().lng; hm.settings.latitude = mapRef.current.getCenter().lat; homeMarkerAPI .addHomeMarker(hm.settings, as) .then(resp => { setHomeKey(resp.data.homeMarker); openSnack("New Home Marker Created"); }) .catch(err => openSnack("Failed to save new Home Marker")); } }; const updateHomeMarker = (longitude: number, latitude: number) => { let hm = HomeMarker.create(); if (homePin) { hm.settings.latitude = latitude; hm.settings.longitude = longitude; hm.settings.userId = as ? as : user.id(); homeMarkerAPI .updateHomeMarker(homeKey, hm.settings, undefined, as) .then(resp => { openSnack("Home Marker Moved"); }) .catch(err => openSnack("Failed to Update Home Marker")); } }; useEffect(() => { loadHomeMarkers(); }, [loadHomeMarkers, as]); const homeDragEnd = (event: MarkerDragEvent) => { let longitude = event.lngLat.lng; let latitude = event.lngLat.lat; setHomePin({ longitude, latitude }); // update the pin in the database updateHomeMarker(longitude, latitude); }; const tools = () => { return ( {props.mapTools} {dragAllowed && ( setGroupSelect(!groupSelect)} onMouseOver={() => setDCHovered(true)} onMouseOut={() => setDCHovered(false)}> )} { setGroupSelect(false); setDragAllowed(!dragAllowed); }} onMouseOver={() => setDCHovered(true)} onMouseOut={() => setDCHovered(false)}> setStyles(true)} onMouseOver={() => setMSHovered(true)} onMouseOut={() => setMSHovered(false)}> setHCHovered(true)} onMouseOut={() => setHCHovered(false)}> setGCHovered(true)} onMouseOut={() => setGCHovered(false)}> ); }; const mapClick = (event: MapMouseEvent) => { if (!watching) { props.mapClick(event); } }; const mapStyleDialog = () => { return ( setStyles(false)}> Select Map Style {/* layer options from the object controller */} {props.layerOptions} ); }; const onMouseEnter = useCallback(() => setMapCursor("pointer"), []); const onMouseLeave = useCallback(() => setMapCursor("auto"), []); useEffect(() => { if (props.editorMode === "drawPolygon" || props.placingMarker) { setMapCursor("crosshair"); } else { setMapCursor("auto"); } }, [props.editorMode, props.placingMarker]); useEffect(() => { if (props.objectGeoData) { let feats: Feature[] = []; props.objectGeoData.forEach(data => { let newFeature = GeometryMapping.geoJSON(data.geoShape, data.shapes, data.holes) as Feature; newFeature.id = data.objectKey; newFeature.properties = { title: data.title, objectKey: data.objectKey, fill: data.colour, lineWidth: data.geoShape === "LineString" ? 5 : 2, origin: data.origin }; feats.push(newFeature); }); let collection: FeatureCollection = { type: "FeatureCollection", features: feats }; setGeoCollection(collection); } }, [props.objectGeoData]); // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { //add the measurementdata into the feature collection to be passed to our geo map layer if (props.measurementData) { let feats: Feature[] = []; props.measurementData.forEach(data => { let newFeature = GeometryMapping.geoJSON(data.geoShape, [ shapeFromCoords(data.coordinates) ]) as Feature; newFeature.id = data.key; newFeature.properties = { lineWidth: 5, origin: pond.DataOrigin.DATA_ORIGIN_ADAPTIVE, totalDist: getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS ? (data.totalDistanceKm * 1000).toFixed(2) : (data.totalDistanceKm * 3280.8398950131).toFixed(2) }; feats.push(newFeature); }); let collection: FeatureCollection = { type: "FeatureCollection", features: feats }; setMeasurementCollection(collection); } }, [props.measurementData]); const onUpdate = (event: any) => { let borders: pond.Shape[] = []; let holes: pond.Shape[] = []; let objectKey: string = ""; if (event.action === "change_coordinates" && event.features) { let feature = event.features[0]; if (feature) { let coords: any[] = feature.geometry.coordinates; objectKey = feature.id; if (feature.geometry.type === "Polygon") { coords.forEach((poly: any[], i) => { //the first shape is the border if (i === 0) { borders.push(shapeFromCoords(poly)); } else { holes.push(shapeFromCoords(poly)); } }); } else if (feature.geometry.type === "LineString") { borders.push(shapeFromCoords(coords)); } } } if (props.editGeoCallback) { props.editGeoCallback(objectKey, borders, holes.length > 0 ? holes : undefined); } }; const onCreate = (event: any, objectToCut?: string, measurement?: boolean) => { let feature = event.features[0]; //console.log(feature) //if the string exist we are adding a hole to an existing object if (objectToCut) { if (props.cutHoleInPolygon) { props.cutHoleInPolygon(objectToCut, feature.geometry.coordinates, feature.geometry.type); } } else if (measurement) { if (props.addNewMeasurement) { props.addNewMeasurement(feature.geometry.coordinates, feature.geometry.type); } } else { if (props.addNewShape) { //console.log("add new shape") props.addNewShape(feature.geometry.coordinates, feature.geometry.type); } } }; const getEditableFeatures = () => { let editableFeatures: FeatureCollection = { type: "FeatureCollection", features: [] }; if (geoCollection) { geoCollection.features.forEach(feature => { //if the origin of the data comes from us (ie. a field/construction site with the adaptive origin) if ( feature.properties && feature.properties.origin === pond.DataOrigin.DATA_ORIGIN_ADAPTIVE ) { editableFeatures.features.push(feature); } }); } return editableFeatures; }; return startingView ? ( {tools()} {mapStyleDialog()} { setCurrentZoom(e.viewState.zoom); }} onMouseEnter={() => { if ( !props.editorMode || !props.placingMarker || props.editorMode === "none" || !props.placingMarker ) onMouseEnter(); }} onMouseLeave={() => { if ( !props.editorMode || !props.placingMarker || props.editorMode === "none" || !props.placingMarker ) onMouseLeave(); }} interactiveLayerIds={layerIDs} // onMove={e => { // setViewport({ // longitude: e.viewState.longitude, // latitude: e.viewState.latitude, // zoom: e.viewState.zoom // }); // }} mapboxAccessToken={MAPBOX_TOKEN} cursor={mapCursor} onClick={e => { mapClick(e); }} doubleClickZoom={false}> {/* map controllers */} {MAPBOX_TOKEN && ( )} {/* markers */} {props.markerData && ( )} {watching && locationFound && ( )} {haveHome && homePin && ( )} {/* map layers */} {geoCollection && ( { setLayerIDs(e); }} /> )} ) : ( ); }