so the problem is that when i'm trying to select an area using leaflet and click somewhere outside of selection tab it removes focus from the selection and after trying to select it doesn't work as intended meaning that i can't select properly
how do i return focus back to the tab?
appreciate any suggestions, if need to clarify something - tell me about it
here are the screenshots of how it should be and how it actually is
EDIT: here's code
import React, { useEffect, useState } from 'react';
import { useDispatch } from 'react-redux';
import L, { FeatureGroup, LatLngBounds, LeafletEvent, LeafletEventHandlerFn, LeafletMouseEvent, Point, PointTuple, Map } from 'leaflet';
import 'leaflet/dist/leaflet.css';
import 'leaflet-area-select';
import { Box, IconButton } from '@material-ui/core';
import GpsNotFixedRoundedIcon from '@material-ui/icons/GpsNotFixedRounded';
import { useStyles } from './styles';
import { disableMap, drawGrid } from '../../../utils/MapHelpers';
import { NewObs, Observation, Panorama } from '../../../state/reducers/types';
import { clearObs, saveGridSize, saveObsBounds, selectObsArea } from '../../../state/actions/inspector';
import OrientationMap from '../OrientationMap';
export type Grid = { gridCoordX: number[]; gridCoordY: number[]; gridLayer: FeatureGroup };
export let inspMap: Map;
interface AreaSelectEvent extends LeafletEvent {
bounds: LatLngBounds;
}
interface ILeafletWrapper {
handleObsForm: (showObsForm: boolean) => void;
handleIsSelectingObs: (isSelecting: boolean) => void;
handleOrientationMap: () => void;
handleGridSizeSelecting: () => void;
isSelectingObs: boolean;
newObs: NewObs;
observations: Observation[];
showOrientationMap: boolean;
isSelectingGridSize: boolean;
panorama: Panorama;
}
export const LeafletWrapper = (props: ILeafletWrapper) => {
const {
isSelectingObs,
handleObsForm,
handleIsSelectingObs,
observations,
showOrientationMap,
handleOrientationMap,
isSelectingGridSize,
handleGridSizeSelecting,
panorama,
} = props;
const { urlTemplates, originalResolution, originalZoomLevel, gridInfo } = panorama;
const [{ gridCoordX, gridCoordY, gridLayer }, setGrid] = useState<Grid>({
gridCoordX: [0],
gridCoordY: [0],
gridLayer: {},
} as Grid);
const [gridSizeBounds, setGridSizeBounds] = useState<LatLngBounds | undefined>();
const classes = useStyles();
const dispatch = useDispatch();
// initialization of Leaflet
useEffect(() => {
inspMap = new L.Map('map', { zoomControl: false, maxBoundsViscosity: 0.9 });
inspMap.setView(inspMap.unproject([originalResolution.x / 2, originalResolution.y / 2], originalZoomLevel), originalZoomLevel);
// you just need to pass something as url, further work with url is rewritten
const TileLayer = L.tileLayer('url', {
// lock to only zoom level for first version of app
minZoom: originalZoomLevel,
maxZoom: originalZoomLevel,
noWrap: true,
tileSize: 8192,
});
// custom internal method to get tile's url, have to use this method due to the inability of
// generating a templated URL from a predefined URLs (they are all unique)
TileLayer.getTileUrl = ({ z, x, y }: { z: number; x: number; y: number }) => {
return urlTemplates[y + '-' + x];
};
TileLayer.addTo(inspMap);
// disable the need to use a ctrl btn to select area
inspMap.selectArea.setControlKey(false);
// draw grid if there is cached panoramas info
if (gridInfo.x) {
const bounds = L.latLngBounds(
inspMap.unproject([0, 0], originalZoomLevel),
inspMap.unproject([gridInfo.x, gridInfo.y], originalZoomLevel),
);
const gridObj = drawGrid(bounds, inspMap, originalZoomLevel, originalResolution);
setGrid(gridObj);
setGridSizeBounds(bounds);
}
return () => {
inspMap.remove();
dispatch(clearObs());
};
}, []);
// disable interactions with leaflet while selectArea is enabled
useEffect(() => {
disableMap(isSelectingObs, inspMap);
if ((isSelectingObs || isSelectingGridSize) && !inspMap.selectArea.enabled()) {
inspMap.selectArea.enable();
inspMap.getContainer().style.cursor = 'crosshair';
inspMap.on('areaselected', handleObsAreaSelected as LeafletEventHandlerFn);
} else {
inspMap.getContainer().style.cursor = 'grab';
}
}, [isSelectingObs, isSelectingGridSize]);
// reset grid
useEffect(() => {
if (isSelectingGridSize && gridLayer?.remove) {
gridLayer.remove();
setGridSizeBounds(undefined);
}
}, [isSelectingGridSize, gridLayer]);
// draw observations
useEffect(() => {
// eslint-disable-next-line
observations.map((obs: any) => {
const index = obs.id;
if (obs.position.bounds) return;
const bounds = L.latLngBounds(
inspMap.unproject(obs.position.leftTopPoint, originalZoomLevel),
inspMap.unproject(obs.position.rightBottomPoint, originalZoomLevel),
);
const rectangle = L.rectangle(bounds, { color: '#ff9200', weight: 2, fillOpacity: 0 }).addTo(inspMap);
dispatch(saveObsBounds({ bounds: { remove: rectangle.remove.bind(rectangle) }, index }));
});
}, [observations]);
const handleObsAreaSelected = (e: AreaSelectEvent) => {
if (isSelectingObs) {
const leftTopPoint = inspMap.project(e.bounds.getNorthWest(), originalZoomLevel);
leftTopPoint.x = Math.round(leftTopPoint.x);
leftTopPoint.y = Math.round(leftTopPoint.y);
const rightBottomPoint = inspMap.project(e.bounds.getSouthEast(), originalZoomLevel);
rightBottomPoint.x = Math.round(rightBottomPoint.x);
rightBottomPoint.y = Math.round(rightBottomPoint.y);
const bounds = L.rectangle(e.bounds, { color: '#ff9200', weight: 2, fillOpacity: 0 });
bounds.addTo(inspMap);
dispatch(selectObsArea({ leftTopPoint, rightBottomPoint, bounds: { remove: bounds.remove.bind(bounds) } }));
handleObsForm(true);
handleIsSelectingObs(false);
inspMap.selectArea.disable();
inspMap.removeEventListener('areaselected', handleObsAreaSelected as LeafletEventHandlerFn);
}
if (isSelectingGridSize) {
handleGridSizeSelecting();
const gridObj = drawGrid(e.bounds, inspMap, originalZoomLevel, originalResolution);
setGrid(gridObj);
setGridSizeBounds(e.bounds);
inspMap.selectArea.disable();
inspMap.removeEventListener('areaselected', handleObsAreaSelected as LeafletEventHandlerFn);
dispatch(saveGridSize({ x: gridObj.gridCoordX[0], y: gridObj.gridCoordY[0] }));
}
};
const panToCell = ({ latlng }: LeafletMouseEvent) => {
// user click coordinates converted to pixel relative coordinate system for original zoom level
const point: Point = inspMap.project(latlng, originalZoomLevel);
const cellCenter = [];
// find center of cell
for (let i = 0; i < gridCoordX.length; i++) {
if (point.x < gridCoordX[i]) {
cellCenter.push(gridCoordX[0] / 2);
break;
}
if (point.x > gridCoordX[i] && (point.x < gridCoordX[i + 1] || i === gridCoordX.length - 1)) {
cellCenter.push(gridCoordX[i] + gridCoordX[0] / 2);
break;
}
}
for (let i = 0; i < gridCoordY.length; i++) {
if (point.y < gridCoordY[i]) {
cellCenter.push(gridCoordY[0] / 2);
break;
}
if (point.y > gridCoordY[i] && (point.y < gridCoordY[i + 1] || i === gridCoordY.length - 1)) {
cellCenter.push(gridCoordY[i] + gridCoordY[0] / 2);
break;
}
}
// temporary locked zoom level due only one zoom level available
inspMap.flyTo(inspMap.unproject(cellCenter as PointTuple, originalZoomLevel), originalZoomLevel);
};
const panToCenter = () => inspMap.panTo(inspMap.unproject([originalResolution.x / 2, originalResolution.y / 2], originalZoomLevel));
const zoomIn = () => inspMap.setZoom(inspMap.getZoom() + 1);
const zoomOut = () => inspMap.setZoom(inspMap.getZoom() - 1);
return (
<Box className={classes.root} height="100%">
<div id="map" style={{ height: '100%', width: '100%' }} />
<Box>
<IconButton className={classes.goToCenterBtn} aria-label="delete" onClick={panToCenter}>
<GpsNotFixedRoundedIcon color="inherit" />
</IconButton>
</Box>
<OrientationMap
panToCell={panToCell}
observations={observations}
showOrientationMap={showOrientationMap}
handleOrientationMap={handleOrientationMap}
handlePanToCenter={panToCenter}
zoomIn={zoomIn}
zoomOut={zoomOut}
gridSizeBounds={gridSizeBounds}
panorama={panorama}
/>
</Box>
);
};
question from:
https://stackoverflow.com/questions/66065754/problem-with-selecting-an-area-using-leaflet