feat(map): add event-driven GPS update
This commit is contained in:
@@ -1,5 +1,4 @@
|
|||||||
import { ROUTE_LOGIN } from '@/constants';
|
import { ROUTE_LOGIN } from '@/constants';
|
||||||
import { apiConfig } from '@/services/ApiConfigService';
|
|
||||||
import { getToken, removeToken } from '@/utils/localStorageUtils';
|
import { getToken, removeToken } from '@/utils/localStorageUtils';
|
||||||
import { history, RequestConfig } from '@umijs/max';
|
import { history, RequestConfig } from '@umijs/max';
|
||||||
import { message } from 'antd';
|
import { message } from 'antd';
|
||||||
@@ -85,18 +84,9 @@ export const handleRequestConfig: RequestConfig = {
|
|||||||
// Request interceptors
|
// Request interceptors
|
||||||
requestInterceptors: [
|
requestInterceptors: [
|
||||||
(url: string, options: any) => {
|
(url: string, options: any) => {
|
||||||
console.log('URL Request:', url, options);
|
|
||||||
|
|
||||||
// Nếu URL không phải absolute URL, thêm base URL
|
|
||||||
let finalUrl = url;
|
|
||||||
if (!url.startsWith('http')) {
|
|
||||||
const baseUrl = apiConfig.getApiBaseUrl();
|
|
||||||
finalUrl = `${baseUrl}${url}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const token = getToken();
|
const token = getToken();
|
||||||
return {
|
return {
|
||||||
url: finalUrl,
|
url,
|
||||||
options: {
|
options: {
|
||||||
...options,
|
...options,
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -1,59 +1,24 @@
|
|||||||
// Hàm lấy IP từ hostname hiện tại (chỉ hoạt động runtime)
|
const proxy: Record<string, any> = {
|
||||||
const getCurrentIP = () => {
|
dev: {
|
||||||
if (typeof window !== 'undefined') {
|
'/api': {
|
||||||
const hostname = window.location.hostname;
|
target: 'http://192.168.30.103:81',
|
||||||
// Nếu là localhost hoặc IP local, trả về IP mặc định
|
changeOrigin: true,
|
||||||
if (
|
},
|
||||||
hostname === 'localhost' ||
|
},
|
||||||
hostname.startsWith('192.168.') ||
|
test: {
|
||||||
hostname.startsWith('10.')
|
'/test': {
|
||||||
) {
|
target: 'https://test-sgw-device.gms.vn',
|
||||||
console.log('Host name: ', hostname);
|
changeOrigin: true,
|
||||||
|
secure: false,
|
||||||
return hostname;
|
},
|
||||||
}
|
},
|
||||||
// Nếu là domain, có thể cần map sang IP tương ứng
|
prod: {
|
||||||
return hostname;
|
'/test': {
|
||||||
}
|
target: 'https://prod-sgw-device.gms.vn',
|
||||||
return process.env.REACT_APP_API_HOST || '192.168.30.102'; // fallback từ env
|
changeOrigin: true,
|
||||||
|
secure: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Hàm tạo proxy config động
|
|
||||||
const createDynamicProxy = () => {
|
|
||||||
const currentIP = getCurrentIP();
|
|
||||||
const isLocalIP =
|
|
||||||
currentIP.startsWith('192.168.') ||
|
|
||||||
currentIP.startsWith('10.') ||
|
|
||||||
currentIP === 'localhost';
|
|
||||||
|
|
||||||
return {
|
|
||||||
dev: {
|
|
||||||
'/api': {
|
|
||||||
target: `http://${currentIP}:81`,
|
|
||||||
changeOrigin: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
test: {
|
|
||||||
'/test': {
|
|
||||||
target: isLocalIP
|
|
||||||
? `http://${currentIP}:81`
|
|
||||||
: 'https://test-sgw-device.gms.vn',
|
|
||||||
changeOrigin: true,
|
|
||||||
secure: !isLocalIP,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
prod: {
|
|
||||||
'/test': {
|
|
||||||
target: isLocalIP
|
|
||||||
? `http://${currentIP}:81`
|
|
||||||
: 'https://prod-sgw-device.gms.vn',
|
|
||||||
changeOrigin: true,
|
|
||||||
secure: !isLocalIP,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const proxy: Record<string, any> = createDynamicProxy();
|
|
||||||
|
|
||||||
export default proxy;
|
export default proxy;
|
||||||
|
|||||||
19
src/hooks/useRealtimeGps.ts
Normal file
19
src/hooks/useRealtimeGps.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
// src/hooks/useRealtimeGps.ts
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { eventBus } from '@/utils/eventBus';
|
||||||
|
|
||||||
|
export default function useRealtimeGps() {
|
||||||
|
const [gpsData, setGpsData] = useState<API.GPSResonse | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleGpsUpdate = (data: API.GPSResonse) => {
|
||||||
|
setGpsData(data);
|
||||||
|
};
|
||||||
|
eventBus.on('gpsData:update', handleGpsUpdate);
|
||||||
|
|
||||||
|
// cleanup khi unmount
|
||||||
|
return () => eventBus.off('gpsData:update', handleGpsUpdate);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return gpsData;
|
||||||
|
}
|
||||||
@@ -1,22 +1,43 @@
|
|||||||
|
// src/models/useGetGpsModel.ts
|
||||||
import { getGPS } from '@/services/controller/DeviceController';
|
import { getGPS } from '@/services/controller/DeviceController';
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState, useEffect } from 'react';
|
||||||
|
import { eventBus } from '@/utils/eventBus';
|
||||||
|
|
||||||
export default function useGetGpsModel() {
|
export default function useGetGpsModel() {
|
||||||
const [gpsData, setGpsData] = useState<API.GPSResonse | null>(null);
|
const [gpsData, setGpsData] = useState<API.GPSResonse | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const getGPSData = useCallback(async () => {
|
|
||||||
|
const getGPSData = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await getGPS(); // đổi URL cho phù hợp
|
// Bypass cache để GPS luôn realtime, gọi trực tiếp API
|
||||||
console.log('GPS Data fetched:', res);
|
const data = await getGPS();
|
||||||
|
console.log('GPS fetched from API:', data);
|
||||||
|
|
||||||
setGpsData(res);
|
setGpsData(data);
|
||||||
|
// Luôn emit event GPS để đảm bảo realtime
|
||||||
|
try {
|
||||||
|
eventBus.emit('gpsData:update', data);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to emit gpsData:update event', e);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Fetch gps data failed', err);
|
console.error('Fetch gps data failed', err);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// ✅ Lắng nghe event cập nhật cache từ nơi khác (nếu có)
|
||||||
|
useEffect(() => {
|
||||||
|
const handleUpdate = (data: API.GPSResonse) => {
|
||||||
|
console.log('GPS cache updated via eventBus');
|
||||||
|
setGpsData(data);
|
||||||
|
};
|
||||||
|
eventBus.on('gpsData:update', handleUpdate);
|
||||||
|
return () => eventBus.off('gpsData:update', handleUpdate);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
gpsData,
|
gpsData,
|
||||||
loading,
|
loading,
|
||||||
|
|||||||
@@ -59,11 +59,6 @@ interface FeatureData {
|
|||||||
text?: string;
|
text?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Interface for layer object
|
|
||||||
interface Layer {
|
|
||||||
layer: VectorLayer<VectorSource>;
|
|
||||||
}
|
|
||||||
|
|
||||||
class MapManager {
|
class MapManager {
|
||||||
mapRef: React.RefObject<HTMLDivElement>;
|
mapRef: React.RefObject<HTMLDivElement>;
|
||||||
map: Map | null;
|
map: Map | null;
|
||||||
|
|||||||
@@ -23,12 +23,16 @@ import {
|
|||||||
InfoOutlined,
|
InfoOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { history, useModel } from '@umijs/max';
|
import { history, useModel } from '@umijs/max';
|
||||||
|
import { eventBus } from '@/utils/eventBus';
|
||||||
import { FloatButton, Popover } from 'antd';
|
import { FloatButton, Popover } from 'antd';
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import GpsInfo from './components/GpsInfo';
|
import GpsInfo from './components/GpsInfo';
|
||||||
import ShipInfo from './components/ShipInfo';
|
import ShipInfo from './components/ShipInfo';
|
||||||
import SosButton from './components/SosButton';
|
import SosButton from './components/SosButton';
|
||||||
import VietNamMap, { MapManager } from './components/VietNamMap';
|
import VietNamMap, { MapManager } from './components/VietNamMap';
|
||||||
|
import Point from 'ol/geom/Point';
|
||||||
|
import LineString from 'ol/geom/LineString';
|
||||||
|
import { fromLonLat } from 'ol/proj';
|
||||||
|
|
||||||
// // Define missing types locally for now
|
// // Define missing types locally for now
|
||||||
// interface AlarmData {
|
// interface AlarmData {
|
||||||
@@ -56,7 +60,11 @@ const HomePage: React.FC = () => {
|
|||||||
const mapRef = useRef<HTMLDivElement>(null);
|
const mapRef = useRef<HTMLDivElement>(null);
|
||||||
const [isShipInfoOpen, setIsShipInfoOpen] = useState(false);
|
const [isShipInfoOpen, setIsShipInfoOpen] = useState(false);
|
||||||
const { gpsData, getGPSData } = useModel('getSos');
|
const { gpsData, getGPSData } = useModel('getSos');
|
||||||
|
const hasCenteredRef = useRef(false);
|
||||||
|
const banzonesCacheRef = useRef<any[]>([]);
|
||||||
|
function isSameBanzones(newData: any[], oldData: any[]): boolean {
|
||||||
|
return JSON.stringify(newData) === JSON.stringify(oldData);
|
||||||
|
}
|
||||||
const onFeatureClick = useCallback((feature: any) => {
|
const onFeatureClick = useCallback((feature: any) => {
|
||||||
console.log('OnClick Feature: ', feature);
|
console.log('OnClick Feature: ', feature);
|
||||||
console.log(
|
console.log(
|
||||||
@@ -90,46 +98,19 @@ const HomePage: React.FC = () => {
|
|||||||
|
|
||||||
const [isShowGPSData, setIsShowGPSData] = useState(true);
|
const [isShowGPSData, setIsShowGPSData] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
// Hàm tổng hợp: gọi GPS trước, sau đó gọi các API còn lại
|
||||||
getGPSData();
|
const refreshAllData = async () => {
|
||||||
const interval = setInterval(() => {
|
try {
|
||||||
getGPSData();
|
// Gọi GPS, khi xong sẽ emit event gpsData:update và các API khác sẽ tự động được gọi qua event handler
|
||||||
getEntitiesData(mapManagerRef.current, gpsData!);
|
await getGPSData();
|
||||||
fetchAndAddTrackPoints(mapManagerRef.current);
|
} catch (e) {
|
||||||
fetchAndProcessEntities(mapManagerRef.current);
|
console.error('refreshAllData error', e);
|
||||||
}, 5000);
|
}
|
||||||
return () => {
|
};
|
||||||
clearInterval(interval);
|
|
||||||
console.log('MapManager destroyed in HomePage cleanup');
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
// Helper function to add features to the map (kept above pollers to avoid use-before-define)
|
||||||
const timer = setTimeout(() => {
|
function addFeatureToMap(mapManager: MapManager, gpsData: GpsData, alarm: API.AlarmResponse,) {
|
||||||
// getGPSData();
|
const isSos = alarm?.alarms.find((a) => a.id === ENTITY_TYPE_ENUM.SOS_WARNING);
|
||||||
getEntitiesData(mapManagerRef.current, gpsData!);
|
|
||||||
fetchAndAddTrackPoints(mapManagerRef.current);
|
|
||||||
fetchAndProcessEntities(mapManagerRef.current);
|
|
||||||
}, 1000); // delay 1s
|
|
||||||
return () => clearTimeout(timer);
|
|
||||||
}, [gpsData]);
|
|
||||||
|
|
||||||
// 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) {
|
if (mapManager?.featureLayer && gpsData) {
|
||||||
mapManager.featureLayer.getSource()?.clear();
|
mapManager.featureLayer.getSource()?.clear();
|
||||||
@@ -143,26 +124,158 @@ const HomePage: React.FC = () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const fetchAndProcessEntities = async (mapManager: MapManager) => {
|
useEffect(() => {
|
||||||
|
// Chỉ poll GPS, các API còn lại sẽ được gọi khi có GPS mới
|
||||||
|
getGPSData();
|
||||||
|
const gpsInterval = setInterval(() => {
|
||||||
|
getGPSData();
|
||||||
|
}, 5000); // cập nhật mỗi 5s
|
||||||
|
|
||||||
|
// Vẽ marker tàu và polyline ngay khi vào trang nếu đã có dữ liệu
|
||||||
|
if (gpsData) {
|
||||||
|
eventBus.emit('ship:update', gpsData);
|
||||||
|
}
|
||||||
|
// Nếu có trackpoints, emit luôn (nếu bạn lưu trackpoints ở state, có thể emit ở đây)
|
||||||
|
// eventBus.emit('trackpoints:update', trackpoints);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearInterval(gpsInterval);
|
||||||
|
console.log('MapManager destroyed in HomePage cleanup');
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Lắng nghe eventBus cho tất cả các loại dữ liệu
|
||||||
|
useEffect(() => {
|
||||||
|
// GPS cập nhật => emit ship update ngay lập tức, gọi API khác không đồng bộ
|
||||||
|
const onGpsUpdate = async (data: API.GPSResonse) => {
|
||||||
|
try {
|
||||||
|
if (data) {
|
||||||
|
// Emit ship update ngay lập tức để marker tàu cập nhật realtime
|
||||||
|
eventBus.emit('ship:update', data);
|
||||||
|
|
||||||
|
// Gọi các API khác trong background, không block ship update
|
||||||
|
setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
const alarm = await queryAlarms();
|
||||||
|
eventBus.emit('alarm:update', alarm);
|
||||||
|
const trackpoints = await queryShipTrackPoints();
|
||||||
|
eventBus.emit('trackpoints:update', trackpoints);
|
||||||
|
const entities = await queryEntities();
|
||||||
|
eventBus.emit('entities:update', entities);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Background API calls error', e);
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('onGpsUpdate handler error', e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// Cập nhật marker tàu khi có GPS mới
|
||||||
|
const onShipUpdate = (data: API.GPSResonse) => {
|
||||||
|
const mapManager = mapManagerRef.current;
|
||||||
|
if (!mapManager || !data) return;
|
||||||
|
|
||||||
|
// Cache feature trong mapManager
|
||||||
|
if (!mapManager.shipFeature) {
|
||||||
|
mapManager.shipFeature = mapManager.addPoint(
|
||||||
|
[data.lon, data.lat],
|
||||||
|
{ bearing: data.h || 0 },
|
||||||
|
{
|
||||||
|
icon: getShipIcon(0, data?.fishing || false),
|
||||||
|
scale: 0.1,
|
||||||
|
animate: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const geom = mapManager.shipFeature.getGeometry();
|
||||||
|
if (geom instanceof Point) {
|
||||||
|
geom.setCoordinates([data.lon, data.lat]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set map center to current GPS position
|
||||||
|
if (!hasCenteredRef.current && mapManager.map && mapManager.map.getView) {
|
||||||
|
mapManager.map.getView().setCenter(fromLonLat([data.lon, data.lat]));
|
||||||
|
hasCenteredRef.current = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// Cập nhật alarm khi có event
|
||||||
|
const onAlarmUpdate = (alarm: API.AlarmResponse) => {
|
||||||
|
if (gpsData) addFeatureToMap(mapManagerRef.current, gpsData, alarm);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Cập nhật trackpoints khi có event
|
||||||
|
const onTrackpointsUpdate = (trackpoints: API.ShipTrackPoint[]) => {
|
||||||
|
const source = mapManagerRef.current?.featureLayer?.getSource();
|
||||||
|
if (source && trackpoints.length > 0) {
|
||||||
|
const features = source.getFeatures();
|
||||||
|
// Tìm polyline theo id
|
||||||
|
const polyline = features.find(f => f.get('id') === MAP_TRACKPOINTS_ID);
|
||||||
|
const coordinates = trackpoints.map((point) => [point.lon, point.lat]);
|
||||||
|
if (polyline && polyline.getGeometry() instanceof LineString) {
|
||||||
|
polyline.getGeometry().setCoordinates(coordinates);
|
||||||
|
} else {
|
||||||
|
mapManagerRef.current.addLineString(
|
||||||
|
coordinates,
|
||||||
|
{ id: MAP_TRACKPOINTS_ID },
|
||||||
|
{ strokeColor: 'blue', strokeWidth: 3 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Cập nhật entities/banzones khi có event
|
||||||
|
const onEntitiesUpdate = async (entities: API.TransformedEntity[]) => {
|
||||||
|
const mapManager = mapManagerRef.current;
|
||||||
|
if (mapManager) {
|
||||||
|
await drawBanzones(mapManager);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
eventBus.on('gpsData:update', onGpsUpdate);
|
||||||
|
eventBus.on('ship:update', onShipUpdate);
|
||||||
|
eventBus.on('alarm:update', onAlarmUpdate);
|
||||||
|
eventBus.on('trackpoints:update', onTrackpointsUpdate);
|
||||||
|
eventBus.on('entities:update', onEntitiesUpdate);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
eventBus.off('gpsData:update', onGpsUpdate);
|
||||||
|
eventBus.off('ship:update', onShipUpdate);
|
||||||
|
eventBus.off('alarm:update', onAlarmUpdate);
|
||||||
|
eventBus.off('trackpoints:update', onTrackpointsUpdate);
|
||||||
|
eventBus.off('entities:update', onEntitiesUpdate);
|
||||||
|
};
|
||||||
|
}, [gpsData]);
|
||||||
|
|
||||||
|
// Vẽ lại banzones (polygon, polyline vùng cấm, vùng cá thử...)
|
||||||
|
async function drawBanzones(mapManager: MapManager) {
|
||||||
try {
|
try {
|
||||||
const entities: API.TransformedEntity[] = await queryEntities();
|
const layer = mapManager.featureLayer?.getSource();
|
||||||
console.log('Fetched entities:', entities.length);
|
layer?.getFeatures()?.forEach(f => {
|
||||||
|
if ([MAP_POLYGON_BAN, MAP_POLYLINE_BAN].includes(f.get('id'))) {
|
||||||
|
layer.removeFeature(f);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const entities = await queryEntities();
|
||||||
for (const entity of entities) {
|
for (const entity of entities) {
|
||||||
if (entity.id === '50:2' && entity.valueString !== '[]') {
|
if (entity.id === '50:2' && entity.valueString !== '[]') {
|
||||||
const banzones = await queryBanzones();
|
const banzones = await queryBanzones();
|
||||||
const zones: any[] = JSON.parse(entity.valueString);
|
const zones: any[] = JSON.parse(entity.valueString);
|
||||||
|
|
||||||
zones.forEach((zone: any) => {
|
zones.forEach((zone: any) => {
|
||||||
const geom = banzones.find((b) => b.id === zone.zone_id);
|
const geom = banzones.find((b) => b.id === zone.zone_id);
|
||||||
if (geom) {
|
if (geom) {
|
||||||
const { geom_type, geom_lines, geom_poly } = geom.geom || {};
|
const { geom_type, geom_lines, geom_poly } = geom.geom || {};
|
||||||
if (geom_type === 2) {
|
if (geom_type === 2) {
|
||||||
const coordinates = convertWKTLineStringToLatLngArray(
|
const coordinates = convertWKTLineStringToLatLngArray(geom_lines || '');
|
||||||
geom_lines || '',
|
|
||||||
);
|
|
||||||
if (coordinates.length > 0) {
|
if (coordinates.length > 0) {
|
||||||
mapManager.addLineString(
|
mapManager.addLineString(
|
||||||
coordinates,
|
coordinates,
|
||||||
@@ -197,33 +310,17 @@ const HomePage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching entities:', error);
|
console.error('Error drawing banzones:', error);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const fetchAndAddTrackPoints = async (mapManager: MapManager) => {
|
useEffect(() => {
|
||||||
try {
|
// Vẽ lại banzones khi vào trang
|
||||||
const trackpoints: API.ShipTrackPoint[] = await queryShipTrackPoints();
|
const mapManager = mapManagerRef.current;
|
||||||
if (trackpoints.length > 0) {
|
if (mapManager) {
|
||||||
mapManager.addLineString(
|
drawBanzones(mapManager);
|
||||||
trackpoints.map((point) => [point.lon, point.lat]),
|
|
||||||
{ id: MAP_TRACKPOINTS_ID },
|
|
||||||
{ strokeColor: 'blue', strokeWidth: 3 },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching ship track points:', error);
|
|
||||||
}
|
}
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const getEntitiesData = async (mapManager: MapManager, gpsData: GpsData) => {
|
|
||||||
try {
|
|
||||||
const alarm: API.AlarmResponse = await queryAlarms();
|
|
||||||
addFeatureToMap(mapManager, gpsData, alarm);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching entities:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -269,14 +366,9 @@ const HomePage: React.FC = () => {
|
|||||||
</Popover>
|
</Popover>
|
||||||
<div className="absolute top-3 right-3 ">
|
<div className="absolute top-3 right-3 ">
|
||||||
<SosButton
|
<SosButton
|
||||||
onRefresh={(value) => {
|
onRefresh={async (value) => {
|
||||||
if (value) {
|
if (value) {
|
||||||
// Ensure null check for gpsData in all usages
|
await refreshAllData();
|
||||||
if (gpsData) {
|
|
||||||
getEntitiesData(mapManagerRef.current, gpsData);
|
|
||||||
} else {
|
|
||||||
console.warn('GPS data is null, skipping getEntitiesData call');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -286,4 +378,4 @@ const HomePage: React.FC = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default HomePage;
|
export default HomePage;
|
||||||
@@ -31,7 +31,6 @@ export const osmLayer = new TileLayer({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// const layerVN = await getLayer('base-vn');
|
// const layerVN = await getLayer('base-vn');
|
||||||
|
|
||||||
export const tinhGeoLayer = createGeoJSONLayer({
|
export const tinhGeoLayer = createGeoJSONLayer({
|
||||||
data: '',
|
data: '',
|
||||||
style: createLabelAndFillStyle('#77BEF0', '#000000'), // vừa màu vừa label
|
style: createLabelAndFillStyle('#77BEF0', '#000000'), // vừa màu vừa label
|
||||||
|
|||||||
30
src/utils/cacheStore.ts
Normal file
30
src/utils/cacheStore.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
// src/utils/cacheStore.ts
|
||||||
|
interface CacheItem<T> {
|
||||||
|
data: T;
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TTL = 1 * 1000; // dữ liệu có hiệu lực trong 1 giây để GPS realtime
|
||||||
|
const cache: Record<string, CacheItem<any>> = {};
|
||||||
|
|
||||||
|
export function getCache<T>(key: string): T | null {
|
||||||
|
const item = cache[key];
|
||||||
|
if (!item) return null;
|
||||||
|
if (Date.now() - item.timestamp > TTL) {
|
||||||
|
delete cache[key];
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return item.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setCache<T>(key: string, data: T) {
|
||||||
|
cache[key] = { data, timestamp: Date.now() };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function invalidate(key: string) {
|
||||||
|
delete cache[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAllCacheKeys(): string[] {
|
||||||
|
return Object.keys(cache);
|
||||||
|
}
|
||||||
24
src/utils/eventBus.ts
Normal file
24
src/utils/eventBus.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
// src/utils/eventBus.ts
|
||||||
|
type EventHandler<T = any> = (data: T) => void;
|
||||||
|
|
||||||
|
class EventBus {
|
||||||
|
private listeners: Record<string, EventHandler[]> = {};
|
||||||
|
|
||||||
|
on(event: string, handler: EventHandler) {
|
||||||
|
if (!this.listeners[event]) this.listeners[event] = [];
|
||||||
|
this.listeners[event].push(handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
off(event: string, handler: EventHandler) {
|
||||||
|
this.listeners[event] = (this.listeners[event] || []).filter(
|
||||||
|
(h) => h !== handler,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(event: string, data?: any) {
|
||||||
|
(this.listeners[event] || []).forEach((h) => h(data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ phải có dòng này
|
||||||
|
export const eventBus = new EventBus();
|
||||||
27
src/utils/queryWithCache.ts
Normal file
27
src/utils/queryWithCache.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
// utils/queryWithCache.ts
|
||||||
|
import { getCache, setCache, invalidate, getAllCacheKeys } from './cacheStore';
|
||||||
|
import { eventBus } from './eventBus';
|
||||||
|
|
||||||
|
export async function queryWithCache<T>(
|
||||||
|
key: string,
|
||||||
|
fetcher: () => Promise<T>,
|
||||||
|
): Promise<T> {
|
||||||
|
const cached = getCache<T>(key);
|
||||||
|
if (cached) return cached;
|
||||||
|
|
||||||
|
const data = await fetcher();
|
||||||
|
setCache(key, data);
|
||||||
|
|
||||||
|
// Phát sự kiện để các component khác biết có data mới
|
||||||
|
eventBus.emit(`${key}:update`, data);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Xóa cache theo key hoặc prefix
|
||||||
|
queryWithCache.clear = (prefix = '') => {
|
||||||
|
const keys = getAllCacheKeys();
|
||||||
|
for (const key of keys) {
|
||||||
|
if (key.startsWith(prefix)) invalidate(key);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,3 +1,15 @@
|
|||||||
{
|
{
|
||||||
"extends": "./src/.umi/tsconfig.json"
|
"extends": "./src/.umi/tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es2017",
|
||||||
|
"lib": ["dom", "es2017"],
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"forceConsistentCasingInFileNames": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user