refactored components

This commit is contained in:
2025-10-22 12:37:04 +09:00
parent 117cb7ac4d
commit a53fbc7912
4 changed files with 310 additions and 229 deletions

View File

@ -3,8 +3,7 @@ import 'ol/ol.css'
import { Modify } from 'ol/interaction'
import { ImageStatic, Vector as VectorSource } from 'ol/source'
import Feature from 'ol/Feature'
import { IRectCoords, SatelliteMapsProvider } from '../../interfaces/map'
import { Extent, getCenter } from 'ol/extent'
import { getCenter } from 'ol/extent'
import { highlightStyleRed, highlightStyleYellow } from './MapStyles'
import { customMapSource, googleMapsSatelliteSource, yandexMapsSatelliteSource } from './MapSources'
import { Geometry, Point } from 'ol/geom'
@ -14,33 +13,31 @@ import { addInteractions, getFeatureByEntityId, handleImageDrop, loadFeatures, p
import useSWR, { SWRConfiguration } from 'swr'
import { fetcher } from '../../http/axiosInstance'
import { BASE_URL } from '../../constants'
import { IconBoxMultiple, IconBoxPadding, IconChevronLeft, IconPlus, IconUpload, IconX, } from '@tabler/icons-react'
import { ICitySettings, IDistrict, IFigure, ILine } from '../../interfaces/gis'
import { IconBoxPadding, IconChevronLeft, } from '@tabler/icons-react'
import { ICitySettings, IFigure, ILine } from '../../interfaces/gis'
import axios from 'axios'
import MapToolbar from './MapToolbar/MapToolbar'
import MapStatusbar from './MapStatusbar/MapStatusbar'
import { setAlignMode, setSatMapsProvider, setTypeRoles, useMapStore, setMapLabel, } from '../../store/map'
import { setAlignMode, setTypeRoles, useMapStore, setMapLabel, } from '../../store/map'
import { useThrottle } from '@uidotdev/usehooks'
import ObjectTree from '../Tree/ObjectTree'
import { setCurrentObjectId, setSelectedDistrict, setSelectedRegion, setSelectedYear, useObjectsStore } from '../../store/objects'
import MapLayers from './MapLayers/MapLayers'
import ObjectParameters from './ObjectParameters/ObjectParameters'
import TabsPane, { ITabsPane } from './TabsPane/TabsPane'
//import { useSearchParams } from 'react-router-dom'
import GeoJSON from 'ol/format/GeoJSON'
import MapLegend from './MapLegend/MapLegend'
import GisService from '../../services/GisService'
import MapMode from './MapMode'
import { satMapsProviders, schemas } from '../../constants/map'
import MapPrint from './MapPrint/MapPrint'
import { Field, Menu, MenuButton, MenuList, MenuPopover, MenuTrigger, Combobox, Option, Button, Divider, Spinner, Portal, Dropdown, Tooltip, Drawer, DrawerHeader, DrawerBody, Text, Link, List } from '@fluentui/react-components'
import { IRegion } from '../../interfaces/fuel'
import { Combobox, Option, Button, Divider, Spinner, Portal, Tooltip, Drawer } from '@fluentui/react-components'
import { useAppStore } from '../../store/app'
import { getDistrictData, getRegionData, setDistrictsData, setRegionsData } from '../../store/regions'
import { ArrowLeft24Regular } from '@fluentui/react-icons'
import { setDistrictsData, setRegionsData } from '../../store/regions'
import View from 'ol/View'
import { Icon, Style } from 'ol/style'
import { fromLonLat } from 'ol/proj'
import MapRegionSelect from './MapRegionSelect/MapRegionSelect'
import MapLayersSelect from './MapLayers/MapLayersSelect'
import MapObjectSearch from './MapObjectSearch/MapObjectSearch'
const swrOptions: SWRConfiguration = {
revalidateOnFocus: false
@ -72,8 +69,7 @@ const MapComponent = ({
const { selectedYear, currentObjectId, selectedRegion, selectedDistrict } = useObjectsStore().id[id]
const {
mode, map, currentTool, alignMode, satMapsProvider,
selectedObjectType, file, measureDraw,
polygonExtent, rectCoords, draw, snap, translate,
selectedObjectType, measureDraw, draw, snap, translate,
drawingLayerSource,
satLayer, staticMapLayer, figuresLayer, linesLayer,
regionsLayer, districtBoundLayer, districtsLayer,
@ -275,13 +271,6 @@ const MapComponent = ({
}
}, [satMapsProvider, satLayer])
// Upload map overlay
const submitOverlay = async (file: File | null, polygonExtent: Extent | undefined, rectCoords: IRectCoords | undefined) => {
await GisService.uploadOverlay(file, polygonExtent, rectCoords).then(res => {
console.log(res)
})
}
// const { data: nodes } = useSWR('/nodes/all', () => fetcher('/nodes/all', BASE_URL.ems), { revalidateOnFocus: false })
// useEffect(() => {
@ -303,9 +292,6 @@ const MapComponent = ({
// }
// }, [nodes, nodeLayerSource])
const [searchObject, setSearchObject] = useState<string | undefined>("")
const throttledSearchObject = useThrottle(searchObject, 500)
useEffect(() => {
if (!selectedObjectType || !map) return
@ -367,12 +353,6 @@ const MapComponent = ({
return res
}), swrOptions)
const { data: searchData } = useSWR(
throttledSearchObject !== "" && selectedDistrict && selectedYear ? `/general/search/objects?q=${throttledSearchObject}&id_city=${selectedDistrict}&year=${selectedYear}` : null,
(url) => fetcher(url, BASE_URL.ems),
swrOptions
)
const { data: figuresData, isValidating: figuresValidating } = useSWR(
selectedDistrict && selectedYear ? `/gis/figures/all?city_id=${selectedDistrict}&year=${selectedYear}&offset=0&limit=${10000}` : null,
(url) => axios.get(url, {
@ -609,214 +589,18 @@ const MapComponent = ({
{active &&
<Portal mountNode={document.querySelector('#header-portal')}>
<div style={{ display: 'flex', gap: '1rem' }}>
<Combobox
placeholder="Поиск"
value={searchObject}
onOptionSelect={(_ev, data) => {
if (data.optionValue) {
setCurrentObjectId(id, data.optionValue);
setSearchObject(
searchData?.find((item: any) => item.id_object.toString() === data.optionValue)?.value ?? ""
);
}
}}
onChange={(e) => {
setSearchObject(e.currentTarget.value); // free typing like Mantine's onChange
}}
clearable
style={{ minWidth: 'auto' }}
>
{searchData
? searchData.map((item: { value: string; id_object: string }) => (
<Option key={item.id_object} value={item.id_object.toString()}>
{item.value}
</Option>
))
: null}
</Combobox>
<MapObjectSearch map_id={id} />
<Button icon={<IconBoxPadding />} appearance={alignMode ? 'primary' : 'transparent'} onClick={() => setAlignMode(id, !alignMode)} />
<Menu persistOnItemClick positioning={{ autoSize: true }}>
<MenuTrigger disableButtonEnhancement>
<MenuButton appearance='subtle' icon={<IconBoxMultiple />}>Слои</MenuButton>
</MenuTrigger>
<MenuPopover>
<MenuList style={{ padding: '1rem' }}>
<Field>Настройка видимости слоёв</Field>
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
<Field label="Спутниковые снимки">
<Dropdown
value={satMapsProviders.find(provider => provider.value === satMapsProvider)?.label}
selectedOptions={[satMapsProvider]}
onOptionSelect={(_ev, data) => {
if (data.optionValue) {
setSatMapsProvider(id, data.optionValue as SatelliteMapsProvider);
}
}}
>
{satMapsProviders.map((provider) => (
<Option key={provider.value} text={provider.label} value={provider.value}>
{provider.label}
</Option>
))}
</Dropdown>
</Field>
</div>
<div style={{
display: 'flex',
}}>
<Button icon={<IconUpload />} appearance='transparent' onClick={() => submitOverlay(file, polygonExtent, rectCoords)} />
<Button icon={<IconPlus />} appearance='transparent' title='Добавить подложку' />
</div>
<MapLayers map={map} />
</MenuList>
</MenuPopover>
</Menu>
<MapLayersSelect map_id={id} />
</div>
</Portal >
}
<div style={{ gridRow: '1 / span 1', display: 'grid', gridTemplateColumns: 'min-content auto', position: 'relative' }}>
<div style={{ gridColumn: '1 / span 1', width: !selectedRegion || (!!selectedRegion && !selectedYear) ? '300px' : 'min-content', height: '100%', position: 'relative', display: 'flex', zIndex: '2' }}>
<Drawer style={{ position: 'absolute', width: '300px', height: '100%', inset: 0, zIndex: 1 }} open={!selectedRegion || (!!selectedRegion && !selectedYear)} type='inline'>
{!!selectedRegion && !selectedYear &&
<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>
}
{!!selectedRegion && !selectedYear ?
<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)
map?.removeInteraction(districtSelect)
}}
onMouseEnter={() => {
const feature = getFeatureByEntityId(district.id, districtsLayer)
if (feature) {
districtSelect.getFeatures().push(feature)
}
}}
onMouseLeave={() => {
districtSelect.getFeatures().clear()
}}
>{district.name}</Link>
))}
</div>
}
{selectedDistrict &&
<Field label="Схема" >
<Dropdown
style={{ minWidth: 'auto' }}
value={selectedYear ? selectedYear.toString() : ""}
selectedOptions={[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} text={el}>
{el}
</Option>
))}
</Dropdown>
</Field>
}
</div>
</DrawerBody>
:
<DrawerBody>
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
{regionsData && regionsData.map((region: IRegion) => (
<Link key={region.id} onClick={() => {
setSelectedRegion(id, region.id)
map?.removeInteraction(regionSelect)
}}
onMouseEnter={() => {
const feature = getFeatureByEntityId(region.id, regionsLayer)
if (feature) {
regionSelect.getFeatures().push(feature)
}
}}
onMouseLeave={() => {
regionSelect.getFeatures().clear()
}}
>{region.name}</Link>
))}
</div>
</DrawerBody>
}
</Drawer>
<MapRegionSelect map_id={id} />
</div>
<div ref={mapElement} id={id} key={id} style={{ gridColumn: '2 / span 1', position: 'relative', width: '100%', height: '100%', maxHeight: '100%', zIndex: '1', filter: colorScheme === 'dark' ? 'invert(100%) hue-rotate(180deg)' : 'unset' }} onDragOver={(e) => e.preventDefault()} onDrop={(e) => handleImageDrop(e, id)}>

View File

@ -0,0 +1,67 @@
import { Button, Dropdown, Field, Menu, MenuButton, MenuList, MenuPopover, MenuTrigger, Option } from '@fluentui/react-components';
import { IconBoxMultiple, IconPlus, IconUpload } from '@tabler/icons-react';
import MapLayers from './MapLayers';
import { setSatMapsProvider, useMapStore } from '../../../store/map';
import { satMapsProviders } from '../../../constants/map';
import { IRectCoords, SatelliteMapsProvider } from '../../../interfaces/map';
import GisService from '../../../services/GisService';
import { Extent } from 'ol/extent';
const MapLayersSelect = ({
map_id
}: {
map_id: string
}) => {
const { map, satMapsProvider, file, polygonExtent, rectCoords } = useMapStore().id[map_id]
// Upload map overlay
const submitOverlay = async (file: File | null, polygonExtent: Extent | undefined, rectCoords: IRectCoords | undefined) => {
await GisService.uploadOverlay(file, polygonExtent, rectCoords).then(res => {
console.log(res)
})
}
return (
<Menu persistOnItemClick positioning={{ autoSize: true }}>
<MenuTrigger disableButtonEnhancement>
<MenuButton appearance='subtle' icon={<IconBoxMultiple />}>Слои</MenuButton>
</MenuTrigger>
<MenuPopover>
<MenuList style={{ padding: '1rem' }}>
<Field>Настройка видимости слоёв</Field>
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
<Field label="Спутниковые снимки">
<Dropdown
value={satMapsProviders.find(provider => provider.value === satMapsProvider)?.label}
selectedOptions={[satMapsProvider]}
onOptionSelect={(_ev, data) => {
if (data.optionValue) {
setSatMapsProvider(map_id, data.optionValue as SatelliteMapsProvider);
}
}}
>
{satMapsProviders.map((provider) => (
<Option key={provider.value} text={provider.label} value={provider.value}>
{provider.label}
</Option>
))}
</Dropdown>
</Field>
</div>
<div style={{
display: 'flex',
}}>
<Button icon={<IconUpload />} appearance='transparent' onClick={() => submitOverlay(file, polygonExtent, rectCoords)} />
<Button icon={<IconPlus />} appearance='transparent' title='Добавить подложку' />
</div>
<MapLayers map={map} />
</MenuList>
</MenuPopover>
</Menu>
)
}
export default MapLayersSelect

View File

@ -0,0 +1,58 @@
import { Combobox, Option } from '@fluentui/react-components';
import { useState } from 'react'
import { setCurrentObjectId, useObjectsStore } from '../../../store/objects';
import useSWR, { SWRConfiguration } from 'swr';
import { useThrottle } from '@uidotdev/usehooks';
import { BASE_URL } from '../../../constants';
import { fetcher } from '../../../http/axiosInstanceNest';
const swrOptions: SWRConfiguration = {
revalidateOnFocus: false
}
const MapObjectSearch = ({
map_id
}: {
map_id: string
}) => {
const [searchObject, setSearchObject] = useState<string | undefined>("")
const throttledSearchObject = useThrottle(searchObject, 500)
const { selectedYear, selectedDistrict } = useObjectsStore().id[map_id]
const { data: searchData } = useSWR(
throttledSearchObject !== "" && selectedDistrict && selectedYear ? `/general/search/objects?q=${throttledSearchObject}&id_city=${selectedDistrict}&year=${selectedYear}` : null,
(url) => fetcher(url, BASE_URL.ems),
swrOptions
)
return (
<Combobox
placeholder="Поиск"
value={searchObject}
onOptionSelect={(_ev, data) => {
if (data.optionValue) {
setCurrentObjectId(map_id, data.optionValue);
setSearchObject(
searchData?.find((item: any) => item.id_object.toString() === data.optionValue)?.value ?? ""
);
}
}}
onChange={(e) => {
setSearchObject(e.currentTarget.value); // free typing like Mantine's onChange
}}
clearable
style={{ minWidth: 'auto' }}
>
{searchData
? searchData.map((item: { value: string; id_object: string }) => (
<Option key={item.id_object} value={item.id_object.toString()}>
{item.value}
</Option>
))
: null}
</Combobox>
)
}
export default MapObjectSearch

View File

@ -0,0 +1,172 @@
import { Button, Drawer, DrawerBody, DrawerHeader, Dropdown, Field, Link, Option, Text } from '@fluentui/react-components'
import { ArrowLeft24Regular } from '@fluentui/react-icons'
import { setSelectedDistrict, setSelectedRegion, setSelectedYear, useObjectsStore } from '../../../store/objects'
import { useMapStore } from '../../../store/map'
import { fromExtent } from 'ol/geom/Polygon'
import { getDistrictData, getRegionData, setDistrictsData, useRegionsStore } from '../../../store/regions'
import { IconX } from '@tabler/icons-react'
import { IDistrict, IRegion } from '../../../interfaces/gis'
import { getFeatureByEntityId } from '../mapUtils'
import useSWR, { SWRConfiguration } from 'swr'
import { fetcher } from '../../../http/axiosInstanceNest'
import { BASE_URL } from '../../../constants'
import { schemas } from '../../../constants/map'
const swrOptions: SWRConfiguration = {
revalidateOnFocus: false
}
const MapRegionSelect = ({
map_id
}: {
map_id: string
}) => {
const { map, districtSelect, regionsLayer, regionSelect, districtsLayer } = useMapStore().id[map_id]
const { selectedRegion, selectedYear, selectedDistrict } = useObjectsStore().id[map_id]
const { regionsData } = useRegionsStore()
const { data: districtsData } = useSWR(selectedRegion ? `/general/districts/?region_id=${selectedRegion}` : null, (url) => fetcher(url, BASE_URL.ems).then(res => {
setDistrictsData(res)
return res
}), swrOptions)
return (
<Drawer style={{ position: 'absolute', width: '300px', height: '100%', inset: 0, zIndex: 1 }} open={!selectedRegion || (!!selectedRegion && !selectedYear)} type='inline'>
{!!selectedRegion && !selectedYear &&
<DrawerHeader style={{ flexDirection: 'row' }}>
<Button icon={<ArrowLeft24Regular />} appearance='subtle' onClick={() => {
if (selectedDistrict) {
setSelectedDistrict(map_id, null)
districtSelect.getFeatures().clear()
regionsLayer.setOpacity(1)
} else {
setSelectedRegion(map_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(map_id, null)
setSelectedDistrict(map_id, null)
setSelectedRegion(map_id, null)
if (map) {
const extent = regionsLayer.getSource()?.getExtent()
if (extent) {
map.getView().fit(fromExtent(extent), { duration: 100 })
regionsLayer.setOpacity(1)
}
}
}} />
</DrawerHeader>
}
{!!selectedRegion && !selectedYear ?
<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(map_id, district.id)
map?.removeInteraction(districtSelect)
}}
onMouseEnter={() => {
const feature = getFeatureByEntityId(district.id, districtsLayer)
if (feature) {
districtSelect.getFeatures().push(feature)
}
}}
onMouseLeave={() => {
districtSelect.getFeatures().clear()
}}
>{district.name}</Link>
))}
</div>
}
{selectedDistrict &&
<Field label="Схема" >
<Dropdown
style={{ minWidth: 'auto' }}
value={selectedYear ? selectedYear.toString() : ""}
selectedOptions={[selectedYear ? selectedYear.toString() : ""]}
onOptionSelect={(_ev, data) => {
if (data.optionValue) {
setSelectedYear(map_id, Number(data.optionValue));
} else {
setSelectedYear(map_id, null);
}
}}
>
{schemas.map((el) => (
<Option key={el} value={el} text={el}>
{el}
</Option>
))}
</Dropdown>
</Field>
}
</div>
</DrawerBody>
:
<DrawerBody>
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
{regionsData && regionsData.map((region: IRegion) => (
<Link key={region.id} onClick={() => {
setSelectedRegion(map_id, region.id)
map?.removeInteraction(regionSelect)
}}
onMouseEnter={() => {
const feature = getFeatureByEntityId(region.id, regionsLayer)
if (feature) {
regionSelect.getFeatures().push(feature)
}
}}
onMouseLeave={() => {
regionSelect.getFeatures().clear()
}}
>{region.name}</Link>
))}
</div>
</DrawerBody>
}
</Drawer>
)
}
export default MapRegionSelect