diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx index 8aaadf7..8428ca6 100644 --- a/app/(tabs)/index.tsx +++ b/app/(tabs)/index.tsx @@ -1,369 +1,188 @@ import { PolygonWithLabel } from "@/components/map/PolygonWithLabel"; import { PolylineWithLabel } from "@/components/map/PolylineWithLabel"; -import { showToastError } from "@/config"; import { - AUTO_REFRESH_INTERVAL, ENTITY, + EVENT_ALARM_DATA, + EVENT_BANZONE_DATA, + EVENT_ENTITY_DATA, + EVENT_GPS_DATA, + EVENT_TRACK_POINTS_DATA, IOS_PLATFORM, LIGHT_THEME, } from "@/constants"; -import { - queryAlarm, - queryEntities, - queryGpsData, - queryTrackPoints, -} from "@/controller/DeviceController"; import { useColorScheme } from "@/hooks/use-color-scheme.web"; import { usePlatform } from "@/hooks/use-platform"; +import { + getAlarmEventBus, + getBanzonesEventBus, + getEntitiesEventBus, + getGpsEventBus, + getTrackPointsEventBus, +} from "@/services/device_events"; import { getShipIcon } from "@/services/map_service"; -import { useBanzones } from "@/state/use-banzones"; +import eventBus from "@/utils/eventBus"; import { convertWKTLineStringToLatLngArray, convertWKTtoLatLngString, } from "@/utils/geom"; -import { Image as ExpoImage } from "expo-image"; import { useEffect, useRef, useState } from "react"; -import { StyleSheet, Text, TouchableOpacity, View } from "react-native"; - +import { + Animated, + Image as RNImage, + StyleSheet, + Text, + TouchableOpacity, + View, +} from "react-native"; import MapView, { Circle, Marker } from "react-native-maps"; -import { SafeAreaProvider } from "react-native-safe-area-context"; - -const testPolyline = - "MULTIPOLYGON(((108.7976074 17.5392966,110.390625 14.2217886,109.4677734 10.8548863,112.9227161 10.6933337,116.4383411 12.565622,116.8997669 17.0466095,109.8685169 17.8013229,108.7973446 17.5393669,108.7976074 17.5392966)))"; +import { SafeAreaView } from "react-native-safe-area-context"; export default function HomeScreen() { - const [gpsData, setGpsData] = useState(null); - const [alarmData, setAlarmData] = useState(null); - const [trackPoints, setTrackPoints] = useState( - null + const [gpsData, setGpsData] = useState( + undefined ); + const [alarmData, setAlarmData] = useState(null); + const [entityData, setEntityData] = useState< + Model.TransformedEntity[] | null + >(null); + const [banzoneData, setBanzoneData] = useState(null); + const [trackPointsData, setTrackPointsData] = useState< + Model.ShipTrackPoint[] | null + >(null); const [circleRadius, setCircleRadius] = useState(100); const [zoomLevel, setZoomLevel] = useState(10); const [isFirstLoad, setIsFirstLoad] = useState(true); const [polylineCoordinates, setPolylineCoordinates] = useState< - number[][] | null - >(null); + number[][] | undefined + >(undefined); const [polygonCoordinates, setPolygonCoordinates] = useState< - number[][][] | null - >(null); - const [, setZoneGeometries] = useState>(new Map()); - const intervalRef = useRef | null>(null); + number[][][] | undefined + >(undefined); const platform = usePlatform(); const theme = useColorScheme(); - const { banzones, getBanzone } = useBanzones(); - const banzonesRef = useRef(banzones); + const scale = useRef(new Animated.Value(0)).current; + const opacity = useRef(new Animated.Value(1)).current; // console.log("Platform: ", platform); // console.log("Theme: ", theme); - const getGpsData = async () => { - try { - const response = await queryGpsData(); - // console.log("GpsData: ", response.data); - // console.log( - // "Heading value:", - // response.data?.h, - // "Type:", - // typeof response.data?.h - // ); - setGpsData(response.data); - } catch (error) { - console.error("Error fetching GPS data:", error); - showToastError("Lỗi", "Không thể lấy dữ liệu GPS"); - } - }; - - const drawPolyline = () => { - const data = convertWKTtoLatLngString(testPolyline); - console.log("Data: ", data); - // setPolygonCoordinates(data[0]); - // ; - // console.log("Banzones: ", banzones.length); - }; + // const [number, setNumber] = useState(0); useEffect(() => { - banzonesRef.current = banzones; - }, [banzones]); + getGpsEventBus(); + getAlarmEventBus(); + getEntitiesEventBus(); + getBanzonesEventBus(); + getTrackPointsEventBus(); + const queryGpsData = (gpsData: Model.GPSResonse) => { + setGpsData(gpsData); + }; + const queryAlarmData = (alarmData: Model.AlarmResponse) => { + // console.log("Alarm Data: ", alarmData.alarms.length); + setAlarmData(alarmData); + }; + const queryEntityData = (entityData: Model.TransformedEntity[]) => { + // console.log("Entities Length Data: ", entityData.length); - const areGeometriesEqual = ( - left?: { - geom_type: number; - geom_lines?: string | null; - geom_poly?: string | null; - }, - right?: { - geom_type: number; - geom_lines?: string | null; - geom_poly?: string | null; - } - ) => { - if (!left && !right) { - return true; - } + setEntityData(entityData); + }; + const queryBanzonesData = (banzoneData: Model.Zone[]) => { + // console.log("Banzone Data: ", banzoneData.length); - if (!left || !right) { - return false; - } + setBanzoneData(banzoneData); + }; + const queryTrackPointsData = (TrackPointsData: Model.ShipTrackPoint[]) => { + // console.log("TrackPoints Data: ", TrackPointsData.length); + setTrackPointsData(TrackPointsData); + }; - return ( - left.geom_type === right.geom_type && - (left.geom_lines || "") === (right.geom_lines || "") && - (left.geom_poly || "") === (right.geom_poly || "") - ); - }; + eventBus.on(EVENT_GPS_DATA, queryGpsData); + console.log("Registering event handlers in HomeScreen"); + eventBus.on(EVENT_GPS_DATA, queryGpsData); + console.log("Subscribed to EVENT_GPS_DATA"); + eventBus.on(EVENT_ALARM_DATA, queryAlarmData); + console.log("Subscribed to EVENT_ALARM_DATA"); + eventBus.on(EVENT_ENTITY_DATA, queryEntityData); + console.log("Subscribed to EVENT_ENTITY_DATA"); + eventBus.on(EVENT_TRACK_POINTS_DATA, queryTrackPointsData); + console.log("Subscribed to EVENT_TRACK_POINTS_DATA"); + eventBus.once(EVENT_BANZONE_DATA, queryBanzonesData); + console.log("Subscribed once to EVENT_BANZONE_DATA"); - const areCoordinatesEqual = ( - current: number[][] | null, - next: number[][] | null - ) => { - if (!current || !next || current.length !== next.length) { - return false; - } - - return current.every( - (coord, index) => - coord[0] === next[index][0] && coord[1] === next[index][1] - ); - }; - - const areMultiPolygonCoordinatesEqual = ( - current: number[][][] | null, - next: number[][][] | null - ) => { - if (!current || !next || current.length !== next.length) { - return false; - } - - return current.every((polygon, polyIndex) => { - const nextPolygon = next[polyIndex]; - if (!nextPolygon || polygon.length !== nextPolygon.length) { - return false; - } - return polygon.every( - (coord, coordIndex) => - coord[0] === nextPolygon[coordIndex][0] && - coord[1] === nextPolygon[coordIndex][1] - ); - }); - }; - - const getAlarmData = async () => { - try { - const response = await queryAlarm(); - // console.log("AlarmData: ", response.data); - setAlarmData(response.data); - } catch (error) { - console.error("Error fetching Alarm Data: ", error); - showToastError("Lỗi", "Không thể lấy dữ liệu báo động"); - } - }; - - const getEntities = async () => { - try { - const entities = await queryEntities(); - if (!entities) { - // Clear tất cả khu vực khi không có dữ liệu - setPolylineCoordinates(null); - setPolygonCoordinates(null); - setZoneGeometries(new Map()); - return; - } - - const currentBanzones = banzonesRef.current || []; - let nextPolyline: number[][] | null = null; - let nextMultiPolygon: number[][][] | null = null; - let foundPolyline = false; - let foundPolygon = false; - - // Process zones để tìm geometries - for (const entity of entities) { - if (entity.id !== ENTITY.ZONE_ALARM_LIST) { - continue; - } - - let zones: any[] = []; - try { - zones = entity.valueString ? JSON.parse(entity.valueString) : []; - } catch (parseError) { - console.error("Error parsing zone list:", parseError); - continue; - } - - // Nếu danh sách zone rỗng, clear tất cả - if (zones.length === 0) { - setPolylineCoordinates(null); - setPolygonCoordinates(null); - setZoneGeometries(new Map()); - return; - } - - for (const zone of zones) { - const geom = currentBanzones.find((b) => b.id === zone.zone_id); - if (!geom) { - continue; - } - - const { geom_type, geom_lines, geom_poly } = geom.geom || {}; - if (typeof geom_type !== "number") { - continue; - } - - if (geom_type === 2) { - foundPolyline = true; - const coordinates = convertWKTLineStringToLatLngArray( - geom_lines || "" - ); - if (coordinates.length > 0) { - nextPolyline = coordinates; - } - } else if (geom_type === 1) { - foundPolygon = true; - const coordinates = convertWKTtoLatLngString(geom_poly || ""); - if (coordinates.length > 0) { - console.log("Polygon Coordinate: ", coordinates); - nextMultiPolygon = coordinates; - } - } - } - } - - // Update state sau khi đã process xong - setZoneGeometries((prevGeometries) => { - const updated = new Map(prevGeometries); - let hasChanges = false; - - for (const entity of entities) { - if (entity.id !== ENTITY.ZONE_ALARM_LIST) { - continue; - } - - let zones: any[] = []; - try { - zones = entity.valueString ? JSON.parse(entity.valueString) : []; - } catch { - continue; - } - - if (zones.length === 0) { - if (updated.size > 0) { - hasChanges = true; - updated.clear(); - } - break; - } - - for (const zone of zones) { - const geom = currentBanzones.find((b) => b.id === zone.zone_id); - if (!geom) { - continue; - } - - const { geom_type, geom_lines, geom_poly } = geom.geom || {}; - if (typeof geom_type !== "number") { - continue; - } - - const key = `${zone.zone_id}_${geom_type}`; - const newGeomData = { geom_type, geom_lines, geom_poly }; - const oldGeom = updated.get(key); - - if (!areGeometriesEqual(oldGeom, newGeomData)) { - hasChanges = true; - updated.set(key, newGeomData); - console.log("Geometry changed", { key, oldGeom, newGeomData }); - } - } - } - - return hasChanges ? updated : prevGeometries; - }); - - // Cập nhật hoặc clear polyline - if (foundPolyline && nextPolyline) { - setPolylineCoordinates((prev) => - areCoordinatesEqual(prev, nextPolyline) ? prev : nextPolyline - ); - } else if (!foundPolyline) { - console.log("Hết cảnh báo qua polyline"); - setPolylineCoordinates(null); - } - - // Cập nhật hoặc clear polygon - if (foundPolygon && nextMultiPolygon) { - setPolygonCoordinates((prev) => - areMultiPolygonCoordinatesEqual(prev, nextMultiPolygon) - ? prev - : nextMultiPolygon - ); - } else if (!foundPolygon) { - console.log("Hết cảnh báo qua polygon"); - setPolygonCoordinates(null); - } - } catch (error) { - console.error("Error fetching Entities: ", error); - // Clear tất cả khi có lỗi - setPolylineCoordinates(null); - setPolygonCoordinates(null); - setZoneGeometries(new Map()); - } - }; - - const getShipTrackPoints = async () => { - try { - const response = await queryTrackPoints(); - // console.log("TrackPoints Data Length: ", response.data.length); - setTrackPoints(response.data); - } catch (error) { - console.error("Error fetching TrackPoints Data: ", error); - showToastError("Lỗi", "Không thể lấy lịch sử di chuyển"); - } - }; - - const fetchAllData = async () => { - await Promise.all([ - getGpsData(), - getAlarmData(), - getShipTrackPoints(), - getEntities(), - ]); - }; - - const setupAutoRefresh = () => { - // Clear existing interval if any - if (intervalRef.current) { - clearInterval(intervalRef.current); - } - - // Set new interval to refresh data every 5 seconds - // Không fetch banzones vì dữ liệu không thay đổi - intervalRef.current = setInterval(async () => { - // console.log("Auto-refreshing data..."); - await fetchAllData(); - }, AUTO_REFRESH_INTERVAL); - }; - - const handleMapReady = () => { - // console.log("Map loaded successfully!"); - // Gọi fetchAllData ngay lập tức (không cần đợi banzones) - fetchAllData(); - setupAutoRefresh(); - // Set isFirstLoad to false sau khi map ready để chỉ zoom lần đầu tiên - setTimeout(() => { - setIsFirstLoad(false); - }, 2000); - }; - - // Cleanup interval on component unmount - useEffect(() => { return () => { - if (intervalRef.current) { - clearInterval(intervalRef.current); - } + console.log("Unregistering event handlers in HomeScreen"); + eventBus.off(EVENT_GPS_DATA, queryGpsData); + console.log("Unsubscribed EVENT_GPS_DATA"); + eventBus.off(EVENT_ALARM_DATA, queryAlarmData); + console.log("Unsubscribed EVENT_ALARM_DATA"); + eventBus.off(EVENT_ENTITY_DATA, queryEntityData); + console.log("Unsubscribed EVENT_ENTITY_DATA"); + eventBus.off(EVENT_TRACK_POINTS_DATA, queryTrackPointsData); + console.log("Unsubscribed EVENT_TRACK_POINTS_DATA"); }; }, []); useEffect(() => { - getBanzone(); - }, [getBanzone]); + if (polylineCoordinates !== undefined) { + console.log("Polyline Khac null"); + } else { + console.log("Polyline null"); + } + }, [polylineCoordinates]); + + useEffect(() => { + setPolylineCoordinates(undefined); + setPolygonCoordinates(undefined); + if (!entityData) return; + if (!banzoneData) return; + for (const entity of entityData) { + if (entity.id !== ENTITY.ZONE_ALARM_LIST) { + continue; + } + + let zones: any[] = []; + try { + zones = entity.valueString ? JSON.parse(entity.valueString) : []; + } catch (parseError) { + console.error("Error parsing zone list:", parseError); + continue; + } + // Nếu danh sách zone rỗng, clear tất cả + if (zones.length === 0) { + setPolylineCoordinates(undefined); + setPolygonCoordinates(undefined); + return; + } + + for (const zone of zones) { + console.log("Zone Data: ", zone); + const geom = banzoneData.find((b) => b.id === zone.zone_id); + if (!geom) { + continue; + } + const { geom_type, geom_lines, geom_poly } = geom.geom || {}; + if (typeof geom_type !== "number") { + continue; + } + if (geom_type === 2) { + // if(oldEntityData.find(e => e.id === )) + // foundPolyline = true; + const coordinates = convertWKTLineStringToLatLngArray( + geom_lines || "" + ); + if (coordinates.length > 0) { + setPolylineCoordinates(coordinates); + } + } else if (geom_type === 1) { + // foundPolygon = true; + const coordinates = convertWKTtoLatLngString(geom_poly || ""); + if (coordinates.length > 0) { + console.log("Polygon Coordinate: ", coordinates); + setPolygonCoordinates(coordinates); + } + } + } + } + }, [banzoneData, entityData]); // Hàm tính radius cố định khi zoom change const calculateRadiusFromZoom = (zoom: number) => { @@ -410,16 +229,51 @@ export default function HomeScreen() { }; }; + const handleMapReady = () => { + setTimeout(() => { + setIsFirstLoad(false); + }, 2000); + }; + + useEffect(() => { + if (alarmData?.level === 3) { + const loop = Animated.loop( + Animated.sequence([ + Animated.parallel([ + Animated.timing(scale, { + toValue: 3, // nở to 3 lần + duration: 1500, + useNativeDriver: true, + }), + Animated.timing(opacity, { + toValue: 0, // mờ dần + duration: 1500, + useNativeDriver: true, + }), + ]), + Animated.parallel([ + Animated.timing(scale, { + toValue: 0, + duration: 0, + useNativeDriver: true, + }), + Animated.timing(opacity, { + toValue: 1, + duration: 0, + useNativeDriver: true, + }), + ]), + ]) + ); + loop.start(); + return () => loop.stop(); + } + }, [alarmData?.level, scale, opacity]); + return ( - - {banzones.length > 0 && ( - Banzones loaded: {banzones.length} - )} + { - console.log("Poi clicked: ", point.nativeEvent); - }} onRegionChangeComplete={handleRegionChangeComplete} style={styles.map} // initialRegion={getMapRegion()} @@ -428,11 +282,12 @@ export default function HomeScreen() { showsBuildings={false} showsIndoors={false} loadingEnabled={true} - mapType="standard" + mapType={platform === IOS_PLATFORM ? "mutedStandard" : "standard"} + rotateEnabled={false} > - {trackPoints && - trackPoints.length > 0 && - trackPoints.map((point, index) => { + {trackPointsData && + trackPointsData.length > 0 && + trackPointsData.map((point, index) => { // console.log(`Rendering circle ${index}:`, point); return ( ); })} - {polylineCoordinates && ( + {polylineCoordinates !== undefined && ( ({ latitude: coord[0], @@ -462,7 +317,7 @@ export default function HomeScreen() { zIndex={50} /> )} - {polygonCoordinates && polygonCoordinates.length > 0 && ( + {polygonCoordinates !== undefined && ( <> {polygonCoordinates.map((polygon, index) => { // Tạo key ổn định từ tọa độ đầu tiên của polygon @@ -472,6 +327,17 @@ export default function HomeScreen() { : `polygon-${index}`; return ( + // ({ + // latitude: coords[0], + // longitude: coords[1], + // }))} + // fillColor="rgba(16, 85, 201, 0.6)" + // strokeColor="rgba(16, 85, 201, 0.8)" + // strokeWidth={2} + // zIndex={50} + // /> ({ @@ -490,7 +356,7 @@ export default function HomeScreen() { })} )} - {gpsData && ( + {gpsData !== undefined && ( - + + {alarmData?.level === 3 && ( + + )} + { + const icon = getShipIcon( + alarmData?.level || 0, + gpsData.fishing + ); + return typeof icon === "string" ? { uri: icon } : icon; + })()} + style={{ + width: 32, + height: 32, + transform: [ + { + rotate: `${ + typeof gpsData.h === "number" && !isNaN(gpsData.h) + ? gpsData.h + : 0 + }deg`, + }, + ], + }} + /> + )} - + { + setPolygonCoordinates(undefined); + setPolylineCoordinates(undefined); + }} + > Get GPS Data - + ); } @@ -540,7 +435,7 @@ const styles = StyleSheet.create({ flex: 1, }, button: { - display: "none", + // display: "none", position: "absolute", top: 50, right: 20, @@ -562,20 +457,25 @@ const styles = StyleSheet.create({ fontSize: 16, fontWeight: "600", }, - titleContainer: { - flexDirection: "row", + + pingContainer: { + width: 32, + height: 32, alignItems: "center", - gap: 8, + justifyContent: "center", + overflow: "visible", }, - stepContainer: { - gap: 8, - marginBottom: 8, - }, - reactLogo: { - height: 178, - width: 290, - bottom: 0, - left: 0, + pingCircle: { position: "absolute", + width: 40, + height: 40, + borderRadius: 20, + backgroundColor: "#ED3F27", + }, + centerDot: { + width: 20, + height: 20, + borderRadius: 10, + backgroundColor: "#0096FF", }, }); diff --git a/components/map/PolygonWithLabel.tsx b/components/map/PolygonWithLabel.tsx index d6ed13b..3944166 100644 --- a/components/map/PolygonWithLabel.tsx +++ b/components/map/PolygonWithLabel.tsx @@ -1,5 +1,5 @@ import { getPolygonCenter } from "@/utils/polyline"; -import React, { memo } from "react"; +import React from "react"; import { StyleSheet, Text, View } from "react-native"; import { Marker, Polygon } from "react-native-maps"; @@ -20,7 +20,7 @@ export interface PolygonWithLabelProps { /** * Component render Polygon kèm Label/Text ở giữa */ -const PolygonWithLabelComponent: React.FC = ({ +export const PolygonWithLabel: React.FC = ({ coordinates, label, content, @@ -65,7 +65,7 @@ const PolygonWithLabelComponent: React.FC = ({ {label && ( @@ -142,24 +142,3 @@ const styles = StyleSheet.create({ opacity: 0.95, }, }); - -// Export memoized component để tránh re-render không cần thiết -export const PolygonWithLabel = memo( - PolygonWithLabelComponent, - (prev, next) => { - // Custom comparison: chỉ re-render khi coordinates, label, content hoặc zoomLevel thay đổi - return ( - prev.coordinates.length === next.coordinates.length && - prev.coordinates.every( - (coord, index) => - coord.latitude === next.coordinates[index]?.latitude && - coord.longitude === next.coordinates[index]?.longitude - ) && - prev.label === next.label && - prev.content === next.content && - prev.zoomLevel === next.zoomLevel && - prev.fillColor === next.fillColor && - prev.strokeColor === next.strokeColor - ); - } -); diff --git a/components/map/PolylineWithLabel.tsx b/components/map/PolylineWithLabel.tsx index fc4a906..9d0366a 100644 --- a/components/map/PolylineWithLabel.tsx +++ b/components/map/PolylineWithLabel.tsx @@ -2,7 +2,7 @@ import { calculateTotalDistance, getMiddlePointOfPolyline, } from "@/utils/polyline"; -import React, { memo } from "react"; +import React from "react"; import { StyleSheet, Text, View } from "react-native"; import { Marker, Polyline } from "react-native-maps"; @@ -21,7 +21,7 @@ export interface PolylineWithLabelProps { /** * Component render Polyline kèm Label/Text ở giữa */ -const PolylineWithLabelComponent: React.FC = ({ +export const PolylineWithLabel: React.FC = ({ coordinates, label, strokeColor = "#FF5733", @@ -103,22 +103,3 @@ const styles = StyleSheet.create({ textAlign: "center", }, }); - -// Export memoized component để tránh re-render không cần thiết -export const PolylineWithLabel = memo( - PolylineWithLabelComponent, - (prev, next) => { - // Custom comparison: chỉ re-render khi coordinates, label hoặc showDistance thay đổi - return ( - prev.coordinates.length === next.coordinates.length && - prev.coordinates.every( - (coord, index) => - coord.latitude === next.coordinates[index]?.latitude && - coord.longitude === next.coordinates[index]?.longitude - ) && - prev.label === next.label && - prev.showDistance === next.showDistance && - prev.strokeColor === next.strokeColor - ); - } -); diff --git a/constants/index.ts b/constants/index.ts index ac31c0b..43f0e29 100644 --- a/constants/index.ts +++ b/constants/index.ts @@ -15,6 +15,12 @@ export const DARK_THEME = "dark"; export const ROUTE_LOGIN = "/login"; export const ROUTE_HOME = "/map"; export const ROUTE_TRIP = "/trip"; +// Event Emitters +export const EVENT_GPS_DATA = "GPS_DATA_EVENT"; +export const EVENT_ALARM_DATA = "ALARM_DATA_EVENT"; +export const EVENT_ENTITY_DATA = "ENTITY_DATA_EVENT"; +export const EVENT_BANZONE_DATA = "BANZONE_DATA_EVENT"; +export const EVENT_TRACK_POINTS_DATA = "TRACK_POINTS_DATA_EVENT"; // Entity Contants export const ENTITY = { diff --git a/eslint.config.js b/eslint.config.js index 5025da6..9f9347f 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,10 +1,10 @@ // https://docs.expo.dev/guides/using-eslint/ -const { defineConfig } = require('eslint/config'); -const expoConfig = require('eslint-config-expo/flat'); +import expoConfig from "eslint-config-expo/flat"; +import { defineConfig } from "eslint/config"; -module.exports = defineConfig([ +export default defineConfig([ expoConfig, { - ignores: ['dist/*'], + ignores: ["dist/*"], }, ]); diff --git a/package-lock.json b/package-lock.json index efb9344..090e4f8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@react-navigation/elements": "^2.6.3", "@react-navigation/native": "^7.1.8", "axios": "^1.13.1", + "eventemitter3": "^5.0.1", "expo": "~54.0.20", "expo-constants": "~18.0.10", "expo-font": "~14.0.9", @@ -6317,6 +6318,12 @@ "node": ">=6" } }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, "node_modules/exec-async": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/exec-async/-/exec-async-2.2.0.tgz", diff --git a/package.json b/package.json index 50e5934..a2bcc46 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@react-navigation/elements": "^2.6.3", "@react-navigation/native": "^7.1.8", "axios": "^1.13.1", + "eventemitter3": "^5.0.1", "expo": "~54.0.20", "expo-constants": "~18.0.10", "expo-font": "~14.0.9", diff --git a/services/device_events.ts b/services/device_events.ts new file mode 100644 index 0000000..a91b84a --- /dev/null +++ b/services/device_events.ts @@ -0,0 +1,164 @@ +import { + AUTO_REFRESH_INTERVAL, + EVENT_ALARM_DATA, + EVENT_BANZONE_DATA, + EVENT_ENTITY_DATA, + EVENT_GPS_DATA, + EVENT_TRACK_POINTS_DATA, +} from "@/constants"; +import { + queryAlarm, + queryEntities, + queryGpsData, + queryTrackPoints, +} from "@/controller/DeviceController"; +import { queryBanzones } from "@/controller/MapController"; +import eventBus from "@/utils/eventBus"; + +const intervals: { + gps: ReturnType | null; + alarm: ReturnType | null; + entities: ReturnType | null; + trackPoints: ReturnType | null; + banzones: ReturnType | null; +} = { + gps: null, + alarm: null, + entities: null, + trackPoints: null, + banzones: null, +}; + +export function getGpsEventBus() { + if (intervals.gps) return; + console.log("Starting GPS poller"); + + const getGpsData = async () => { + try { + console.log("GPS: fetching data..."); + const resp = await queryGpsData(); + if (resp && resp.data) { + console.log("GPS: emitting data", resp.data); + eventBus.emit(EVENT_GPS_DATA, resp.data); + } else { + console.log("GPS: no data returned"); + } + } catch (err) { + console.error("GPS: fetch error", err); + } + }; + + // Run immediately once, then schedule + getGpsData(); + intervals.gps = setInterval(() => { + getGpsData(); + }, AUTO_REFRESH_INTERVAL); +} + +export function getAlarmEventBus() { + if (intervals.alarm) return; + console.log("Goi ham get Alarm"); + const getAlarmData = async () => { + try { + console.log("Alarm: fetching data..."); + const resp = await queryAlarm(); + if (resp && resp.data) { + console.log( + "Alarm: emitting data", + resp.data?.alarms?.length ?? resp.data + ); + eventBus.emit(EVENT_ALARM_DATA, resp.data); + } else { + console.log("Alarm: no data returned"); + } + } catch (err) { + console.error("Alarm: fetch error", err); + } + }; + + getAlarmData(); + intervals.alarm = setInterval(() => { + getAlarmData(); + }, AUTO_REFRESH_INTERVAL); +} + +export function getEntitiesEventBus() { + if (intervals.entities) return; + console.log("Goi ham get Entities"); + const getEntitiesData = async () => { + try { + console.log("Entities: fetching data..."); + const resp = await queryEntities(); + if (resp && resp.length > 0) { + console.log("Entities: emitting", resp.length); + eventBus.emit(EVENT_ENTITY_DATA, resp); + } else { + console.log("Entities: no data returned"); + } + } catch (err) { + console.error("Entities: fetch error", err); + } + }; + + getEntitiesData(); + intervals.entities = setInterval(() => { + getEntitiesData(); + }, AUTO_REFRESH_INTERVAL); +} + +export function getTrackPointsEventBus() { + if (intervals.trackPoints) return; + console.log("Goi ham get Track Points"); + const getTrackPointsData = async () => { + try { + console.log("TrackPoints: fetching data..."); + const resp = await queryTrackPoints(); + if (resp && resp.data && resp.data.length > 0) { + console.log("TrackPoints: emitting", resp.data.length); + eventBus.emit(EVENT_TRACK_POINTS_DATA, resp.data); + } else { + console.log("TrackPoints: no data returned"); + } + } catch (err) { + console.error("TrackPoints: fetch error", err); + } + }; + + getTrackPointsData(); + intervals.trackPoints = setInterval(() => { + getTrackPointsData(); + }, AUTO_REFRESH_INTERVAL); +} + +export function getBanzonesEventBus() { + if (intervals.banzones) return; + const getBanzonesData = async () => { + try { + console.log("Banzones: fetching data..."); + const resp = await queryBanzones(); + if (resp && resp.data && resp.data.length > 0) { + console.log("Banzones: emitting", resp.data.length); + eventBus.emit(EVENT_BANZONE_DATA, resp.data); + } else { + console.log("Banzones: no data returned"); + } + } catch (err) { + console.error("Banzones: fetch error", err); + } + }; + + getBanzonesData(); + intervals.banzones = setInterval(() => { + getBanzonesData(); + }, AUTO_REFRESH_INTERVAL * 60); +} + +export function stopEvents() { + Object.keys(intervals).forEach((k) => { + const key = k as keyof typeof intervals; + if (intervals[key]) { + clearInterval(intervals[key] as ReturnType); + intervals[key] = null; + } + }); +} diff --git a/utils/eventBus.ts b/utils/eventBus.ts new file mode 100644 index 0000000..1f03296 --- /dev/null +++ b/utils/eventBus.ts @@ -0,0 +1,5 @@ +import EventEmitter from "eventemitter3"; + +const eventBus = new EventEmitter(); + +export default eventBus;