fix: adjust ping animation duration and simplify keyframes for better performance
This commit is contained in:
parent
b9f69ade3b
commit
da93032a24
|
@ -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(() => {
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue