diff --git a/sgw-device-v1.1.zip b/sgw-device-v1.1.zip new file mode 100644 index 0000000..3c5c7c5 Binary files /dev/null and b/sgw-device-v1.1.zip differ diff --git a/src/components/Footer/Footer.tsx b/src/components/Footer/Footer.tsx index f6223f7..2d43f3f 100644 --- a/src/components/Footer/Footer.tsx +++ b/src/components/Footer/Footer.tsx @@ -1,7 +1,15 @@ import { DefaultFooter } from '@ant-design/pro-components'; - +import './style.less'; const Footer = () => { - return ; + return ( + + ); }; export default Footer; diff --git a/src/components/Footer/style.less b/src/components/Footer/style.less new file mode 100644 index 0000000..925951f --- /dev/null +++ b/src/components/Footer/style.less @@ -0,0 +1,3 @@ +.ant-pro-global-footer-copyright { + color: white !important; /* hoặc mã màu bạn muốn */ +} diff --git a/src/constants/enums.ts b/src/constants/enums.ts index 3d0d90e..ce0b962 100644 --- a/src/constants/enums.ts +++ b/src/constants/enums.ts @@ -6,3 +6,7 @@ export enum STATUS { UPDATE_FISHING_LOG_SUCCESS = 'UPDATE_FISHING_LOG_SUCCESS', UPDATE_FISHING_LOG_FAIL = 'UPDATE_FISHING_LOG_FAIL', } + +export enum ENTITY_TYPE_ENUM { + SOS_WARNING = '50:15', +} diff --git a/src/models/getSos.ts b/src/models/getSos.ts index 21a2a72..209c74a 100644 --- a/src/models/getSos.ts +++ b/src/models/getSos.ts @@ -10,7 +10,7 @@ export default function useGetGpsModel() { const res = await getGPS(); // đổi URL cho phù hợp console.log('GPS Data fetched:', res); - setGpsData(res || []); + setGpsData(res); } catch (err) { console.error('Fetch gps data failed', err); } finally { diff --git a/src/pages/Auth/index.tsx b/src/pages/Auth/index.tsx index 34d60ee..cf9863b 100644 --- a/src/pages/Auth/index.tsx +++ b/src/pages/Auth/index.tsx @@ -1,3 +1,4 @@ +import Footer from '@/components/Footer/Footer'; import { ROUTE_HOME } from '@/constants'; import { login } from '@/services/controller/AuthController'; import { parseJwt } from '@/utils/jwtTokenUtils'; @@ -127,6 +128,17 @@ const LoginPage = () => { /> +
+
+
); }; diff --git a/src/pages/Home/components/BaseMap.tsx b/src/pages/Home/components/BaseMap.tsx index b097269..e814dec 100644 --- a/src/pages/Home/components/BaseMap.tsx +++ b/src/pages/Home/components/BaseMap.tsx @@ -7,16 +7,21 @@ import { } from '@/utils/mapUtils'; import { Feature, Map, View } from 'ol'; import { Coordinate } from 'ol/coordinate'; +import { easeOut } from 'ol/easing'; +import { EventsKey } from 'ol/events'; import { createEmpty, extend, Extent, isEmpty } from 'ol/extent'; import { LineString, MultiPolygon, Point, Polygon } from 'ol/geom'; import BaseLayer from 'ol/layer/Base'; import VectorLayer from 'ol/layer/Vector'; +import { unByKey } from 'ol/Observable'; import { fromLonLat } from 'ol/proj'; +import { getVectorContext } from 'ol/render'; +import RenderEvent from 'ol/render/Event'; import VectorSource from 'ol/source/Vector'; +import { Circle as CircleStyle, Style } from 'ol/style'; import Fill from 'ol/style/Fill'; import Icon from 'ol/style/Icon'; import Stroke from 'ol/style/Stroke'; -import Style from 'ol/style/Style'; import Text from 'ol/style/Text'; interface MapManagerConfig { @@ -25,6 +30,12 @@ interface MapManagerConfig { onFeaturesClick?: (features: any[]) => void; onError?: (error: string[]) => void; } +interface AnimationConfig { + duration?: number; + maxRadius?: number; + strokeColor?: string; + strokeWidthBase?: number; +} interface StyleConfig { icon?: string; // URL của icon @@ -66,6 +77,7 @@ class MapManager { private errors: string[]; private layers: BaseLayer[]; // Assuming layers is defined elsewhere private isInitialized: boolean; // Thêm thuộc tính để theo dõi trạng thái khởi tạo + private eventKey: EventsKey | undefined; constructor( mapRef: React.RefObject, @@ -83,11 +95,12 @@ class MapManager { this.errors = []; this.layers = []; // Initialize layers (adjust based on actual usage) this.isInitialized = false; // Khởi tạo là false + this.eventKey = undefined; // Khởi tạo eventKey } async getListLayers(): Promise<[]> { const resp: [] = await getAllLayer(); - console.log('resp', resp); + // console.log('resp', resp); return resp; } @@ -328,8 +341,10 @@ class MapManager { // Lưu feature this.features.push(feature); this.featureLayer?.getSource()?.addFeature(feature); - - console.log('Point added successfully:', { coord, data, styleConfig }); + if (styleConfig.animate) { + this.animatedMarker(feature, styleConfig.animationConfig); + } + // console.log('Point added successfully:', { coord, data, styleConfig }); return feature; } catch (error: any) { @@ -340,6 +355,74 @@ class MapManager { return null; } } + animatedMarker = ( + feature: Feature, + config: AnimationConfig = {}, + ): void => { + const { + duration = 3000, + maxRadius = 20, + strokeColor = 'rgba(255, 0, 0, 0.8)', + strokeWidthBase = 0.25, + } = config; + console.log('Starting animatedMarker with config:', config); + + const flashGeom = feature.getGeometry()?.clone() as Point | undefined; + if (!flashGeom || !this.featureLayer) { + console.error('Invalid geometry or featureLayer for animation'); + return; + } + + // Tạo 2 "pha" sóng, mỗi sóng bắt đầu cách nhau duration/2 + const waveCount = 1; + const waveOffsets = Array.from( + { length: waveCount }, + (_, i) => (duration / waveCount) * i, + ); + const start = Date.now(); + + const listenerKey: EventsKey = this.featureLayer.on( + 'postrender', + (event: RenderEvent) => { + const frameState = event.frameState; + if (!frameState) return; + + const elapsedTotal = frameState.time - start; + if (elapsedTotal >= duration * 3) { + // chạy 2 chu kỳ rồi dừng (bạn có thể bỏ điều kiện này để chạy mãi) + unByKey(listenerKey); + return; + } + + const vectorContext = getVectorContext(event); + + waveOffsets.forEach((offset) => { + const elapsed = + (frameState.time - start - offset + duration) % duration; + const elapsedRatio = elapsed / duration; + + const radius = easeOut(elapsedRatio) * (maxRadius - 5) + 5; + const opacity = easeOut(1 - elapsedRatio); + + const style = new Style({ + image: new CircleStyle({ + radius, + stroke: new Stroke({ + color: strokeColor.replace('0.8', opacity.toString()), + width: strokeWidthBase + opacity, + }), + }), + }); + + vectorContext.setStyle(style); + vectorContext.drawGeometry(flashGeom); + }); + + this.map!.render(); + }, + ); + this.eventKey = listenerKey; + }; addPolygon( coords: Coordinate[][], @@ -510,12 +593,12 @@ class MapManager { if (this.featureLayer && this.isInitialized) { this.features.push(feature); this.featureLayer.getSource()?.addFeature(feature); - console.log( - 'LineString added successfully at', - new Date().toLocaleTimeString(), - ':', - { coords, data, styleConfig }, - ); + // console.log( + // 'LineString added successfully at', + // new Date().toLocaleTimeString(), + // ':', + // { coords, data, styleConfig }, + // ); // Đảm bảo bản đồ được render lại this.map?.render(); return feature; @@ -540,10 +623,10 @@ class MapManager { styleConfig: Record = {}, zoom: number, ) => { - console.log( - `createPolygonStyle called with zoom: ${zoom}, styleConfig:`, - styleConfig, - ); // Debug + // console.log( + // `createPolygonStyle called with zoom: ${zoom}, styleConfig:`, + // styleConfig, + // ); // Debug const textContent = `Tên: ${data?.name ?? 'Chưa có'}\n\n Loại: ${ data?.type ?? 0 }\n\nThông tin: ${data?.description ?? 'Chưa có'}`; @@ -581,7 +664,7 @@ class MapManager { text: textStyle, }); - console.log(`Polygon style created:`, style); // Debug + // console.log(`Polygon style created:`, style); // Debug return [style]; }; diff --git a/src/pages/Home/index.tsx b/src/pages/Home/index.tsx index c2eeaf1..c54d0f1 100644 --- a/src/pages/Home/index.tsx +++ b/src/pages/Home/index.tsx @@ -4,6 +4,7 @@ import { MAP_TRACKPOINTS_ID, ROUTE_TRIP, } from '@/constants'; +import { ENTITY_TYPE_ENUM } from '@/constants/enums'; import { queryAlarms, queryEntities, @@ -29,6 +30,21 @@ import ShipInfo from './components/ShipInfo'; import SosButton from './components/SosButton'; import VietNamMap, { MapManager } from './components/VietNamMap'; +// // Define missing types locally for now +// interface AlarmData { +// level: number; +// } + +// interface EntityData { +// id: string; +// valueString: string; +// } + +// interface TrackPoint { +// lat: number; +// lon: number; +// } + export interface GpsData { lat: number; lon: number; @@ -78,9 +94,9 @@ const HomePage: React.FC = () => { getGPSData(); const interval = setInterval(() => { getGPSData(); - getEntitiesData(); - getShipTrackPoints(); - getEntityData(); + getEntitiesData(mapManagerRef.current, gpsData!); + fetchAndAddTrackPoints(mapManagerRef.current); + fetchAndProcessEntities(mapManagerRef.current); }, 5000); return () => { clearInterval(interval); @@ -91,61 +107,108 @@ const HomePage: React.FC = () => { useEffect(() => { const timer = setTimeout(() => { // getGPSData(); - getEntitiesData(); - getShipTrackPoints(); - getEntityData(); + getEntitiesData(mapManagerRef.current, gpsData!); + fetchAndAddTrackPoints(mapManagerRef.current); + fetchAndProcessEntities(mapManagerRef.current); }, 1000); // delay 1s return () => clearTimeout(timer); }, [gpsData]); - const getEntitiesData = async () => { + // Helper function to add features to the map + const addFeatureToMap = ( + mapManager: MapManager, + gpsData: GpsData, + alarm: API.AlarmResponse, + ) => { + console.log( + 'Adding feature to map with GPS data:', + gpsData, + 'and alarm:', + alarm, + ); + const isSos = alarm?.alarms.find( + (a) => a.id === ENTITY_TYPE_ENUM.SOS_WARNING, + ); + console.log('Is SOS Alarm Present:', isSos); + + if (mapManager?.featureLayer && gpsData) { + mapManager.featureLayer.getSource()?.clear(); + mapManager.addPoint( + [gpsData.lon, gpsData.lat], + { bearing: gpsData.h || 0 }, + { + icon: getShipIcon(alarm.level || 0, gpsData?.fishing || false), + scale: 0.1, + animate: isSos ? true : false, + }, + ); + } + }; + + const fetchAndProcessEntities = async (mapManager: MapManager) => { try { - const alarm = await queryAlarms(); - // console.log('Fetched alarm data:', alarm); - // console.log('GPS Data:', gpsData); + const entities: API.TransformedEntity[] = await queryEntities(); + console.log('Fetched entities:', entities.length); - if (mapManagerRef.current?.featureLayer && gpsData) { - // Clear trước - mapManagerRef.current.featureLayer.getSource()?.clear(); + for (const entity of entities) { + if (entity.id === '50:2' && entity.valueString !== '[]') { + const banzones = await queryBanzones(); + const zones: any[] = JSON.parse(entity.valueString); - try { - // Tạo feature mới - const feature = mapManagerRef.current.addPoint( - [gpsData.lon, gpsData.lat], - { - bearing: gpsData.h || 0, - }, - { - icon: getShipIcon(alarm.level || 0, gpsData?.fishing || false), - scale: 0.1, - }, - ); - } catch (parseError) { - console.error( - `Error parsing valueString for entity: ${parseError}`, - parseError, - ); + zones.forEach((zone: any) => { + const geom = banzones.find((b) => b.id === zone.zone_id); + if (geom) { + const { geom_type, geom_lines, geom_poly } = geom.geom || {}; + if (geom_type === 2) { + const coordinates = convertWKTLineStringToLatLngArray( + geom_lines || '', + ); + if (coordinates.length > 0) { + mapManager.addLineString( + coordinates, + { + id: MAP_POLYLINE_BAN, + text: `${geom.name} - ${zone.message}`, + }, + { strokeColor: 'red', strokeWidth: 4 }, + ); + } + } else if (geom_type === 1) { + const coordinates = convertWKTtoLatLngString(geom_poly || ''); + if (coordinates.length > 0) { + mapManager.addPolygon( + coordinates, + { + id: MAP_POLYGON_BAN, + name: geom.name, + type: getBanzoneNameByType(geom.type || 4), + description: zone.message, + }, + { + strokeColor: '#FCC61D', + strokeWidth: 2, + fillColor: '#FCC61D', + }, + ); + } + } + } + }); } } } catch (error) { console.error('Error fetching entities:', error); } }; - const getShipTrackPoints = async () => { + + const fetchAndAddTrackPoints = async (mapManager: MapManager) => { try { - const trackpoints = await queryShipTrackPoints(); - // console.log('Fetched ship track points:', trackpoints.length); + const trackpoints: API.ShipTrackPoint[] = await queryShipTrackPoints(); if (trackpoints.length > 0) { - mapManagerRef.current?.addLineString( + mapManager.addLineString( trackpoints.map((point) => [point.lon, point.lat]), - { - id: MAP_TRACKPOINTS_ID, - // You can add more properties here if needed - }, - { - strokeColor: 'blue', - strokeWidth: 3, - }, + { id: MAP_TRACKPOINTS_ID }, + { strokeColor: 'blue', strokeWidth: 3 }, ); } } catch (error) { @@ -153,70 +216,10 @@ const HomePage: React.FC = () => { } }; - const getEntityData = async () => { + const getEntitiesData = async (mapManager: MapManager, gpsData: GpsData) => { try { - const entities = await queryEntities(); - console.log('Fetched entities:', entities.length); - if (entities.length > 0) { - entities.forEach(async (entity) => { - if (entity.id == '50:2') { - const banzones = await queryBanzones(); - console.log('Fetched banzones:', banzones.length); - if (entity.valueString != '[]') { - const zones = JSON.parse(entity.valueString); - console.log('Parsed zones:', zones); - for (const zone of zones) { - const geom = banzones.find((b) => b.id === zone.zone_id); - if (geom) { - if (geom.geom?.geom_type === 2) { - // Polyline - const coordinates = convertWKTLineStringToLatLngArray( - geom?.geom?.geom_lines || '', - ); - if (coordinates.length > 0) { - mapManagerRef.current.addLineString( - coordinates, - { - id: MAP_POLYLINE_BAN, - text: `${geom.name} - ${zone.message}`, - }, - { - strokeColor: 'red', - strokeWidth: 4, - }, - ); - } - } else if (geom.geom?.geom_type === 1) { - const coordinates = convertWKTtoLatLngString( - geom?.geom?.geom_poly || '', - ); - if (coordinates.length > 0) { - mapManagerRef.current.addPolygon( - coordinates, - { - id: MAP_POLYGON_BAN, - name: geom.name, - type: getBanzoneNameByType(geom.type || 4), - description: zone.message, - }, - { - strokeColor: 'yellow', - strokeWidth: 2, - fillColor: 'rgba(255, 255, 0, 0.3)', // Màu vàng với độ trong suốt - }, - ); - } - } - } else { - console.log('No geometry found for zone:', zone.id); - } - } - } else { - console.log('Zone rỗng'); - } - } - }); - } + const alarm: API.AlarmResponse = await queryAlarms(); + addFeatureToMap(mapManager, gpsData, alarm); } catch (error) { console.error('Error fetching entities:', error); } @@ -268,7 +271,12 @@ const HomePage: React.FC = () => { { if (value) { - getEntitiesData(); + // Ensure null check for gpsData in all usages + if (gpsData) { + getEntitiesData(mapManagerRef.current, gpsData); + } else { + console.warn('GPS data is null, skipping getEntitiesData call'); + } } }} />