fix: adjust ping animation duration and simplify keyframes for better performance

This commit is contained in:
vergiLgood1 2025-05-15 10:09:31 +07:00
parent b9f69ade3b
commit da93032a24
2 changed files with 98 additions and 48 deletions

View File

@ -2,19 +2,49 @@
import { useEffect, useCallback, useRef, useState } from "react" import { useEffect, useCallback, useRef, useState } from "react"
import type { IIncidentLogs } from "@/app/_utils/types/crimes" import type { IIncidentLogs } from "@/app/_utils/types/crimes"
import { BASE_BEARING, BASE_DURATION, BASE_PITCH, ZOOM_3D } from "@/app/_utils/const/map" import { BASE_BEARING, BASE_DURATION, BASE_PITCH, BASE_ZOOM, PITCH_3D, ZOOM_3D } from "@/app/_utils/const/map"
import IncidentLogsPopup from "../pop-up/incident-logs-popup" import IncidentLogsPopup from "../pop-up/incident-logs-popup"
import type mapboxgl from "mapbox-gl"
import type { MapMouseEvent, MapGeoJSONFeature } from "react-map-gl/mapbox"
interface RecentIncidentsLayerProps { interface IRecentIncidentsLayerProps {
visible?: boolean visible?: boolean
map: any map: mapboxgl.Map
incidents?: IIncidentLogs[] incidents?: IIncidentLogs[]
} }
export default function RecentIncidentsLayer({ visible = false, map, incidents = [] }: RecentIncidentsLayerProps) { // Define a proper structure for GeoJSON feature properties
interface IIncidentFeatureProperties {
id: string
role_id?: string
user_id?: string
name?: string
email?: string
telephone?: string
avatar?: string
role?: string
address?: string
description?: string
timestamp: string
category?: string
district?: string
severity?: string
status?: boolean | string
source?: string
isVeryRecent: boolean
timeDiff: number
}
// Define a proper incident object type that will be set in state
interface IIncidentDetails extends Omit<IIncidentLogs, 'timestamp'> {
timestamp: Date
isVeryRecent?: boolean
}
export default function RecentIncidentsLayer({ visible = false, map, incidents = [] }: IRecentIncidentsLayerProps) {
const isInteractingWithMarker = useRef(false) const isInteractingWithMarker = useRef(false)
const animationFrameRef = useRef<number | null>(null) const animationFrameRef = useRef<number | null>(null)
const [selectedIncident, setSelectedIncident] = useState<IIncidentLogs | null>(null) const [selectedIncident, setSelectedIncident] = useState<IIncidentDetails | null>(null)
// Filter incidents from the last 24 hours // Filter incidents from the last 24 hours
const recentIncidents = incidents.filter((incident) => { const recentIncidents = incidents.filter((incident) => {
@ -30,7 +60,7 @@ export default function RecentIncidentsLayer({ visible = false, map, incidents =
const twoHoursInMs = 2 * 60 * 60 * 1000 // 2 hours in milliseconds const twoHoursInMs = 2 * 60 * 60 * 1000 // 2 hours in milliseconds
const handleIncidentClick = useCallback( const handleIncidentClick = useCallback(
(e: any) => { (e: MapMouseEvent & { features?: MapGeoJSONFeature[] }) => {
if (!map) return if (!map) return
const features = map.queryRenderedFeatures(e.point, { layers: ["recent-incidents"] }) const features = map.queryRenderedFeatures(e.point, { layers: ["recent-incidents"] })
@ -48,40 +78,42 @@ export default function RecentIncidentsLayer({ visible = false, map, incidents =
e.originalEvent.stopPropagation() e.originalEvent.stopPropagation()
e.preventDefault() e.preventDefault()
const incidentDetails = { const props = incident.properties as IIncidentFeatureProperties
id: incident.properties.id,
description: incident.properties.description, const IincidentDetails: IIncidentDetails = {
status: incident.properties?.status || "Active", id: props.id,
verified: incident.properties?.status, description: props.description || "",
verified: Boolean(props.status),
longitude: (incident.geometry as any).coordinates[0], longitude: (incident.geometry as any).coordinates[0],
latitude: (incident.geometry as any).coordinates[1], latitude: (incident.geometry as any).coordinates[1],
timestamp: new Date(incident.properties.timestamp || Date.now()), timestamp: new Date(props.timestamp || Date.now()),
category: incident.properties.category, category: props.category || "Unknown",
address: incident.properties.address, address: props.address || "Unknown",
district: incident.properties.district, district: props.district || "Unknown",
severity: incident.properties.severity, severity: (props.severity === "Low" || props.severity === "Medium" || props.severity === "High") ? props.severity : "Unknown",
source: incident.properties.source, source: props.source || "Unknown",
user_id: incident.properties.user_id, user_id: props.user_id || "Unknown",
name: incident.properties.name, name: props.name || "Unknown",
email: incident.properties.email, email: props.email || "Unknown",
phone: incident.properties.telephone, phone: props.telephone || "Unknown",
avatar: incident.properties.avatar, avatar: props.avatar || "Unknown",
role_id: incident.properties.role_id, role_id: props.role_id || "Unknown",
role: incident.properties.role, role: props.role || "Unknown",
isVeryRecent: incident.properties.isVeryRecent, isVeryRecent: props.isVeryRecent,
} }
// Fly to the incident location // Fly to the incident location
map.flyTo({ map.flyTo({
center: [incidentDetails.longitude, incidentDetails.latitude], center: [IincidentDetails.longitude, IincidentDetails.latitude],
zoom: ZOOM_3D, zoom: ZOOM_3D,
bearing: BASE_BEARING, bearing: BASE_BEARING,
pitch: BASE_PITCH, pitch: PITCH_3D,
duration: BASE_DURATION, duration: BASE_DURATION,
}) })
// Set selected incident for the popup // Set selected incident for the popup
setSelectedIncident(incidentDetails) setSelectedIncident(IincidentDetails)
// Reset the flag after a delay // Reset the flag after a delay
setTimeout(() => { setTimeout(() => {
@ -93,8 +125,17 @@ export default function RecentIncidentsLayer({ visible = false, map, incidents =
// Handle popup close // Handle popup close
const handleClosePopup = useCallback(() => { const handleClosePopup = useCallback(() => {
if (!map) return
map.easeTo({
zoom: BASE_ZOOM,
bearing: BASE_BEARING,
pitch: BASE_PITCH,
duration: BASE_DURATION,
});
setSelectedIncident(null) setSelectedIncident(null)
}, []) }, [map])
useEffect(() => { useEffect(() => {
if (!map || !visible) return if (!map || !visible) return
@ -102,17 +143,30 @@ export default function RecentIncidentsLayer({ visible = false, map, incidents =
// Convert incidents to GeoJSON with an additional property for recency // Convert incidents to GeoJSON with an additional property for recency
const now = new Date().getTime() const now = new Date().getTime()
const recentData = { // Define our GeoJSON structure with proper typing
type: "FeatureCollection" as const, interface IncidentGeoJSON {
type: 'FeatureCollection'
features: Array<{
type: 'Feature'
geometry: {
type: 'Point'
coordinates: [number, number]
}
properties: IIncidentFeatureProperties
}>
}
const recentData: IncidentGeoJSON = {
type: "FeatureCollection",
features: recentIncidents.map((incident) => { features: recentIncidents.map((incident) => {
const timestamp = incident.timestamp ? new Date(incident.timestamp).getTime() : now const timestamp = incident.timestamp ? new Date(incident.timestamp).getTime() : now
const timeDiff = now - timestamp const timeDiff = now - timestamp
const isVeryRecent = timeDiff <= twoHoursInMs const isVeryRecent = timeDiff <= twoHoursInMs
return { return {
type: "Feature" as const, type: "Feature",
geometry: { geometry: {
type: "Point" as const, type: "Point",
coordinates: [incident.longitude, incident.latitude], coordinates: [incident.longitude, incident.latitude],
}, },
properties: { properties: {
@ -132,8 +186,8 @@ export default function RecentIncidentsLayer({ visible = false, map, incidents =
severity: incident.severity, severity: incident.severity,
status: incident.verified, status: incident.verified,
source: incident.source, source: incident.source,
isVeryRecent: isVeryRecent, // Add this property to identify very recent incidents isVeryRecent,
timeDiff: timeDiff, // Time difference in milliseconds timeDiff,
}, },
} }
}), }),
@ -143,7 +197,8 @@ export default function RecentIncidentsLayer({ visible = false, map, incidents =
try { try {
// Check if source exists and update it // Check if source exists and update it
if (map.getSource("recent-incidents-source")) { if (map.getSource("recent-incidents-source")) {
; (map.getSource("recent-incidents-source") as any).setData(recentData) const source = map.getSource("recent-incidents-source") as mapboxgl.GeoJSONSource
source.setData(recentData)
} else { } else {
// If not, add source // If not, add source
map.addSource("recent-incidents-source", { map.addSource("recent-incidents-source", {
@ -271,7 +326,7 @@ export default function RecentIncidentsLayer({ visible = false, map, incidents =
} }
// Create animation for very recent incidents // Create animation for very recent incidents
const animatePulse = () => { const animatePing = () => {
if (!map || !map.getLayer("very-recent-incidents-pulse")) { if (!map || !map.getLayer("very-recent-incidents-pulse")) {
return return
} }
@ -295,7 +350,7 @@ export default function RecentIncidentsLayer({ visible = false, map, incidents =
]) ])
// Continue animation // Continue animation
animationFrameRef.current = requestAnimationFrame(animatePulse) animationFrameRef.current = requestAnimationFrame(animatePing)
} }
// Start animation if visible // Start animation if visible
@ -303,7 +358,7 @@ export default function RecentIncidentsLayer({ visible = false, map, incidents =
if (animationFrameRef.current) { if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current) cancelAnimationFrame(animationFrameRef.current)
} }
animationFrameRef.current = requestAnimationFrame(animatePulse) animationFrameRef.current = requestAnimationFrame(animatePing)
} }
// Ensure click handler is properly registered // Ensure click handler is properly registered
@ -338,7 +393,7 @@ export default function RecentIncidentsLayer({ visible = false, map, incidents =
cancelAnimationFrame(animationFrameRef.current) cancelAnimationFrame(animationFrameRef.current)
} }
} }
}, [map, visible, recentIncidents, handleIncidentClick]) }, [map, visible, recentIncidents, handleIncidentClick, twoHoursInMs])
// Close popup when layer becomes invisible // Close popup when layer becomes invisible
useEffect(() => { useEffect(() => {

View File

@ -1184,17 +1184,12 @@ label#internal {
========================= */ ========================= */
.animate-ping { .animate-ping {
animation: ping 1.5s cubic-bezier(0, 0, 0.2, 1) infinite; animation: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite;
} }
@keyframes ping { @keyframes ping {
0% { 75%, 100% {
transform: translate(-50%, 100%) scale(0.5); transform: scale(2);
opacity: 1;
}
75%,
100% {
transform: translate(-50%, 100%) scale(2);
opacity: 0; opacity: 0;
} }
} }