504 lines
15 KiB
TypeScript
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();
|
|
}
|