Refactor map layers and pop-ups for improved incident handling and UI updates
- Removed unused CrimePopup component and replaced with IncidentPopup in layers.tsx - Consolidated incident click handling logic in UnitsLayer and RecentIncidentsLayer - Updated UnitsLayer to fetch nearest units and display them in the IncidentPopup - Enhanced UI for displaying nearby police units with loading states and better formatting - Cleaned up console logs for production readiness - Adjusted map flyTo parameters for better user experience - Added wave circle animations for incident markers
This commit is contained in:
parent
f4b6d19eb2
commit
58f033d0e4
|
@ -1,6 +1,6 @@
|
|||
import { IUnits } from '@/app/_utils/types/units';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getUnits } from '../action';
|
||||
import { getNearestUnits, getUnits, INearestUnits } from '../action';
|
||||
|
||||
export const useGetUnitsQuery = () => {
|
||||
return useQuery<IUnits[]>({
|
||||
|
@ -8,3 +8,10 @@ export const useGetUnitsQuery = () => {
|
|||
queryFn: () => getUnits(),
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetNearestUnitsQuery = (lat: number, lon: number, max_results?: number) => {
|
||||
return useQuery<INearestUnits[]>({
|
||||
queryKey: ['nearest-units', lat, lon],
|
||||
queryFn: () => getNearestUnits(lat, lon, max_results),
|
||||
});
|
||||
}
|
|
@ -1,11 +1,25 @@
|
|||
'use server';
|
||||
|
||||
import { createClient } from '@/app/_utils/supabase/client';
|
||||
import { IUnits } from '@/app/_utils/types/units';
|
||||
import { getInjection } from '@/di/container';
|
||||
import db from '@/prisma/db';
|
||||
import { AuthenticationError } from '@/src/entities/errors/auth';
|
||||
import { InputParseError } from '@/src/entities/errors/common';
|
||||
|
||||
export interface INearestUnits {
|
||||
code_unit: string;
|
||||
name: string;
|
||||
type: string;
|
||||
address: string;
|
||||
district_id: string;
|
||||
lat_unit: number;
|
||||
lon_unit: number;
|
||||
distance_meters: number;
|
||||
}
|
||||
|
||||
const supabase = createClient();
|
||||
|
||||
export async function getUnits(): Promise<IUnits[]> {
|
||||
const instrumentationService = getInjection('IInstrumentationService');
|
||||
return await instrumentationService.instrumentServerAction(
|
||||
|
@ -63,3 +77,40 @@ export async function getUnits(): Promise<IUnits[]> {
|
|||
}
|
||||
);
|
||||
}
|
||||
|
||||
export async function getNearestUnits(lat: number, lon: number, max_results: number = 5): Promise<INearestUnits[]> {
|
||||
const instrumentationService = getInjection('IInstrumentationService');
|
||||
return await instrumentationService.instrumentServerAction(
|
||||
'District Crime Data',
|
||||
{ recordResponse: true },
|
||||
async () => {
|
||||
try {
|
||||
|
||||
const { data, error } = await supabase.rpc('nearby_units', {
|
||||
lat,
|
||||
lon,
|
||||
max_results
|
||||
}).select();
|
||||
|
||||
if (error) {
|
||||
console.error('Error fetching nearest units:', error);
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
console.error('No data returned from RPC');
|
||||
return [];
|
||||
}
|
||||
|
||||
return data as INearestUnits[];
|
||||
|
||||
} catch (err) {
|
||||
const crashReporterService = getInjection('ICrashReporterService');
|
||||
crashReporterService.report(err);
|
||||
throw new Error(
|
||||
'An error happened. The developers have been notified. Please try again later.'
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
import { useState, useEffect } from 'react';
|
||||
import mapboxgl from 'mapbox-gl';
|
||||
import RecentCrimesLayer from '../layers/recent-crimes-layer';
|
||||
import EWSAlertLayer from '../layers/ews-alert-layer';
|
||||
|
||||
import { IIncidentLog } from '@/app/_utils/types/ews';
|
||||
|
|
|
@ -231,23 +231,23 @@ export default function ClusterLayer({
|
|||
const props = feature.properties
|
||||
const coordinates = (feature.geometry as any).coordinates.slice()
|
||||
|
||||
if (props) {
|
||||
if (props) {
|
||||
const popupHTML = `
|
||||
<div class="p-3">
|
||||
<h3 class="font-bold">${props.district_name}</h3>
|
||||
<div class="mt-2">
|
||||
<p>Total Crimes: <b>${props.crime_count}</b></p>
|
||||
<p>Crime Level: <b>${props.level}</b></p>
|
||||
<p>Year: ${props.year} - Month: ${props.month}</p>
|
||||
${filterCategory !== "all" ? `<p>Category: ${filterCategory}</p>` : ""}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
// if (props) {
|
||||
// if (props) {
|
||||
// const popupHTML = `
|
||||
// <div class="p-3">
|
||||
// <h3 class="font-bold">${props.district_name}</h3>
|
||||
// <div class="mt-2">
|
||||
// <p>Total Crimes: <b>${props.crime_count}</b></p>
|
||||
// <p>Crime Level: <b>${props.level}</b></p>
|
||||
// <p>Year: ${props.year} - Month: ${props.month}</p>
|
||||
// ${filterCategory !== "all" ? `<p>Category: ${filterCategory}</p>` : ""}
|
||||
// </div>
|
||||
// </div>
|
||||
// `
|
||||
|
||||
new mapboxgl.Popup().setLngLat(coordinates).setHTML(popupHTML).addTo(map)
|
||||
}
|
||||
}
|
||||
// new mapboxgl.Popup().setLngLat(coordinates).setHTML(popupHTML).addTo(map)
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -436,21 +436,21 @@ export default function ClusterLayer({
|
|||
const props = feature.properties
|
||||
const coordinates = (feature.geometry as any).coordinates.slice()
|
||||
|
||||
if (props) {
|
||||
const popupHTML = `
|
||||
<div class="p-3">
|
||||
<h3 class="font-bold">${props.district_name}</h3>
|
||||
<div class="mt-2">
|
||||
<p>Total Crimes: <b>${props.crime_count}</b></p>
|
||||
<p>Crime Level: <b>${props.level}</b></p>
|
||||
<p>Year: ${props.year} - Month: ${props.month}</p>
|
||||
${filterCategory !== "all" ? `<p>Category: ${filterCategory}</p>` : ""}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
// if (props) {
|
||||
// const popupHTML = `
|
||||
// <div class="p-3">
|
||||
// <h3 class="font-bold">${props.district_name}</h3>
|
||||
// <div class="mt-2">
|
||||
// <p>Total Crimes: <b>${props.crime_count}</b></p>
|
||||
// <p>Crime Level: <b>${props.level}</b></p>
|
||||
// <p>Year: ${props.year} - Month: ${props.month}</p>
|
||||
// ${filterCategory !== "all" ? `<p>Category: ${filterCategory}</p>` : ""}
|
||||
// </div>
|
||||
// </div>
|
||||
// `
|
||||
|
||||
new mapboxgl.Popup().setLngLat(coordinates).setHTML(popupHTML).addTo(map)
|
||||
}
|
||||
// new mapboxgl.Popup().setLngLat(coordinates).setHTML(popupHTML).addTo(map)
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -532,24 +532,24 @@ export default function ClusterLayer({
|
|||
const props = feature.properties;
|
||||
const coordinates = (feature.geometry as any).coordinates.slice();
|
||||
|
||||
if (props) {
|
||||
const popupHTML = `
|
||||
<div class="p-3">
|
||||
<h3 class="font-bold">${props.district_name}</h3>
|
||||
<div class="mt-2">
|
||||
<p>Total Crimes: <b>${props.crime_count}</b></p>
|
||||
<p>Crime Level: <b>${props.level}</b></p>
|
||||
<p>Year: ${props.year} - Month: ${props.month}</p>
|
||||
${filterCategory !== "all" ? `<p>Category: ${filterCategory}</p>` : ""}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
// if (props) {
|
||||
// const popupHTML = `
|
||||
// <div class="p-3">
|
||||
// <h3 class="font-bold">${props.district_name}</h3>
|
||||
// <div class="mt-2">
|
||||
// <p>Total Crimes: <b>${props.crime_count}</b></p>
|
||||
// <p>Crime Level: <b>${props.level}</b></p>
|
||||
// <p>Year: ${props.year} - Month: ${props.month}</p>
|
||||
// ${filterCategory !== "all" ? `<p>Category: ${filterCategory}</p>` : ""}
|
||||
// </div>
|
||||
// </div>
|
||||
// `;
|
||||
|
||||
new mapboxgl.Popup()
|
||||
.setLngLat(coordinates)
|
||||
.setHTML(popupHTML)
|
||||
.addTo(map);
|
||||
}
|
||||
// // new mapboxgl.Popup()
|
||||
// // .setLngLat(coordinates)
|
||||
// // .setHTML(popupHTML)
|
||||
// // .addTo(map);
|
||||
// }
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ import DigitalClock from '../markers/digital-clock';
|
|||
import { Badge } from '@/app/_components/ui/badge';
|
||||
import { Button } from '@/app/_components/ui/button';
|
||||
import { IconCancel } from '@tabler/icons-react';
|
||||
import { CustomAnimatedPopup } from '@/app/_utils/map/custom-animated-popup';
|
||||
|
||||
|
||||
interface EWSAlertLayerProps {
|
||||
map: mapboxgl.Map | null;
|
||||
|
@ -245,32 +245,32 @@ export default function EWSAlertLayer({
|
|||
);
|
||||
|
||||
// Create and attach the animated popup
|
||||
const popup = new CustomAnimatedPopup({
|
||||
closeOnClick: false,
|
||||
openingAnimation: {
|
||||
duration: 300,
|
||||
easing: 'ease-out',
|
||||
transform: 'scale'
|
||||
},
|
||||
closingAnimation: {
|
||||
duration: 200,
|
||||
easing: 'ease-in-out',
|
||||
transform: 'scale'
|
||||
}
|
||||
}).setDOMContent(popupElement);
|
||||
// const popup = new CustomAnimatedPopup({
|
||||
// closeOnClick: false,
|
||||
// openingAnimation: {
|
||||
// duration: 300,
|
||||
// easing: 'ease-out',
|
||||
// transform: 'scale'
|
||||
// },
|
||||
// closingAnimation: {
|
||||
// duration: 200,
|
||||
// easing: 'ease-in-out',
|
||||
// transform: 'scale'
|
||||
// }
|
||||
// }).setDOMContent(popupElement);
|
||||
|
||||
marker.setPopup(popup);
|
||||
markersRef.current.set(incident.id, marker);
|
||||
// marker.setPopup(popup);
|
||||
// markersRef.current.set(incident.id, marker);
|
||||
|
||||
// Add wave circles around the incident point
|
||||
if (map) {
|
||||
popup.addWaveCircles(map, new mapboxgl.LngLat(longitude, latitude), {
|
||||
color: incident.priority === 'high' ? '#ff0000' :
|
||||
incident.priority === 'medium' ? '#ff9900' : '#0066ff',
|
||||
maxRadius: 300,
|
||||
count: 4
|
||||
});
|
||||
}
|
||||
// // Add wave circles around the incident point
|
||||
// if (map) {
|
||||
// popup.addWaveCircles(map, new mapboxgl.LngLat(longitude, latitude), {
|
||||
// color: incident.priority === 'high' ? '#ff0000' :
|
||||
// incident.priority === 'medium' ? '#ff9900' : '#0066ff',
|
||||
// maxRadius: 300,
|
||||
// count: 4
|
||||
// });
|
||||
// }
|
||||
|
||||
// Fly to the incident if it's new
|
||||
const isNewIncident = activeIncidents.length > 0 &&
|
||||
|
@ -292,9 +292,9 @@ export default function EWSAlertLayer({
|
|||
map.getContainer().dispatchEvent(flyToEvent);
|
||||
|
||||
// Auto-open popup for the newest incident
|
||||
setTimeout(() => {
|
||||
popup.addTo(map);
|
||||
}, 2000);
|
||||
// setTimeout(() => {
|
||||
// popup.addTo(map);
|
||||
// }, 2000);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -67,7 +67,7 @@ export default function HistoricalIncidentsLayer({
|
|||
year: incident.properties.year,
|
||||
};
|
||||
|
||||
console.log("Historical incident clicked:", incidentDetails);
|
||||
// console.log("Historical incident clicked:", incidentDetails);
|
||||
|
||||
// Ensure markers stay visible when clicking on them
|
||||
if (map.getLayer("historical-incidents")) {
|
||||
|
@ -103,7 +103,7 @@ export default function HistoricalIncidentsLayer({
|
|||
useEffect(() => {
|
||||
if (!map || !visible) return;
|
||||
|
||||
console.log("Setting up historical incidents layer");
|
||||
// console.log("Setting up historical incidents layer");
|
||||
|
||||
// Filter incidents from 2020 to current year
|
||||
const historicalData = {
|
||||
|
@ -148,7 +148,7 @@ export default function HistoricalIncidentsLayer({
|
|||
),
|
||||
};
|
||||
|
||||
console.log(`Found ${historicalData.features.length} historical incidents from 2020 to ${currentYear}`);
|
||||
// console.log(`Found ${historicalData.features.length} historical incidents from 2020 to ${currentYear}`);
|
||||
|
||||
const setupLayerAndSource = () => {
|
||||
try {
|
||||
|
|
|
@ -19,7 +19,7 @@ import type { ITooltipsControl } from "../controls/top/tooltips"
|
|||
import type { IUnits } from "@/app/_utils/types/units"
|
||||
import UnitsLayer from "./units-layer"
|
||||
import DistrictFillLineLayer from "./district-layer"
|
||||
import CrimePopup from "../pop-up/crime-popup"
|
||||
|
||||
import TimezoneLayer from "./timezone"
|
||||
import FaultLinesLayer from "./fault-lines"
|
||||
import EWSAlertLayer from "./ews-alert-layer"
|
||||
|
@ -28,6 +28,7 @@ import PanicButtonDemo from "../controls/panic-button-demo"
|
|||
import type { IIncidentLog } from "@/app/_utils/types/ews"
|
||||
import { addMockIncident, getAllIncidents, resolveIncident } from "@/app/_utils/mock/ews-data"
|
||||
import RecentIncidentsLayer from "./recent-incidents-layer"
|
||||
import IncidentPopup from "../pop-up/incident-popup"
|
||||
|
||||
// Interface for crime incident
|
||||
interface ICrimeIncident {
|
||||
|
@ -247,24 +248,19 @@ export default function Layers({
|
|||
}
|
||||
|
||||
const handleCloseDistrictPopup = useCallback(() => {
|
||||
console.log("Closing district popup")
|
||||
// console.log("Closing district popup")
|
||||
|
||||
animateExtrusionDown()
|
||||
handlePopupClose()
|
||||
}, [handlePopupClose, animateExtrusionDown])
|
||||
|
||||
const handleCloseIncidentPopup = useCallback(() => {
|
||||
console.log("Closing incident popup")
|
||||
handlePopupClose()
|
||||
}, [handlePopupClose])
|
||||
|
||||
const handleDistrictClick = useCallback(
|
||||
(feature: IDistrictFeature) => {
|
||||
console.log("District clicked:", feature)
|
||||
// console.log("District clicked:", feature)
|
||||
|
||||
// If we're currently interacting with a marker, don't process district click
|
||||
if (isInteractingWithMarker.current) {
|
||||
console.log("Ignoring district click because we're interacting with a marker")
|
||||
// console.log("Ignoring district click because we're interacting with a marker")
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -325,139 +321,6 @@ export default function Layers({
|
|||
}
|
||||
}, [mapboxMap, map])
|
||||
|
||||
useEffect(() => {
|
||||
if (!mapboxMap) return
|
||||
|
||||
const handleIncidentClickEvent = (e: Event) => {
|
||||
const customEvent = e as CustomEvent
|
||||
console.log("Received incident_click event in layers:", customEvent.detail)
|
||||
|
||||
if (!customEvent.detail) {
|
||||
console.error("Empty incident click event data")
|
||||
return
|
||||
}
|
||||
|
||||
// Set the marker interaction flag to prevent district selection
|
||||
isInteractingWithMarker.current = true
|
||||
|
||||
const incidentId = customEvent.detail.id || customEvent.detail.incidentId || customEvent.detail.incident_id
|
||||
|
||||
if (!incidentId) {
|
||||
console.error("No incident ID found in event data:", customEvent.detail)
|
||||
return
|
||||
}
|
||||
|
||||
console.log("Looking for incident with ID:", incidentId)
|
||||
|
||||
let foundIncident: ICrimeIncident | undefined
|
||||
|
||||
if (
|
||||
customEvent.detail.latitude !== undefined &&
|
||||
customEvent.detail.longitude !== undefined &&
|
||||
customEvent.detail.category !== undefined
|
||||
) {
|
||||
foundIncident = {
|
||||
id: incidentId,
|
||||
district: customEvent.detail.district,
|
||||
category: customEvent.detail.category,
|
||||
type_category: customEvent.detail.type,
|
||||
description: customEvent.detail.description,
|
||||
status: customEvent.detail.status || "Unknown",
|
||||
timestamp: customEvent.detail.timestamp ? new Date(customEvent.detail.timestamp) : undefined,
|
||||
latitude: customEvent.detail.latitude,
|
||||
longitude: customEvent.detail.longitude,
|
||||
address: customEvent.detail.address,
|
||||
}
|
||||
} else {
|
||||
for (const crime of crimes) {
|
||||
for (const incident of crime.crime_incidents) {
|
||||
if (incident.id === incidentId || incident.id?.toString() === incidentId?.toString()) {
|
||||
console.log("Found matching incident:", incident)
|
||||
foundIncident = {
|
||||
id: incident.id,
|
||||
district: crime.districts.name,
|
||||
description: incident.description,
|
||||
status: incident.status || "unknown",
|
||||
timestamp: incident.timestamp,
|
||||
category: incident.crime_categories.name,
|
||||
type_category: incident.crime_categories.type,
|
||||
address: incident.locations.address,
|
||||
latitude: incident.locations.latitude,
|
||||
longitude: incident.locations.longitude,
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if (foundIncident) break
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundIncident) {
|
||||
console.error("Could not find incident with ID:", incidentId)
|
||||
isInteractingWithMarker.current = false
|
||||
return
|
||||
}
|
||||
|
||||
if (!foundIncident.latitude || !foundIncident.longitude) {
|
||||
console.error("Found incident has invalid coordinates:", foundIncident)
|
||||
isInteractingWithMarker.current = false
|
||||
return
|
||||
}
|
||||
|
||||
console.log("Setting selected incident:", foundIncident)
|
||||
|
||||
// Clear district selection when showing an incident
|
||||
setSelectedDistrict(null)
|
||||
selectedDistrictRef.current = null
|
||||
setFocusedDistrictId(null)
|
||||
|
||||
setSelectedIncident(foundIncident)
|
||||
|
||||
// Reset the marker interaction flag after a delay
|
||||
setTimeout(() => {
|
||||
isInteractingWithMarker.current = false
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
mapboxMap.getCanvas().addEventListener("incident_click", handleIncidentClickEvent as EventListener)
|
||||
document.addEventListener("incident_click", handleIncidentClickEvent as EventListener)
|
||||
|
||||
console.log("Set up incident click event listener")
|
||||
|
||||
return () => {
|
||||
console.log("Removing incident click event listener")
|
||||
if (mapboxMap && mapboxMap.getCanvas()) {
|
||||
mapboxMap.getCanvas().removeEventListener("incident_click", handleIncidentClickEvent as EventListener)
|
||||
}
|
||||
document.removeEventListener("incident_click", handleIncidentClickEvent as EventListener)
|
||||
}
|
||||
}, [mapboxMap, crimes, setFocusedDistrictId])
|
||||
|
||||
// Add a listener for unit clicks to set the marker interaction flag
|
||||
useEffect(() => {
|
||||
if (!mapboxMap) return
|
||||
|
||||
const handleUnitClickEvent = (e: Event) => {
|
||||
// Set the marker interaction flag to prevent district selection
|
||||
isInteractingWithMarker.current = true
|
||||
|
||||
// Reset the flag after a delay
|
||||
setTimeout(() => {
|
||||
isInteractingWithMarker.current = false
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
mapboxMap.getCanvas().addEventListener("unit_click", handleUnitClickEvent as EventListener)
|
||||
document.addEventListener("unit_click", handleUnitClickEvent as EventListener)
|
||||
|
||||
return () => {
|
||||
if (mapboxMap && mapboxMap.getCanvas()) {
|
||||
mapboxMap.getCanvas().removeEventListener("unit_click", handleUnitClickEvent as EventListener)
|
||||
}
|
||||
document.removeEventListener("unit_click", handleUnitClickEvent as EventListener)
|
||||
}
|
||||
}, [mapboxMap])
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedDistrictRef.current) {
|
||||
const districtId = selectedDistrictRef.current.id
|
||||
|
@ -540,7 +403,7 @@ export default function Layers({
|
|||
}, [crimes, filterCategory, year, month, crimeDataByDistrict])
|
||||
|
||||
const handleSetFocusedDistrictId = useCallback((id: string | null, isMarkerClick = false) => {
|
||||
console.log("Setting focused district ID:", id, "from marker click:", isMarkerClick)
|
||||
// console.log("Setting focused district ID:", id, "from marker click:", isMarkerClick)
|
||||
|
||||
// If this is from a marker click, set the marker interaction flag
|
||||
if (isMarkerClick) {
|
||||
|
@ -683,14 +546,7 @@ export default function Layers({
|
|||
</div>
|
||||
)}
|
||||
|
||||
{selectedIncident && selectedIncident.latitude && selectedIncident.longitude && (
|
||||
<CrimePopup
|
||||
longitude={selectedIncident.longitude}
|
||||
latitude={selectedIncident.latitude}
|
||||
onClose={handleCloseIncidentPopup}
|
||||
incident={selectedIncident}
|
||||
/>
|
||||
)}
|
||||
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -51,7 +51,7 @@ export default function RecentIncidentsLayer({ visible = false, map, incidents =
|
|||
category: incident.properties.category,
|
||||
}
|
||||
|
||||
console.log("Recent incident clicked:", incidentDetails)
|
||||
// console.log("Recent incident clicked:", incidentDetails)
|
||||
|
||||
// Ensure markers stay visible
|
||||
if (map.getLayer("recent-incidents")) {
|
||||
|
@ -87,7 +87,7 @@ export default function RecentIncidentsLayer({ visible = false, map, incidents =
|
|||
useEffect(() => {
|
||||
if (!map || !visible) return
|
||||
|
||||
console.log(`Setting up recent incidents layer with ${recentIncidents.length} incidents from the last 24 hours`)
|
||||
// console.log(`Setting up recent incidents layer with ${recentIncidents.length} incidents from the last 24 hours`)
|
||||
|
||||
// Convert incidents to GeoJSON
|
||||
const recentData = {
|
||||
|
|
|
@ -41,7 +41,7 @@ export default function UnclusteredPointLayer({
|
|||
timestamp: new Date(incident.properties.timestamp || Date.now()),
|
||||
}
|
||||
|
||||
console.log("Incident clicked:", incidentDetails)
|
||||
// console.log("Incident clicked:", incidentDetails)
|
||||
|
||||
// Ensure markers stay visible when clicking on them
|
||||
if (map.getLayer("unclustered-point")) {
|
||||
|
|
|
@ -8,6 +8,10 @@ import type mapboxgl from "mapbox-gl"
|
|||
|
||||
import { generateCategoryColorMap } from "@/app/_utils/colors"
|
||||
import UnitPopup from "../pop-up/unit-popup"
|
||||
|
||||
import { BASE_BEARING, BASE_DURATION, BASE_PITCH, BASE_ZOOM } from "@/app/_utils/const/map"
|
||||
import { INearestUnits } from "@/app/(pages)/(admin)/dashboard/crime-management/units/action"
|
||||
import { useGetNearestUnitsQuery } from "@/app/(pages)/(admin)/dashboard/crime-management/units/_queries/queries"
|
||||
import IncidentPopup from "../pop-up/incident-popup"
|
||||
|
||||
interface UnitsLayerProps {
|
||||
|
@ -39,6 +43,13 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible
|
|||
const [unitIncident, setUnitIncident] = useState<IDistrictIncidents[]>([])
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false)
|
||||
|
||||
const [incidentCoords, setIncidentCoords] = useState<{ lat: number, lon: number } | null>(null)
|
||||
const { data: nearestUnits, isLoading: isLoadingNearestUnits } = useGetNearestUnitsQuery(
|
||||
incidentCoords?.lat ?? 0,
|
||||
incidentCoords?.lon ?? 0,
|
||||
5
|
||||
)
|
||||
|
||||
// Use either provided units or loaded units
|
||||
const unitsData = useMemo(() => {
|
||||
return units.length > 0 ? units : loadedUnits || []
|
||||
|
@ -64,18 +75,9 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible
|
|||
|
||||
// Process units data to GeoJSON format
|
||||
const unitsGeoJSON = useMemo(() => {
|
||||
console.log("Units data being processed:", unitsData) // Debug log
|
||||
|
||||
return {
|
||||
type: "FeatureCollection" as const,
|
||||
features: unitsData.map((unit) => {
|
||||
// Debug log for individual units
|
||||
console.log("Processing unit:", unit.code_unit, unit.name, {
|
||||
longitude: unit.longitude,
|
||||
latitude: unit.latitude,
|
||||
district: unit.district_name,
|
||||
})
|
||||
|
||||
return {
|
||||
type: "Feature" as const,
|
||||
properties: {
|
||||
|
@ -235,6 +237,7 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible
|
|||
|
||||
// Find the unit in our data
|
||||
const unit = unitsData.find((u) => u.code_unit === properties.id)
|
||||
|
||||
if (!unit) {
|
||||
console.log("Unit not found in data:", properties.id)
|
||||
return
|
||||
|
@ -245,9 +248,11 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible
|
|||
// Find all incidents in the same district as the unit
|
||||
const districtIncidents: IDistrictIncidents[] = []
|
||||
crimes.forEach((crime) => {
|
||||
// Check if this crime is in the same district as the unit
|
||||
|
||||
if (selectedUnit?.code_unit === unit.code_unit) {
|
||||
console.log("Processing crime:", crime.district_id, unit.district_id) // Debug log
|
||||
|
||||
// Check if this crime is in the same district as the unit
|
||||
if (crime.district_id === unit.district_id) {
|
||||
crime.crime_incidents.forEach((incident) => {
|
||||
if (incident.locations && typeof incident.locations.distance_to_unit !== "undefined") {
|
||||
districtIncidents.push({
|
||||
|
@ -265,7 +270,7 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible
|
|||
// Sort by distance (closest first)
|
||||
districtIncidents.sort((a, b) => a.distance_meters - b.distance_meters)
|
||||
|
||||
console.log("Sorted district incidents:", districtIncidents)
|
||||
// console.log("Sorted district incidents:", districtIncidents)
|
||||
|
||||
// Update the state with the distance results
|
||||
setUnitIncident(districtIncidents)
|
||||
|
@ -274,11 +279,11 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible
|
|||
// Fly to the unit location
|
||||
map.flyTo({
|
||||
center: [unit.longitude || 0, unit.latitude || 0],
|
||||
zoom: 14,
|
||||
zoom: 12.5,
|
||||
pitch: 45,
|
||||
bearing: 0,
|
||||
duration: 2000,
|
||||
})
|
||||
bearing: BASE_BEARING,
|
||||
duration: BASE_DURATION,
|
||||
})
|
||||
|
||||
// Set the selected unit and query parameters
|
||||
setSelectedUnit(unit)
|
||||
|
@ -339,10 +344,10 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible
|
|||
// Fly to the incident location
|
||||
map.flyTo({
|
||||
center: [longitude, latitude],
|
||||
zoom: 15,
|
||||
zoom: 16,
|
||||
pitch: 45,
|
||||
bearing: 0,
|
||||
duration: 2000,
|
||||
bearing: BASE_BEARING,
|
||||
duration: BASE_DURATION,
|
||||
})
|
||||
|
||||
// Create incident object from properties
|
||||
|
@ -358,19 +363,23 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible
|
|||
latitude,
|
||||
}
|
||||
|
||||
// Debug log
|
||||
console.log("Incident clicked:", incident)
|
||||
|
||||
// Set the selected incident and query parameters
|
||||
setSelectedIncident(incident)
|
||||
setSelectedUnit(null) // Clear any selected unit
|
||||
setSelectedEntityId(properties.id)
|
||||
setIsUnitSelected(false)
|
||||
setSelectedDistrictId(properties.district_id)
|
||||
setIncidentCoords({ lat: latitude, lon: longitude })
|
||||
|
||||
// Highlight the connected lines for this incident
|
||||
if (map.getLayer("units-connection-lines")) {
|
||||
map.setFilter("units-connection-lines", ["==", ["get", "incident_id"], properties.id])
|
||||
}
|
||||
|
||||
// Dispatch a custom event for other components to react to
|
||||
// Dispatch a custom event for other components to react to
|
||||
const customEvent = new CustomEvent("incident_click", {
|
||||
detail: {
|
||||
id: properties.id,
|
||||
|
@ -428,10 +437,11 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible
|
|||
useEffect(() => {
|
||||
if (!map || !visible) return
|
||||
|
||||
// Debug log to confirm map layers
|
||||
console.log(
|
||||
"Available map layers:",
|
||||
map.getStyle().layers?.map((l) => l.id),
|
||||
// Debug log untuk memeriksa keberadaan layer
|
||||
console.log("Setting up event handlers, map layers:",
|
||||
map.getStyle().layers?.filter(l =>
|
||||
l.id === "units-points" || l.id === "incidents-points"
|
||||
).map(l => l.id)
|
||||
)
|
||||
|
||||
// Define event handlers that can be referenced for both adding and removing
|
||||
|
@ -443,27 +453,51 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible
|
|||
map.getCanvas().style.cursor = ""
|
||||
}
|
||||
|
||||
// Add click event for units-points layer
|
||||
if (map.getLayer("units-points")) {
|
||||
map.off("click", "units-points", unitClickHandler)
|
||||
map.on("click", "units-points", unitClickHandler)
|
||||
// Fungsi untuk setup event handler
|
||||
const setupHandlers = () => {
|
||||
// Add click event for units-points layer
|
||||
if (map.getLayer("units-points")) {
|
||||
map.off("click", "units-points", unitClickHandler)
|
||||
map.on("click", "units-points", unitClickHandler)
|
||||
map.on("mouseenter", "units-points", handleMouseEnter)
|
||||
map.on("mouseleave", "units-points", handleMouseLeave)
|
||||
console.log("✅ Unit points handler attached")
|
||||
} else {
|
||||
console.log("❌ units-points layer not found")
|
||||
}
|
||||
|
||||
// Change cursor on hover
|
||||
map.on("mouseenter", "units-points", handleMouseEnter)
|
||||
map.on("mouseleave", "units-points", handleMouseLeave)
|
||||
// Add click event for incidents-points layer
|
||||
if (map.getLayer("incidents-points")) {
|
||||
map.off("click", "incidents-points", incidentClickHandler)
|
||||
map.on("click", "incidents-points", incidentClickHandler)
|
||||
map.on("mouseenter", "incidents-points", handleMouseEnter)
|
||||
map.on("mouseleave", "incidents-points", handleMouseLeave)
|
||||
console.log("✅ Incident points handler attached")
|
||||
} else {
|
||||
console.log("❌ incidents-points layer not found")
|
||||
}
|
||||
}
|
||||
|
||||
// Add click event for incidents-points layer
|
||||
if (map.getLayer("incidents-points")) {
|
||||
map.off("click", "incidents-points", incidentClickHandler)
|
||||
map.on("click", "incidents-points", incidentClickHandler)
|
||||
// Setup handlers langsung
|
||||
setupHandlers()
|
||||
|
||||
// Change cursor on hover
|
||||
map.on("mouseenter", "incidents-points", handleMouseEnter)
|
||||
map.on("mouseleave", "incidents-points", handleMouseLeave)
|
||||
}
|
||||
// Safety check: pastikan handler terpasang setelah layer mungkin dimuat
|
||||
const checkLayersTimeout = setTimeout(() => {
|
||||
setupHandlers()
|
||||
}, 1000)
|
||||
|
||||
// Listen for style.load event to reattach handlers setelah perubahan style
|
||||
map.on('style.load', setupHandlers)
|
||||
map.on('sourcedata', (e) => {
|
||||
if (e.sourceId === 'incidents-source' && e.isSourceLoaded && map.getLayer('incidents-points')) {
|
||||
setupHandlers()
|
||||
}
|
||||
})
|
||||
|
||||
return () => {
|
||||
clearTimeout(checkLayersTimeout)
|
||||
map.off('style.load', setupHandlers)
|
||||
|
||||
if (map) {
|
||||
if (map.getLayer("units-points")) {
|
||||
map.off("click", "units-points", unitClickHandler)
|
||||
|
@ -482,6 +516,7 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible
|
|||
|
||||
// Reset map filters when popup is closed
|
||||
const handleClosePopup = useCallback(() => {
|
||||
console.log("Closing popup, clearing selected states")
|
||||
setSelectedUnit(null)
|
||||
setSelectedIncident(null)
|
||||
setSelectedEntityId(undefined)
|
||||
|
@ -490,6 +525,14 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible
|
|||
setIsLoading(false)
|
||||
|
||||
if (map && map.getLayer("units-connection-lines")) {
|
||||
map.easeTo({
|
||||
zoom: BASE_ZOOM,
|
||||
pitch: BASE_PITCH,
|
||||
bearing: BASE_BEARING,
|
||||
duration: BASE_DURATION,
|
||||
easing: (t) => t * (2 - t),
|
||||
})
|
||||
|
||||
map.setFilter("units-connection-lines", ["has", "unit_id"])
|
||||
}
|
||||
}, [map])
|
||||
|
@ -501,6 +544,15 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible
|
|||
}
|
||||
}, [visible, handleClosePopup])
|
||||
|
||||
// Debug untuk komponen render
|
||||
useEffect(() => {
|
||||
console.log("Render state:", {
|
||||
selectedUnit: selectedUnit?.code_unit,
|
||||
selectedIncident: selectedIncident?.id,
|
||||
visible
|
||||
})
|
||||
}, [selectedUnit, selectedIncident, visible])
|
||||
|
||||
if (!visible) return null
|
||||
|
||||
return (
|
||||
|
@ -599,8 +651,10 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible
|
|||
latitude={selectedIncident.latitude}
|
||||
onClose={handleClosePopup}
|
||||
incident={selectedIncident}
|
||||
nearestUnit={nearestUnits}
|
||||
isLoadingNearestUnit={isLoadingNearestUnits}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ interface IncidentPopupProps {
|
|||
}
|
||||
}
|
||||
|
||||
export default function IncidentPopup({ longitude, latitude, onClose, incident }: IncidentPopupProps) {
|
||||
export default function CrimePopup({ longitude, latitude, onClose, incident }: IncidentPopupProps) {
|
||||
const formatDate = (date?: Date) => {
|
||||
if (!date) return "Unknown date"
|
||||
return new Date(date).toLocaleDateString()
|
||||
|
@ -178,28 +178,6 @@ export default function IncidentPopup({ longitude, latitude, onClose, incident }
|
|||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
{/* Connection line */}
|
||||
<div
|
||||
className="absolute bottom-0 left-1/2 transform -translate-x-1/2 translate-y-full"
|
||||
style={{
|
||||
width: '2px',
|
||||
height: '20px',
|
||||
backgroundColor: 'red',
|
||||
boxShadow: '0 0 4px rgba(0, 0, 0, 0.3)'
|
||||
}}
|
||||
/>
|
||||
{/* Connection dot */}
|
||||
<div
|
||||
className="absolute bottom-0 left-1/2 transform -translate-x-1/2 translate-y-full"
|
||||
style={{
|
||||
width: '6px',
|
||||
height: '6px',
|
||||
backgroundColor: 'red',
|
||||
borderRadius: '50%',
|
||||
marginTop: '20px',
|
||||
boxShadow: '0 0 4px rgba(0, 0, 0, 0.3)'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Popup>
|
||||
)
|
||||
|
|
|
@ -5,10 +5,11 @@ import { Badge } from "@/app/_components/ui/badge"
|
|||
import { Card } from "@/app/_components/ui/card"
|
||||
import { Separator } from "@/app/_components/ui/separator"
|
||||
import { Button } from "@/app/_components/ui/button"
|
||||
import { MapPin, AlertTriangle, Calendar, Clock, Bookmark, Navigation, X, FileText } from "lucide-react"
|
||||
import { MapPin, AlertTriangle, Calendar, Clock, Bookmark, Navigation, X, FileText, Shield } from "lucide-react"
|
||||
import { IDistanceResult } from "@/app/_utils/types/crimes"
|
||||
import { ScrollArea } from "@/app/_components/ui/scroll-area"
|
||||
import { Skeleton } from "@/app/_components/ui/skeleton"
|
||||
import { INearestUnits } from "@/app/(pages)/(admin)/dashboard/crime-management/units/action"
|
||||
|
||||
interface IncidentPopupProps {
|
||||
longitude: number
|
||||
|
@ -22,8 +23,8 @@ interface IncidentPopupProps {
|
|||
district?: string
|
||||
district_id?: string
|
||||
}
|
||||
distances?: IDistanceResult[]
|
||||
isLoadingDistances?: boolean
|
||||
nearestUnit?: INearestUnits[]
|
||||
isLoadingNearestUnit?: boolean
|
||||
}
|
||||
|
||||
export default function IncidentPopup({
|
||||
|
@ -31,8 +32,8 @@ export default function IncidentPopup({
|
|||
latitude,
|
||||
onClose,
|
||||
incident,
|
||||
distances = [],
|
||||
isLoadingDistances = false
|
||||
nearestUnit = [],
|
||||
isLoadingNearestUnit = false
|
||||
}: IncidentPopupProps) {
|
||||
|
||||
const formatDate = (date?: Date | string) => {
|
||||
|
@ -130,31 +131,39 @@ export default function IncidentPopup({
|
|||
)}
|
||||
</div>
|
||||
|
||||
{/* Distances to police units section */}
|
||||
{/* NearestUnit to police nearestUnits section */}
|
||||
<Separator className="my-3" />
|
||||
|
||||
<div>
|
||||
<h4 className="text-sm font-medium mb-2">Nearby Police Units</h4>
|
||||
<h4 className="text-sm font-medium mb-2 flex items-center">
|
||||
<Shield className="h-4 w-4 mr-1.5 text-blue-600" />
|
||||
Nearby Units
|
||||
</h4>
|
||||
|
||||
{isLoadingDistances ? (
|
||||
{isLoadingNearestUnit ? (
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-6 w-full" />
|
||||
<Skeleton className="h-6 w-full" />
|
||||
<Skeleton className="h-6 w-full" />
|
||||
</div>
|
||||
) : distances.length > 0 ? (
|
||||
<ScrollArea className="h-[120px] rounded-md border p-2">
|
||||
) : nearestUnit.length > 0 ? (
|
||||
<ScrollArea className="h-[100px] rounded-md border p-2">
|
||||
<div className="space-y-2">
|
||||
{distances.map((item) => (
|
||||
<div key={item.unit_code} className="flex justify-between items-center text-xs border-b pb-1">
|
||||
{nearestUnit.map((unit) => (
|
||||
<div key={unit.code_unit} className="flex justify-between items-center text-xs border-b pb-1">
|
||||
<div>
|
||||
<p className="font-medium">{item.unit_name || "Unknown Unit"}</p>
|
||||
<p className="text-muted-foreground text-[10px]">
|
||||
{item.unit_type || "Police Unit"}
|
||||
<p className="font-medium">
|
||||
{unit.name || "Unknown"}
|
||||
<span className="ml-2 text-[10px] text-slate-500 font-normal">
|
||||
({unit.type || "Unknown type"})
|
||||
</span>
|
||||
</p>
|
||||
<p className="text-muted-foreground text-[10px] truncate" style={{ maxWidth: "160px" }}>
|
||||
{unit.address || "No address"}
|
||||
</p>
|
||||
</div>
|
||||
<Badge variant="outline" className="ml-2">
|
||||
{formatDistance(item.distance_meters)}
|
||||
<Badge variant="outline" className="ml-2 whitespace-nowrap text-blue-600">
|
||||
{formatDistance(unit.distance_meters)}
|
||||
</Badge>
|
||||
</div>
|
||||
))}
|
||||
|
@ -162,7 +171,7 @@ export default function IncidentPopup({
|
|||
</ScrollArea>
|
||||
) : (
|
||||
<p className="text-xs text-muted-foreground text-center p-2">
|
||||
No police units data available
|
||||
No nearby units found
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
@ -176,28 +185,6 @@ export default function IncidentPopup({
|
|||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
{/* Connection line */}
|
||||
<div
|
||||
className="absolute bottom-0 left-1/2 transform -translate-x-1/2 translate-y-full"
|
||||
style={{
|
||||
width: '2px',
|
||||
height: '20px',
|
||||
backgroundColor: 'red',
|
||||
boxShadow: '0 0 4px rgba(0, 0, 0, 0.3)'
|
||||
}}
|
||||
/>
|
||||
{/* Connection dot */}
|
||||
<div
|
||||
className="absolute bottom-0 left-1/2 transform -translate-x-1/2 translate-y-full"
|
||||
style={{
|
||||
width: '6px',
|
||||
height: '6px',
|
||||
backgroundColor: 'red',
|
||||
borderRadius: '50%',
|
||||
marginTop: '20px',
|
||||
boxShadow: '0 0 4px rgba(0, 0, 0, 0.3)'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Popup>
|
||||
)
|
||||
|
|
|
@ -1,323 +1,323 @@
|
|||
import mapboxgl from 'mapbox-gl';
|
||||
// import mapboxgl from 'mapbox-gl';
|
||||
|
||||
interface AnimationOptions {
|
||||
duration: number;
|
||||
easing: string;
|
||||
transform: string;
|
||||
}
|
||||
// interface AnimationOptions {
|
||||
// duration: number;
|
||||
// easing: string;
|
||||
// transform: string;
|
||||
// }
|
||||
|
||||
interface CustomPopupOptions extends mapboxgl.PopupOptions {
|
||||
openingAnimation?: AnimationOptions;
|
||||
closingAnimation?: AnimationOptions;
|
||||
}
|
||||
// interface CustomPopupOptions extends mapboxgl.PopupOptions {
|
||||
// openingAnimation?: AnimationOptions;
|
||||
// closingAnimation?: AnimationOptions;
|
||||
// }
|
||||
|
||||
// Extend the native Mapbox Popup
|
||||
export class CustomAnimatedPopup extends mapboxgl.Popup {
|
||||
private openingAnimation: AnimationOptions;
|
||||
private closingAnimation: AnimationOptions;
|
||||
private animating = false;
|
||||
// // Extend the native Mapbox Popup
|
||||
// export class CustomAnimatedPopup extends mapboxgl.Popup {
|
||||
// private openingAnimation: AnimationOptions;
|
||||
// private closingAnimation: AnimationOptions;
|
||||
// private animating = false;
|
||||
|
||||
constructor(options: CustomPopupOptions = {}) {
|
||||
// Extract animation options and pass the rest to the parent class
|
||||
const {
|
||||
openingAnimation,
|
||||
closingAnimation,
|
||||
className,
|
||||
...mapboxOptions
|
||||
} = options;
|
||||
// constructor(options: CustomPopupOptions = {}) {
|
||||
// // Extract animation options and pass the rest to the parent class
|
||||
// const {
|
||||
// openingAnimation,
|
||||
// closingAnimation,
|
||||
// className,
|
||||
// ...mapboxOptions
|
||||
// } = options;
|
||||
|
||||
// Add our custom class to the className
|
||||
const customClassName = `custom-animated-popup ${className || ''}`.trim();
|
||||
// // Add our custom class to the className
|
||||
// const customClassName = `custom-animated-popup ${className || ''}`.trim();
|
||||
|
||||
// Call the parent constructor
|
||||
super({
|
||||
...mapboxOptions,
|
||||
className: customClassName,
|
||||
});
|
||||
// // Call the parent constructor
|
||||
// super({
|
||||
// ...mapboxOptions,
|
||||
// className: customClassName,
|
||||
// });
|
||||
|
||||
// Store animation options
|
||||
this.openingAnimation = openingAnimation || {
|
||||
duration: 300,
|
||||
easing: 'ease-out',
|
||||
transform: 'scale'
|
||||
};
|
||||
// // Store animation options
|
||||
// this.openingAnimation = openingAnimation || {
|
||||
// duration: 300,
|
||||
// easing: 'ease-out',
|
||||
// transform: 'scale'
|
||||
// };
|
||||
|
||||
this.closingAnimation = closingAnimation || {
|
||||
duration: 200,
|
||||
easing: 'ease-in-out',
|
||||
transform: 'scale'
|
||||
};
|
||||
// this.closingAnimation = closingAnimation || {
|
||||
// duration: 200,
|
||||
// easing: 'ease-in-out',
|
||||
// transform: 'scale'
|
||||
// };
|
||||
|
||||
// Override the parent's add method
|
||||
const parentAdd = this.addTo;
|
||||
this.addTo = (map: mapboxgl.Map) => {
|
||||
// Call the parent method first
|
||||
parentAdd.call(this, map);
|
||||
// // Override the parent's add method
|
||||
// const parentAdd = this.addTo;
|
||||
// this.addTo = (map: mapboxgl.Map) => {
|
||||
// // Call the parent method first
|
||||
// parentAdd.call(this, map);
|
||||
|
||||
// Apply animation after a short delay to ensure the element is in the DOM
|
||||
setTimeout(() => this.animateOpen(), 10);
|
||||
// // Apply animation after a short delay to ensure the element is in the DOM
|
||||
// setTimeout(() => this.animateOpen(), 10);
|
||||
|
||||
return this;
|
||||
};
|
||||
}
|
||||
// return this;
|
||||
// };
|
||||
// }
|
||||
|
||||
// Override the remove method to add animation
|
||||
remove(): this {
|
||||
if (this.animating) {
|
||||
return this;
|
||||
}
|
||||
// // Override the remove method to add animation
|
||||
// remove(): this {
|
||||
// if (this.animating) {
|
||||
// return this;
|
||||
// }
|
||||
|
||||
this.animateClose(() => {
|
||||
super.remove();
|
||||
});
|
||||
// this.animateClose(() => {
|
||||
// super.remove();
|
||||
// });
|
||||
|
||||
return this;
|
||||
}
|
||||
// return this;
|
||||
// }
|
||||
|
||||
// Animation methods
|
||||
private animateOpen(): void {
|
||||
const container = this._container;
|
||||
if (!container) return;
|
||||
// // Animation methods
|
||||
// private animateOpen(): void {
|
||||
// const container = this._container;
|
||||
// if (!container) return;
|
||||
|
||||
// Apply initial state
|
||||
container.style.opacity = '0';
|
||||
container.style.transform = 'scale(0.8)';
|
||||
container.style.transition = `
|
||||
opacity ${this.openingAnimation.duration}ms ${this.openingAnimation.easing},
|
||||
transform ${this.openingAnimation.duration}ms ${this.openingAnimation.easing}
|
||||
`;
|
||||
// // Apply initial state
|
||||
// container.style.opacity = '0';
|
||||
// container.style.transform = 'scale(0.8)';
|
||||
// container.style.transition = `
|
||||
// opacity ${this.openingAnimation.duration}ms ${this.openingAnimation.easing},
|
||||
// transform ${this.openingAnimation.duration}ms ${this.openingAnimation.easing}
|
||||
// `;
|
||||
|
||||
// Force reflow
|
||||
void container.offsetHeight;
|
||||
// // Force reflow
|
||||
// void container.offsetHeight;
|
||||
|
||||
// Apply final state to trigger animation
|
||||
container.style.opacity = '1';
|
||||
container.style.transform = 'scale(1)';
|
||||
}
|
||||
// // Apply final state to trigger animation
|
||||
// container.style.opacity = '1';
|
||||
// container.style.transform = 'scale(1)';
|
||||
// }
|
||||
|
||||
private animateClose(callback: () => void): void {
|
||||
const container = this._container;
|
||||
if (!container) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
// private animateClose(callback: () => void): void {
|
||||
// const container = this._container;
|
||||
// if (!container) {
|
||||
// callback();
|
||||
// return;
|
||||
// }
|
||||
|
||||
this.animating = true;
|
||||
// this.animating = true;
|
||||
|
||||
// Setup transition
|
||||
container.style.transition = `
|
||||
opacity ${this.closingAnimation.duration}ms ${this.closingAnimation.easing},
|
||||
transform ${this.closingAnimation.duration}ms ${this.closingAnimation.easing}
|
||||
`;
|
||||
// // Setup transition
|
||||
// container.style.transition = `
|
||||
// opacity ${this.closingAnimation.duration}ms ${this.closingAnimation.easing},
|
||||
// transform ${this.closingAnimation.duration}ms ${this.closingAnimation.easing}
|
||||
// `;
|
||||
|
||||
// Apply closing animation
|
||||
container.style.opacity = '0';
|
||||
container.style.transform = 'scale(0.8)';
|
||||
// // Apply closing animation
|
||||
// container.style.opacity = '0';
|
||||
// container.style.transform = 'scale(0.8)';
|
||||
|
||||
// Execute callback after animation completes
|
||||
setTimeout(() => {
|
||||
this.animating = false;
|
||||
callback();
|
||||
}, this.closingAnimation.duration);
|
||||
}
|
||||
// // Execute callback after animation completes
|
||||
// setTimeout(() => {
|
||||
// this.animating = false;
|
||||
// callback();
|
||||
// }, this.closingAnimation.duration);
|
||||
// }
|
||||
|
||||
// Add method to create expanding wave circles
|
||||
addWaveCircles(map: mapboxgl.Map, lngLat: mapboxgl.LngLat, options: {
|
||||
color?: string,
|
||||
maxRadius?: number,
|
||||
duration?: number,
|
||||
count?: number,
|
||||
showCenter?: boolean
|
||||
} = {}): void {
|
||||
const {
|
||||
color = 'red',
|
||||
maxRadius = 80, // Reduce max radius for less "over" effect
|
||||
duration = 2000, // Faster animation
|
||||
count = 2, // Fewer circles
|
||||
showCenter = true
|
||||
} = options;
|
||||
// // Add method to create expanding wave circles
|
||||
// addWaveCircles(map: mapboxgl.Map, lngLat: mapboxgl.LngLat, options: {
|
||||
// color?: string,
|
||||
// maxRadius?: number,
|
||||
// duration?: number,
|
||||
// count?: number,
|
||||
// showCenter?: boolean
|
||||
// } = {}): void {
|
||||
// const {
|
||||
// color = 'red',
|
||||
// maxRadius = 80, // Reduce max radius for less "over" effect
|
||||
// duration = 2000, // Faster animation
|
||||
// count = 2, // Fewer circles
|
||||
// showCenter = true
|
||||
// } = options;
|
||||
|
||||
const sourceId = `wave-circles-${Math.random().toString(36).substring(2, 9)}`;
|
||||
// const sourceId = `wave-circles-${Math.random().toString(36).substring(2, 9)}`;
|
||||
|
||||
if (!map.getSource(sourceId)) {
|
||||
map.addSource(sourceId, {
|
||||
type: 'geojson',
|
||||
data: {
|
||||
type: 'FeatureCollection',
|
||||
features: [{
|
||||
type: 'Feature',
|
||||
geometry: {
|
||||
type: 'Point',
|
||||
coordinates: [lngLat.lng, lngLat.lat]
|
||||
},
|
||||
properties: {
|
||||
radius: 0
|
||||
}
|
||||
}]
|
||||
}
|
||||
});
|
||||
// if (!map.getSource(sourceId)) {
|
||||
// map.addSource(sourceId, {
|
||||
// type: 'geojson',
|
||||
// data: {
|
||||
// type: 'FeatureCollection',
|
||||
// features: [{
|
||||
// type: 'Feature',
|
||||
// geometry: {
|
||||
// type: 'Point',
|
||||
// coordinates: [lngLat.lng, lngLat.lat]
|
||||
// },
|
||||
// properties: {
|
||||
// radius: 0
|
||||
// }
|
||||
// }]
|
||||
// }
|
||||
// });
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const layerId = `${sourceId}-layer-${i}`;
|
||||
const delay = i * (duration / count);
|
||||
// for (let i = 0; i < count; i++) {
|
||||
// const layerId = `${sourceId}-layer-${i}`;
|
||||
// const delay = i * (duration / count);
|
||||
|
||||
map.addLayer({
|
||||
id: layerId,
|
||||
type: 'circle',
|
||||
source: sourceId,
|
||||
paint: {
|
||||
'circle-radius': ['interpolate', ['linear'], ['get', 'radius'], 0, 0, 100, maxRadius],
|
||||
'circle-color': 'transparent',
|
||||
'circle-opacity': ['interpolate', ['linear'], ['get', 'radius'],
|
||||
0, showCenter ? 0.15 : 0, // Lower opacity
|
||||
100, 0
|
||||
],
|
||||
'circle-stroke-width': 1.5, // Thinner stroke
|
||||
'circle-stroke-color': color
|
||||
}
|
||||
});
|
||||
// map.addLayer({
|
||||
// id: layerId,
|
||||
// type: 'circle',
|
||||
// source: sourceId,
|
||||
// paint: {
|
||||
// 'circle-radius': ['interpolate', ['linear'], ['get', 'radius'], 0, 0, 100, maxRadius],
|
||||
// 'circle-color': 'transparent',
|
||||
// 'circle-opacity': ['interpolate', ['linear'], ['get', 'radius'],
|
||||
// 0, showCenter ? 0.15 : 0, // Lower opacity
|
||||
// 100, 0
|
||||
// ],
|
||||
// 'circle-stroke-width': 1.5, // Thinner stroke
|
||||
// 'circle-stroke-color': color
|
||||
// }
|
||||
// });
|
||||
|
||||
this.animateWaveCircle(map, sourceId, layerId, duration, delay);
|
||||
}
|
||||
}
|
||||
}
|
||||
// this.animateWaveCircle(map, sourceId, layerId, duration, delay);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
private animateWaveCircle(
|
||||
map: mapboxgl.Map,
|
||||
sourceId: string,
|
||||
layerId: string,
|
||||
duration: number,
|
||||
delay: number
|
||||
): void {
|
||||
let start: number | null = null;
|
||||
let animationId: number;
|
||||
// private animateWaveCircle(
|
||||
// map: mapboxgl.Map,
|
||||
// sourceId: string,
|
||||
// layerId: string,
|
||||
// duration: number,
|
||||
// delay: number
|
||||
// ): void {
|
||||
// let start: number | null = null;
|
||||
// let animationId: number;
|
||||
|
||||
const animate = (timestamp: number) => {
|
||||
if (!start) {
|
||||
start = timestamp + delay;
|
||||
}
|
||||
// const animate = (timestamp: number) => {
|
||||
// if (!start) {
|
||||
// start = timestamp + delay;
|
||||
// }
|
||||
|
||||
const progress = Math.max(0, timestamp - start);
|
||||
const progressPercent = Math.min(progress / duration, 1);
|
||||
// const progress = Math.max(0, timestamp - start);
|
||||
// const progressPercent = Math.min(progress / duration, 1);
|
||||
|
||||
if (map.getSource(sourceId)) {
|
||||
(map.getSource(sourceId) as mapboxgl.GeoJSONSource).setData({
|
||||
type: 'FeatureCollection',
|
||||
features: [{
|
||||
type: 'Feature',
|
||||
geometry: {
|
||||
type: 'Point',
|
||||
coordinates: (map.getSource(sourceId) as any)._data.features[0].geometry.coordinates
|
||||
},
|
||||
properties: {
|
||||
radius: progressPercent * 100
|
||||
}
|
||||
}]
|
||||
});
|
||||
}
|
||||
// if (map.getSource(sourceId)) {
|
||||
// (map.getSource(sourceId) as mapboxgl.GeoJSONSource).setData({
|
||||
// type: 'FeatureCollection',
|
||||
// features: [{
|
||||
// type: 'Feature',
|
||||
// geometry: {
|
||||
// type: 'Point',
|
||||
// coordinates: (map.getSource(sourceId) as any)._data.features[0].geometry.coordinates
|
||||
// },
|
||||
// properties: {
|
||||
// radius: progressPercent * 100
|
||||
// }
|
||||
// }]
|
||||
// });
|
||||
// }
|
||||
|
||||
if (progressPercent < 1) {
|
||||
animationId = requestAnimationFrame(animate);
|
||||
} else if (map.getLayer(layerId)) {
|
||||
// Restart the animation for continuous effect
|
||||
start = null;
|
||||
animationId = requestAnimationFrame(animate);
|
||||
}
|
||||
};
|
||||
// if (progressPercent < 1) {
|
||||
// animationId = requestAnimationFrame(animate);
|
||||
// } else if (map.getLayer(layerId)) {
|
||||
// // Restart the animation for continuous effect
|
||||
// start = null;
|
||||
// animationId = requestAnimationFrame(animate);
|
||||
// }
|
||||
// };
|
||||
|
||||
// Start the animation after delay
|
||||
setTimeout(() => {
|
||||
animationId = requestAnimationFrame(animate);
|
||||
}, delay);
|
||||
// // Start the animation after delay
|
||||
// setTimeout(() => {
|
||||
// animationId = requestAnimationFrame(animate);
|
||||
// }, delay);
|
||||
|
||||
// Clean up on popup close
|
||||
this.once('close', () => {
|
||||
cancelAnimationFrame(animationId);
|
||||
if (map.getLayer(layerId)) {
|
||||
map.removeLayer(layerId);
|
||||
}
|
||||
if (map.getSource(sourceId) && !map.getLayer(layerId)) {
|
||||
map.removeSource(sourceId);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
// // Clean up on popup close
|
||||
// this.once('close', () => {
|
||||
// cancelAnimationFrame(animationId);
|
||||
// if (map.getLayer(layerId)) {
|
||||
// map.removeLayer(layerId);
|
||||
// }
|
||||
// if (map.getSource(sourceId) && !map.getLayer(layerId)) {
|
||||
// map.removeSource(sourceId);
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
|
||||
// Add styles to document when in browser environment
|
||||
if (typeof document !== 'undefined') {
|
||||
// Add styles only if they don't exist yet
|
||||
if (!document.getElementById('custom-animated-popup-styles')) {
|
||||
const style = document.createElement('style');
|
||||
style.id = 'custom-animated-popup-styles';
|
||||
style.textContent = `
|
||||
.custom-animated-popup {
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
.custom-animated-popup .mapboxgl-popup-content {
|
||||
overflow: hidden;
|
||||
}
|
||||
// // Add styles to document when in browser environment
|
||||
// if (typeof document !== 'undefined') {
|
||||
// // Add styles only if they don't exist yet
|
||||
// if (!document.getElementById('custom-animated-popup-styles')) {
|
||||
// const style = document.createElement('style');
|
||||
// style.id = 'custom-animated-popup-styles';
|
||||
// style.textContent = `
|
||||
// .custom-animated-popup {
|
||||
// will-change: transform, opacity;
|
||||
// }
|
||||
// .custom-animated-popup .mapboxgl-popup-content {
|
||||
// overflow: hidden;
|
||||
// }
|
||||
|
||||
/* Marker styles with wave circles */
|
||||
.marker-gempa {
|
||||
position: relative;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
cursor: pointer;
|
||||
}
|
||||
// /* Marker styles with wave circles */
|
||||
// .marker-gempa {
|
||||
// position: relative;
|
||||
// width: 30px;
|
||||
// height: 30px;
|
||||
// cursor: pointer;
|
||||
// }
|
||||
|
||||
.circles {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
// .circles {
|
||||
// position: relative;
|
||||
// width: 100%;
|
||||
// height: 100%;
|
||||
// display: flex;
|
||||
// align-items: center;
|
||||
// justify-content: center;
|
||||
// }
|
||||
|
||||
.circle1, .circle2, .circle3 {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
border: 2px solid red;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
// .circle1, .circle2, .circle3 {
|
||||
// position: absolute;
|
||||
// border-radius: 50%;
|
||||
// border: 2px solid red;
|
||||
// width: 100%;
|
||||
// height: 100%;
|
||||
// opacity: 0;
|
||||
// animation: pulse 2s infinite;
|
||||
// }
|
||||
|
||||
.circle2 {
|
||||
animation-delay: 0.5s;
|
||||
}
|
||||
// .circle2 {
|
||||
// animation-delay: 0.5s;
|
||||
// }
|
||||
|
||||
.circle3 {
|
||||
animation-delay: 1s;
|
||||
}
|
||||
// .circle3 {
|
||||
// animation-delay: 1s;
|
||||
// }
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
transform: scale(0.5);
|
||||
opacity: 0;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.8;
|
||||
}
|
||||
100% {
|
||||
transform: scale(1.5);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
// @keyframes pulse {
|
||||
// 0% {
|
||||
// transform: scale(0.5);
|
||||
// opacity: 0;
|
||||
// }
|
||||
// 50% {
|
||||
// opacity: 0.8;
|
||||
// }
|
||||
// 100% {
|
||||
// transform: scale(1.5);
|
||||
// opacity: 0;
|
||||
// }
|
||||
// }
|
||||
|
||||
.blink {
|
||||
animation: blink 1s infinite;
|
||||
color: red;
|
||||
font-size: 20px;
|
||||
}
|
||||
// .blink {
|
||||
// animation: blink 1s infinite;
|
||||
// color: red;
|
||||
// font-size: 20px;
|
||||
// }
|
||||
|
||||
@keyframes blink {
|
||||
0% { opacity: 1; }
|
||||
50% { opacity: 0.3; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
}
|
||||
// @keyframes blink {
|
||||
// 0% { opacity: 1; }
|
||||
// 50% { opacity: 0.3; }
|
||||
// 100% { opacity: 1; }
|
||||
// }
|
||||
// `;
|
||||
// document.head.appendChild(style);
|
||||
// }
|
||||
// }
|
||||
|
|
Loading…
Reference in New Issue