871 lines
28 KiB
TypeScript
871 lines
28 KiB
TypeScript
import DraggablePanel from "@/components/DraggablePanel";
|
|
import IconButton from "@/components/IconButton";
|
|
import AlarmList from "@/components/map/AlarmList";
|
|
import ShipInfo from "@/components/map/ShipInfo";
|
|
import { TagState, TagStateCallbackPayload } from "@/components/map/TagState";
|
|
import ZoneInMap from "@/components/map/ZoneInMap";
|
|
import ShipSearchForm, {
|
|
SearchShipResponse,
|
|
} from "@/components/ShipSearchForm";
|
|
import { ThemedText } from "@/components/themed-text";
|
|
import { EVENT_SEARCH_THINGS, IOS_PLATFORM, LIGHT_THEME } from "@/constants";
|
|
import { queryBanzoneById } from "@/controller/MapController";
|
|
import { usePlatform } from "@/hooks/use-platform";
|
|
import { useThemeContext } from "@/hooks/use-theme-context";
|
|
import { searchThingEventBus } from "@/services/device_events";
|
|
import { getShipIcon } from "@/services/map_service";
|
|
import eventBus from "@/utils/eventBus";
|
|
import { AntDesign, Ionicons } from "@expo/vector-icons";
|
|
import { useEffect, useRef, useState } from "react";
|
|
import {
|
|
Animated,
|
|
Dimensions,
|
|
Image,
|
|
ScrollView,
|
|
StyleSheet,
|
|
Text,
|
|
TouchableOpacity,
|
|
View,
|
|
} from "react-native";
|
|
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
|
import MapView, { Marker } from "react-native-maps";
|
|
|
|
interface ZoneDataParsed {
|
|
zone_type?: number;
|
|
zone_name?: string;
|
|
zone_id?: string;
|
|
message?: string;
|
|
alarm_type?: number;
|
|
lat?: number;
|
|
lon?: number;
|
|
s?: number;
|
|
h?: number;
|
|
fishing?: boolean;
|
|
gps_time?: number;
|
|
}
|
|
export interface AlarmData {
|
|
thing_id: string;
|
|
ship_name?: string;
|
|
zone: ZoneDataParsed;
|
|
type: "approaching" | "entered" | "fishing";
|
|
}
|
|
|
|
export interface BanzoneWithAlarm {
|
|
alarms: AlarmData;
|
|
zone?: Model.Zone;
|
|
}
|
|
|
|
export default function HomeScreen() {
|
|
const mapRef = useRef<MapView>(null);
|
|
const [alarms, setAlarms] = useState<AlarmData[]>([]);
|
|
const [banzoneWithAlarm, setBanzoneWithAlarm] =
|
|
useState<BanzoneWithAlarm | null>(null);
|
|
const [allBanZones, setAllBanZones] = useState<BanzoneWithAlarm[]>([]);
|
|
const [showAllAlarmsOnMap, setShowAllAlarmsOnMap] = useState(false);
|
|
const [isFirstLoad, setIsFirstLoad] = useState(true);
|
|
|
|
const [shipSearchFormOpen, setShipSearchFormOpen] = useState(false);
|
|
const [isPanelExpanded, setIsPanelExpanded] = useState(false);
|
|
const [things, setThings] = useState<Model.ThingsResponse | null>(null);
|
|
const platform = usePlatform();
|
|
const theme = useThemeContext().colorScheme;
|
|
const scale = useRef(new Animated.Value(0)).current;
|
|
const opacity = useRef(new Animated.Value(1)).current;
|
|
const [shipSearchFormData, setShipSearchFormData] = useState<
|
|
SearchShipResponse | undefined
|
|
>(undefined);
|
|
const [tagStatePayload, setTagStatePayload] =
|
|
useState<TagStateCallbackPayload | null>(null);
|
|
|
|
const [isShowAlarmList, setIsShowAlarmList] = useState(false);
|
|
// Control mount so we can animate close before unmounting
|
|
const [isAlarmListMounted, setIsAlarmListMounted] = useState(false);
|
|
// Thêm state để quản lý tracksViewChanges
|
|
const [tracksViewChanges, setTracksViewChanges] = useState(true);
|
|
|
|
// Alarm list animation
|
|
const screenHeight = Dimensions.get("window").height;
|
|
const alarmListHeight = Math.round(screenHeight * 0.3);
|
|
const alarmTranslateY = useRef(new Animated.Value(alarmListHeight)).current;
|
|
const alarmOpacity = useRef(new Animated.Value(0)).current;
|
|
const uiAnim = useRef(new Animated.Value(1)).current; // 1 visible, 0 hidden
|
|
|
|
useEffect(() => {
|
|
if (tagStatePayload) {
|
|
searchThings();
|
|
}
|
|
}, [tagStatePayload]);
|
|
|
|
const openAlarmList = () => {
|
|
setIsAlarmListMounted(true);
|
|
setIsShowAlarmList(true);
|
|
};
|
|
|
|
const closeAlarmList = () => {
|
|
// Trigger the animation; the effect will unmount when complete
|
|
setIsShowAlarmList(false);
|
|
setBanzoneWithAlarm(null);
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (shipSearchFormData) {
|
|
searchThings();
|
|
}
|
|
}, [shipSearchFormData]);
|
|
|
|
const bodySearchThings: Model.SearchThingBody = {
|
|
offset: 0,
|
|
limit: 50,
|
|
order: "name",
|
|
dir: "asc",
|
|
metadata: {
|
|
not_empty: "ship_id",
|
|
},
|
|
};
|
|
|
|
useEffect(() => {
|
|
searchThingEventBus(bodySearchThings);
|
|
const querySearchThingsData = (thingsData: Model.ThingsResponse) => {
|
|
const sortedThings: Model.Thing[] = (thingsData.things ?? []).sort(
|
|
(a, b) => {
|
|
const stateLevelA = a.metadata?.state_level || 0;
|
|
const stateLevelB = b.metadata?.state_level || 0;
|
|
return stateLevelB - stateLevelA; // Giảm dần
|
|
}
|
|
);
|
|
const sortedThingsResponse: Model.ThingsResponse = {
|
|
...thingsData,
|
|
things: sortedThings,
|
|
};
|
|
// console.log("Things Updated: ", sortedThingsResponse.things?.length);
|
|
|
|
setThings(sortedThingsResponse);
|
|
};
|
|
eventBus.on(EVENT_SEARCH_THINGS, querySearchThingsData);
|
|
|
|
return () => {
|
|
eventBus.off(EVENT_SEARCH_THINGS, querySearchThingsData);
|
|
};
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (things?.things) {
|
|
const alarmTypes = [
|
|
{
|
|
key: "zone_approaching_alarm_list",
|
|
type: "approaching" as const,
|
|
},
|
|
{ key: "zone_entered_alarm_list", type: "entered" as const },
|
|
{ key: "zone_fishing_alarm_list", type: "fishing" as const },
|
|
];
|
|
|
|
const newAlarms: AlarmData[] = [];
|
|
|
|
for (const thing of things.things) {
|
|
for (const { key, type } of alarmTypes) {
|
|
if ((thing.metadata as any)?.[key] != "[]") {
|
|
const zoneList: ZoneDataParsed[] = JSON.parse(
|
|
(thing?.metadata as any)?.[key] || "[]"
|
|
);
|
|
for (const zone of zoneList) {
|
|
const alarmData: AlarmData = {
|
|
thing_id: thing.id || "",
|
|
ship_name: thing.metadata?.ship_name,
|
|
zone: zone,
|
|
type: type,
|
|
};
|
|
newAlarms.push(alarmData);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update alarms, removing old ones not in newAlarms
|
|
setAlarms((prev) => {
|
|
const toKeep = prev.filter((a) =>
|
|
newAlarms.some(
|
|
(na) =>
|
|
na.thing_id === a.thing_id && na.zone.zone_id === a.zone.zone_id
|
|
)
|
|
);
|
|
const toAdd = newAlarms.filter(
|
|
(na) =>
|
|
!prev.some(
|
|
(a) =>
|
|
a.thing_id === na.thing_id && a.zone.zone_id === a.zone.zone_id
|
|
)
|
|
);
|
|
return [...toKeep, ...toAdd];
|
|
});
|
|
}
|
|
}, [things]);
|
|
|
|
useEffect(() => {
|
|
const approaching = alarms.filter((a) => a.type === "approaching");
|
|
const entered = alarms.filter((a) => a.type === "entered");
|
|
const fishing = alarms.filter((a) => a.type === "fishing");
|
|
|
|
if (approaching.length > 0) {
|
|
console.log("ZoneApproachingAlarm: ", approaching);
|
|
} else {
|
|
// console.log("No ZoneApproachingAlarm");
|
|
}
|
|
if (entered.length > 0) {
|
|
console.log("ZoneEnteredAlarm: ", entered);
|
|
} else {
|
|
// console.log("No ZoneEnteredAlarm");
|
|
}
|
|
if (fishing.length > 0) {
|
|
console.log("ZoneFishingAlarm: ", fishing);
|
|
} else {
|
|
// console.log("No ZoneFishingAlarm");
|
|
}
|
|
}, [alarms]);
|
|
|
|
// Load all banzones when alarms change (for showing all alarms on map)
|
|
useEffect(() => {
|
|
if (alarms.length > 0 && showAllAlarmsOnMap) {
|
|
loadAllBanZones();
|
|
}
|
|
}, [alarms, showAllAlarmsOnMap]);
|
|
|
|
const calculateRadiusFromZoom = (zoom: number) => {
|
|
const baseZoom = 10;
|
|
const baseRadius = 100;
|
|
const zoomDifference = baseZoom - zoom;
|
|
const calculatedRadius = baseRadius * Math.pow(2, zoomDifference);
|
|
// console.log("Caculate Radius: ", calculatedRadius);
|
|
|
|
return Math.max(calculatedRadius, 50);
|
|
};
|
|
|
|
// Xử lý khi region (zoom) thay đổi
|
|
const handleRegionChangeComplete = (newRegion: any) => {
|
|
// Tính zoom level từ latitudeDelta
|
|
// zoom = log2(360 / (latitudeDelta * 2)) + 8
|
|
const zoom = Math.round(Math.log2(360 / (newRegion.latitudeDelta * 2)) + 8);
|
|
const newRadius = calculateRadiusFromZoom(zoom);
|
|
// setCircleRadius(newRadius);
|
|
// setZoomLevel(zoom);
|
|
// console.log("Zoom level:", zoom, "Circle radius:", newRadius);
|
|
};
|
|
|
|
// Tính toán region để focus vào marker của tàu (chỉ lần đầu tiên)
|
|
const getMapRegion = () => {
|
|
if (!isFirstLoad) {
|
|
// Sau lần đầu, return undefined để không force region
|
|
return undefined;
|
|
}
|
|
if (things?.things?.length === 0) {
|
|
return {
|
|
latitude: 15.70581,
|
|
longitude: 116.152685,
|
|
latitudeDelta: 0.05,
|
|
longitudeDelta: 0.05,
|
|
};
|
|
}
|
|
|
|
return {
|
|
latitude: things?.things?.[0]?.metadata?.gps
|
|
? JSON.parse(things.things[0].metadata.gps).lat
|
|
: 15.70581,
|
|
longitude: things?.things?.[0]?.metadata?.gps
|
|
? JSON.parse(things.things[0].metadata.gps).lon
|
|
: 116.152685,
|
|
latitudeDelta: 0.05,
|
|
longitudeDelta: 0.05,
|
|
};
|
|
};
|
|
|
|
const handleMapReady = () => {
|
|
setTimeout(() => {
|
|
setIsFirstLoad(false);
|
|
}, 2000);
|
|
};
|
|
|
|
useEffect(() => {
|
|
for (var thing of things?.things || []) {
|
|
if (thing.metadata?.state_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();
|
|
}
|
|
}
|
|
}, [things, scale, opacity]);
|
|
|
|
// Tắt tracksViewChanges sau khi map đã load xong
|
|
useEffect(() => {
|
|
if (!isFirstLoad) {
|
|
// Delay một chút để đảm bảo markers đã render xong
|
|
const timer = setTimeout(() => {
|
|
setTracksViewChanges(false);
|
|
}, 3000);
|
|
return () => clearTimeout(timer);
|
|
}
|
|
}, [isFirstLoad]);
|
|
|
|
// Animate alarm panel when isShowAlarmList changes
|
|
// Keep the overlay mounted while animating it in/out.
|
|
useEffect(() => {
|
|
if (isShowAlarmList) {
|
|
// Ensure mounted then animate to visible
|
|
setIsAlarmListMounted(true);
|
|
Animated.parallel([
|
|
Animated.timing(alarmTranslateY, {
|
|
toValue: 0,
|
|
duration: 300,
|
|
useNativeDriver: true,
|
|
}),
|
|
Animated.timing(alarmOpacity, {
|
|
toValue: 1,
|
|
duration: 300,
|
|
useNativeDriver: true,
|
|
}),
|
|
]).start();
|
|
} else {
|
|
// Animate out, then unmount
|
|
Animated.parallel([
|
|
Animated.timing(alarmTranslateY, {
|
|
toValue: alarmListHeight,
|
|
duration: 300,
|
|
useNativeDriver: true,
|
|
}),
|
|
Animated.timing(alarmOpacity, {
|
|
toValue: 0,
|
|
duration: 300,
|
|
useNativeDriver: true,
|
|
}),
|
|
]).start(() => {
|
|
setIsAlarmListMounted(false);
|
|
});
|
|
}
|
|
}, [alarmTranslateY, alarmListHeight, alarmOpacity, isShowAlarmList]);
|
|
|
|
useEffect(() => {
|
|
Animated.timing(uiAnim, {
|
|
toValue: isAlarmListMounted ? 0 : 1,
|
|
duration: 200,
|
|
useNativeDriver: true,
|
|
}).start();
|
|
}, [uiAnim, isAlarmListMounted]);
|
|
|
|
const searchThings = async () => {
|
|
// console.log("FormSearch Playload in Search Thing: ", shipSearchFormData);
|
|
|
|
// Xây dựng query state dựa trên logic bạn cung cấp
|
|
const stateNormalQuery = tagStatePayload?.isNormal ? "normal" : "";
|
|
const stateSosQuery = tagStatePayload?.isSos ? "sos" : "";
|
|
const stateWarningQuery = tagStatePayload?.isWarning
|
|
? stateNormalQuery + ",warning"
|
|
: stateNormalQuery;
|
|
const stateCriticalQuery = tagStatePayload?.isDangerous
|
|
? stateWarningQuery + ",critical"
|
|
: stateWarningQuery;
|
|
|
|
// Nếu bật tất cả filter thì không cần truyền stateQuery
|
|
const stateQuery =
|
|
tagStatePayload?.isNormal &&
|
|
tagStatePayload?.isWarning &&
|
|
tagStatePayload?.isDangerous &&
|
|
tagStatePayload?.isSos
|
|
? ""
|
|
: [stateCriticalQuery, stateSosQuery].filter(Boolean).join(",");
|
|
let metaFormQuery = {};
|
|
if (tagStatePayload?.isDisconected)
|
|
metaFormQuery = { ...metaFormQuery, connected: false };
|
|
if (shipSearchFormData?.ship_name) {
|
|
metaFormQuery = {
|
|
...metaFormQuery,
|
|
ship_name: shipSearchFormData?.ship_name,
|
|
};
|
|
}
|
|
if (shipSearchFormData?.reg_number) {
|
|
metaFormQuery = {
|
|
...metaFormQuery,
|
|
reg_number: shipSearchFormData?.reg_number,
|
|
};
|
|
}
|
|
if (
|
|
shipSearchFormData?.ship_length[0] !== 0 ||
|
|
shipSearchFormData?.ship_length[1] !== 100
|
|
) {
|
|
metaFormQuery = {
|
|
...metaFormQuery,
|
|
ship_length: shipSearchFormData?.ship_length,
|
|
};
|
|
}
|
|
if (
|
|
shipSearchFormData?.ship_power[0] !== 0 ||
|
|
shipSearchFormData?.ship_power[1] !== 100000
|
|
) {
|
|
metaFormQuery = {
|
|
...metaFormQuery,
|
|
ship_power: shipSearchFormData?.ship_power,
|
|
};
|
|
}
|
|
if (shipSearchFormData?.alarm_list) {
|
|
metaFormQuery = {
|
|
...metaFormQuery,
|
|
alarm_list: shipSearchFormData?.alarm_list,
|
|
};
|
|
}
|
|
if (shipSearchFormData?.ship_type) {
|
|
metaFormQuery = {
|
|
...metaFormQuery,
|
|
ship_type: shipSearchFormData?.ship_type,
|
|
};
|
|
}
|
|
if (shipSearchFormData?.ship_group_id) {
|
|
metaFormQuery = {
|
|
...metaFormQuery,
|
|
ship_group_id: shipSearchFormData?.ship_group_id,
|
|
};
|
|
}
|
|
// Tạo metadata query
|
|
const metaStateQuery =
|
|
stateQuery !== "" ? { state_level: stateQuery } : null;
|
|
|
|
// Tạo body search với filter
|
|
const searchParams: Model.SearchThingBody = {
|
|
offset: 0,
|
|
limit: 50,
|
|
order: "name",
|
|
dir: "asc",
|
|
metadata: {
|
|
...metaFormQuery,
|
|
...metaStateQuery,
|
|
not_empty: "ship_id",
|
|
},
|
|
};
|
|
// console.log("Search Params: ", searchParams);
|
|
|
|
// Gọi API tìm kiếm
|
|
searchThingEventBus(searchParams);
|
|
};
|
|
const handleOnSubmitSearchForm = async (data: SearchShipResponse) => {
|
|
setShipSearchFormData(data);
|
|
setShipSearchFormOpen(false);
|
|
};
|
|
|
|
const loadAllBanZones = async () => {
|
|
try {
|
|
const banzonePromises = alarms.map(async (alarm) => {
|
|
try {
|
|
const banzone = await queryBanzoneById(alarm.zone.zone_id || "");
|
|
return {
|
|
alarms: alarm,
|
|
zone: banzone.data,
|
|
};
|
|
} catch (error) {
|
|
console.warn(`Cannot get banzone for zone_id: ${alarm.zone.zone_id}`);
|
|
return null;
|
|
}
|
|
});
|
|
|
|
const banzoneResults = await Promise.all(banzonePromises);
|
|
const validBanZones = banzoneResults.filter(
|
|
(banzone): banzone is NonNullable<typeof banzone> => banzone !== null
|
|
);
|
|
setAllBanZones(validBanZones);
|
|
} catch (error) {
|
|
console.error("Error loading all banzones:", error);
|
|
}
|
|
};
|
|
|
|
const handleAlarmPress = async (alarm: AlarmData) => {
|
|
console.log("Alarm pressed from list:", alarm);
|
|
try {
|
|
const banzone = await queryBanzoneById(alarm.zone.zone_id || "");
|
|
// console.log("Banzone API response:", banzone);
|
|
// console.log("Banzone data:", banzone.data);
|
|
// console.log("Zone geometry:", banzone.data?.geometry);
|
|
const banzoneWithAlarm: BanzoneWithAlarm = {
|
|
alarms: alarm,
|
|
zone: banzone.data,
|
|
};
|
|
|
|
setBanzoneWithAlarm(banzoneWithAlarm);
|
|
setShowAllAlarmsOnMap(false);
|
|
} catch (error) {
|
|
console.error("Cannot get Banzone:", error);
|
|
}
|
|
};
|
|
|
|
const hasActiveFilters = shipSearchFormData
|
|
? shipSearchFormData.ship_name !== "" ||
|
|
shipSearchFormData.reg_number !== "" ||
|
|
shipSearchFormData.ship_length[0] !== 0 ||
|
|
shipSearchFormData.ship_length[1] !== 100 ||
|
|
shipSearchFormData.ship_power[0] !== 0 ||
|
|
shipSearchFormData.ship_power[1] !== 100000 ||
|
|
shipSearchFormData.ship_type !== "" ||
|
|
shipSearchFormData.alarm_list !== "" ||
|
|
shipSearchFormData.ship_group_id !== "" ||
|
|
shipSearchFormData.group_id !== ""
|
|
: false;
|
|
|
|
return (
|
|
<GestureHandlerRootView style={styles.container}>
|
|
<View style={styles.container}>
|
|
<MapView
|
|
ref={mapRef}
|
|
onMapReady={handleMapReady}
|
|
onRegionChangeComplete={handleRegionChangeComplete}
|
|
style={styles.map}
|
|
region={getMapRegion()}
|
|
userInterfaceStyle={theme === LIGHT_THEME ? "light" : "dark"}
|
|
showsBuildings={false}
|
|
showsIndoors={false}
|
|
loadingEnabled={true}
|
|
mapType={platform === IOS_PLATFORM ? "mutedStandard" : "standard"}
|
|
rotateEnabled={false}
|
|
// onMarkerPress={onMarkerPress}
|
|
>
|
|
{!banzoneWithAlarm && things?.things && things.things.length > 0 && (
|
|
<>
|
|
{things.things
|
|
.filter((thing) => thing.metadata?.gps) // Filter trước để tránh null check
|
|
.map((thing, index) => {
|
|
const gpsData: Model.GPSResponse = JSON.parse(
|
|
thing.metadata!.gps!
|
|
);
|
|
|
|
// Tạo unique key dựa trên thing.id hoặc tọa độ
|
|
const uniqueKey = thing.id
|
|
? `marker-${thing.id}-${index}`
|
|
: `marker-${gpsData.lat.toFixed(6)}-${gpsData.lon.toFixed(
|
|
6
|
|
)}-${index}`;
|
|
|
|
return (
|
|
<Marker
|
|
key={uniqueKey}
|
|
coordinate={{
|
|
latitude: gpsData.lat,
|
|
longitude: gpsData.lon,
|
|
}}
|
|
zIndex={50}
|
|
anchor={{ x: 0.5, y: 0.5 }}
|
|
title={thing.metadata?.ship_name}
|
|
description={`Trạng thái: ${
|
|
gpsData.fishing ? "Đang đánh bắt" : "Không đánh bắt"
|
|
}`}
|
|
// Chỉ tracks changes khi cần thiết
|
|
tracksViewChanges={
|
|
platform === IOS_PLATFORM ? tracksViewChanges : true
|
|
}
|
|
// Thêm identifier để iOS optimize
|
|
identifier={uniqueKey}
|
|
>
|
|
{/* <Callout tooltip></Callout> */}
|
|
<View className="w-8 h-8 items-center justify-center">
|
|
<View style={styles.pingContainer}>
|
|
{thing.metadata?.state_level === 3 && (
|
|
<Animated.View
|
|
style={[
|
|
styles.pingCircle,
|
|
{
|
|
transform: [{ scale }],
|
|
opacity,
|
|
},
|
|
]}
|
|
/>
|
|
)}
|
|
<Image
|
|
source={(() => {
|
|
const icon = getShipIcon(
|
|
thing.metadata?.state_level || 0,
|
|
gpsData.fishing
|
|
);
|
|
// console.log("Ship icon:", icon, "for level:", alarmData?.level, "fishing:", 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`,
|
|
},
|
|
],
|
|
}}
|
|
/>
|
|
</View>
|
|
</View>
|
|
</Marker>
|
|
);
|
|
})}
|
|
</>
|
|
)}
|
|
{(banzoneWithAlarm ||
|
|
(showAllAlarmsOnMap && allBanZones.length > 0)) && (
|
|
<ZoneInMap
|
|
banzones={banzoneWithAlarm ? [banzoneWithAlarm] : allBanZones}
|
|
mapRef={mapRef}
|
|
/>
|
|
)}
|
|
</MapView>
|
|
<View className="absolute top-12 left-5">
|
|
{!isAlarmListMounted && !isPanelExpanded && (
|
|
<IconButton
|
|
icon={<AntDesign name="filter" size={16} />}
|
|
type="primary"
|
|
size="large"
|
|
style={{
|
|
borderRadius: 10,
|
|
backgroundColor: hasActiveFilters ? "#B8D576" : "#fff",
|
|
}}
|
|
onPress={() => setShipSearchFormOpen(true)}
|
|
></IconButton>
|
|
)}
|
|
</View>
|
|
{/* <View className="absolute top-14 right-2 shadow-md">
|
|
<SosButton />
|
|
</View>
|
|
<GPSInfoPanel gpsData={gpsData!} /> */}
|
|
|
|
{/* Draggable Panel */}
|
|
{!isAlarmListMounted && (
|
|
<DraggablePanel
|
|
minHeightPct={0.1}
|
|
maxHeightPct={0.6}
|
|
initialState="min"
|
|
onExpandedChange={(expanded) => {
|
|
// console.log("Panel expanded:", expanded);
|
|
setIsPanelExpanded(expanded);
|
|
}}
|
|
>
|
|
<>
|
|
<View className="flex flex-row gap-4 items-center justify-center">
|
|
<TagState
|
|
normalCount={things?.metadata?.total_state_level_0 || 0}
|
|
warningCount={things?.metadata?.total_state_level_1 || 0}
|
|
dangerousCount={things?.metadata?.total_state_level_2 || 0}
|
|
sosCount={things?.metadata?.total_sos || 0}
|
|
disconnectedCount={
|
|
(things?.metadata?.total_thing ?? 0) -
|
|
(things?.metadata?.total_connected ?? 0) || 0
|
|
}
|
|
onTagPress={(tagState) => {
|
|
setTagStatePayload(tagState);
|
|
}}
|
|
/>
|
|
</View>
|
|
<View style={{ width: "100%", paddingVertical: 8, flex: 1 }}>
|
|
<View className="flex flex-row items-center">
|
|
<View className="flex-1 justify-center">
|
|
<Text
|
|
style={{
|
|
fontSize: 20,
|
|
textAlign: "center",
|
|
fontWeight: "600",
|
|
}}
|
|
>
|
|
Danh sách tàu thuyền
|
|
</Text>
|
|
</View>
|
|
{isPanelExpanded && (
|
|
<IconButton
|
|
icon={<AntDesign name="filter" size={16} />}
|
|
type="primary"
|
|
shape="circle"
|
|
size="middle"
|
|
style={{
|
|
// borderRadius: 10,
|
|
backgroundColor: hasActiveFilters ? "#B8D576" : "#fff",
|
|
}}
|
|
onPress={() => setShipSearchFormOpen(true)}
|
|
></IconButton>
|
|
)}
|
|
</View>
|
|
|
|
<ScrollView
|
|
style={{ width: "100%", marginTop: 8, flex: 1 }}
|
|
contentContainerStyle={{
|
|
alignItems: "center",
|
|
paddingBottom: 24,
|
|
}}
|
|
showsVerticalScrollIndicator={false}
|
|
>
|
|
{things && things.things && things.things.length > 0 && (
|
|
<>
|
|
{things.things.map((thing, index) => {
|
|
return (
|
|
<View
|
|
key={thing.id ?? index}
|
|
style={{ width: "100%", alignItems: "center" }}
|
|
>
|
|
<ShipInfo thingMetadata={thing.metadata} />
|
|
{index < (things.things?.length ?? 0) - 1 && (
|
|
<View
|
|
style={{
|
|
width: "50%",
|
|
height: 1,
|
|
backgroundColor: "#E5E7EB",
|
|
marginVertical: 8,
|
|
borderRadius: 1,
|
|
}}
|
|
/>
|
|
)}
|
|
</View>
|
|
);
|
|
})}
|
|
</>
|
|
)}
|
|
</ScrollView>
|
|
</View>
|
|
</>
|
|
</DraggablePanel>
|
|
)}
|
|
{/* Alarm list overlay */}
|
|
{isAlarmListMounted && (
|
|
<Animated.View
|
|
style={{
|
|
position: "absolute",
|
|
left: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
height: alarmListHeight,
|
|
transform: [{ translateY: alarmTranslateY }],
|
|
opacity: alarmOpacity,
|
|
elevation: 10,
|
|
zIndex: 10,
|
|
}}
|
|
>
|
|
<View className="bg-white rounded-t-3xl shadow-md overflow-hidden h-full z-50">
|
|
<View className="flex-row items-center justify-between px-4 py-2">
|
|
<Text className="text-lg font-semibold">
|
|
Danh sách cảnh báo
|
|
</Text>
|
|
<TouchableOpacity
|
|
onPress={() => closeAlarmList()}
|
|
style={{ padding: 6 }}
|
|
>
|
|
<Ionicons name="close" size={20} color="#374151" />
|
|
</TouchableOpacity>
|
|
</View>
|
|
<View
|
|
style={{
|
|
flex: 1,
|
|
backgroundColor: "#F9FAFB",
|
|
zIndex: 50,
|
|
}}
|
|
>
|
|
{/* <ThemedText className="text-lg font-semibold">Body</ThemedText> */}
|
|
<AlarmList data={alarms} onPress={handleAlarmPress} />
|
|
</View>
|
|
</View>
|
|
</Animated.View>
|
|
)}
|
|
<ShipSearchForm
|
|
initialValues={shipSearchFormData}
|
|
isOpen={shipSearchFormOpen}
|
|
onClose={() => setShipSearchFormOpen(false)}
|
|
onSubmit={handleOnSubmitSearchForm}
|
|
/>
|
|
{!isAlarmListMounted && alarms.length > 0 && (
|
|
<View className="absolute top-12 right-5 space-y-2">
|
|
<IconButton
|
|
icon={<Ionicons name="warning" size={16} color="#fff" />}
|
|
type="danger"
|
|
size="middle"
|
|
onPress={() => openAlarmList()}
|
|
>
|
|
<ThemedText className="text-sm font-semibold">
|
|
{alarms.length}
|
|
</ThemedText>
|
|
</IconButton>
|
|
</View>
|
|
)}
|
|
</View>
|
|
</GestureHandlerRootView>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
},
|
|
map: {
|
|
flex: 1,
|
|
},
|
|
button: {
|
|
// display: "none",
|
|
position: "absolute",
|
|
top: 50,
|
|
right: 20,
|
|
backgroundColor: "#007AFF",
|
|
paddingHorizontal: 16,
|
|
paddingVertical: 12,
|
|
borderRadius: 8,
|
|
elevation: 5,
|
|
shadowColor: "#000",
|
|
shadowOffset: {
|
|
width: 0,
|
|
height: 2,
|
|
},
|
|
shadowOpacity: 0.25,
|
|
shadowRadius: 3.84,
|
|
},
|
|
buttonText: {
|
|
color: "#fff",
|
|
fontSize: 16,
|
|
fontWeight: "600",
|
|
},
|
|
|
|
pingContainer: {
|
|
width: 32,
|
|
height: 32,
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
overflow: "visible",
|
|
},
|
|
pingCircle: {
|
|
position: "absolute",
|
|
width: 40,
|
|
height: 40,
|
|
borderRadius: 20,
|
|
backgroundColor: "#ED3F27",
|
|
},
|
|
centerDot: {
|
|
width: 20,
|
|
height: 20,
|
|
borderRadius: 10,
|
|
backgroundColor: "#0096FF",
|
|
},
|
|
});
|