Region/district select; proper Map tabs

This commit is contained in:
2025-09-24 17:54:38 +09:00
parent 9758ab65b6
commit 2b0b08ae4e
7 changed files with 469 additions and 197 deletions

View File

@ -10,12 +10,12 @@ import { customMapSource, googleMapsSatelliteSource, yandexMapsSatelliteSource }
import { Geometry, SimpleGeometry } from 'ol/geom' import { Geometry, SimpleGeometry } from 'ol/geom'
import { fromExtent } from 'ol/geom/Polygon' import { fromExtent } from 'ol/geom/Polygon'
import { Coordinate } from 'ol/coordinate' import { Coordinate } from 'ol/coordinate'
import { addInteractions, handleImageDrop, loadFeatures, processFigure, processLine } from './mapUtils' import { addInteractions, getFeatureByEntityId, handleImageDrop, loadFeatures, processFigure, processLine, zoomToFeature } from './mapUtils'
import useSWR, { SWRConfiguration } from 'swr' import useSWR, { SWRConfiguration } from 'swr'
import { fetcher } from '../../http/axiosInstance' import { fetcher } from '../../http/axiosInstance'
import { BASE_URL } from '../../constants' import { BASE_URL } from '../../constants'
import { IconBoxMultiple, IconBoxPadding, IconChevronLeft, IconPlus, IconUpload, } from '@tabler/icons-react' import { IconBoxMultiple, IconBoxPadding, IconChevronLeft, IconPlus, IconUpload, IconX, } from '@tabler/icons-react'
import { ICitySettings, IFigure, ILine } from '../../interfaces/gis' import { ICitySettings, IDistrict, IFigure, ILine } from '../../interfaces/gis'
import axios from 'axios' import axios from 'axios'
import MapToolbar from './MapToolbar/MapToolbar' import MapToolbar from './MapToolbar/MapToolbar'
import MapStatusbar from './MapStatusbar/MapStatusbar' import MapStatusbar from './MapStatusbar/MapStatusbar'
@ -33,9 +33,11 @@ import GisService from '../../services/GisService'
import MapMode from './MapMode' import MapMode from './MapMode'
import { satMapsProviders, schemas } from '../../constants/map' import { satMapsProviders, schemas } from '../../constants/map'
import MapPrint from './MapPrint/MapPrint' import MapPrint from './MapPrint/MapPrint'
import { Field, Menu, MenuButton, MenuList, MenuPopover, MenuTrigger, Combobox, Option, Button, Divider, Spinner, Portal, Dropdown } from '@fluentui/react-components' import { Field, Menu, MenuButton, MenuList, MenuPopover, MenuTrigger, Combobox, Option, Button, Divider, Spinner, Portal, Dropdown, Tooltip, Drawer, DrawerHeader, DrawerBody, Text, Link } from '@fluentui/react-components'
import { IRegion } from '../../interfaces/fuel' import { IRegion } from '../../interfaces/fuel'
import { useAppStore } from '../../store/app' import { useAppStore } from '../../store/app'
import { getDistrictData, getRegionData, setDistrictsData, setRegionsData } from '../../store/regions'
import { ArrowLeft24Regular } from '@fluentui/react-icons'
const swrOptions: SWRConfiguration = { const swrOptions: SWRConfiguration = {
revalidateOnFocus: false revalidateOnFocus: false
@ -70,8 +72,8 @@ const MapComponent = ({
polygonExtent, rectCoords, draw, snap, translate, polygonExtent, rectCoords, draw, snap, translate,
drawingLayerSource, drawingLayerSource,
satLayer, staticMapLayer, figuresLayer, linesLayer, satLayer, staticMapLayer, figuresLayer, linesLayer,
regionsLayer, districtBoundLayer, baseLayer, regionsLayer, districtBoundLayer, baseLayer, districtsLayer,
printAreaDraw, printAreaDraw, statusText, statusTextPosition, regionSelect, districtSelect
} = useMapStore().id[id] } = useMapStore().id[id]
// Tab settings // Tab settings
@ -103,7 +105,7 @@ const MapComponent = ({
if (regionsLayer && regionBoundsData) { if (regionsLayer && regionBoundsData) {
if (Array.isArray(regionBoundsData)) { if (Array.isArray(regionBoundsData)) {
regionBoundsData.map(bound => { regionBoundsData.map(bound => {
const geoJson = new GeoJSON({ featureProjection: 'EPSG:3857' }) const geoJson = new GeoJSON() //new GeoJSON({ featureProjection: 'EPSG:3857' })
const geometry = geoJson.readGeometry(bound) as Geometry const geometry = geoJson.readGeometry(bound) as Geometry
const feature = new Feature(geometry) const feature = new Feature(geometry)
@ -111,8 +113,15 @@ const MapComponent = ({
regionsLayer.getSource()?.addFeature(feature) regionsLayer.getSource()?.addFeature(feature)
}) })
if (map) {
const extent = regionsLayer.getSource()?.getExtent()
if (extent) {
map.getView().fit(fromExtent(extent))
}
}
} }
//regionsLayer.current.getSource()?.addFeature()
} }
}, [regionBoundsData]) }, [regionBoundsData])
@ -126,7 +135,7 @@ const MapComponent = ({
districtBoundLayer.setSource(bounds) districtBoundLayer.setSource(bounds)
bounds.on('featuresloadend', function () { bounds.on('featuresloadend', function () {
// map.current?.setView(new View({ // map?.setView(new View({
// extent: bounds.getExtent() // extent: bounds.getExtent()
// })) // }))
}) })
@ -275,9 +284,15 @@ const MapComponent = ({
} }
}, [currentObjectId]) }, [currentObjectId])
const { data: regionsData } = useSWR(`/general/regions`, (url) => fetcher(url, BASE_URL.ems), swrOptions) const { data: regionsData } = useSWR(`/general/regions`, (url) => fetcher(url, BASE_URL.ems).then(res => {
setRegionsData(res)
return res
}), swrOptions)
const { data: districtsData } = useSWR(selectedRegion ? `/general/districts/?region_id=${selectedRegion}` : null, (url) => fetcher(url, BASE_URL.ems), swrOptions) const { data: districtsData } = useSWR(selectedRegion ? `/general/districts/?region_id=${selectedRegion}` : null, (url) => fetcher(url, BASE_URL.ems).then(res => {
setDistrictsData(res)
return res
}), swrOptions)
const { data: searchData } = useSWR( const { data: searchData } = useSWR(
throttledSearchObject !== "" && selectedDistrict && selectedYear ? `/general/search/objects?q=${throttledSearchObject}&id_city=${selectedDistrict}&year=${selectedYear}` : null, throttledSearchObject !== "" && selectedDistrict && selectedYear ? `/general/search/objects?q=${throttledSearchObject}&id_city=${selectedDistrict}&year=${selectedYear}` : null,
@ -384,7 +399,7 @@ const MapComponent = ({
useEffect(() => { useEffect(() => {
if (!selectedRegion) { if (!selectedRegion) {
setSelectedRegion(id, undefined) setSelectedRegion(id, null)
setSelectedYear(id, null) setSelectedYear(id, null)
} }
}, [selectedRegion, selectedDistrict, id]) }, [selectedRegion, selectedDistrict, id])
@ -466,8 +481,94 @@ const MapComponent = ({
} }
}, [mode, map, printAreaDraw]) }, [mode, map, printAreaDraw])
useEffect(() => {
if (districtsData && Array.isArray(districtsData)) {
const list: Number[] = []
districtsData.map(district => {
list.push(district.id as Number)
})
fetch(`${BASE_URL.ems}/gis/bounds/city`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ list })
}).then(async response => {
const data = await response.json()
if (Array.isArray(data)) {
data.map(bound => {
const geoJson = new GeoJSON() //new GeoJSON({ featureProjection: 'EPSG:3857' })
const geometry = geoJson.readGeometry(bound) as Geometry
const feature = new Feature(geometry)
feature.setProperties(bound.properties)
districtsLayer.getSource()?.addFeature(feature)
})
}
})
}
}, [districtsData])
const mapTooltipRef = useRef<HTMLDivElement>(null)
useEffect(() => {
if (mapTooltipRef.current) {
const leftOffset = 30
const topOffset = -30
mapTooltipRef.current.style.left = (statusTextPosition[0] + leftOffset).toString() + 'px'
mapTooltipRef.current.style.top = (statusTextPosition[1] + topOffset).toString() + 'px'
}
}, [statusTextPosition, mapTooltipRef])
// zoom on region select
useEffect(() => {
if (selectedRegion && !selectedDistrict) {
const feature = getFeatureByEntityId(selectedRegion, regionsLayer)
if (feature) {
regionSelect.getFeatures().push(feature)
}
zoomToFeature(id, feature)
}
}, [selectedRegion, selectedDistrict])
// zoom on district select
useEffect(() => {
if (selectedDistrict) {
const feature = getFeatureByEntityId(selectedDistrict, districtsLayer)
if (feature) {
districtSelect.getFeatures().push(feature)
regionsLayer.setOpacity(0)
}
zoomToFeature(id, feature)
}
}, [selectedDistrict])
useEffect(() => {
if (selectedYear) {
regionsLayer.setOpacity(0)
districtsLayer.setOpacity(0)
}
}, [selectedYear])
return ( return (
<> <div style={{ display: 'flex', flexDirection: 'column', position: 'relative', width: '100%', height: '100%' }}>
{statusText && active && !selectedYear &&
<Tooltip hideDelay={0} showDelay={0} content={statusText} relationship='description' visible>
<div style={{ position: 'absolute', zIndex: 9999, userSelect: 'none', pointerEvents: 'none' }} ref={mapTooltipRef}>
{/* {statusText} */}
</div>
</Tooltip>
}
<MapPrint id={id} mapElement={mapElement} /> <MapPrint id={id} mapElement={mapElement} />
{active && {active &&
@ -499,7 +600,7 @@ const MapComponent = ({
: null} : null}
</Combobox> </Combobox>
<Combobox {/* <Combobox
placeholder="Регион" placeholder="Регион"
clearable clearable
// 👇 show label instead of id // 👇 show label instead of id
@ -512,7 +613,7 @@ const MapComponent = ({
if (data.optionValue) { if (data.optionValue) {
setSelectedRegion(id, Number(data.optionValue)); setSelectedRegion(id, Number(data.optionValue));
} else { } else {
setSelectedRegion(id, undefined); setSelectedRegion(id, null);
} }
}} }}
style={{ minWidth: 'auto' }} style={{ minWidth: 'auto' }}
@ -524,9 +625,9 @@ const MapComponent = ({
</Option> </Option>
)) ))
: null} : null}
</Combobox> </Combobox> */}
<Combobox {/* <Combobox
placeholder="Населённый пункт" placeholder="Населённый пункт"
clearable clearable
value={ value={
@ -554,28 +655,7 @@ const MapComponent = ({
) )
) )
: null} : null}
</Combobox> </Combobox> */}
<Combobox
placeholder="Схема"
clearable
style={{ minWidth: 'auto' }}
value={selectedYear ? selectedYear.toString() : ""}
onOptionSelect={(_ev, data) => {
if (data.optionValue) {
setSelectedYear(id, Number(data.optionValue));
} else {
setSelectedYear(id, null);
}
}}
>
{schemas.map((el) => (
<Option key={el} value={el}>
{el}
</Option>
))}
</Combobox>
<Button icon={<IconBoxPadding />} appearance={alignMode ? 'primary' : 'transparent'} onClick={() => setAlignMode(id, !alignMode)} /> <Button icon={<IconBoxPadding />} appearance={alignMode ? 'primary' : 'transparent'} onClick={() => setAlignMode(id, !alignMode)} />
@ -621,41 +701,141 @@ const MapComponent = ({
</MenuList> </MenuList>
</MenuPopover> </MenuPopover>
</Menu> </Menu>
{/* <Menu position="bottom-end" transitionProps={{ transition: 'pop-top-right' }}>
<Menu.Target>
<Button variant='transparent'>
<Group gap={7} wrap='nowrap' style={{ flexShrink: 0 }} title='Слои'>
<IconBoxMultiple style={{ width: rem(20), height: rem(20) }} />
<IconChevronDown style={{ width: rem(12), height: rem(12) }} stroke={1.5} />
</Group>
</Button>
</Menu.Target>
<Menu.Dropdown miw={300}>
<Menu.Label>{'Настройка видимости слоёв'}</Menu.Label>
<Flex p='sm' direction='column' gap='xs'>
<Flex align='center' direction='row' gap='sm'>
<MantineSelect value={satMapsProvider} data={satMapsProviders} onChange={(value) => setSatMapsProvider(id, value as SatelliteMapsProvider)} />
</Flex>
<Flex direction='row'>
<ActionIcon size='lg' variant='transparent' onClick={() => submitOverlay(file, polygonExtent, rectCoords)}>
<IconUpload style={{ width: rem(20), height: rem(20) }} />
</ActionIcon>
<ActionIcon size='lg' variant='transparent' title='Добавить подложку'>
<IconPlus style={{ width: rem(20), height: rem(20) }} />
</ActionIcon>
</Flex>
<MapLayers map={map} />
</Flex>
</Menu.Dropdown>
</Menu> */}
</div> </div>
</Portal > </Portal >
} }
<div style={{ position: 'absolute', width: '100%', height: '100%' }}> <div style={{ position: 'absolute', width: '100%', height: '100%' }}>
{!selectedRegion &&
<Drawer style={{ position: 'absolute', height: '100%', zIndex: 9999 }} open={!selectedRegion} type='inline'>
{/* <DrawerHeader style={{ flexDirection: 'row' }}>
<Text weight='bold' size={500}></Text>
</DrawerHeader> */}
<DrawerBody>
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
{regionsData && regionsData.map((region: IRegion) => (
<Link key={region.id} onClick={() => setSelectedRegion(id, region.id)}
onMouseEnter={() => {
const feature = getFeatureByEntityId(region.id, regionsLayer)
if (feature) {
regionSelect.getFeatures().push(feature)
}
}}
onMouseLeave={() => {
regionSelect.getFeatures().clear()
}}
>{region.name}</Link>
))}
</div>
</DrawerBody>
</Drawer>}
<Drawer style={{ position: 'absolute', height: '100%', zIndex: 9999 }} open={!!selectedRegion && !selectedYear} type='inline'>
<DrawerHeader style={{ flexDirection: 'row' }}>
<Button icon={<ArrowLeft24Regular />} appearance='subtle' onClick={() => {
if (selectedDistrict) {
setSelectedDistrict(id, null)
districtSelect.getFeatures().clear()
regionsLayer.setOpacity(1)
} else {
setSelectedRegion(id, null)
regionSelect.getFeatures().clear()
if (map) {
const extent = regionsLayer.getSource()?.getExtent()
if (extent) {
map.getView().fit(fromExtent(extent), { duration: 100 })
regionsLayer.setOpacity(1)
}
}
}
}} />
{selectedDistrict ?
<Text weight='bold' size={500}>{getDistrictData(selectedDistrict)?.name}</Text>
:
<Text weight='bold' size={500}>{selectedRegion && getRegionData(selectedRegion)?.name}</Text>}
<Button appearance='subtle' style={{ marginLeft: 'auto' }} icon={<IconX />} onClick={() => {
setSelectedYear(id, null)
setSelectedDistrict(id, null)
setSelectedRegion(id, null)
if (map) {
const extent = regionsLayer.getSource()?.getExtent()
if (extent) {
map.getView().fit(fromExtent(extent), { duration: 100 })
regionsLayer.setOpacity(1)
}
}
}} />
</DrawerHeader>
<DrawerBody>
<div key={selectedRegion} style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
{selectedDistrict ?
selectedRegion && Object.entries(getRegionData(selectedRegion) as IRegion).map(([key, value]) => (
<div key={key} style={{ display: 'flex', justifyContent: 'space-between' }}>
<span>{key}</span>
<span>{value}</span>
</div>
))
:
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
<div>
{selectedRegion && Object.entries(getRegionData(selectedRegion) as IRegion).map(([key, value]) => (
<div key={key} style={{ display: 'flex', justifyContent: 'space-between' }}>
<span>{key}</span>
<span>{value}</span>
</div>
))}
</div>
{districtsData && districtsData.map((district: IDistrict) => (
<Link key={district.id} onClick={() => setSelectedDistrict(id, district.id)}
onMouseEnter={() => {
const feature = getFeatureByEntityId(district.id, districtsLayer)
if (feature) {
districtSelect.getFeatures().push(feature)
}
}}
onMouseLeave={() => {
districtSelect.getFeatures().clear()
}}
>{district.name}</Link>
))}
</div>
}
{selectedDistrict && <Combobox
placeholder="Схема"
clearable
style={{ minWidth: 'auto' }}
value={selectedYear ? selectedYear.toString() : ""}
onOptionSelect={(_ev, data) => {
if (data.optionValue) {
setSelectedYear(id, Number(data.optionValue));
} else {
setSelectedYear(id, null);
}
}}
>
{schemas.map((el) => (
<Option key={el} value={el}>
{el}
</Option>
))}
</Combobox>}
</div>
</DrawerBody>
</Drawer>
<div style={{ display: 'flex', flexDirection: 'column', width: '100%', height: '100%' }}> <div style={{ display: 'flex', flexDirection: 'column', width: '100%', height: '100%' }}>
<div style={{ display: 'flex', height: '94%', padding: '0.5rem', flexGrow: 1 }}> <div style={{ display: 'flex', height: '94%', padding: '0.5rem', flexGrow: 1 }}>
<div style={{ display: 'flex', flexDirection: 'column', width: '100%', maxWidth: '380px' }}> <div style={{ display: 'flex', flexDirection: 'column', width: '100%', maxWidth: '380px' }}>
@ -699,7 +879,7 @@ const MapComponent = ({
<div style={{ display: 'flex', flexDirection: 'column', width: '100%', alignItems: 'center' }} > <div style={{ display: 'flex', flexDirection: 'column', width: '100%', alignItems: 'center' }} >
<div style={{ ...mapControlsStyle, display: 'flex', flexDirection: 'column', width: 'fit-content' }}> <div style={{ ...mapControlsStyle, display: 'flex', flexDirection: 'column', width: 'fit-content' }}>
<MapMode map_id={id} /> {selectedDistrict && selectedYear && <MapMode map_id={id} />}
</div> </div>
</div> </div>
@ -714,16 +894,11 @@ const MapComponent = ({
</div> </div>
</div> </div>
<div style={{ display: 'flex', width: '100%' }}>
<MapStatusbar
map_id={id}
mapControlsStyle={mapControlsStyle}
/>
</div>
</div> </div>
</div> </div>
<div style={{ position: 'absolute', width: '100%', height: '100%', maxHeight: '100%' }} ref={mapElement} onDragOver={(e) => e.preventDefault()} onDrop={(e) => handleImageDrop(e, id)}> <div id={id} key={id} style={{ position: 'relative', width: '100%', height: '100%', maxHeight: '100%' }} ref={mapElement} onDragOver={(e) => e.preventDefault()} onDrop={(e) => handleImageDrop(e, id)}>
<div ref={tooltipRef}></div> <div ref={tooltipRef}></div>
</div> </div>
@ -743,7 +918,13 @@ const MapComponent = ({
</div> </div>
)} )}
</> <div style={{ display: 'flex', bottom: '0', width: '100%' }}>
<MapStatusbar
map_id={id}
mapControlsStyle={mapControlsStyle}
/>
</div>
</div>
) )
} }

View File

@ -18,6 +18,8 @@ import { measureStyleFunction, modifyStyle } from "./Measure/MeasureStyles";
import { getCurrentTool, getDraw, getDrawingLayerSource, getImageLayer, getMap, getMeasureClearPrevious, getMeasureDraw, getMeasureModify, getMeasureSource, getMeasureType, getOverlayLayerSource, getSnap, getTipPoint, getTranslate, PrintOrientation, setDraw, setFile, setMeasureDraw, setPolygonExtent, setRectCoords, setSnap, setTranslate } from "../../store/map"; import { getCurrentTool, getDraw, getDrawingLayerSource, getImageLayer, getMap, getMeasureClearPrevious, getMeasureDraw, getMeasureModify, getMeasureSource, getMeasureType, getOverlayLayerSource, getSnap, getTipPoint, getTranslate, PrintOrientation, setDraw, setFile, setMeasureDraw, setPolygonExtent, setRectCoords, setSnap, setTranslate } from "../../store/map";
import Collection from "ol/Collection"; import Collection from "ol/Collection";
import { SketchCoordType } from "ol/interaction/Draw"; import { SketchCoordType } from "ol/interaction/Draw";
import VectorImageLayer from "ol/layer/VectorImage";
import VectorSource from "ol/source/Vector";
const calculateAngle = (coords: [number, number][]) => { const calculateAngle = (coords: [number, number][]) => {
const [start, end] = coords; const [start, end] = coords;
@ -482,18 +484,25 @@ export const addInteractions = (
} }
} }
export const zoomToFeature = (map_id: string, feature: Feature) => { export const zoomToFeature = (map_id: string, feature: Feature | undefined) => {
const geometry = feature.getGeometry() if (feature) {
const extent = geometry?.getExtent() const geometry = feature.getGeometry()
const extent = geometry?.getExtent()
if (getMap(map_id) && extent) { if (getMap(map_id) && extent) {
getMap(map_id)?.getView().fit(extent, { getMap(map_id)?.getView().fit(extent, {
duration: 300, duration: 300,
maxZoom: 19, maxZoom: 19,
}) })
}
} }
} }
export const getFeatureByEntityId = (entity_id: number, layer: VectorImageLayer<Feature<Geometry>, VectorSource<Feature<Geometry>>>) => {
console.log(entity_id, layer.getSource()?.getFeatures())
return layer.getSource()?.getFeatures().find(feature => feature.getProperties().entity_id === entity_id)
}
// Function to save features to localStorage // Function to save features to localStorage
export const saveFeatures = (map_id: string) => { export const saveFeatures = (map_id: string) => {
const features = getDrawingLayerSource(map_id).getFeatures() const features = getDrawingLayerSource(map_id).getFeatures()

View File

@ -1,5 +1,3 @@
import { useEffect } from "react";
import { v4 as uuidv4 } from "uuid";
import { Tab, TabList } from "@fluentui/react-tabs"; import { Tab, TabList } from "@fluentui/react-tabs";
import MapComponent from "../components/map/MapComponent"; import MapComponent from "../components/map/MapComponent";
@ -7,76 +5,49 @@ import {
useAppStore, useAppStore,
setCurrentTab, setCurrentTab,
deleteMapTab, deleteMapTab,
addMapTab,
} from "../store/app"; } from "../store/app";
import { initializeMapState, useMapStore } from "../store/map"; import { initializeMapState, useMapStore } from "../store/map";
import { initializeObjectsState } from "../store/objects"; import { initializeObjectsState } from "../store/objects";
import { Button } from "@fluentui/react-components";
import { Add12Filled, Dismiss12Filled, Map16Regular } from "@fluentui/react-icons";
function MapTest() { function MapTest() {
const { mapTab, currentTab } = useAppStore(); const { mapTab, currentTab } = useAppStore()
const { id } = useMapStore(); const { id } = useMapStore()
const tabs = [
{
id: uuidv4(),
year: 2018,
region: 11,
district: 146,
},
// {
// id: uuidv4(),
// year: 2023,
// region: 11,
// district: 146,
// },
];
useEffect(() => {
tabs.forEach((tab) => {
useAppStore.setState((state) => {
initializeObjectsState(tab.id, tab.region, tab.district, null, tab.year);
initializeMapState(tab.id);
return {
mapTab: {
...state.mapTab,
[tab.id]: {
year: tab.year,
region: tab.region,
district: tab.district,
},
},
};
});
});
setCurrentTab(tabs[0].id);
return () => {
tabs.forEach((tab) => deleteMapTab(tab.id));
};
}, []);
return ( return (
<div style={{ height: "100%", width: "100%", position: "relative" }}> <div style={{ height: "100%", width: "100%", position: "relative" }}>
<div style={{ display: "flex", flexDirection: "column", height: "100%" }}> <div style={{ display: "flex", flexDirection: "column", height: "100%" }}>
<TabList <TabList
size='small'
selectedValue={currentTab} selectedValue={currentTab}
onTabSelect={(_, data) => setCurrentTab(data.value as string)} onTabSelect={(_, data) => setCurrentTab(data.value as string)}
style={{ borderBottom: '1px solid var(--colorNeutralShadowKey)' }}
> >
{Object.entries(mapTab).map(([key]) => ( {Object.entries(mapTab).map(([key]) => (
<Tab value={key} key={key}> <Tab key={key} value={key} icon={<Map16Regular />}>
{id[key]?.mapLabel ?? `Tab ${key}`} {id[key]?.mapLabel ?? `Tab ${key}`}
<Button style={{ marginLeft: '0.5rem' }} size='small' icon={<Dismiss12Filled />} appearance='subtle' onClick={(e) => {
e.stopPropagation()
deleteMapTab(key)
}} />
</Tab> </Tab>
))} ))}
<Button icon={<Add12Filled />} title="Открыть новую вкладку" appearance='subtle' onClick={() => {
const newId = addMapTab();
initializeObjectsState(newId, null, null, null, null);
initializeMapState(newId);
}} />
</TabList> </TabList>
<div style={{ flexGrow: 1, position: "relative" }}> <div style={{ flexGrow: 1, position: "relative" }}>
{Object.entries(mapTab).map(([key]) => {Object.entries(mapTab).map(([key]) =>
currentTab === key ? ( (<div key={key} style={{ height: "100%", position: "relative", display: currentTab === key ? 'unset' : 'none' }}>
<div key={key} style={{ height: "100%", position: "relative" }}> <MapComponent key={key} id={key} active={currentTab === key} />
<MapComponent key={key} id={key} active={true} /> </div>)
</div>
) : null
)} )}
</div> </div>
</div> </div>

View File

@ -1,38 +1,52 @@
import { create } from 'zustand'; import { create } from 'zustand'
import { v4 as uuidv4 } from 'uuid'
import { initializeObjectsState } from './objects'
import { initializeMapState } from './map'
export type Mode = 'edit' | 'view' export type Mode = 'edit' | 'view'
export type ColorScheme = 'light' | 'dark' | 'auto' export type ColorScheme = 'light' | 'dark' | 'auto'
export interface MapTabState {
year: number | null
region: number | null
district: number | null
}
export interface AppState { export interface AppState {
colorScheme: ColorScheme, colorScheme: ColorScheme,
mapTab: Record<string, { mapTab: Record<string, MapTabState>,
year: number | null,
region: number | null,
district: number | null
}>,
currentTab: string | null; currentTab: string | null;
} }
export const useAppStore = create<AppState>(() => ({ export const useAppStore = create<AppState>(() => {
colorScheme: 'auto', const firstId = uuidv4()
currentTab: null,
mapTab: {}
}))
const getColorScheme = () => { initializeObjectsState(firstId, null, null, null, null);
initializeMapState(firstId)
return {
colorScheme: "auto",
currentTab: firstId,
mapTab: {
[firstId]: { year: null, region: null, district: null },
},
}
})
export const getColorScheme = () => {
useAppStore.getState().colorScheme useAppStore.getState().colorScheme
} }
const setColorScheme = (colorScheme: ColorScheme) => { export const setColorScheme = (colorScheme: ColorScheme) => {
useAppStore.setState(() => ({ colorScheme: colorScheme })) useAppStore.setState(() => ({ colorScheme: colorScheme }))
localStorage.setItem('colorScheme', colorScheme.toString()) localStorage.setItem('colorScheme', colorScheme.toString())
} }
const getCurrentTab = () => useAppStore.getState().currentTab export const getCurrentTab = () => useAppStore.getState().currentTab
const setCurrentTab = (id: string | null) => useAppStore.setState(() => ({ currentTab: id })) export const setCurrentTab = (id: string | null) => useAppStore.setState(() => ({ currentTab: id }))
const setMapTabYear = (id: string, year: number | null) => export const setMapTabYear = (id: string, year: number | null) =>
useAppStore.setState((state) => { useAppStore.setState((state) => {
return { return {
mapTab: { mapTab: {
@ -42,17 +56,31 @@ const setMapTabYear = (id: string, year: number | null) =>
} }
}) })
const deleteMapTab = (id: string) => export const deleteMapTab = (id: string) =>
useAppStore.setState((state) => { useAppStore.setState((state) => {
const { [id]: _, ...remainingTabs } = state.mapTab; const { [id]: _, ...remainingTabs } = state.mapTab;
return { mapTab: remainingTabs }; const keys = Object.keys(remainingTabs);
return {
mapTab: remainingTabs,
currentTab: keys.length > 0 ? keys[keys.length - 1] : null,
};
}) })
export { export const addMapTab = () => {
deleteMapTab, const id = uuidv4();
getCurrentTab,
setCurrentTab, useAppStore.setState((state) => ({
setMapTabYear, mapTab: {
getColorScheme, ...state.mapTab,
setColorScheme [id]: {
year: null,
region: null,
district: null,
},
},
currentTab: id, // optionally switch to this new tab
}));
return id; // so you can use the new id in your components
} }

View File

@ -21,11 +21,12 @@ import { VectorImage } from 'ol/layer';
import { click, pointerMove } from 'ol/events/condition'; import { click, pointerMove } from 'ol/events/condition';
import { measureStyleFunction, modifyStyle } from '../components/map/Measure/MeasureStyles'; import { measureStyleFunction, modifyStyle } from '../components/map/Measure/MeasureStyles';
import MapBrowserEvent from 'ol/MapBrowserEvent'; import MapBrowserEvent from 'ol/MapBrowserEvent';
import { transform } from 'ol/proj'; import { get, transform } from 'ol/proj';
import { applyTransformations, calculateTransformations, fixedAspectRatioBox, zoomToFeature } from '../components/map/mapUtils'; import { applyTransformations, calculateTransformations, fixedAspectRatioBox, getGridCellPosition, zoomToFeature } from '../components/map/mapUtils';
import { setCurrentObjectId, setSelectedRegion } from './objects'; import { getSelectedRegion, setCurrentObjectId, setSelectedDistrict, setSelectedRegion } from './objects';
import View from 'ol/View'; import View from 'ol/View';
import { getPrintOrientation } from './print'; import { getPrintOrientation } from './print';
import { getDistrictsData, getRegionsData } from './regions';
export type Mode = 'edit' | 'view' | 'print' export type Mode = 'edit' | 'view' | 'print'
@ -45,7 +46,8 @@ interface MapState {
currentX: number | undefined; currentX: number | undefined;
currentY: number | undefined; currentY: number | undefined;
currentCoordinate: Coordinate | null; currentCoordinate: Coordinate | null;
statusText: string; statusText: string | null;
statusTextPosition: [number, number];
satMapsProvider: SatelliteMapsProvider; satMapsProvider: SatelliteMapsProvider;
selectedObjectType: number | null; selectedObjectType: number | null;
alignMode: boolean; alignMode: boolean;
@ -72,7 +74,7 @@ interface MapState {
figuresLayer: VectorLayer<VectorSource>; figuresLayer: VectorLayer<VectorSource>;
linesLayer: VectorLayer<VectorSource>; linesLayer: VectorLayer<VectorSource>;
regionsLayer: VectorImage; regionsLayer: VectorImage;
citiesLayer: VectorLayer; districtsLayer: VectorImage;
districtBoundLayer: VectorImage; districtBoundLayer: VectorImage;
imageLayer: ImageLayer<ImageStatic>; imageLayer: ImageLayer<ImageStatic>;
selectedArea: Feature | null; selectedArea: Feature | null;
@ -81,6 +83,7 @@ interface MapState {
measureModify: Modify; measureModify: Modify;
overlayLayer: VectorLayer; overlayLayer: VectorLayer;
regionSelect: Select; regionSelect: Select;
districtSelect: Select;
lineSelect: Select; lineSelect: Select;
previousView: View | undefined | null; previousView: View | undefined | null;
printArea: Extent | null; printArea: Extent | null;
@ -115,6 +118,8 @@ export const initializeMapState = (
// Region select // Region select
const regionSelect = new Select({ condition: pointerMove, style: selectStyle, layers: (layer) => layer.get('type') === 'region' }) const regionSelect = new Select({ condition: pointerMove, style: selectStyle, layers: (layer) => layer.get('type') === 'region' })
const districtSelect = new Select({ condition: pointerMove, style: selectStyle, layers: (layer) => layer.get('type') === 'district' })
// Line select // Line select
const lineSelect = new Select({ condition: click, style: highlightStyleRed, hitTolerance: hitTolerance, layers: (layer) => layer.get('type') === 'line', }) const lineSelect = new Select({ condition: click, style: highlightStyleRed, hitTolerance: hitTolerance, layers: (layer) => layer.get('type') === 'line', })
lineSelect.on('select', (e) => { lineSelect.on('select', (e) => {
@ -183,7 +188,7 @@ export const initializeMapState = (
const districtBoundLayer = new VectorImage({ style: new Style({ stroke: new Stroke({ color: 'red', width: 2 }), }) }) const districtBoundLayer = new VectorImage({ style: new Style({ stroke: new Stroke({ color: 'red', width: 2 }), }) })
const citiesLayer = new VectorLayer({ source: new VectorSource(), properties: { id: uuidv4(), name: 'Города' } }) const districtsLayer = new VectorImage({ source: new VectorSource(), properties: { id: uuidv4(), name: 'Населенные пункты', type: 'district' } })
const linesLayer = new VectorLayer({ const linesLayer = new VectorLayer({
source: new VectorSource(), source: new VectorSource(),
@ -266,10 +271,10 @@ export const initializeMapState = (
baseLayer, baseLayer,
satLayer, satLayer,
staticMapLayer, staticMapLayer,
regionsLayer,
districtBoundLayer,
citiesLayer,
linesLayer, linesLayer,
districtsLayer,
districtBoundLayer,
regionsLayer,
figuresLayer, figuresLayer,
drawingLayer, drawingLayer,
imageLayer, imageLayer,
@ -282,30 +287,48 @@ export const initializeMapState = (
}) })
map.addInteraction(regionSelect) map.addInteraction(regionSelect)
map.addInteraction(districtSelect)
map.addInteraction(lineSelect) map.addInteraction(lineSelect)
map.addInteraction(lineHover) map.addInteraction(lineHover)
map.addInteraction(figureSelect) map.addInteraction(figureSelect)
map.addInteraction(figureHover) map.addInteraction(figureHover)
// map.on('pointermove', function (e: MapBrowserEvent<UIEvent>) { const pointerHandler = (e: MapBrowserEvent<UIEvent>) => {
// setCurrentCoordinate(id, e.coordinate) setCurrentCoordinate(id, e.coordinate)
// const currentExtent = get('EPSG:3857')?.getExtent() as Extent const currentExtent = get('EPSG:3857')?.getExtent() as Extent
// const { tileX, tileY } = getGridCellPosition(e.coordinate[0], e.coordinate[1], currentExtent, Number(map?.getView().getZoom()?.toFixed(0))) const { tileX, tileY } = getGridCellPosition(e.coordinate[0], e.coordinate[1], currentExtent, Number(map?.getView().getZoom()?.toFixed(0)))
// setCurrentZ(id, Number(map?.getView().getZoom()?.toFixed(0))) setCurrentZ(id, Number(map?.getView().getZoom()?.toFixed(0)))
// setCurrentX(id, tileX) setCurrentX(id, tileX)
// setCurrentY(id, tileY) setCurrentY(id, tileY)
// const pixel = map?.getEventPixel(e.originalEvent) const pixel = map?.getEventPixel(e.originalEvent)
// if (pixel) { if (pixel) {
// map?.forEachFeatureAtPixel(pixel, function (feature, layer) { let found = false;
// if (layer.get('type') === 'region') { map?.forEachFeatureAtPixel(pixel, (feature, layer) => {
// if (feature.get('entity_id')) { if (layer.get("type") === "region" && feature.get("entity_id")) {
// setStatusText(id, feature.get('entity_id')) found = true
// }
// } const name = getRegionsData()?.find(region => region.id === feature.get("entity_id"))?.name || feature.get("entity_id") || ""
// }) setStatusText(id, name);
// } setStatusTextPosition(id, pixel[0], pixel[1]);
// }) }
if (layer.get("type") === "district" && feature.get("entity_id")) {
found = true
const name = getDistrictsData()?.find(district => district.id === feature.get("entity_id"))?.name || feature.get("entity_id") || ""
setStatusText(id, name);
setStatusTextPosition(id, pixel[0], pixel[1]);
}
});
if (!found) {
setStatusText(id, "");
}
}
}
map.on('pointermove', pointerHandler)
map.on('click', function (e: MapBrowserEvent<UIEvent>) { map.on('click', function (e: MapBrowserEvent<UIEvent>) {
if (getAlignMode(id)) { if (getAlignMode(id)) {
@ -332,11 +355,22 @@ export const initializeMapState = (
if (pixel) { if (pixel) {
map?.forEachFeatureAtPixel(pixel, function (feature, layer) { map?.forEachFeatureAtPixel(pixel, function (feature, layer) {
if (layer) { if (layer) {
if (layer.get('type') === 'region') { if (layer.get('type') === 'region' && layer.getOpacity() !== 0) {
zoomToFeature(id, feature as Feature) zoomToFeature(id, feature as Feature)
if (feature.get('entity_id')) { if (feature.get('entity_id')) {
setSelectedRegion(id, feature.get('entity_id')) setSelectedRegion(id, feature.get('entity_id'))
//regionsLayer.setVisible(false)
regionsLayer.setOpacity(0)
}
}
if (layer.get('type') === 'district' && layer.getOpacity() !== 0) {
zoomToFeature(id, feature as Feature)
if (feature.get('entity_id')) {
setSelectedDistrict(id, feature.get('entity_id'))
} }
} }
} }
@ -360,7 +394,9 @@ export const initializeMapState = (
} }
}) })
regionsLayer.setVisible(!isViewCovered) if (!getSelectedRegion(id)) {
regionsLayer.setOpacity(isViewCovered ? 0 : 1)
}
}) })
map.setView( map.setView(
@ -386,7 +422,8 @@ export const initializeMapState = (
currentX: undefined, currentX: undefined,
currentY: undefined, currentY: undefined,
currentCoordinate: null, currentCoordinate: null,
statusText: '', statusText: null,
statusTextPosition: [0, 0],
satMapsProvider: 'google', satMapsProvider: 'google',
selectedObjectType: null, selectedObjectType: null,
alignMode: false, alignMode: false,
@ -411,8 +448,8 @@ export const initializeMapState = (
staticMapLayer: staticMapLayer, staticMapLayer: staticMapLayer,
figuresLayer: figuresLayer, figuresLayer: figuresLayer,
linesLayer: linesLayer, linesLayer: linesLayer,
districtsLayer: districtsLayer,
regionsLayer: regionsLayer, regionsLayer: regionsLayer,
citiesLayer: citiesLayer,
districtBoundLayer: districtBoundLayer, districtBoundLayer: districtBoundLayer,
imageLayer: imageLayer, imageLayer: imageLayer,
selectedArea: null, selectedArea: null,
@ -421,6 +458,7 @@ export const initializeMapState = (
measureModify: measureModify, measureModify: measureModify,
nodeLayer: nodeLayer, nodeLayer: nodeLayer,
overlayLayer: overlayLayer, overlayLayer: overlayLayer,
districtSelect: districtSelect,
regionSelect: regionSelect, regionSelect: regionSelect,
lineSelect: lineSelect, lineSelect: lineSelect,
previousView: null, previousView: null,
@ -644,7 +682,16 @@ export const setCurrentCoordinate = (id: string, c: Coordinate | null) => useMap
} }
}) })
export const setStatusText = (id: string, t: string) => useMapStore.setState((state) => { export const setStatusTextPosition = (id: string, left: number, top: number) => useMapStore.setState((state) => {
return {
id: {
...state.id,
[id]: { ...state.id[id], statusTextPosition: [left, top] }
}
}
})
export const setStatusText = (id: string, t: string | null) => useMapStore.setState((state) => {
return { return {
id: { id: {
...state.id, ...state.id,

View File

@ -2,7 +2,7 @@ import { create } from 'zustand';
interface ObjectsState { interface ObjectsState {
id: Record<string, { id: Record<string, {
selectedRegion: number | undefined; selectedRegion: number | null;
selectedDistrict: number | null; selectedDistrict: number | null;
selectedCity: number | null; selectedCity: number | null;
selectedYear: number | null; selectedYear: number | null;
@ -16,7 +16,7 @@ export const useObjectsStore = create<ObjectsState>(() => ({
export const initializeObjectsState = ( export const initializeObjectsState = (
id: string, id: string,
selectedRegion: number | undefined, selectedRegion: number | null,
selectedDistrict: number | null, selectedDistrict: number | null,
selectedCity: number | null, selectedCity: number | null,
selectedYear: number | null, selectedYear: number | null,
@ -46,7 +46,7 @@ export const setSelectedCity = (id: string, city: number | null) => useObjectsSt
}) })
export const getSelectedRegion = (id: string) => useObjectsStore.getState().id[id].selectedRegion export const getSelectedRegion = (id: string) => useObjectsStore.getState().id[id].selectedRegion
export const setSelectedRegion = (id: string, region: number | undefined) => useObjectsStore.setState((state) => { export const setSelectedRegion = (id: string, region: number | null) => useObjectsStore.setState((state) => {
return { return {
id: { id: {
...state.id, ...state.id,

View File

@ -0,0 +1,36 @@
import { create } from 'zustand';
import { IDistrict, IRegion } from '../interfaces/gis';
export interface RegionsState {
regionsData: IRegion[],
districtsData: IDistrict[],
}
export const useRegionsStore = create<RegionsState>(() => ({
regionsData: [],
districtsData: []
}))
export const getRegionData = (id: number) => {
return useRegionsStore.getState().regionsData.find(region => region.id === id)
}
export const getRegionsData = () => {
return useRegionsStore.getState().regionsData
}
export const setRegionsData = (regionsData: any) => {
useRegionsStore.setState(() => ({ regionsData: regionsData }))
}
export const getDistrictData = (id: number) => {
return useRegionsStore.getState().districtsData.find(district => district.id === id)
}
export const getDistrictsData = () => {
return useRegionsStore.getState().districtsData
}
export const setDistrictsData = (districtsData: any) => {
useRegionsStore.setState(() => ({ districtsData: districtsData }))
}