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

381 lines
11 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,
},
});
if (!geo) {
console.error(
`Geographic data for district ${district.id} in year ${crime.year} not found, skipping.`
);
return [];
}
// Generate random coordinates within district boundary (with small variation)
const radius = 0.02; // Approximately 2km
const angle = Math.random() * Math.PI * 2; // Random angle in radians
const distance = Math.sqrt(Math.random()) * radius; // Random distance within circle
// Calculate offset using simple approximation (not exact but good enough for this purpose)
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;
// List of common street names in Jember
const jemberStreets = [
'Jalan Pahlawan',
'Jalan Merdeka',
'Jalan Cendrawasih',
'Jalan Srikandi',
'Jalan Sumbermujur',
'Jalan Taman Siswa',
'Jalan Pantai',
'Jalan Raya Sumberbaru',
'Jalan Abdurrahman Saleh',
];
const user = await this.prisma.users.findFirst({
where: {
email: 'admin@sigap.id',
},
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 [];
}
// Generate a random address in Jember
const streetName =
jemberStreets[Math.floor(Math.random() * jemberStreets.length)];
const buildingNumber = Math.floor(Math.random() * 200) + 1;
const randomAddress = `${streetName} No. ${buildingNumber}, ${district.name}, Jember`;
const locationData: Partial<ICreateLocations> = {
district_id: district.id,
event_id: event.id,
address: randomAddress,
type: 'crime',
latitude: latitude,
longitude: longitude,
location: `POINT(${longitude} ${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,
},
select: {
id: true,
address: true,
},
});
if (!location) {
console.error(
`Location not found for district ${district.name} (${crime.year}), 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);
// 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';
const randomLocation = [
`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}`,
];
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}`,
];
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();
}