MIF_E31221222/sigap-website/prisma/seeds/crime-incidents.ts

504 lines
15 KiB
TypeScript

import {
PrismaClient,
crime_rates,
crime_status,
crimes,
} from '@prisma/client';
import { generateId } from '../../app/_utils/common';
import { createClient } from '../../app/_utils/supabase/client';
import * as fs from 'fs';
import * as path from 'path';
import csv from 'csv-parser';
type ICreateLocations = {
id: string;
created_at: Date;
updated_at: Date;
district_id: string;
event_id: string;
address: string;
type: string;
latitude: number;
longitude: number;
land_area: number;
location: string;
};
export class CrimeIncidentsSeeder {
private crimeMonthlyData: Map<
string,
{ number_of_crime: number; crime_cleared: number }
> = new Map();
constructor(
private prisma: PrismaClient,
private supabase = createClient()
) {}
private async loadCrimeMonthlyData(): Promise<void> {
const csvFilePath = path.resolve(
__dirname,
'../data/excels/crimes/crime_monthly.csv'
);
return new Promise((resolve, reject) => {
fs.createReadStream(csvFilePath)
.pipe(csv())
.on('data', (row) => {
const key = `${row.district_id}-${row.month_num}-${row.year}`;
this.crimeMonthlyData.set(key, {
number_of_crime: parseInt(row.number_of_crime),
crime_cleared: parseInt(row.crime_cleared),
});
})
.on('end', () => {
console.log(
`Loaded ${this.crimeMonthlyData.size} crime monthly records.`
);
resolve();
})
.on('error', (error) => {
console.error('Error loading crime monthly data:', error);
reject(error);
});
});
}
async run(): Promise<void> {
console.log('🌱 Seeding crime incidents data...');
try {
// Load crime monthly data first
await this.loadCrimeMonthlyData();
// Check if crime incidents already exist
const existingIncidents = await this.prisma.crime_incidents.findFirst();
if (existingIncidents) {
console.log('Crime incidents data already exists, skipping import.');
return;
}
// Get all monthly crime records (exclude yearly summaries)
const crimeRecords = await this.prisma.crimes.findMany({
where: {
month: { not: null },
// Only process records with incidents (number_of_crime > 0)
number_of_crime: { gt: 0 },
},
orderBy: [{ district_id: 'asc' }, { year: 'asc' }, { month: 'asc' }],
});
console.log(
`Found ${crimeRecords.length} monthly crime records with incidents to process.`
);
// Get all crime categories for random assignment
const crimeCategories = await this.prisma.crime_categories.findMany();
if (crimeCategories.length === 0) {
console.error(
'No crime categories found, please seed crime categories first.'
);
return;
}
// Process each crime record
let totalIncidentsCreated = 0;
for (const crimeRecord of crimeRecords) {
const incidents = await this.createIncidentsForCrime(
crimeRecord,
crimeCategories
);
totalIncidentsCreated += incidents.length;
// Log progress every 50 crime records
if (totalIncidentsCreated % 50 === 0) {
console.log(`Created ${totalIncidentsCreated} incidents so far...`);
}
}
console.log(
`✅ Successfully created ${totalIncidentsCreated} crime incidents.`
);
} catch (error) {
console.error('❌ Error seeding crime incidents:', error);
throw error;
}
}
private async createIncidentsForCrime(
crime: crimes,
categories: any[]
): Promise<any[]> {
const incidentsCreated = [];
// Get district information
const district = await this.prisma.districts.findUnique({
where: { id: crime.district_id },
include: { cities: true },
});
if (!district) {
console.error(`District ${crime.district_id} not found, skipping.`);
return [];
}
const geo = await this.prisma.geographics.findFirst({
where: {
district_id: district.id,
year: crime.year,
},
select: {
latitude: true,
longitude: true,
land_area: true,
},
});
if (!geo) {
console.error(
`Geographic data for district ${district.id} in year ${crime.year} not found, skipping.`
);
return [];
}
// Generate multiple coordinates for this district to add more variety
// We'll create a pool of 5-10 potential locations and select from them randomly for each incident
const locationPool = [];
const numLocations = Math.floor(Math.random() * 6) + 5; // 5-10 locations
// Scale radius based on district land area if available
// This creates more realistic distribution based on district size
let baseRadius = 0.02; // Default ~2km
if (geo.land_area) {
// Adjust radius based on land area - larger districts get larger radius
// Square root of area provides a reasonable scale factor
const areaFactor = Math.sqrt(geo.land_area) / 100;
baseRadius = Math.max(0.01, Math.min(0.05, areaFactor * 0.03));
}
for (let i = 0; i < numLocations; i++) {
// Create more varied locations by using different radiuses
const radiusVariation = Math.random() * 0.5 + 0.5; // 50% to 150% of base radius
const radius = baseRadius * radiusVariation;
// Different angle for each location
const angle = Math.random() * Math.PI * 2;
// Use different distance distribution patterns
// Some close to center, some at middle distance, some near the edge
let distance;
const patternType = Math.floor(Math.random() * 3);
switch (patternType) {
case 0: // Close to center
distance = Math.random() * 0.4 * radius;
break;
case 1: // Middle range
distance = (0.4 + Math.random() * 0.3) * radius;
break;
case 2: // Edge of district
distance = (0.7 + Math.random() * 0.3) * radius;
break;
}
if (!distance || !angle) {
console.error(
`Invalid distance or angle for location generation, skipping.`
);
continue;
}
// Calculate offset with improved approximation
const latOffset = distance * Math.cos(angle);
const lngOffset = distance * Math.sin(angle);
// Apply offset to base coordinates
const latitude = geo.latitude + latOffset;
const longitude = geo.longitude + lngOffset;
locationPool.push({
latitude,
longitude,
radius: distance * 1000, // Convert to meters for reference
});
}
// List of common street names in Jember with more variety
const jemberStreets = [
'Jalan Pahlawan',
'Jalan Merdeka',
'Jalan Cendrawasih',
'Jalan Srikandi',
'Jalan Sumbermujur',
'Jalan Taman Siswa',
'Jalan Pantai',
'Jalan Raya Sumberbaru',
'Jalan Abdurrahman Saleh',
'Jalan Mastrip',
'Jalan PB Sudirman',
'Jalan Kalimantan',
'Jalan Sumatra',
'Jalan Jawa',
'Jalan Gajah Mada',
'Jalan Letjen Suprapto',
'Jalan Hayam Wuruk',
'Jalan Trunojoyo',
'Jalan Imam Bonjol',
'Jalan Diponegoro',
'Jalan Ahmad Yani',
'Jalan Kartini',
'Jalan Gatot Subroto',
];
// More varied place types
const placeTypes = [
'Perumahan',
'Apartemen',
'Komplek',
'Pasar',
'Toko',
'Terminal',
'Stasiun',
'Kampus',
'Sekolah',
'Perkantoran',
'Pertokoan',
'Pusat Perbelanjaan',
'Taman',
'Alun-alun',
'Simpang',
'Pertigaan',
'Perempatan',
];
const user = await this.prisma.users.findFirst({
where: {
email: 'admin@gmail.com',
},
select: {
id: true,
},
});
if (!user) {
console.error(`User not found, skipping.`);
return [];
}
const event = await this.prisma.events.findFirst({
where: {
user_id: user.id,
},
});
if (!event) {
console.error(`Event not found, skipping.`);
return [];
}
// Get crime_cleared data from the loaded CSV
const key = `${crime.district_id}-${crime.month}-${crime.year}`;
const crimeMonthlyInfo = this.crimeMonthlyData.get(key);
// Default values if not found in CSV
let crimesCleared = 0;
if (crimeMonthlyInfo) {
crimesCleared = crimeMonthlyInfo.crime_cleared;
// Safety check to ensure crime_cleared doesn't exceed number_of_crime
if (crimesCleared > crime.number_of_crime) {
crimesCleared = crime.number_of_crime;
}
} else {
console.warn(
`No crime monthly data found for ${key}, using default values.`
);
}
// Create incidents based on the number_of_crime value
for (let i = 0; i < crime.number_of_crime; i++) {
// Select random category
const randomCategory =
categories[Math.floor(Math.random() * categories.length)];
// Calculate a date within the crime's month
const year = crime.year;
if (!year) {
console.error(
`Year is not defined for crime record ${crime.id}, skipping.`
);
return [];
}
const month = (crime.month as number) - 1; // JavaScript months are 0-indexed
const maxDay = new Date(year, month + 1, 0).getDate(); // Get last day of month
const day = Math.floor(Math.random() * maxDay) + 1;
const hour = Math.floor(Math.random() * 24);
const minute = Math.floor(Math.random() * 60);
const timestamp = new Date(year, month, day, hour, minute);
// Select a random location from our pool
const randomLocationIndex = Math.floor(
Math.random() * locationPool.length
);
const selectedLocation = locationPool[randomLocationIndex];
// Generate varied address details
const streetName =
jemberStreets[Math.floor(Math.random() * jemberStreets.length)];
const buildingNumber = Math.floor(Math.random() * 200) + 1;
const placeType =
placeTypes[Math.floor(Math.random() * placeTypes.length)];
// Create more varied addresses
let randomAddress;
const addressType = Math.floor(Math.random() * 3);
switch (addressType) {
case 0:
randomAddress = `${streetName} No. ${buildingNumber}, ${district.name}, Jember`;
break;
case 1:
randomAddress = `${placeType} ${district.name}, ${streetName}, Jember`;
break;
case 2:
randomAddress = `${streetName} Blok ${String.fromCharCode(65 + Math.floor(Math.random() * 26))}-${Math.floor(Math.random() * 20) + 1}, ${district.name}, Jember`;
break;
}
const locationData: Partial<ICreateLocations> = {
district_id: district.id,
event_id: event.id,
address: randomAddress,
type: 'crime',
latitude: selectedLocation.latitude,
longitude: selectedLocation.longitude,
location: `POINT(${selectedLocation.longitude} ${selectedLocation.latitude})`,
};
let { data: newLocation, error } = await this.supabase
.from('locations')
.insert([locationData])
.select();
if (error) {
console.error(
`Error inserting into locations for district ${district.name} (${crime.year}):`,
error
);
return [];
}
const location = await this.prisma.locations.findFirst({
where: {
event_id: event.id,
district_id: district.id,
address: randomAddress,
},
select: {
id: true,
address: true,
},
});
if (!location) {
console.error(
`Location not found for district ${district.name} (${crime.year}), skipping.`
);
return [];
}
// Generate a unique ID for the incident
const incidentId = generateId({
prefix: 'CI',
segments: {
codes: [district.cities.id],
sequentialDigits: 4,
year,
},
format: '{prefix}-{codes}-{sequence}-{year}',
separator: '-',
randomSequence: false,
uniquenessStrategy: 'counter',
});
// Determine status based on crime_cleared
// If i < crimesCleared, this incident is resolved, otherwise unresolved
const status = i < crimesCleared ? 'resolved' : 'unresolved';
// More detailed location descriptions
const locs = [
`di daerah ${district.name}`,
`di sekitar ${district.name}`,
`di wilayah ${district.name}`,
`di jalan utama ${district.name}`,
`di perumahan ${district.name}`,
`di pasar ${district.name}`,
`di perbatasan ${district.name}`,
`di kawasan ${placeType.toLowerCase()} ${district.name}`,
`di persimpangan jalan ${streetName}`,
`di dekat ${placeType.toLowerCase()} ${district.name}`,
`di belakang ${placeType.toLowerCase()} ${district.name}`,
`di area ${streetName}`,
`di sekitar ${streetName} ${district.name}`,
`tidak jauh dari pusat ${district.name}`,
`di pinggiran ${district.name}`,
];
const randomLocation = locs[Math.floor(Math.random() * locs.length)];
const descriptions = [
`Kasus ${randomCategory.name.toLowerCase()} ${location.address}`,
`Laporan ${randomCategory.name.toLowerCase()} terjadi pada ${timestamp} ${randomLocation}`,
`${randomCategory.name} dilaporkan ${randomLocation}`,
`Insiden ${randomCategory.name.toLowerCase()} terjadi ${randomLocation}`,
`Kejadian ${randomCategory.name.toLowerCase()} ${randomLocation}`,
`${randomCategory.name} terdeteksi ${randomLocation} pada ${timestamp.toLocaleTimeString()}`,
`Pelaporan ${randomCategory.name.toLowerCase()} di ${location.address}`,
`Kasus ${randomCategory.name.toLowerCase()} terjadi di ${streetName}`,
`${randomCategory.name} terjadi di dekat ${placeType.toLowerCase()} ${district.name}`,
`Insiden ${randomCategory.name.toLowerCase()} dilaporkan warga setempat ${randomLocation}`,
];
const randomDescription =
descriptions[Math.floor(Math.random() * descriptions.length)];
// Create the crime incident
const incident = await this.prisma.crime_incidents.create({
data: {
id: incidentId,
crime_id: crime.id,
crime_category_id: randomCategory.id,
location_id: location.id,
description: randomDescription,
victim_count: 0,
status: status,
timestamp: timestamp,
},
});
incidentsCreated.push(incident);
}
return incidentsCreated;
}
}
// This allows the file to be executed standalone for testing
if (require.main === module) {
const testSeeder = async () => {
const prisma = new PrismaClient();
const seeder = new CrimeIncidentsSeeder(prisma);
try {
await seeder.run();
} catch (e) {
console.error('Error during seeding:', e);
process.exit(1);
} finally {
await prisma.$disconnect();
}
};
testSeeder();
}