dengdeyan's picture
add polyline
d747847
import React, { useEffect, useState } from "react";
import {
Card, CardContent, Typography, Button, List, ListItemButton,
ListItemText, Divider, Stack, FormControl, InputLabel, Select,
MenuItem, Menu, TextField, Dialog, DialogTitle, DialogContent, ToggleButton,
ToggleButtonGroup,
DialogActions, Tabs, Tab, Box
} from "@mui/material";
import DownloadIcon from "@mui/icons-material/Download";
import DeleteIcon from "@mui/icons-material/Delete";
import EditIcon from "@mui/icons-material/Edit";
import AddIcon from "@mui/icons-material/Add";
import { getDefaultBucket } from "../utils/awsConfig";
const DEFAULT_CUSTOM_MODEL = {
bucket: getDefaultBucket(),
prefix: "data/3dtiles",
tileset: "custom/tileset.json",
offsetHeight: 0,
};
const DEFAULT_URL_MODEL = {
tilesetUrl: "",
offsetHeight: 0,
};
function TabPanel(props) {
const { children, value, index, ...other } = props;
return (
<div
role="tabpanel"
hidden={value !== index}
id={`simple-tabpanel-${index}`}
aria-labelledby={`simple-tab-${index}`}
{...other}
>
{value === index && <Box sx={{ pt: 2 }}>{children}</Box>}
</div>
);
}
function a11yProps(index) {
return {
id: `simple-tab-${index}`,
"aria-controls": `simple-tabpanel-${index}`,
};
}
function ExportMenu({ onExport, disabled }) {
const [anchorEl, setAnchorEl] = useState(null);
const open = Boolean(anchorEl);
const handleClick = (event) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
const handleSelect = (format) => {
onExport(format);
handleClose();
};
return (
<>
<Button
variant="outlined"
startIcon={<DownloadIcon />}
onClick={handleClick}
disabled={disabled}
fullWidth
>
Export
</Button>
<Menu anchorEl={anchorEl} open={open} onClose={handleClose}>
<MenuItem onClick={() => handleSelect('geojson')}>As GeoJSON (.json)</MenuItem>
<MenuItem onClick={() => handleSelect('csv')}>As CSV (.csv)</MenuItem>
<MenuItem onClick={() => handleSelect('kml')}>As KML (.kml)</MenuItem>
</Menu>
</>
);
}
export default function MeasurePanel({
points,
onSelectPoint,
onDeletePoint,
onToggleMeasure,
measuring,
offsetHeight,
models,
selectedModelId,
customModel,
urlModel,
onModelChange,
onSaveCustomModel,
onSaveUrlModel,
onOffsetHeightChange,
onEditModel,
pointAppearance,
onPointAppearanceChange,
// New geometry props
polylines,
polygons,
drawingState,
selectedGeometry,
onStartDrawing,
onFinishDrawing,
onCancelDrawing,
onSelectGeometry,
onDeleteGeometry,
onExportGeometries,
}) {
const [selectedIndex, setSelectedIndex] = useState(null);
const [customDialogOpen, setCustomDialogOpen] = useState(false);
const [draftCustomModel, setDraftCustomModel] = useState(
customModel || DEFAULT_CUSTOM_MODEL
);
const [urlDialogOpen, setUrlDialogOpen] = useState(false);
const [draftUrlModel, setDraftUrlModel] = useState(
urlModel || DEFAULT_URL_MODEL
);
const [tabIndex, setTabIndex] = useState(0);
const handleTabChange = (event, newValue) => {
setTabIndex(newValue);
};
useEffect(() => {
if (!customDialogOpen) {
setDraftCustomModel(customModel || DEFAULT_CUSTOM_MODEL);
}
}, [customDialogOpen, customModel]);
useEffect(() => {
if (!urlDialogOpen) {
setDraftUrlModel(urlModel || DEFAULT_URL_MODEL);
}
}, [urlDialogOpen, urlModel]);
const handleSelect = (index) => {
// Allow deselecting
const nextIndex = selectedIndex === index ? null : index;
setSelectedIndex(nextIndex);
if (onSelectPoint) onSelectPoint(points[index]);
};
const openCustomDialog = () => {
setDraftCustomModel(customModel || DEFAULT_CUSTOM_MODEL);
setCustomDialogOpen(true);
};
const openUrlDialog = () => {
setDraftUrlModel(urlModel || DEFAULT_URL_MODEL);
setUrlDialogOpen(true);
};
const handleModelChange = (event) => {
const nextModelId = event.target.value;
if (onModelChange) onModelChange(nextModelId);
if (nextModelId === "custom") {
openCustomDialog();
}
if (nextModelId === "url") {
openUrlDialog();
}
};
const handleCustomFieldChange = (field) => (event) => {
setDraftCustomModel((prev) => ({
...prev,
[field]: event.target.value,
}));
};
const handleCustomSave = () => {
const nextOffset = Number(draftCustomModel.offsetHeight);
const payload = {
...draftCustomModel,
offsetHeight: Number.isNaN(nextOffset) ? 0 : nextOffset,
};
if (onSaveCustomModel) onSaveCustomModel(payload);
setCustomDialogOpen(false);
};
const handleUrlFieldChange = (field) => (event) => {
setDraftUrlModel((prev) => ({
...prev,
[field]: event.target.value,
}));
};
const handleUrlSave = () => {
const nextOffset = Number(draftUrlModel.offsetHeight);
const payload = {
...draftUrlModel,
offsetHeight: Number.isNaN(nextOffset) ? 0 : nextOffset,
};
if (onSaveUrlModel) onSaveUrlModel(payload);
setUrlDialogOpen(false);
};
const handleCustomClose = () => {
setCustomDialogOpen(false);
};
const handleUrlClose = () => {
setUrlDialogOpen(false);
};
return (
<Card
sx={{
width: 400,
position: "absolute",
top: 20,
left: 20,
bgcolor: "background.paper",
boxShadow: 5,
borderRadius: 3,
}}
>
<CardContent>
<Typography variant="h6" gutterBottom>
3D Measurement
</Typography>
<Stack spacing={2} mt={2}>
<Stack direction="row" spacing={1}>
<FormControl size="small" fullWidth sx={{ flex: 1 }}>
<InputLabel id="model-select-label">Model</InputLabel>
<Select
labelId="model-select-label"
value={selectedModelId}
label="Model"
onChange={handleModelChange}
>
{models.map((model) => (
<MenuItem key={model.id} value={model.id}>
{model.name}
</MenuItem>
))}
</Select>
</FormControl>
{(selectedModelId === "custom" || selectedModelId === "url" || selectedModelId === "upload") && (
<Button
variant="outlined"
startIcon={<EditIcon />}
onClick={() => {
if (selectedModelId === "custom") openCustomDialog();
if (selectedModelId === "url") openUrlDialog();
if (selectedModelId === "upload" && onEditModel) onEditModel();
}}
>
{selectedModelId === "upload" ? "Upload" : "Edit"}
</Button>
)}
</Stack>
<TextField
size="small"
label="Offset Height"
type="number"
value={offsetHeight}
onChange={(event) =>
onOffsetHeightChange && onOffsetHeightChange(event.target.value)
}
inputProps={{ step: 1 }}
/>
</Stack>
<Box sx={{ borderBottom: 1, borderColor: "divider", mt: 2 }}>
<Tabs
value={tabIndex}
onChange={handleTabChange}
aria-label="points and buildings tabs"
>
<Tab label={`Points (${points.length})`} {...a11yProps(0)} sx={{ textTransform: 'none' }} />
<Tab label={`Polylines (${polylines.length})`} {...a11yProps(1)} sx={{ textTransform: 'none' }} />
<Tab label={`Polygons (${polygons.length})`} {...a11yProps(2)} sx={{ textTransform: 'none' }} />
</Tabs>
</Box>
<TabPanel value={tabIndex} index={0}>
<List dense sx={{ maxHeight: 200, overflowY: "auto" }}>
{points.length === 0 && (
<Typography variant="body2" color="text.secondary" sx={{ px: 1 }}>
No points measured yet.
</Typography>
)}
{points.map((p, index) => (
<React.Fragment key={p.id}>
<ListItemButton
selected={selectedIndex === index}
onClick={() => handleSelect(index)}
sx={{
borderRadius: 2,
"&.Mui-selected": {
bgcolor: "primary.light",
color: "white",
"&:hover": { bgcolor: "primary.main" },
},
}}
>
<ListItemText
primary={`Point ${p.id}`}
secondary={`(${p.lat.toFixed(8)}, ${p.lon.toFixed(8)}, ${p.alt.toFixed(4)}) | Iz = ${p.accuracy.toFixed(3)} m`}
/>
</ListItemButton>
{index < points.length - 1 && <Divider component="li" />}
</React.Fragment>
))}
</List>
<Stack spacing={1} sx={{ mt: 1 }}>
<ToggleButtonGroup
value={pointAppearance}
exclusive
fullWidth
size="small"
onChange={(event, newAppearance) => {
// Prevent unselecting all buttons
if (newAppearance !== null) {
onPointAppearanceChange(newAppearance);
}
}}
aria-label="point appearance"
>
<ToggleButton value="ellipsoid" sx={{ flex: 1 }}>Error Ellipsoid</ToggleButton>
<ToggleButton value="point" sx={{ flex: 1 }}>Simple Point</ToggleButton>
</ToggleButtonGroup>
<Button
variant="contained"
color={measuring ? "error" : "primary"}
onClick={onToggleMeasure}
fullWidth
>
{measuring ? "End Measure" : "Start Measure"}
</Button>
<Stack direction="row" spacing={1}>
<ExportMenu onExport={(format) => onExportGeometries('point', format)} disabled={points.length === 0} />
<Button
variant="outlined"
color="error"
startIcon={<DeleteIcon />}
onClick={onDeletePoint}
disabled={selectedIndex === null}
fullWidth
>
Delete
</Button>
</Stack>
</Stack>
</TabPanel>
<TabPanel value={tabIndex} index={1}>
<List dense sx={{ maxHeight: 150, overflowY: "auto" }}>
{polylines.length === 0 && (
<Typography variant="body2" color="text.secondary" sx={{ px: 1 }}>
No polylines created yet.
</Typography>
)}
{polylines.map((line) => (
<ListItemButton
key={line.id}
selected={selectedGeometry?.type === 'polyline' && selectedGeometry?.id === line.id}
onClick={() => onSelectGeometry('polyline', line.id)}
sx={{
borderRadius: 2,
"&.Mui-selected": {
bgcolor: "primary.light",
color: "white",
"&:hover": { bgcolor: "primary.dark" },
},
}}
>
<ListItemText primary={line.id} secondary={`${line.points.length} points`} />
</ListItemButton>
))}
</List>
<Stack spacing={1} sx={{ mt: 2 }}>
<Button
variant="contained"
startIcon={<AddIcon />}
onClick={() => onStartDrawing('polyline')}
disabled={drawingState.mode !== 'none'}
>
Create Polyline
</Button>
<Stack direction="row" spacing={1}>
<ExportMenu onExport={(format) => onExportGeometries('polyline', format)} disabled={polylines.length === 0} />
<Button
variant="outlined"
color="error"
startIcon={<DeleteIcon />}
onClick={onDeleteGeometry}
disabled={selectedGeometry?.type !== 'polyline'}
fullWidth
>
Delete
</Button>
</Stack>
</Stack>
</TabPanel>
<TabPanel value={tabIndex} index={2}>
<List dense sx={{ maxHeight: 150, overflowY: "auto" }}>
{polygons.length === 0 && (
<Typography variant="body2" color="text.secondary" sx={{ px: 1 }}>
No polygons created yet.
</Typography>
)}
{polygons.map((poly) => (
<ListItemButton
key={poly.id}
selected={selectedGeometry?.type === 'polygon' && selectedGeometry?.id === poly.id}
onClick={() => onSelectGeometry('polygon', poly.id)}
sx={{
borderRadius: 2,
"&.Mui-selected": {
bgcolor: "primary.light",
color: "white",
"&:hover": { bgcolor: "primary.dark" },
},
}}
>
<ListItemText primary={poly.id} secondary={`${poly.points.length} points`} />
</ListItemButton>
))}
</List>
<Stack spacing={1} sx={{ mt: 2 }}>
<Button
variant="contained"
startIcon={<AddIcon />}
onClick={() => onStartDrawing('polygon')}
disabled={drawingState.mode !== 'none'}
>
Create Polygon
</Button>
<Stack direction="row" spacing={1}>
<ExportMenu onExport={(format) => onExportGeometries('polygon', format)} disabled={polygons.length === 0} />
<Button
variant="outlined"
color="error"
startIcon={<DeleteIcon />}
onClick={onDeleteGeometry}
disabled={selectedGeometry?.type !== 'polygon'}
fullWidth
>
Delete
</Button>
</Stack>
</Stack>
</TabPanel>
</CardContent>
<Dialog open={customDialogOpen} onClose={handleCustomClose} maxWidth="sm" fullWidth>
<DialogTitle>Custom S3 Model</DialogTitle>
<DialogContent>
<Stack spacing={2} mt={1}>
<TextField
size="small"
label="Bucket"
value={draftCustomModel.bucket}
onChange={handleCustomFieldChange("bucket")}
fullWidth
/>
<TextField
size="small"
label="Prefix"
value={draftCustomModel.prefix}
onChange={handleCustomFieldChange("prefix")}
fullWidth
/>
<TextField
size="small"
label="Tileset"
value={draftCustomModel.tileset}
onChange={handleCustomFieldChange("tileset")}
fullWidth
/>
<TextField
size="small"
label="Offset Height"
type="number"
value={draftCustomModel.offsetHeight}
onChange={handleCustomFieldChange("offsetHeight")}
inputProps={{ step: 1 }}
fullWidth
/>
</Stack>
</DialogContent>
<DialogActions>
<Button onClick={handleCustomClose}>Cancel</Button>
<Button variant="contained" onClick={handleCustomSave}>
Save
</Button>
</DialogActions>
</Dialog>
<Dialog open={urlDialogOpen} onClose={handleUrlClose} maxWidth="sm" fullWidth>
<DialogTitle>URL Model</DialogTitle>
<DialogContent>
<Stack spacing={2} mt={1}>
<TextField
size="small"
label="Tileset URL"
placeholder="http://localhost:8000/tileset.json"
value={draftUrlModel.tilesetUrl}
onChange={handleUrlFieldChange("tilesetUrl")}
fullWidth
/>
<TextField
size="small"
label="Offset Height"
type="number"
value={draftUrlModel.offsetHeight}
onChange={handleUrlFieldChange("offsetHeight")}
inputProps={{ step: 1 }}
fullWidth
/>
</Stack>
</DialogContent>
<DialogActions>
<Button onClick={handleUrlClose}>Cancel</Button>
<Button variant="contained" onClick={handleUrlSave}>
Save
</Button>
</DialogActions>
</Dialog>
</Card>
);
}