ag visual farm added to the dev branch field related stuff not imported yet, and the geocoder has some bugs with the new version
This commit is contained in:
parent
b4da0d6859
commit
022925d7d7
41 changed files with 8895 additions and 79 deletions
704
src/maps/MapBase.tsx
Normal file
704
src/maps/MapBase.tsx
Normal file
|
|
@ -0,0 +1,704 @@
|
|||
import { useEffect, useRef, useState } from "react";
|
||||
import "mapbox-gl/dist/mapbox-gl.css";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
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";
|
||||
|
||||
//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<string, pond.GeoData>;
|
||||
measurementData?: Map<string, MeasurementData>;
|
||||
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<ViewData | undefined>();
|
||||
//const [viewport, setViewport] = useState<ViewData>(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<MapRef>(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<string[]>([]);
|
||||
const [geoCollection, setGeoCollection] = useState<FeatureCollection>();
|
||||
const [measurementCollection, setMeasurementCollection] = useState<FeatureCollection>();
|
||||
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)
|
||||
.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)
|
||||
.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 (
|
||||
<Box>
|
||||
<Box
|
||||
style={{
|
||||
marginLeft: 10,
|
||||
position: "absolute",
|
||||
top: isMobile ? "120px" : "7%",
|
||||
left: isMobile ? 0 : 75
|
||||
}}
|
||||
className="mapboxgl-ctrl-top-left mapboxgl-ctrl">
|
||||
{props.mapTools}
|
||||
</Box>
|
||||
<Box
|
||||
style={{
|
||||
marginRight: 10,
|
||||
position: "absolute",
|
||||
bottom: isMobile ? "7%" : "3%",
|
||||
right: 10
|
||||
}}
|
||||
className="mapboxgl-ctrl-bottom-right mapboxgl-ctrl">
|
||||
<Box>
|
||||
{dragAllowed && (
|
||||
<IconButton
|
||||
className={classes.iconButtons}
|
||||
style={
|
||||
groupSelect ? { background: "yellow" } : dcHovered ? { background: "grey" } : {}
|
||||
}
|
||||
title="Group Select"
|
||||
onClick={() => setGroupSelect(!groupSelect)}
|
||||
onMouseOver={() => setDCHovered(true)}
|
||||
onMouseOut={() => setDCHovered(false)}>
|
||||
<MarkerMove />
|
||||
</IconButton>
|
||||
)}
|
||||
</Box>
|
||||
<Grid container direction={isMobile ? "column" : "row"}>
|
||||
<Grid item>
|
||||
<IconButton
|
||||
className={classes.iconButtons}
|
||||
style={
|
||||
dragAllowed ? { background: "yellow" } : dcHovered ? { background: "grey" } : {}
|
||||
}
|
||||
title="Toggle Drag"
|
||||
onClick={() => {
|
||||
setGroupSelect(false);
|
||||
setDragAllowed(!dragAllowed);
|
||||
}}
|
||||
onMouseOver={() => setDCHovered(true)}
|
||||
onMouseOut={() => setDCHovered(false)}>
|
||||
<MarkerMove />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<IconButton
|
||||
className={classes.iconButtons}
|
||||
style={msHovered ? { background: "grey" } : {}}
|
||||
title="Map Style"
|
||||
onClick={() => setStyles(true)}
|
||||
onMouseOver={() => setMSHovered(true)}
|
||||
onMouseOut={() => setMSHovered(false)}>
|
||||
<Layers />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<IconButton
|
||||
className={classes.iconButtons}
|
||||
style={hcHovered ? { background: "grey" } : {}}
|
||||
title="Home Pin"
|
||||
onClick={homeControl}
|
||||
onMouseOver={() => setHCHovered(true)}
|
||||
onMouseOut={() => setHCHovered(false)}>
|
||||
<HomeIcon />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<IconButton
|
||||
className={classes.iconButtons}
|
||||
style={
|
||||
watching ? { background: "yellow" } : gcHovered ? { background: "grey" } : {}
|
||||
}
|
||||
title="Find Me"
|
||||
onClick={geolocateToggle}
|
||||
onMouseOver={() => setGCHovered(true)}
|
||||
onMouseOut={() => setGCHovered(false)}>
|
||||
<Icon className={locationFound ? "" : classes.geoIconSearch}>
|
||||
<GpsFixed
|
||||
classes={{
|
||||
colorSecondary: classes.geoIconColor
|
||||
}}
|
||||
color={watching ? "secondary" : "inherit"}
|
||||
/>
|
||||
</Icon>
|
||||
</IconButton>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const mapClick = (event: MapMouseEvent) => {
|
||||
if (!watching) {
|
||||
props.mapClick(event);
|
||||
}
|
||||
};
|
||||
|
||||
const mapStyleDialog = () => {
|
||||
return (
|
||||
<ResponsiveDialog open={styles} onClose={() => setStyles(false)}>
|
||||
<DialogTitle>Select Map Style</DialogTitle>
|
||||
<DialogContent>
|
||||
<Select
|
||||
fullWidth
|
||||
label="Style"
|
||||
value={mapStyle}
|
||||
onChange={e => setMapStyle(e.target.value as string)}>
|
||||
{mapStyleOptions.map(op => (
|
||||
<MenuItem key={op.value} value={op.value}>
|
||||
{op.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
{/* layer options from the object controller */}
|
||||
{props.layerOptions}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setStyles(false)}>Close</Button>
|
||||
</DialogActions>
|
||||
</ResponsiveDialog>
|
||||
);
|
||||
};
|
||||
|
||||
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 ? (
|
||||
<Box height="100%">
|
||||
{tools()}
|
||||
{mapStyleDialog()}
|
||||
<Map
|
||||
ref={mapRef}
|
||||
maxZoom={18}
|
||||
initialViewState={startingView}
|
||||
// {...viewport}
|
||||
mapStyle={mapStyle}
|
||||
onZoomEnd={e => {
|
||||
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 && (
|
||||
<Geocoder
|
||||
mapboxAccessToken={MAPBOX_TOKEN}
|
||||
position="top-right"
|
||||
customEntries={props.customSearchEntries}
|
||||
resultFunction={props.geocoderResultFunction}
|
||||
customTransition={props.geocoderTransitionFunction}
|
||||
/>
|
||||
)}
|
||||
<DrawController
|
||||
featureCollection={getEditableFeatures()}
|
||||
editMode={props.editorMode}
|
||||
onCreate={onCreate}
|
||||
onUpdate={onUpdate}
|
||||
measurement={props.drawMeasurement}
|
||||
linePointLimit={props.linePointLimit}
|
||||
/>
|
||||
{/* markers */}
|
||||
{props.markerData && (
|
||||
<Markers
|
||||
markerData={props.markerData}
|
||||
dragOn={dragAllowed}
|
||||
groupSelect={groupSelect}
|
||||
mapZoomLevel={currentZoom}
|
||||
displayDetails={props.displayMarkerDetails}
|
||||
showMarkerTitle={props.displayMarkerTitles}
|
||||
/>
|
||||
)}
|
||||
|
||||
{watching && locationFound && (
|
||||
<Marker longitude={geoPosition.longitude} latitude={geoPosition.latitude}>
|
||||
<Box className={classes.geoDot} />
|
||||
</Marker>
|
||||
)}
|
||||
{haveHome && homePin && (
|
||||
<Marker
|
||||
longitude={homePin.longitude}
|
||||
latitude={homePin.latitude}
|
||||
draggable
|
||||
onDragEnd={homeDragEnd}
|
||||
offset={[0, -25]}>
|
||||
<Box
|
||||
className={classes.pin}
|
||||
style={{ pointerEvents: "none", width: 50, height: 50, background: "red" }}>
|
||||
<Box
|
||||
style={{
|
||||
transform: "rotate(-45deg)",
|
||||
width: 30,
|
||||
height: 30,
|
||||
marginTop: -10,
|
||||
marginLeft: -10,
|
||||
pointerEvents: "none"
|
||||
}}>
|
||||
<HomeIcon type="light" />
|
||||
</Box>
|
||||
</Box>
|
||||
</Marker>
|
||||
)}
|
||||
{/* map layers */}
|
||||
{geoCollection && (
|
||||
<GeoMapLayer
|
||||
objectCollection={geoCollection}
|
||||
measurementCollection={measurementCollection}
|
||||
showTitle={props.objectTitles}
|
||||
setInteractiveLayers={e => {
|
||||
setLayerIDs(e);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Map>
|
||||
</Box>
|
||||
) : (
|
||||
<Box>
|
||||
<LinearProgress />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
99
src/maps/MapMarkerSettings.tsx
Normal file
99
src/maps/MapMarkerSettings.tsx
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
import {
|
||||
Box,
|
||||
Button,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
Slider,
|
||||
TextField
|
||||
} from "@mui/material";
|
||||
import ColourPicker from "common/ColourPicker";
|
||||
import ResponsiveDialog from "common/ResponsiveDialog";
|
||||
import { pond } from "protobuf-ts/pond";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
close: () => void;
|
||||
theme: pond.ObjectTheme;
|
||||
sizeControl?: boolean;
|
||||
currentSize?: number;
|
||||
colourControl?: boolean;
|
||||
updateObject: (newTheme: pond.ObjectTheme) => void;
|
||||
}
|
||||
|
||||
export default function MapMarkerSettings(props: Props) {
|
||||
const { theme, sizeControl, colourControl, updateObject, currentSize } = props;
|
||||
//const [objectSettings, setObjectSettings] = useState(props.objectSettings);
|
||||
const [newColour, setNewColour] = useState("");
|
||||
const [newSize, setNewSize] = useState(50);
|
||||
const markerMin = 40;
|
||||
const markerMax = 125;
|
||||
|
||||
useEffect(() => {
|
||||
if (currentSize) {
|
||||
setNewSize(currentSize);
|
||||
} else {
|
||||
setNewSize(50);
|
||||
}
|
||||
}, [currentSize]);
|
||||
|
||||
const reSize = (val: number) => {
|
||||
let size = val;
|
||||
if (val > markerMax) {
|
||||
size = markerMax;
|
||||
}
|
||||
if (val < markerMin) {
|
||||
size = markerMin;
|
||||
}
|
||||
setNewSize(size);
|
||||
};
|
||||
|
||||
const update = () => {
|
||||
let newTheme = theme;
|
||||
newTheme.color = newColour;
|
||||
newTheme.height = newSize;
|
||||
newTheme.width = newSize;
|
||||
updateObject(newTheme);
|
||||
};
|
||||
|
||||
return (
|
||||
<ResponsiveDialog open={props.open} onClose={props.close}>
|
||||
<DialogTitle>Change Marker Settings</DialogTitle>
|
||||
<DialogContent>
|
||||
{sizeControl && (
|
||||
<Box paddingTop={1.5}>
|
||||
Marker Size
|
||||
<Slider
|
||||
value={newSize}
|
||||
min={markerMin}
|
||||
max={markerMax}
|
||||
onChange={(e, val) => reSize(val as number)}
|
||||
valueLabelDisplay="auto"
|
||||
/>
|
||||
<TextField type="number" value={newSize} onChange={e => reSize(+e.target.value)} />
|
||||
</Box>
|
||||
)}
|
||||
{colourControl && (
|
||||
<Box>
|
||||
Marker Colour
|
||||
<ColourPicker colour={newColour} onChange={color => setNewColour(color)} />
|
||||
</Box>
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button style={{ color: "red" }} onClick={props.close}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
update();
|
||||
props.close();
|
||||
}}>
|
||||
Update Marker
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</ResponsiveDialog>
|
||||
);
|
||||
}
|
||||
146
src/maps/mapControllers/Geocoder.tsx
Normal file
146
src/maps/mapControllers/Geocoder.tsx
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
import { ControlPosition, useMap } from "react-map-gl/mapbox-legacy";
|
||||
import MapboxGeocoder, { Result } from "@mapbox/mapbox-gl-geocoder";
|
||||
import { useEffect } from "react";
|
||||
|
||||
// export interface GeocoderObject {
|
||||
// id: string;
|
||||
// place_name: string;
|
||||
// center: number[];
|
||||
// place_type: string[];
|
||||
// }
|
||||
|
||||
// interface GeocoderResult {
|
||||
// result: GeocoderObject;
|
||||
// }
|
||||
interface Props {
|
||||
mapboxAccessToken: string;
|
||||
position: ControlPosition;
|
||||
customEntries?: Result[];
|
||||
customTransition?: (objectResult: Result) => void;
|
||||
resultFunction?: (objectResult: Result) => void;
|
||||
}
|
||||
|
||||
//const MAPBOX_TOKEN = process.env.REACT_APP_MAPBOX_ACCESS_TOKEN;
|
||||
// const MAPBOX_TOKEN = import.meta.env.VITE_MAPBOX_ACCESS_TOKEN;
|
||||
|
||||
// const geoCoder = new MapboxGeocoder({
|
||||
// accessToken: MAPBOX_TOKEN ?? "",
|
||||
// flyTo: true,
|
||||
// marker: false,
|
||||
// zoom: 18
|
||||
// });
|
||||
|
||||
export default function Geocoder(props: Props) {
|
||||
const { mapboxAccessToken, position, customEntries, resultFunction, customTransition } = props;
|
||||
const { current: map } = useMap();
|
||||
|
||||
const localSearch = (query: string) => {
|
||||
const matchesCoords = query.match(
|
||||
/^[ ]*(?:Lat: )?(-?\d+\.?\d*)[, ]+(?:Lng: )?(-?\d+\.?\d*)[ ]*$/i
|
||||
);
|
||||
let matchingResults: Result[] = [];
|
||||
if (matchesCoords) {
|
||||
const coord1 = Number(matchesCoords[1]);
|
||||
const coord2 = Number(matchesCoords[2]);
|
||||
const matchingCoords: Result[] = [];
|
||||
|
||||
if (coord1 < -90 || coord1 > 90) {
|
||||
// coord1 is longitude
|
||||
matchingCoords.push({
|
||||
id: "Lat: " + coord2 + " Lng: " + coord1,
|
||||
place_name: "Lat: " + coord2 + " Lng: " + coord1,
|
||||
center: [coord1, coord2],
|
||||
place_type: ["coordinate"]
|
||||
} as Result);
|
||||
}
|
||||
|
||||
if (coord2 < -90 || coord2 > 90) {
|
||||
// coord2 is longitude
|
||||
matchingCoords.push({
|
||||
id: "Lat: " + coord1 + " Lng: " + coord2,
|
||||
place_name: "Lat: " + coord1 + " Lng: " + coord2,
|
||||
center: [coord2, coord1],
|
||||
place_type: ["coordinate"]
|
||||
} as Result);
|
||||
}
|
||||
|
||||
if (matchingCoords.length === 0) {
|
||||
// else could be either lng, lat or lat, lng
|
||||
matchingCoords.push({
|
||||
id: "Lat: " + coord2 + " Lng: " + coord1,
|
||||
place_name: "Lat: " + coord2 + " Lng: " + coord1,
|
||||
center: [coord1, coord2],
|
||||
place_type: ["coordinate"]
|
||||
} as Result);
|
||||
matchingCoords.push({
|
||||
id: "Lat: " + coord1 + " Lng: " + coord2,
|
||||
place_name: "Lat: " + coord1 + " Lng: " + coord2,
|
||||
center: [coord2, coord1],
|
||||
place_type: ["coordinate"]
|
||||
} as Result);
|
||||
}
|
||||
|
||||
matchingResults = matchingResults.concat(matchingCoords);
|
||||
}
|
||||
|
||||
customEntries?.forEach(obj => {
|
||||
if (obj.place_name.toLowerCase().includes(query.toLowerCase())) {
|
||||
matchingResults.push(obj);
|
||||
}
|
||||
});
|
||||
return matchingResults;
|
||||
};
|
||||
|
||||
// //set the geocoder options
|
||||
// useEffect(() => {
|
||||
// //connects the localSearch function to the geocoder if there are custom entries passed in
|
||||
// if (customEntries) {
|
||||
// //need to determine a new way to handle custom search with updated geocoder
|
||||
// //geoCoder.options.localGeocoder = localSearch;
|
||||
// }
|
||||
// //disables the transition that the geocoder would fire if a custom transition was passed in
|
||||
// if (customTransition) {
|
||||
// geoCoder.setFlyTo(false);
|
||||
// }
|
||||
// }, [customEntries, customTransition]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
//attach custom functions to event handlers
|
||||
useEffect(() => {
|
||||
const geoCoder = new MapboxGeocoder({
|
||||
accessToken: mapboxAccessToken,
|
||||
localGeocoder: localSearch,
|
||||
flyTo: customTransition ? false : true,
|
||||
marker: false,
|
||||
zoom: 18,
|
||||
});
|
||||
//events that fire as the search bar is typed into, note the function will run AFTER the event fires
|
||||
|
||||
//event is fired when the search bar is cleared
|
||||
geoCoder.on("clear", () => {});
|
||||
|
||||
//event is fired when starting to load the results
|
||||
geoCoder.on("loading", () => {});
|
||||
|
||||
//event is fired when the results come back from a search
|
||||
geoCoder.on("results", () => {});
|
||||
|
||||
//event is fired when there is an error
|
||||
geoCoder.on("error", () => {});
|
||||
|
||||
//event is fired when an option is selected from the results
|
||||
geoCoder.on("result", (res: Result) => {
|
||||
if (resultFunction) {
|
||||
resultFunction(res);
|
||||
}
|
||||
if (customTransition) {
|
||||
customTransition(res);
|
||||
}
|
||||
});
|
||||
//once the map ref and access token are set up add the control to the map
|
||||
if (map && mapboxAccessToken) {
|
||||
map.addControl(geoCoder, position);
|
||||
}
|
||||
}, [map, mapboxAccessToken, position]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
return null;
|
||||
}
|
||||
112
src/maps/mapControllers/drawController.tsx
Normal file
112
src/maps/mapControllers/drawController.tsx
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
import { useEffect, useRef } from "react";
|
||||
import { useMap } from "react-map-gl/mapbox-legacy";
|
||||
import { FeatureCollection } from "geojson";
|
||||
import MapboxDraw from "@mapbox/mapbox-gl-draw";
|
||||
|
||||
interface Props {
|
||||
featureCollection: FeatureCollection;
|
||||
editMode?: "drawPolygon" | "drawLine" | "drawPoint" | "edit" | "none" | "delete";
|
||||
linePointLimit?: number;
|
||||
measurement?: boolean;
|
||||
onCreate: (event: any, objectToCut?: string, measurement?: boolean) => void;
|
||||
onUpdate: (event: any) => void;
|
||||
}
|
||||
const draw = new MapboxDraw({
|
||||
displayControlsDefault: false
|
||||
});
|
||||
|
||||
export default function DrawController(props: Props) {
|
||||
const { featureCollection, editMode, onCreate, onUpdate, linePointLimit, measurement } = props;
|
||||
//const [shapeToCut, setShapeToCut] = useState();
|
||||
const cutRef = useRef(); //the object id for what shape to cut a hole
|
||||
const mRef = useRef<boolean>();
|
||||
const lineLimitRef = useRef<number | undefined>();
|
||||
const pointsRef = useRef<number[][]>([]);
|
||||
const { current: map } = useMap();
|
||||
|
||||
//use effect is just for adding and removing the controller from the map
|
||||
useEffect(() => {
|
||||
if (map) {
|
||||
if (editMode && editMode !== "none" && !map.hasControl(draw)) {
|
||||
map.addControl(draw);
|
||||
} else if ((!editMode || editMode === "none") && map.hasControl(draw)) {
|
||||
map.removeControl(draw);
|
||||
}
|
||||
}
|
||||
}, [map, editMode]);
|
||||
|
||||
useEffect(() => {
|
||||
lineLimitRef.current = linePointLimit;
|
||||
mRef.current = measurement;
|
||||
}, [linePointLimit, measurement]);
|
||||
|
||||
//useEffect for adding event handlers to the map
|
||||
useEffect(() => {
|
||||
if (map) {
|
||||
map.on("draw.create", e => {
|
||||
//console.log("draw create");
|
||||
//using refs rather than state variables you can effectively get the state in an event listener
|
||||
onCreate(e, cutRef.current, mRef.current);
|
||||
});
|
||||
map.on("draw.update", e => {
|
||||
onUpdate(e);
|
||||
});
|
||||
map.on("click", ["shapefill", "shapeborder"], e => {
|
||||
//set the ref to be undefined
|
||||
cutRef.current = undefined;
|
||||
if (map.hasControl(draw) && e.features) {
|
||||
let mode = draw.getMode();
|
||||
let id = e.features[0].properties?.objectKey;
|
||||
if (mode === "simple_select" && id) {
|
||||
if (draw.get(id)) {
|
||||
draw.changeMode("direct_select", {
|
||||
// The id of the feature that will be directly selected (required)
|
||||
featureId: id
|
||||
});
|
||||
}
|
||||
} else if (mode === "draw_polygon") {
|
||||
//if a feature was clicked on set ref to have its id
|
||||
cutRef.current = id;
|
||||
//setShapeToCut(id);
|
||||
}
|
||||
}
|
||||
});
|
||||
map.on("click", e => {
|
||||
if (map.hasControl(draw) && draw.getMode() === "draw_line_string") {
|
||||
if (lineLimitRef.current) {
|
||||
pointsRef.current.push([e.lngLat.lng, e.lngLat.lat]);
|
||||
if (pointsRef.current.length === lineLimitRef.current) {
|
||||
draw.changeMode("simple_select"); //changing the mode causes the create event to fire
|
||||
pointsRef.current = [];
|
||||
draw.changeMode("draw_line_string");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [map]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
//useEffect for changing draw mode for the controller based on the editor mode from the object controller
|
||||
useEffect(() => {
|
||||
if (editMode === "drawPolygon") {
|
||||
draw.deleteAll();
|
||||
draw.changeMode("draw_polygon");
|
||||
} else if (editMode === "drawLine") {
|
||||
draw.deleteAll();
|
||||
draw.changeMode("draw_line_string");
|
||||
} else if (editMode === "drawPoint") {
|
||||
draw.deleteAll();
|
||||
draw.changeMode("draw_point");
|
||||
} else if (editMode === "delete") {
|
||||
draw.deleteAll();
|
||||
draw.changeMode("simple_select");
|
||||
} else if (editMode === "edit") {
|
||||
draw.set(featureCollection);
|
||||
if (draw.getMode() !== "simple_select" && draw.getMode() !== "direct_select") {
|
||||
draw.changeMode("simple_select");
|
||||
}
|
||||
}
|
||||
}, [editMode, featureCollection]);
|
||||
|
||||
return null;
|
||||
}
|
||||
148
src/maps/mapDrawers/BinDrawer.tsx
Normal file
148
src/maps/mapDrawers/BinDrawer.tsx
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
import { Box } from "@mui/material";
|
||||
import DisplayDrawer from "common/DisplayDrawer";
|
||||
import MapMarkerSettings from "maps/MapMarkerSettings";
|
||||
import { Bin as IBin } from "models";
|
||||
import Bin from "pages/Bin";
|
||||
import { pond } from "protobuf-ts/pond";
|
||||
import { useBinAPI, useSnackbar } from "providers";
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
selectedBin: string;
|
||||
bins: Map<string, IBin>;
|
||||
removeMarker: (key: string) => void;
|
||||
updateMarker: (key: string, newSettings: pond.BinSettings) => void;
|
||||
moveMap: (long: number, lat: number) => void;
|
||||
}
|
||||
|
||||
export default function BinDrawer(props: Props) {
|
||||
const { open, onClose, selectedBin, bins, removeMarker, updateMarker, moveMap } = props;
|
||||
const [bin, setBin] = useState<IBin>(IBin.create());
|
||||
const [openMarkerSettings, setOpenMarkerSettings] = useState(false);
|
||||
const binAPI = useBinAPI();
|
||||
const { openSnack } = useSnackbar();
|
||||
|
||||
useEffect(() => {
|
||||
let b = bins.get(selectedBin);
|
||||
if (b) {
|
||||
setBin(b);
|
||||
}
|
||||
}, [selectedBin, bins]);
|
||||
|
||||
const closeDrawer = () => {
|
||||
onClose();
|
||||
};
|
||||
|
||||
const displayNext = () => {
|
||||
let binArr = Array.from(bins.values());
|
||||
let index = binArr.indexOf(bin);
|
||||
let found = false;
|
||||
do {
|
||||
if (index === binArr.length - 1) {
|
||||
index = 0;
|
||||
} else {
|
||||
index++;
|
||||
}
|
||||
let nextBin = binArr[index];
|
||||
let location = nextBin.location();
|
||||
if (
|
||||
location !== null &&
|
||||
location !== undefined &&
|
||||
location.longitude !== 0 &&
|
||||
location.longitude !== 0
|
||||
) {
|
||||
setBin(nextBin);
|
||||
moveMap(location.longitude, location.latitude);
|
||||
found = true;
|
||||
}
|
||||
} while (!found);
|
||||
};
|
||||
|
||||
const displayPrev = () => {
|
||||
let binArr = Array.from(bins.values());
|
||||
let index = binArr.indexOf(bin);
|
||||
let found = false;
|
||||
do {
|
||||
if (index === 0) {
|
||||
index = binArr.length - 1;
|
||||
} else {
|
||||
index--;
|
||||
}
|
||||
let nextBin = binArr[index];
|
||||
let location = nextBin.location();
|
||||
if (
|
||||
location !== null &&
|
||||
location !== undefined &&
|
||||
location.longitude !== 0 &&
|
||||
location.longitude !== 0
|
||||
) {
|
||||
setBin(nextBin);
|
||||
moveMap(location.longitude, location.latitude);
|
||||
found = true;
|
||||
}
|
||||
} while (!found);
|
||||
};
|
||||
|
||||
const drawerBody = () => {
|
||||
return <Box>{bin.key() !== "" && <Bin binKey={bin.key()} displayMobile fromMap />}</Box>;
|
||||
};
|
||||
|
||||
const remove = () => {
|
||||
let settings = bin.settings;
|
||||
settings.location = null;
|
||||
|
||||
binAPI
|
||||
.updateBin(bin.key(), settings)
|
||||
.then(resp => {
|
||||
openSnack("Removed bin marker");
|
||||
removeMarker(bin.key());
|
||||
})
|
||||
.catch(err => {
|
||||
openSnack("Failed to remove bin marker");
|
||||
});
|
||||
};
|
||||
|
||||
const update = () => {
|
||||
setOpenMarkerSettings(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DisplayDrawer
|
||||
open={open}
|
||||
onClose={closeDrawer}
|
||||
displayNext={displayNext}
|
||||
displayPrev={displayPrev}
|
||||
title={bin.name()}
|
||||
width={"40vw"}
|
||||
drawerBody={drawerBody()}
|
||||
updateElement={update}
|
||||
removeElement={remove}
|
||||
/>
|
||||
<MapMarkerSettings
|
||||
close={() => {
|
||||
setOpenMarkerSettings(false);
|
||||
}}
|
||||
open={openMarkerSettings}
|
||||
theme={bin.settings.theme ?? pond.ObjectTheme.create()}
|
||||
sizeControl
|
||||
currentSize={bin.settings.theme?.height}
|
||||
updateObject={newTheme => {
|
||||
let settings = bin.settings;
|
||||
settings.theme = newTheme;
|
||||
binAPI
|
||||
.updateBin(bin.key(), settings)
|
||||
.then(resp => {
|
||||
openSnack("marker settings updated");
|
||||
updateMarker(bin.key(), settings);
|
||||
})
|
||||
.catch(() => {
|
||||
openSnack("failed to update marker settings");
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
153
src/maps/mapDrawers/BinYardDrawer.tsx
Normal file
153
src/maps/mapDrawers/BinYardDrawer.tsx
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
import BinyardDisplay from "bin/BinyardDisplay";
|
||||
import DisplayDrawer from "common/DisplayDrawer";
|
||||
import MapMarkerSettings from "maps/MapMarkerSettings";
|
||||
//import Bins from "pages/Bins";
|
||||
import { pond } from "protobuf-ts/pond";
|
||||
import { useBinYardAPI, useSnackbar } from "providers";
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
selectedYard: string;
|
||||
yards: Map<string, pond.BinYardSettings>;
|
||||
removeMarker: (key: string) => void;
|
||||
updateMarker: (key: string, objectSettings: pond.BinYardSettings) => void;
|
||||
moveMap: (long: number, lat: number) => void;
|
||||
}
|
||||
|
||||
export default function BinYardDrawer(props: Props) {
|
||||
const { open, onClose, selectedYard, yards, removeMarker, updateMarker, moveMap } = props;
|
||||
const [yard, setYard] = useState<pond.BinYardSettings>(pond.BinYardSettings.create());
|
||||
const [openMarkerSettings, setOpenMarkerSettings] = useState(false);
|
||||
const { openSnack } = useSnackbar();
|
||||
const binyardAPI = useBinYardAPI();
|
||||
|
||||
useEffect(() => {
|
||||
let yard = yards.get(selectedYard);
|
||||
if (yard) {
|
||||
setYard(yard);
|
||||
}
|
||||
}, [selectedYard, yards]);
|
||||
|
||||
const closeDrawer = () => {
|
||||
onClose();
|
||||
};
|
||||
|
||||
const displayNext = () => {
|
||||
let yardArr = Array.from(yards.values());
|
||||
let index = yardArr.indexOf(yard);
|
||||
let found = false;
|
||||
do {
|
||||
if (index === yardArr.length - 1) {
|
||||
index = 0;
|
||||
} else {
|
||||
index++;
|
||||
}
|
||||
let nextYard = yardArr[index];
|
||||
if (
|
||||
nextYard.latitude !== 0 &&
|
||||
nextYard.longitude !== 0 &&
|
||||
nextYard.latitude !== undefined &&
|
||||
nextYard.longitude !== undefined
|
||||
) {
|
||||
setYard(nextYard);
|
||||
moveMap(nextYard.longitude, nextYard.latitude);
|
||||
found = true;
|
||||
}
|
||||
} while (!found);
|
||||
};
|
||||
|
||||
const displayPrev = () => {
|
||||
let yardArr = Array.from(yards.values());
|
||||
let index = yardArr.indexOf(yard);
|
||||
let found = false;
|
||||
do {
|
||||
if (index === 0) {
|
||||
index = yardArr.length - 1;
|
||||
} else {
|
||||
index--;
|
||||
}
|
||||
let nextYard = yardArr[index];
|
||||
if (
|
||||
nextYard.latitude !== 0 &&
|
||||
nextYard.longitude !== 0 &&
|
||||
nextYard.latitude !== undefined &&
|
||||
nextYard.longitude !== undefined
|
||||
) {
|
||||
setYard(nextYard);
|
||||
moveMap(nextYard.longitude, nextYard.latitude);
|
||||
found = true;
|
||||
}
|
||||
} while (!found);
|
||||
};
|
||||
|
||||
const drawerBody = () => {
|
||||
//return <Bins insert yardFilter={yard.key} />;
|
||||
return <BinyardDisplay insert yardKey={yard.key} />;
|
||||
};
|
||||
|
||||
/**
|
||||
* function to remove the marker and coordinates from the object
|
||||
*/
|
||||
const remove = () => {
|
||||
//set the long/lat of the yard to 0 and call an update
|
||||
let settings = yard;
|
||||
settings.longitude = 0;
|
||||
settings.latitude = 0;
|
||||
binyardAPI
|
||||
.updateBinYard(yard.key, settings)
|
||||
.then(resp => {
|
||||
openSnack("Marker Removed");
|
||||
//then use the removeMarker prop function to update the markers in the parent map
|
||||
removeMarker(yard.key);
|
||||
})
|
||||
.catch(resp => {
|
||||
openSnack("there was a problem removing the marker");
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* function to update the settings of the marker
|
||||
*/
|
||||
const update = () => {
|
||||
setOpenMarkerSettings(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DisplayDrawer
|
||||
open={open}
|
||||
onClose={closeDrawer}
|
||||
displayNext={displayNext}
|
||||
displayPrev={displayPrev}
|
||||
title={yard.name}
|
||||
width={"40vw"}
|
||||
drawerBody={drawerBody()}
|
||||
updateElement={update}
|
||||
removeElement={remove}
|
||||
/>
|
||||
<MapMarkerSettings
|
||||
close={() => {
|
||||
setOpenMarkerSettings(false);
|
||||
}}
|
||||
open={openMarkerSettings}
|
||||
theme={yard.theme ?? pond.ObjectTheme.create()}
|
||||
colourControl
|
||||
updateObject={newTheme => {
|
||||
let settings = yard;
|
||||
settings.theme = newTheme;
|
||||
binyardAPI
|
||||
.updateBinYard(yard.key, settings)
|
||||
.then(resp => {
|
||||
openSnack("marker settings updated");
|
||||
updateMarker(yard.key, settings);
|
||||
})
|
||||
.catch(() => {
|
||||
openSnack("failed to update marker settings");
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
153
src/maps/mapDrawers/DeviceDrawer.tsx
Normal file
153
src/maps/mapDrawers/DeviceDrawer.tsx
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
import { Box } from "@mui/material";
|
||||
import DisplayDrawer from "common/DisplayDrawer";
|
||||
import DeviceViewer from "device/DeviceViewer";
|
||||
import MapMarkerSettings from "maps/MapMarkerSettings";
|
||||
import { Device } from "models";
|
||||
import { pond } from "protobuf-ts/pond";
|
||||
import { useDeviceAPI, useSnackbar } from "providers";
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
selectedDevice: string;
|
||||
devices: Map<string, Device>;
|
||||
removeMarker: (key: string) => void;
|
||||
updateMarker: (key: string, newSettings: pond.DeviceSettings) => void;
|
||||
moveMap: (long: number, lat: number) => void;
|
||||
}
|
||||
|
||||
export default function DeviceDrawer(props: Props) {
|
||||
const { open, onClose, selectedDevice, devices, removeMarker, updateMarker, moveMap } = props;
|
||||
const [device, setDevice] = useState<Device>(Device.create());
|
||||
const [openMarkerSettings, setOpenMarkerSettings] = useState(false);
|
||||
const deviceAPI = useDeviceAPI();
|
||||
const { openSnack } = useSnackbar();
|
||||
|
||||
useEffect(() => {
|
||||
let d = devices.get(selectedDevice);
|
||||
if (d) {
|
||||
setDevice(d);
|
||||
}
|
||||
}, [selectedDevice, devices]);
|
||||
|
||||
const closeDrawer = () => {
|
||||
onClose();
|
||||
};
|
||||
|
||||
const displayNext = () => {
|
||||
let devArr = Array.from(devices.values());
|
||||
let index = devArr.indexOf(device);
|
||||
let found = false;
|
||||
do {
|
||||
if (index === devArr.length - 1) {
|
||||
index = 0;
|
||||
} else {
|
||||
index++;
|
||||
}
|
||||
let nextDev = devArr[index];
|
||||
if (
|
||||
nextDev.location().longitude !== 0 &&
|
||||
nextDev.location().longitude !== 0 &&
|
||||
nextDev.location().latitude !== undefined &&
|
||||
nextDev.location().longitude !== undefined
|
||||
) {
|
||||
setDevice(nextDev);
|
||||
moveMap(nextDev.location().longitude, nextDev.location().latitude);
|
||||
found = true;
|
||||
}
|
||||
} while (!found);
|
||||
};
|
||||
|
||||
const displayPrev = () => {
|
||||
let devArr = Array.from(devices.values());
|
||||
let index = devArr.indexOf(device);
|
||||
let found = false;
|
||||
do {
|
||||
if (index === 0) {
|
||||
index = devArr.length - 1;
|
||||
} else {
|
||||
index--;
|
||||
}
|
||||
let nextDev = devArr[index];
|
||||
if (
|
||||
nextDev.location().longitude !== 0 &&
|
||||
nextDev.location().longitude !== 0 &&
|
||||
nextDev.location().latitude !== undefined &&
|
||||
nextDev.location().longitude !== undefined
|
||||
) {
|
||||
setDevice(nextDev);
|
||||
moveMap(nextDev.location().longitude, nextDev.location().latitude);
|
||||
found = true;
|
||||
}
|
||||
} while (!found);
|
||||
};
|
||||
|
||||
const drawerBody = () => {
|
||||
return device.id() !== 0 ? <DeviceViewer device={device} isMobile={true} /> : <Box></Box>;
|
||||
};
|
||||
|
||||
/**
|
||||
* function to remove the marker and coordinates from the object
|
||||
*/
|
||||
const remove = () => {
|
||||
//set the long/lat of the yard to 0 and call an update
|
||||
let settings = device.settings;
|
||||
settings.longitude = 0;
|
||||
settings.latitude = 0;
|
||||
deviceAPI
|
||||
.update(device.id(), settings)
|
||||
.then(resp => {
|
||||
openSnack("Marker Removed");
|
||||
//then use the removeMarker prop function to update the markers in the parent map
|
||||
removeMarker(device.id().toString());
|
||||
})
|
||||
.catch(err => {
|
||||
openSnack("Failed to remove marker");
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* function to update the settings of the marker
|
||||
*/
|
||||
const update = () => {
|
||||
setOpenMarkerSettings(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DisplayDrawer
|
||||
open={open}
|
||||
onClose={closeDrawer}
|
||||
displayNext={displayNext}
|
||||
displayPrev={displayPrev}
|
||||
title={device.name()}
|
||||
width={"40vw"}
|
||||
drawerBody={drawerBody()}
|
||||
updateElement={update}
|
||||
removeElement={remove}
|
||||
/>
|
||||
<MapMarkerSettings
|
||||
close={() => {
|
||||
setOpenMarkerSettings(false);
|
||||
}}
|
||||
open={openMarkerSettings}
|
||||
theme={device.settings.theme ?? pond.ObjectTheme.create()}
|
||||
colourControl
|
||||
updateObject={newTheme => {
|
||||
let settings = device.settings;
|
||||
settings.theme = newTheme;
|
||||
deviceAPI
|
||||
.update(device.id(), settings)
|
||||
.then(resp => {
|
||||
openSnack("marker settings updated");
|
||||
updateMarker(device.id().toString(), settings);
|
||||
})
|
||||
.catch(() => {
|
||||
openSnack("failed to update marker settings");
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
326
src/maps/mapDrawers/FieldDrawer.tsx
Normal file
326
src/maps/mapDrawers/FieldDrawer.tsx
Normal file
|
|
@ -0,0 +1,326 @@
|
|||
import {
|
||||
Box,
|
||||
Divider,
|
||||
Drawer,
|
||||
Grid2 as Grid,
|
||||
IconButton,
|
||||
Tab,
|
||||
Tabs,
|
||||
Theme,
|
||||
Typography
|
||||
} from "@mui/material";
|
||||
import { Notes } from "@mui/icons-material";
|
||||
import DisplayDrawer from "common/DisplayDrawer";
|
||||
//import HarvestPlanDisplay from "harvestPlan/HarvestPlanDisplay";
|
||||
import { Field, fieldScope, /*HarvestPlan,*/ teamScope } from "models";
|
||||
import React, { useEffect, useState } from "react";
|
||||
//import TaskViewer from "tasks/TaskViewer";
|
||||
import Weather from "weather/weather";
|
||||
import { getThemeType } from "theme";
|
||||
import GrainDescriber from "grain/GrainDescriber";
|
||||
import Chat from "chat/Chat";
|
||||
import { pond } from "protobuf-ts/pond";
|
||||
import { useMobile } from "hooks";
|
||||
import { useGlobalState, /*useHarvestPlanAPI,*/ useUserAPI } from "providers";
|
||||
import { makeStyles } from "@mui/styles";
|
||||
//import FieldActions from "field/FieldActions";
|
||||
|
||||
interface TabPanelProps {
|
||||
children?: React.ReactNode;
|
||||
index: any;
|
||||
value: any;
|
||||
}
|
||||
|
||||
function TabPanelMine(props: TabPanelProps) {
|
||||
const { children, value, index, ...other } = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
role="tabpanel"
|
||||
hidden={value !== index}
|
||||
aria-labelledby={`simple-tab-${index}`}
|
||||
style={{ height: "94%", paddingTop: "15px" }}
|
||||
{...other}>
|
||||
{value === index && <React.Fragment>{children}</React.Fragment>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
interface Props {
|
||||
open: boolean;
|
||||
closeDrawer: () => void;
|
||||
selectedFieldKey: string;
|
||||
fields: Map<string, Field>;
|
||||
openSettings: (fieldKey: string) => void;
|
||||
moveMap: (long: number, lat: number) => void;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) => ({
|
||||
avatar: {
|
||||
color: getThemeType() === "light" ? theme.palette.common.black : theme.palette.common.white,
|
||||
backgroundColor: "transparent",
|
||||
width: theme.spacing(5),
|
||||
height: theme.spacing(5),
|
||||
border: "1px solid",
|
||||
borderColor: theme.palette.divider
|
||||
},
|
||||
dark: {
|
||||
backgroundColor: getThemeType() === "light" ? "rgb(245, 245, 245)" : "rgb(50, 50, 50)",
|
||||
padding: 5
|
||||
},
|
||||
light: {
|
||||
backgroundColor: getThemeType() === "light" ? "rgb(235, 235, 235)" : "rgb(60, 60, 60)",
|
||||
padding: 5
|
||||
}
|
||||
|
||||
}));
|
||||
|
||||
export default function FieldDrawer(props: Props) {
|
||||
const { open, closeDrawer, selectedFieldKey, fields, moveMap } = props;
|
||||
const [field, setField] = useState<Field>(Field.create());
|
||||
//const [hPlan, setHPlan] = useState<HarvestPlan>(HarvestPlan.create());
|
||||
const [value, setValue] = useState(0);
|
||||
const classes = useStyles();
|
||||
const [openNote, setOpenNote] = useState(false);
|
||||
const isMobile = useMobile();
|
||||
const [{ as, user }] = useGlobalState();
|
||||
//const hPlanAPI = useHarvestPlanAPI();
|
||||
const [planLoading, setPlanLoading] = useState(false);
|
||||
const userAPI = useUserAPI();
|
||||
const [permissions, setPermissions] = useState<pond.Permission[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
let field = fields.get(selectedFieldKey);
|
||||
if (field) {
|
||||
setField(field);
|
||||
}
|
||||
}, [selectedFieldKey, fields]);
|
||||
|
||||
useEffect(() => {
|
||||
let scope;
|
||||
if (as) {
|
||||
//if they are viewing as a team regardless of whether the field is adaptive or external (from JD or CNH) get the users permission to the team
|
||||
scope = teamScope(as);
|
||||
} else if (
|
||||
field.settings.fieldGeoData &&
|
||||
field.settings.fieldGeoData.origin === pond.DataOrigin.DATA_ORIGIN_ADAPTIVE
|
||||
) {
|
||||
//if they are viewing as a user and the field is an adaptive field get the users permission to the field
|
||||
scope = fieldScope(field.key());
|
||||
}
|
||||
|
||||
//if the scope was set get the permissions the user has in that scope
|
||||
if (scope) {
|
||||
userAPI.getUser(user.id(), scope).then(resp => {
|
||||
field.permissions = resp.permissions;
|
||||
setPermissions(resp.permissions);
|
||||
});
|
||||
} else {
|
||||
//if scope wasn't set that means that they are viewing an external field as a user and so will have full permissions
|
||||
//because they would have full permissions to the organization that the field is linked to since users cannot share organizations
|
||||
//and they are only linked to team/user combinations during creation and cannot be shared afterward
|
||||
let perms = [
|
||||
pond.Permission.PERMISSION_READ,
|
||||
pond.Permission.PERMISSION_WRITE,
|
||||
pond.Permission.PERMISSION_SHARE,
|
||||
pond.Permission.PERMISSION_USERS
|
||||
];
|
||||
field.permissions = perms;
|
||||
setPermissions(perms);
|
||||
}
|
||||
}, [as, user, userAPI, field]); //eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
// useEffect(() => {
|
||||
// if (field.key() !== "") {
|
||||
// hPlanAPI
|
||||
// .listHarvestPlans(1, 0, "desc", "createDate", field.key(), as)
|
||||
// .then(resp => {
|
||||
// if (resp.data.harvestPlan.length > 0) {
|
||||
// let plan = resp.data.harvestPlan[0];
|
||||
// setHPlan(HarvestPlan.any(plan));
|
||||
// } else {
|
||||
// setHPlan(HarvestPlan.create());
|
||||
// }
|
||||
// setPlanLoading(false);
|
||||
// })
|
||||
// .catch(err => {
|
||||
// //openSnack("Failed to load plan");
|
||||
// });
|
||||
// }
|
||||
// }, [field, as, hPlanAPI]);
|
||||
|
||||
const handleChange = (event: React.ChangeEvent<{}>, newValue: number) => {
|
||||
setValue(newValue);
|
||||
};
|
||||
|
||||
const displayNext = () => {
|
||||
let arr = Array.from(fields.values());
|
||||
let index = arr.indexOf(field);
|
||||
let found = false;
|
||||
do {
|
||||
if (index === arr.length - 1) {
|
||||
index = 0;
|
||||
} else {
|
||||
index++;
|
||||
}
|
||||
let nextField = arr[index];
|
||||
let location = nextField.center();
|
||||
if (
|
||||
location !== null &&
|
||||
location !== undefined &&
|
||||
location.longitude !== 0 &&
|
||||
location.longitude !== 0
|
||||
) {
|
||||
setField(nextField);
|
||||
moveMap(location.longitude, location.latitude);
|
||||
found = true;
|
||||
}
|
||||
} while (!found);
|
||||
};
|
||||
|
||||
const displayPrev = () => {
|
||||
let arr = Array.from(fields.values());
|
||||
let index = arr.indexOf(field);
|
||||
let found = false;
|
||||
do {
|
||||
if (index === 0) {
|
||||
index = arr.length - 1;
|
||||
} else {
|
||||
index--;
|
||||
}
|
||||
let nextField = arr[index];
|
||||
let location = nextField.center();
|
||||
if (
|
||||
location !== null &&
|
||||
location !== undefined &&
|
||||
location.longitude !== 0 &&
|
||||
location.longitude !== 0
|
||||
) {
|
||||
setField(nextField);
|
||||
moveMap(location.longitude, location.latitude);
|
||||
found = true;
|
||||
}
|
||||
} while (!found);
|
||||
};
|
||||
|
||||
const drawerBody = () => {
|
||||
let taskLoadKeys: string[] = [];
|
||||
if (!planLoading) {
|
||||
field.key() !== "" && taskLoadKeys.push(field.key());
|
||||
//hPlan.key() !== "" && taskLoadKeys.push(hPlan.key());
|
||||
}
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Tabs
|
||||
style={{ position: "sticky" }}
|
||||
value={value}
|
||||
centered
|
||||
onChange={handleChange}
|
||||
TabIndicatorProps={{ style: { background: "rgba(255,255,0,255)" } }}>
|
||||
<Tab label="Field Overview" />
|
||||
<Tab label="Weather" />
|
||||
<Tab label="Field Activities" />
|
||||
</Tabs>
|
||||
<TabPanelMine value={value} index={0}>
|
||||
<Box paddingLeft={2} paddingRight={2} paddingBottom={5}>
|
||||
<IconButton className={classes.avatar} onClick={() => setOpenNote(true)}>
|
||||
<Notes />
|
||||
</IconButton>
|
||||
<Box>
|
||||
<Typography variant="h5" style={{ fontWeight: 700 }}>
|
||||
{field.fieldName()} Details
|
||||
</Typography>
|
||||
</Box>
|
||||
<Grid container direction="column" alignItems="center">
|
||||
<Grid container direction="row" className={classes.dark}>
|
||||
<Grid>Approximate Area:</Grid>
|
||||
<Grid>{field.acres()}ac</Grid>
|
||||
</Grid>
|
||||
<Grid container direction="row" className={classes.light}>
|
||||
<Grid>Grain:</Grid>
|
||||
<Grid>
|
||||
{field.grain() === pond.Grain.GRAIN_CUSTOM
|
||||
? field.customType()
|
||||
: GrainDescriber(field.crop()).name}
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid container direction="row" className={classes.dark}>
|
||||
<Grid>Grain Variant:</Grid>
|
||||
<Grid>{field.settings.grainSubtype}</Grid>
|
||||
</Grid>
|
||||
<Grid container direction="row" className={classes.light}>
|
||||
<Grid>Land Location:</Grid>
|
||||
<Grid>{field.landLoc()}</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Box padding={3}>
|
||||
<Divider style={{ padding: 2 }} />
|
||||
</Box>
|
||||
{/* <HarvestPlanDisplay
|
||||
plan={hPlan}
|
||||
permissions={permissions}
|
||||
planField={field}
|
||||
loading={planLoading}
|
||||
fieldList={Array.from(fields.values())}
|
||||
changePlan={updatedPlan => {
|
||||
if (updatedPlan) {
|
||||
setHPlan(updatedPlan);
|
||||
}
|
||||
}}
|
||||
/> */}
|
||||
</Box>
|
||||
</TabPanelMine>
|
||||
<TabPanelMine value={value} index={1}>
|
||||
<Weather longitude={field.center().longitude} latitude={field.center().latitude} />
|
||||
</TabPanelMine>
|
||||
<TabPanelMine value={value} index={2}>
|
||||
{/* <TaskViewer drawerView objectKey={field.key()} loadKeys={taskLoadKeys} /> */}
|
||||
</TabPanelMine>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
const noteDrawer = () => {
|
||||
return (
|
||||
<Drawer
|
||||
anchor={isMobile ? "bottom" : "right"}
|
||||
open={openNote}
|
||||
onClose={() => setOpenNote(false)}>
|
||||
<Box height={isMobile ? "50vh" : "100vh"} padding={2}>
|
||||
<Typography style={{ fontWeight: 650 }}>Notes</Typography>
|
||||
<Chat type={pond.NoteType.NOTE_TYPE_FIELD} objectKey={field.key()} />
|
||||
</Box>
|
||||
</Drawer>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DisplayDrawer
|
||||
open={open}
|
||||
onClose={closeDrawer}
|
||||
displayNext={displayNext}
|
||||
displayPrev={displayPrev}
|
||||
title={field.name()}
|
||||
width={"40vw"}
|
||||
drawerBody={drawerBody()}
|
||||
updateElement={
|
||||
field.settings.fieldGeoData &&
|
||||
field.settings.fieldGeoData.origin === pond.DataOrigin.DATA_ORIGIN_ADAPTIVE
|
||||
? () => {
|
||||
props.openSettings(selectedFieldKey);
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
// objectActions={
|
||||
// field.settings.fieldGeoData &&
|
||||
// field.settings.fieldGeoData.origin === pond.DataOrigin.DATA_ORIGIN_ADAPTIVE ? (
|
||||
// <FieldActions field={field} permissions={permissions} refreshCallback={() => {}} />
|
||||
// ) : (
|
||||
// undefined
|
||||
// )
|
||||
// }
|
||||
/>
|
||||
{noteDrawer()}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
148
src/maps/mapDrawers/GrainBagDrawer.tsx
Normal file
148
src/maps/mapDrawers/GrainBagDrawer.tsx
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
import { Box } from "@mui/material";
|
||||
import DisplayDrawer from "common/DisplayDrawer";
|
||||
import MapMarkerSettings from "maps/MapMarkerSettings";
|
||||
import { GrainBag as BagModel } from "models/GrainBag";
|
||||
//import GrainBag from "pages/grainBag";
|
||||
import { pond } from "protobuf-ts/pond";
|
||||
import { useGrainBagAPI, useSnackbar } from "providers";
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
selectedBag: string;
|
||||
grainBags: Map<string, BagModel>;
|
||||
removeMarker: (key: string) => void;
|
||||
updateMarker: (key: string, newSettings: pond.GrainBagSettings) => void;
|
||||
moveMap: (long: number, lat: number) => void;
|
||||
}
|
||||
|
||||
export default function GrainBagDrawer(props: Props) {
|
||||
const { open, onClose, selectedBag, grainBags, removeMarker, updateMarker, moveMap } = props;
|
||||
const [bag, setBag] = useState<BagModel>(BagModel.create());
|
||||
const [openMarkerSettings, setOpenMarkerSettings] = useState(false);
|
||||
const bagAPI = useGrainBagAPI();
|
||||
const { openSnack } = useSnackbar();
|
||||
|
||||
useEffect(() => {
|
||||
let b = grainBags.get(selectedBag);
|
||||
if (b) {
|
||||
setBag(b);
|
||||
}
|
||||
}, [selectedBag, grainBags]);
|
||||
|
||||
const closeDrawer = () => {
|
||||
onClose();
|
||||
};
|
||||
|
||||
const displayNext = () => {
|
||||
let bagArr = Array.from(grainBags.values());
|
||||
let index = bagArr.indexOf(bag);
|
||||
let found = false;
|
||||
do {
|
||||
if (index === bagArr.length - 1) {
|
||||
index = 0;
|
||||
} else {
|
||||
index++;
|
||||
}
|
||||
let nextBag = bagArr[index];
|
||||
let location = nextBag.centerLocation();
|
||||
if (
|
||||
location !== null &&
|
||||
location !== undefined &&
|
||||
location.longitude !== 0 &&
|
||||
location.longitude !== 0
|
||||
) {
|
||||
setBag(nextBag);
|
||||
moveMap(location.longitude, location.latitude);
|
||||
found = true;
|
||||
}
|
||||
} while (!found);
|
||||
};
|
||||
|
||||
const displayPrev = () => {
|
||||
let bagArr = Array.from(grainBags.values());
|
||||
let index = bagArr.indexOf(bag);
|
||||
let found = false;
|
||||
do {
|
||||
if (index === 0) {
|
||||
index = bagArr.length - 1;
|
||||
} else {
|
||||
index--;
|
||||
}
|
||||
let nextBag = bagArr[index];
|
||||
let location = nextBag.centerLocation();
|
||||
if (
|
||||
location !== null &&
|
||||
location !== undefined &&
|
||||
location.longitude !== 0 &&
|
||||
location.longitude !== 0
|
||||
) {
|
||||
setBag(nextBag);
|
||||
moveMap(location.longitude, location.latitude);
|
||||
found = true;
|
||||
}
|
||||
} while (!found);
|
||||
};
|
||||
|
||||
const drawerBody = () => {
|
||||
//return <Box>{bag.key() !== "" && <GrainBag bagKey={bag.key()} mobileView />}</Box>;
|
||||
return <Box></Box>
|
||||
};
|
||||
|
||||
const update = () => {
|
||||
setOpenMarkerSettings(true);
|
||||
};
|
||||
|
||||
const remove = () => {
|
||||
let settings = bag.settings;
|
||||
settings.startLocation = undefined;
|
||||
settings.endLocation = undefined;
|
||||
bagAPI
|
||||
.updateGrainBag(bag.key(), bag.name(), settings)
|
||||
.then(resp => {
|
||||
openSnack("Grain bag location removed");
|
||||
removeMarker(bag.key());
|
||||
})
|
||||
.catch(err => {
|
||||
openSnack("Failed to remove grain bag location");
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DisplayDrawer
|
||||
open={open}
|
||||
onClose={closeDrawer}
|
||||
displayNext={displayNext}
|
||||
displayPrev={displayPrev}
|
||||
title={bag.name()}
|
||||
width={"40vw"}
|
||||
drawerBody={drawerBody()}
|
||||
updateElement={update}
|
||||
removeElement={remove}
|
||||
/>
|
||||
<MapMarkerSettings
|
||||
close={() => {
|
||||
setOpenMarkerSettings(false);
|
||||
}}
|
||||
open={openMarkerSettings}
|
||||
colourControl
|
||||
theme={bag.settings.theme ?? pond.ObjectTheme.create()}
|
||||
updateObject={newTheme => {
|
||||
let settings = bag.settings;
|
||||
settings.theme = newTheme;
|
||||
bagAPI
|
||||
.updateGrainBag(bag.key(), bag.name(), settings)
|
||||
.then(resp => {
|
||||
openSnack("marker settings updated");
|
||||
updateMarker(bag.key(), settings);
|
||||
})
|
||||
.catch(() => {
|
||||
openSnack("failed to update marker settings");
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
95
src/maps/mapLayers/geoMapLayer.tsx
Normal file
95
src/maps/mapLayers/geoMapLayer.tsx
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
import React, { useEffect } from "react";
|
||||
import { Layer, Source } from "react-map-gl/mapbox-legacy";
|
||||
import { FeatureCollection } from "geojson";
|
||||
|
||||
interface Props {
|
||||
objectCollection: FeatureCollection;
|
||||
measurementCollection?: FeatureCollection;
|
||||
showTitle?: boolean;
|
||||
setInteractiveLayers: (layers: string[]) => void;
|
||||
}
|
||||
|
||||
export default function GeoMapLayer(props: Props) {
|
||||
const { objectCollection, measurementCollection, showTitle, setInteractiveLayers } = props;
|
||||
|
||||
// useEffect(() => {
|
||||
// console.log(objectCollection);
|
||||
// console.log(measurementCollection);
|
||||
// }, [measurementCollection, objectCollection]);
|
||||
|
||||
useEffect(() => {
|
||||
setInteractiveLayers(["shapefill", "shapeborder", "measurementBorder"]);
|
||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Source type="geojson" data={objectCollection} id={"objects"}>
|
||||
<Layer
|
||||
id="shapefill"
|
||||
type="fill"
|
||||
paint={{
|
||||
"fill-color": ["get", "fill"], //using react-map-gl's data driven expressions you can get data from the properties of the feature
|
||||
"fill-opacity": 0.7
|
||||
}}
|
||||
/>
|
||||
<Layer
|
||||
id="shapeborder"
|
||||
type="line"
|
||||
layout={{
|
||||
"line-join": "round",
|
||||
"line-cap": "round"
|
||||
}}
|
||||
paint={{
|
||||
"line-color": "white",
|
||||
"line-width": ["get", "lineWidth"]
|
||||
}}
|
||||
/>
|
||||
{showTitle && (
|
||||
<Layer
|
||||
id="shapetitle"
|
||||
type="symbol"
|
||||
layout={{
|
||||
"text-field": ["get", "title"],
|
||||
"text-size": 18,
|
||||
"text-font": ["Open Sans Bold"],
|
||||
"text-anchor": "center"
|
||||
}}
|
||||
paint={{
|
||||
"text-color": "white"
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Source>
|
||||
<Source type="geojson" data={measurementCollection} id={"measurements"}>
|
||||
<Layer
|
||||
id="measurementBorder"
|
||||
type="line"
|
||||
layout={{
|
||||
"line-join": "round",
|
||||
"line-cap": "round"
|
||||
}}
|
||||
paint={{
|
||||
"line-color": "white",
|
||||
"line-width": ["get", "lineWidth"]
|
||||
}}
|
||||
/>
|
||||
<Layer
|
||||
id="totalDistance"
|
||||
type="symbol"
|
||||
layout={{
|
||||
"text-field": ["get", "totalDist"],
|
||||
"text-size": 18,
|
||||
"text-font": ["Open Sans Bold"],
|
||||
//"symbol-placement": "line-center",
|
||||
"text-anchor": "bottom-left",
|
||||
"icon-allow-overlap": true,
|
||||
"text-offset": [0, -1]
|
||||
}}
|
||||
paint={{
|
||||
"text-color": "white"
|
||||
}}
|
||||
/>
|
||||
</Source>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
247
src/maps/mapMarkers/Markers.tsx
Normal file
247
src/maps/mapMarkers/Markers.tsx
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
import { Box,Grid2 as Grid, Theme, Tooltip, Typography } from "@mui/material";
|
||||
import { makeStyles } from "@mui/styles";
|
||||
import MiniPie from "charts/MiniPie";
|
||||
import { useMobile } from "hooks";
|
||||
import { clone } from "lodash";
|
||||
import { pond } from "protobuf-ts/pond";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { Marker } from "react-map-gl/mapbox-legacy";
|
||||
|
||||
//interface for markers
|
||||
export interface MarkerData {
|
||||
title: string; //displayed in the tooltip
|
||||
longitude: number;
|
||||
latitude: number;
|
||||
colour: string;
|
||||
centered?: boolean;
|
||||
details?: string[];
|
||||
markerIcon?: JSX.Element;
|
||||
subtype?: number;
|
||||
graphPercent?: number; //if defined show graph behind markericon
|
||||
customImage?: JSX.Element; //if defined use image rather than basic marker with icon
|
||||
customSize?: number; //if the size is different from the normal or mini markers
|
||||
mini?: boolean; //whether the marker is the small scouting marker or the normal size object marker
|
||||
visibleLevels?: { min?: number; max?: number }; //zoom levels that determine when the marker is visible
|
||||
clickFunc?: (event: React.PointerEvent<HTMLElement>, index: number, isMobile: boolean) => void; //function the marker will run when clicked
|
||||
updateFunc?: (location: pond.Location) => void; //function the marker will run when something changes
|
||||
}
|
||||
|
||||
interface Props {
|
||||
markerData: MarkerData[];
|
||||
mapZoomLevel: number;
|
||||
dragOn?: boolean;
|
||||
groupSelect?: boolean;
|
||||
showMarkerTitle?: boolean;
|
||||
displayDetails?: boolean;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) => ({
|
||||
pin: {
|
||||
borderRadius: "50rem",
|
||||
display: "inline-block",
|
||||
borderBottomRightRadius: "0",
|
||||
transform: "rotate(45deg)",
|
||||
cursor: "pointer",
|
||||
boxShadow: "4px 4px 10px black"
|
||||
},
|
||||
details: {
|
||||
transform: "rotate(45deg)",
|
||||
background: theme.palette.background.default,
|
||||
opacity: 0.75,
|
||||
padding: 5,
|
||||
borderRadius: 10,
|
||||
paddingLeft: 40,
|
||||
clipPath: "polygon(25% 0, 100% 0%, 100% 100%, 25% 100%, 0% 50%)"
|
||||
},
|
||||
titleDisplay: {
|
||||
textAlign: "center",
|
||||
width: 100,
|
||||
marginLeft: -25,
|
||||
marginTop: 10,
|
||||
backgroundColor: theme.palette.background.default,
|
||||
borderRadius: 30,
|
||||
boxShadow: "4px 4px 10px black"
|
||||
}
|
||||
}));
|
||||
|
||||
export default function Markers(props: Props) {
|
||||
const { markerData, dragOn, mapZoomLevel, displayDetails, showMarkerTitle, groupSelect } = props;
|
||||
const classes = useStyles();
|
||||
const isMobile = useMobile();
|
||||
const [groupedMarkers, setGroupedMarkers] = useState<Map<number, MarkerData>>(new Map());
|
||||
|
||||
const moveGroup = (oldLong: number, oldLat: number, newLong: number, newLat: number) => {
|
||||
//find the diff of the oldLong (the starting longitude of the marker that was actually dragged) and the newLong
|
||||
const diffLong = newLong - oldLong;
|
||||
//find the diff of the oldLat and new Lat
|
||||
const diffLat = newLat - oldLat;
|
||||
|
||||
let gm = clone(groupedMarkers);
|
||||
|
||||
gm.forEach(marker => {
|
||||
if (marker.updateFunc) {
|
||||
marker.longitude = marker.longitude + diffLong;
|
||||
marker.latitude = marker.latitude + diffLat;
|
||||
marker.updateFunc(
|
||||
pond.Location.create({ longitude: marker.longitude, latitude: marker.latitude })
|
||||
);
|
||||
}
|
||||
});
|
||||
setGroupedMarkers(gm);
|
||||
};
|
||||
|
||||
//when group select is turned off, clear the grouped markers
|
||||
useEffect(() => {
|
||||
if (!groupSelect) {
|
||||
setGroupedMarkers(new Map());
|
||||
}
|
||||
}, [groupSelect]);
|
||||
|
||||
const renderMarkers = useCallback(() => {
|
||||
let markerList: JSX.Element[] = [];
|
||||
markerData.forEach((marker, index) => {
|
||||
let markerSize = marker.mini ? 20 : 50;
|
||||
if (marker.customSize) {
|
||||
markerSize = marker.customSize;
|
||||
}
|
||||
|
||||
let yOff = 0;
|
||||
if (!marker.centered) {
|
||||
yOff = -markerSize * 0.7;
|
||||
}
|
||||
|
||||
let vis = "block";
|
||||
if (marker.visibleLevels) {
|
||||
let max = marker.visibleLevels.max;
|
||||
let min = marker.visibleLevels.min;
|
||||
if ((max && mapZoomLevel > max) || (min && mapZoomLevel < min)) {
|
||||
vis = "none";
|
||||
}
|
||||
}
|
||||
|
||||
let gm = groupedMarkers.get(index);
|
||||
if (gm) {
|
||||
marker.longitude = gm.longitude;
|
||||
marker.latitude = gm.latitude;
|
||||
}
|
||||
|
||||
let border = marker.customImage ? "" : "1px solid white";
|
||||
if (gm) {
|
||||
border = "2px solid black";
|
||||
}
|
||||
|
||||
let blah: JSX.Element = (
|
||||
<Marker
|
||||
//style={{border: gm ? "2px solid black" : ""}}
|
||||
key={"marker-" + index}
|
||||
longitude={marker.longitude}
|
||||
latitude={marker.latitude}
|
||||
//offsetTop={yOff}
|
||||
offset={[0, yOff]}
|
||||
draggable={dragOn}
|
||||
onDragEnd={e => {
|
||||
let newLong = e.lngLat.lng;
|
||||
let newLat = e.lngLat.lat;
|
||||
if (groupSelect && groupedMarkers.size > 0) {
|
||||
moveGroup(marker.longitude, marker.latitude, newLong, newLat);
|
||||
} else {
|
||||
if (marker.updateFunc) {
|
||||
marker.longitude = newLong;
|
||||
marker.latitude = newLat;
|
||||
marker.updateFunc(
|
||||
pond.Location.create({ latitude: e.lngLat.lat, longitude: e.lngLat.lng })
|
||||
);
|
||||
}
|
||||
}
|
||||
}}>
|
||||
<Box id={"marker-" + index} />
|
||||
<Tooltip title={marker.title} placement={"right"} arrow>
|
||||
<Box
|
||||
className={!marker.customImage ? classes.pin : undefined}
|
||||
style={{
|
||||
display: vis,
|
||||
border: border,
|
||||
background: marker.customImage ? "none" : marker.colour,
|
||||
width: markerSize,
|
||||
height: markerSize
|
||||
}}
|
||||
onPointerUp={e => {
|
||||
if (!dragOn && marker.clickFunc) {
|
||||
marker.clickFunc(e, index, isMobile);
|
||||
} else if (groupSelect) {
|
||||
let group = clone(groupedMarkers);
|
||||
group.set(index, marker);
|
||||
setGroupedMarkers(group);
|
||||
}
|
||||
}}>
|
||||
{marker.graphPercent !== undefined && (
|
||||
<Box
|
||||
width={"100%"}
|
||||
height={"100%"}
|
||||
style={{ pointerEvents: "none", position: "absolute" }}>
|
||||
<MiniPie
|
||||
max={100}
|
||||
current={marker.graphPercent}
|
||||
colour={marker.colour}
|
||||
size={markerSize}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
{marker.customImage ? (
|
||||
marker.customImage
|
||||
) : (
|
||||
<Box
|
||||
style={{
|
||||
transform: "rotate(-45deg)",
|
||||
width: markerSize * 0.6,
|
||||
height: markerSize * 0.6,
|
||||
paddingTop: marker.mini ? "10%" : "30%",
|
||||
pointerEvents: "none"
|
||||
}}>
|
||||
{marker.markerIcon}
|
||||
</Box>
|
||||
)}
|
||||
{marker.details && displayDetails && (
|
||||
<Box
|
||||
className={classes.details}
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: markerSize / 2,
|
||||
top: markerSize / 2,
|
||||
width: 200,
|
||||
marginTop: 80
|
||||
}}>
|
||||
<Grid container direction="column" wrap="nowrap">
|
||||
{marker.details.map((detail, i) => (
|
||||
<Grid key={i}>
|
||||
<Typography noWrap style={{ fontSize: 10, fontWeight: 750, height: 15 }}>
|
||||
{detail}
|
||||
</Typography>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Tooltip>
|
||||
{showMarkerTitle && <Box className={classes.titleDisplay}>{marker.title}</Box>}
|
||||
</Marker>
|
||||
);
|
||||
markerList.push(<React.Fragment key={index}>{blah}</React.Fragment>);
|
||||
});
|
||||
return markerList;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
markerData,
|
||||
classes,
|
||||
displayDetails,
|
||||
dragOn,
|
||||
groupSelect,
|
||||
groupedMarkers,
|
||||
isMobile,
|
||||
showMarkerTitle,
|
||||
mapZoomLevel
|
||||
]);
|
||||
|
||||
return <React.Fragment>{renderMarkers()}</React.Fragment>;
|
||||
}
|
||||
213
src/maps/mapMarkers/fieldMarkers/FieldMarkerSettings.tsx
Normal file
213
src/maps/mapMarkers/fieldMarkers/FieldMarkerSettings.tsx
Normal file
|
|
@ -0,0 +1,213 @@
|
|||
import {
|
||||
Box,
|
||||
Button,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
Grid2 as Grid,
|
||||
MenuItem,
|
||||
Slider,
|
||||
TextField
|
||||
} from "@mui/material";
|
||||
import ResponsiveDialog from "common/ResponsiveDialog";
|
||||
import { FieldMarker } from "models";
|
||||
import { pond } from "protobuf-ts/pond";
|
||||
import { useFieldMarkerAPI, useSnackbar } from "providers";
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
close: (refresh?: boolean) => void;
|
||||
longitude?: number;
|
||||
latitude?: number;
|
||||
fieldMarker?: FieldMarker;
|
||||
}
|
||||
|
||||
const fieldMarkerTypes = [
|
||||
{
|
||||
value: pond.FieldMarkerType.FIELD_MARKER_TYPE_OTHER,
|
||||
label: "Other"
|
||||
},
|
||||
{
|
||||
value: pond.FieldMarkerType.FIELD_MARKER_TYPE_WEEDS,
|
||||
label: "Weeds"
|
||||
},
|
||||
{
|
||||
value: pond.FieldMarkerType.FIELD_MARKER_TYPE_DISEASE,
|
||||
label: "Disease"
|
||||
},
|
||||
{
|
||||
value: pond.FieldMarkerType.FIELD_MARKER_TYPE_PESTS,
|
||||
label: "Pests"
|
||||
},
|
||||
{
|
||||
value: pond.FieldMarkerType.FIELD_MARKER_TYPE_ROCKS,
|
||||
label: "Rocks"
|
||||
}
|
||||
];
|
||||
|
||||
export default function FieldMarkerSettings(props: Props) {
|
||||
const { open, close, fieldMarker, longitude, latitude } = props;
|
||||
const fieldMarkerAPI = useFieldMarkerAPI();
|
||||
const { openSnack } = useSnackbar();
|
||||
const [title, setTitle] = useState("Field Marker");
|
||||
const [type, setType] = useState<pond.FieldMarkerType>(
|
||||
pond.FieldMarkerType.FIELD_MARKER_TYPE_OTHER
|
||||
);
|
||||
const [severity, setSeverity] = useState(0);
|
||||
const [description, setDescription] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (fieldMarker) {
|
||||
setTitle(fieldMarker.name());
|
||||
setType(fieldMarker.type());
|
||||
setSeverity(fieldMarker.severity());
|
||||
setDescription(fieldMarker.description());
|
||||
}
|
||||
}, [fieldMarker]);
|
||||
|
||||
const saveNewFieldMarker = () => {
|
||||
let newFM: FieldMarker = FieldMarker.create();
|
||||
newFM.settings.name = title;
|
||||
newFM.settings.type = type;
|
||||
newFM.settings.description = description;
|
||||
newFM.settings.latitude = latitude ?? 0;
|
||||
newFM.settings.longitude = longitude ?? 0;
|
||||
newFM.settings.severity = severity;
|
||||
//newFM.settings.affectedArea = fmAffectedArea;
|
||||
fieldMarkerAPI
|
||||
.addFieldMarker(newFM.settings)
|
||||
.then(resp => {
|
||||
openSnack("New marker has been saved");
|
||||
close(true);
|
||||
})
|
||||
.catch(err => openSnack("There was a problem saving your marker"));
|
||||
};
|
||||
|
||||
const updateFieldMarker = (fieldMarker: FieldMarker) => {
|
||||
let fm = fieldMarker;
|
||||
fm.settings.description = description;
|
||||
fm.settings.severity = severity;
|
||||
//fm.settings.affectedArea = fmAffectedArea;
|
||||
fieldMarkerAPI
|
||||
.updateFieldMarker(fm.key(), fm.settings)
|
||||
.then(resp => {
|
||||
openSnack("Updated Field Marker");
|
||||
close(true);
|
||||
})
|
||||
.catch(resp => {
|
||||
openSnack("Failed to update Field Marker");
|
||||
});
|
||||
};
|
||||
|
||||
const removeFieldMarker = (fieldMarker: FieldMarker) => {
|
||||
fieldMarkerAPI
|
||||
.removeFieldMarker(fieldMarker.key())
|
||||
.then(resp => {
|
||||
openSnack("Field Marker Removed");
|
||||
close(true);
|
||||
})
|
||||
.catch(() => {
|
||||
openSnack("Failed to remove Field Marker");
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<ResponsiveDialog open={open} onClose={() => close()} fullScreen={false}>
|
||||
<DialogTitle>{fieldMarker ? "Update Marker" : "Place Marker"}</DialogTitle>
|
||||
<DialogContent>
|
||||
<Box width={300}>
|
||||
<TextField
|
||||
id={"fieldMarkerName"}
|
||||
fullWidth
|
||||
margin="normal"
|
||||
label="Name"
|
||||
value={title}
|
||||
disabled={fieldMarker !== undefined}
|
||||
onChange={e => setTitle(e.target.value)}
|
||||
/>
|
||||
<TextField
|
||||
id={"FieldMarkerType"}
|
||||
fullWidth
|
||||
margin="normal"
|
||||
label="type"
|
||||
value={type}
|
||||
onChange={e => setType(+e.target.value)}
|
||||
select
|
||||
disabled={fieldMarker !== undefined}>
|
||||
{fieldMarkerTypes.map(option => (
|
||||
<MenuItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
Severity
|
||||
<Slider
|
||||
value={severity}
|
||||
min={0}
|
||||
max={4}
|
||||
step={1}
|
||||
onChange={(e, val) => setSeverity(val as number)}
|
||||
/>
|
||||
{/* Not doing affected area until i figure out how to resize with zoom */}
|
||||
{/* Affected Area
|
||||
<Slider
|
||||
value={fmAffectedArea}
|
||||
min={0}
|
||||
max={1000}
|
||||
onChange={(e, val) => setFMAffectedArea(val as number)}
|
||||
/> */}
|
||||
<TextField
|
||||
id={"fieldMarkerDescription"}
|
||||
fullWidth
|
||||
margin="normal"
|
||||
label="Description"
|
||||
value={description}
|
||||
onChange={e => setDescription(e.target.value)}
|
||||
/>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Grid container direction="row">
|
||||
<Grid>
|
||||
{fieldMarker !== undefined && (
|
||||
<Button
|
||||
variant="contained"
|
||||
style={{ color: "black", backgroundColor: "red", margin: 5 }}
|
||||
onClick={() => removeFieldMarker(fieldMarker)}>
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
</Grid>
|
||||
<Grid>
|
||||
<Button
|
||||
style={{ margin: 5 }}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => close()}>
|
||||
Cancel
|
||||
</Button>
|
||||
{fieldMarker !== undefined ? (
|
||||
<Button
|
||||
style={{ margin: 5 }}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => updateFieldMarker(fieldMarker)}>
|
||||
Update
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
style={{ margin: 5 }}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={saveNewFieldMarker}>
|
||||
Plot Field Marker
|
||||
</Button>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</DialogActions>
|
||||
</ResponsiveDialog>
|
||||
);
|
||||
}
|
||||
|
||||
1904
src/maps/mapObjectControllers/AgMapController.tsx
Normal file
1904
src/maps/mapObjectControllers/AgMapController.tsx
Normal file
File diff suppressed because it is too large
Load diff
419
src/maps/mapObjectTools/agMapTools.tsx
Normal file
419
src/maps/mapObjectTools/agMapTools.tsx
Normal file
|
|
@ -0,0 +1,419 @@
|
|||
import {
|
||||
Grid2 as Grid,
|
||||
IconButton,
|
||||
Theme,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
Typography,
|
||||
Box,
|
||||
FormControlLabel,
|
||||
Checkbox
|
||||
} from "@mui/material";
|
||||
import AddMarkerIcon from "products/AgIcons/AddMarker";
|
||||
import FieldMapIcon from "products/AgIcons/FieldMap";
|
||||
import FieldNamesIcon from "products/AgIcons/FieldNames";
|
||||
import ScoutIcon from "products/AgIcons/ScoutIcon";
|
||||
import AddBinIcon from "products/Bindapt/AddBinIcon";
|
||||
import BinsIcon from "products/Bindapt/BinsIcon";
|
||||
import { VisibilityOff } from "@mui/icons-material";
|
||||
import React, { useState } from "react";
|
||||
import AddFieldIcon from "products/AgIcons/AddField";
|
||||
import EditIcon from "products/AgIcons/Edit";
|
||||
import DeleteIcon from "products/AgIcons/Delete";
|
||||
import FieldListIcon from "products/AgIcons/FieldList";
|
||||
import DeviceIcon from "products/Bindapt/BindaptIcon";
|
||||
import { pond } from "protobuf-ts/pond";
|
||||
//import { useHistory } from "react-router";
|
||||
import { makeStyles } from "@mui/styles";
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) => ({
|
||||
iconButtons: {
|
||||
background: theme.palette.background.default,
|
||||
margin: 5,
|
||||
boxShadow: "4px 4px 10px black"
|
||||
},
|
||||
list: {
|
||||
background: theme.palette.background.default,
|
||||
opacity: 0.75,
|
||||
borderRadius: 15,
|
||||
marginTop: 10,
|
||||
boxShadow: "4px 4px 10px black"
|
||||
},
|
||||
liFont: {
|
||||
fontWeight: 700,
|
||||
marginLeft: -15
|
||||
},
|
||||
listItems: {
|
||||
borderRadius: "3rem"
|
||||
}
|
||||
}));
|
||||
|
||||
interface Props {
|
||||
toggleEditorMode: (
|
||||
modeName: "edit" | "drawPolygon" | "drawLine" | "drawPoint" | "none" | "delete" | undefined
|
||||
) => void;
|
||||
//these modes are for the imported editor, could possibly be removed when new editor is made
|
||||
binMarkerDetails: boolean;
|
||||
toggleBinMarkerDetails: (detailsVisible: boolean) => void; //controls whether the details are visible on the
|
||||
toggleIsNewObject: (newObject: boolean) => void; //controls whether creating a new object, in this case bin, when clicking on the map to place a marker
|
||||
toggleMarkerType: (type: pond.ObjectType) => void;
|
||||
toggleFieldTitles: (show: boolean) => void;
|
||||
toggleObjectEditType: (type: pond.ObjectType) => void;
|
||||
toggleMeasurement: (measurement: boolean) => void;
|
||||
//because the draw control for the map actually changes the modes itself internally will need to have the object controller on the outside change its mode as well
|
||||
editorMode: "edit" | "drawPolygon" | "drawLine" | "drawPoint" | "none" | "delete" | undefined;
|
||||
}
|
||||
|
||||
export default function AgMapTools(props: Props) {
|
||||
const classes = useStyles();
|
||||
const {
|
||||
toggleEditorMode,
|
||||
toggleBinMarkerDetails,
|
||||
binMarkerDetails,
|
||||
toggleIsNewObject,
|
||||
toggleMarkerType,
|
||||
toggleFieldTitles,
|
||||
toggleObjectEditType,
|
||||
toggleMeasurement
|
||||
} = props;
|
||||
const [markerType, setMarkerType] = useState<pond.ObjectType | undefined>();
|
||||
const [isNew, setIsNew] = useState(false);
|
||||
const [fieldControlDisplay, setFieldControlDisplay] = useState<"none" | "block">("none");
|
||||
const [binControlDisplay, setBinControlDisplay] = useState<"none" | "block">("none");
|
||||
const [scoutingControlDisplay, setScoutingControlDisplay] = useState<"none" | "block">("none");
|
||||
const [fieldControlHovered, setFieldControlHovered] = useState(false);
|
||||
const [binControlHovered, setBinControlHovered] = useState(false);
|
||||
const [scoutingControlHovered, setScoutingControlHovered] = useState(false);
|
||||
const [showFieldTitle, setShowFieldTitles] = useState(false);
|
||||
//const history = useHistory();
|
||||
|
||||
const goToMyFields = () => {
|
||||
console.log("navigate to my fields page")
|
||||
//history.push("/myFields");
|
||||
};
|
||||
|
||||
const toggleControls = (control: "field" | "bin" | "scout") => {
|
||||
if (control === "field") {
|
||||
if (fieldControlDisplay === "none") {
|
||||
setFieldControlDisplay("block");
|
||||
setBinControlDisplay("none");
|
||||
setScoutingControlDisplay("none");
|
||||
} else {
|
||||
setFieldControlDisplay("none");
|
||||
}
|
||||
}
|
||||
if (control === "bin") {
|
||||
if (binControlDisplay === "none") {
|
||||
setBinControlDisplay("block");
|
||||
setFieldControlDisplay("none");
|
||||
setScoutingControlDisplay("none");
|
||||
} else {
|
||||
setBinControlDisplay("none");
|
||||
}
|
||||
}
|
||||
if (control === "scout") {
|
||||
if (scoutingControlDisplay === "none") {
|
||||
setScoutingControlDisplay("block");
|
||||
setFieldControlDisplay("none");
|
||||
setBinControlDisplay("none");
|
||||
} else {
|
||||
setScoutingControlDisplay("none");
|
||||
}
|
||||
}
|
||||
toggleEditorMode("none");
|
||||
toggleIsNewObject(false);
|
||||
changeMarkerType(pond.ObjectType.OBJECT_TYPE_UNKNOWN);
|
||||
};
|
||||
|
||||
const changeMarkerType = (type: pond.ObjectType) => {
|
||||
setMarkerType(type);
|
||||
toggleMarkerType(type);
|
||||
};
|
||||
|
||||
const changeNewStatus = (isNew: boolean) => {
|
||||
setIsNew(isNew);
|
||||
toggleIsNewObject(isNew);
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Grid container direction="column">
|
||||
<Grid>
|
||||
<Grid container direction="row">
|
||||
<Grid>
|
||||
<IconButton
|
||||
className={classes.iconButtons}
|
||||
style={
|
||||
binControlDisplay === "block"
|
||||
? { background: "yellow" }
|
||||
: binControlHovered
|
||||
? { background: "grey" }
|
||||
: {}
|
||||
}
|
||||
title="Bin Controls"
|
||||
onClick={e => {
|
||||
toggleControls("bin");
|
||||
}}
|
||||
onMouseOver={() => setBinControlHovered(true)}
|
||||
onMouseOut={() => setBinControlHovered(false)}>
|
||||
{binControlDisplay === "block" ? <BinsIcon type="dark" /> : <BinsIcon />}
|
||||
</IconButton>
|
||||
</Grid>
|
||||
<Grid>
|
||||
<IconButton
|
||||
className={classes.iconButtons}
|
||||
style={
|
||||
fieldControlDisplay === "block"
|
||||
? { background: "yellow" }
|
||||
: fieldControlHovered
|
||||
? { background: "grey" }
|
||||
: {}
|
||||
}
|
||||
title="Field Controls"
|
||||
onClick={e => {
|
||||
toggleControls("field");
|
||||
}}
|
||||
onMouseOver={() => setFieldControlHovered(true)}
|
||||
onMouseOut={() => setFieldControlHovered(false)}>
|
||||
{fieldControlDisplay === "block" ? <FieldMapIcon type="dark" /> : <FieldMapIcon />}
|
||||
</IconButton>
|
||||
</Grid>
|
||||
<Grid>
|
||||
<IconButton
|
||||
className={classes.iconButtons}
|
||||
style={
|
||||
scoutingControlDisplay === "block"
|
||||
? { background: "yellow" }
|
||||
: scoutingControlHovered
|
||||
? { background: "grey" }
|
||||
: {}
|
||||
}
|
||||
title="Scouting Controls"
|
||||
onClick={e => {
|
||||
toggleControls("scout");
|
||||
}}
|
||||
onMouseOver={() => setScoutingControlHovered(true)}
|
||||
onMouseOut={() => setScoutingControlHovered(false)}>
|
||||
{scoutingControlDisplay === "block" ? <ScoutIcon type="dark" /> : <ScoutIcon />}
|
||||
</IconButton>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid container direction="row">
|
||||
<Grid style={{ display: binControlDisplay }}>
|
||||
<List className={classes.list}>
|
||||
<ListItem
|
||||
id="createBin"
|
||||
//button
|
||||
onClick={e => {
|
||||
changeNewStatus(true);
|
||||
//setPlaceLine(false);
|
||||
changeMarkerType(pond.ObjectType.OBJECT_TYPE_BIN);
|
||||
}}
|
||||
className={classes.listItems}
|
||||
style={
|
||||
isNew && markerType === pond.ObjectType.OBJECT_TYPE_BIN
|
||||
? { background: "green" }
|
||||
: {}
|
||||
}>
|
||||
<ListItemIcon>
|
||||
<AddBinIcon />
|
||||
</ListItemIcon>
|
||||
<Typography className={classes.liFont}>Create New Bin</Typography>
|
||||
</ListItem>
|
||||
<ListItem
|
||||
id="addBin"
|
||||
//button
|
||||
onClick={e => {
|
||||
changeNewStatus(false);
|
||||
//setPlaceLine(false);
|
||||
changeMarkerType(pond.ObjectType.OBJECT_TYPE_BIN);
|
||||
}}
|
||||
className={classes.listItems}
|
||||
style={
|
||||
!isNew && markerType === pond.ObjectType.OBJECT_TYPE_BIN
|
||||
? { background: "green" }
|
||||
: {}
|
||||
}>
|
||||
<ListItemIcon>
|
||||
<BinsIcon singleBin />
|
||||
</ListItemIcon>
|
||||
<Typography className={classes.liFont}>Set Bin Location</Typography>
|
||||
</ListItem>
|
||||
<ListItem
|
||||
id="addBinYard"
|
||||
//button
|
||||
onClick={e => {
|
||||
changeNewStatus(false);
|
||||
//setPlaceLine(false);
|
||||
changeMarkerType(pond.ObjectType.OBJECT_TYPE_BINYARD);
|
||||
}}
|
||||
className={classes.listItems}
|
||||
style={
|
||||
markerType === pond.ObjectType.OBJECT_TYPE_BINYARD
|
||||
? { background: "green" }
|
||||
: {}
|
||||
}>
|
||||
<ListItemIcon>
|
||||
<BinsIcon />
|
||||
</ListItemIcon>
|
||||
<Typography className={classes.liFont}>Set Bin Yard Location</Typography>
|
||||
</ListItem>
|
||||
<ListItem id="binName" className={classes.listItems}>
|
||||
<FormControlLabel
|
||||
label="Bin Details"
|
||||
control={
|
||||
<Checkbox
|
||||
value={binMarkerDetails}
|
||||
onChange={e => toggleBinMarkerDetails(e.target.checked)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
</List>
|
||||
</Grid>
|
||||
<Grid style={{ display: fieldControlDisplay }}>
|
||||
<Box>
|
||||
<Box className={classes.list}></Box>
|
||||
<List className={classes.list} style={{ marginLeft: 60 }}>
|
||||
<ListItem
|
||||
id="addField"
|
||||
//button
|
||||
onClick={() => {
|
||||
toggleEditorMode("drawPolygon");
|
||||
toggleMeasurement(false);
|
||||
}}
|
||||
className={classes.listItems}
|
||||
style={props.editorMode === "drawPolygon" ? { background: "green" } : {}}>
|
||||
<ListItemIcon>
|
||||
<AddFieldIcon />
|
||||
</ListItemIcon>
|
||||
<Typography className={classes.liFont}>Add Field/Hole</Typography>
|
||||
</ListItem>
|
||||
<ListItem
|
||||
id="plotGrainBag"
|
||||
//button
|
||||
onClick={() => {
|
||||
toggleEditorMode("drawLine");
|
||||
toggleMeasurement(false);
|
||||
toggleObjectEditType(pond.ObjectType.OBJECT_TYPE_GRAIN_BAG);
|
||||
}}
|
||||
className={classes.listItems}
|
||||
style={props.editorMode === "drawLine" ? { background: "green" } : {}}>
|
||||
<ListItemIcon></ListItemIcon>
|
||||
<Typography className={classes.liFont}>Add Grain Bag</Typography>
|
||||
</ListItem>
|
||||
<ListItem
|
||||
id="editField"
|
||||
//button
|
||||
onClick={() => {
|
||||
toggleEditorMode("edit");
|
||||
toggleMeasurement(false);
|
||||
}}
|
||||
className={classes.listItems}
|
||||
style={props.editorMode === "edit" ? { background: "green" } : {}}>
|
||||
<ListItemIcon>
|
||||
<EditIcon />
|
||||
</ListItemIcon>
|
||||
<Typography className={classes.liFont}>Edit Field Structures</Typography>
|
||||
</ListItem>
|
||||
<ListItem
|
||||
id="deleteField"
|
||||
//button
|
||||
onClick={() => {
|
||||
toggleEditorMode("delete");
|
||||
toggleMeasurement(false);
|
||||
}}
|
||||
className={classes.listItems}
|
||||
style={props.editorMode === "delete" ? { background: "green" } : {}}>
|
||||
<ListItemIcon>
|
||||
<DeleteIcon />
|
||||
</ListItemIcon>
|
||||
<Typography className={classes.liFont}>Delete Field</Typography>
|
||||
</ListItem>
|
||||
<ListItem
|
||||
id="fieldList"
|
||||
//button
|
||||
onClick={goToMyFields}
|
||||
className={classes.listItems}>
|
||||
<ListItemIcon>
|
||||
<FieldListIcon />
|
||||
</ListItemIcon>
|
||||
<Typography className={classes.liFont}>Go to My Fields</Typography>
|
||||
</ListItem>
|
||||
<ListItem
|
||||
id="fieldNames"
|
||||
//button
|
||||
onClick={() => {
|
||||
toggleFieldTitles(!showFieldTitle);
|
||||
setShowFieldTitles(!showFieldTitle);
|
||||
}}
|
||||
className={classes.listItems}>
|
||||
<ListItemIcon>
|
||||
{showFieldTitle ? <VisibilityOff /> : <FieldNamesIcon />}
|
||||
</ListItemIcon>
|
||||
<Typography className={classes.liFont}>View Field Names</Typography>
|
||||
</ListItem>
|
||||
<ListItem
|
||||
id="addDevice"
|
||||
//button
|
||||
onClick={e => {
|
||||
changeMarkerType(pond.ObjectType.OBJECT_TYPE_DEVICE);
|
||||
}}
|
||||
className={classes.listItems}
|
||||
style={
|
||||
markerType === pond.ObjectType.OBJECT_TYPE_DEVICE
|
||||
? { background: "green" }
|
||||
: {}
|
||||
}>
|
||||
<ListItemIcon>
|
||||
<DeviceIcon />
|
||||
</ListItemIcon>
|
||||
<Typography className={classes.liFont}>Set Device Location</Typography>
|
||||
</ListItem>
|
||||
</List>
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid style={{ display: scoutingControlDisplay }}>
|
||||
<List className={classes.list} style={{ marginLeft: 120 }}>
|
||||
<ListItem
|
||||
id="addCustomMarker"
|
||||
//button
|
||||
onClick={e => {
|
||||
changeMarkerType(pond.ObjectType.OBJECT_TYPE_FIELDMARKER);
|
||||
// if (watching) {
|
||||
// props.newMarker(geoPosition.longitude, geoPosition.latitude, "custom");
|
||||
// }
|
||||
}}
|
||||
className={classes.listItems}
|
||||
style={
|
||||
markerType === pond.ObjectType.OBJECT_TYPE_FIELDMARKER
|
||||
? { background: "green" }
|
||||
: {}
|
||||
}>
|
||||
<ListItemIcon>
|
||||
<AddMarkerIcon />
|
||||
</ListItemIcon>
|
||||
<Typography className={classes.liFont}>Set Custom Marker</Typography>
|
||||
</ListItem>
|
||||
<ListItem
|
||||
id="addMeasurementLine"
|
||||
//button
|
||||
onClick={e => {
|
||||
toggleEditorMode("drawLine");
|
||||
toggleObjectEditType(pond.ObjectType.OBJECT_TYPE_UNKNOWN); //TODO: make this a measurement object type in the future
|
||||
toggleMeasurement(true);
|
||||
}}
|
||||
className={classes.listItems}
|
||||
style={props.editorMode === "drawLine" ? { background: "green" } : {}}>
|
||||
<ListItemIcon></ListItemIcon>
|
||||
<Typography className={classes.liFont}>Measurement Line</Typography>
|
||||
</ListItem>
|
||||
</List>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
284
src/maps/mapboxStyleOverrides.css
Normal file
284
src/maps/mapboxStyleOverrides.css
Normal file
|
|
@ -0,0 +1,284 @@
|
|||
/* Basics */
|
||||
.mapboxgl-ctrl-geocoder,
|
||||
.mapboxgl-ctrl-geocoder *,
|
||||
.mapboxgl-ctrl-geocoder *:after,
|
||||
.mapboxgl-ctrl-geocoder *:before {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.mapboxgl-ctrl-top-right {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
padding-left: 20px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* .mapboxgl-ctrl-top-left {
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
.mapboxgl-ctrl-top-right {
|
||||
right: 0;
|
||||
top: 0;
|
||||
}
|
||||
.mapboxgl-ctrl-bottom-left {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
.mapboxgl-ctrl-bottom-right {
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
} */
|
||||
|
||||
.mapboxgl-ctrl-geocoder {
|
||||
font-size: 18px;
|
||||
line-height: 24px;
|
||||
font-family: "Open Sans", "Helvetica Neue", Arial, Helvetica, sans-serif;
|
||||
position: relative;
|
||||
background-color: #424242;
|
||||
width: 100%;
|
||||
min-width: 240px;
|
||||
z-index: 1;
|
||||
border-radius: 20px;
|
||||
transition: width 0.25s, min-width 0.25s;
|
||||
opacity: 0.95;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.mapboxgl-ctrl-geocoder--input {
|
||||
font: inherit;
|
||||
width: 100%;
|
||||
border: 0;
|
||||
background-color: transparent;
|
||||
margin: 0;
|
||||
height: 50px;
|
||||
color: #fff;
|
||||
padding: 6px 45px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mapboxgl-ctrl-geocoder--input::-ms-clear {
|
||||
display: none; /* hide input clear button in IE */
|
||||
}
|
||||
|
||||
.mapboxgl-ctrl-geocoder--input:focus {
|
||||
color: #fff;
|
||||
outline: 0;
|
||||
box-shadow: none;
|
||||
outline: thin dotted;
|
||||
}
|
||||
|
||||
.mapboxgl-ctrl-geocoder .mapboxgl-ctrl-geocoder--pin-right > * {
|
||||
z-index: 2;
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 7px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mapboxgl-ctrl-geocoder,
|
||||
.mapboxgl-ctrl-geocoder .suggestions {
|
||||
box-shadow: 0 0 10px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Collapsed */
|
||||
.mapboxgl-ctrl-geocoder.mapboxgl-ctrl-geocoder--collapsed {
|
||||
width: 50px;
|
||||
min-width: 50px;
|
||||
transition: width 0.25s, min-width 0.25s;
|
||||
}
|
||||
|
||||
/* Suggestions */
|
||||
.mapboxgl-ctrl-geocoder .suggestions {
|
||||
background-color: #424242;
|
||||
opacity: 0.95;
|
||||
border-radius: 4px;
|
||||
left: 0;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
top: 110%; /* fallback */
|
||||
top: calc(100% + 6px);
|
||||
z-index: 1000;
|
||||
overflow: hidden;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.mapboxgl-ctrl-bottom-left .suggestions,
|
||||
.mapboxgl-ctrl-bottom-right .suggestions {
|
||||
top: auto;
|
||||
bottom: 100%;
|
||||
}
|
||||
|
||||
.mapboxgl-ctrl-geocoder .suggestions > li > a {
|
||||
cursor: default;
|
||||
display: block;
|
||||
padding: 6px 12px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.mapboxgl-ctrl-geocoder .suggestions > .active > a,
|
||||
.mapboxgl-ctrl-geocoder .suggestions > li > a:hover {
|
||||
color: #fff;
|
||||
background-color: #303030;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mapboxgl-ctrl-geocoder--suggestion-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.mapboxgl-ctrl-geocoder--suggestion-title,
|
||||
.mapboxgl-ctrl-geocoder--suggestion-address {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Icons */
|
||||
.mapboxgl-ctrl-geocoder--icon {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
fill: #757575;
|
||||
top: 15px;
|
||||
}
|
||||
|
||||
.mapboxgl-ctrl-geocoder--icon-search {
|
||||
position: absolute;
|
||||
top: 13px;
|
||||
left: 12px;
|
||||
width: 23px;
|
||||
height: 23px;
|
||||
}
|
||||
|
||||
.mapboxgl-ctrl-geocoder--button {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
background: #424242;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.mapboxgl-ctrl-geocoder--icon-close {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-top: 8px;
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
.mapboxgl-ctrl-geocoder--button:hover .mapboxgl-ctrl-geocoder--icon-close {
|
||||
fill: #909090;
|
||||
}
|
||||
|
||||
.mapboxgl-ctrl-geocoder--icon-loading {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
margin-top: 5px;
|
||||
margin-right: 0px;
|
||||
-moz-animation: rotate 0.8s infinite cubic-bezier(0.45, 0.05, 0.55, 0.95);
|
||||
-webkit-animation: rotate 0.8s infinite cubic-bezier(0.45, 0.05, 0.55, 0.95);
|
||||
animation: rotate 0.8s infinite cubic-bezier(0.45, 0.05, 0.55, 0.95);
|
||||
}
|
||||
|
||||
/* Animation */
|
||||
@-webkit-keyframes rotate {
|
||||
from {
|
||||
-webkit-transform: rotate(0);
|
||||
transform: rotate(0);
|
||||
}
|
||||
to {
|
||||
-webkit-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
from {
|
||||
-webkit-transform: rotate(0);
|
||||
transform: rotate(0);
|
||||
}
|
||||
to {
|
||||
-webkit-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Media queries*/
|
||||
@media screen and (min-width: 640px) {
|
||||
.mapboxgl-ctrl-geocoder.mapboxgl-ctrl-geocoder--collapsed {
|
||||
width: 36px;
|
||||
min-width: 36px;
|
||||
}
|
||||
|
||||
.mapboxgl-ctrl-geocoder {
|
||||
width: 33.3333%;
|
||||
font-size: 15px;
|
||||
line-height: 20px;
|
||||
max-width: 360px;
|
||||
}
|
||||
.mapboxgl-ctrl-geocoder .suggestions {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.mapboxgl-ctrl-geocoder--icon {
|
||||
top: 8px;
|
||||
}
|
||||
|
||||
.mapboxgl-ctrl-geocoder--icon-close {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-top: 3px;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.mapboxgl-ctrl-geocoder--icon-search {
|
||||
left: 7px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.mapboxgl-ctrl-geocoder--input {
|
||||
height: 36px;
|
||||
padding: 6px 35px;
|
||||
}
|
||||
|
||||
.mapboxgl-ctrl-geocoder--icon-loading {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
margin-top: -2px;
|
||||
margin-right: -5px;
|
||||
}
|
||||
|
||||
.mapbox-gl-geocoder--error {
|
||||
color: #909090;
|
||||
padding: 6px 12px;
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
/* popup styles */
|
||||
.mapboxgl-popup-content {
|
||||
background: #1f1e1d !important;
|
||||
box-shadow: 5px 5px 20px 4px rgb(0, 0, 0) !important;
|
||||
border-radius: 3 3 10px 10px !important;
|
||||
}
|
||||
|
||||
.mapboxgl-popup-tip {
|
||||
border-bottom-color: #1f1e1d !important;
|
||||
}
|
||||
|
||||
.mapboxgl-popup-close-button {
|
||||
background-color: #e8dd40 !important;
|
||||
border-radius: 3px !important;
|
||||
position: absolute !important;
|
||||
top: -5px !important;
|
||||
right: -5px !important;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue