import { getBadgeStatus } from '@/components/shared/ThingShared'; import { DURATION_POLLING_PRESENTATIONS } from '@/constants'; import useGetShipSos from '@/models/slave/sgw/useShipSos'; import { apiSearchThings } from '@/services/master/ThingController'; import { formatUnixTime } from '@/utils/slave/sgw/timeUtils'; import { DownOutlined, FilterOutlined, UpOutlined } from '@ant-design/icons'; import type { ActionType, ProColumns } from '@ant-design/pro-components'; import { ParamsType, useToken } from '@ant-design/pro-components'; import ProTable from '@ant-design/pro-table'; import { useIntl } from '@umijs/max'; import { Button, Drawer, Flex, message, notification, Tag, Tooltip, Typography, } from 'antd'; import { Feature } from 'ol'; import VectorLayer from 'ol/layer/Vector'; import VectorSource from 'ol/source/Vector'; import { useCallback, useEffect, useRef, useState } from 'react'; import TagState from '../../../../components/shared/TagState'; import { BaseMap } from './components/BaseMap'; import MultipleShips from './components/MultipleShips'; import ShipDetail, { ShipDetailMessageAction } from './components/ShipDetail'; import ShipSearchForm, { SearchShipResponse, } from './components/ShipSearchForm'; import { renderSosOverlay } from './components/SosOverlay'; import VietNamMap, { VietNamMapRef } from './components/VietNamMap'; import { getShipNameColor } from './config/MapConfig'; import styles from './index.less'; import { DATA_LAYER, GPSParseResult, TagStateCallbackPayload, TEMPORARY_LAYER, } from './type'; const MapPage = () => { // Create a ref for VietNamMap to access BaseMap methods const vietNamMapRef = useRef(null); const baseMap = useRef(null); // Lưu các cancel functions của sosPulse để cleanup const sosPulseCancelRefs = useRef<(() => void)[]>([]); // Lưu các cleanup functions của overlay để cleanup const overlayCleanupRefs = useRef void>>(new Map()); const [stateQuery, setStateQuery] = useState< TagStateCallbackPayload | undefined >(undefined); const token = useToken(); const intl = useIntl(); const [things, setThings] = useState(null); const [isCollapsed, setIsCollapsed] = useState(true); const tableRef = useRef(); const [notificationApi, contextHolder] = notification.useNotification(); const [messageApi, messageContextHolder] = message.useMessage(); const [showMultiShipsDrawer, setShowMultiShipsDrawer] = useState(false); const [multipleThingsSelected, setMultipleThingsSelected] = useState< SgwModel.SgwThing[] >([]); const [formSearchData, setFormSearchData] = useState(null); const [openFormSearch, setOpenFormSearch] = useState(false); const { shipSos, getShipSosWs } = useGetShipSos(); const handleClickOneThing = (thing: SgwModel.SgwThing) => { if (baseMap.current === null) { console.log('BaseMap is not ready yet'); return; } notificationApi.open({ message: ( {`${intl.formatMessage({ id: 'map.ship_detail.name', defaultMessage: 'Device Information', })} ${thing.name}`} ), key: `ship-detail-${thing.id}`, description: baseMap && ( ), placement: 'bottomRight', actions: , duration: 100, onClose() { baseMap.current?.clearFeatures(TEMPORARY_LAYER); baseMap.current?.toggleLayer(DATA_LAYER, true); baseMap.current?.zoomToFeaturesInLayer(DATA_LAYER); }, style: { width: window.innerWidth <= 576 ? '80vw' : window.innerWidth <= 768 ? '60vw' : '800px', maxWidth: '800px', height: 'auto', borderRadius: '12px', boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)', }, }); }; const onFeaturesClick = useCallback((features: Feature[]) => { console.log('Multiple features clicked:', features); const things: SgwModel.SgwThing[] = features.map((feature) => feature.get('thing'), ) as SgwModel.SgwThing[]; setMultipleThingsSelected(things); setShowMultiShipsDrawer(true); }, []); const onFeatureClick = useCallback( (feature: Feature) => { const thing: SgwModel.SgwThing = feature.get('thing'); baseMap.current?.zoomToFeatures([feature]); handleClickOneThing(thing); }, [baseMap], ); const onError = useCallback( (error: any) => { console.error( intl.formatMessage({ id: 'home.mapError' }), 'at', new Date().toLocaleTimeString(), ':', error, ); }, [intl], ); // Handler called when map is ready const handleMapReady = useCallback((baseMapInstance: BaseMap) => { // console.log('Map is ready!', baseMapInstance); baseMap.current = baseMapInstance; // Create a vector layer for dynamic features const vectorDataLayer = new VectorLayer({ source: new VectorSource(), }); vectorDataLayer.set('id', DATA_LAYER); const vectorTemporaryLayer = new VectorLayer({ source: new VectorSource(), }); vectorTemporaryLayer.set('id', TEMPORARY_LAYER); baseMapInstance.addLayer(vectorDataLayer); baseMapInstance.addLayer(vectorTemporaryLayer); getShipSosWs(); }, []); useEffect(() => { if (shipSos !== null && things && things.things) { const check = things?.things?.find( (thing) => thing.id === shipSos.thing_id, ); if (check) { tableRef.current?.reload(); } } }, [shipSos]); useEffect(() => { if (formSearchData && tableRef.current) { tableRef.current.reload(); // gọi lại request } }, [formSearchData]); const queryThings = async (params: ParamsType) => { try { const { current = 1, pageSize = 10, keyword = '' } = params; const offset = current === 1 ? 0 : (current - 1) * pageSize; const query: MasterModel.SearchThingPaginationBody = { offset: offset, limit: 200, order: 'name', dir: 'asc', }; const stateNormalQuery = stateQuery?.isNormal ? 'normal' : ''; const stateSosQuery = stateQuery?.isSos ? 'sos' : ''; const stateWarningQuery = stateQuery?.isWarning ? stateNormalQuery + ',warning' : stateNormalQuery; const stateCriticalQuery = stateQuery?.isCritical ? stateWarningQuery + ',critical' : stateWarningQuery; const stateQueryParams = stateQuery?.isNormal && stateQuery?.isWarning && stateQuery?.isCritical && stateQuery?.isSos ? '' : [stateCriticalQuery, stateSosQuery].filter(Boolean).join(','); let metaFormQuery: Record = {}; if (stateQuery?.isDisconnected) metaFormQuery = { ...metaFormQuery, connected: false }; const metaStateQuery = stateQueryParams !== '' ? { state_level: stateQueryParams } : null; if (formSearchData) { const { ship_name, reg_number, ship_length, ship_power, ship_type, alarm_list, group_id, ship_group_id, } = formSearchData; if (ship_name) metaFormQuery.ship_name = ship_name; if (reg_number) metaFormQuery.ship_reg_number = reg_number; if (Array.isArray(ship_length) && ship_length.length === 2) { metaFormQuery.ship_length = ship_length; } if (Array.isArray(ship_power) && ship_power.length === 2) { metaFormQuery.ship_power = ship_power; } if (Array.isArray(ship_type) && ship_type.length > 0) { metaFormQuery.ship_type = ship_type; } if (Array.isArray(alarm_list) && alarm_list.length > 0) { metaFormQuery.alarm_list = alarm_list; } if (group_id) metaFormQuery.group_id = group_id; if (Array.isArray(ship_group_id) && ship_group_id.length > 0) { metaFormQuery.ship_group_id = ship_group_id; } } const metaQuery = { ...query, metadata: { ship_name: keyword, ...metaFormQuery, ...metaStateQuery, not_empty: 'ship_id', }, }; setThings(null); const resp = await apiSearchThings(metaQuery, 'sgw'); return resp; } catch (error) { console.error('Error when searchThings: ', error); return {}; } }; const handleZoomToFeature = (layerName: string) => { if (!baseMap) return; // Get all features and zoom to them const vectorLayer = baseMap.current?.getLayerById(layerName); if (vectorLayer && vectorLayer instanceof VectorLayer) { const features = vectorLayer.getSource()?.getFeatures(); if (features && features.length > 0) { baseMap.current?.zoomToFeatures(features); } } }; useEffect(() => { if (!things || !baseMap.current || !things.things) return; // Cleanup các animation và overlay cũ trước khi render lại sosPulseCancelRefs.current.forEach((cancel) => cancel()); sosPulseCancelRefs.current = []; overlayCleanupRefs.current.forEach((cleanup) => cleanup()); overlayCleanupRefs.current.clear(); baseMap.current.clearOverlays(); baseMap.current.clearFeatures(DATA_LAYER); for (const thing of things.things) { const gpsData: GPSParseResult = JSON.parse(thing.metadata?.gps || '{}'); if (gpsData.lat && gpsData.lon) { if (thing.metadata?.state_level === 3) { console.log('Cos tau sos'); const feature = baseMap.current?.addPoint( DATA_LAYER, [gpsData.lon, gpsData.lat], { thing, type: 'sos-point', }, ); // Bắt đầu hiệu ứng gợn sóng const cancel = baseMap.current?.sosPulse(feature, DATA_LAYER); if (cancel) { sosPulseCancelRefs.current.push(cancel); } // Tạo overlay hiển thị thông tin SOS const overlayElement = document.createElement('div'); const cleanup = renderSosOverlay(overlayElement, { shipName: thing.metadata?.ship_name || thing.name, reason: thing.metadata.sos || 'Không rõ lý do', time: thing.metadata?.sos_time ? formatUnixTime(thing.metadata.sos_time) : undefined, }); overlayCleanupRefs.current.set(thing.id!, cleanup); baseMap.current.addOrUpdateOverlay( `sos-${thing.id}`, [gpsData.lon, gpsData.lat], overlayElement, ); } else { baseMap.current?.addPoint(DATA_LAYER, [gpsData.lon, gpsData.lat], { thing, type: 'main-point', }); } } } handleZoomToFeature(DATA_LAYER); // Cleanup khi component unmount return () => { sosPulseCancelRefs.current.forEach((cancel) => cancel()); sosPulseCancelRefs.current = []; overlayCleanupRefs.current.forEach((cleanup) => cleanup()); overlayCleanupRefs.current.clear(); }; }, [things]); const columns: ProColumns[] = [ { title: intl.formatMessage({ id: 'thing.name', defaultMessage: 'Name', }), dataIndex: ['metadata', 'ship_name'], key: 'ship_name', hideInSearch: true, width: '30%', ellipsis: true, render: (_, row) => { const text = row?.metadata?.ship_name || ''; return ( {text} ); }, }, { title: intl.formatMessage({ id: 'thing.status', defaultMessage: 'Status', }), dataIndex: ['metadata', 'state_level'], key: 'status', hideInSearch: true, width: '70%', filters: true, onFilter: true, valueType: 'select', valueEnum: { 0: { text: intl.formatMessage({ id: 'thing.status.normal', defaultMessage: 'Normal', }), status: 'Normal', }, 1: { text: intl.formatMessage({ id: 'thing.status.warning', defaultMessage: 'Warning', }), status: 'Warning', }, 2: { text: intl.formatMessage({ id: 'thing.status.critical', defaultMessage: 'Critical', }), status: 'Critical', }, 3: { text: intl.formatMessage({ id: 'thing.status.sos', defaultMessage: 'SOS', }), status: 'SOS', }, }, ellipsis: true, render: (_, row) => { const alarm = JSON.parse(row?.metadata?.alarm_list || '{}'); const text = alarm?.map((a: any) => a?.name).join(', '); return ( {getBadgeStatus(row?.metadata?.state_level || 0)} {text} ); }, }, ]; const handleTagStateChange = (payload: TagStateCallbackPayload) => { setStateQuery(payload); tableRef.current?.reload(); }; return (
{contextHolder} {messageContextHolder}
{/* VietNamMap with ref */} { setShowMultiShipsDrawer(false); setMultipleThingsSelected([]); }} title={ Thông tin {multipleThingsSelected.length} tàu } size="large" > { setFormSearchData(value); }} />
); }; export default MapPage;