Refactor seeding scripts to utilize generateIdWithDbCounter for unique ID generation across crime categories, incidents, and units. Implement a new utility function for generating distributed points within district areas to enhance incident location variability. Comment out unnecessary type drops and creations in SQL migration files for clarity. Add migration scripts to drop and re-add the phone field in the units table, ensuring data integrity during schema updates.
This commit is contained in:
parent
0747897fc7
commit
e891df87d0
File diff suppressed because it is too large
Load Diff
|
@ -316,12 +316,12 @@ using ((bucket_id = 'avatars'::text));
|
|||
|
||||
|
||||
|
||||
drop type "gis"."geometry_dump";
|
||||
-- drop type "gis"."geometry_dump";
|
||||
|
||||
drop type "gis"."valid_detail";
|
||||
-- drop type "gis"."valid_detail";
|
||||
|
||||
create type "gis"."geometry_dump" as ("path" integer[], "geom" geometry);
|
||||
-- create type "gis"."geometry_dump" as ("path" integer[], "geom" geometry);
|
||||
|
||||
create type "gis"."valid_detail" as ("valid" boolean, "reason" character varying, "location" geometry);
|
||||
-- create type "gis"."valid_detail" as ("valid" boolean, "reason" character varying, "location" geometry);
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `phone` on the `units` table. All the data in the column will be lost.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "units" DROP COLUMN "phone";
|
|
@ -0,0 +1,2 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "units" ADD COLUMN "phone" TEXT;
|
|
@ -1,6 +1,6 @@
|
|||
// prisma/seeds/CrimeCategoriesSeeder.ts
|
||||
import { generateId } from "../../app/_utils/common";
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import { generateId, generateIdWithDbCounter } from '../../app/_utils/common';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { crimeCategoriesData } from '../data/jsons/crime-category';
|
||||
|
||||
import path from 'path';
|
||||
|
@ -36,16 +36,14 @@ export class CrimeCategoriesSeeder {
|
|||
const data = XLSX.utils.sheet_to_json(sheet) as ICrimeCategory[];
|
||||
|
||||
for (const category of crimeCategoriesData) {
|
||||
const newId = await generateId({
|
||||
const newId = await generateIdWithDbCounter('crime_categories', {
|
||||
prefix: 'CC',
|
||||
segments: {
|
||||
sequentialDigits: 4,
|
||||
},
|
||||
randomSequence: false,
|
||||
uniquenessStrategy: 'counter',
|
||||
format: '{prefix}-{sequence}',
|
||||
separator: '-',
|
||||
tableName: 'crime_categories',
|
||||
storage: 'database',
|
||||
uniquenessStrategy: 'counter',
|
||||
});
|
||||
|
||||
await this.prisma.crime_categories.create({
|
||||
|
|
|
@ -4,7 +4,7 @@ import {
|
|||
crime_status,
|
||||
crimes,
|
||||
} from '@prisma/client';
|
||||
import { generateId } from '../../app/_utils/common';
|
||||
import { generateId, generateIdWithDbCounter } from '../../app/_utils/common';
|
||||
import { createClient } from '../../app/_utils/supabase/client';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
@ -64,6 +64,94 @@ export class CrimeIncidentsSeeder {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates well-distributed points within a district's area
|
||||
* @param centerLat - The center latitude of the district
|
||||
* @param centerLng - The center longitude of the district
|
||||
* @param landArea - Land area in square km
|
||||
* @param numPoints - Number of points to generate
|
||||
* @returns Array of {latitude, longitude, radius} points
|
||||
*/
|
||||
private generateDistributedPoints(
|
||||
centerLat: number,
|
||||
centerLng: number,
|
||||
landArea: number,
|
||||
numPoints: number
|
||||
): Array<{ latitude: number; longitude: number; radius: number }> {
|
||||
const points = [];
|
||||
|
||||
// Calculate a reasonable radius based on land area
|
||||
// Using square root of area as an approximation of district "radius"
|
||||
const areaFactor = Math.sqrt(landArea) / 100;
|
||||
const baseRadius = Math.max(0.01, Math.min(0.05, areaFactor * 0.03));
|
||||
|
||||
// Create a grid-based distribution for better coverage
|
||||
const gridSize = Math.ceil(Math.sqrt(numPoints * 1.5)); // Slightly larger grid for variety
|
||||
|
||||
// Define district bounds approximately
|
||||
// 0.1 degrees is roughly 11km at equator
|
||||
const estimatedDistrictRadius = Math.sqrt(landArea / Math.PI) / 111;
|
||||
const bounds = {
|
||||
minLat: centerLat - estimatedDistrictRadius,
|
||||
maxLat: centerLat + estimatedDistrictRadius,
|
||||
minLng: centerLng - estimatedDistrictRadius,
|
||||
maxLng: centerLng + estimatedDistrictRadius,
|
||||
};
|
||||
|
||||
const latStep = (bounds.maxLat - bounds.minLat) / gridSize;
|
||||
const lngStep = (bounds.maxLng - bounds.minLng) / gridSize;
|
||||
|
||||
// Generate points in each grid cell with some randomness
|
||||
let totalPoints = 0;
|
||||
for (let i = 0; i < gridSize && totalPoints < numPoints; i++) {
|
||||
for (let j = 0; j < gridSize && totalPoints < numPoints; j++) {
|
||||
// Base position within the grid cell
|
||||
const cellLat =
|
||||
bounds.minLat + (i + 0.2 + Math.random() * 0.6) * latStep;
|
||||
const cellLng =
|
||||
bounds.minLng + (j + 0.2 + Math.random() * 0.6) * lngStep;
|
||||
|
||||
// Distance from center (for radius reference)
|
||||
const latDiff = cellLat - centerLat;
|
||||
const lngDiff = cellLng - centerLng;
|
||||
const distance = Math.sqrt(latDiff * latDiff + lngDiff * lngDiff);
|
||||
|
||||
// Add some randomness to avoid perfect grid pattern
|
||||
const jitter = baseRadius * 0.2;
|
||||
const latitude = cellLat + (Math.random() * 2 - 1) * jitter;
|
||||
const longitude = cellLng + (Math.random() * 2 - 1) * jitter;
|
||||
|
||||
points.push({
|
||||
latitude,
|
||||
longitude,
|
||||
radius: distance * 111000, // Convert to meters (approx)
|
||||
});
|
||||
|
||||
totalPoints++;
|
||||
}
|
||||
}
|
||||
|
||||
// Add some completely random points for diversity
|
||||
while (points.length < numPoints) {
|
||||
const latitude =
|
||||
centerLat + (Math.random() * 2 - 1) * estimatedDistrictRadius;
|
||||
const longitude =
|
||||
centerLng + (Math.random() * 2 - 1) * estimatedDistrictRadius;
|
||||
|
||||
const latDiff = latitude - centerLat;
|
||||
const lngDiff = longitude - centerLng;
|
||||
const distance = Math.sqrt(latDiff * latDiff + lngDiff * lngDiff);
|
||||
|
||||
points.push({
|
||||
latitude,
|
||||
longitude,
|
||||
radius: distance * 111000, // Convert to meters (approx)
|
||||
});
|
||||
}
|
||||
|
||||
return points;
|
||||
}
|
||||
|
||||
async run(): Promise<void> {
|
||||
console.log('🌱 Seeding crime incidents data...');
|
||||
|
||||
|
@ -161,66 +249,16 @@ export class CrimeIncidentsSeeder {
|
|||
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
|
||||
// Generate a variable number of incidents between 10 and 25 for more variability
|
||||
const numLocations = Math.floor(Math.random() * 16) + 10; // 10-25 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
|
||||
});
|
||||
}
|
||||
// Generate distributed locations using our new utility function
|
||||
const locationPool = this.generateDistributedPoints(
|
||||
geo.latitude,
|
||||
geo.longitude,
|
||||
geo.land_area || 100, // Default to 100 km² if not available
|
||||
numLocations
|
||||
);
|
||||
|
||||
// List of common street names in Jember with more variety
|
||||
const jemberStreets = [
|
||||
|
@ -409,20 +447,21 @@ export class CrimeIncidentsSeeder {
|
|||
}
|
||||
|
||||
// Generate a unique ID for the incident
|
||||
const incidentId = await generateId({
|
||||
prefix: 'CI',
|
||||
segments: {
|
||||
codes: [district.cities.id],
|
||||
sequentialDigits: 4,
|
||||
year,
|
||||
const incidentId = await generateIdWithDbCounter(
|
||||
'crime_incidents',
|
||||
{
|
||||
prefix: 'CI',
|
||||
segments: {
|
||||
codes: [district.city_id],
|
||||
sequentialDigits: 4,
|
||||
year,
|
||||
},
|
||||
format: '{prefix}-{codes}-{sequence}-{year}',
|
||||
separator: '-',
|
||||
uniquenessStrategy: 'counter',
|
||||
},
|
||||
format: '{prefix}-{codes}-{sequence}-{year}',
|
||||
separator: '-',
|
||||
randomSequence: false,
|
||||
uniquenessStrategy: 'counter',
|
||||
storage: 'database',
|
||||
tableName: 'crime_incidents',
|
||||
});
|
||||
/(\d{4})(?=-\d{4}$)/ // Pattern to extract the 4-digit counter
|
||||
);
|
||||
|
||||
// Determine status based on crime_cleared
|
||||
// If i < crimesCleared, this incident is resolved, otherwise unresolved
|
||||
|
|
|
@ -8,7 +8,7 @@ import {
|
|||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { parse } from 'csv-parse/sync';
|
||||
import { generateId } from '../../app/_utils/common';
|
||||
import { generateId, generateIdWithDbCounter } from '../../app/_utils/common';
|
||||
|
||||
export class CrimesSeeder {
|
||||
constructor(private prisma: PrismaClient) {}
|
||||
|
@ -177,20 +177,23 @@ export class CrimesSeeder {
|
|||
|
||||
const year = parseInt(record.year);
|
||||
// Create a unique ID for monthly crime data
|
||||
const crimeId = await generateId({
|
||||
prefix: 'CR',
|
||||
segments: {
|
||||
codes: [city.id],
|
||||
sequentialDigits: 4,
|
||||
year,
|
||||
const crimeId = await generateIdWithDbCounter(
|
||||
'crimes',
|
||||
{
|
||||
prefix: 'CR',
|
||||
segments: {
|
||||
codes: [city.id],
|
||||
sequentialDigits: 4,
|
||||
year,
|
||||
},
|
||||
format: '{prefix}-{codes}-{sequence}-{year}',
|
||||
separator: '-',
|
||||
uniquenessStrategy: 'counter',
|
||||
},
|
||||
format: '{prefix}-{codes}-{sequence}-{year}',
|
||||
separator: '-',
|
||||
randomSequence: false,
|
||||
uniquenessStrategy: 'counter',
|
||||
storage: 'database',
|
||||
tableName: 'crimes',
|
||||
});
|
||||
/(\d{4})(?=-\d{4}$)/ // Pattern to extract the 4-digit counter
|
||||
);
|
||||
|
||||
console.log('Creating crime ID:', crimeId);
|
||||
|
||||
await this.prisma.crimes.create({
|
||||
data: {
|
||||
|
@ -259,20 +262,36 @@ export class CrimesSeeder {
|
|||
}
|
||||
|
||||
// Create a unique ID for yearly crime data
|
||||
const crimeId = await generateId({
|
||||
prefix: 'CR',
|
||||
segments: {
|
||||
codes: [city.id],
|
||||
sequentialDigits: 4,
|
||||
year,
|
||||
// const crimeId = await generateId({
|
||||
// prefix: 'CR',
|
||||
// segments: {
|
||||
// codes: [city.id],
|
||||
// sequentialDigits: 4,
|
||||
// year,
|
||||
// },
|
||||
// format: '{prefix}-{codes}-{sequence}-{year}',
|
||||
// separator: '-',
|
||||
// randomSequence: false,
|
||||
// uniquenessStrategy: 'counter',
|
||||
// storage: 'database',
|
||||
// tableName: 'crimes',
|
||||
// });
|
||||
|
||||
const crimeId = await generateIdWithDbCounter(
|
||||
'crimes',
|
||||
{
|
||||
prefix: 'CR',
|
||||
segments: {
|
||||
codes: [city.id],
|
||||
sequentialDigits: 4,
|
||||
year,
|
||||
},
|
||||
format: '{prefix}-{codes}-{sequence}-{year}',
|
||||
separator: '-',
|
||||
uniquenessStrategy: 'counter',
|
||||
},
|
||||
format: '{prefix}-{codes}-{sequence}-{year}',
|
||||
separator: '-',
|
||||
randomSequence: false,
|
||||
uniquenessStrategy: 'counter',
|
||||
storage: 'database',
|
||||
tableName: 'crimes',
|
||||
});
|
||||
/(\d{4})(?=-\d{4}$)/ // Pattern to extract the 4-digit counter
|
||||
);
|
||||
|
||||
await this.prisma.crimes.create({
|
||||
data: {
|
||||
|
@ -337,19 +356,20 @@ export class CrimesSeeder {
|
|||
}
|
||||
|
||||
// Create a unique ID for all-year summary data
|
||||
const crimeId = await generateId({
|
||||
prefix: 'CR',
|
||||
segments: {
|
||||
codes: [city.id],
|
||||
sequentialDigits: 4,
|
||||
const crimeId = await generateIdWithDbCounter(
|
||||
'crimes',
|
||||
{
|
||||
prefix: 'CR',
|
||||
segments: {
|
||||
codes: [city.id],
|
||||
sequentialDigits: 4,
|
||||
},
|
||||
format: '{prefix}-{codes}-{sequence}',
|
||||
separator: '-',
|
||||
uniquenessStrategy: 'counter',
|
||||
},
|
||||
format: '{prefix}-{codes}-{sequence}',
|
||||
separator: '-',
|
||||
randomSequence: false,
|
||||
uniquenessStrategy: 'counter',
|
||||
storage: 'database',
|
||||
tableName: 'crimes',
|
||||
});
|
||||
/(\d{4})$/ // Pattern to extract the 4-digit counter at the end
|
||||
);
|
||||
|
||||
await this.prisma.crimes.create({
|
||||
data: {
|
||||
|
|
|
@ -5,7 +5,7 @@ import * as path from 'path';
|
|||
import axios from 'axios';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { createClient } from '../../app/_utils/supabase/client';
|
||||
import { generateId } from '../../app/_utils/common';
|
||||
import { generateId, generateIdWithDbCounter } from '../../app/_utils/common';
|
||||
|
||||
// Interface untuk data Excel row
|
||||
interface ExcelRow {
|
||||
|
@ -89,17 +89,14 @@ export class UnitSeeder {
|
|||
const address = location.address;
|
||||
const phone = location.telepon?.replace(/-/g, '');
|
||||
|
||||
const newId = await generateId({
|
||||
const newId = await generateIdWithDbCounter('units', {
|
||||
prefix: 'UT',
|
||||
format: '{prefix}-{sequence}',
|
||||
separator: '-',
|
||||
randomSequence: false,
|
||||
uniquenessStrategy: 'counter',
|
||||
segments: {
|
||||
sequentialDigits: 4,
|
||||
},
|
||||
storage: 'database',
|
||||
tableName: 'units',
|
||||
format: '{prefix}-{sequence}',
|
||||
separator: '-',
|
||||
uniquenessStrategy: 'counter',
|
||||
});
|
||||
|
||||
let locationData: CreateLocationDto = {
|
||||
|
@ -154,17 +151,14 @@ export class UnitSeeder {
|
|||
const address = location.address;
|
||||
const phone = location.telepon?.replace(/-/g, '');
|
||||
|
||||
const newId = await generateId({
|
||||
const newId = await generateIdWithDbCounter('units', {
|
||||
prefix: 'UT',
|
||||
format: '{prefix}-{sequence}',
|
||||
separator: '-',
|
||||
randomSequence: false,
|
||||
uniquenessStrategy: 'counter',
|
||||
segments: {
|
||||
sequentialDigits: 4,
|
||||
},
|
||||
storage: 'database',
|
||||
tableName: 'units',
|
||||
format: '{prefix}-{sequence}',
|
||||
separator: '-',
|
||||
uniquenessStrategy: 'counter',
|
||||
});
|
||||
|
||||
const locationData: CreateLocationDto = {
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
drop type "gis"."geometry_dump";
|
||||
-- drop type "gis"."geometry_dump";
|
||||
|
||||
drop type "gis"."valid_detail";
|
||||
-- drop type "gis"."valid_detail";
|
||||
|
||||
create type "gis"."geometry_dump" as ("path" integer[], "geom" geometry);
|
||||
-- create type "gis"."geometry_dump" as ("path" integer[], "geom" geometry);
|
||||
|
||||
create type "gis"."valid_detail" as ("valid" boolean, "reason" character varying, "location" geometry);
|
||||
-- create type "gis"."valid_detail" as ("valid" boolean, "reason" character varying, "location" geometry);
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue