780 lines
25 KiB
TypeScript
780 lines
25 KiB
TypeScript
import {
|
|
crime_incidents,
|
|
crime_rates,
|
|
crime_status,
|
|
crimes,
|
|
locations,
|
|
PrismaClient,
|
|
} from "@prisma/client";
|
|
import { generateId, generateIdWithDbCounter } from "../../app/_utils/common";
|
|
import { createClient } from "../../app/_utils/supabase/client";
|
|
import * as fs from "fs";
|
|
import * as path from "path";
|
|
import { CRegex } from "../../app/_utils/const/regex";
|
|
|
|
import { districtCenters } from "../data/jsons/district-center";
|
|
|
|
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;
|
|
};
|
|
|
|
// New type for crime data structure from JSON
|
|
interface CrimeData {
|
|
district_id: string;
|
|
district_name: string;
|
|
month: string;
|
|
year: number;
|
|
[key: string]: string | number; // For dynamic crime category fields ending with "_crimes"
|
|
crime_cleared: number;
|
|
number_of_crime: number;
|
|
}
|
|
|
|
export class CrimeIncidentsByTypeSeeder {
|
|
private crimeMonthlyData: Map<
|
|
string,
|
|
{
|
|
number_of_crime: number;
|
|
crime_cleared: number;
|
|
categories: { [category: string]: number };
|
|
}
|
|
> = new Map();
|
|
|
|
private monthNameMap: { [key: string]: number } = {
|
|
"JAN": 1,
|
|
"FEB": 2,
|
|
"MAR": 3,
|
|
"APR": 4,
|
|
"MAY": 5,
|
|
"JUN": 6,
|
|
"JUL": 7,
|
|
"AUG": 8,
|
|
"SEP": 9,
|
|
"OCT": 10,
|
|
"NOV": 11,
|
|
"DEC": 12,
|
|
};
|
|
|
|
constructor(
|
|
private prisma: PrismaClient,
|
|
private supabase = createClient(),
|
|
) {}
|
|
|
|
private async loadCrimeMonthlyData(): Promise<void> {
|
|
const jsonFilePath = path.resolve(
|
|
__dirname,
|
|
"../data/jsons/crimes/cbt.json",
|
|
);
|
|
|
|
return new Promise((resolve, reject) => {
|
|
fs.readFile(jsonFilePath, "utf8", (err, data) => {
|
|
if (err) {
|
|
console.error(
|
|
"Error reading crime monthly JSON data:",
|
|
err,
|
|
);
|
|
reject(err);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const crimeData: CrimeData[] = JSON.parse(data);
|
|
|
|
for (const row of crimeData) {
|
|
const monthNum = this.monthNameToNumber(row.month);
|
|
const key =
|
|
`${row.district_id}-${monthNum}-${row.year}`;
|
|
|
|
// Extract crime categories (fields ending with "_crimes")
|
|
const categories: { [category: string]: number } = {};
|
|
for (const [field, value] of Object.entries(row)) {
|
|
if (
|
|
field.endsWith("_crimes") &&
|
|
typeof value === "number"
|
|
) {
|
|
// Extract category name from field name (remove "_crimes" suffix)
|
|
const categoryName = field.slice(0, -7);
|
|
categories[categoryName] = value;
|
|
}
|
|
}
|
|
|
|
this.crimeMonthlyData.set(key, {
|
|
number_of_crime: row.number_of_crime as number,
|
|
crime_cleared: row.crime_cleared as number,
|
|
categories: categories,
|
|
});
|
|
}
|
|
|
|
resolve();
|
|
} catch (error) {
|
|
console.error(
|
|
"Error parsing crime monthly JSON data:",
|
|
error,
|
|
);
|
|
reject(error);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
private monthNameToNumber(monthName: string): number {
|
|
const month = this.monthNameMap[monthName.toUpperCase()];
|
|
if (!month) {
|
|
console.warn(
|
|
`Unknown month name: ${monthName}, defaulting to January`,
|
|
);
|
|
return 1;
|
|
}
|
|
return month;
|
|
}
|
|
|
|
/**
|
|
* Generates mock incidents for 2025 data
|
|
*/
|
|
private async generateMock2025Incidents(): Promise<void> {
|
|
const crimes2025 = await this.prisma.crimes.findMany({
|
|
where: {
|
|
year: 2025,
|
|
month: { not: null },
|
|
source_type: "cbt",
|
|
},
|
|
orderBy: [{ district_id: "asc" }, { month: "asc" }],
|
|
});
|
|
|
|
if (crimes2025.length === 0) {
|
|
return;
|
|
}
|
|
|
|
const crimeCategories = await this.prisma.crime_categories.findMany();
|
|
|
|
let totalIncidentsCreated = 0;
|
|
let totalMatched = 0;
|
|
let totalMismatched = 0;
|
|
let totalResolved = 0;
|
|
let districtsProcessed = new Set();
|
|
|
|
for (const crimeRecord of crimes2025) {
|
|
if (
|
|
!crimeRecord.number_of_crime || crimeRecord.number_of_crime <= 0
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
const incidents = await this.createMockIncidentsForCrime(
|
|
crimeRecord,
|
|
crimeCategories,
|
|
);
|
|
|
|
totalIncidentsCreated += incidents.length;
|
|
districtsProcessed.add(crimeRecord.district_id);
|
|
totalResolved += incidents.filter((inc) =>
|
|
inc.status === "resolved"
|
|
).length;
|
|
totalMatched += Math.min(3, Math.floor(Math.random() * 5));
|
|
}
|
|
|
|
console.log("\n📊 2025 Mock Data Summary:");
|
|
console.log(`├─ Total incidents created: ${totalIncidentsCreated}`);
|
|
console.log(`├─ Districts processed: ${districtsProcessed.size}`);
|
|
console.log(
|
|
`├─ Categories: ${totalMatched} matched, ${totalMismatched} mismatched`,
|
|
);
|
|
console.log(`└─ Total resolved cases: ${totalResolved}`);
|
|
}
|
|
|
|
private async createMockIncidentsForCrime(
|
|
crime: crimes,
|
|
allCategories: any[],
|
|
): Promise<any[]> {
|
|
const incidentsCreated = [];
|
|
|
|
const district = await this.prisma.districts.findUnique({
|
|
where: { id: crime.district_id },
|
|
include: { cities: true },
|
|
});
|
|
|
|
if (!district) {
|
|
return [];
|
|
}
|
|
|
|
const geo = await this.prisma.geographics.findFirst({
|
|
where: {
|
|
district_id: district.id,
|
|
year: crime.year ?? 2025,
|
|
},
|
|
orderBy: {
|
|
year: "desc",
|
|
},
|
|
select: {
|
|
latitude: true,
|
|
longitude: true,
|
|
land_area: true,
|
|
},
|
|
});
|
|
|
|
if (!geo) {
|
|
return [];
|
|
}
|
|
|
|
const numberOfCrimes = crime.number_of_crime || 10;
|
|
const crimesCleared = Math.floor(
|
|
numberOfCrimes * (0.3 + Math.random() * 0.4),
|
|
);
|
|
|
|
const locationPool = this.generateDistributedPoints(
|
|
geo.land_area!,
|
|
numberOfCrimes,
|
|
district.id,
|
|
district.name,
|
|
);
|
|
|
|
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",
|
|
];
|
|
|
|
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: "sigapcompany@gmail.com",
|
|
},
|
|
select: {
|
|
id: true,
|
|
},
|
|
});
|
|
|
|
if (!user) {
|
|
return [];
|
|
}
|
|
|
|
const event = await this.prisma.events.findFirst({
|
|
where: {
|
|
user_id: user.id,
|
|
},
|
|
});
|
|
|
|
if (!event) {
|
|
return [];
|
|
}
|
|
|
|
const incidentsToCreate: Partial<crime_incidents>[] = [];
|
|
const locationsToCreate: Partial<locations>[] = [];
|
|
|
|
const categoryDistribution: { categoryId: string; count: number }[] =
|
|
[];
|
|
|
|
let remainingCrimes = numberOfCrimes;
|
|
const shuffledCategories = [...allCategories].sort(() =>
|
|
Math.random() - 0.5
|
|
);
|
|
|
|
const categoriesToUse = Math.max(
|
|
1,
|
|
Math.min(3, Math.floor(Math.random() * 5)),
|
|
);
|
|
|
|
for (
|
|
let i = 0;
|
|
i < categoriesToUse && i < shuffledCategories.length &&
|
|
remainingCrimes > 0;
|
|
i++
|
|
) {
|
|
const category = shuffledCategories[i];
|
|
|
|
if (
|
|
i === categoriesToUse - 1 || i === shuffledCategories.length - 1
|
|
) {
|
|
categoryDistribution.push({
|
|
categoryId: category.id,
|
|
count: remainingCrimes,
|
|
});
|
|
remainingCrimes = 0;
|
|
} else {
|
|
const count = Math.max(
|
|
1,
|
|
Math.floor(remainingCrimes * (0.2 + Math.random() * 0.6)),
|
|
);
|
|
categoryDistribution.push({
|
|
categoryId: category.id,
|
|
count: count,
|
|
});
|
|
remainingCrimes -= count;
|
|
}
|
|
}
|
|
|
|
let resolvedCount = 0;
|
|
|
|
for (const category of categoryDistribution) {
|
|
for (let i = 0; i < category.count; i++) {
|
|
const year = 2025;
|
|
const month = (crime.month as number) - 1;
|
|
|
|
const maxDay = new Date(year, month + 1, 0).getDate();
|
|
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);
|
|
|
|
const randomLocationIndex = Math.floor(
|
|
Math.random() * locationPool.length,
|
|
);
|
|
const selectedLocation = locationPool[randomLocationIndex];
|
|
|
|
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)];
|
|
|
|
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 incident",
|
|
latitude: selectedLocation.latitude,
|
|
longitude: selectedLocation.longitude,
|
|
location:
|
|
`POINT(${selectedLocation.longitude} ${selectedLocation.latitude})`,
|
|
};
|
|
|
|
locationsToCreate.push(locationData);
|
|
|
|
const incidentId = await generateIdWithDbCounter(
|
|
"crime_incidents",
|
|
{
|
|
prefix: "CI",
|
|
segments: {
|
|
codes: [district.city_id],
|
|
sequentialDigits: 4,
|
|
year,
|
|
},
|
|
format: "{prefix}-{codes}-{sequence}-{year}",
|
|
separator: "-",
|
|
uniquenessStrategy: "counter",
|
|
},
|
|
CRegex.CR_YEAR_SEQUENCE,
|
|
);
|
|
|
|
const status = resolvedCount < crimesCleared
|
|
? ("resolved" as crime_status)
|
|
: ("unresolved" as crime_status);
|
|
|
|
if (status === "resolved") {
|
|
resolvedCount++;
|
|
}
|
|
|
|
const categoryDetails = allCategories.find((c) =>
|
|
c.id === category.categoryId
|
|
);
|
|
|
|
const categoryName = categoryDetails.name;
|
|
|
|
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 ${categoryName.toLowerCase()} ${randomAddress}`,
|
|
`Laporan ${categoryName.toLowerCase()} terjadi pada ${timestamp} ${randomLocation}`,
|
|
`${categoryName} dilaporkan ${randomLocation}`,
|
|
`Insiden ${categoryName.toLowerCase()} terjadi ${randomLocation}`,
|
|
`Kejadian ${categoryName.toLowerCase()} ${randomLocation}`,
|
|
`${categoryName} terdeteksi ${randomLocation} pada ${timestamp.toLocaleTimeString()}`,
|
|
`Pelaporan ${categoryName.toLowerCase()} di ${randomAddress}`,
|
|
`Kasus ${categoryName.toLowerCase()} terjadi di ${streetName}`,
|
|
`${categoryName} terjadi di dekat ${placeType.toLowerCase()} ${district.name}`,
|
|
`Insiden ${categoryName.toLowerCase()} dilaporkan warga setempat ${randomLocation}`,
|
|
];
|
|
|
|
const randomDescription = descriptions[
|
|
Math.floor(Math.random() * descriptions.length)
|
|
];
|
|
|
|
incidentsToCreate.push({
|
|
id: incidentId,
|
|
crime_id: crime.id,
|
|
crime_category_id: category.categoryId,
|
|
location_id: undefined as string | undefined,
|
|
description: randomDescription,
|
|
victim_count: 0,
|
|
status: status,
|
|
timestamp: timestamp,
|
|
});
|
|
}
|
|
}
|
|
|
|
try {
|
|
await this.chunkedInsertLocations(locationsToCreate);
|
|
} catch (error) {
|
|
return [];
|
|
}
|
|
|
|
const createdLocations = await this.prisma.locations.findMany({
|
|
where: {
|
|
event_id: event.id,
|
|
district_id: district.id,
|
|
address: {
|
|
in: locationsToCreate
|
|
.map((loc) => loc.address)
|
|
.filter((address): address is string =>
|
|
address !== undefined
|
|
),
|
|
},
|
|
},
|
|
select: {
|
|
id: true,
|
|
address: true,
|
|
},
|
|
});
|
|
|
|
const addressToId = new Map<string, string>();
|
|
for (const loc of createdLocations) {
|
|
if (loc.address !== null) {
|
|
addressToId.set(loc.address, loc.id);
|
|
}
|
|
}
|
|
|
|
for (let i = 0; i < incidentsToCreate.length; i++) {
|
|
const address = locationsToCreate[i].address;
|
|
if (typeof address === "string") {
|
|
incidentsToCreate[i].location_id = addressToId.get(address);
|
|
}
|
|
}
|
|
|
|
await this.chunkedInsertIncidents(incidentsToCreate);
|
|
|
|
incidentsCreated.push(...incidentsToCreate);
|
|
|
|
return incidentsCreated;
|
|
}
|
|
|
|
private async chunkedInsertIncidents(data: any[], chunkSize: number = 200) {
|
|
for (let i = 0; i < data.length; i += chunkSize) {
|
|
const chunk = data.slice(i, i + chunkSize);
|
|
await this.prisma.crime_incidents.createMany({
|
|
data: chunk,
|
|
skipDuplicates: true,
|
|
});
|
|
}
|
|
}
|
|
|
|
private async chunkedInsertLocations(
|
|
locations: any[],
|
|
chunkSize: number = 200,
|
|
) {
|
|
for (let i = 0; i < locations.length; i += chunkSize) {
|
|
const chunk = locations.slice(i, i + chunkSize);
|
|
let { error } = await this.supabase
|
|
.from("locations")
|
|
.insert(chunk)
|
|
.select();
|
|
if (error) {
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
|
|
async run(): Promise<void> {
|
|
console.log("🌱 Seeding crime incidents data...");
|
|
|
|
try {
|
|
await this.loadCrimeMonthlyData();
|
|
|
|
const existingIncidents = await this.prisma.crime_incidents
|
|
.findFirst({
|
|
where: {
|
|
crimes: {
|
|
source_type: "cbt",
|
|
},
|
|
},
|
|
});
|
|
|
|
if (existingIncidents) {
|
|
const existing2025Incidents = await this.prisma.crime_incidents
|
|
.findFirst({
|
|
where: {
|
|
timestamp: {
|
|
gte: new Date("2025-01-01"),
|
|
lt: new Date("2026-01-01"),
|
|
},
|
|
},
|
|
});
|
|
|
|
if (!existing2025Incidents) {
|
|
await this.generateMock2025Incidents();
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
const crimeRecords = await this.prisma.crimes.findMany({
|
|
where: {
|
|
month: { not: null },
|
|
number_of_crime: { gt: 0 },
|
|
source_type: "cbt",
|
|
year: { lt: 2025 },
|
|
},
|
|
orderBy: [{ district_id: "asc" }, { year: "asc" }, {
|
|
month: "asc",
|
|
}],
|
|
});
|
|
|
|
const crimeCategories = await this.prisma.crime_categories
|
|
.findMany();
|
|
if (crimeCategories.length === 0) {
|
|
console.error(
|
|
"No crime categories found, please seed crime categories first",
|
|
);
|
|
return;
|
|
}
|
|
|
|
let totalIncidentsCreated = 0;
|
|
let skippedMonths = 0;
|
|
let processingStats = {
|
|
districts: new Set(),
|
|
years: new Set(),
|
|
totalMatched: 0,
|
|
totalMismatched: 0,
|
|
totalResolved: 0,
|
|
totalUnresolved: 0,
|
|
};
|
|
|
|
let yearlyStats = new Map<number, {
|
|
incidents: number;
|
|
resolved: number;
|
|
matched: number;
|
|
mismatched: number;
|
|
districts: Set<string>;
|
|
}>();
|
|
|
|
for (const crimeRecord of crimeRecords) {
|
|
if (crimeRecord.number_of_crime === 0) {
|
|
skippedMonths++;
|
|
continue;
|
|
}
|
|
|
|
const key =
|
|
`${crimeRecord.district_id}-${crimeRecord.month}-${crimeRecord.year}`;
|
|
const crimeMonthlyInfo = this.crimeMonthlyData.get(key);
|
|
|
|
if (
|
|
!crimeMonthlyInfo || crimeMonthlyInfo.number_of_crime <= 0
|
|
) {
|
|
skippedMonths++;
|
|
continue;
|
|
}
|
|
|
|
processingStats.districts.add(crimeRecord.district_id);
|
|
processingStats.years.add(crimeRecord.year);
|
|
|
|
const incidents = await this.createMockIncidentsForCrime(
|
|
crimeRecord,
|
|
crimeCategories,
|
|
);
|
|
|
|
const year = crimeRecord.year || 0;
|
|
if (!yearlyStats.has(year)) {
|
|
yearlyStats.set(year, {
|
|
incidents: 0,
|
|
resolved: 0,
|
|
matched: 0,
|
|
mismatched: 0,
|
|
districts: new Set(),
|
|
});
|
|
}
|
|
|
|
const yearStats = yearlyStats.get(year)!;
|
|
yearStats.incidents += incidents.length;
|
|
yearStats.districts.add(crimeRecord.district_id);
|
|
|
|
const resolvedCount = incidents.filter((inc) =>
|
|
inc.status === "resolved"
|
|
).length;
|
|
yearStats.resolved += resolvedCount;
|
|
|
|
if (crimeMonthlyInfo) {
|
|
const categoryCount =
|
|
Object.keys(crimeMonthlyInfo.categories).length;
|
|
yearStats.matched += categoryCount - 1;
|
|
yearStats.mismatched += 1;
|
|
}
|
|
|
|
totalIncidentsCreated += incidents.length;
|
|
}
|
|
|
|
for (const [year, stats] of Array.from(yearlyStats.entries())) {
|
|
console.log(`\n📊 ${year} Data Summary:`);
|
|
console.log(`├─ Total incidents created: ${stats.incidents}`);
|
|
console.log(`├─ Districts processed: ${stats.districts.size}`);
|
|
console.log(
|
|
`├─ Categories: ${stats.matched} matched, ${stats.mismatched} mismatched`,
|
|
);
|
|
console.log(`└─ Total resolved cases: ${stats.resolved}`);
|
|
}
|
|
|
|
await this.generateMock2025Incidents();
|
|
|
|
console.log("✅ Seeding selesai!");
|
|
} catch (error) {
|
|
console.error("❌ Error seeding crime incidents:", error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
private generateDistributedPoints(
|
|
landArea: number,
|
|
numPoints: number,
|
|
districtId: string,
|
|
districtName: string,
|
|
): Array<{ latitude: number; longitude: number; radius: number }> {
|
|
const points = [];
|
|
const districtNameLower = districtName.toLowerCase();
|
|
|
|
const districtCenter = districtCenters.find(
|
|
(center) => center.kecamatan.toLowerCase() === districtNameLower,
|
|
);
|
|
|
|
if (!districtCenter) {
|
|
return [];
|
|
}
|
|
|
|
const centerLat = districtCenter.lat;
|
|
const centerLng = districtCenter.lng;
|
|
|
|
let scalingFactor = 0.3;
|
|
|
|
const effectiveLandArea = Math.max(landArea || 1, 1);
|
|
|
|
const radiusKm = Math.sqrt(effectiveLandArea) * scalingFactor;
|
|
const radiusDeg = radiusKm / 111;
|
|
|
|
for (let i = 0; i < numPoints; i++) {
|
|
const angle = Math.random() * 2 * Math.PI;
|
|
const distanceFactor = Math.pow(Math.random(), 1.5);
|
|
const distance = distanceFactor * radiusDeg;
|
|
|
|
const latitude = centerLat + distance * Math.cos(angle);
|
|
const longitude = centerLng +
|
|
distance * Math.sin(angle) /
|
|
Math.cos(centerLat * Math.PI / 180);
|
|
|
|
const pointRadius = distance * 111000;
|
|
|
|
points.push({
|
|
latitude,
|
|
longitude,
|
|
radius: pointRadius,
|
|
});
|
|
}
|
|
|
|
return points;
|
|
}
|
|
}
|
|
|
|
if (require.main === module) {
|
|
const testSeeder = async () => {
|
|
const prisma = new PrismaClient();
|
|
const seeder = new CrimeIncidentsByTypeSeeder(prisma);
|
|
try {
|
|
await seeder.run();
|
|
} catch (e) {
|
|
console.error("Error during seeding:", e);
|
|
process.exit(1);
|
|
} finally {
|
|
await prisma.$disconnect();
|
|
}
|
|
};
|
|
|
|
testSeeder();
|
|
}
|