This commit is contained in:
cracklesparkle
2024-12-06 12:42:34 +09:00
parent bd0a317e76
commit e9595f9703
16 changed files with 770 additions and 390 deletions

View File

@ -4,17 +4,15 @@ import Map from 'ol/Map'
import View from 'ol/View'
import { Draw, Modify, Select, Snap, Translate } from 'ol/interaction'
import { ImageStatic, OSM, Vector as VectorSource, XYZ } from 'ol/source'
import { Tile as TileLayer, Vector as VectorLayer } from 'ol/layer'
import { Tile as TileLayer, VectorImage, Vector as VectorLayer } from 'ol/layer'
import { click, never, platformModifierKeyOnly, primaryAction, shiftKeyOnly } from 'ol/events/condition'
import Feature from 'ol/Feature'
import { IRectCoords, SatelliteMapsProvider } from '../../interfaces/map'
import { Extent } from 'ol/extent'
import { drawingLayerStyle, highlightStyleRed, highlightStyleYellow, overlayStyle, regionsLayerStyle } from './MapStyles'
import { googleMapsSatelliteSource, regionsLayerSource, yandexMapsSatelliteSource } from './MapSources'
import { mapCenter } from './MapConstants'
import ImageLayer from 'ol/layer/Image'
import VectorImageLayer from 'ol/layer/VectorImage'
import { LineString, Point } from 'ol/geom'
import { LineString, Point, SimpleGeometry } from 'ol/geom'
import { fromExtent } from 'ol/geom/Polygon'
import Collection from 'ol/Collection'
import { Coordinate } from 'ol/coordinate'
@ -24,22 +22,37 @@ import { get, transform } from 'ol/proj'
import useSWR from 'swr'
import { fetcher } from '../../http/axiosInstance'
import { BASE_URL } from '../../constants'
import { Accordion, ActionIcon, Autocomplete, Box, CloseButton, Flex, Select as MantineSelect, Text as MantineText, MantineStyleProp, rem, ScrollAreaAutosize, Slider, useMantineColorScheme, Portal, Tree, Group, TreeNodeData, Button, useTree, Timeline, Text, Stack, Overlay } from '@mantine/core'
import { IconChevronDown, IconPlus, IconSettings, IconTable, IconUpload } from '@tabler/icons-react'
import { Accordion, ActionIcon, Autocomplete, Box, CloseButton, Flex, Select as MantineSelect, MantineStyleProp, rem, ScrollAreaAutosize, Slider, useMantineColorScheme, Portal, Timeline, Text, Stack, NavLink, Grid, Checkbox } from '@mantine/core'
import { IconPlus, IconSearch, IconSettings, IconTable, IconUpload } from '@tabler/icons-react'
import { getGridCellPosition } from './mapUtils'
import { IFigure, ILine } from '../../interfaces/gis'
import axios from 'axios'
import ObjectParameter from './ObjectParameter'
import { IObjectData, IObjectList, IObjectParam } from '../../interfaces/objects'
import ObjectData from './ObjectData'
import { IObjectParam } from '../../interfaces/objects'
import MapToolbar from './MapToolbar/MapToolbar'
import MapStatusbar from './MapStatusbar/MapStatusbar'
import { measureStyleFunction, modifyStyle } from './Measure/MeasureStyles'
import { useMapStore } from '../../store/map'
import { MapTreeCheckbox } from './MapTree/MapTreeCheckbox'
import { v4 as uuidv4 } from 'uuid'
import { useThrottle } from '@uidotdev/usehooks'
import ObjectTree from '../Tree/ObjectTree'
import { setCurrentObjectId, setSelectedCity, setSelectedYear, useObjectsStore } from '../../store/objects'
// Settings for cities
const citySettings = [
{
city_id: 145,
// scale: 10000,
scale: 9000,
offset_x: 14442665.697619518,
offset_y: 8884520.63524492,
rotation: 0
}
]
const MapComponent = () => {
const { selectedCity, selectedYear, currentObjectId } = useObjectsStore()
const mapState = useMapStore()
const [currentCoordinate, setCurrentCoordinate] = useState<Coordinate | null>(null)
@ -121,24 +134,27 @@ const MapComponent = () => {
}
}))
const figuresLayer = useRef<VectorLayer>(new VectorLayer({
const figuresLayer = useRef<VectorImage>(new VectorImage({
source: new VectorSource(),
declutter: true,
properties: {
id: uuidv4(),
name: 'Фигуры'
}
}))
const linesLayer = useRef<VectorLayer>(new VectorLayer({
const linesLayer = useRef<VectorImage>(new VectorImage({
source: new VectorSource(),
declutter: true,
properties: {
id: uuidv4(),
name: 'Линии'
}
}))
const regionsLayer = useRef<VectorImageLayer>(new VectorImageLayer({
const regionsLayer = useRef<VectorImage>(new VectorImage({
source: regionsLayerSource,
declutter: true,
style: regionsLayerStyle,
properties: {
id: uuidv4(),
@ -356,14 +372,14 @@ const MapComponent = () => {
],
target: mapElement.current as HTMLDivElement,
view: new View({
center: transform([129.7659541, 62.009504], 'EPSG:4326', 'EPSG:3857'),//center: fromLonLat([130.401113, 67.797368]),
zoom: 17,
center: transform([129.7466541, 62.083504], 'EPSG:4326', 'EPSG:3857'),//center: fromLonLat([130.401113, 67.797368]),
zoom: 16,
maxZoom: 21,
//extent: mapExtent,
}),
})
map.current.on('pointermove', function (e: MapBrowserEvent<any>) {
map.current.on('pointermove', function (e: MapBrowserEvent<UIEvent>) {
setCurrentCoordinate(e.coordinate)
const currentExtent = get('EPSG:3857')?.getExtent() as Extent
const { tileX, tileY } = getGridCellPosition(e.coordinate[0], e.coordinate[1], currentExtent, Number(map.current?.getView().getZoom()?.toFixed(0)))
@ -372,7 +388,7 @@ const MapComponent = () => {
setCurrentY(tileY)
})
map.current.on('click', function (e: MapBrowserEvent<any>) {
map.current.on('click', function (e: MapBrowserEvent<UIEvent>) {
const pixel = map.current?.getEventPixel(e.originalEvent)
if (pixel) {
@ -430,7 +446,7 @@ const MapComponent = () => {
}
}, [mapState.currentTool])
const [satelliteOpacity, setSatelliteOpacity] = useState<number>(1)
const [satelliteOpacity, setSatelliteOpacity] = useState<number>(0)
const [statusText, setStatusText] = useState('')
@ -513,31 +529,14 @@ const MapComponent = () => {
}
}, [nodes])
const [currentObjectId, setCurrentObjectId] = useState<string | null>(null)
const [selectedCity, setSelectedCity] = useState<number | null>(null)
const [selectedYear, setSelectedYear] = useState<number | null>(2023)
const [citiesPage, setCitiesPage] = useState<number>(0)
const [searchCity, setSearchCity] = useState<string | undefined>("")
const throttledSearchCity = useThrottle(searchCity, 500);
const { data: existingObjectsList } = useSWR(
selectedYear && selectedCity ? `/general/objects/list?year=${selectedYear}&city_id=${selectedCity}&planning=0` : null,
(url) => fetcher(url, BASE_URL.ems),
{
revalidateOnFocus: false
}
)
const [searchObject, setSearchObject] = useState<string | undefined>("")
const throttledSearchObject = useThrottle(searchObject, 500);
const { data: planningObjectsList } = useSWR(
selectedYear && selectedCity ? `/general/objects/list?year=${selectedYear}&city_id=${selectedCity}&planning=1` : null,
(url) => fetcher(url, BASE_URL.ems),
{
revalidateOnFocus: false
}
)
const [objectsList, setObjectsList] = useState<TreeNodeData[] | null>(null)
const [selectedObjectList, setSelectedObjectList] = useState<number | null>(null)
useEffect(() => {
@ -549,7 +548,7 @@ const MapComponent = () => {
if (selectedObjectList == feature.get('type')) {
feature.setStyle(highlightStyleYellow);
} else {
feature.setStyle(null); // Reset to default style
feature.setStyle(undefined); // Reset to default style
}
})
}
@ -560,81 +559,70 @@ const MapComponent = () => {
if (selectedObjectList == feature.get('type')) {
feature.setStyle(highlightStyleYellow);
} else {
feature.setStyle(null); // Reset to default style
feature.setStyle(undefined); // Reset to default style
}
})
}
}, [selectedObjectList])
useEffect(() => {
if (existingObjectsList && planningObjectsList) {
setObjectsList([
{
label: 'Существующие',
value: 'existing',
children: existingObjectsList.map((list: IObjectList) => ({
label:list.name,
count: list.count,
value: list.id,
})),
},
{
label: 'Планируемые',
value: 'planning',
children: planningObjectsList.map((list: IObjectList) => ({
label: list.name,
count: list.count,
value: list.id
}))
}
])
}
}, [existingObjectsList, planningObjectsList])
useEffect(() => {
if (currentObjectId) {
if (figuresLayer.current) {
// Reset styles and apply highlight to matching features
figuresLayer.current.getSource()?.getFeatures().forEach((feature) => {
figuresLayer.current.getSource()?.getFeatures().forEach((feature: Feature) => {
if (currentObjectId == feature.get('object_id')) {
feature.setStyle(highlightStyleRed);
const geometry = feature.getGeometry()
if (geometry) {
map.current?.getView().fit(geometry as SimpleGeometry, { duration: 500, maxZoom: 18 })
//map.current?.getView().animate({ center: calculateCenter(geometry as SimpleGeometry).center })
}
} else {
feature.setStyle(null); // Reset to default style
feature.setStyle(undefined); // Reset to default style
}
})
}
if (linesLayer.current) {
// Reset styles and apply highlight to matching features
linesLayer.current.getSource()?.getFeatures().forEach((feature) => {
linesLayer.current.getSource()?.getFeatures().forEach((feature: Feature) => {
if (currentObjectId == feature.get('object_id')) {
feature.setStyle(highlightStyleRed);
const geometry = feature.getGeometry()
if (geometry) {
map.current?.getView().fit(geometry as SimpleGeometry, { duration: 500, maxZoom: 18 })
}
} else {
feature.setStyle(null); // Reset to default style
feature.setStyle(undefined); // Reset to default style
}
})
}
}
}, [currentObjectId])
const { data: currentObjectData } = useSWR(
currentObjectId ? `/general/objects/${currentObjectId}` : null,
(url) => fetcher(url, BASE_URL.ems),
{
revalidateOnFocus: false
}
)
const { data: valuesData } = useSWR(
currentObjectId ? `/general/values/all?object_id=${currentObjectId}` : null,
(url) => fetcher(url, BASE_URL.ems),
{
revalidateOnFocus: false
revalidateOnFocus: false,
revalidateIfStale: false
}
)
const { data: citiesData } = useSWR(
`/general/cities/all?limit=${10}&offset=${citiesPage || 0}${searchCity ? `&search=${searchCity}` : ''}`,
`/general/cities/all?limit=${10}&offset=${citiesPage || 0}${throttledSearchCity ? `&search=${throttledSearchCity}` : ''}`,
(url) => fetcher(url, BASE_URL.ems),
{
revalidateOnFocus: false
}
)
const { data: searchData } = useSWR(
throttledSearchObject !== "" && selectedCity && selectedYear ? `/general/search/objects?q=${throttledSearchObject}&id_city=${selectedCity}&year=${selectedYear}` : null,
(url) => fetcher(url, BASE_URL.ems),
{
revalidateOnFocus: false
@ -664,16 +652,45 @@ const MapComponent = () => {
)
useEffect(() => {
let offset_x = 14444582.697619518
let offset_y = 8866450.63524492
let scale = {
w: 100000,
h: 100000
}
let rotation = 0
if (citySettings.find(el => el.city_id === selectedCity)) {
console.log("City settings found")
if (citySettings.find(el => el.city_id === selectedCity)?.scale) {
scale = {
w: citySettings.find(el => el.city_id === selectedCity).scale,
h: citySettings.find(el => el.city_id === selectedCity).scale
}
}
if (citySettings.find(el => el.city_id === selectedCity)?.offset_x && citySettings.find(el => el.city_id === selectedCity)?.offset_y) {
offset_x = citySettings.find(el => el.city_id === selectedCity).offset_x
offset_y = citySettings.find(el => el.city_id === selectedCity).offset_y
}
if (citySettings.find(el => el.city_id === selectedCity)?.rotation) {
rotation = citySettings.find(el => el.city_id === selectedCity)?.rotation
}
} else {
console.log("City settings NOT found")
}
if (Array.isArray(figuresData)) {
figuresLayer.current.getSource()?.clear()
if (figuresData.length > 0) {
const scaling = {
w: 10000, // responseData[0].width
h: 10000 // responseData[0].width
w: scale.w, // responseData[0].width
h: scale.h // responseData[0].width
}
figuresData.map((figure: IFigure) => {
processFigure(figure, scaling, mapCenter, figuresLayer)
processFigure(figure, scaling, [offset_x, offset_y], figuresLayer)
})
}
}
@ -682,17 +699,54 @@ const MapComponent = () => {
linesLayer.current.getSource()?.clear()
if (linesData.length > 0) {
const scaling = {
w: 10000, // responseData[0].width
h: 10000 // responseData[0].width
w: scale.w, // responseData[0].width
h: scale.h // responseData[0].width
}
linesData.map((line: ILine) => {
processLine(line, scaling, mapCenter, linesLayer)
processLine(line, scaling, [offset_x, offset_y], linesLayer)
})
}
}
}, [figuresData, linesData, selectedCity, selectedYear])
useEffect(() => {
// if (map.current) {
// map.current.on('postcompose', function () {
// if (colorScheme === 'dark') {
// document.querySelector('canvas').style.filter = 'grayscale(80%) invert(100%) hue-rotate(180deg)'
// } else {
// document.querySelector('canvas').style.filter = 'grayscale(0%) invert(0%) hue-rotate(0deg)'
// }
// })
// }
if (baseLayer.current) {
baseLayer.current.on('prerender', function (e) {
if (colorScheme === 'dark') {
if (e.context) {
const context = e.context as CanvasRenderingContext2D;
context.filter = 'grayscale(80%) invert(100%) hue-rotate(180deg) ';
context.globalCompositeOperation = 'source-over';
}
} else {
if (e.context) {
const context = e.context as CanvasRenderingContext2D;
context.filter = 'none';
}
}
})
baseLayer.current.on('postrender', function (e) {
if (e.context) {
const context = e.context as CanvasRenderingContext2D;
context.filter = 'none';
}
})
}
}, [colorScheme])
return (
<Box w={'100%'} h='100%' pos={'relative'}>
<Portal target='#header-portal'>
@ -703,11 +757,37 @@ const MapComponent = () => {
<MantineSelect variant='filled' value={satMapsProvider} data={[{ label: 'Google', value: 'google' }, { label: 'Yandex', value: 'yandex' }, { label: 'Custom', value: 'custom' }]} onChange={(value) => setSatMapsProvider(value as SatelliteMapsProvider)} />
</Flex>
<form>
<Autocomplete
placeholder="Поиск"
flex={'1'}
data={searchData ? searchData.map((item: { value: string, id_object: string }) => ({ label: item.value, value: item.id_object.toString() })) : []}
//onSelect={(e) => console.log(e.currentTarget.value)}
onChange={(value) => setSearchObject(value)}
onOptionSubmit={(value) => setCurrentObjectId(value)}
rightSection={
searchObject !== '' && (
<CloseButton
size="sm"
onMouseDown={(event) => event.preventDefault()}
onClick={() => {
setSearchObject('')
//setSelectedCity(null)
}}
aria-label="Clear value"
/>
)
}
leftSection={<IconSearch size={16} />}
value={searchObject}
/>
</form>
<form>
<Autocomplete
placeholder="Район"
flex={'1'}
data={citiesData ? citiesData.map((item: any) => ({ label: item.name, value: item.id.toString() })) : []}
data={citiesData ? citiesData.map((item: { name: string, id: number }) => ({ label: item.name, value: item.id.toString() })) : []}
//onSelect={(e) => console.log(e.currentTarget.value)}
onChange={(value) => setSearchCity(value)}
onOptionSubmit={(value) => setSelectedCity(Number(value))}
@ -729,20 +809,12 @@ const MapComponent = () => {
</form>
<MantineSelect
data={[
{ label: '2018', value: '2018' },
{ label: '2019', value: '2019' },
{ label: '2020', value: '2020' },
{ label: '2021', value: '2021' },
{ label: '2022', value: '2022' },
{ label: '2023', value: '2023' },
{ label: '2024', value: '2024' },
]}
w='84px'
data={['2018', '2019', '2020', '2021', '2022', '2023', '2024'].map(el => ({ label: el, value: el }))}
onChange={(e) => {
setSelectedYear(Number(e))
}}
defaultValue={selectedYear?.toString()}
//variant="unstyled"
allowDeselect={false}
/>
@ -766,7 +838,7 @@ const MapComponent = () => {
<Flex direction='column' mah={'86%'} pl='sm' style={{
...mapControlsStyle,
maxWidth: '300px',
maxWidth: '340px',
width: '100%',
top: '8px',
left: '8px',
@ -791,165 +863,87 @@ const MapComponent = () => {
</ActionIcon>
</Flex>
<Accordion variant='filled' style={{ backgroundColor: 'transparent' }} defaultValue='Объекты'>
<Accordion.Item key={'objects'} value={'Объекты'}>
<Accordion.Control icon={<IconTable />}>{'Объекты'}</Accordion.Control>
<Accordion.Panel>
{objectsList &&
<Tree
data={objectsList}
selectOnClick
levelOffset={23}
renderNode={({ node, expanded, hasChildren, elementProps }) => (
<Group gap={6} {...elementProps} onClick={async (e) => {
elementProps.onClick(e)
if (node.value !== 'existing' && node.value !== 'planning') {
setSelectedObjectList(Number(node.value))
try {
// Fetch data from the API based on the node's value
await fetcher(`/general/objects/list?type=${node.value}&city_id=${selectedCity}&year=${selectedYear}&planning=0`, BASE_URL.ems).then(res => {
setObjectsList((prevList) => {
return prevList.map((item) => {
return {
...item,
children: [
...(item.children.map(child => {
if (child.value == node.value) {
return {
...child,
children: res.map(object => {
return {
label: object.object_id,
value: object.object_id
}
})
}
} else {
return { ...child }
}
}) || []),
],
}
}
)
}
);
})
} catch (error) {
console.error('Error fetching data:', error);
}
}
}}>
{(hasChildren || node?.count) && (
<IconChevronDown
size={18}
style={{ transform: expanded ? 'rotate(180deg)' : 'rotate(0deg)' }}
/>
)}
<MantineText size='sm'>{`${node.label} ${node?.count ? `(${node.count})` : ''}`}</MantineText>
</Group>
)}
/>
}
</Accordion.Panel>
</Accordion.Item>
{/* {currentObjectId &&
<Accordion.Item key={'current_object'} value={currentObjectId}>
<Accordion.Control icon={<IconTable />}>
{'Текущий объект'}
</Accordion.Control>
{selectedCity &&
<Accordion variant='filled' style={{ backgroundColor: 'transparent' }} defaultValue='Объекты'>
<Accordion.Item key={'objects'} value={'Объекты'}>
<Accordion.Control icon={<IconTable />}>{'Объекты'}</Accordion.Control>
<Accordion.Panel>
<ObjectData {...currentObjectData as IObjectData} />
<ObjectTree />
</Accordion.Panel>
</Accordion.Item>
} */}
{valuesData &&
<Accordion.Item key={'parameters'} value={'Параметры объекта'}>
<Accordion.Control icon={<IconTable />}>{'Параметры объекта'}</Accordion.Control>
<Accordion.Panel>
<Flex gap={'sm'} direction={'column'}>
{Array.isArray(valuesData) &&
// Group objects by `id_param`
Object.entries(
valuesData.reduce((acc, param) => {
if (!acc[param.id_param]) {
acc[param.id_param] = [];
}
acc[param.id_param].push(param);
return acc;
}, {} as Record<string, IObjectParam[]>)
).map(([id_param, params]) => {
// Step 1: Sort the parameters by date_s (start date) and date_po (end date)
const sortedParams = params.sort((b, a) => {
const dateA = new Date(a.date_s || 0);
const dateB = new Date(b.date_s || 0);
return dateA.getTime() - dateB.getTime();
});
{valuesData &&
<Accordion.Item key={'parameters'} value={'Параметры объекта'}>
<Accordion.Control icon={<IconTable />}>{'Параметры объекта'}</Accordion.Control>
<Accordion.Panel>
<Flex gap={'sm'} direction={'column'}>
{Array.isArray(valuesData) &&
Object.entries(
valuesData.reduce((acc, param) => {
if (!acc[param.id_param]) {
acc[param.id_param] = [];
}
acc[param.id_param].push(param);
return acc;
}, {} as Record<string, IObjectParam[]>)
).map(([id_param, params]) => {
// Step 1: Sort the parameters by date_s (start date) and date_po (end date)
const sortedParams = (params as IObjectParam[]).sort((b, a) => {
const dateA = new Date(a.date_s || 0);
const dateB = new Date(b.date_s || 0);
return dateA.getTime() - dateB.getTime();
});
return sortedParams.length > 1 ? (
// Step 2: Render Mantine Timeline for multiple entries with the same `id_param`
<Timeline
key={id_param}
active={0}
bulletSize={18}
>
{sortedParams.map((param, index) => (
<Timeline.Item
key={index}
style={{
filter: index !== 0 ? 'grayscale(100%) opacity(50%)' : 'none'
}}
>
<ObjectParameter param={param} showLabel={false} />
<Text size='xs'>
{new Date(param.date_s).toLocaleDateString('en-GB').split('/').join('.')} - {new Date(param.date_po).toLocaleDateString('en-GB') === '01/01/1970' ? 'По сей день' : new Date(param.date_po).toLocaleDateString('en-GB').split('/').join('.')}
</Text>
</Timeline.Item>
))}
</Timeline>
) : (
// Step 3: Render ObjectParameter directly if there's only one entry
<ObjectParameter key={id_param} param={sortedParams[0]} />
);
})
}
</Flex>
</Accordion.Panel>
</Accordion.Item>
}
</Accordion>
return sortedParams.length > 1 ? (
// Step 2: Render Mantine Timeline for multiple entries with the same `id_param`
<Timeline
key={id_param}
active={0}
bulletSize={18}
>
{sortedParams.map((param: IObjectParam, index: number) => (
<Timeline.Item
key={index}
style={{
filter: index !== 0 ? 'grayscale(100%) opacity(50%)' : 'none'
}}
>
<ObjectParameter param={param} showLabel={false} />
<Text size='xs'>
{new Date(param.date_s).toLocaleDateString('en-GB').split('/').join('.')} - {new Date(param.date_po).toLocaleDateString('en-GB') === '01/01/1970' ? 'По сей день' : new Date(param.date_po).toLocaleDateString('en-GB').split('/').join('.')}
</Text>
</Timeline.Item>
))}
</Timeline>
) : (
<ObjectParameter key={id_param} param={sortedParams[0]} />
);
})
}
</Flex>
</Accordion.Panel>
</Accordion.Item>
}
</Accordion>
}
<Accordion variant='filled' style={{ backgroundColor: 'transparent' }} defaultValue='Слои'>
<Accordion.Item key={'objects'} value={'Слои'}>
<Accordion.Control icon={<IconTable />}>{'Слои'}</Accordion.Control>
<Accordion.Panel>
{map.current?.getLayers().getArray() &&
<Tree
data={map.current?.getLayers().getArray().map(layer => {
return {
label: layer.get('name'),
value: layer.get('id')
}
}) as TreeNodeData[]}
//selectOnClick
expandOnClick={false}
levelOffset={23}
renderNode={MapTreeCheckbox}
/>
}
{map.current?.getLayers().getArray() && map.current?.getLayers().getArray().map((layer, index) => (
<Flex key={`layer-${index}`} gap='xs' align='center'>
<Checkbox.Indicator
checked={layer.getLayerState().visible}
onClick={() => layer.getLayerState().visible ? layer.setVisible(false) : layer.setVisible(true)}
/>
<NavLink p={0} label={layer.get('name')} onClick={() => { console.log(layer.getLayerState()) }} />
</Flex>
))}
</Accordion.Panel>
</Accordion.Item>
</Accordion>
</Stack>
</ScrollAreaAutosize>
</Flex>